import { StateField, StateEffect } from '@codemirror/state'; import { EditorView, Decoration, DecorationSet } from '@codemirror/view'; export interface IMark { from: number; to: number; kind: Kinds; } /** * Manage marks in multiple editor views (e.g. cells). */ export interface ISimpleMarkManager { putMarks(view: EditorView, positions: IMark[]): void; /** * Clear marks from all editor views. */ clearAllMarks(): void; clearEditorMarks(view: EditorView): void; } export type MarkDecorationSpec = Parameters[0] & { class: string; }; namespace Private { export let specCounter = 0; } export function createMarkManager( specs: Record ): ISimpleMarkManager { 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; const addMark = StateEffect.define>({ map: ({ from, to, kind }, change) => ({ from: change.mapPos(from), to: change.mapPos(to), kind }) }); const removeMark = StateEffect.define(); const markField = StateField.define({ 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(); return { putMarks(view: EditorView, positions: IMark[]) { const effects: StateEffect[] = 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[] = [removeMark.of(null)]; view.dispatch({ effects }); } }; }