дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(workbench): toggle the toolbar, status bar and tool window bars from View › Appearance (#2206)
The main toolbar and status bar could not be hidden, Tool Window Bars was a Settings row only, and widgets could be switched only from the status bar's context menu. View > Appearance now has Toolbar, Tool Window Bars, Status Bar and Status Bar Widgets, with check marks in the macOS and in-app menus, plus Settings toggles. Both new settings default to shown. With the toolbar hidden, the window keeps a compact header with the title, which Distraction Free now shares. Controls stay reachable with a bar hidden: the Git branches popup and background tasks have their own hosts, and background tasks has an action. Co-authored-by: lleviathe <[email protected]>
Цей коміт міститься в:
1 джерело
75565927d8
коміт
8dd29bad41
42 files changed
+1001
-144
No files matched your search
@@ -4,7 +4,8 @@ import { _resetI18nForTests, setLocale } from "@/platform/i18n";
|
||||
import { useKeymap } from "@/platform/keymap";
|
||||
import * as ipc from "@/platform/ipc";
|
||||
import { resetDistractionFree, setDistractionFree } from "@/workbench/distractionFree";
|
||||
import { registerToolWindow } from "@/workbench/contributions";
|
||||
import { registerStatusItem, registerToolWindow } from "@/workbench/contributions";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { _reloadLayouts, saveLayout } from "@/workbench/toolwindows/layouts";
|
||||
import { clearContext, setContext } from "@/platform/context";
|
||||
import {
|
||||
@@ -29,7 +30,7 @@ describe("menu state from the frontend", () => {
|
||||
vi.spyOn(ipc.commands, "appBuildInfo").mockRejectedValue(new Error("No build info in tests"));
|
||||
vi.spyOn(ipc.commands, "menuConfigure").mockResolvedValue();
|
||||
vi.spyOn(ipc.commands, "menuModel").mockResolvedValue([
|
||||
{ kind: "action", id: "test.menuFocus", label: "Test", hint: null, arg: null },
|
||||
{ kind: "action", id: "test.menuFocus", label: "Test", hint: null, arg: null, checked: null },
|
||||
]);
|
||||
const pushDisabled = vi.spyOn(ipc.commands, "menuSetDisabled").mockResolvedValue();
|
||||
const offAction = registerAction({ id: "test.menuFocus", title: "Test", run: () => {} });
|
||||
@@ -84,11 +85,11 @@ describe("menu state from the frontend", () => {
|
||||
id: "x",
|
||||
label: "X",
|
||||
children: [
|
||||
{ kind: "action", id: "test.menu.whenEditor", label: "B", hint: null, arg: null },
|
||||
{ kind: "action", id: "test.menu.always", label: "A", hint: null, arg: null },
|
||||
{ kind: "action", id: "test.menu.whenEditor", label: "B", hint: null, arg: null, checked: null },
|
||||
{ kind: "action", id: "test.menu.always", label: "A", hint: null, arg: null, checked: null },
|
||||
],
|
||||
},
|
||||
{ kind: "action", id: "not.registered", label: "C", hint: null, arg: null },
|
||||
{ kind: "action", id: "not.registered", label: "C", hint: null, arg: null, checked: null },
|
||||
]);
|
||||
expect([...ids].sort()).toEqual(["not.registered", "test.menu.always", "test.menu.whenEditor"]);
|
||||
// Unregistered ids stay enabled: the action may register later (lazy features).
|
||||
@@ -206,3 +207,58 @@ describe("window-scoped menu labels", () => {
|
||||
resetDistractionFree();
|
||||
});
|
||||
});
|
||||
|
||||
describe("View › Appearance check marks", () => {
|
||||
it("sends the toggle actions that are on", () => {
|
||||
let on = true;
|
||||
const offs = [
|
||||
registerAction({ id: "test.menu.toggleOn", title: "On", toggled: () => on, run: () => {} }),
|
||||
registerAction({ id: "test.menu.toggleOff", title: "Off", toggled: () => false, run: () => {} }),
|
||||
];
|
||||
const checked = () => buildMenuConfig("macos").checked!.filter((id) => id.startsWith("test.menu."));
|
||||
expect(checked()).toEqual(["test.menu.toggleOn"]);
|
||||
on = false;
|
||||
expect(checked()).toEqual([]);
|
||||
offs.forEach((d) => d());
|
||||
});
|
||||
|
||||
it("lists titled status bar widgets in bar order with their visibility", () => {
|
||||
const widget = (id: string, align: "left" | "right", priority: number, title?: string, defaultVisible?: boolean) =>
|
||||
registerStatusItem({ id, align, priority, title, defaultVisible, component: () => null });
|
||||
const offs = [
|
||||
widget("w.encoding", "right", 2, "File Encoding"),
|
||||
widget("w.caret", "right", 1, "Line:Column Number"),
|
||||
widget("w.tasks", "left", 0),
|
||||
widget("w.memory", "right", 3, "Memory Indicator", false),
|
||||
widget("plugin.demo.w", "left", 1, "Demo Widget"),
|
||||
];
|
||||
useProject.setState({ settings: { appearance: { statusBarWidgets: { "w.caret": false } } } as never });
|
||||
expect(dynamicMenuItems().statusBarWidgets).toEqual([
|
||||
{ action: "view.toggleStatusBarWidget", arg: "plugin.demo.w", label: "Demo Widget", checked: true },
|
||||
{ action: "view.toggleStatusBarWidget", arg: "w.caret", label: "Line:Column Number", checked: false },
|
||||
{ action: "view.toggleStatusBarWidget", arg: "w.encoding", label: "File Encoding", checked: true },
|
||||
{ action: "view.toggleStatusBarWidget", arg: "w.memory", label: "Memory Indicator", checked: false },
|
||||
]);
|
||||
useProject.setState({ settings: null });
|
||||
offs.forEach((d) => d());
|
||||
});
|
||||
|
||||
it("lists only the widgets a LightEdit window's status bar has", () => {
|
||||
const offs = [
|
||||
registerStatusItem({
|
||||
id: "w.caret",
|
||||
align: "right",
|
||||
priority: 1,
|
||||
title: "Line:Column Number",
|
||||
lightEdit: true,
|
||||
component: () => null,
|
||||
}),
|
||||
registerStatusItem({ id: "w.branch", align: "right", priority: 2, title: "Git Branch", component: () => null }),
|
||||
];
|
||||
useProject.setState({ project: null, lightEdit: true });
|
||||
expect(dynamicMenuItems().statusBarWidgets!.map((i) => i.arg)).toEqual(["w.caret"]);
|
||||
useProject.setState({ lightEdit: false });
|
||||
expect(dynamicMenuItems().statusBarWidgets!.map((i) => i.arg)).toEqual(["w.caret", "w.branch"]);
|
||||
offs.forEach((d) => d());
|
||||
});
|
||||
});
|
||||
@@ -20,8 +20,10 @@ import {
|
||||
useKeymap,
|
||||
} from "@/platform/keymap";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useContributions, useMenuSlotItems } from "@/workbench/contributions";
|
||||
import { useContributions, useMenuSlotItems, useStandaloneEditor } from "@/workbench/contributions";
|
||||
import { isDistractionFree, useDistractionFree } from "@/workbench/distractionFree";
|
||||
import { orderStatusItems } from "@/workbench/StatusBar";
|
||||
import { statusWidgetRows } from "@/workbench/statusBarWidgets";
|
||||
import { useToolWindowLayouts } from "@/workbench/toolwindows/layouts";
|
||||
import { hostOs, type HostOs } from "./host";
|
||||
|
||||
@@ -77,10 +79,40 @@ export function buildMenuConfig(os: HostOs): MenuConfig {
|
||||
const accelerator = first && menuAccelerator(first);
|
||||
if (accelerator) accelerators[id] = accelerator;
|
||||
}
|
||||
return { locale: useI18n.getState().locale, labels, accelerators, dynamic: dynamicMenuItems() };
|
||||
return {
|
||||
locale: useI18n.getState().locale,
|
||||
labels,
|
||||
accelerators,
|
||||
dynamic: dynamicMenuItems(),
|
||||
checked: toggledActions(),
|
||||
};
|
||||
}
|
||||
|
||||
/** View › Tool Windows (every registered window, by title) and Window › Tool Window Layouts. */
|
||||
/** Toggle actions that are on: their menu items show a check mark (View › Appearance). */
|
||||
function toggledActions(): string[] {
|
||||
return [...useActions.getState().actions.values()]
|
||||
.filter((a) => a.toggled?.())
|
||||
.map((a) => a.id)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** View › Appearance › Status Bar Widgets: the status bar's context menu, in bar order. */
|
||||
function statusBarWidgetItems(): DynamicItem[] {
|
||||
const { project, lightEdit, settings } = useProject.getState();
|
||||
// A LightEdit window's status bar has only the items that apply there (Workbench.tsx).
|
||||
const lightEditFrame = !project && (lightEdit || useStandaloneEditor.getState().title !== null);
|
||||
const items = [...useContributions.getState().statusItems.values()].filter((i) => !lightEditFrame || i.lightEdit);
|
||||
const { left, right } = orderStatusItems(items);
|
||||
const overrides = settings?.appearance?.statusBarWidgets;
|
||||
return statusWidgetRows([...left, ...right], overrides).map((row) => ({
|
||||
action: "view.toggleStatusBarWidget",
|
||||
arg: row.id,
|
||||
label: row.verbatim ? row.title : tx(row.title),
|
||||
checked: row.visible,
|
||||
}));
|
||||
}
|
||||
|
||||
/** View › Tool Windows (every registered window, by title), View › Appearance › Status Bar Widgets and Window › Tool Window Layouts. */
|
||||
export function dynamicMenuItems(): Record<string, DynamicItem[]> {
|
||||
const toolWindows = [...useContributions.getState().toolWindows.values()]
|
||||
.map((tw) => ({ action: `toolwindow.${tw.id}`, arg: null, label: tw.verbatim ? tw.title : tx(tw.title) }))
|
||||
@@ -96,7 +128,7 @@ export function dynamicMenuItems(): Record<string, DynamicItem[]> {
|
||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
]),
|
||||
);
|
||||
return { ...slots, toolWindows, toolWindowLayouts: layouts };
|
||||
return { ...slots, toolWindows, toolWindowLayouts: layouts, statusBarWidgets: statusBarWidgetItems() };
|
||||
}
|
||||
|
||||
/** Action ids of every IDE item in a menu model. */
|
||||
@@ -186,7 +218,7 @@ export function installMenuBridge(): () => void {
|
||||
if (s.active !== prev.active) push();
|
||||
}),
|
||||
useContributions.subscribe((s, prev) => {
|
||||
if (s.toolWindows !== prev.toolWindows) push();
|
||||
if (s.toolWindows !== prev.toolWindows || s.statusItems !== prev.statusItems) push();
|
||||
}),
|
||||
useToolWindowLayouts.subscribe(push),
|
||||
useMenuSlotItems.subscribe(push),
|
||||
@@ -195,6 +227,9 @@ export function installMenuBridge(): () => void {
|
||||
// Opening / closing a project changes the Open Recent list.
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.project?.root !== prev.project?.root) void commands.menuRefresh();
|
||||
if (s.lightEdit !== prev.lightEdit) push();
|
||||
// View › Appearance check marks and Status Bar Widgets.
|
||||
if (s.settings?.appearance !== prev.settings?.appearance) push();
|
||||
}),
|
||||
];
|
||||
return () => {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ActionContext } from "@/platform/actions";
|
||||
import type { Settings } from "@/platform/ipc";
|
||||
|
||||
const settingsSet = vi.fn();
|
||||
|
||||
vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...real,
|
||||
isTauri: () => true,
|
||||
commands: { ...real.commands, settingsSet: (...a: unknown[]) => settingsSet(...a) },
|
||||
};
|
||||
});
|
||||
|
||||
import { useProject } from "@/platform/project";
|
||||
import { appearanceToggleActions } from "../appearanceRuntime";
|
||||
import { validateAppearanceExtras } from "../model/behaviorSchema";
|
||||
|
||||
let appearance: Record<string, unknown>;
|
||||
|
||||
beforeEach(() => {
|
||||
appearance = {};
|
||||
const settings = () => ({ appearance: { ...appearance } }) as unknown as Settings;
|
||||
useProject.setState({ settings: settings() });
|
||||
settingsSet.mockReset().mockImplementation(async (_scope: string, key: string, json: string) => {
|
||||
appearance[key.replace("appearance.", "")] = JSON.parse(json);
|
||||
return { status: "ok", data: settings() };
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => useProject.setState({ settings: null }));
|
||||
|
||||
describe("View › Appearance toggles", () => {
|
||||
it("shows the toolbar and the status bar unless settings hide them", () => {
|
||||
expect(validateAppearanceExtras({})).toMatchObject({ showMainToolbar: true, showStatusBar: true });
|
||||
expect(validateAppearanceExtras({ showMainToolbar: false, showStatusBar: "no" })).toMatchObject({
|
||||
showMainToolbar: false,
|
||||
showStatusBar: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["view.toggleCompactMode", "compactMode", false],
|
||||
["view.toggleToolbar", "showMainToolbar", true],
|
||||
["view.toggleToolWindowBars", "showToolWindowBars", true],
|
||||
["view.toggleStatusBar", "showStatusBar", true],
|
||||
])("%s flips appearance.%s in the global settings", async (id, key, initial) => {
|
||||
const action = appearanceToggleActions().find((a) => a.id === id)!;
|
||||
expect(action.toggled?.()).toBe(initial);
|
||||
await action.run({ ctx: {} } as ActionContext);
|
||||
expect(settingsSet).toHaveBeenCalledWith("global", `appearance.${key}`, JSON.stringify(!initial));
|
||||
expect(action.toggled?.()).toBe(!initial);
|
||||
});
|
||||
});
|
||||
@@ -90,3 +90,10 @@ describe("page tree", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("indexes toolbar and status bar visibility in Appearance settings", () => {
|
||||
const appearance = BEHAVIOR_PAGES.find((p) => p.id === "appearance")!;
|
||||
const labels = specLabels(appearance);
|
||||
expect(labels).toContain("Show main toolbar");
|
||||
expect(labels).toContain("Show status bar");
|
||||
});
|
||||
@@ -114,21 +114,43 @@ export function zoomActions(): Action[] {
|
||||
keywords: ["zoom", "scale", "100%", "accessibility"],
|
||||
run: () => setZoom(100),
|
||||
},
|
||||
{
|
||||
// View › Appearance › Compact Mode (Settings › Appearance › "Compact mode").
|
||||
id: "view.toggleCompactMode",
|
||||
title: "Compact Mode",
|
||||
category: "View",
|
||||
keywords: ["appearance", "density", "smaller"],
|
||||
toggled: () => validateSettings(useProject.getState().settings).appearance.compactMode,
|
||||
run: async () => {
|
||||
if (!isTauri()) return;
|
||||
const next = !validateSettings(useProject.getState().settings).appearance.compactMode;
|
||||
useProject.setState({
|
||||
settings: await call(commands.settingsSet("global", "appearance.compactMode", JSON.stringify(next))),
|
||||
});
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// View › Appearance toggles over boolean appearance settings (IDEA's ToggleActions)
|
||||
|
||||
type AppearanceFlag = "compactMode" | "showMainToolbar" | "showToolWindowBars" | "showStatusBar";
|
||||
|
||||
const appearanceFlag = (key: AppearanceFlag) => validateSettings(useProject.getState().settings).appearance[key];
|
||||
|
||||
function appearanceToggle(id: string, title: string, key: AppearanceFlag, keywords: string[]): Action {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
category: "View",
|
||||
keywords: ["appearance", ...keywords],
|
||||
toggled: () => appearanceFlag(key),
|
||||
run: async () => {
|
||||
if (!isTauri()) return;
|
||||
const next = !appearanceFlag(key);
|
||||
useProject.setState({
|
||||
settings: await call(commands.settingsSet("global", `appearance.${key}`, JSON.stringify(next))),
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function appearanceToggleActions(): Action[] {
|
||||
return [
|
||||
appearanceToggle("view.toggleCompactMode", "Compact Mode", "compactMode", ["density", "smaller"]),
|
||||
appearanceToggle("view.toggleToolbar", "Toolbar", "showMainToolbar", ["main toolbar", "header", "hide"]),
|
||||
appearanceToggle("view.toggleToolWindowBars", "Tool Window Bars", "showToolWindowBars", [
|
||||
"stripes",
|
||||
"tool window buttons",
|
||||
"hide",
|
||||
]),
|
||||
appearanceToggle("view.toggleStatusBar", "Status Bar", "showStatusBar", ["hide"]),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
settingsTitle,
|
||||
useSettingsDialog,
|
||||
} from "./model/staging";
|
||||
import { zoomActions } from "./appearanceRuntime";
|
||||
import { appearanceToggleActions, zoomActions } from "./appearanceRuntime";
|
||||
import { activateFileTypes } from "./fileTypesRuntime";
|
||||
import { trackRuntimeSettings } from "./runtime";
|
||||
import { installActionProxies, installSettingsHost } from "./window/bridge";
|
||||
@@ -123,6 +123,7 @@ export function activate(): () => void {
|
||||
registerOverlay({ id: "settings.dialog", component: LazySettingsDialog }),
|
||||
registerOverlay({ id: "settings.projectStructure", component: LazyProjectStructureDialog }),
|
||||
registerActions(zoomActions()),
|
||||
registerActions(appearanceToggleActions()),
|
||||
registerActions(settingsActions((page, newProjects) => void showSettings(page, newProjects))),
|
||||
registerActions([
|
||||
{
|
||||
|
||||
@@ -45,6 +45,9 @@ export interface AppearanceExtras {
|
||||
showToolWindowNames: boolean;
|
||||
showToolWindowBadges: boolean;
|
||||
widescreenToolWindows: boolean;
|
||||
/** View › Appearance › Toolbar; hidden leaves IDEA's compact window header. */
|
||||
showMainToolbar: boolean;
|
||||
showStatusBar: boolean;
|
||||
}
|
||||
|
||||
export interface DateFormatValid {
|
||||
@@ -105,6 +108,8 @@ export const DEFAULT_APPEARANCE_EXTRAS: AppearanceExtras = {
|
||||
showToolWindowNames: false,
|
||||
showToolWindowBadges: true,
|
||||
widescreenToolWindows: false,
|
||||
showMainToolbar: true,
|
||||
showStatusBar: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_GENERAL_EXTRAS: GeneralExtras = {
|
||||
@@ -163,6 +168,8 @@ export function validateAppearanceExtras(raw: unknown): AppearanceExtras {
|
||||
showToolWindowNames: b("showToolWindowNames"),
|
||||
showToolWindowBadges: b("showToolWindowBadges"),
|
||||
widescreenToolWindows: b("widescreenToolWindows"),
|
||||
showMainToolbar: b("showMainToolbar"),
|
||||
showStatusBar: b("showStatusBar"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -642,6 +642,8 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
|
||||
{
|
||||
title: "UI Options",
|
||||
options: [
|
||||
{ kind: "toggle", key: "appearance.showMainToolbar", label: "Show main toolbar" },
|
||||
{ kind: "toggle", key: "appearance.showStatusBar", label: "Show status bar" },
|
||||
{
|
||||
kind: "custom",
|
||||
label: "Main menu",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { branchesByRepo } from "./multiRoot";
|
||||
/** Title bar branch widget (branch name + incoming / outgoing markers) + Branches popup. */
|
||||
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type RefObject } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
|
||||
import clsx from "clsx";
|
||||
import { create } from "zustand";
|
||||
import type { Branch, BranchInfo } from "@/bindings";
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type MenuItem,
|
||||
T,
|
||||
} from "@/ui";
|
||||
import type { PopupAnchor } from "@/ui/Popup";
|
||||
import { buildBranchRows, withExpanded, type BranchRow, type PopupAction } from "./branches";
|
||||
import { branchPresentation, extensionPopupActions, useBranchWidgetExtensions } from "./branchWidgetExtensions";
|
||||
import { branchMenu, commonBranchMenu, inRepo, tagMenu } from "./branchMenu";
|
||||
@@ -68,7 +69,6 @@ export function BranchWidget() {
|
||||
const otherOp = useVcs((s) => (current ? (s.operations[current] ?? null) : null));
|
||||
const open = useBranchesPopup((s) => s.open);
|
||||
const extensions = useBranchWidgetExtensions((s) => s.extensions);
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
if (!available || !selected) return null;
|
||||
// Another feature presents the current branch (the GitHub pull request on it).
|
||||
const presented = other ? null : branchPresentation(extensions);
|
||||
@@ -98,7 +98,7 @@ export function BranchWidget() {
|
||||
return (
|
||||
<>
|
||||
<BarButton
|
||||
ref={ref}
|
||||
data-branches-widget
|
||||
variant="title"
|
||||
className={styles.widget}
|
||||
tooltip={tooltip}
|
||||
@@ -120,11 +120,17 @@ export function BranchWidget() {
|
||||
<BranchSync behind={branch.behind} ahead={branch.ahead} showCounts={false} />
|
||||
<Icon name="chevron-down" size={14} className={styles.chevron} />
|
||||
</BarButton>
|
||||
{open && <BranchesPopup anchor={ref} onClose={() => useBranchesPopup.setState({ open: false })} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function BranchesPopupHost() {
|
||||
const open = useBranchesPopup((s) => s.open);
|
||||
if (!open) return null;
|
||||
const anchor = document.querySelector("[data-branches-widget]") ?? { x: window.innerWidth / 2 - 160, y: 60 };
|
||||
return <BranchesPopup anchor={anchor} onClose={() => useBranchesPopup.setState({ open: false })} />;
|
||||
}
|
||||
|
||||
const POPUP_ACTIONS: PopupAction[] = [
|
||||
{ id: "vcs.update", label: "Update Project…", icon: "git-pull", group: "sync" },
|
||||
{ id: "vcs.commit", label: "Commit…", icon: "commit", group: "sync" },
|
||||
@@ -283,7 +289,7 @@ const caretAt = (e: KeyboardEvent, where: "start" | "end") => {
|
||||
return where === "end" ? input.selectionStart === input.value.length : input.selectionStart === 0;
|
||||
};
|
||||
|
||||
function BranchesPopup({ anchor, onClose }: { anchor: RefObject<HTMLElement | null>; onClose: () => void }) {
|
||||
function BranchesPopup({ anchor, onClose }: { anchor: PopupAnchor; onClose: () => void }) {
|
||||
const multi = useVcs((s) => s.roots.length > 1);
|
||||
const extensions = useBranchWidgetExtensions((s) => s.extensions);
|
||||
const actions = useMemo(() => [...POPUP_ACTIONS, ...extensionPopupActions(extensions)], [extensions]);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { BranchesPopupHost, showBranchesPopup, useBranchesPopup } from "../BranchWidget";
|
||||
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return { ...real, commands: { ...real.commands, vcsTags: async () => ({ status: "ok", data: [] }) } };
|
||||
});
|
||||
|
||||
afterEach(() => useBranchesPopup.setState({ open: false }));
|
||||
|
||||
it("opens Git Branches without a mounted toolbar widget", () => {
|
||||
render(<BranchesPopupHost />);
|
||||
act(showBranchesPopup);
|
||||
expect(screen.getByRole("dialog", { name: "Git Branches" })).toBeInTheDocument();
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(useBranchesPopup.getState().open).toBe(false);
|
||||
});
|
||||
@@ -24,7 +24,7 @@ import { registerLogPaneActions } from "./logPaneActions";
|
||||
import { controlLabel, controlOperation, inProgress } from "./operations";
|
||||
import { annotationsShown, blameSlot } from "./blame";
|
||||
import { inlineBlame } from "./inlineBlame";
|
||||
import { BranchWidget, useBranchesPopup } from "./BranchWidget";
|
||||
import { BranchWidget, BranchesPopupHost, useBranchesPopup } from "./BranchWidget";
|
||||
import { showConflictsDialog, takeContinueWhenResolved } from "./conflictsUi";
|
||||
import { registerCompareActions } from "./compare";
|
||||
import { DIFF_KIND, diffTitle, type DiffData } from "./diffInput";
|
||||
@@ -238,6 +238,7 @@ export function activate(): () => void {
|
||||
trackMergeRebaseItem(),
|
||||
installHunkPopup(),
|
||||
registerOverlay({ id: "vcs.dialogs", component: VcsDialogHost }),
|
||||
registerOverlay({ id: "vcs.branches", component: BranchesPopupHost }),
|
||||
activateConflictBanner(),
|
||||
activateConflictMarkers(),
|
||||
trackCommitWindow(),
|
||||
|
||||
Reference in new issue
Заблокувати користувача