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

GitOrigin-RevId: 4b3f24c7cf18837ba87859340991d9bb2532560a
This commit is contained in:
Alf Eaton 2024-04-23 09:33:37 +01:00 committed by Copybot
parent ce00af7838
commit 45ee8aca93
3 changed files with 116 additions and 19 deletions

View file

@ -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 `
}

View file

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

View file

@ -226,4 +226,63 @@ describe('<CodeMirrorEditor/> lists in Rich Text mode', function () {
cy.get('.cm-content').should('have.text', [' foo', ' bazbar'].join('')) 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('')
)
})
}) })