[visual] Avoid showing list environment markup when the selection is within list items (#13461)

GitOrigin-RevId: cadab83774d52dc6c4867fdd7300a1217423b837
This commit is contained in:
Alf Eaton 2023-07-04 09:10:22 +01:00 committed by Copybot
parent afbd143f0f
commit c16d2d5840
5 changed files with 126 additions and 212 deletions

View file

@ -20,6 +20,7 @@ import {
} from '../../utils/tree-operations/ancestors' } from '../../utils/tree-operations/ancestors'
import { getEnvironmentName } from '../../utils/tree-operations/environments' import { getEnvironmentName } from '../../utils/tree-operations/environments'
import { ListEnvironment } from '../../lezer-latex/latex.terms.mjs' import { ListEnvironment } from '../../lezer-latex/latex.terms.mjs'
import { SyntaxNode } from '@lezer/common'
export const ancestorListType = (state: EditorState): string | null => { export const ancestorListType = (state: EditorState): string | null => {
const ancestorNode = ancestorWithType(state, ListEnvironment) const ancestorNode = ancestorWithType(state, ListEnvironment)
@ -303,6 +304,21 @@ const toggleListForRange = (
return { range } return { range }
} }
export const getListItems = (node: SyntaxNode): SyntaxNode[] => {
const items: SyntaxNode[] = []
node.cursor().iterate(nodeRef => {
if (nodeRef.type.is('Item')) {
items.push(nodeRef.node)
}
if (nodeRef.type.is('ListEnvironment') && nodeRef.node !== node) {
return false
}
})
return items
}
export const toggleListForRanges = export const toggleListForRanges =
(environment: string) => (view: EditorView) => { (environment: string) => (view: EditorView) => {
view.dispatch( view.dispatch(

View file

@ -48,6 +48,7 @@ import { EditableGraphicsWidget } from './visual-widgets/editable-graphics'
import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-graphics' import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-graphics'
import { CloseBrace, OpenBrace } from '../../lezer-latex/latex.terms.mjs' import { CloseBrace, OpenBrace } from '../../lezer-latex/latex.terms.mjs'
import { FootnoteWidget } from './visual-widgets/footnote' import { FootnoteWidget } from './visual-widgets/footnote'
import { getListItems } from '../toolbar/lists'
type Options = { type Options = {
fileTreeManager: { fileTreeManager: {
@ -225,92 +226,91 @@ export const atomicDecorations = (options: Options) => {
tree.iterate({ tree.iterate({
enter(nodeRef) { enter(nodeRef) {
if (nodeRef.type.is('$Environment')) { if (nodeRef.type.is('$Environment')) {
if (shouldDecorate(state, nodeRef)) { const envName = getUnstarredEnvironmentName(nodeRef.node, state)
const envName = getUnstarredEnvironmentName(nodeRef.node, state) const hideInEnvironmentTypes = ['figure', 'table']
const hideInEnvironmentTypes = ['figure', 'table'] if (envName && hideInEnvironmentTypes.includes(envName)) {
if (envName && hideInEnvironmentTypes.includes(envName)) { const beginNode = nodeRef.node.getChild('BeginEnv')
const beginNode = nodeRef.node.getChild('BeginEnv') const endNode = nodeRef.node.getChild('EndEnv')
const endNode = nodeRef.node.getChild('EndEnv') if (
if ( beginNode &&
beginNode && endNode &&
endNode && hasClosingBrace(beginNode) &&
hasClosingBrace(beginNode) && hasClosingBrace(endNode)
hasClosingBrace(endNode) ) {
) { const beginLine = state.doc.lineAt(beginNode.from)
const beginLine = state.doc.lineAt(beginNode.from) const endLine = state.doc.lineAt(endNode.from)
const endLine = state.doc.lineAt(endNode.from)
const begin = { const begin = {
from: beginLine.from, from: beginLine.from,
to: extendForwardsOverEmptyLines(state.doc, beginLine), to: extendForwardsOverEmptyLines(state.doc, beginLine),
} }
const end = { const end = {
from: extendBackwardsOverEmptyLines(state.doc, endLine), from: extendBackwardsOverEmptyLines(state.doc, endLine),
to: endLine.to, to: endLine.to,
} }
if (shouldDecorate(state, { from: begin.from, to: end.to })) {
decorations.push(
Decoration.replace({
widget: new EnvironmentLineWidget(envName, 'begin'),
block: true,
}).range(begin.from, begin.to),
Decoration.replace({
widget: new EnvironmentLineWidget(envName, 'end'),
block: true,
}).range(end.from, end.to)
)
const centeringNode = centeringNodeForEnvironment(nodeRef)
if (centeringNode) {
const line = state.doc.lineAt(centeringNode.from)
const from = extendBackwardsOverEmptyLines(state.doc, line)
const to = extendForwardsOverEmptyLines(state.doc, line)
if (shouldDecorate(state, { from: begin.from, to: end.to })) {
decorations.push( decorations.push(
Decoration.replace({ Decoration.replace({
widget: new EnvironmentLineWidget(envName, 'begin'),
block: true, block: true,
}).range(begin.from, begin.to), }).range(from, to)
Decoration.replace({
widget: new EnvironmentLineWidget(envName, 'end'),
block: true,
}).range(end.from, end.to)
) )
const centeringNode = centeringNodeForEnvironment(nodeRef)
if (centeringNode) {
const line = state.doc.lineAt(centeringNode.from)
const from = extendBackwardsOverEmptyLines(state.doc, line)
const to = extendForwardsOverEmptyLines(state.doc, line)
decorations.push(
Decoration.replace({
block: true,
}).range(from, to)
)
}
} }
} }
} else if (nodeRef.type.is('ListEnvironment')) { }
const beginNode = nodeRef.node.getChild('BeginEnv') } else if (nodeRef.type.is('ListEnvironment')) {
const endNode = nodeRef.node.getChild('EndEnv') const beginNode = nodeRef.node.getChild('BeginEnv')
const endNode = nodeRef.node.getChild('EndEnv')
if (
beginNode &&
endNode &&
hasClosingBrace(beginNode) &&
hasClosingBrace(endNode)
) {
const beginLine = state.doc.lineAt(beginNode.from)
const endLine = state.doc.lineAt(endNode.from)
const begin = {
from: beginLine.from,
to: extendForwardsOverEmptyLines(state.doc, beginLine),
}
const end = {
from: extendBackwardsOverEmptyLines(state.doc, endLine),
to: endLine.to,
}
if ( if (
beginNode && !selectionIntersects(state.selection, begin) &&
endNode && !selectionIntersects(state.selection, end) &&
hasClosingBrace(beginNode) && getListItems(nodeRef.node).length > 0 // not empty
hasClosingBrace(endNode)
) { ) {
const beginLine = state.doc.lineAt(beginNode.from) decorations.push(
const endLine = state.doc.lineAt(endNode.from) Decoration.replace({
block: true,
const begin = { }).range(begin.from, begin.to),
from: beginLine.from, Decoration.replace({
to: extendForwardsOverEmptyLines(state.doc, beginLine), block: true,
} }).range(end.from, end.to)
const end = { )
from: extendBackwardsOverEmptyLines(state.doc, endLine),
to: endLine.to,
}
if (
!selectionIntersects(state.selection, begin) &&
!selectionIntersects(state.selection, end)
) {
decorations.push(
Decoration.replace({
block: true,
}).range(begin.from, begin.to),
Decoration.replace({
block: true,
}).range(end.from, end.to)
)
}
} }
} }
} }

View file

@ -43,47 +43,27 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
// create a nested list // create a nested list
cy.get('.cm-line') cy.get('.cm-line')
.eq(2) .eq(1)
.type(isMac ? '{cmd}]' : '{ctrl}]') .type(isMac ? '{cmd}]' : '{ctrl}]')
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
'have.text',
[
'\\begin{itemize}',
' Test',
'\\begin{itemize}',
' Test',
'\\end{itemize}',
'\\end{itemize}',
].join('')
)
}) })
it('creates a nested list inside an indented list', function () { it('creates a nested list inside an indented list', function () {
const content = [ const content = [
'\\begin{itemize}', '\\begin{itemize}',
'\\item Test', ' \\item Test',
'\\item Test', ' \\item Test',
'\\end{itemize}', '\\end{itemize}',
].join('\n') ].join('\n')
mountEditor(content) mountEditor(content)
// create a nested list // create a nested list
cy.get('.cm-line') cy.get('.cm-line')
.eq(2) .eq(1)
.type(isMac ? '{cmd}]' : '{ctrl}]') .type(isMac ? '{cmd}]' : '{ctrl}]')
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
'have.text',
[
'\\begin{itemize}',
' Test',
'\\begin{itemize}',
' Test',
'\\end{itemize}',
'\\end{itemize}',
].join('')
)
}) })
it('creates a nested list on Tab at the start of an item', function () { it('creates a nested list on Tab at the start of an item', function () {
@ -96,24 +76,14 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
mountEditor(content) mountEditor(content)
// move to the start of the item and press Tab // move to the start of the item and press Tab
cy.get('.cm-line').eq(2).as('line') cy.get('.cm-line').eq(1).as('line')
cy.get('@line').click() cy.get('@line').click()
cy.get('@line').type('{leftArrow}'.repeat(4)) cy.get('@line').type('{leftArrow}'.repeat(4))
cy.get('@line').trigger('keydown', { cy.get('@line').trigger('keydown', {
key: 'Tab', key: 'Tab',
}) })
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
'have.text',
[
'\\begin{itemize}',
' Test',
'\\begin{itemize}',
' Test',
'\\end{itemize}',
'\\end{itemize}',
].join('')
)
}) })
it('does not creates a nested list on Tab when not at the start of an item', function () { it('does not creates a nested list on Tab when not at the start of an item', function () {
@ -126,22 +96,13 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
mountEditor(content) mountEditor(content)
// focus a line (at the end of a list item) and press Tab // focus a line (at the end of a list item) and press Tab
cy.get('.cm-line').eq(2).as('line') cy.get('.cm-line').eq(1).as('line')
cy.get('@line').click() cy.get('@line').click()
cy.get('@line').trigger('keydown', { cy.get('@line').trigger('keydown', {
key: 'Tab', key: 'Tab',
}) })
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', [' Test', ' Test '].join(''))
'have.text',
[
//
'\\begin{itemize}',
' Test',
' Test ',
'\\end{itemize}',
].join('')
)
}) })
it('removes a nested list on Shift-Tab', function () { it('removes a nested list on Shift-Tab', function () {
@ -154,41 +115,22 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
mountEditor(content) mountEditor(content)
// move to the start of the list item and press Tab // move to the start of the list item and press Tab
cy.get('.cm-line').eq(2).as('line') cy.get('.cm-line').eq(1).as('line')
cy.get('@line').click() cy.get('@line').click()
cy.get('@line').type('{leftArrow}'.repeat(4)) cy.get('@line').type('{leftArrow}'.repeat(4))
cy.get('@line').trigger('keydown', { cy.get('@line').trigger('keydown', {
key: 'Tab', key: 'Tab',
}) })
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
'have.text',
[
'\\begin{itemize}',
' Test',
'\\begin{itemize}',
' Test',
'\\end{itemize}',
'\\end{itemize}',
].join('')
)
// focus the indented line and press Shift-Tab // focus the indented line and press Shift-Tab
cy.get('.cm-line').eq(4).trigger('keydown', { cy.get('.cm-line').eq(1).trigger('keydown', {
key: 'Tab', key: 'Tab',
shiftKey: true, shiftKey: true,
}) })
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
'have.text',
[
//
'\\begin{itemize}',
' Test',
' Test',
'\\end{itemize}',
].join('')
)
}) })
it('does not remove a top-level nested list on Shift-Tab', function () { it('does not remove a top-level nested list on Shift-Tab', function () {
@ -201,21 +143,12 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
mountEditor(content) mountEditor(content)
// focus a list item and press Shift-Tab // focus a list item and press Shift-Tab
cy.get('.cm-line').eq(2).trigger('keydown', { cy.get('.cm-line').eq(1).trigger('keydown', {
key: 'Tab', key: 'Tab',
shiftKey: true, shiftKey: true,
}) })
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
'have.text',
[
//
'\\begin{itemize}',
' Test',
' Test',
'\\end{itemize}',
].join('')
)
}) })
it('handles up arrow at the start of a list item', function () { it('handles up arrow at the start of a list item', function () {
@ -227,7 +160,7 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
].join('\n') ].join('\n')
mountEditor(content) mountEditor(content)
cy.get('.cm-line').eq(2).as('line') cy.get('.cm-line').eq(1).as('line')
cy.get('@line').click() cy.get('@line').click()
cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item
cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item
@ -246,7 +179,7 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
].join('\n') ].join('\n')
mountEditor(content) mountEditor(content)
cy.get('.cm-line').eq(2).as('line') cy.get('.cm-line').eq(1).as('line')
cy.get('@line').click() cy.get('@line').click()
cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item cy.get('@line').type('{leftArrow}'.repeat(3)) // to the start of the item
cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item cy.get('@line').type('{upArrow}{Shift}{rightArrow}{rightArrow}{rightArrow}') // up and extend to the end of the item
@ -290,15 +223,12 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
].join('\n') ].join('\n')
mountEditor(content) mountEditor(content)
cy.get('.cm-line').eq(2).as('line') cy.get('.cm-line').eq(0).as('line')
cy.get('@line').click() cy.get('@line').click()
cy.get('@line').type('\\ite') cy.get('@line').type('\\ite')
cy.get('@line').type('{enter}') cy.get('@line').type('{enter}')
cy.get('@line').type('second') cy.get('@line').type('second')
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', [' first', ' second'].join(''))
'have.text',
['\\begin{itemize}', ' first', ' second', '\\end{itemize}'].join('')
)
}) })
}) })

View file

@ -130,18 +130,10 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
clickToolbarButton('Bullet List') clickToolbarButton('Bullet List')
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', ' test')
'have.text',
[
//
'\\begin{itemize}',
' test',
'\\end{itemize}',
].join('')
)
cy.get('.cm-line').eq(1).type('ing') cy.get('.cm-line').eq(0).type('ing')
cy.get('.cm-line').eq(1).should('have.text', ' testing') cy.get('.cm-line').eq(0).should('have.text', ' testing')
}) })
it('should insert a numbered list', function () { it('should insert a numbered list', function () {
@ -150,17 +142,9 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
clickToolbarButton('Numbered List') clickToolbarButton('Numbered List')
cy.get('.cm-content').should( cy.get('.cm-content').should('have.text', ' test')
'have.text',
[
//
'\\begin{enumerate}',
' test',
'\\end{enumerate}',
].join('')
)
cy.get('.cm-line').eq(1).type('ing') cy.get('.cm-line').eq(0).type('ing')
cy.get('.cm-line').eq(1).should('have.text', ' testing') cy.get('.cm-line').eq(0).should('have.text', ' testing')
}) })
}) })

View file

@ -68,25 +68,21 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
// select the first autocomplete item // select the first autocomplete item
cy.findByRole('option').eq(0).click() cy.findByRole('option').eq(0).click()
cy.get('@first-line').should('have.text', '\\begin{itemize}') cy.get('@first-line')
cy.get('@second-line')
.should('have.text', ' ') .should('have.text', ' ')
.find('.ol-cm-item') .find('.ol-cm-item')
.should('have.length', 1) .should('have.length', 1)
cy.get('@third-line').should('have.text', '\\end{itemize}')
cy.get('@second-line').type('test{Enter}test') cy.get('@first-line').type('test{Enter}test')
cy.get('@first-line').should('have.text', '\\begin{itemize}') cy.get('@first-line')
.should('have.text', ' test')
.find('.ol-cm-item')
.should('have.length', 1)
cy.get('@second-line') cy.get('@second-line')
.should('have.text', ' test') .should('have.text', ' test')
.find('.ol-cm-item') .find('.ol-cm-item')
.should('have.length', 1) .should('have.length', 1)
cy.get('@third-line')
.should('have.text', ' test')
.find('.ol-cm-item')
.should('have.length', 1)
cy.get('@fourth-line').should('have.text', '\\end{itemize}')
}) })
it('finishes a list on Enter in the last item if empty', function () { it('finishes a list on Enter in the last item if empty', function () {
@ -95,10 +91,9 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
// select the first autocomplete item // select the first autocomplete item
cy.findByRole('option').eq(0).click() cy.findByRole('option').eq(0).click()
cy.get('@second-line').type('test{Enter}{Enter}') cy.get('@first-line').type('test{Enter}{Enter}')
cy.get('.cm-line') cy.get('@first-line')
.eq(0)
.should('have.text', ' test') .should('have.text', ' test')
.find('.ol-cm-item') .find('.ol-cm-item')
.should('have.length', 1) .should('have.length', 1)
@ -113,18 +108,7 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
cy.findByRole('option').eq(0).click() cy.findByRole('option').eq(0).click()
cy.get('@second-line').type('test{Enter}test{Enter}{upArrow}{Enter}{Enter}') cy.get('@second-line').type('test{Enter}test{Enter}{upArrow}{Enter}{Enter}')
cy.get('.cm-content').should('have.text', ' testtest')
const lines = [
'\\begin{itemize}',
' test',
' ',
' ',
' test',
' ',
'\\end{itemize}',
]
cy.get('.cm-content').should('have.text', lines.join(''))
}) })
forEach(['textbf', 'textit', 'underline']).it( forEach(['textbf', 'textit', 'underline']).it(