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[];
|
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_LIST_MODEL: GitFileListModel = { submodules: [], files: [] };
|
||||||
const EMPTY_VIEW_STATE: GitViewState = { nodes: [], listModel: EMPTY_LIST_MODEL, expandablePaths: [] };
|
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.
|
// submodule groups, both keyed by their path.
|
||||||
@state() private expandedDirectories = new Set<string>();
|
@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 {
|
protected override willUpdate(changedProperties: PropertyValues<this>): void {
|
||||||
if (!changedProperties.has("context")) return;
|
if (!changedProperties.has("context")) return;
|
||||||
const previous = changedProperties.get("context");
|
const previous = changedProperties.get("context");
|
||||||
@@ -70,7 +81,7 @@ export class WorkspaceGitPanel extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="split">
|
<section class="split">
|
||||||
<div class=${this.view === "tree" ? "list tree" : "list"}>
|
<div class="list">
|
||||||
${this.renderFileList(context, status, viewState)}
|
${this.renderFileList(context, status, viewState)}
|
||||||
</div>
|
</div>
|
||||||
<div class="viewer">${renderDiffViewer(context)}</div>
|
<div class="viewer">${renderDiffViewer(context)}</div>
|
||||||
@@ -166,6 +177,14 @@ export class WorkspaceGitPanel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private computeViewState(status: GitStatusResponse | undefined): GitViewState {
|
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 (status === undefined || !status.isGitRepo || status.files.length === 0) return EMPTY_VIEW_STATE;
|
||||||
if (this.view === "tree") {
|
if (this.view === "tree") {
|
||||||
const nodes = buildGitFileTree(status.files, status.submodules);
|
const nodes = buildGitFileTree(status.files, status.submodules);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { GitStatusFile } from "./api";
|
import type { GitStatusFile } from "./api";
|
||||||
|
import { pointerName, segmentName } from "./gitFileShared";
|
||||||
|
|
||||||
/** A changed file inside a submodule, shown flat in list view. `path` is the
|
/** 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
|
* 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;
|
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 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
|
* 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;
|
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 {
|
function createDirectoryAccumulator(path: string): DirectoryAccumulator {
|
||||||
return { path, directories: new Map(), files: [], isSubmodule: false };
|
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));
|
const files = [...directory.files].sort((left, right) => left.name.localeCompare(right.name));
|
||||||
return [...(directory.pointer === undefined ? [] : [directory.pointer]), ...directories, ...files];
|
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