Ver Fonte

[visual] Handle Enter in an empty list item at the end of a nested list (#18034)

GitOrigin-RevId: 4b3f24c7cf18837ba87859340991d9bb2532560a
Alf Eaton há 2 anos atrás
pai
commit
45ee8aca93

+ 13 - 0
services/web/frontend/js/features/source-editor/extensions/visual/utils/list-item.ts

@@ -0,0 +1,13 @@
+import { EditorState } from '@codemirror/state'
+import {
+  getIndentation,
+  IndentContext,
+  indentString,
+} from '@codemirror/language'
+
+export const createListItem = (state: EditorState, pos: number) => {
+  const cx = new IndentContext(state)
+  const columns = getIndentation(cx, pos) ?? 0
+  const indent = indentString(state, columns)
+  return `${indent}\\item `
+}

+ 44 - 19
services/web/frontend/js/features/source-editor/extensions/visual/visual-keymap.ts

@@ -1,16 +1,17 @@
 import { keymap } from '@codemirror/view'
-import { EditorSelection, Prec } from '@codemirror/state'
-import { ancestorNodeOfType } from '../../utils/tree-query'
 import {
-  getIndentation,
-  IndentContext,
-  indentString,
-} from '@codemirror/language'
+  ChangeSpec,
+  EditorSelection,
+  Prec,
+  SelectionRange,
+} from '@codemirror/state'
+import { ancestorNodeOfType } from '../../utils/tree-query'
 import {
   cursorIsAtStartOfListItem,
   indentDecrease,
   indentIncrease,
 } from '../toolbar/commands'
+import { createListItem } from '@/features/source-editor/extensions/visual/utils/list-item'
 
 /**
  * A keymap which provides behaviours for the visual editor,
@@ -39,29 +40,53 @@ export const visualKeymap = Prec.highest(
                 if (line.text.trim() === '\\item') {
                   // no content on this line
 
-                  // delete this line
-                  const changes = state.changes({
+                  // outside the end of the current list
+                  const pos = listNode.to + 1
+
+                  // delete the current line
+                  const deleteCurrentLine = {
                     from: line.from,
                     to: line.to + 1,
                     insert: '',
-                  })
+                  }
+
+                  const changes: ChangeSpec[] = [deleteCurrentLine]
 
-                  // the start of the line after the list environment
-                  const range = EditorSelection.cursor(endLine.to + 1).map(
-                    changes
-                  )
+                  // the new cursor position
+                  let range: SelectionRange
+
+                  // if this is a nested list, insert a new empty list item after this list
+                  if (
+                    listNode.parent?.parent?.parent?.parent?.type.is(
+                      'ListEnvironment'
+                    )
+                  ) {
+                    const newListItem = createListItem(state, pos)
+
+                    changes.push({
+                      from: pos,
+                      insert: newListItem + '\n',
+                    })
+
+                    // place the cursor at the end of the new list item
+                    range = EditorSelection.cursor(pos + newListItem.length)
+                  } else {
+                    // place the cursor outside the end of the current list
+                    range = EditorSelection.cursor(pos)
+                  }
 
                   handled = true
 
-                  return { changes, range }
+                  return {
+                    changes,
+                    range: range.map(state.changes(deleteCurrentLine)),
+                  }
                 }
               }
 
-              // create a new list item
-              const cx = new IndentContext(state)
-              const columns = getIndentation(cx, from) ?? 0
-              const indent = indentString(state, columns)
-              const insert = `\n${indent}\\item `
+              // handle a list item that isn't at the end of a list
+
+              const insert = '\n' + createListItem(state, from)
 
               const countWhitespaceAfterPosition = (pos: number) => {
                 const line = state.doc.lineAt(pos)

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

@@ -226,4 +226,63 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
 
     cy.get('.cm-content').should('have.text', [' foo', ' bazbar'].join(''))
   })
+
+  it('handles Enter in an empty list item at the end of a top-level list', function () {
+    const content = [
+      '\\begin{itemize}',
+      '\\item foo',
+      '\\item ',
+      '\\end{itemize}',
+      '',
+    ].join('\n')
+    mountEditor(content)
+
+    cy.get('.cm-line').eq(2).click()
+    cy.focused().type('{enter}')
+
+    cy.get('.cm-content').should('have.text', [' foo'].join(''))
+  })
+
+  it('handles Enter in an empty list item at the end of a nested list', function () {
+    const content = [
+      '\\begin{itemize}',
+      '\\item foo bar',
+      '\\begin{itemize}',
+      '\\item baz',
+      '\\item ',
+      '\\end{itemize}',
+      '\\end{itemize}',
+    ].join('\n')
+    mountEditor(content)
+
+    cy.get('.cm-line').eq(3).click()
+    cy.focused().type('{enter}')
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [' foo', ' bar', ' baz', ' '].join('')
+    )
+  })
+
+  it('handles Enter in an empty list item at the end of a nested list with subsequent items', function () {
+    const content = [
+      '\\begin{itemize}',
+      '\\item foo bar',
+      '\\begin{itemize}',
+      '\\item baz',
+      '\\item ',
+      '\\end{itemize}',
+      '\\item test',
+      '\\end{itemize}',
+    ].join('\n')
+    mountEditor(content)
+
+    cy.get('.cm-line').eq(3).click()
+    cy.focused().type('{enter}')
+
+    cy.get('.cm-content').should(
+      'have.text',
+      [' foo', ' bar', ' baz', ' ', ' test'].join('')
+    )
+  })
 })