char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somdiff: add (codemirror) inline editing of diffsc5dd844

main
2.2 KiB63 linesraw
1import { highlight } from "@berry/api/web.ts";
2import { type Extension, type Range, StateEffect, StateField } from "@codemirror/state";
3import {
4  Decoration,
5  type DecorationSet,
6  EditorView,
7  ViewPlugin,
8  type ViewUpdate,
9} from "@codemirror/view";
10
11export function highlighting(path: string, render = highlight): Extension {
12  const set = StateEffect.define<DecorationSet>();
13  const tokens = StateField.define<DecorationSet>({
14    create: () => Decoration.none,
15    update(value, transaction) {
16      for (const effect of transaction.effects) if (effect.is(set)) return effect.value;
17      return value.map(transaction.changes);
18    },
19    provide: (field) => EditorView.decorations.from(field),
20  });
21  return [
22    tokens,
23    ViewPlugin.fromClass(
24      class {
25        timer: ReturnType<typeof setTimeout>;
26        disposed = false;
27        constructor(readonly view: EditorView) {
28          this.timer = setTimeout(() => void this.light(), 0);
29        }
30        update(update: ViewUpdate) {
31          if (!update.docChanged) return;
32          clearTimeout(this.timer);
33          this.timer = setTimeout(() => void this.light(), 150);
34        }
35        async light() {
36          const doc = this.view.state.doc;
37          const lines = await render(doc.toString(), { path }).catch(() => null);
38          if (!lines || this.disposed || this.view.state.doc !== doc) return;
39          const marks: Range<Decoration>[] = [];
40          for (let i = 0; i < Math.min(lines.length, doc.lines); i++) {
41            let at = doc.line(i + 1).from;
42            const visit = (node: Node) => {
43              const start = at;
44              if (node.nodeType === 3) at += node.textContent!.length;
45              else {
46                for (const child of node.childNodes) visit(child);
47                if (node instanceof Element && /^a-[a-z]+$/.test(node.localName) && start < at) {
48                  marks.push(Decoration.mark({ tagName: node.localName }).range(start, at));
49                }
50              }
51            };
52            for (const node of lines[i]) visit(node);
53          }
54          this.view.dispatch({ effects: set.of(Decoration.set(marks, true)) });
55        }
56        destroy() {
57          this.disposed = true;
58          clearTimeout(this.timer);
59        }
60      },
61    ),
62  ];
63}