Archived
refactor(ui): dedupe git file helpers and memoize git view model
- N1: extract the copy-pasted pointerName/segmentName helpers from gitFileList.ts and gitFileTree.ts into gitFileShared.ts. - N3: drop the dead conditional "tree" class (no CSS rule exists). - N4 (P3): memoize computeViewState on (status, view) identity so renders from expand/collapse or diff selection skip the full model rebuild; expand state is read live at render time, never cached.
This commit is contained in:
@@ -13,6 +13,12 @@ interface GitViewState {
|
||||
readonly expandablePaths: readonly string[];
|
||||
}
|
||||
|
||||
interface GitViewStateCache {
|
||||
readonly status: GitStatusResponse | undefined;
|
||||
readonly view: GitFileView;
|
||||
readonly viewState: GitViewState;
|
||||
}
|
||||
|
||||
const EMPTY_LIST_MODEL: GitFileListModel = { submodules: [], files: [] };
|
||||
const EMPTY_VIEW_STATE: GitViewState = { nodes: [], listModel: EMPTY_LIST_MODEL, expandablePaths: [] };
|
||||
|
||||
@@ -45,6 +51,11 @@ export class WorkspaceGitPanel extends LitElement {
|
||||
// submodule groups, both keyed by their path.
|
||||
@state() private expandedDirectories = new Set<string>();
|
||||
|
||||
// Memoized on (status, view) identity: renders triggered by expand/collapse
|
||||
// or diff selection reuse the model, while a new poll or view switch rebuilds
|
||||
// it. Expand state is read live at render time, never baked into the model.
|
||||
private viewStateCache: GitViewStateCache | undefined;
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (!changedProperties.has("context")) return;
|
||||
const previous = changedProperties.get("context");
|
||||
@@ -70,7 +81,7 @@ export class WorkspaceGitPanel extends LitElement {
|
||||
</div>
|
||||
</section>
|
||||
<section class="split">
|
||||
<div class=${this.view === "tree" ? "list tree" : "list"}>
|
||||
<div class="list">
|
||||
${this.renderFileList(context, status, viewState)}
|
||||
</div>
|
||||
<div class="viewer">${renderDiffViewer(context)}</div>
|
||||
@@ -166,6 +177,14 @@ export class WorkspaceGitPanel extends LitElement {
|
||||
}
|
||||
|
||||
private computeViewState(status: GitStatusResponse | undefined): GitViewState {
|
||||
const cached = this.viewStateCache;
|
||||
if (cached !== undefined && cached.status === status && cached.view === this.view) return cached.viewState;
|
||||
const viewState = this.buildViewState(status);
|
||||
this.viewStateCache = { status, view: this.view, viewState };
|
||||
return viewState;
|
||||
}
|
||||
|
||||
private buildViewState(status: GitStatusResponse | undefined): GitViewState {
|
||||
if (status === undefined || !status.isGitRepo || status.files.length === 0) return EMPTY_VIEW_STATE;
|
||||
if (this.view === "tree") {
|
||||
const nodes = buildGitFileTree(status.files, status.submodules);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GitStatusFile } from "./api";
|
||||
import { pointerName, segmentName } from "./gitFileShared";
|
||||
|
||||
/** A changed file inside a submodule, shown flat in list view. `path` is the
|
||||
* full superproject-relative path (the diff key); `relativePath` is shown in
|
||||
@@ -70,15 +71,3 @@ function ownerSubmodule(path: string, submodules: readonly string[]): string | u
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function pointerName(file: GitStatusFile): string {
|
||||
const from = file.submoduleFromCommit;
|
||||
const to = file.submoduleToCommit;
|
||||
return from !== undefined && to !== undefined ? `${from} → ${to}` : "commit";
|
||||
}
|
||||
|
||||
function segmentName(path: string): string {
|
||||
const segments = path.split("/");
|
||||
const last = segments[segments.length - 1];
|
||||
return last ?? path;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { GitStatusFile } from "./api";
|
||||
import { pointerName, segmentName } from "./gitFileShared";
|
||||
|
||||
describe("pointerName", () => {
|
||||
it("renders the moved pointer as <from> → <to>", () => {
|
||||
expect(pointerName(pointer("1111111", "2222222"))).toBe("1111111 → 2222222");
|
||||
});
|
||||
|
||||
it("falls back to a plain label when either end is unresolved", () => {
|
||||
expect(pointerName(pointer(undefined, "2222222"))).toBe("commit");
|
||||
expect(pointerName(pointer("1111111", undefined))).toBe("commit");
|
||||
expect(pointerName(pointer(undefined, undefined))).toBe("commit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("segmentName", () => {
|
||||
it("returns the final path segment", () => {
|
||||
expect(segmentName("deps/sub/module")).toBe("module");
|
||||
});
|
||||
|
||||
it("returns the whole path when there is no separator", () => {
|
||||
expect(segmentName("HARL")).toBe("HARL");
|
||||
});
|
||||
});
|
||||
|
||||
function pointer(from: string | undefined, to: string | undefined): GitStatusFile {
|
||||
return {
|
||||
path: "HARL",
|
||||
index: "unmodified",
|
||||
workingTree: "modified",
|
||||
...(from === undefined ? {} : { submoduleFromCommit: from }),
|
||||
...(to === undefined ? {} : { submoduleToCommit: to }),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { GitStatusFile } from "./api";
|
||||
|
||||
/**
|
||||
* Display label for a submodule commit-pointer row: the `<old> → <new>`
|
||||
* short-SHA summary, or "commit" when the server did not resolve both ends.
|
||||
*/
|
||||
export function pointerName(file: GitStatusFile): string {
|
||||
const from = file.submoduleFromCommit;
|
||||
const to = file.submoduleToCommit;
|
||||
return from !== undefined && to !== undefined ? `${from} → ${to}` : "commit";
|
||||
}
|
||||
|
||||
/** Final segment of a path, used as the display name for tree/list rows. */
|
||||
export function segmentName(path: string): string {
|
||||
const segments = path.split("/");
|
||||
const last = segments[segments.length - 1];
|
||||
return last ?? path;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GitStatusFile } from "./api";
|
||||
import { pointerName, segmentName } from "./gitFileShared";
|
||||
|
||||
/**
|
||||
* A changed file placed at a leaf of the Git file tree. `path` is the full
|
||||
@@ -93,12 +94,6 @@ export function collectGitFileTreeDirectoryPaths(nodes: readonly GitFileTreeNode
|
||||
return paths;
|
||||
}
|
||||
|
||||
function pointerName(file: GitStatusFile): string {
|
||||
const from = file.submoduleFromCommit;
|
||||
const to = file.submoduleToCommit;
|
||||
return from !== undefined && to !== undefined ? `${from} → ${to}` : "commit";
|
||||
}
|
||||
|
||||
function createDirectoryAccumulator(path: string): DirectoryAccumulator {
|
||||
return { path, directories: new Map(), files: [], isSubmodule: false };
|
||||
}
|
||||
@@ -125,9 +120,3 @@ function finalizeChildren(directory: DirectoryAccumulator): GitFileTreeNode[] {
|
||||
const files = [...directory.files].sort((left, right) => left.name.localeCompare(right.name));
|
||||
return [...(directory.pointer === undefined ? [] : [directory.pointer]), ...directories, ...files];
|
||||
}
|
||||
|
||||
function segmentName(path: string): string {
|
||||
const segments = path.split("/");
|
||||
const last = segments[segments.length - 1];
|
||||
return last ?? path;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user