Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions apps/web/src/components/diffs/StyledDiffCodeView.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {
CodeView,
FileDiff,
FileRenderer,
getSharedHighlighter,
type CodeViewItem,
type CodeViewScrollTarget,
type FileContents,
} from "@pierre/diffs";
import { useWorkerPool, type CodeViewProps } from "@pierre/diffs/react";
import { WorkerPoolManager, type WorkerRequest, type WorkerResponse } from "@pierre/diffs/worker";
import DiffsWorker from "@pierre/diffs/worker/worker.js?worker";
import { getDiffLineStat, getRenderablePatch } from "../../lib/diffRendering";
import {
act,
StrictMode,
Expand Down Expand Up @@ -305,6 +310,157 @@ describe("code-view worker lifecycle", () => {
});
});

describe("collapsed diff content", () => {
let pool: WorkerPoolManager;
let container: HTMLDivElement;
let viewer: FileDiff<string> | CodeView<string> | undefined;

const loadedDiff = () => {
const parsed = getRenderablePatch(
[
"diff --git a/before.ts b/after.ts",
"similarity index 50%",
"rename from before.ts",
"rename to after.ts",
"--- a/before.ts",
"+++ b/after.ts",
"@@ -1,2 +1,2 @@",
" const shared = 1;",
"-const before = 2;",
"+const after = 3;",
].join("\n"),
"collapsed-content",
);
if (parsed?.kind !== "files" || !parsed.files[0]) throw new Error("Expected diff");
return parsed.files[0];
};
const code = () =>
[...container.querySelectorAll("diffs-container")]
.flatMap((element) => [...(element.shadowRoot?.querySelectorAll("[data-line]") ?? [])])
.map((line) => line.textContent)
.join("\n");

beforeEach(async () => {
testState.requests = [];
testState.failWorkers = false;
testState.holdInitialization = false;
pool = new WorkerPoolManager(
{ workerFactory: () => new DiffsWorker(), poolSize: 1 },
{ theme: "pierre-dark", langs: ["typescript"], preferredHighlighter: "shiki-wasm" },
);
await pool.initialize();
container = document.createElement("div");
container.style.cssText = "height: 400px; width: 800px; overflow: auto";
document.body.append(container);
});

afterEach(() => {
viewer?.cleanUp();
viewer = undefined;
pool.terminate();
container.remove();
vi.restoreAllMocks();
});

it("defers plain AST and highlight work across collapsed renders until expansion", async () => {
const fileDiff = loadedDiff();
const highlight = vi.spyOn(pool, "highlightDiffAST");
const plainAST = vi.spyOn(pool, "getPlainDiffAST");
const options = { theme: "pierre-dark", collapsed: true, disableErrorHandling: true } as const;
const file = new FileDiff<string>(options, pool);
viewer = file;
file.render({ fileDiff, containerWrapper: container });
file.render({ fileDiff, forceRender: true });
expect.soft(highlight.mock.calls.length).toBe(0);
expect.soft(plainAST.mock.calls.length).toBe(0);
expect(code()).toBe("");
expect(container.querySelector("diffs-container")?.shadowRoot?.textContent).toContain(
"after.ts",
);

file.setOptions({ ...options, collapsed: false });
file.render({ fileDiff, forceRender: true });
expect(highlight).toHaveBeenCalled();
await pool.primeDiffHighlightCache(fileDiff);
await vi.waitFor(() => expect(code()).toContain("const after = 3;"));
expect(code()).toContain("const before = 2;");
expect(testState.requests.filter((request) => request.type === "diff")).toHaveLength(1);
});

it("finds newly loaded collapsed lines and unfolds the match with its annotation and header", async () => {
const fileDiff = loadedDiff();
const item: CodeViewItem<string> = {
type: "diff",
id: "renamed",
fileDiff,
collapsed: true,
annotations: [{ side: "additions", lineNumber: 2, metadata: "Review this change" }],
};
const view = new CodeView<string>(
{
theme: "pierre-dark",
disableErrorHandling: true,
onRevealSearchMatch: (match) =>
view.setItems([{ ...match, collapsed: false, version: (match.version ?? 0) + 1 }]),
renderHeaderMetadata: (file) => {
if (!("hunks" in file)) return;
const metadata = document.createElement("span");
const { additions, deletions } = getDiffLineStat([file]);
metadata.textContent = "+" + additions + " / -" + deletions;
return metadata;
},
renderAnnotation: (annotation) => {
const note = document.createElement("span");
note.textContent = annotation.metadata ?? "";
return note;
},
},
pool,
);
viewer = view;
view.setup(container);
view.openSearch();
const search = container.querySelector("[data-diffs-search]")!.shadowRoot!;
const input = search.querySelector<HTMLInputElement>("input[data-search]")!;
input.value = "const after";
input.dispatchEvent(new Event("input", { bubbles: true }));
view.setItems([item]);
view.render(true);
expect(code()).toBe("");
expect(search.textContent).toContain("1 results");
expect(container.textContent).toContain("+1 / -1");

input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
view.render(true);
await vi.waitFor(() => expect(code()).toContain("const after = 3;"));
expect(search.textContent).toContain("1 of 1");
expect(container.textContent).toContain("Review this change");
expect(container.textContent).toContain("+1 / -1");
expect(container.querySelector("diffs-container")?.shadowRoot?.textContent).toContain(
"after.ts",
);

await pool.primeDiffHighlightCache(fileDiff);
view.render(true);
const highlight = vi.spyOn(pool, "highlightDiffAST");
const plainAST = vi.spyOn(pool, "getPlainDiffAST");
const updatedDiff = { ...fileDiff, name: "updated.ts", cacheKey: "collapsed-update" };
view.setItems([{ ...item, fileDiff: updatedDiff, version: 2 }]);
view.render(true);
expect(highlight).not.toHaveBeenCalled();
expect(plainAST).not.toHaveBeenCalled();
expect(code()).toBe("");
expect(search.textContent).toContain("1 of 1");
expect(container.querySelector("diffs-container")?.shadowRoot?.textContent).toContain(
"updated.ts",
);
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
view.render(true);
await vi.waitFor(() => expect(code()).toContain("const after = 3;"));
expect(container.textContent).toContain("Review this change");
});
});

type RevealHandle = NonNullable<Parameters<typeof useCodeViewFileReveal>[0]>;
const revealCalls: {
scope: string;
Expand Down
59 changes: 59 additions & 0 deletions patches/@pierre%2Fdiffs@1.5.2.patch
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,43 @@ index 0000000..29370ad
+export { CodeViewSearch, collectMatches };
+
+//# sourceMappingURL=CodeViewSearch.js.map
diff --git a/dist/components/FileDiff.js b/dist/components/FileDiff.js
index 6f66bd3..3125fac 100644
--- a/dist/components/FileDiff.js
+++ b/dist/components/FileDiff.js
@@ -1,5 +1,6 @@
-import { CUSTOM_HEADER_SLOT_ID, DEFAULT_THEMES, EMPTY_RENDER_RANGE, HEADER_FILENAME_SUFFIX_SLOT_ID, HEADER_METADATA_SLOT_ID, HEADER_PREFIX_SLOT_ID } from "../constants.js";
+import { CUSTOM_HEADER_SLOT_ID, DEFAULT_THEMES, HEADER_FILENAME_SUFFIX_SLOT_ID, HEADER_METADATA_SLOT_ID, HEADER_PREFIX_SLOT_ID } from "../constants.js";
import { getThemes } from "../utils/getThemes.js";
+import { getHighlighterThemeStyles } from "../utils/getHighlighterThemeStyles.js";
import { getHighlighterIfLoaded, getSharedHighlighter } from "../highlighter/shared_highlighter.js";
import { dequeueRender, queueRender } from "../managers/UniversalRenderingManager.js";
import { areThemesEqual } from "../utils/areThemesEqual.js";
@@ -691,11 +692,20 @@ var FileDiff = class {
this.removeRenderedCode();
this.clearAuxiliaryNodes();
try {
- const hunksResult = this.hunksRenderer.renderDiff(latestDiff, EMPTY_RENDER_RANGE);
- if (hunksResult != null) this.applyThemeState(fileContainer, hunksResult.themeStyles, themeType, hunksResult.baseThemeType);
- if (hunksResult?.headerElement != null) this.applyHeaderToDOM(hunksResult.headerElement, fileContainer, hunksResult.fileDiff);
+ // An empty render range still highlights the full diff. Headers only need the theme.
+ const theme = this.getTheme();
+ const highlighter = getHighlighterIfLoaded({ theme, lang: "text" });
+ if (highlighter != null) this.applyThemeState(fileContainer, getHighlighterThemeStyles({ theme, highlighter }), themeType, typeof theme === "string" ? highlighter.getTheme(theme).type : void 0);
+ else getSharedHighlighter({
+ themes: getThemes(theme),
+ langs: ["text"],
+ preferredHighlighter: this.workerManager?.getPreferredHighlighter() ?? this.options.preferredHighlighter
+ }).then(() => {
+ if (this.enabled && this.fileContainer === fileContainer && this.options.collapsed && areThemesEqual(theme, this.getTheme())) this.rerender();
+ }).catch((error) => console.error(error));
+ if (!disableFileHeader) this.applyHeaderToDOM(this.hunksRenderer.renderHeader(latestDiff), fileContainer, latestDiff);
this.renderSeparators([]);
- this.renderedDiff = hunksResult?.fileDiff ?? latestDiff;
+ this.renderedDiff = latestDiff;
this.injectUnsafeCSS();
} catch (error) {
if (disableErrorHandling) throw error;
diff --git a/dist/components/VirtualizedFile.d.ts b/dist/components/VirtualizedFile.d.ts
index 5e68e35..79dcd88 100644
--- a/dist/components/VirtualizedFile.d.ts
Expand Down Expand Up @@ -773,6 +810,28 @@ index d1f57cd..5908ad7 100644
const file = this.getRenderedFile();
if (!this.isSimpleMode()) this.computeApproximateSize(true);
else if (shouldUpdateBuffer && previousRenderRange != null && file != null) {
diff --git a/dist/components/VirtualizedFileDiff.js b/dist/components/VirtualizedFileDiff.js
index 96f970f..1bb59c6 100644
--- a/dist/components/VirtualizedFileDiff.js
+++ b/dist/components/VirtualizedFileDiff.js
@@ -552,7 +552,7 @@ var VirtualizedFileDiff = class extends FileDiff {
rerender() {
if (!this.enabled || this.fileDiff == null && this.additionFile == null && this.deletionFile == null) return;
const latestDiff = this.getLatestDiff();
- const nextRenderDiff = latestDiff == null ? void 0 : this.hunksRenderer.getDiffForNextRender(latestDiff);
+ const nextRenderDiff = latestDiff == null || this.options.collapsed === true ? latestDiff : this.hunksRenderer.getDiffForNextRender(latestDiff);
if (this.isSimpleMode()) this.pendingRender = void 0;
this.forceRenderOverride = true;
this.virtualizer.instanceChanged(this, !areDiffTargetsEqual(this.getRenderedDiff(), nextRenderDiff));
@@ -724,7 +724,7 @@ var VirtualizedFileDiff = class extends FileDiff {
const previousRenderedDiff = this.getRenderedDiff();
const { pendingRender } = this;
const previousLayoutDiff = pendingRender?.diff ?? previousRenderedDiff;
- const pendingRenderDiff = this.hunksRenderer.getDiffForNextRender(latestDiff);
+ const pendingRenderDiff = this.options.collapsed === true ? latestDiff : this.hunksRenderer.getDiffForNextRender(latestDiff);
this.pendingRender = {
latestDiff,
diff: pendingRenderDiff
diff --git a/dist/components/web-components.js b/dist/components/web-components.js
index 6e7b486..5b7290f 100644
--- a/dist/components/web-components.js
Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading