commands.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state'
  2. import { Command, EditorView } from '@codemirror/view'
  3. import {
  4. closeSearchPanel,
  5. openSearchPanel,
  6. searchPanelOpen,
  7. } from '@codemirror/search'
  8. import { toggleRanges, wrapRanges } from '../../commands/ranges'
  9. import {
  10. ancestorListType,
  11. toggleListForRanges,
  12. unwrapBulletList,
  13. unwrapNumberedList,
  14. wrapInBulletList,
  15. wrapInNumberedList,
  16. } from './lists'
  17. import { snippet } from '@codemirror/autocomplete'
  18. import { snippets } from './snippets'
  19. import { minimumListDepthForSelection } from '../../utils/tree-operations/ancestors'
  20. import { isVisual } from '../visual/visual'
  21. export const toggleBold = toggleRanges('\\textbf')
  22. export const toggleItalic = toggleRanges('\\textit')
  23. // TODO: apply as a snippet?
  24. // TODO: read URL from clipboard?
  25. export const wrapInHref = wrapRanges('\\href{}{', '}', false, (range, view) =>
  26. isVisual(view) ? range : EditorSelection.cursor(range.from - 2)
  27. )
  28. export const toggleBulletList = toggleListForRanges('itemize')
  29. export const toggleNumberedList = toggleListForRanges('enumerate')
  30. export const wrapInInlineMath = wrapRanges('\\(', '\\)')
  31. export const wrapInDisplayMath = wrapRanges('\n\\[', '\\]\n')
  32. export const ensureEmptyLine = (
  33. state: EditorState,
  34. range: SelectionRange,
  35. direction: 'above' | 'below' = 'below'
  36. ) => {
  37. let pos = range.anchor
  38. let suffix = ''
  39. let prefix = ''
  40. const line = state.doc.lineAt(pos)
  41. if (line.text.trim().length) {
  42. if (direction === 'below') {
  43. pos = Math.min(line.to + 1, state.doc.length)
  44. } else {
  45. pos = Math.max(line.from - 1, 0)
  46. }
  47. const neighbouringLine = state.doc.lineAt(pos)
  48. if (neighbouringLine.length && direction === 'below') {
  49. suffix = '\n'
  50. } else if (neighbouringLine.length && direction === 'above') {
  51. prefix = '\n'
  52. }
  53. }
  54. return { pos, suffix, prefix }
  55. }
  56. export const insertFigure: Command = view => {
  57. const { state, dispatch } = view
  58. const { pos, suffix } = ensureEmptyLine(state, state.selection.main)
  59. const template = `\n${snippets.figure}\n${suffix}`
  60. snippet(template)({ state, dispatch }, { label: 'Figure' }, pos, pos)
  61. return true
  62. }
  63. export const insertTable = (view: EditorView, sizeX: number, sizeY: number) => {
  64. const { state, dispatch } = view
  65. const visual = isVisual(view)
  66. const placeholder = visual ? '' : '#{}'
  67. const placeholderAtStart = visual ? '#{}' : ''
  68. const { pos, suffix } = ensureEmptyLine(state, state.selection.main)
  69. const template = `${placeholderAtStart}\n\\begin{table}
  70. \t\\centering
  71. \t\\begin{tabular}{${'c'.repeat(sizeX)}}
  72. ${(
  73. '\t\t' +
  74. `${placeholder} & ${placeholder}`.repeat(sizeX - 1) +
  75. '\\\\\n'
  76. ).repeat(sizeY)}\t\\end{tabular}
  77. \t\\caption{Caption}
  78. \t\\label{tab:my_label}
  79. \\end{table}${suffix}`
  80. snippet(template)({ state, dispatch }, { label: 'Table' }, pos, pos)
  81. return true
  82. }
  83. export const insertCite: Command = view => {
  84. const { state, dispatch } = view
  85. const pos = state.selection.main.anchor
  86. const template = snippets.cite
  87. snippet(template)({ state, dispatch }, { label: 'Cite' }, pos, pos)
  88. return true
  89. }
  90. export const insertRef: Command = view => {
  91. const { state, dispatch } = view
  92. const pos = state.selection.main.anchor
  93. const template = snippets.ref
  94. snippet(template)({ state, dispatch }, { label: 'Ref' }, pos, pos)
  95. return true
  96. }
  97. export const indentDecrease: Command = view => {
  98. if (minimumListDepthForSelection(view.state) < 2) {
  99. return false
  100. }
  101. switch (ancestorListType(view.state)) {
  102. case 'itemize':
  103. return unwrapBulletList(view)
  104. case 'enumerate':
  105. return unwrapNumberedList(view)
  106. default:
  107. return false
  108. }
  109. }
  110. export const cursorIsAtStartOfListItem = (state: EditorState) => {
  111. return state.selection.ranges.every(range => {
  112. const line = state.doc.lineAt(range.from)
  113. const prefix = state.sliceDoc(line.from, range.from)
  114. return /\\item\s*$/.test(prefix)
  115. })
  116. }
  117. export const indentIncrease: Command = view => {
  118. if (minimumListDepthForSelection(view.state) < 1) {
  119. return false
  120. }
  121. switch (ancestorListType(view.state)) {
  122. case 'itemize':
  123. return wrapInBulletList(view)
  124. case 'enumerate':
  125. return wrapInNumberedList(view)
  126. default:
  127. return false
  128. }
  129. }
  130. export const toggleSearch: Command = view => {
  131. if (searchPanelOpen(view.state)) {
  132. closeSearchPanel(view)
  133. } else {
  134. openSearchPanel(view)
  135. }
  136. return true
  137. }