дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
perf(vcs): keep Git Log scrolling smooth as pages load
Each page of the log recomputed hidden long edges by scanning every row once per lane, and the store laid out the whole graph a second time for fields nothing read. Long edges are now found in one pass over the rows, the unused store graph is gone, and the next page is requested while a page of rows is still ahead so fast scrolling doesn't reach the end before git answers. The commit list also contains its layout, so rows entering while scrolling no longer relay out the workbench.
Цей коміт міститься в:
1 джерело
50b55ffc9e
коміт
cb614eff95
8 files changed
+49
-70
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -67,7 +67,6 @@ beforeEach(() => {
|
||||
logTabs: [],
|
||||
filters: emptyFilters(),
|
||||
commits: [],
|
||||
rows: [],
|
||||
selection: [],
|
||||
selected: null,
|
||||
done: false,
|
||||
|
||||
@@ -82,7 +82,6 @@ beforeEach(() => {
|
||||
tab: "log",
|
||||
logTabs: [],
|
||||
commits: [],
|
||||
rows: [],
|
||||
filters: emptyFilters(),
|
||||
selection: [],
|
||||
selected: null,
|
||||
|
||||
@@ -42,7 +42,6 @@ beforeEach(() => {
|
||||
tab: "log",
|
||||
logTabs: [],
|
||||
commits: [],
|
||||
rows: [],
|
||||
filters: emptyFilters(),
|
||||
selection: [],
|
||||
selected: null,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<number> {
|
||||
const out = new Set<number>();
|
||||
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<number, number>();
|
||||
for (let r = 0; r <= rows.length; r++) {
|
||||
const through = r < rows.length ? passingLanes(rows[r]!) : new Set<number>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<LogState, "generation" | "viewsVersion"> => ({
|
||||
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<string, LogView>();
|
||||
|
||||
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<void> {
|
||||
]),
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
);
|
||||
const { rows, state } = computeGraph(commits.map(graphCommit));
|
||||
setViewState(id, {
|
||||
commits,
|
||||
rows,
|
||||
graph: state,
|
||||
done: before
|
||||
? cur.done
|
||||
: commits.length >= cap
|
||||
|
||||
Reference in new issue
Заблокувати користувача