visual-line-selection.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import {
  2. SelectionRange,
  3. EditorSelection,
  4. EditorState,
  5. Transaction,
  6. } from '@codemirror/state'
  7. import { Command, EditorView } from '@codemirror/view'
  8. const getNextLineBoundary = (
  9. selection: SelectionRange,
  10. forward: boolean,
  11. view: EditorView,
  12. includeWrappingCharacter = false
  13. ) => {
  14. const newSelection = view.moveToLineBoundary(
  15. EditorSelection.cursor(
  16. selection.head,
  17. 1,
  18. selection.bidiLevel || undefined,
  19. selection.goalColumn
  20. ),
  21. forward
  22. )
  23. // Adjust to be "before" the simulated line break
  24. let offset = 0
  25. if (
  26. forward &&
  27. !includeWrappingCharacter &&
  28. view.lineBlockAt(selection.head).to !== newSelection.head
  29. ) {
  30. offset = 1
  31. }
  32. return EditorSelection.cursor(
  33. newSelection.head - offset,
  34. selection.assoc,
  35. selection.bidiLevel || undefined,
  36. newSelection.goalColumn
  37. )
  38. }
  39. const changeSelection = (
  40. view: EditorView,
  41. how: (selection: SelectionRange) => SelectionRange,
  42. extend = false
  43. ) => {
  44. view.dispatch({
  45. selection: EditorSelection.create(
  46. view.state.selection.ranges.map(start => {
  47. const newSelection = how(start)
  48. const anchor = extend ? start.anchor : newSelection.head
  49. return EditorSelection.range(
  50. anchor,
  51. newSelection.head,
  52. newSelection.goalColumn,
  53. newSelection.bidiLevel || undefined
  54. )
  55. }),
  56. view.state.selection.mainIndex
  57. ),
  58. scrollIntoView: true,
  59. userEvent: 'select',
  60. })
  61. }
  62. export const cursorToEndOfVisualLine = (view: EditorView) =>
  63. changeSelection(view, range => getNextLineBoundary(range, true, view), false)
  64. export const selectToEndOfVisualLine = (view: EditorView) =>
  65. changeSelection(view, range => getNextLineBoundary(range, true, view), true)
  66. export const selectRestOfVisualLine = (view: EditorView) =>
  67. changeSelection(
  68. view,
  69. range => getNextLineBoundary(range, true, view, true),
  70. true
  71. )
  72. export const cursorToBeginningOfVisualLine = (view: EditorView) =>
  73. changeSelection(view, range => getNextLineBoundary(range, false, view), false)
  74. export const selectToBeginningOfVisualLine = (view: EditorView) =>
  75. changeSelection(view, range => getNextLineBoundary(range, false, view), true)
  76. export const deleteToVisualLineEnd: Command = view =>
  77. deleteBy(view, pos => {
  78. const lineEnd = getNextLineBoundary(
  79. EditorSelection.cursor(pos),
  80. true,
  81. view,
  82. true
  83. ).to
  84. return pos < lineEnd ? lineEnd : Math.min(view.state.doc.length, pos + 1)
  85. })
  86. export const deleteToVisualLineStart: Command = view =>
  87. deleteBy(view, pos => {
  88. const lineStart = getNextLineBoundary(
  89. EditorSelection.cursor(pos),
  90. false,
  91. view
  92. ).to
  93. return pos > lineStart ? lineStart : Math.max(0, pos - 1)
  94. })
  95. /* eslint-disable */
  96. /**
  97. * The following definitions are from CodeMirror 6, licensed under the MIT license:
  98. * https://github.com/codemirror/commands/blob/main/src/commands.ts
  99. */
  100. type CommandTarget = { state: EditorState; dispatch: (tr: Transaction) => void }
  101. function deleteBy(target: CommandTarget, by: (start: number) => number) {
  102. if (target.state.readOnly) return false
  103. let event = 'delete.selection',
  104. { state } = target
  105. let changes = state.changeByRange(range => {
  106. let { from, to } = range
  107. if (from == to) {
  108. let towards = by(from)
  109. if (towards < from) {
  110. event = 'delete.backward'
  111. towards = skipAtomic(target, towards, false)
  112. } else if (towards > from) {
  113. event = 'delete.forward'
  114. towards = skipAtomic(target, towards, true)
  115. }
  116. from = Math.min(from, towards)
  117. to = Math.max(to, towards)
  118. } else {
  119. from = skipAtomic(target, from, false)
  120. to = skipAtomic(target, to, true)
  121. }
  122. return from == to
  123. ? { range }
  124. : { changes: { from, to }, range: EditorSelection.cursor(from) }
  125. })
  126. if (changes.changes.empty) return false
  127. target.dispatch(
  128. state.update(changes, {
  129. scrollIntoView: true,
  130. userEvent: event,
  131. effects:
  132. event == 'delete.selection'
  133. ? EditorView.announce.of(state.phrase('selection_deleted'))
  134. : undefined,
  135. })
  136. )
  137. return true
  138. }
  139. function skipAtomic(target: CommandTarget, pos: number, forward: boolean) {
  140. if (target instanceof EditorView)
  141. for (let ranges of target.state
  142. .facet(EditorView.atomicRanges)
  143. .map(f => f(target)))
  144. ranges.between(pos, pos, (from, to) => {
  145. if (from < pos && to > pos) pos = forward ? to : from
  146. })
  147. return pos
  148. }