дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
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.
158 рядки
4.9 KiB
TypeScript
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…");
|
|
});
|
|
});
|