fix(vcs): align the diff titles and find bar with the panes, and title revisions by commit and path (#2778)

After the side-by-side divider was dragged, the right half of the title row and of the find bar stayed at 50% and no longer lined up with the right pane. Both now follow the right pane's actual start. A revision side's title shows its short commit id (HEAD resolved) and the file's path in the repository.

Co-authored-by: PLZY <41029728+PLZY@users.noreply.github.com>
This commit is contained in:
zhangdagou
2026-11-12 00:47:25 +08:00
committed by GitHub
co-authored by PLZY
parent 0b3ca2fc9a
commit 8d8c7a481b
3 changed files with 71 additions and 6 deletions
+13 -1
View File
@@ -71,6 +71,17 @@
.label + .label {
border-left: 1px solid var(--border);
}
/* Side by side, the right label starts at the right pane (`--diff-b-start`, set by the viewer). */
.labels > .label:first-child:not(:only-child) {
flex: 0 0 var(--diff-b-start, 50%);
}
.labelFile {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
color: var(--fg-subtle);
font-family: var(--font-ui);
}
.badge {
padding: 0 5px;
border-radius: var(--radius-small);
@@ -93,13 +104,14 @@
border-bottom: none;
}
.findRowSplit > div {
flex: 0 0 50%;
flex: 0 0 var(--diff-b-start, 50%);
border-right: 1px solid var(--border);
}
.findRowRight {
justify-content: flex-end;
}
.findRowRight > div {
flex-basis: calc(100% - var(--diff-b-start, 50%));
border-right: none;
border-left: 1px solid var(--border);
}
+45 -5
View File
@@ -88,7 +88,7 @@ import { inclusionGutter } from "./hunkInclusion";
import { hunksOverlapping } from "./hunks";
import { effectiveKind, stripSlash } from "./model";
import { errorMessage, logConsole, readWorkingText, runOp, saveAll } from "./ops";
import { refreshNow, useVcs } from "./store";
import { refreshNow, repoOf, useVcs } from "./store";
import styles from "./DiffEditor.module.css";
/** Commands of the focused diff viewer (diff.* actions, vcs.nextChange / prevChange). */
@@ -252,12 +252,14 @@ export function DiffEditor({
const [chunkInfo, setChunkInfo] = useState({ count: 0, current: -1, moves: 0, whitespaceOnly: false });
const [busy, setBusy] = useState(false);
const hostRef = useRef<HTMLDivElement>(null);
const rootRef = useRef<HTMLDivElement>(null);
const viewsRef = useRef<Views | null>(null);
const staticsRef = useRef(statics);
const focusedSide = useRef<Side>("b");
/** Bumped when the views are rebuilt, so the find bar re-applies its query. */
const [viewsGen, setViewsGen] = useState(0);
const version = useVcs((s) => s.version);
const heads = useVcs((s) => s.heads);
const leftKey = sideKey(left);
const rightKey = sideKey(right);
const contextKey = JSON.stringify(data.context ?? null);
@@ -577,6 +579,27 @@ export function DiffEditor({
views = { a: null, b: view, merge: null, panes: null, destroy: () => view.destroy() };
if (scrollTop != null) requestAnimationFrame(() => (view.scrollDOM.scrollTop = scrollTop));
}
// The labels and the find bar above side-by-side panes start the right side where its pane
// does, wherever the divider (dragged, see UnalignedPanes) and the window put it.
const root = rootRef.current;
if (views.a && root && typeof ResizeObserver !== "undefined") {
const paneB = views.b.dom;
const align = () =>
root.style.setProperty(
"--diff-b-start",
`${paneB.getBoundingClientRect().left - root.getBoundingClientRect().left}px`,
);
const observer = new ResizeObserver(align);
observer.observe(root);
observer.observe(paneB);
align();
const destroyPanes = views.destroy;
views.destroy = () => {
observer.disconnect();
root.style.removeProperty("--diff-b-start");
destroyPanes();
};
}
viewsRef.current = views;
const built = viewerHost(views, dataRef.current, opts.mode, () => focusedSide.current);
const detach = contribs.map((c) => c.attach?.(built));
@@ -1106,8 +1129,23 @@ export function DiffEditor({
const canStage = editableB && isWorktree(right) && left.type === "rev" && !!useVcs.getState().root;
const hasSource = !!source;
/** IDEA's title of a revision: its short commit id (HEAD resolved) and the file's path in the repository. */
const revTitle = (s: DiffSide): { label: string; file: string | null } => {
if (s.type !== "rev") return { label: sideLabel(s), file: null };
const file = s.path ?? path;
const root = repoOf(file);
const commit = !s.label && s.rev === "HEAD" && root ? heads[root]?.headCommit : null;
return {
label: commit ? commit.slice(0, 8) : sideLabel(s),
file: root && file.startsWith(`${root}/`) ? file.slice(root.length + 1) : file,
};
};
const titleA = revTitle(left);
const titleB = revTitle(right);
return (
<div
ref={rootRef}
className={styles.root}
onKeyDown={onKeyDown}
onFocusCapture={() => {
@@ -1278,8 +1316,9 @@ export function DiffEditor({
</div>
{opts.mode === "split" ? (
<div className={styles.labels}>
<span className={styles.label} title={sideLabel(left)}>
{sideLabel(left)}
<span className={styles.label} title={[titleA.label, titleA.file].filter(Boolean).join(" ")}>
{titleA.label}
{titleA.file && <span className={styles.labelFile}>{titleA.file}</span>}
{eolDiffers && eolA && <span className={styles.badge}>{badgeA}</span>}
{editableA && (
<span className={styles.badge}>
@@ -1287,8 +1326,9 @@ export function DiffEditor({
</span>
)}
</span>
<span className={styles.label} title={sideLabel(right)}>
{sideLabel(right)}
<span className={styles.label} title={[titleB.label, titleB.file].filter(Boolean).join(" ")}>
{titleB.label}
{titleB.file && <span className={styles.labelFile}>{titleB.file}</span>}
{eolDiffers && eolB && <span className={styles.badge}>{badgeB}</span>}
{editableB && (
<span className={styles.badge}>{left.type === "rev" ? tx("working copy") : tx("editable")}</span>
@@ -51,6 +51,7 @@ import { DiffEditorTab } from "../diff-editor-tab";
import { diffInput, withChain, type DiffData } from "../diffInput";
import { useToolWindowLayout } from "@/workbench/toolwindows/store";
import { useProject } from "@/platform/project";
import { useVcs } from "../store";
import { applyKeymapSettings, installKeyboardDispatcher, useKeymap } from "@/platform/keymap";
import { setContext } from "@/platform/context";
import { registerCompareActions } from "../compare";
@@ -159,6 +160,18 @@ describe("Diff toolbar", () => {
expect(screen.queryByText("Contents differ only in line separators")).toBeNull();
});
it("titles a revision with its short commit id, HEAD resolved, and the file's repository path", async () => {
const before = useVcs.getState();
useVcs.setState({ roots: ["/p"], heads: { "/p": { head: "main", headCommit: "81a179900123456789abcdef" } } });
const mounted = render(
<DiffEditor input={{ ...input, data: { ...(input.data as object), path: "/p/src/a.ts", mode: "split" } }} />,
);
await waitFor(() => expect(mounted.container.querySelectorAll(".cm-editor")).toHaveLength(2));
expect(screen.getByTitle("81a17990 src/a.ts")).toBeInTheDocument();
expect(screen.getByTitle("abc123 src/a.ts")).toBeInTheDocument();
mounted.unmount();
useVcs.setState({ roots: before.roots, heads: before.heads });
});
it("puts Jump to Source right after Next Difference, in the navigation group", async () => {
render(<DiffEditor input={input} />);
await screen.findByRole("button", { name: "Jump to Source" });