[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'
import { getEnvironmentName } from '../../utils/tree-operations/environments'
import { ListEnvironment } from '../../lezer-latex/latex.terms.mjs'
import { SyntaxNode } from '@lezer/common'
export const ancestorListType = (state: EditorState): string | null => {
const ancestorNode = ancestorWithType(state, ListEnvironment)
@ -303,6 +304,21 @@ const toggleListForRange = (
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 =
(environment: string) => (view: EditorView) => {
view.dispatch(

View file

@ -48,6 +48,7 @@ import { EditableGraphicsWidget } from './visual-widgets/editable-graphics'
import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-graphics'
import { CloseBrace, OpenBrace } from '../../lezer-latex/latex.terms.mjs'
import { FootnoteWidget } from './visual-widgets/footnote'
import { getListItems } from '../toolbar/lists'
type Options = {
fileTreeManager: {
@ -225,92 +226,91 @@ export const atomicDecorations = (options: Options) => {
tree.iterate({
enter(nodeRef) {
if (nodeRef.type.is('$Environment')) {
if (shouldDecorate(state, nodeRef)) {
const envName = getUnstarredEnvironmentName(nodeRef.node, state)
const hideInEnvironmentTypes = ['figure', 'table']
if (envName && hideInEnvironmentTypes.includes(envName)) {
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 envName = getUnstarredEnvironmentName(nodeRef.node, state)
const hideInEnvironmentTypes = ['figure', 'table']
if (envName && hideInEnvironmentTypes.includes(envName)) {
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,
}
const begin = {
from: beginLine.from,
to: extendForwardsOverEmptyLines(state.doc, beginLine),
}
const end = {
from: extendBackwardsOverEmptyLines(state.doc, endLine),
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(
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)
}).range(from, 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')
const endNode = nodeRef.node.getChild('EndEnv')
}
} else if (nodeRef.type.is('ListEnvironment')) {
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 (
beginNode &&
endNode &&
hasClosingBrace(beginNode) &&
hasClosingBrace(endNode)
!selectionIntersects(state.selection, begin) &&
!selectionIntersects(state.selection, end) &&
getListItems(nodeRef.node).length > 0 // not empty
) {
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 (
!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)
)
}
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
cy.get('.cm-line')
.eq(2)
.eq(1)
.type(isMac ? '{cmd}]' : '{ctrl}]')
cy.get('.cm-content').should(
'have.text',
[
'\\begin{itemize}',
' Test',
'\\begin{itemize}',
' Test',
'\\end{itemize}',
'\\end{itemize}',
].join('')
)
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
})
it('creates a nested list inside an indented list', function () {
const content = [
'\\begin{itemize}',
'\\item Test',
'\\item Test',
' \\item Test',
' \\item Test',
'\\end{itemize}',
].join('\n')
mountEditor(content)
// create a nested list
cy.get('.cm-line')
.eq(2)
.eq(1)
.type(isMac ? '{cmd}]' : '{ctrl}]')
cy.get('.cm-content').should(
'have.text',
[
'\\begin{itemize}',
' Test',
'\\begin{itemize}',
' Test',
'\\end{itemize}',
'\\end{itemize}',
].join('')
)
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
})
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)
// 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').type('{leftArrow}'.repeat(4))
cy.get('@line').trigger('keydown', {
key: 'Tab',
})
cy.get('.cm-content').should(
'have.text',
[
'\\begin{itemize}',
' Test',
'\\begin{itemize}',
' Test',
'\\end{itemize}',
'\\end{itemize}',
].join('')
)
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
})
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)
// 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').trigger('keydown', {
key: 'Tab',
})
cy.get('.cm-content').should(
'have.text',
[
//
'\\begin{itemize}',
' Test',
' Test ',
'\\end{itemize}',
].join('')
)
cy.get('.cm-content').should('have.text', [' Test', ' Test '].join(''))
})
it('removes a nested list on Shift-Tab', function () {
@ -154,41 +115,22 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
mountEditor(content)
// 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').type('{leftArrow}'.repeat(4))
cy.get('@line').trigger('keydown', {
key: 'Tab',
})
cy.get('.cm-content').should(
'have.text',
[
'\\begin{itemize}',
' Test',
'\\begin{itemize}',
' Test',
'\\end{itemize}',
'\\end{itemize}',
].join('')
)
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
// 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',
shiftKey: true,
})
cy.get('.cm-content').should(
'have.text',
[
//
'\\begin{itemize}',
' Test',
' Test',
'\\end{itemize}',
].join('')
)
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
})
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)
// 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',
shiftKey: true,
})
cy.get('.cm-content').should(
'have.text',
[
//
'\\begin{itemize}',
' Test',
' Test',
'\\end{itemize}',
].join('')
)
cy.get('.cm-content').should('have.text', [' Test', ' Test'].join(''))
})
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')
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').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
@ -246,7 +179,7 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
].join('\n')
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').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
@ -290,15 +223,12 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
].join('\n')
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').type('\\ite')
cy.get('@line').type('{enter}')
cy.get('@line').type('second')
cy.get('.cm-content').should(
'have.text',
['\\begin{itemize}', ' first', ' second', '\\end{itemize}'].join('')
)
cy.get('.cm-content').should('have.text', [' first', ' second'].join(''))
})
})

View file

@ -130,18 +130,10 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
clickToolbarButton('Bullet List')
cy.get('.cm-content').should(
'have.text',
[
//
'\\begin{itemize}',
' test',
'\\end{itemize}',
].join('')
)
cy.get('.cm-content').should('have.text', ' test')
cy.get('.cm-line').eq(1).type('ing')
cy.get('.cm-line').eq(1).should('have.text', ' testing')
cy.get('.cm-line').eq(0).type('ing')
cy.get('.cm-line').eq(0).should('have.text', ' testing')
})
it('should insert a numbered list', function () {
@ -150,17 +142,9 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
clickToolbarButton('Numbered List')
cy.get('.cm-content').should(
'have.text',
[
//
'\\begin{enumerate}',
' test',
'\\end{enumerate}',
].join('')
)
cy.get('.cm-content').should('have.text', ' test')
cy.get('.cm-line').eq(1).type('ing')
cy.get('.cm-line').eq(1).should('have.text', ' testing')
cy.get('.cm-line').eq(0).type('ing')
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
cy.findByRole('option').eq(0).click()
cy.get('@first-line').should('have.text', '\\begin{itemize}')
cy.get('@second-line')
cy.get('@first-line')
.should('have.text', ' ')
.find('.ol-cm-item')
.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')
.should('have.text', ' test')
.find('.ol-cm-item')
.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 () {
@ -95,10 +91,9 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
// select the first autocomplete item
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')
.eq(0)
cy.get('@first-line')
.should('have.text', ' test')
.find('.ol-cm-item')
.should('have.length', 1)
@ -113,18 +108,7 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
cy.findByRole('option').eq(0).click()
cy.get('@second-line').type('test{Enter}test{Enter}{upArrow}{Enter}{Enter}')
const lines = [
'\\begin{itemize}',
' test',
' ',
' ',
' test',
' ',
'\\end{itemize}',
]
cy.get('.cm-content').should('have.text', lines.join(''))
cy.get('.cm-content').should('have.text', ' testtest')
})
forEach(['textbf', 'textit', 'underline']).it(