723 lines · 23.5 KB · TypeScript
RawDownload
import {  getSharedHighlighter,  VirtualizedFile,  Virtualizer,  type FileContents,} from "@pierre/diffs";import { Editor, TextDocument } from "@pierre/diffs/editor";import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const renderingManagerUrl = new URL(  "./managers/UniversalRenderingManager.js",  import.meta.resolve("@pierre/diffs"),);const { clearRenderQueue } = (await import(/* @vite-ignore */ renderingManagerUrl.href)) as {  clearRenderQueue(): void;}; // Layout measurements are controlled here. The real reconciler, document and// renderer calculate positions. This does not simulate native CSS wrapping.class MeasuredElement {  static geometryReads = 0;  children: MeasuredElement[] = [];  dataset: Record<string, string> = {};  nextElementSibling: MeasuredElement | null = null;  width = 283;   constructor(readonly height = 0) {}   getBoundingClientRect() {    MeasuredElement.geometryReads += 1;    return { top: 0, height: this.height, width: this.width };  }} class MeasuredCodeElement extends MeasuredElement {  readonly tagName = "CODE";   get firstElementChild() {    return this.children[0] ?? null;  }} const observers: RecordedResizeObserver[] = [];const animationFrames = new Map<number, FrameRequestCallback>();let nextFrameId = 0; class RecordedResizeObserver {  readonly targets = new Set<Element>();   constructor(readonly callback: ResizeObserverCallback) {    observers.push(this);  }   observe(target: Element) {    this.targets.add(target);  }   unobserve(target: Element) {    this.targets.delete(target);  }   disconnect() {    this.targets.clear();  }   deliver(target: HTMLElement) {    const rect = target.getBoundingClientRect();    const size = { inlineSize: rect.width, blockSize: rect.height };    this.callback(      [        {          target,          contentRect: rect,          contentBoxSize: [size],          borderBoxSize: [size],          devicePixelContentBoxSize: [size],        },      ],      this as unknown as ResizeObserver,    );  }} function drainRenderFrames() {  const errors = vi.spyOn(console, "error");  try {    for (let frame = 0; animationFrames.size > 0; frame += 1) {      if (frame === 10) throw new Error("The production render queue did not settle");      const callbacks = [...animationFrames.values()];      animationFrames.clear();      for (const callback of callbacks) callback(frame);    }    expect(errors).not.toHaveBeenCalled();  } finally {    errors.mockRestore();  }} function measuredElement(element: MeasuredElement): HTMLElement {  return element as unknown as HTMLElement;} class LayoutVirtualizer extends Virtualizer {  override getOffsetInScrollContainer(_element: HTMLElement) {    return 0;  }} class MeasuredFile extends VirtualizedFile {  override top = 0;   override attachEditor(editor: Parameters<VirtualizedFile["attachEditor"]>[0]) {    this.editor = editor;    return () => {      this.editor = undefined;    };  }   async initialize(file: FileContents) {    this.prepareCodeViewItem(file, 0);    await this.fileRenderer.initializeHighlighter();    expect(      this.fileRenderer.renderFile(file, {        startingLine: 5950,        totalLines: 51,        bufferBefore: 0,        bufferAfter: 0,      }),    ).toBeDefined();    this.fileContainer = measuredElement(new MeasuredElement());  }   measure(    rows: ReadonlyArray<readonly [lineIndex: number, height: number]>,    contentWidth = 226.25,  ) {    const content = new MeasuredElement();    content.width = contentWidth;    content.children = rows.map(([lineIndex, height]) => {      const row = new MeasuredElement(height);      row.dataset.lineIndex = String(lineIndex);      return row;    });    const code = new MeasuredCodeElement();    code.width = contentWidth + 56.75;    code.children = [new MeasuredElement(), content];    this.code = measuredElement(code);    this.reconcileHeights();  }   resizeContent(contentWidth: number, codeWidth = contentWidth + 56.75) {    const code = this.code;    const content = code?.children[1];    if (!(code instanceof MeasuredElement) || !(content instanceof MeasuredElement)) {      throw new Error("Expected measured code and content");    }    code.width = codeWidth;    content.width = contentWidth;  }   observeLayout() {    const pre = new MeasuredElement();    if (!(this.code instanceof MeasuredElement)) throw new Error("Expected measured code");    pre.children = [this.code];    this.resizeManager.setup(measuredElement(pre) as HTMLPreElement, {      disableAnnotations: true,      columnVariables: "measure",    });    const code = this.code;    const observer = observers.find((candidate) => candidate.targets.has(code));    if (observer === undefined) throw new Error("The real resize manager did not observe code");    return () => observer.deliver(code);  }   dispose() {    this.fileContainer = undefined;    this.code = undefined;    this.cleanUp();  }} const instances: MeasuredFile[] = [];const editors: Editor<undefined>[] = []; beforeAll(async () => {  await getSharedHighlighter({    themes: ["pierre-dark"],    langs: ["text"],    preferredHighlighter: "shiki-wasm",  });}); beforeEach(() => {  observers.length = 0;  animationFrames.clear();  MeasuredElement.geometryReads = 0;  vi.stubGlobal("HTMLElement", MeasuredElement);  vi.stubGlobal("Document", MeasuredElement);  vi.stubGlobal("ResizeObserver", RecordedResizeObserver);  vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {    const id = ++nextFrameId;    animationFrames.set(id, callback);    return id;  });  vi.stubGlobal("cancelAnimationFrame", (id: number) => animationFrames.delete(id));}); afterEach(() => {  for (const editor of editors.splice(0)) editor.cleanUp();  for (const instance of instances.splice(0)) instance.dispose();  clearRenderQueue();  animationFrames.clear();  vi.unstubAllGlobals();}); async function makeFixture(  overflow: "wrap" | "scroll" = "wrap",  lineCount = 6001,  contentWidth = 226.25,) {  const contents = Array.from({ length: lineCount }, (_, index) => `line ${index}`).join("\n");  const file: FileContents = {    name: "wrapped.txt",    contents,    cacheKey: `wrapped:${overflow}`,    lang: "text",  };  const document = new TextDocument(file.name, contents, "text");  const instance = new MeasuredFile(    {      overflow,      disableFileHeader: true,      theme: "pierre-dark",      preferredHighlighter: "shiki-wasm",      useTokenTransformer: true,      controlledSelection: true,    },    new LayoutVirtualizer(),  );  instances.push(instance);  await instance.initialize(file);  instance.measure(    [      [0, 80],      [120, 60],      [4999, 100],      [5000, 80],      [5999, 100],      [6000, 60],    ],    contentWidth,  );  const apply = (change: { startLine: number } | undefined, passStartLine = true) => {    if (change === undefined) throw new Error("Expected a document change");    file.contents = document.getText();    instance.applyDocumentChange(      document,      undefined,      false,      passStartLine ? change.startLine : undefined,    );  };  const append = () => {    const position = document.positionAt(document.getText().length);    apply(document.applyEdits([{ range: { start: position, end: position }, newText: "\n" }]));  };  return { instance, document, file, apply, append };} describe("wrapped editor document changes", () => {  it("preserves the position above an EOF insertion across layout checkpoints", async () => {    const { instance, document, append } = await makeFixture();    const previousLastLine = document.lineCount;    const before = instance.getLinePosition(previousLastLine);    expect(before).toEqual({ top: 120328, height: 60 });    const viewport = { top: before!.top - 100, bottom: before!.top + 80 };    expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2156 });     append();     expect(document.lineCount).toBe(previousLastLine + 1);    expect(instance.getLinePosition(previousLastLine)).toEqual({ top: before!.top, height: 20 });    expect(instance.getLinePosition(document.lineCount)).toEqual({ top: 120348, height: 20 });    expect(instance.getVirtualizedHeight()).toBe(120376);    expect(instance.getAdvancedStickySpecs(viewport)).toEqual({ topOffset: 118240, height: 2136 });  });   it("invalidates changed and shifted rows after an insertion in the middle", async () => {    const { instance, document, apply } = await makeFixture();    const before = instance.getLinePosition(5001);    const position = { line: 5000, character: 2 };    apply(document.applyEdits([{ range: { start: position, end: position }, newText: "\n" }]));     expect(instance.getLinePosition(5001)).toEqual({ top: before!.top, height: 20 });    expect(instance.getLineHeight(4999)).toBe(100);    expect(instance.getLineHeight(5000)).toBe(20);    expect(instance.getLineHeight(5999)).toBe(20);    expect(instance.getLinePosition(document.lineCount)).toEqual({ top: 120208, height: 20 });  });   it("keeps preceding measurements when a deletion crosses a checkpoint", async () => {    const { instance, document, apply } = await makeFixture();    const before = instance.getLinePosition(5000);    apply(      document.applyEdits([        {          range: { start: { line: 4999, character: 2 }, end: { line: 5001, character: 2 } },          newText: "",        },      ]),    );     expect(document.lineCount).toBe(5999);    expect(instance.getLinePosition(5000)).toEqual({ top: before!.top, height: 20 });    expect(instance.getLineHeight(120)).toBe(60);    expect(instance.getLineHeight(4999)).toBe(20);    expect(instance.getLineHeight(6000)).toBe(20);    expect(instance.getLinePosition(document.lineCount)).toEqual({ top: 120068, height: 20 });  });   it("uses the earliest changed line for edits at multiple selections", async () => {    const { instance, document, apply } = await makeFixture();    const before = instance.getLinePosition(121);    apply(      document.applyEdits(        [120, 5000].map((line) => ({          range: { start: { line, character: 2 }, end: { line, character: 2 } },          newText: "\n",        })),      ),    );     expect(document.lineCount).toBe(6003);    expect(instance.getLinePosition(121)).toEqual({ top: before!.top, height: 20 });    expect(instance.getLineHeight(0)).toBe(80);    expect(instance.getLineHeight(120)).toBe(20);    expect(instance.getLineHeight(4999)).toBe(20);  });   it("retains the unchanged prefix through repeated Enter, undo and redo", async () => {    const { instance, document, apply, append } = await makeFixture();    const before = instance.getLinePosition(6001);    for (let count = 0; count < 60; count += 1) append();    expect(document.lineCount).toBe(6061);    expect(instance.getLinePosition(6001)?.top).toBe(before!.top);    apply(document.undo()?.[0]);    expect(instance.getLinePosition(6001)?.top).toBe(before!.top);    apply(document.redo()?.[0]);    expect(document.lineCount).toBe(6061);    expect(instance.getLinePosition(6001)?.top).toBe(before!.top);  });   it("keeps unwrapped positions unchanged", async () => {    const { instance, append } = await makeFixture("scroll");    const before = instance.getLinePosition(6001);    append();    expect(instance.getLinePosition(6001)).toEqual(before);    expect(instance.getLinePosition(6002)).toEqual({ top: 120028, height: 20 });  });   it("fully invalidates measurements when the first changed line is unknown", async () => {    const { instance, document, apply } = await makeFixture();    const position = document.positionAt(document.getText().length);    apply(      document.applyEdits([{ range: { start: position, end: position }, newText: "\n" }]),      false,    );    expect(instance.getLinePosition(6001)).toEqual({ top: 120008, height: 20 });    expect(instance.getLineHeight(0)).toBe(20);  });   it("still discards all measured rows after a metric change", async () => {    const { instance, file, append } = await makeFixture();    append();    instance.setMetrics({ hunkLineCount: 50, lineHeight: 24, diffHeaderHeight: 44, spacing: 8 });    instance.prepareCodeViewItem(file, 0);    expect(instance.getLinePosition(6001)).toEqual({ top: 144008, height: 24 });  });   it("still discards all measured rows when annotations change", async () => {    const { instance, file, append } = await makeFixture();    append();    instance.setLineAnnotations([{ lineNumber: 10, metadata: undefined }]);    instance.prepareCodeViewItem(file, 0);    expect(instance.getLinePosition(6001)).toEqual({ top: 120008, height: 20 });  });}); describe("wrapped measurement widths", () => {  it.each([    [226.25, 482.25],    [482.25, 226.25],  ])(    "drops offscreen measurements when content changes from %spx to %spx",    async (before, after) => {      const { instance } = await makeFixture("wrap", 6001, before);      instance.measure([[6000, 60]], after);      expect(instance.getLineHeight(0)).toBe(20);      expect(instance.getLineHeight(120)).toBe(20);      expect(instance.getLineHeight(6000)).toBe(60);      expect(instance.getVirtualizedHeight()).toBe(120076);    },  );   it.each([    [226.25, 482.25],    [482.25, 226.25],  ])(    "does not retain old-width prefix heights after a %spx to %spx resize and edit",    async (before, after) => {      const { instance, append } = await makeFixture("wrap", 6001, before);      instance.measure([[6000, 60]], after);      append();      expect(instance.getLineHeight(0)).toBe(20);      expect(instance.getLineHeight(120)).toBe(20);      expect(instance.getLinePosition(6001)).toEqual({ top: 120008, height: 20 });    },  );   it("repairs an edit before resize delivery when the real resize and render queues drain", async () => {    const { instance, append } = await makeFixture();    instance.measure([[6000, 60]]);    const deliverResize = instance.observeLayout();    instance.resizeContent(482.25);    const readsBeforeEdit = MeasuredElement.geometryReads;    append();    expect(MeasuredElement.geometryReads).toBe(readsBeforeEdit);    // No synchronous geometry read: the resize entry owns invalidation.    expect(instance.getLineHeight(0)).toBe(80);    deliverResize();    drainRenderFrames();    expect(instance.getLineHeight(0)).toBe(20);    expect(instance.getLinePosition(6001)).toEqual({ top: 120008, height: 60 });  });   it("keeps measured prefixes through same-width reconciliation and editing", async () => {    const { instance, append } = await makeFixture();    instance.measure([[6000, 60]]);    append();    expect(instance.getLineHeight(0)).toBe(80);    expect(instance.getLineHeight(120)).toBe(60);    expect(instance.getLinePosition(6001)).toEqual({ top: 120328, height: 20 });  });   it("preserves measurements on first and repeated same-width resize deliveries", async () => {    const { instance } = await makeFixture();    const before = instance.getVirtualizedHeight();    const deliverResize = instance.observeLayout();    deliverResize();    deliverResize();    drainRenderFrames();    expect(instance.getLineHeight(0)).toBe(80);    expect(instance.getVirtualizedHeight()).toBe(before);  });   it.each([    [226.25, 482.25],    [482.25, 226.25],  ])("handles a %spx to %spx resize before first observer delivery", async (before, after) => {    const { instance } = await makeFixture("wrap", 6001, before);    instance.measure([[6000, 20]], before);    const deliverResize = instance.observeLayout();    instance.resizeContent(after);    deliverResize();    drainRenderFrames();    expect(instance.getLineHeight(0)).toBe(20);    expect(instance.getVirtualizedHeight()).toBe(120036);  });   it("keeps width validity when a new editor attaches to the same file", async () => {    const { instance } = await makeFixture();    instance.measure([[6000, 20]]);    const deliverResize = instance.observeLayout();    vi.stubGlobal("SVGSVGElement", EditorElement);    vi.stubGlobal(      "document",      Object.assign(new EditorElement(), { createElement: () => new EditorElement() }),    );    const first = new Editor<undefined>();    editors.push(first);    first.edit(instance);    first.cleanUp();    const second = new Editor<undefined>();    editors.push(second);    second.edit(instance);    instance.resizeContent(482.25);    deliverResize();    drainRenderFrames();    expect(instance.getLineHeight(0)).toBe(20);    expect(instance.getVirtualizedHeight()).toBe(120036);  });   it("ignores stale resize deliveries after cleanup", async () => {    const { instance } = await makeFixture();    const deliverResize = instance.observeLayout();    instance.dispose();    clearRenderQueue();    animationFrames.clear();    deliverResize();    expect(animationFrames.size).toBe(0);

Lines 1–500 of 723.