visual-keymap.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import { keymap } from '@codemirror/view'
  2. import { EditorSelection, Prec } from '@codemirror/state'
  3. import { ancestorNodeOfType } from '../../utils/tree-query'
  4. import { toggleRanges } from '../../commands/ranges'
  5. import {
  6. getIndentation,
  7. IndentContext,
  8. indentString,
  9. } from '@codemirror/language'
  10. import {
  11. cursorIsAtStartOfListItem,
  12. indentDecrease,
  13. indentIncrease,
  14. } from '../toolbar/commands'
  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. // delete this line
  39. const changes = state.changes({
  40. from: line.from,
  41. to: line.to + 1,
  42. insert: '',
  43. })
  44. // the start of the line after the list environment
  45. const range = EditorSelection.cursor(endLine.to + 1).map(
  46. changes
  47. )
  48. handled = true
  49. return { changes, range }
  50. }
  51. }
  52. // create a new list item
  53. const cx = new IndentContext(state)
  54. const columns = getIndentation(cx, from) ?? 0
  55. const indent = indentString(state, columns)
  56. const insert = `\n${indent}\\item `
  57. handled = true
  58. return {
  59. changes: { from, insert },
  60. range: EditorSelection.cursor(from + insert.length),
  61. }
  62. }
  63. const sectioningNode = ancestorNodeOfType(
  64. state,
  65. from,
  66. 'SectioningCommand'
  67. )
  68. if (sectioningNode) {
  69. // jump out of a section heading to the start of the next line
  70. const nextLineNumber = state.doc.lineAt(from).number + 1
  71. if (nextLineNumber <= state.doc.lines) {
  72. const line = state.doc.line(nextLineNumber)
  73. handled = true
  74. return {
  75. range: EditorSelection.cursor(line.from),
  76. }
  77. }
  78. }
  79. }
  80. return { range }
  81. })
  82. if (handled) {
  83. view.dispatch(changes, {
  84. scrollIntoView: true,
  85. userEvent: 'input',
  86. })
  87. }
  88. return handled
  89. },
  90. },
  91. // Increase list indent
  92. {
  93. key: 'Mod-]',
  94. preventDefault: true,
  95. run: indentIncrease,
  96. },
  97. // Decrease list indent
  98. {
  99. key: 'Mod-[',
  100. preventDefault: true,
  101. run: indentDecrease,
  102. },
  103. // Increase list indent
  104. {
  105. key: 'Tab',
  106. preventDefault: true,
  107. run: view =>
  108. cursorIsAtStartOfListItem(view.state) && indentIncrease(view),
  109. },
  110. // Decrease list indent
  111. {
  112. key: 'Shift-Tab',
  113. preventDefault: true,
  114. run: indentDecrease,
  115. },
  116. // Override bolding in RT mode
  117. {
  118. key: 'Ctrl-b',
  119. mac: 'Mod-b',
  120. preventDefault: true,
  121. run: toggleRanges('\\textbf'),
  122. },
  123. {
  124. key: 'Ctrl-i',
  125. mac: 'Mod-i',
  126. preventDefault: true,
  127. run: toggleRanges('\\textit'),
  128. },
  129. ])
  130. )