char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somperf(diff): virtualize rich rows with searchable text fallbacksece94e9

main
8.4 KiB230 linesraw
1import { assert, assertEquals } from "@std/assert";
2import { withDom } from "@berry/util/dom.ts";
3import { rendering } from "./render.tsx";
4import type { FileDiff } from "./types.ts";
5
6const file = (lines: string[], path = "file.txt"): FileDiff => ({
7  path,
8  from: null,
9  status: "modified",
10  modes: { old: "100644", new: "100644" },
11  binary: false,
12  hunks: [{ old: 1, new: 1, lines }],
13});
14
15async function page(
16  test: (env: {
17    root: HTMLElement;
18    render: ReturnType<typeof rendering>;
19    intersect(element: Element, near: boolean, visible?: boolean): void;
20    settle(): Promise<void>;
21  }) => Promise<void>,
22  globals: Record<string, unknown> = {},
23) {
24  const observers: Visibility[] = [];
25  class Visibility {
26    targets = new Set<Element>();
27    constructor(
28      readonly callback: IntersectionObserverCallback,
29      readonly options?: IntersectionObserverInit,
30    ) {
31      observers.push(this);
32    }
33    observe(target: Element) {
34      this.targets.add(target);
35    }
36    unobserve(target: Element) {
37      this.targets.delete(target);
38    }
39    disconnect() {
40      this.targets.clear();
41    }
42  }
43  await withDom(async (window) => {
44    const root = document.createElement("div");
45    document.body.append(root);
46    const render = rendering(root);
47    try {
48      await test({
49        root,
50        render,
51        intersect(element, near, visible = near) {
52          for (const observer of observers) {
53            if (!observer.targets.has(element)) continue;
54            observer.callback(
55              [{
56                target: element,
57                isIntersecting: observer.options?.rootMargin ? near : visible,
58              } as IntersectionObserverEntry],
59              observer as unknown as IntersectionObserver,
60            );
61          }
62        },
63        settle: () => window.happyDOM.waitUntilComplete(),
64      });
65    } finally {
66      render.dispose();
67    }
68    for (const observer of observers) assertEquals(observer.targets.size, 0);
69  }, { globals: { IntersectionObserver: Visibility, ...globals } });
70}
71
72Deno.test("large diffs keep all text searchable but only nearby chunks have rich rows", () =>
73  page(async ({ root, render, intersect, settle }) => {
74    const lines = Array.from({ length: 10_000 }, (_, i) => `+unique line ${i}`);
75    const content = render.file(file(lines), "unified", new Set(), () => {});
76    root.append(content.element);
77    const chunks = [...root.querySelectorAll(".diff-chunk")];
78    assert(chunks.length > 10);
79    assertEquals(root.querySelectorAll("table").length, 0);
80    assert(root.textContent!.includes("unique line 9999"));
81    assert(root.querySelectorAll("*").length < 1000);
82
83    let previous: Element | undefined;
84    for (const chunk of [chunks[0], chunks[1], chunks.at(-1)!]) {
85      if (previous) intersect(previous, false);
86      intersect(chunk, true);
87      await settle();
88      assertEquals(root.querySelectorAll("table").length, 1);
89      assert(root.querySelectorAll("tr").length < 200);
90      assert(root.textContent!.includes("unique line 9999"));
91      if (previous) assert(previous.querySelector("pre"));
92      previous = chunk;
93    }
94    intersect(chunks[0], true);
95    content.dispose();
96    await settle();
97    assertEquals(chunks[0].querySelector("table"), null);
98  }));
99
100Deno.test("hydrating and evicting chunks leave native find matches and selections intact", () =>
101  page(async ({ root, render, intersect, settle }) => {
102    const content = render.file(file(["+a searchable needle"]), "unified", new Set(), () => {});
103    root.append(content.element);
104    const chunk = root.querySelector(".diff-chunk")!;
105    const pre = chunk.querySelector("pre")!;
106    const selection = document.getSelection()!;
107    const range = document.createRange();
108    range.setStart(pre.firstChild!, 2);
109    range.setEnd(pre.firstChild!, 12);
110    selection.addRange(range);
111    intersect(chunk, true);
112    await settle();
113    assertEquals(chunk.querySelector("pre"), pre);
114    assertEquals(selection.toString(), "searchable");
115
116    selection.removeAllRanges();
117    document.dispatchEvent(new Event("selectionchange"));
118    await settle();
119    const table = chunk.querySelector("table")!;
120    assert(table);
121    range.selectNodeContents(table.querySelector("td.text")!);
122    selection.addRange(range);
123    intersect(chunk, false);
124    await settle();
125    assertEquals(chunk.querySelector("table"), table);
126    assertEquals(selection.toString(), "a searchable needle");
127    selection.removeAllRanges();
128    document.dispatchEvent(new Event("selectionchange"));
129    await settle();
130    assertEquals(chunk.querySelector("table"), null);
131    assertEquals(chunk.querySelector("pre")!.textContent, "a searchable needle\n");
132    content.dispose();
133  }));
134
135Deno.test("line pairing and word changes survive chunk boundaries in either layout", async () => {
136  for (const layout of ["unified", "split"] as const) {
137    await page(async ({ root, render, intersect, settle }) => {
138      const lines = [
139        ...Array.from({ length: 150 }, (_, i) => `-const before_${i} = value;`),
140        ...Array.from({ length: 150 }, (_, i) => `+const after_${i} = value;`),
141      ];
142      const content = render.file(file(lines), layout, new Set(), () => {});
143      root.append(content.element);
144      for (const chunk of root.querySelectorAll(".diff-chunk")) intersect(chunk, true);
145      await settle();
146      assertEquals(root.querySelectorAll("td.text.del").length, 150);
147      assertEquals(root.querySelectorAll("td.text.add").length, 150);
148      assertEquals(root.querySelector("td.del mark.word")!.textContent, "before_0");
149      assertEquals(root.querySelector("td.add mark.word")!.textContent, "after_0");
150      if (layout === "split") {
151        for (const [i, row] of [...root.querySelectorAll("tr")].entries()) {
152          assertEquals(row.querySelector(".del")!.textContent, `const before_${i} = value;`);
153          assertEquals(row.querySelector(".add")!.textContent, `const after_${i} = value;`);
154        }
155      }
156      content.dispose();
157    });
158  }
159});
160
161Deno.test("syntax work waits for visible chunks, skips departed files, and releases offscreen results", async () => {
162  const requests: { id: number; source: string }[] = [];
163  let worker: Highlighter | undefined;
164  class Highlighter {
165    onmessage?: (event: { data: { id: number; html: string } }) => void;
166    constructor() {
167      worker = this;
168    }
169    postMessage(request: typeof requests[number]) {
170      requests.push(request);
171    }
172    reply() {
173      for (const { id, source } of requests.splice(0)) {
174        this.onmessage?.({ data: { id, html: `<a-k>${source}</a-k>` } });
175      }
176    }
177  }
178  await page(async ({ root, render, intersect, settle }) => {
179    const first = render.file(file(["-old", "+new"], "first.ts"), "unified", new Set(), () => {});
180    const second = render.file(file(["+second"], "second.ts"), "unified", new Set(), () => {});
181    root.append(first.element, second.element);
182    const a = first.element.querySelector(".diff-chunk")!;
183    const b = second.element.querySelector(".diff-chunk")!;
184    intersect(a, true, false);
185    await settle();
186    assertEquals(worker, undefined);
187    intersect(a, true);
188    intersect(b, true);
189    await settle();
190    assertEquals(requests.map((request) => request.source), ["old", "new"]);
191    intersect(a, false);
192    intersect(b, false);
193    await settle();
194    worker!.reply();
195    await settle();
196    assertEquals(requests.length, 0);
197    assertEquals(root.querySelectorAll("a-k, table").length, 0);
198
199    intersect(a, true);
200    await settle();
201    assertEquals(requests.map((request) => request.source), ["old", "new"]);
202    const table = a.querySelector("table")!;
203    const selection = document.getSelection()!;
204    const range = document.createRange();
205    range.selectNodeContents(a.querySelector("td.text")!);
206    selection.addRange(range);
207    worker!.reply();
208    await settle();
209    assertEquals(a.querySelector("table"), table);
210    assertEquals(a.querySelector("a-k"), null);
211    assertEquals(selection.toString(), "old");
212    selection.removeAllRanges();
213    document.dispatchEvent(new Event("selectionchange"));
214    await settle();
215    assertEquals(a.querySelector("a-k")?.textContent, "old");
216    intersect(a, true);
217    await settle();
218    assertEquals(requests.length, 0);
219
220    intersect(b, true);
221    await settle();
222    assertEquals(requests.map((request) => request.source), ["second"]);
223    second.dispose();
224    second.element.remove();
225    worker!.reply();
226    await settle();
227    assertEquals(second.element.querySelector("a-k"), null);
228    first.dispose();
229  }, { Worker: Highlighter });
230});