sections.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state'
  2. import { EditorView } from '@codemirror/view'
  3. import { syntaxTree } from '@codemirror/language'
  4. import { ancestorsOfNodeWithType } from '../../utils/tree-operations/ancestors'
  5. import { SyntaxNode } from '@lezer/common'
  6. export const findCurrentSectionHeadingLevel = (state: EditorState) => {
  7. const selections = state.selection.ranges.map(range =>
  8. rangeInfo(state, range)
  9. )
  10. const currentLevels = new Set(selections.map(item => item.level))
  11. return currentLevels.size === 1 ? selections[0] : null
  12. }
  13. type RangeInfo = {
  14. range: SelectionRange
  15. command?: SyntaxNode
  16. ctrlSeq?: SyntaxNode
  17. level: string
  18. }
  19. export const rangeInfo = (
  20. state: EditorState,
  21. range: SelectionRange
  22. ): RangeInfo => {
  23. const tree = syntaxTree(state)
  24. const node = tree.resolveInner(range.anchor)
  25. const command = ancestorsOfNodeWithType(node, 'SectioningCommand').next()
  26. .value
  27. const ctrlSeq = command?.firstChild
  28. const level = ctrlSeq
  29. ? state.sliceDoc(ctrlSeq.from + 1, ctrlSeq.to).trim()
  30. : 'text'
  31. return { command, ctrlSeq, level, range }
  32. }
  33. export const setSectionHeadingLevel = (view: EditorView, level: string) => {
  34. view.dispatch(
  35. view.state.changeByRange(range => {
  36. const info = rangeInfo(view.state, range)
  37. if (level === info.level) {
  38. return { range }
  39. }
  40. if (level === 'text' && info.command) {
  41. // remove
  42. const argument = info.command.getChild('SectioningArgument')
  43. if (argument) {
  44. const content = view.state.sliceDoc(
  45. argument.from + 1,
  46. argument.to - 1
  47. )
  48. // map through the prefix only
  49. const changedRange = range.map(
  50. view.state.changes([
  51. { from: info.command.from, to: argument.from + 1, insert: '' },
  52. ]),
  53. 1
  54. )
  55. return {
  56. range: changedRange,
  57. changes: [
  58. {
  59. from: info.command.from,
  60. to: info.command.to,
  61. insert: content,
  62. },
  63. ],
  64. }
  65. }
  66. return { range }
  67. } else if (info.level === 'text') {
  68. // add
  69. const insert = {
  70. prefix: `\\${level}{`,
  71. suffix: '}',
  72. }
  73. const originalRange = range
  74. const line = view.state.doc.lineAt(range.anchor)
  75. if (range.empty) {
  76. // expand range to cover the whole line
  77. range = EditorSelection.range(line.from, line.to)
  78. } else {
  79. if (range.from !== line.from) {
  80. insert.prefix = '\n' + insert.prefix
  81. }
  82. if (range.to !== line.to) {
  83. insert.suffix += '\n'
  84. }
  85. }
  86. const content = view.state.sliceDoc(range.from, range.to)
  87. // map through the prefix only
  88. const changedRange = originalRange.map(
  89. view.state.changes([
  90. { from: range.from, insert: `${insert.prefix}` },
  91. ]),
  92. 1
  93. )
  94. return {
  95. range: changedRange,
  96. // create a single change, including the content
  97. changes: [
  98. {
  99. from: range.from,
  100. to: range.to,
  101. insert: `${insert.prefix}${content}${insert.suffix}`,
  102. },
  103. ],
  104. }
  105. } else {
  106. // change
  107. if (!info.ctrlSeq) {
  108. return { range }
  109. }
  110. const changes = view.state.changes([
  111. {
  112. from: info.ctrlSeq.from + 1,
  113. to: info.ctrlSeq.to,
  114. insert: level,
  115. },
  116. ])
  117. return {
  118. range: range.map(changes),
  119. changes,
  120. }
  121. }
  122. })
  123. )
  124. }