highlight-selection-matches.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. /**
  2. * This file is adapted from CodeMirror 6, licensed under the MIT license:
  3. * https://github.com/codemirror/search/blob/main/src/selection-match.ts
  4. */
  5. import { EditorView, layer, RectangleMarker } from '@codemirror/view'
  6. import {
  7. CharCategory,
  8. EditorSelection,
  9. EditorState,
  10. Extension,
  11. } from '@codemirror/state'
  12. import { SearchCursor } from '@codemirror/search'
  13. import { rectangleMarkerForRange } from '../utils/layer'
  14. /*
  15. This extension highlights text that matches the selection.
  16. It uses the `"cm-selectionMatch"` class for the highlighting.
  17. */
  18. export const highlightSelectionMatches = (): Extension => [
  19. layer({
  20. above: false,
  21. markers(view) {
  22. return buildMarkers(view, view.state)
  23. },
  24. update(update) {
  25. return update.docChanged || update.selectionSet || update.viewportChanged
  26. },
  27. class: 'ol-cm-selectionMatchesLayer',
  28. }),
  29. EditorView.baseTheme({
  30. '.ol-cm-selectionMatchesLayer': {
  31. contain: 'size style',
  32. pointerEvents: 'none',
  33. },
  34. '.cm-selectionMatch': {
  35. position: 'absolute',
  36. },
  37. }),
  38. ]
  39. // Whether the characters directly outside the given positions are non-word characters
  40. function insideWordBoundaries(
  41. check: (char: string) => CharCategory,
  42. state: EditorState,
  43. from: number,
  44. to: number
  45. ): boolean {
  46. return (
  47. (from === 0 ||
  48. check(state.sliceDoc(from - 1, from)) !== CharCategory.Word) &&
  49. (to === state.doc.length ||
  50. check(state.sliceDoc(to, to + 1)) !== CharCategory.Word)
  51. )
  52. }
  53. // Whether the characters directly at the given positions are word characters
  54. function insideWord(
  55. check: (char: string) => CharCategory,
  56. state: EditorState,
  57. from: number,
  58. to: number
  59. ): boolean {
  60. return (
  61. check(state.sliceDoc(from, from + 1)) === CharCategory.Word &&
  62. check(state.sliceDoc(to - 1, to)) === CharCategory.Word
  63. )
  64. }
  65. const buildMarkers = (
  66. view: EditorView,
  67. state: EditorState
  68. ): RectangleMarker[] => {
  69. const sel = state.selection
  70. if (sel.ranges.length > 1) {
  71. return []
  72. }
  73. const range = sel.main
  74. if (range.empty) {
  75. return []
  76. }
  77. const len = range.to - range.from
  78. if (len < 3 || len > 200) {
  79. return []
  80. }
  81. const query = state.sliceDoc(range.from, range.to) // TODO: allow and include leading/trailing space?
  82. if (query === '') {
  83. return []
  84. }
  85. const check = state.charCategorizer(range.head)
  86. if (
  87. !(
  88. insideWordBoundaries(check, state, range.from, range.to) &&
  89. insideWord(check, state, range.from, range.to)
  90. )
  91. ) {
  92. return []
  93. }
  94. const markers: RectangleMarker[] = []
  95. for (const part of view.visibleRanges) {
  96. const cursor = new SearchCursor(state.doc, query, part.from, part.to)
  97. while (!cursor.next().done) {
  98. const { from, to } = cursor.value
  99. if (!check || insideWordBoundaries(check, state, from, to)) {
  100. markers.push(
  101. ...rectangleMarkerForRange(
  102. view,
  103. 'cm-selectionMatch',
  104. EditorSelection.range(from, to)
  105. )
  106. )
  107. if (markers.length > 100) {
  108. return []
  109. }
  110. }
  111. }
  112. }
  113. return markers
  114. }