diff --git a/services/web/frontend/js/features/source-editor/extensions/toolbar/lists.ts b/services/web/frontend/js/features/source-editor/extensions/toolbar/lists.ts
index f1cd9e2986..cc6fb39a3a 100644
--- a/services/web/frontend/js/features/source-editor/extensions/toolbar/lists.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/toolbar/lists.ts
@@ -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(
diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts
index 54d3eb4a00..b5dcb3cd58 100644
--- a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts
@@ -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)
+ )
}
}
}
diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-list.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-list.spec.tsx
index 2715025d4f..0fe272c510 100644
--- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-list.spec.tsx
+++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-list.spec.tsx
@@ -43,47 +43,27 @@ describe(' 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(' 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(' 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(' 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(' 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(' 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(' 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(' 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(''))
})
})
diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx
index fa2404de64..7df4d4368c 100644
--- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx
+++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx
@@ -130,18 +130,10 @@ describe(' 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(' 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')
})
})
diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx
index 12ec483590..98325bbaa0 100644
--- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx
+++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx
@@ -68,25 +68,21 @@ describe(' 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(' 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(' 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(