@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
108 lines (97 loc) • 2.8 kB
text/typescript
import { StateField, StateEffect } from '@codemirror/state';
import { EditorView, Decoration, DecorationSet } from '@codemirror/view';
export interface IMark<Kinds> {
from: number;
to: number;
kind: Kinds;
}
/**
* Manage marks in multiple editor views (e.g. cells).
*/
export interface ISimpleMarkManager<Kinds> {
putMarks(view: EditorView, positions: IMark<Kinds>[]): void;
/**
* Clear marks from all editor views.
*/
clearAllMarks(): void;
clearEditorMarks(view: EditorView): void;
}
export type MarkDecorationSpec = Parameters<typeof Decoration.mark>[0] & {
class: string;
};
namespace Private {
export let specCounter = 0;
}
export function createMarkManager<Kinds extends string | number>(
specs: Record<Kinds, MarkDecorationSpec>
): ISimpleMarkManager<Kinds> {
const specId = ++Private.specCounter;
const kindToMark = Object.fromEntries(
Object.entries(specs).map(([k, spec]) => [
k as Kinds,
Decoration.mark({
...(spec as MarkDecorationSpec),
_id: Private.specCounter
})
])
) as Record<Kinds, Decoration>;
const addMark = StateEffect.define<IMark<Kinds>>({
map: ({ from, to, kind }, change) => ({
from: change.mapPos(from),
to: change.mapPos(to),
kind
})
});
const removeMark = StateEffect.define<null>();
const markField = StateField.define<DecorationSet>({
create() {
return Decoration.none;
},
update(marks, tr) {
marks = marks.map(tr.changes);
for (let e of tr.effects) {
if (e.is(addMark)) {
marks = marks.update({
add: [
kindToMark[e.value.kind].range(
Math.min(e.value.from, tr.newDoc.length),
Math.min(e.value.to, tr.newDoc.length)
)
]
});
} else if (e.is(removeMark)) {
marks = marks.update({
filter: (from, to, value) => {
return value.spec['_id'] !== specId;
}
});
}
}
return marks;
},
provide: f => EditorView.decorations.from(f)
});
const views = new Set<EditorView>();
return {
putMarks(view: EditorView, positions: IMark<Kinds>[]) {
const effects: StateEffect<unknown>[] = positions.map(position =>
addMark.of(position)
);
if (!view.state.field(markField, false)) {
effects.push(StateEffect.appendConfig.of([markField]));
}
view.dispatch({ effects });
views.add(view);
},
clearAllMarks() {
for (let view of views) {
this.clearEditorMarks(view);
}
views.clear();
},
clearEditorMarks(view: EditorView) {
const effects: StateEffect<unknown>[] = [removeMark.of(null)];
view.dispatch({ effects });
}
};
}