visual-keymap.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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. handled = true
  57. return {
  58. changes: { from, insert },
  59. range: EditorSelection.cursor(from + insert.length),
  60. }
  61. }
  62. const sectioningNode = ancestorNodeOfType(
  63. state,
  64. from,
  65. 'SectioningCommand'
  66. )
  67. if (sectioningNode) {
  68. // jump out of a section heading to the start of the next line
  69. const nextLineNumber = state.doc.lineAt(from).number + 1
  70. if (nextLineNumber <= state.doc.lines) {
  71. const line = state.doc.line(nextLineNumber)
  72. handled = true
  73. return {
  74. range: EditorSelection.cursor(line.from),
  75. }
  76. }
  77. }
  78. }
  79. return { range }
  80. })
  81. if (handled) {
  82. view.dispatch(changes, {
  83. scrollIntoView: true,
  84. userEvent: 'input',
  85. })
  86. }
  87. return handled
  88. },
  89. },
  90. // Increase list indent
  91. {
  92. key: 'Mod-]',
  93. preventDefault: true,
  94. run: indentIncrease,
  95. },
  96. // Decrease list indent
  97. {
  98. key: 'Mod-[',
  99. preventDefault: true,
  100. run: indentDecrease,
  101. },
  102. // Increase list indent
  103. {
  104. key: 'Tab',
  105. preventDefault: true,
  106. run: view =>
  107. cursorIsAtStartOfListItem(view.state) && indentIncrease(view),
  108. },
  109. // Decrease list indent
  110. {
  111. key: 'Shift-Tab',
  112. preventDefault: true,
  113. run: indentDecrease,
  114. },
  115. ])
  116. )