2023-04-14 06:57:15 -04:00
|
|
|
import { EditorSelection, StateEffect, StateField } from '@codemirror/state'
|
|
|
|
import { Highlight } from '../services/types/doc'
|
|
|
|
import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'
|
2023-04-24 09:54:20 -04:00
|
|
|
import { highlightDecorationsField } from './highlights'
|
2023-04-14 06:57:15 -04:00
|
|
|
import { throttle, isEqual } from 'lodash'
|
2023-04-14 07:35:41 -04:00
|
|
|
import { updateHasEffect } from '../../source-editor/utils/effects'
|
2023-04-14 06:57:15 -04:00
|
|
|
|
|
|
|
export type HighlightLocations = {
|
|
|
|
before: number
|
|
|
|
after: number
|
|
|
|
next?: Highlight
|
|
|
|
previous?: Highlight
|
|
|
|
}
|
|
|
|
|
|
|
|
const setHighlightLocationsEffect = StateEffect.define<HighlightLocations>()
|
|
|
|
const hasSetHighlightLocationsEffect = updateHasEffect(
|
|
|
|
setHighlightLocationsEffect
|
|
|
|
)
|
|
|
|
|
|
|
|
// Returns the range within the document that is currently visible to the user
|
|
|
|
function visibleRange(view: EditorView) {
|
|
|
|
const { top, bottom } = view.scrollDOM.getBoundingClientRect()
|
|
|
|
const first = view.lineBlockAtHeight(top - view.documentTop)
|
|
|
|
const last = view.lineBlockAtHeight(bottom - view.documentTop)
|
|
|
|
return { from: first.from, to: last.to }
|
|
|
|
}
|
|
|
|
|
|
|
|
function calculateHighlightLocations(view: EditorView): HighlightLocations {
|
|
|
|
const highlightsBefore: Highlight[] = []
|
|
|
|
const highlightsAfter: Highlight[] = []
|
|
|
|
let next
|
|
|
|
let previous
|
|
|
|
|
2023-04-24 09:54:20 -04:00
|
|
|
const highlights =
|
|
|
|
view.state.field(highlightDecorationsField).highlights || []
|
2023-04-14 06:57:15 -04:00
|
|
|
|
|
|
|
if (highlights.length === 0) {
|
|
|
|
return { before: 0, after: 0 }
|
|
|
|
}
|
|
|
|
|
|
|
|
const { from: visibleFrom, to: visibleTo } = visibleRange(view)
|
|
|
|
|
|
|
|
for (const highlight of highlights) {
|
|
|
|
if (highlight.range.to <= visibleFrom) {
|
|
|
|
highlightsBefore.push(highlight)
|
|
|
|
} else if (highlight.range.from >= visibleTo) {
|
|
|
|
highlightsAfter.push(highlight)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const before = highlightsBefore.length
|
|
|
|
const after = highlightsAfter.length
|
|
|
|
if (before > 0) {
|
|
|
|
previous = highlightsBefore[highlightsBefore.length - 1]
|
|
|
|
}
|
|
|
|
if (after > 0) {
|
|
|
|
next = highlightsAfter[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
before,
|
|
|
|
after,
|
|
|
|
previous,
|
|
|
|
next,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const plugin = ViewPlugin.fromClass(
|
|
|
|
class {
|
|
|
|
// eslint-disable-next-line no-useless-constructor
|
|
|
|
constructor(readonly view: EditorView) {}
|
|
|
|
|
|
|
|
dispatchIfChanged() {
|
|
|
|
const oldLocations = this.view.state.field(highlightLocationsField)
|
|
|
|
const newLocations = calculateHighlightLocations(this.view)
|
|
|
|
|
|
|
|
if (!isEqual(oldLocations, newLocations)) {
|
|
|
|
this.view.dispatch({
|
|
|
|
effects: setHighlightLocationsEffect.of(newLocations),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
update(update: ViewUpdate) {
|
|
|
|
if (!hasSetHighlightLocationsEffect(update)) {
|
|
|
|
// Normally, a timeout is a poor choice, but in this case it doesn't
|
|
|
|
// matter that there is a slight delay or that it might run after the
|
|
|
|
// viewer has been torn down
|
|
|
|
window.setTimeout(() => this.dispatchIfChanged())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
eventHandlers: {
|
|
|
|
scroll: throttle(
|
|
|
|
(event, view: EditorView) => {
|
|
|
|
view.plugin(plugin)?.dispatchIfChanged()
|
|
|
|
},
|
|
|
|
120,
|
|
|
|
{ trailing: true }
|
|
|
|
),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
export const highlightLocationsField = StateField.define<HighlightLocations>({
|
|
|
|
create() {
|
|
|
|
return { before: 0, visible: 0, after: 0 }
|
|
|
|
},
|
|
|
|
update(highlightLocations, tr) {
|
|
|
|
for (const effect of tr.effects) {
|
|
|
|
if (effect.is(setHighlightLocationsEffect)) {
|
|
|
|
return effect.value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return highlightLocations
|
|
|
|
},
|
|
|
|
provide: () => [plugin],
|
|
|
|
})
|
|
|
|
|
|
|
|
export function highlightLocations() {
|
|
|
|
return highlightLocationsField
|
|
|
|
}
|
|
|
|
|
|
|
|
export function scrollToHighlight(view: EditorView, highlight: Highlight) {
|
|
|
|
view.dispatch({
|
|
|
|
effects: EditorView.scrollIntoView(
|
|
|
|
EditorSelection.range(highlight.range.from, highlight.range.to)
|
|
|
|
),
|
|
|
|
})
|
|
|
|
}
|