Просмотр исходного кода

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

GitOrigin-RevId: cadab83774d52dc6c4867fdd7300a1217423b837
Alf Eaton 3 лет назад
Родитель
Сommit
c16d2d5840

+ 16 - 0
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(

+ 75 - 75
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)
+                )
               }
             }
           }

+ 20 - 90
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-list.spec.tsx

@@ -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(''))
   })
 })

+ 8 - 24
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-toolbar.spec.tsx

@@ -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-line').eq(1).type('ing')
-    cy.get('.cm-line').eq(1).should('have.text', ' testing')
+    cy.get('.cm-content').should('have.text', ' test')
+
+    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-line').eq(1).type('ing')
-    cy.get('.cm-line').eq(1).should('have.text', ' testing')
+    cy.get('.cm-content').should('have.text', ' test')
+
+    cy.get('.cm-line').eq(0).type('ing')
+    cy.get('.cm-line').eq(0).should('have.text', ' testing')
   })
 })

+ 7 - 23
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx

@@ -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('@second-line')
+    cy.get('@first-line')
       .should('have.text', ' test')
       .find('.ol-cm-item')
       .should('have.length', 1)
-    cy.get('@third-line')
+    cy.get('@second-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(