highlight-active-line.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import {
  2. Decoration,
  3. DecorationSet,
  4. EditorView,
  5. layer,
  6. LayerMarker,
  7. RectangleMarker,
  8. ViewPlugin,
  9. ViewUpdate,
  10. } from '@codemirror/view'
  11. import { sourceOnly } from './visual/visual'
  12. import { fullHeightCoordsAtPos } from '../utils/layer'
  13. /**
  14. * An alternative version of the built-in highlightActiveLine extension,
  15. * using a custom approach for highlighting the full height of the active “visual line” of a wrapped line.
  16. */
  17. export const highlightActiveLine = (visual: boolean) => {
  18. // this extension should only be active in the source editor
  19. return sourceOnly(visual, [
  20. activeLineLayer,
  21. singleLineHighlighter,
  22. highlightActiveLineTheme,
  23. ])
  24. }
  25. const highlightActiveLineTheme = EditorView.baseTheme({
  26. '.ol-cm-activeLineLayer': {
  27. pointerEvents: 'none',
  28. },
  29. })
  30. /**
  31. * Line decoration approach used for non-wrapped lines, adapted from built-in
  32. * CodeMirror 6 highlightActiveLine, licensed under the MIT license:
  33. * https://github.com/codemirror/view/blob/main/src/active-line.ts
  34. */
  35. const lineDeco = Decoration.line({ class: 'cm-activeLine' })
  36. const singleLineHighlighter = ViewPlugin.fromClass(
  37. class {
  38. decorations: DecorationSet
  39. constructor(view: EditorView) {
  40. this.decorations = this.getDeco(view)
  41. }
  42. update(update: ViewUpdate) {
  43. if (update.geometryChanged || update.selectionSet) {
  44. this.decorations = this.getDeco(update.view)
  45. }
  46. }
  47. getDeco(view: EditorView) {
  48. const deco = []
  49. // NOTE: only highlighting the active line for the main selection
  50. const { main } = view.state.selection
  51. // No active line highlight when text is selected
  52. if (main.empty) {
  53. const line = view.lineBlockAt(main.head)
  54. if (line.height <= view.defaultLineHeight) {
  55. deco.push(lineDeco.range(line.from))
  56. }
  57. }
  58. return Decoration.set(deco)
  59. }
  60. },
  61. {
  62. decorations: v => v.decorations,
  63. }
  64. )
  65. // Custom layer approach, used only for wrapped lines
  66. const activeLineLayer = layer({
  67. above: false,
  68. class: 'ol-cm-activeLineLayer',
  69. markers(view: EditorView): readonly LayerMarker[] {
  70. const markers: LayerMarker[] = []
  71. // NOTE: only highlighting the active line for the main selection
  72. const { main } = view.state.selection
  73. // no active line highlight when text is selected
  74. if (!main.empty) {
  75. return markers
  76. }
  77. // Use line decoration when line doesn't wrap
  78. if (view.lineBlockAt(main.head).height <= view.defaultLineHeight) {
  79. return markers
  80. }
  81. const coords = fullHeightCoordsAtPos(
  82. view,
  83. main.head,
  84. main.assoc || undefined
  85. )
  86. if (coords) {
  87. const scrollRect = view.scrollDOM.getBoundingClientRect()
  88. const contentRect = view.contentDOM.getBoundingClientRect()
  89. const scrollTop = view.scrollDOM.scrollTop
  90. const top = coords.top - scrollRect.top + scrollTop
  91. const left = contentRect.left - scrollRect.left
  92. const width = contentRect.right - contentRect.left
  93. const height = coords.bottom - coords.top
  94. markers.push(
  95. new RectangleMarker('cm-activeLine', left, top, width, height)
  96. )
  97. }
  98. return markers
  99. },
  100. update(update: ViewUpdate): boolean {
  101. return update.geometryChanged || update.selectionSet
  102. },
  103. })