Skip to content
Open
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
12 changes: 7 additions & 5 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { useTheme } from "../hooks/useTheme";
import {
buildFileDiffContentVersion,
buildFileDiffIdentityKey,
getCollapsedFileDiffStub,
getDiffCollapseIconClassName,
getDiffLineStat,
getRenderablePatch,
Expand Down Expand Up @@ -478,15 +479,16 @@ export default function DiffPanel({
renderableFileEntries
.filter(({ fileDiff }) => !lazySource || readyFilePaths.has(resolveFileDiffPath(fileDiff)))
.map(({ fileDiff, fileKey, fileVersion }) => {
const pending = fileDiff.cacheKey?.endsWith(":pending") === true;
// Header-only placeholders use the viewer's collapsed geometry until their patch arrives.
const collapsed = collapsedDiffFileKeys.has(fileKey) || pending;
return {
fileDiff,
fileDiff:
lazySource && collapsed && !pending ? getCollapsedFileDiffStub(fileDiff) : fileDiff,
filePath: resolveFileDiffPath(fileDiff),
fileKey,
fileVersion,
// Header-only placeholders use the viewer's collapsed geometry until their patch arrives.
collapsed:
collapsedDiffFileKeys.has(fileKey) ||
fileDiff.cacheKey?.endsWith(":pending") === true,
collapsed,
};
}),
[collapsedDiffFileKeys, renderableFileEntries, lazySource, readyFilePaths],
Expand Down
79 changes: 79 additions & 0 deletions apps/web/src/lib/diffRendering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
buildFileDiffIdentityKey,
buildFileDiffRenderKey,
buildPatchCacheKey,
getCollapsedFileDiffStub,
getDiffLineStat,
getRenderablePatch,
resolveFileDiffPath,
Expand Down Expand Up @@ -426,3 +427,81 @@ describe("a file whose name a patch header cannot carry plainly", () => {
expect(resolveFileDiffPreviousPath(file)).toBe("old\tname.ts");
});
});

describe("getCollapsedFileDiffStub", () => {
const renamedPatch = [
"diff --git a/old-name.ts b/new-name.ts",
"similarity index 50%",
"rename from old-name.ts",
"rename to new-name.ts",
"--- a/old-name.ts",
"+++ b/new-name.ts",
"@@ -1,2 +1,2 @@",
" const shared = 1;",
"-const before = 2;",
"+const after = 3;",
"diff --git a/other.ts b/other.ts",
"--- a/other.ts",
"+++ b/other.ts",
"@@ -1 +1 @@",
"-old",
"+new",
].join("\n");

const parseFiles = () => {
const parsed = getRenderablePatch(renamedPatch, "collapsed-stub");
if (parsed?.kind !== "files") throw new Error("expected parsed files");
const [renamed, other] = parsed.files;
if (!renamed || !other) throw new Error("expected two files");
return { renamed, other };
};

it("drops the content and keeps the header fields", () => {
const { renamed } = parseFiles();
expect(renamed.hunks.length).toBeGreaterThan(0);

const stub = getCollapsedFileDiffStub(renamed);

expect(stub.hunks).toEqual([]);
expect(stub.additionLines).toEqual([]);
expect(stub.deletionLines).toEqual([]);
expect(stub.splitLineCount).toBe(0);
expect(stub.unifiedLineCount).toBe(0);
expect(stub.name).toBe(renamed.name);
expect(stub.prevName).toBe(renamed.prevName);
expect(stub.prevName).toBe("old-name.ts");
expect(stub.type).toBe(renamed.type);
expect(stub.lang).toBe(renamed.lang);
expect(stub.newObjectId).toBe(renamed.newObjectId);
expect(stub.prevObjectId).toBe(renamed.prevObjectId);
});

it("marks the stub collapsed rather than pending", () => {
const { renamed } = parseFiles();

const stub = getCollapsedFileDiffStub(renamed);

expect(stub.cacheKey).toBe(`${renamed.cacheKey}:collapsed`);
expect(stub.cacheKey?.endsWith(":pending")).toBe(false);
});

it("returns the same stub for the same input and a different one for another", () => {
const { renamed, other } = parseFiles();

const first = getCollapsedFileDiffStub(renamed);
const second = getCollapsedFileDiffStub(renamed);
const otherStub = getCollapsedFileDiffStub(other);

expect(second).toBe(first);
expect(otherStub).not.toBe(first);
expect(otherStub.name).toBe("other.ts");
});

it("keeps the file's identity key", () => {
const { renamed } = parseFiles();

const stub = getCollapsedFileDiffStub(renamed);

expect(buildFileDiffIdentityKey(stub)).toBe(buildFileDiffIdentityKey(renamed));
});
});
24 changes: 24 additions & 0 deletions apps/web/src/lib/diffRendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,30 @@ export function resolveFileDiffPreviousPath(fileDiff: FileDiffMetadata): string
return fileDiffPath(fileDiff.prevName ?? fileDiff.name ?? "");
}

const collapsedFileDiffStubs = new WeakMap<FileDiffMetadata, FileDiffMetadata>();

/**
* Header-only stand-in for a collapsed file, so the viewer gets no content and never highlights
* lines it does not display. The `:collapsed` suffix keeps it apart from a `:pending` placeholder,
* which disables the expand control.
*/
export function getCollapsedFileDiffStub(fileDiff: FileDiffMetadata): FileDiffMetadata {
const cached = collapsedFileDiffStubs.get(fileDiff);
if (cached) return cached;
const stub: FileDiffMetadata = {
...fileDiff,
hunks: [],
additionLines: [],
deletionLines: [],
splitLineCount: 0,
unifiedLineCount: 0,
isPartial: true,
cacheKey: `${fileDiff.cacheKey ?? resolveFileDiffPath(fileDiff)}:collapsed`,
};
collapsedFileDiffStubs.set(fileDiff, stub);
return stub;
}

/**
* Stable across re-renders of the same file, distinct for every block in a
* patch. A type change (regular file to symlink) arrives as a deletion and an
Expand Down
Loading