Files

103 lines
3.4 KiB
TypeScript

// Ref: https://codemirror.net/examples/bundle/
// and https://codemirror.net/examples/decoration/
import { EditorView, Decoration, DecorationSet, WidgetType } from '@codemirror/view';
import { ViewPlugin, ViewUpdate } from '@codemirror/view';
import { syntaxTree } from '@codemirror/language';
import { Range } from '@codemirror/state';
import { SyntaxNodeRef } from '@lezer/common';
import { ReplacementExtension } from '../types';
import nodeIntersectsSelection from './nodeIntersectsSelection';
export const makeInlineReplaceExtension = (extensionSpec: ReplacementExtension) => ViewPlugin.fromClass(class {
public decorations: DecorationSet;
public constructor(view: EditorView) {
this.updateDecorations(view);
}
private updateDecorations(view: EditorView) {
const doc = view.state.doc;
const cursorLine = doc.lineAt(view.state.selection.main.anchor);
const selection = view.state.selection;
const parentTagCounts = new Map<string, number>();
const decorateNode = (node: SyntaxNodeRef) => {
const widgetOrDecoration = extensionSpec.createDecoration(node, view.state, parentTagCounts);
let decoration;
if (widgetOrDecoration instanceof WidgetType) {
decoration = Decoration.replace({
widget: widgetOrDecoration,
});
} else if (widgetOrDecoration instanceof Decoration) {
decoration = widgetOrDecoration;
}
if (decoration) {
const range = extensionSpec.getDecorationRange?.(node, view.state) ?? [node.from, node.to];
const rangeLineFrom = doc.lineAt(range[0]);
const rangeLineTo = range.length === 2 ? doc.lineAt(range[1]) : rangeLineFrom;
// A different start/end line causes errors.
if (rangeLineFrom.number === rangeLineTo.number) {
if (range.length === 1) {
widgets.push(decoration.range(range[0]));
} else {
widgets.push(decoration.range(range[0], range[1]));
}
}
}
};
const widgets: Range<Decoration>[] = [];
for (const { from, to } of view.visibleRanges) {
parentTagCounts.clear();
syntaxTree(view.state).iterate({
from, to,
enter: node => {
parentTagCounts.set(node.name, (parentTagCounts.get(node.name) ?? 0) + 1);
const strategy = extensionSpec.getRevealStrategy?.(node, view.state) ?? 'line';
let isSelected = false;
if (typeof strategy === 'boolean') {
isSelected = strategy;
} else if (strategy === 'line') {
const nodeLine = doc.lineAt(node.from);
const lineContainsSelection = cursorLine.number === nodeLine.number;
isSelected = lineContainsSelection || nodeIntersectsSelection(selection, node);
} else if (strategy === 'select') {
isSelected = nodeIntersectsSelection(selection, node);
} else if (strategy === 'active') {
const parent = node.node.parent;
isSelected = nodeIntersectsSelection(selection, node) || (!!parent && nodeIntersectsSelection(selection, parent));
}
const shouldHide = (
(extensionSpec.hideWhenContainsSelection ?? true) && isSelected
);
if (!shouldHide) {
decorateNode(node);
}
},
leave: node => {
parentTagCounts.set(node.name, (parentTagCounts.get(node.name) ?? 0) - 1);
},
});
}
this.decorations = Decoration.set(widgets, true);
}
public update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged || update.selectionSet) {
this.updateDecorations(update.view);
}
}
}, {
decorations: view => view.decorations,
});
export default makeInlineReplaceExtension;