selection.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import {
  2. EditorSelection,
  3. StateEffect,
  4. Line,
  5. Text,
  6. StateField,
  7. EditorState,
  8. } from '@codemirror/state'
  9. import { EditorView } from '@codemirror/view'
  10. import { hasEffect, updateHasEffect } from '../../utils/effects'
  11. export const selectionIntersects = (
  12. selection: EditorSelection,
  13. extents: { from: number; to: number }
  14. ) =>
  15. selection.ranges.some(
  16. range =>
  17. // Case 1: from is inside node
  18. (extents.from <= range.from && extents.to >= range.from) ||
  19. // Case 2: to is inside node
  20. (extents.from <= range.to && extents.to >= range.to)
  21. )
  22. export const placeSelectionInsideBlock = (
  23. view: EditorView,
  24. event: MouseEvent
  25. ) => {
  26. const line = view.lineBlockAtHeight(event.pageY - view.documentTop)
  27. const selectionRange = EditorSelection.cursor(line.to)
  28. const selection = event.ctrlKey
  29. ? view.state.selection.addRange(selectionRange)
  30. : selectionRange
  31. return { selection, effects: EditorView.scrollIntoView(line.to) }
  32. }
  33. export const extendBackwardsOverEmptyLines = (
  34. doc: Text,
  35. line: Line,
  36. limit: number = Number.POSITIVE_INFINITY
  37. ) => {
  38. let { number, from } = line
  39. for (
  40. let lineNumber = number - 1;
  41. lineNumber > 0 && number - lineNumber <= limit;
  42. lineNumber--
  43. ) {
  44. const line = doc.line(lineNumber)
  45. if (line.text.trim().length > 0) {
  46. break
  47. }
  48. from = line.from
  49. }
  50. return from
  51. }
  52. export const extendForwardsOverEmptyLines = (
  53. doc: Text,
  54. line: Line,
  55. limit: number = Number.POSITIVE_INFINITY
  56. ) => {
  57. let { number, to } = line
  58. for (
  59. let lineNumber = number + 1;
  60. lineNumber <= doc.lines && lineNumber - number <= limit;
  61. lineNumber++
  62. ) {
  63. const line = doc.line(lineNumber)
  64. if (line.text.trim().length > 0) {
  65. break
  66. }
  67. to = line.to
  68. }
  69. return to
  70. }
  71. export const mouseDownEffect = StateEffect.define<boolean>()
  72. export const hasMouseDownEffect = hasEffect(mouseDownEffect)
  73. export const updateHasMouseDownEffect = updateHasEffect(mouseDownEffect)
  74. /**
  75. * A listener for mousedown and mouseup events, dispatching an event
  76. * to record the current mousedown status, which is stored in a state field.
  77. */
  78. const mouseDownListener = EditorView.domEventHandlers({
  79. mousedown: (event, view) => {
  80. // not wrapped in a timeout, so update listeners know that the mouse is down before they process the selection
  81. view.dispatch({
  82. effects: mouseDownEffect.of(true),
  83. })
  84. },
  85. mouseup: (event, view) => {
  86. // wrap in a timeout, so update listeners receive this effect after the new selection has finished being handled
  87. window.setTimeout(() => {
  88. view.dispatch({
  89. effects: mouseDownEffect.of(false),
  90. })
  91. })
  92. },
  93. contextmenu: (event: MouseEvent, view) => {
  94. // treat a `contextmenu` event as a `mouseup` event, which isn't fired
  95. window.setTimeout(() => {
  96. view.dispatch({
  97. effects: mouseDownEffect.of(false),
  98. })
  99. })
  100. },
  101. drop: (event: MouseEvent, view) => {
  102. // treat a `drop` event as a `mouseup` event, which isn't fired
  103. window.setTimeout(() => {
  104. view.dispatch({
  105. effects: mouseDownEffect.of(false),
  106. })
  107. })
  108. },
  109. })
  110. const mousedownSelectionState = StateField.define<EditorSelection | undefined>({
  111. create() {
  112. return undefined
  113. },
  114. update(value, tr) {
  115. if (value && tr.docChanged) {
  116. value = value.map(tr.changes)
  117. }
  118. for (const effect of tr.effects) {
  119. // store the previous selection on mousedown
  120. if (effect.is(mouseDownEffect)) {
  121. value = effect.value ? tr.startState.selection : undefined
  122. }
  123. }
  124. return value
  125. },
  126. })
  127. export const getMousedownSelection = (state: EditorState) =>
  128. state.field(mousedownSelectionState)
  129. export const mousedown = [mouseDownListener, mousedownSelectionState]