visual-keymap.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { keymap } from '@codemirror/view'
  2. import {
  3. ChangeSpec,
  4. EditorSelection,
  5. Prec,
  6. SelectionRange,
  7. } from '@codemirror/state'
  8. import { ancestorNodeOfType } from '../../utils/tree-query'
  9. import {
  10. cursorIsAtStartOfListItem,
  11. indentDecrease,
  12. indentIncrease,
  13. } from '../toolbar/commands'
  14. import { createListItem } from '@/features/source-editor/extensions/visual/utils/list-item'
  15. /**
  16. * A keymap which provides behaviours for the visual editor,
  17. * including lists and text formatting.
  18. */
  19. export const visualKeymap = Prec.highest(
  20. keymap.of([
  21. // create a new list item with the same indentation
  22. {
  23. key: 'Enter',
  24. run: view => {
  25. const { state } = view
  26. let handled = false
  27. const changes = state.changeByRange(range => {
  28. if (range.empty) {
  29. const { from } = range
  30. const listNode = ancestorNodeOfType(state, from, 'ListEnvironment')
  31. if (listNode) {
  32. const line = state.doc.lineAt(range.from)
  33. const endLine = state.doc.lineAt(listNode.to)
  34. if (line.number === endLine.number - 1) {
  35. // last item line
  36. if (line.text.trim() === '\\item') {
  37. // no content on this line
  38. // outside the end of the current list
  39. const pos = listNode.to + 1
  40. // delete the current line
  41. const deleteCurrentLine = {
  42. from: line.from,
  43. to: line.to + 1,
  44. insert: '',
  45. }
  46. const changes: ChangeSpec[] = [deleteCurrentLine]
  47. // the new cursor position
  48. let range: SelectionRange
  49. // if this is a nested list, insert a new empty list item after this list
  50. if (
  51. listNode.parent?.parent?.parent?.parent?.type.is(
  52. 'ListEnvironment'
  53. )
  54. ) {
  55. const newListItem = createListItem(state, pos)
  56. changes.push({
  57. from: pos,
  58. insert: newListItem + '\n',
  59. })
  60. // place the cursor at the end of the new list item
  61. range = EditorSelection.cursor(pos + newListItem.length)
  62. } else {
  63. // place the cursor outside the end of the current list
  64. range = EditorSelection.cursor(pos)
  65. }
  66. handled = true
  67. return {
  68. changes,
  69. range: range.map(state.changes(deleteCurrentLine)),
  70. }
  71. }
  72. }
  73. // handle a list item that isn't at the end of a list
  74. const insert = '\n' + createListItem(state, from)
  75. const countWhitespaceAfterPosition = (pos: number) => {
  76. const line = state.doc.lineAt(pos)
  77. const followingText = state.sliceDoc(pos, line.to)
  78. const matches = followingText.match(/^(\s+)/)
  79. return matches ? matches[1].length : 0
  80. }
  81. // move the cursor past any whitespace on the new line
  82. const pos =
  83. from + insert.length + countWhitespaceAfterPosition(from)
  84. handled = true
  85. return {
  86. changes: { from, insert },
  87. range: EditorSelection.cursor(pos),
  88. }
  89. }
  90. const sectioningNode = ancestorNodeOfType(
  91. state,
  92. from,
  93. 'SectioningCommand'
  94. )
  95. if (sectioningNode) {
  96. // jump out of a section heading to the start of the next line
  97. const nextLineNumber = state.doc.lineAt(from).number + 1
  98. if (nextLineNumber <= state.doc.lines) {
  99. const line = state.doc.line(nextLineNumber)
  100. handled = true
  101. return {
  102. range: EditorSelection.cursor(line.from),
  103. }
  104. }
  105. }
  106. }
  107. return { range }
  108. })
  109. if (handled) {
  110. view.dispatch(changes, {
  111. scrollIntoView: true,
  112. userEvent: 'input',
  113. })
  114. }
  115. return handled
  116. },
  117. },
  118. // Increase list indent
  119. {
  120. key: 'Mod-]',
  121. preventDefault: true,
  122. run: indentIncrease,
  123. },
  124. // Decrease list indent
  125. {
  126. key: 'Mod-[',
  127. preventDefault: true,
  128. run: indentDecrease,
  129. },
  130. // Increase list indent
  131. {
  132. key: 'Tab',
  133. preventDefault: true,
  134. run: view =>
  135. cursorIsAtStartOfListItem(view.state) && indentIncrease(view),
  136. },
  137. // Decrease list indent
  138. {
  139. key: 'Shift-Tab',
  140. preventDefault: true,
  141. run: indentDecrease,
  142. },
  143. ])
  144. )