diff --git a/src/features/vcs/GitLog.module.css b/src/features/vcs/GitLog.module.css index 706d1ac39..b3463bbef 100644 --- a/src/features/vcs/GitLog.module.css +++ b/src/features/vcs/GitLog.module.css @@ -161,6 +161,8 @@ min-height: 0; overflow: auto; outline: none; + /* Rows coming and going while scrolling relay out the list alone, not the workbench. */ + contain: strict; } .row { position: absolute; diff --git a/src/features/vcs/GitLog.tsx b/src/features/vcs/GitLog.tsx index 43a769231..332e8cfb6 100644 --- a/src/features/vcs/GitLog.tsx +++ b/src/features/vcs/GitLog.tsx @@ -77,6 +77,7 @@ import { toggleRepository, updateLogOptions, useGitLog, + PAGE, } from "./logStore"; import { logOptions, refLabels, useLogOptions, type LogOptions } from "./logOptions"; import { KIND_COLOR, baseName, dirName, fileCount, formatTime, relTo, type EffectiveKind } from "./model"; @@ -1391,8 +1392,10 @@ function CommitList() { const items = virtualizer.getVirtualItems(); const last = items[items.length - 1]; + // The next page is requested while a page of rows is still ahead, so scrolling + // quickly doesn't reach the end before `git log` answers. useEffect(() => { - if (!finished && !loading && last && last.index >= commits.length - 40) void loadMore(scope ?? undefined); + if (!finished && !loading && last && last.index >= commits.length - PAGE) void loadMore(scope ?? undefined); }, [last, commits.length, finished, loading, active, scope]); const selectedIndex = selected ? commits.findIndex((c) => commitKey(c) === selected) : -1; diff --git a/src/features/vcs/__tests__/logLoading.test.ts b/src/features/vcs/__tests__/logLoading.test.ts index a331dbb69..c81e95679 100644 --- a/src/features/vcs/__tests__/logLoading.test.ts +++ b/src/features/vcs/__tests__/logLoading.test.ts @@ -67,7 +67,6 @@ beforeEach(() => { logTabs: [], filters: emptyFilters(), commits: [], - rows: [], selection: [], selected: null, done: false, diff --git a/src/features/vcs/__tests__/logUiFeatures.test.tsx b/src/features/vcs/__tests__/logUiFeatures.test.tsx index 4321c269e..a116a8d92 100644 --- a/src/features/vcs/__tests__/logUiFeatures.test.tsx +++ b/src/features/vcs/__tests__/logUiFeatures.test.tsx @@ -82,7 +82,6 @@ beforeEach(() => { tab: "log", logTabs: [], commits: [], - rows: [], filters: emptyFilters(), selection: [], selected: null, diff --git a/src/features/vcs/__tests__/logViews.test.tsx b/src/features/vcs/__tests__/logViews.test.tsx index d32a19e71..0b7d349d6 100644 --- a/src/features/vcs/__tests__/logViews.test.tsx +++ b/src/features/vcs/__tests__/logViews.test.tsx @@ -42,7 +42,6 @@ beforeEach(() => { tab: "log", logTabs: [], commits: [], - rows: [], filters: emptyFilters(), selection: [], selected: null, diff --git a/src/features/vcs/__tests__/multiRoot.test.ts b/src/features/vcs/__tests__/multiRoot.test.ts index 9949a70f0..5b6aad9d9 100644 --- a/src/features/vcs/__tests__/multiRoot.test.ts +++ b/src/features/vcs/__tests__/multiRoot.test.ts @@ -224,12 +224,10 @@ describe("Git log background refresh", () => { const refresh = reloadLog({ preserve: true }); await vi.waitFor(() => expect(finish).toBeDefined()); expect(useGitLog.getState().commits).toBe(before.commits); - expect(useGitLog.getState().rows).toBe(before.rows); expect(useGitLog.getState().loading).toBe(true); finish({ status: "ok", data: [commit("new"), commit("old")] }); await refresh; expect(useGitLog.getState().commits.map((c) => c.id)).toEqual(["new", "old"]); - expect(useGitLog.getState().rows).toHaveLength(2); expect(useGitLog.getState().selected).toBe(commitKey(before.commits[0]!)); expect(useGitLog.getState().loading).toBe(false); }); @@ -290,7 +288,6 @@ describe("Git log background refresh", () => { log.mockRejectedValue(new Error("unavailable")); await reloadLog({ preserve: true }); expect(useGitLog.getState().commits).toBe(before.commits); - expect(useGitLog.getState().rows).toBe(before.rows); expect(useGitLog.getState().error).toContain("unavailable"); expect(useGitLog.getState().loading).toBe(false); }); diff --git a/src/features/vcs/graph.ts b/src/features/vcs/graph.ts index 477b34eb3..849cd3744 100644 --- a/src/features/vcs/graph.ts +++ b/src/features/vcs/graph.ts @@ -458,7 +458,14 @@ const LONG_EDGE_STUB = 2; const onLane = (i: number) => (s: GraphSegment) => s.from === i && s.to === i; -const passes = (row: GraphRow, i: number) => row.lane !== i && row.up.some(onLane(i)) && row.down.some(onLane(i)); +/** Lanes a row's vertical lines pass straight through (other than its node's). */ +function passingLanes(row: GraphRow): Set { + const out = new Set(); + for (const s of row.up) + if (s.from === s.to && s.from !== row.lane && row.down.some((d) => d.from === s.from && d.to === s.from)) + out.add(s.from); + return out; +} /** * Show Long Edges off: the middle of a straight run of a lane through `minLength` or @@ -466,41 +473,41 @@ const passes = (row: GraphRow, i: number) => row.lane !== i && row.up.some(onLan */ export function hideLongEdges(rows: readonly GraphRow[], minLength = LONG_EDGE): GraphRow[] { const out = rows.slice(); - const width = rows.reduce((max, row) => Math.max(max, row.width), 0); const edit = (r: number, fn: (row: GraphRow) => GraphRow) => { out[r] = fn(out[r]!); }; - for (let i = 0; i < width; i++) { + const hide = (i: number, start: number, end: number) => { const lane = onLane(i); - let start = -1; - for (let r = 0; r <= rows.length; r++) { - if (r < rows.length && passes(rows[r]!, i)) { - if (start < 0) start = r; - continue; - } - if (start >= 0 && r - start >= minLength) { - const from = start + LONG_EDGE_STUB; - const to = r - 1 - LONG_EDGE_STUB; - for (let k = from; k <= to; k++) { - edit(k, (row) => ({ - ...row, - up: row.up.filter((s) => !lane(s)), - down: row.down.filter((s) => !lane(s)), - upTargets: row.upTargets?.filter((_, i) => !lane(row.up[i]!)), - downTargets: row.downTargets?.filter((_, i) => !lane(row.down[i]!)), - })); - } - edit(from - 1, (row) => ({ - ...row, - down: row.down.map((s) => (lane(s) ? { ...s, arrow: "down", targetRow: r } : s)), - })); - edit(to + 1, (row) => ({ - ...row, - up: row.up.map((s) => (lane(s) ? { ...s, arrow: "up", targetRow: start - 1 } : s)), - })); - } - start = -1; + const from = start + LONG_EDGE_STUB; + const to = end - 1 - LONG_EDGE_STUB; + for (let k = from; k <= to; k++) { + edit(k, (row) => ({ + ...row, + up: row.up.filter((s) => !lane(s)), + down: row.down.filter((s) => !lane(s)), + upTargets: row.upTargets?.filter((_, i) => !lane(row.up[i]!)), + downTargets: row.downTargets?.filter((_, i) => !lane(row.down[i]!)), + })); } + edit(from - 1, (row) => ({ + ...row, + down: row.down.map((s) => (lane(s) ? { ...s, arrow: "down", targetRow: end } : s)), + })); + edit(to + 1, (row) => ({ + ...row, + up: row.up.map((s) => (lane(s) ? { ...s, arrow: "up", targetRow: start - 1 } : s)), + })); + }; + // One pass over the rows, following every lane's current run at once. + const runs = new Map(); + for (let r = 0; r <= rows.length; r++) { + const through = r < rows.length ? passingLanes(rows[r]!) : new Set(); + for (const [lane, start] of runs) { + if (through.has(lane)) continue; + if (r - start >= minLength) hide(lane, start, r); + runs.delete(lane); + } + for (const lane of through) if (!runs.has(lane)) runs.set(lane, r); } return out; } diff --git a/src/features/vcs/logStore.ts b/src/features/vcs/logStore.ts index de7015e2e..2771d0e3b 100644 --- a/src/features/vcs/logStore.ts +++ b/src/features/vcs/logStore.ts @@ -1,5 +1,5 @@ /** Git log tool window state: filters, paged commits, graph, selection. */ -import { branchesByRepo, commitKey, commitRepo, graphCommit, mergeRepoLogs, type RepoCommit } from "./multiRoot"; +import { branchesByRepo, commitKey, commitRepo, mergeRepoLogs, type RepoCommit } from "./multiRoot"; import { create } from "zustand"; import { useContext } from "react"; import { LogIdContext } from "./logContext"; @@ -8,7 +8,6 @@ import { maybeEditorService } from "@/workbench/contributions"; import type { Branch, BranchInfo, Commit, LogQuery } from "@/bindings"; import { call, commands, nextRequestId, type RequestId } from "@/platform/ipc"; import { workflowSettings } from "@/platform/workflowSettings"; -import { computeGraph, emptyGraphState, type GraphRow, type GraphState } from "./graph"; import { logOptions, parseLogOptions, setLogOptions, useLogOptions, type LogOptions } from "./logOptions"; import { clearConsole, errorMessage } from "./ops"; import { currentRepo, currentRepoRoot, repoName, repoOf, useVcs } from "./store"; @@ -48,8 +47,6 @@ export interface LogFilters { interface LogState { filters: LogFilters; commits: RepoCommit[]; - rows: GraphRow[]; - graph: GraphState; loading: boolean; done: boolean; error: string | null; @@ -179,8 +176,6 @@ const initialLog = (): Omit => ({ filters: emptyFilters(), seek: null, commits: [], - rows: [], - graph: emptyGraphState(), loading: false, done: false, error: null, @@ -293,8 +288,6 @@ type LogView = Pick< | "filters" | "seek" | "commits" - | "rows" - | "graph" | "loading" | "done" | "error" @@ -306,13 +299,11 @@ type LogView = Pick< > & { options: LogOptions }; function freshView(filters: LogFilters): LogView { - const { commits, rows, graph, loading, done, error, selected, selection, revealTick, fragments } = initialLog(); + const { commits, loading, done, error, selected, selection, revealTick, fragments } = initialLog(); return { filters, seek: null, commits, - rows, - graph, loading, done, error, @@ -329,26 +320,11 @@ function freshView(filters: LogFilters): LogView { const parked = new Map(); function currentView(): LogView { - const { - filters, - commits, - rows, - graph, - done, - error, - selected, - selection, - revealTick, - fragments, - loading, - generation, - seek, - } = useGitLog.getState(); + const { filters, commits, done, error, selected, selection, revealTick, fragments, loading, generation, seek } = + useGitLog.getState(); return { filters, commits, - rows, - graph, loading, seek, generation, @@ -764,7 +740,7 @@ export function reloadLog( cancelLogRequests(id); const next = nextGeneration(); setViewState(id, () => ({ - ...(preserve ? {} : { commits: [], rows: [], graph: emptyGraphState(), done: false }), + ...(preserve ? {} : { commits: [], done: false }), loading: true, error: null, generation: next, @@ -1017,11 +993,8 @@ function loadPage(replace: boolean, id: string, before = false): Promise { ]), Number.MAX_SAFE_INTEGER, ); - const { rows, state } = computeGraph(commits.map(graphCommit)); setViewState(id, { commits, - rows, - graph: state, done: before ? cur.done : commits.length >= cap