visual-keymap.ts 4.0 KB

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