overleaf/services/web/frontend/js/features/source-editor/extensions/annotations.ts
Alf Eaton e025088065 Merge pull request #13241 from overleaf/ae-extensions-documentation
Add documentation for CodeMirror extensions

GitOrigin-RevId: e5f07084173f201919272f9d46dcdaef4b817874
2023-07-17 10:28:53 +00:00

154 lines
3.7 KiB
TypeScript

import { EditorView, ViewUpdate } from '@codemirror/view'
import { Diagnostic, linter, lintGutter } from '@codemirror/lint'
import {
Compartment,
Extension,
RangeSet,
RangeValue,
StateEffect,
StateField,
Text,
} from '@codemirror/state'
import { Annotation } from '../../../../../types/annotation'
const compileLintSourceConf = new Compartment()
export const annotations = () => [
compileDiagnosticsState,
compileLintSourceConf.of(compileLogLintSource()),
/**
* The built-in lint gutter extension, configured with zero hover delay.
*/
lintGutter({
hoverTime: 0,
}),
/**
* A theme which moves the lint gutter outside the line numbers.
*/
EditorView.baseTheme({
'.cm-gutter-lint': {
order: -1,
},
}),
]
export const lintSourceConfig = {
delay: 100,
// Show highlights only for errors
markerFilter(diagnostics: readonly Diagnostic[]) {
return diagnostics.filter(d => d.severity === 'error')
},
// Do not show any tooltips for highlights within the editor content
tooltipFilter() {
return []
},
needsRefresh(update: ViewUpdate) {
return update.selectionSet
},
}
/**
* A lint source using the compile log diagnostics
*/
const compileLogLintSource = (): Extension =>
linter(view => {
const items: Diagnostic[] = []
const cursor = view.state.field(compileDiagnosticsState).iter()
while (cursor.value !== null) {
items.push({
...cursor.value.diagnostic,
from: cursor.from,
to: cursor.to,
})
cursor.next()
}
return items
}, lintSourceConfig)
class DiagnosticRangeValue extends RangeValue {
constructor(public diagnostic: Diagnostic) {
super()
}
}
const setCompileDiagnosticsEffect = StateEffect.define<Diagnostic[]>()
/**
* A state field for the compile log diagnostics
*/
export const compileDiagnosticsState = StateField.define<
RangeSet<DiagnosticRangeValue>
>({
create() {
return RangeSet.empty
},
update(value, transaction) {
for (const effect of transaction.effects) {
if (effect.is(setCompileDiagnosticsEffect)) {
return RangeSet.of(
effect.value.map(diagnostic =>
new DiagnosticRangeValue(diagnostic).range(
diagnostic.from,
diagnostic.to
)
),
true
)
}
}
if (transaction.docChanged) {
value = value.map(transaction.changes)
}
return value
},
})
export const setAnnotations = (doc: Text, annotations: Annotation[]) => {
const diagnostics: Diagnostic[] = []
for (const annotation of annotations) {
// ignore "whole document" (row: -1) annotations
if (annotation.row !== -1) {
try {
diagnostics.push(convertAnnotationToDiagnostic(doc, annotation))
} catch (error) {
// ignore invalid annotations
console.debug('invalid annotation position', error)
}
}
}
return {
effects: setCompileDiagnosticsEffect.of(diagnostics),
}
}
export const showCompileLogDiagnostics = (show: boolean) => {
return {
effects: [
// reconfigure the compile log lint source
compileLintSourceConf.reconfigure(show ? compileLogLintSource() : []),
],
}
}
const convertAnnotationToDiagnostic = (
doc: Text,
annotation: Annotation
): Diagnostic => {
if (annotation.row < 0) {
throw new Error(`Invalid annotation row ${annotation.row}`)
}
const line = doc.line(annotation.row + 1)
return {
from: line.from,
to: line.to, // NOTE: highlight whole line as synctex doesn't output column number
severity: annotation.type,
message: annotation.text,
// source: annotation.source, // NOTE: the source is displayed in the tooltip
}
}