overleaf/services/web/frontend/js/features/source-editor/extensions/highlight-active-line.ts

123 lines
3.2 KiB
TypeScript
Raw Normal View History

import {
Decoration,
DecorationSet,
EditorView,
layer,
LayerMarker,
RectangleMarker,
ViewPlugin,
ViewUpdate,
} from '@codemirror/view'
import { sourceOnly } from './visual/visual'
import { fullHeightCoordsAtPos } from '../utils/layer'
/**
* An alternative version of the built-in highlightActiveLine extension,
* using a custom approach for highlighting the full height of the active visual line of a wrapped line.
*/
export const highlightActiveLine = (visual: boolean) => {
// this extension should only be active in the source editor
return sourceOnly(visual, [
activeLineLayer,
singleLineHighlighter,
highlightActiveLineTheme,
])
}
const highlightActiveLineTheme = EditorView.baseTheme({
'.ol-cm-activeLineLayer': {
pointerEvents: 'none',
},
})
/**
* Line decoration approach used for non-wrapped lines, adapted from built-in
* CodeMirror 6 highlightActiveLine, licensed under the MIT license:
* https://github.com/codemirror/view/blob/main/src/active-line.ts
*/
const lineDeco = Decoration.line({ class: 'cm-activeLine' })
const singleLineHighlighter = ViewPlugin.fromClass(
class {
decorations: DecorationSet
constructor(view: EditorView) {
this.decorations = this.getDeco(view)
}
update(update: ViewUpdate) {
if (update.geometryChanged || update.selectionSet) {
this.decorations = this.getDeco(update.view)
}
}
getDeco(view: EditorView) {
const deco = []
// NOTE: only highlighting the active line for the main selection
const { main } = view.state.selection
// No active line highlight when text is selected
if (main.empty) {
const line = view.lineBlockAt(main.head)
if (line.height <= view.defaultLineHeight) {
deco.push(lineDeco.range(line.from))
}
}
return Decoration.set(deco)
}
},
{
decorations: v => v.decorations,
}
)
// Custom layer approach, used only for wrapped lines
const activeLineLayer = layer({
above: false,
class: 'ol-cm-activeLineLayer',
markers(view: EditorView): readonly LayerMarker[] {
const markers: LayerMarker[] = []
// NOTE: only highlighting the active line for the main selection
const { main } = view.state.selection
// no active line highlight when text is selected
if (!main.empty) {
return markers
}
// Use line decoration when line doesn't wrap
if (view.lineBlockAt(main.head).height <= view.defaultLineHeight) {
return markers
}
const coords = fullHeightCoordsAtPos(
view,
main.head,
main.assoc || undefined
)
if (coords) {
const scrollRect = view.scrollDOM.getBoundingClientRect()
const contentRect = view.contentDOM.getBoundingClientRect()
const scrollTop = view.scrollDOM.scrollTop
const top = coords.top - scrollRect.top + scrollTop
const left = contentRect.left - scrollRect.left
const width = contentRect.right - contentRect.left
const height = coords.bottom - coords.top
markers.push(
new RectangleMarker('cm-activeLine', left, top, width, height)
)
}
return markers
},
update(update: ViewUpdate): boolean {
return update.geometryChanged || update.selectionSet
},
})