Файли
openidea/src/features/project-tree/contextMenu.test.ts
T
l0ng-ai a404804ee7 feat(editor): Add folding, navigation, editing and window shortcuts
Editing and folding
- Move Caret to Code Block Start / End with Selection, Auto-Indent Lines,
  Move Element Left / Right (swaps call arguments, parameters and list
  entries).
- Fold Code Block folds the {...} block around the caret; Expand to Level
  and Expand All to Level 1-5.
- Custom folding regions (//region, #region, <editor-fold>) fold to their
  description; Navigate > Custom Folding lists them.
- Error Description shows the problems at the caret; Next / Previous
  Highlighted Usage steps through usage highlights.
- Editor tooltips stay below the editor's top edge, so hints on the first
  lines no longer hide under the tab bar.

Navigation, project and run
- Navigate > File Path lists the file and its folders to reveal in the file
  manager.
- Copy... (F5) opens the Copy dialog for the selection or the open file; the
  project view's Refactor submenu gets Copy File.
- Debug... chooses a configuration to debug; Quick Evaluate Expression shows
  the selection's value in an expandable hint.
- Inspect Current File runs the inspection without the scope dialog.
- Surround with Live Template lists only $SELECTION$ templates; Java gets
  Callable, read / write lock and iterate templates.

Windows
- Stretch the active tool window left / right / up / down, Restore Default
  Layout, Go to Next / Previous Splitter, Close Active Tab on a second key,
  Stop Background Processes.

All actions have default shortcuts in both keymaps, VS Code preset entries,
main menu items and Chinese translations.
2026-10-28 23:49:14 +08:00

158 рядки
4.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { registerActions } from "@/platform/actions";
import { useProject } from "@/platform/project";
import { resolveMenuItems, type MenuItem } from "@/ui";
import { registerMenuGroup } from "@/workbench/contributions";
import { contextMenuItems, revealLabel } from "./contextMenu";
import type { ProjectNode } from "./model";
import { useProjectTree } from "./store";
const ROOT = "/p";
const SCRATCHES = "/config/scratches";
const node = (path: string, dir = false, root = false): ProjectNode => ({
key: path,
path,
chain: [path],
label: path.slice(path.lastIndexOf("/") + 1),
dir,
root,
entry: null,
ignored: false,
excluded: false,
symlink: false,
});
/** Labels as the menu shows them ("-" for separators). */
const labels = (items: MenuItem[]) => resolveMenuItems(items).map((r) => (r.kind === "separator" ? "-" : r.label));
const sub = (items: MenuItem[], label: string) => {
const item = resolveMenuItems(items).find((r) => r.label === label);
return item?.submenu ? labels(item.submenu) : undefined;
};
const OTHER_ACTIONS = [
"find.inPath",
"find.replaceInPath",
"code.reformatFile",
"code.optimizeImportsFile",
"terminal.openAt",
"vcs.localHistory",
"localHistory.putLabel",
"diff.compareWith",
"diff.compareFiles",
"vcs.commit",
"vcs.addToVcs",
"vcs.annotate",
"vcs.showDiff",
"vcs.rollback",
"vcs.push",
];
let disposers: Array<() => void> = [];
beforeEach(() => {
useProjectTree.setState({ root: ROOT });
useProject.setState({
project: { name: "p", root: ROOT, isGit: true, settings: {} } as never,
settings: {} as never,
});
disposers = [
registerActions(OTHER_ACTIONS.map((id) => ({ id, title: id, run: () => undefined }))),
registerMenuGroup({
id: "test.run",
menus: ["projectView"],
items: (_menu, ctx) =>
ctx.paths.length === 1 && !ctx.dirs?.[0] ? [{ label: "Run 'a'", icon: "run", run: () => undefined }] : [],
}),
];
});
afterEach(() => {
disposers.forEach((d) => d());
useProject.setState({ project: null, settings: null });
useProjectTree.setState({ root: null });
});
describe("project view context menu", () => {
it("follows IDEA's order for a file", () => {
const items = contextMenuItems([node(`${ROOT}/src/a.py`)]);
expect(labels(items)).toEqual([
"New",
"-",
"Cut",
"Copy",
"Copy Path/Reference…",
"Paste",
"-",
"Find Usages",
"Find in Files…",
"Replace in Files…",
"-",
"Refactor",
"-",
"Reformat Code",
"Optimize Imports",
"Delete…",
"-",
"Run 'a'",
"-",
"Open In",
"-",
"Local History",
"Reload from Disk",
"-",
"Git",
"-",
"Compare With…",
]);
expect(sub(items, "Refactor")).toEqual(["Rename…", "Move File…", "Copy File…", "-", "Safe Delete…"]);
expect(sub(items, "Open In")).toEqual([revealLabel(), "Terminal"]);
expect(sub(items, "Git")?.slice(0, 4)).toEqual(["Commit File…", "Add", "-", "Annotate with Git Blame"]);
});
it("shows Mark Directory as for folders, without file-only items", () => {
const l = labels(contextMenuItems([node(`${ROOT}/src`, true)]));
expect(l).toContain("Mark Directory as");
expect(sub(contextMenuItems([node(`${ROOT}/src`, true)]), "Refactor")).toContain("Move Directory…");
for (const fileOnly of ["Find Usages", "Reformat Code", "Optimize Imports", "Compare With…", "Run 'a'"])
expect(l).not.toContain(fileOnly);
});
it("keeps the project root menu short and disables what cannot apply", () => {
const items = contextMenuItems([node(ROOT, true, true)]);
const l = labels(items);
expect(l).not.toContain("Refactor");
expect(l).not.toContain("Mark Directory as");
const resolved = resolveMenuItems(items);
for (const label of ["Cut", "Copy", "Delete…"])
expect(resolved.find((r) => r.label === label)?.disabled).toBe(true);
});
it("drops project-only groups for scratch files", () => {
const l = labels(contextMenuItems([node(`${SCRATCHES}/s.py`)]));
expect(l).not.toContain("Git");
expect(l).not.toContain("Find Usages");
expect(l).toContain("Refactor");
expect(l).toContain("Reload from Disk");
});
it("offers Compare Files for two files", () => {
const l = labels(contextMenuItems([node(`${ROOT}/a.py`), node(`${ROOT}/b.py`)]));
expect(l).toContain("Compare Files");
expect(l).not.toContain("Compare With…");
});
it("never shows an empty submenu", () => {
disposers.forEach((d) => d());
disposers = [];
useProject.setState({
project: { name: "p", root: ROOT, isGit: true, settings: {} } as never,
settings: {} as never,
});
const l = labels(contextMenuItems([node(`${ROOT}/a.py`)]));
expect(l).not.toContain("Git");
expect(l).not.toContain("Local History");
expect(l).not.toContain("Find in Files…");
});
});