unified-query
Version:
Composable search input with autocompletion and a rich query-language parser for the Unified Data System
118 lines (117 loc) • 5.3 kB
JavaScript
// src/lib/plugins.ts
import { Decoration, ViewPlugin, ViewUpdate, EditorView, WidgetType } from '@codemirror/view';
import { RangeSetBuilder, RangeSet } from '@codemirror/state';
import { parse } from './parsers/index.js';
/* reusable decorations -------------------------------------------------- */
const decoKeyword = Decoration.mark({ class: 'cm-qs-keyword' });
const decoArg = Decoration.mark({ class: 'cm-qs-arg' });
const decoIgnored = Decoration.line({ class: 'cm-qs-ignored' });
export const highlighter = ViewPlugin.fromClass(class {
decorations;
constructor(view) { this.decorations = this.build(view); }
update(u) {
if (u.docChanged || u.viewportChanged)
this.decorations = this.build(u.view);
}
/* ------------------------------------------------------------------ */
/* build decorations for visible segments */
/* ------------------------------------------------------------------ */
build(view) {
const visFrom = view.visibleRanges[0]?.from ?? 0;
const visTo = view.visibleRanges.at(-1)?.to ?? view.state.doc.length;
const builder = new RangeSetBuilder();
const { segments } = parse(view.state.doc.toString());
console.log({ segments });
for (const seg of segments) {
if (seg.to < visFrom || seg.from > visTo)
continue; // outside viewport
if (seg.keyword != 'head') {
/* keyword span (“@” + ident) ----------------------------------- */
builder.add(seg.from, seg.from + seg.keyword.length + 1, decoKeyword);
}
}
return builder.finish();
}
}, { decorations: v => v.decorations });
/* ---------------------------------------------------------------------- */
/* Widget that renders the entity name in place of a UUID */
/* ---------------------------------------------------------------------- */
class EntityNameWidget extends WidgetType {
name;
constructor(name) {
super();
this.name = name;
}
toDOM() {
const span = document.createElement('span');
span.textContent = this.name;
span.className = 'cm-entity-name'; // Style this in your theme / global CSS
return span;
}
ignoreEvent() { return true; }
}
/**
* CodeMirror view‑plugin factory that replaces UUID tokens with the
* corresponding entity names.
*
* @param entityMap – mapping from canonical UUID strings to Entity objects.
* An `Entity` must provide at least a `name` property.
*/
export function uuidNamePlugin(entityMap) {
return ViewPlugin.fromClass(class {
view;
decorations;
constructor(view) {
this.view = view;
this.decorations = this.build(view);
}
update(u) {
if (u.docChanged || u.viewportChanged) {
this.decorations = this.build(u.view);
}
}
/* ------------------------------------------------------------------ */
/* Build decorations for the visible viewport */
/* ------------------------------------------------------------------ */
build(view) {
const visFrom = view.visibleRanges[0]?.from ?? 0;
const visTo = view.visibleRanges.at(-1)?.to ?? view.state.doc.length;
const builder = new RangeSetBuilder();
const { segments } = parse(view.state.doc.toString());
for (const seg of segments) {
if (seg.to < visFrom || seg.from > visTo)
continue; // outside viewport
for (const tok of seg.tokens) {
if (tok.kind !== 'uuid')
continue;
const ent = entityMap.get(tok.value);
if (!ent)
continue; // no mapping
// Replace the UUID (including a trailing "*" for deep refs) with the entity name.
const name = ent.name;
const widget = Decoration.replace({
widget: new EntityNameWidget(name),
// Maintain the order with surrounding text to prevent cursor jumps
side: 0,
});
// Subtract 1 for the '*' if present in the uuid token (deep: true flag)
builder.add(tok.from, tok.to - (tok.deep ? 1 : 0), widget);
}
}
return builder.finish();
}
}, {
decorations: v => v.decorations,
provide: plugin => EditorView.atomicRanges.of(view => {
return view.plugin(plugin)?.decorations || Decoration.none;
})
});
}
/* ---------------------------------------------------------------------- */
/* Convenience helper to append/replace the plugin in an extension array. */
/* ---------------------------------------------------------------------- */
export function withUuidNamePlugin(ext, entityMap) {
return [...ext.filter(e => !(e && e.isUuidNamePlugin)), uuidNamePlugin(entityMap)];
}
// Tag the plugin so we can identify/replace it later (see helper above)
uuidNamePlugin.isUuidNamePlugin = true;