highlight-active-line.ts 3.0 KB

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