Порівняти коміти

...
1 Коміти
Автор SHA1 Повідомлення Дата
l0ng-ai 1c00a1125d fix(ui): remember splitter positions in the Git and other tool windows
SplitGroup only remembers sizes with a storageKey, and none of the Git views
passed one, so the Commit, Local Changes, Log, Stash/Shelf and revision
splitters went back to their defaults whenever the view was recreated. Give
them, the Commit dialog, and the Todo, Find, Problems, Usages, Build, HTTP
Client and Dependency Analyzer splits a key, with stable panel ids.

A view whose preview pane comes and goes keys its sizes per panel set;
SplitGroup now applies that set's saved sizes when the key changes, once the
panels have registered, so toggling a preview restores its size without
remounting the view.
2026-11-14 23:41:17 +08:00
13 змінених файлів з 169 додано та 42 видалено

No files matched your search

+5 -1
Переглянути файл
@@ -440,7 +440,11 @@ export function DependencyAnalyzer(_props: { input: EditorInput }) {
const treeView = (
<div className={styles.body}>
<SplitGroup orientation="horizontal" id="maven-dependency-analyzer-split">
<SplitGroup
orientation="horizontal"
id="maven-dependency-analyzer-split"
storageKey="openidea.dependencyAnalyzer.split"
>
<SplitPanel id="dependencies" minSize={200} className={styles.panel}>
<Tree<Node>
aria-label="Dependencies"
+6 -1
Переглянути файл
@@ -339,7 +339,12 @@ export function BuildView() {
}
return (
<div className={styles.view}>
<SplitGroup orientation="horizontal" className={styles.split} id="build-view-split">
<SplitGroup
orientation="horizontal"
className={styles.split}
id="build-view-split"
storageKey="openidea.build.split"
>
<SplitPanel id="build-tree" defaultSize="38%" minSize="120px" className={styles.pane}>
<BuildTree />
</SplitPanel>
+1 -1
Переглянути файл
@@ -410,7 +410,7 @@ export function ProblemsView() {
<div className={styles.view}>
<div className={styles.body} onKeyDownCapture={onKeyDownCapture}>
{options.preview ? (
<SplitGroup orientation="horizontal" id="problems-preview-split">
<SplitGroup orientation="horizontal" id="problems-preview-split" storageKey="openidea.problems.previewSplit">
<SplitPanel id="problems" minSize={120} className={styles.panel}>
{list}
</SplitPanel>
+1 -1
Переглянути файл
@@ -401,7 +401,7 @@ function UsagesSplit({ preview, children }: { preview: ReactNode; children: Reac
const show = useUsagesOptions((s) => s.preview);
if (!show) return children;
return (
<SplitGroup orientation="horizontal" id="usages-preview-split">
<SplitGroup orientation="horizontal" id="usages-preview-split" storageKey="openidea.usages.previewSplit">
<SplitPanel id="usages" minSize={160} className={styles.panel}>
{children}
</SplitPanel>
+1 -1
Переглянути файл
@@ -261,7 +261,7 @@ export function HttpClientView() {
void refreshHistory();
}, []);
return (
<SplitGroup orientation="horizontal" className={styles.root}>
<SplitGroup orientation="horizontal" className={styles.root} storageKey="openidea.httpClient.split">
<SplitPanel id="http-history" defaultSize="32%" minSize="160px">
<div className={styles.list} role="list" aria-label={tx("Response history")}>
{running && (
+6 -1
Переглянути файл
@@ -422,7 +422,12 @@ function FindTabView({ tab }: { tab: FindTab }) {
</>
)}
</div>
<SplitGroup orientation="horizontal" className={styles.split} id="find-results-split">
<SplitGroup
orientation="horizontal"
className={styles.split}
id="find-results-split"
storageKey={showPreview ? "openidea.find.previewSplit" : undefined}
>
<SplitPanel id="results" minSize={120} className={styles.splitPanel}>
<Tree<Node>
className={styles.tree}
+5 -1
Переглянути файл
@@ -478,7 +478,11 @@ export function TodoView() {
{tab === "scope" && <ScopeBar />}
{tab === "changes" && <ChangelistBar />}
<div className={styles.body}>
<SplitGroup orientation="horizontal" id="todo-preview-split">
<SplitGroup
orientation="horizontal"
id="todo-preview-split"
storageKey={showPreview ? "openidea.todo.previewSplit" : undefined}
>
<SplitPanel id="items" minSize={160} className={styles.panel}>
<Tree<Node>
handleRef={tree}
+10 -6
Переглянути файл
@@ -69,14 +69,18 @@ function CommitDialog({ onClose }: { onClose: () => void }) {
]}
>
<div className={styles.body}>
<SplitGroup orientation="vertical" className={styles.split}>
<SplitPanel defaultSize="58%" minSize="25%">
<SplitGroup orientation="horizontal" className={styles.split}>
<SplitPanel defaultSize="64%" minSize="35%">
<SplitGroup orientation="vertical" className={styles.split} storageKey="openidea.vcs.commitDialogSplit">
<SplitPanel id="top" defaultSize="58%" minSize="25%">
<SplitGroup
orientation="horizontal"
className={styles.split}
storageKey="openidea.vcs.commitDialogOptionsSplit"
>
<SplitPanel id="changes" defaultSize="64%" minSize="35%">
<ChangesTree mode="dialog" />
</SplitPanel>
<SplitHandle />
<SplitPanel defaultSize="36%" minSize="200px">
<SplitPanel id="options" defaultSize="36%" minSize="200px">
<div className={styles.options} role="group" aria-label={tx("Commit Options")}>
<CommitOptionsBody />
</div>
@@ -84,7 +88,7 @@ function CommitDialog({ onClose }: { onClose: () => void }) {
</SplitGroup>
</SplitPanel>
<SplitHandle />
<SplitPanel defaultSize="42%" minSize="120px">
<SplitPanel id="message" defaultSize="42%" minSize="120px">
<div className={viewStyles.commit}>
<CommitMessageField included={included} runner={runner} autoFocus />
</div>
+22 -12
Переглянути файл
@@ -184,12 +184,12 @@ export function StashContent({ tab }: { tab: CommitTab }) {
const combine = useWorkflowSettings((s) => s.vcs.git.combineStashAndShelf);
if (combine)
return (
<SplitGroup orientation="vertical" className={styles.split}>
<SplitPanel defaultSize="50%" minSize="80px">
<SplitGroup orientation="vertical" className={styles.split} storageKey="openidea.vcs.stashShelfSplit">
<SplitPanel id="stashes" defaultSize="50%" minSize="80px">
<StashesPanel />
</SplitPanel>
<SplitHandle />
<SplitPanel defaultSize="50%" minSize="80px">
<SplitPanel id="shelf" defaultSize="50%" minSize="80px">
<ShelfPanel />
</SplitPanel>
</SplitGroup>
@@ -486,14 +486,18 @@ function ChangesPanel() {
const path = useCommit((s) => (s.selection.length === 1 ? s.selection[0] : null));
const hasChanges = useVcs((s) => s.status.length > 0);
return (
<SplitGroup orientation="horizontal" className={styles.split}>
<SplitPanel defaultSize="62%" minSize="20%">
<SplitGroup orientation="vertical" className={styles.split}>
<SplitPanel defaultSize="62%" minSize="20%">
<SplitGroup
orientation="horizontal"
className={styles.split}
storageKey={preview ? "openidea.vcs.commitPreviewSplit" : undefined}
>
<SplitPanel id="commit" defaultSize="62%" minSize="20%">
<SplitGroup orientation="vertical" className={styles.split} storageKey="openidea.vcs.commitMessageSplit">
<SplitPanel id="changes" defaultSize="62%" minSize="20%">
<ChangesTree mode="commit" />
</SplitPanel>
<SplitHandle />
<SplitPanel defaultSize="38%" minSize="120px">
<SplitPanel id="message" defaultSize="38%" minSize="120px">
<CommitPanel included={included} />
</SplitPanel>
</SplitGroup>
@@ -501,7 +505,7 @@ function ChangesPanel() {
{preview && (
<>
<SplitHandle />
<SplitPanel defaultSize="38%" minSize="120px">
<SplitPanel id="preview" defaultSize="38%" minSize="120px">
{path ? (
<DiffEditor input={diffInput(workingDiffData(path))} />
) : (
@@ -525,14 +529,20 @@ export function LocalChangesView() {
const path = useCommit((s) => (s.selection.length === 1 ? s.selection[0] : null));
const anchor = useToolWindowLayout((s) => s.placements.git?.anchor ?? "bottom");
return (
<SplitGroup orientation={anchor === "bottom" ? "horizontal" : "vertical"} className={styles.split}>
<SplitPanel defaultSize="50%" minSize="20%">
<SplitGroup
orientation={anchor === "bottom" ? "horizontal" : "vertical"}
className={styles.split}
storageKey={
preview ? `openidea.vcs.localChangesSplit.${anchor === "bottom" ? "horizontal" : "vertical"}` : undefined
}
>
<SplitPanel id="changes" defaultSize="50%" minSize="20%">
<ChangesTree mode="local" />
</SplitPanel>
{preview && (
<>
<SplitHandle />
<SplitPanel defaultSize="50%" minSize="120px">
<SplitPanel id="preview" defaultSize="50%" minSize="120px">
{path ? (
<DiffEditor input={diffInput(workingDiffData(path))} />
) : (
+3 -1
Переглянути файл
@@ -327,6 +327,7 @@ export function LogView() {
key={showBranches ? "branches" : "no-branches"}
orientation="horizontal"
id={showBranches ? "git-log" : "git-log-no-branches"}
storageKey={`openidea.vcs.logSplit.${showBranches ? "branches" : "noBranches"}.${showDetails ? "details" : "noDetails"}`}
className={styles.split}
>
{showBranches && (
@@ -2681,7 +2682,7 @@ export function CommitDetails({ commits }: { commits: Commit[] }) {
};
const details = (
<SplitGroup orientation="vertical" id="git-log-details">
<SplitGroup orientation="vertical" id="git-log-details" storageKey="openidea.vcs.logDetailsSplit">
<SplitPanel id="files" defaultSize="60%" minSize="60px">
<div
className={styles.filesPane}
@@ -2802,6 +2803,7 @@ export function CommitDetails({ commits }: { commits: Commit[] }) {
<SplitGroup
orientation={diffLocation === "right" ? "horizontal" : "vertical"}
id={`git-log-preview-${diffLocation}`}
storageKey={`openidea.vcs.logPreviewSplit.${diffLocation}`}
>
<SplitPanel id="changes" defaultSize="55%" minSize="20%">
{details}
+1 -1
Переглянути файл
@@ -146,7 +146,7 @@ function RevisionBrowser({
) : !files ? (
<Spinner />
) : (
<SplitGroup orientation="horizontal" id="git-revision-browser">
<SplitGroup orientation="horizontal" id="git-revision-browser" storageKey="openidea.vcs.revisionBrowserSplit">
<SplitPanel id="files" defaultSize="30%" minSize="20%">
<Tree
roots={tree}
+43
Переглянути файл
@@ -0,0 +1,43 @@
import { afterEach, expect, it } from "vitest";
import { createRef } from "react";
import { render } from "@testing-library/react";
import type { GroupImperativeHandle } from "react-resizable-panels";
import { SplitGroup, SplitHandle, SplitPanel } from "./Splitter";
afterEach(() => localStorage.clear());
function Split({ storageKey, preview }: { storageKey: string; preview: boolean }) {
return (
<SplitGroup orientation="horizontal" storageKey={storageKey} groupRef={ref}>
<SplitPanel id="tree" defaultSize="60%" />
{preview && (
<>
<SplitHandle />
<SplitPanel id="preview" defaultSize="40%" />
</>
)}
</SplitGroup>
);
}
const ref = createRef<GroupImperativeHandle>();
it("starts from the sizes saved under its key", () => {
localStorage.setItem("split.preview", JSON.stringify({ tree: 30, preview: 70 }));
render(<Split storageKey="split.preview" preview />);
expect(ref.current!.getLayout()).toEqual({ tree: 30, preview: 70 });
});
it("applies the saved sizes of a panel set that appears later", () => {
localStorage.setItem("split.preview", JSON.stringify({ tree: 25, preview: 75 }));
const view = render(<Split storageKey="split.plain" preview={false} />);
view.rerender(<Split storageKey="split.preview" preview />);
expect(ref.current!.getLayout()).toEqual({ tree: 25, preview: 75 });
});
it("ignores saved sizes for other panels", () => {
localStorage.setItem("split.preview", JSON.stringify({ other: 25, preview: 75 }));
const view = render(<Split storageKey="split.plain" preview={false} />);
view.rerender(<Split storageKey="split.preview" preview />);
expect(Object.keys(ref.current!.getLayout()).sort()).toEqual(["preview", "tree"]);
expect(ref.current!.getLayout().tree).not.toBe(25);
});
+65 -15
Переглянути файл
@@ -2,36 +2,84 @@
* Thin wrappers over react-resizable-panels with IDE styling: 1px hairline
* separators with a wider invisible hit area and an accent hover state.
*/
import { Group, Panel, Separator, type GroupProps, type PanelProps, type SeparatorProps } from "react-resizable-panels";
import {
Group,
Panel,
Separator,
type GroupImperativeHandle,
type GroupProps,
type PanelProps,
type SeparatorProps,
} from "react-resizable-panels";
import clsx from "clsx";
import { useState } from "react";
import { useEffect, useImperativeHandle, useRef, useState } from "react";
import styles from "./Splitter.module.css";
function savedLayout(storageKey: string | undefined): Record<string, number> | undefined {
try {
const value: unknown = storageKey ? JSON.parse(localStorage.getItem(storageKey) ?? "null") : null;
return value &&
typeof value === "object" &&
!Array.isArray(value) &&
Object.values(value).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 100)
? (value as Record<string, number>)
: undefined;
} catch {
return undefined;
}
}
/**
* `storageKey` remembers the sizes the user drags to. A group whose panels come and go (a
* preview pane) passes a key per panel set; when it changes, that set's sizes are applied.
*/
export function SplitGroup({
className,
storageKey,
defaultLayout,
onLayoutChanged,
groupRef,
...rest
}: GroupProps & { storageKey?: string }) {
const [saved] = useState(() => {
try {
const value: unknown = storageKey ? JSON.parse(localStorage.getItem(storageKey) ?? "null") : null;
return value &&
typeof value === "object" &&
!Array.isArray(value) &&
Object.values(value).every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 100)
? (value as Record<string, number>)
: undefined;
} catch {
return undefined;
}
});
const [saved] = useState(() => savedLayout(storageKey));
const group = useRef<GroupImperativeHandle | null>(null);
const mountedKey = useRef(storageKey);
// Sizes to apply once the group's panels match them (a panel added with the new key registers later).
const pending = useRef<Record<string, number> | null>(null);
const applyPending = (current: Record<string, number>) => {
const layout = pending.current;
if (!layout) return;
const ids = Object.keys(current);
if (ids.length !== Object.keys(layout).length || !ids.every((id) => id in layout)) return;
pending.current = null;
group.current?.setLayout(layout);
};
useEffect(() => {
if (storageKey === mountedKey.current) return;
mountedKey.current = storageKey;
pending.current = savedLayout(storageKey) ?? null;
const current = group.current?.getLayout();
if (current) applyPending(current);
}, [storageKey]);
useImperativeHandle(
groupRef,
() => ({
getLayout: () => group.current?.getLayout() ?? {},
setLayout: (layout) => group.current?.setLayout(layout) ?? layout,
}),
[],
);
const onLayoutChange: GroupProps["onLayoutChange"] = (layout) => {
applyPending(layout);
rest.onLayoutChange?.(layout);
};
if (!storageKey)
return (
<Group
className={clsx(styles.group, className)}
{...rest}
groupRef={group}
onLayoutChange={onLayoutChange}
defaultLayout={defaultLayout}
onLayoutChanged={onLayoutChanged}
/>
@@ -40,6 +88,8 @@ export function SplitGroup({
<Group
className={clsx(styles.group, className)}
{...rest}
groupRef={group}
onLayoutChange={onLayoutChange}
defaultLayout={saved ?? defaultLayout}
onLayoutChanged={(layout, meta) => {
if (storageKey && meta.isUserInteraction) {