diff --git a/src/features/build-tools/BuildToolView.tsx b/src/features/build-tools/BuildToolView.tsx
index 898ebd1f1..5bec54b98 100644
--- a/src/features/build-tools/BuildToolView.tsx
+++ b/src/features/build-tools/BuildToolView.tsx
@@ -232,6 +232,7 @@ function BuildBanner({ info, tool }: { info: MavenModel | GradleModel; tool: Bui
function NodeRow({ node, state }: { node: BuildNode; state: TreeRowState }) {
const choices = useBuild((s) => s.settings.maven.profiles);
+ const skipTests = useBuild((s) => s.settings.maven.skipTests);
if (node.kind === "profile") {
return (
e.stopPropagation()} onDoubleClick={(e) => e.stopPropagation()}>
@@ -255,7 +256,15 @@ function NodeRow({ node, state }: { node: BuildNode; state: TreeRowState }) {
return (
- {nodeLabel(node)}
+
+ {nodeLabel(node)}
+
{detail && {detail}}
);
diff --git a/src/features/build-tools/BuildTools.module.css b/src/features/build-tools/BuildTools.module.css
index 9ec659220..0e63104ec 100644
--- a/src/features/build-tools/BuildTools.module.css
+++ b/src/features/build-tools/BuildTools.module.css
@@ -79,6 +79,10 @@
text-overflow: ellipsis;
color: var(--fg);
}
+.skippedGoal {
+ color: var(--fg-muted);
+ text-decoration: line-through;
+}
.selectedLabel {
color: var(--fg-strong);
}
@@ -95,7 +99,7 @@
/* Execute Maven Goal / Gradle Task popup */
.popup {
- width: 560px;
+ width: 620px;
max-width: calc(100vw - 32px);
overflow: hidden;
}
@@ -124,18 +128,37 @@
padding: 6px 10px;
border-bottom: 1px solid var(--border-popup);
}
+.commandIcon {
+ flex: none;
+}
.inputPrefix {
flex: none;
- font-family: var(--font-mono);
- font-size: var(--fs-code);
- color: var(--fg-subtle);
+ font-family: var(--font-ui);
+ font-size: var(--fs-ui);
+ color: var(--fg-strong);
}
.input {
flex: 1;
- font-family: var(--font-mono);
+ min-width: 0;
+ font-family: var(--font-ui);
+}
+.completionSection {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ gap: 10px;
+ padding: 0;
+ color: var(--fg-subtle);
+ font-size: var(--fs-small);
+}
+.completionSection::after {
+ content: "";
+ flex: 1;
+ height: 1px;
+ background: var(--border-popup);
}
.completions {
- max-height: 280px;
+ max-height: min(380px, 55vh);
height: auto;
}
.completion {
@@ -146,8 +169,10 @@
flex: 1;
}
.completionLabel {
- font-family: var(--font-mono);
- font-size: var(--fs-code);
+ flex: 1;
+ min-width: 0;
+ font-family: var(--font-ui);
+ font-size: var(--fs-ui);
color: var(--fg-emphasis);
white-space: pre;
overflow: hidden;
@@ -157,6 +182,12 @@
color: var(--fg-strong);
}
.completionDetail {
+ flex: 0 1 auto;
+ min-width: 0;
+ max-width: 45%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-style: italic;
margin-left: auto;
color: var(--fg-subtle);
font-size: var(--fs-small);
diff --git a/src/features/build-tools/ExecutePopup.test.tsx b/src/features/build-tools/ExecutePopup.test.tsx
new file mode 100644
index 000000000..f24f80f56
--- /dev/null
+++ b/src/features/build-tools/ExecutePopup.test.tsx
@@ -0,0 +1,132 @@
+import { afterEach, beforeEach, expect, it } from "vitest";
+import { act, cleanup, fireEvent, render, screen, within } from "@testing-library/react";
+import type { GradleModel } from "@/platform/ipc";
+import { ExecutePopupHost } from "./ExecutePopup";
+import { goalCompletions } from "./model";
+import { pushHistory, useBuild } from "./store";
+
+beforeEach(() => {
+ localStorage.clear();
+ useBuild.setState({ root: "/p", model: null, executeOpen: "maven" });
+});
+afterEach(() => {
+ cleanup();
+ useBuild.setState({ executeOpen: null });
+});
+
+it("separates recent commands from Maven goals and accepts a recent command with Tab", () => {
+ pushHistory("maven", "package -o");
+ render();
+ expect(screen.getByText("Recent")).toBeTruthy();
+ expect(screen.getByText("Maven Goals")).toBeTruthy();
+ const input = screen.getByRole("textbox");
+ fireEvent.keyDown(input, { key: "Tab" });
+ expect(input).toHaveValue("package -o");
+});
+
+it("skips section headers during keyboard navigation", () => {
+ pushHistory("maven", "package -o");
+ render();
+ const input = screen.getByRole("textbox");
+ fireEvent.keyDown(input, { key: "ArrowDown" });
+ fireEvent.keyDown(input, { key: "ArrowDown" });
+ fireEvent.keyDown(input, { key: "Tab" });
+ expect(input).toHaveValue("clean ");
+});
+
+it("omits the empty Recent section when no commands have been run", () => {
+ render();
+ expect(screen.queryByText("Recent")).toBeNull();
+ expect(screen.getByText("Maven Goals")).toBeTruthy();
+ const input = screen.getByRole("textbox");
+ fireEvent.keyDown(input, { key: "Tab" });
+ expect(input).toHaveValue("clean ");
+});
+
+it("starts PageUp on a completion rather than a section", () => {
+ render();
+ const input = screen.getByRole("textbox");
+ fireEvent.keyDown(input, { key: "PageUp" });
+ fireEvent.keyDown(input, { key: "Enter" });
+ expect(input).toHaveValue("clean ");
+});
+
+it.each([
+ [["ArrowUp"], "site-deploy "],
+ [["ArrowUp", "ArrowDown"], "package -o"],
+ [["ArrowDown", "ArrowUp"], "site-deploy "],
+ [["ArrowDown", "ArrowDown", "ArrowUp"], "package -o"],
+ [["PageDown", "PageUp"], "package -o"],
+] as const)("navigates sections and wraps with %j", (keys, expected) => {
+ pushHistory("maven", "package -o");
+ render();
+ const input = screen.getByRole("textbox");
+ for (const key of keys) fireEvent.keyDown(input, { key });
+ fireEvent.keyDown(input, { key: "Tab" });
+ expect(input).toHaveValue(expected);
+});
+
+it("clears selection when typing changes the rows, and ignores section clicks", () => {
+ pushHistory("maven", "package -o");
+ render();
+ const input = screen.getByRole("textbox");
+ fireEvent.keyDown(input, { key: "ArrowUp" });
+ fireEvent.change(input, { target: { value: "cl" } });
+ fireEvent.click(screen.getByText("Maven Goals"));
+ expect(input).toHaveValue("cl");
+ fireEvent.keyDown(input, { key: "Tab" });
+ expect(input).toHaveValue("clean ");
+});
+
+it("keeps an empty completion list safe for all navigation and acceptance keys", () => {
+ render();
+ const input = screen.getByRole("textbox");
+ fireEvent.change(input, { target: { value: "-Dunknown" } });
+ for (const key of ["ArrowDown", "ArrowUp", "PageDown", "PageUp", "Tab", "Enter"]) {
+ fireEvent.keyDown(input, { key });
+ expect(input).toHaveValue("-Dunknown");
+ }
+ expect(screen.queryByText("Maven Goals")).toBeNull();
+});
+
+it("PageDown selects a completion rather than a section at its landing position", () => {
+ const history = Array.from({ length: 9 }, (_, i) => `command-${i}`);
+ for (const line of history) pushHistory("maven", line);
+ render();
+ const input = screen.getByRole("textbox");
+ fireEvent.keyDown(input, { key: "ArrowDown" });
+ fireEvent.keyDown(input, { key: "PageDown" });
+ fireEvent.keyDown(input, { key: "Enter" });
+ expect(input).toHaveValue(`${goalCompletions("", [])[0]!.line} `);
+});
+
+it("resets selection when changing the Gradle build used for completion", () => {
+ const build = (dir: string, name: string): GradleModel => ({
+ dir,
+ projects: [
+ { path: ":", name, dir, buildFile: "build.gradle.kts", tasks: [{ name, group: "build", description: null }] },
+ ],
+ javaVersion: null,
+ command: "gradle",
+ env: {},
+ syncedAt: null,
+ stale: false,
+ syncError: null,
+ });
+ useBuild.setState({
+ executeOpen: "gradle",
+ model: { maven: null, gradle: [build("a", "assemble"), build("b", "build")] },
+ });
+ render();
+ const input = screen.getByRole("textbox");
+ expect(screen.getByText("Gradle Tasks")).toBeTruthy();
+ fireEvent.keyDown(input, { key: "ArrowDown" });
+ fireEvent.click(screen.getByRole("button", { name: "Project to run in" }));
+ fireEvent.click(within(screen.getByRole("listbox", { name: "Project to run in" })).getByText("build"));
+ expect(screen.queryByRole("option", { selected: true })).toBeNull();
+ fireEvent.keyDown(input, { key: "Tab" });
+ expect(input).toHaveValue("build ");
+ act(() => useBuild.setState({ model: { maven: null, gradle: [] } }));
+ fireEvent.keyDown(input, { key: "Tab" });
+ expect(input).toHaveValue("build ");
+});
diff --git a/src/features/build-tools/ExecutePopup.tsx b/src/features/build-tools/ExecutePopup.tsx
index a4ac94853..77d731f27 100644
--- a/src/features/build-tools/ExecutePopup.tsx
+++ b/src/features/build-tools/ExecutePopup.tsx
@@ -7,12 +7,18 @@
import { useMemo, useState, type KeyboardEvent } from "react";
import clsx from "clsx";
import type { BuildTool } from "@/platform/ipc";
-import { HighlightedText, Input, List, Popup, Select, listNavigate, T } from "@/ui";
+import { HighlightedText, Icon, Input, List, Popup, Select, listNavigate, T } from "@/ui";
import { goalCompletions, gradleProjectForKey, gradleProjectKey, taskCompletions, type Completion } from "./model";
import { executeBuildLine } from "./runAnything";
import { readHistory, useBuild } from "./store";
import styles from "./BuildTools.module.css";
+type CompletionRow =
+ | { kind: "section"; key: string; label: string }
+ | { kind: "completion"; key: string; completion: Completion };
+
+const isSection = (row: CompletionRow) => row.kind === "section";
+
export function ExecutePopupHost() {
const tool = useBuild((s) => s.executeOpen);
if (!tool) return null;
@@ -23,7 +29,7 @@ function ExecutePopup({ tool }: { tool: BuildTool }) {
const model = useBuild((s) => s.model);
const current = useBuild((s) => s.current[tool]);
const [line, setLine] = useState("");
- const [active, setActive] = useState(-1);
+ const [selection, setSelection] = useState<{ rows: CompletionRow[]; index: number } | null>(null);
const history = useMemo(() => readHistory(tool), [tool]);
const maven = model?.maven;
const gradle = model?.gradle;
@@ -54,6 +60,33 @@ function ExecutePopup({ tool }: { tool: BuildTool }) {
return selected ? taskCompletions(line, selected.build, history).slice(0, 200) : [];
}, [tool, maven, gradle, project, line, history]);
+ const command = tool === "maven" ? "mvn" : "gradle";
+ const commandIcon = tool === "maven" ? "file-maven" : "file-gradle";
+ const rows = useMemo(() => {
+ const sections = [
+ { key: "recent", label: "Recent", items: completions.filter((item) => item.detail === "recent") },
+ {
+ key: "commands",
+ label: tool === "maven" ? "Maven Goals" : "Gradle Tasks",
+ items: completions.filter((item) => item.detail !== "recent"),
+ },
+ ];
+ return sections.flatMap((section) => {
+ if (!section.items.length) return [];
+ return [
+ { kind: "section", key: section.key, label: section.label },
+ ...section.items.map((completion): CompletionRow => ({
+ kind: "completion",
+ key: `${section.key}:${completion.line}`,
+ completion,
+ })),
+ ];
+ });
+ }, [completions, tool]);
+ const firstCompletion = rows.findIndex((row) => !isSection(row));
+ const active = selection?.rows === rows ? selection.index : -1;
+ const setActive = (index: number) => setSelection({ rows, index });
+
const close = () => useBuild.setState({ executeOpen: null });
const execute = () => {
@@ -61,27 +94,28 @@ function ExecutePopup({ tool }: { tool: BuildTool }) {
if (text && executeBuildLine(tool, text, project)) close();
};
- const accept = (c: Completion | undefined) => {
- if (!c) return;
- setLine(c.detail === "recent" ? c.line : `${c.line} `);
+ const accept = (row: CompletionRow | undefined) => {
+ if (row?.kind !== "completion") return;
+ const completion = row.completion;
+ setLine(completion.detail === "recent" ? completion.line : `${completion.line} `);
setActive(-1);
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "PageDown" || e.key === "PageUp") {
- const next = listNavigate(e.key, { items: completions, active: Math.max(active, 0), wrap: true });
+ const next = listNavigate(e.key, { items: rows, active, wrap: true, isSection });
if (next !== null) {
e.preventDefault();
- setActive(active < 0 && e.key === "ArrowDown" ? 0 : next);
+ setActive(next < 0 ? firstCompletion : next);
}
return;
}
if (e.key === "Tab") {
e.preventDefault();
- accept(completions[Math.max(active, 0)]);
+ accept(rows[active < 0 ? firstCompletion : active]);
} else if (e.key === "Enter") {
e.preventDefault();
- if (active >= 0) accept(completions[active]);
+ if (active >= 0) accept(rows[active]);
else execute();
}
};
@@ -107,7 +141,8 @@ function ExecutePopup({ tool }: { tool: BuildTool }) {
- {tool === "maven" ? "mvn" : "gradle"}
+
+ {command}
`${c.detail === "recent" ? "h" : "c"}:${c.line}`}
+ items={rows}
+ getKey={(row) => row.key}
+ isSection={isSection}
+ renderSection={(row) => (
+ {row.kind === "section" && {row.label}}
+ )}
activeIndex={active}
onActiveChange={setActive}
onActivate={(c) => accept(c)}
focusable={false}
hoverSelects={false}
- rowHeight={22}
+ rowHeight={24}
+ sectionHeight={28}
className={styles.completions}
aria-label="Completions"
empty={{line.trim() ? "No completions" : "Type a goal"}
}
- renderItem={(c, state) => (
-
-
-
-
- {c.detail && {c.detail}}
-
- )}
+ renderItem={(row, state) => {
+ if (row.kind !== "completion") return null;
+ const completion = row.completion;
+ const recent = completion.detail === "recent";
+ const matchStart = completion.line.length - completion.label.length;
+ const indices = recent
+ ? undefined
+ : Array.from(
+ { length: Math.min(word.length, completion.label.length) },
+ (_, index) => matchStart + index,
+ );
+ const detail = recent || completion.detail === "phase" ? undefined : completion.detail;
+ return (
+
+
+
+ {command}
+
+ {detail && {detail}}
+
+ );
+ }}
/>
diff --git a/src/features/build-tools/actions.test.tsx b/src/features/build-tools/actions.test.tsx
index 9944b0f00..d2980516e 100644
--- a/src/features/build-tools/actions.test.tsx
+++ b/src/features/build-tools/actions.test.tsx
@@ -4,9 +4,11 @@ import { allActions, executeAction, isActionEnabled, registerAction } from "@/pl
import { getContext, setContext } from "@/platform/context";
import type { BuildModel, RunConfig } from "@/platform/ipc";
import { useContributions } from "@/workbench/contributions";
-import { MavenView } from "./BuildToolView";
+import { GradleView, MavenView } from "./BuildToolView";
import { activate } from "./index";
+import { gradleProjectKey } from "./model";
import { useBuild } from "./store";
+import styles from "./BuildTools.module.css";
const mavenModel: BuildModel = {
maven: {
@@ -119,7 +121,12 @@ describe("build tools feature", () => {
await executeAction("maven.toggleSkipTests");
render();
expect(screen.getByText("Build files changed since the last reload")).toBeTruthy();
- const phase = screen.getByText("package");
+ const phase = screen.getByText("test");
+ expect(phase).toHaveClass(styles.skippedGoal!);
+ await act(() => executeAction("maven.toggleSkipTests"));
+ expect(phase).not.toHaveClass(styles.skippedGoal!);
+ await act(() => executeAction("maven.toggleSkipTests"));
+ expect(phase).toHaveClass(styles.skippedGoal!);
await act(async () => {
phase.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));
});
@@ -128,10 +135,42 @@ describe("build tools feature", () => {
const { config, temporary } = started[0] as { config: RunConfig; temporary: boolean };
expect(temporary).toBe(true);
expect(config).toMatchObject({
- name: "spring-demo [package]",
+ name: "spring-demo [test]",
command: "/p/mvnw",
- args: ["package", "-DskipTests"],
+ args: ["test", "-DskipTests"],
kind: "maven",
});
});
+
+ it("does not mark Gradle test tasks as skipped when Maven Skip Tests is on", async () => {
+ const build = {
+ dir: "",
+ projects: [
+ {
+ path: ":",
+ name: "demo",
+ dir: "",
+ buildFile: "build.gradle.kts",
+ tasks: [{ name: "test", group: "verification", description: null }],
+ },
+ ],
+ command: "gradle",
+ env: {},
+ javaVersion: null,
+ syncedAt: null,
+ stale: false,
+ syncError: null,
+ };
+ const key = gradleProjectKey(build, build.projects[0]!);
+ act(() =>
+ useBuild.setState({
+ root: "/p",
+ model: { maven: mavenModel.maven, gradle: [build] },
+ expanded: { maven: new Set(), gradle: new Set([key, `${key}:tasks`, `${key}:tasks:verification`]) },
+ }),
+ );
+ await executeAction("maven.toggleSkipTests");
+ render();
+ expect(screen.getByText("test")).not.toHaveClass(styles.skippedGoal!);
+ });
});
diff --git a/src/features/build-tools/runAnything.ts b/src/features/build-tools/runAnything.ts
index 0861ccbe8..d272979cd 100644
--- a/src/features/build-tools/runAnything.ts
+++ b/src/features/build-tools/runAnything.ts
@@ -83,7 +83,11 @@ export const buildToolsRunAnything: RunAnythingProvider = {
.map((c): RunAnythingItem => ({
key: `${c.detail === "recent" ? "h" : "c"}:${c.line}`,
label: `${WORD[tool]} ${c.line}`,
- detail: c.detail,
+ detail: c.detail === "recent" || c.detail === "phase" ? undefined : c.detail,
+ group:
+ c.detail === "recent"
+ ? { id: "recent", label: "Recent" }
+ : { id: tool, label: tool === "maven" ? "Maven Goals" : "Gradle Tasks" },
icon: tool === "maven" ? "file-maven" : "file-gradle",
completion: `${WORD[tool]} ${c.detail === "recent" ? c.line : `${c.line} `}`,
run: () => void executeBuildLine(tool, c.line),
diff --git a/src/features/run-anything/RunAnything.module.css b/src/features/run-anything/RunAnything.module.css
index 426d29660..25d66f8e6 100644
--- a/src/features/run-anything/RunAnything.module.css
+++ b/src/features/run-anything/RunAnything.module.css
@@ -45,17 +45,27 @@
}
.input {
flex: 1;
- font-family: var(--font-mono);
+ font-family: var(--font-ui);
}
.rows {
max-height: 360px;
height: auto;
}
.section {
- padding: 4px 10px 0;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ gap: 10px;
+ padding: 0;
font-size: var(--fs-small);
color: var(--fg-subtle);
}
+.section::after {
+ content: "";
+ flex: 1;
+ height: 1px;
+ background: var(--border-popup);
+}
.row {
display: flex;
align-items: center;
@@ -68,18 +78,25 @@
color: var(--fg-muted);
}
.label {
+ flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
- font-family: var(--font-mono);
- font-size: var(--fs-code);
+ font-family: var(--font-ui);
+ font-size: var(--fs-ui);
color: var(--fg-emphasis);
}
.activeRow .label {
color: var(--fg-strong);
}
.detail {
+ flex: 0 1 auto;
+ min-width: 0;
+ max-width: 45%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-style: italic;
margin-left: auto;
color: var(--fg-subtle);
font-size: var(--fs-small);
diff --git a/src/features/run-anything/model.test.ts b/src/features/run-anything/model.test.ts
index 2bafac8e7..a6e31dd9f 100644
--- a/src/features/run-anything/model.test.ts
+++ b/src/features/run-anything/model.test.ts
@@ -71,3 +71,79 @@ describe("run anything", () => {
expect(cfg.name).toHaveLength(58);
});
});
+
+describe("provider command sections", () => {
+ it("preserves same-label results, order and the limit for providers without item groups", () => {
+ const items = Array.from({ length: 10 }, (_, i) => ({
+ key: String(i),
+ label: "Run",
+ detail: `configuration ${i}`,
+ completion: "run ",
+ run: () => {},
+ }));
+ const providers = ["first", "second"].map((id) => ({ id, group: id, items: () => items }));
+ const rows = buildRows("", ["run"], providers, () => {});
+ expect(rows.filter((r) => r.kind === "section").map((r) => r.key)).toEqual(["s:recent", "s:first", "s:second"]);
+ for (const id of ["first", "second"]) {
+ const results = rows.filter((r) => r.kind === "item" && r.group === id);
+ expect(results.map((r) => r.kind === "item" && r.item)).toEqual(items.slice(0, 8));
+ }
+ });
+
+ it("merges build-tool recents with shared history without duplicate commands", () => {
+ const provider: RunAnythingProvider = {
+ id: "build-tools",
+ group: "Build Tools",
+ items: () => [
+ {
+ key: "recent",
+ label: "mvn clean",
+ completion: "mvn clean ",
+ group: { id: "recent", label: "Recent" },
+ run: () => {},
+ },
+ { key: "goal", label: "mvn clean", group: { id: "maven", label: "Maven Goals" }, run: () => {} },
+ ],
+ };
+ const rows = buildRows("mvn", ["mvn clean"], [provider], () => {});
+ expect(rows.map((r) => (r.kind === "section" ? r.label : r.item.label))).toEqual([
+ "Recent",
+ "mvn clean",
+ "Maven Goals",
+ "mvn clean",
+ ]);
+ expect(new Set(rows.map((r) => r.key)).size).toBe(rows.length);
+ });
+
+ it("limits each section separately so recent commands cannot hide available goals", () => {
+ const provider: RunAnythingProvider = {
+ id: "build-tools",
+ group: "Build Tools",
+ items: () => [
+ ...Array.from({ length: 20 }, (_, i) => ({
+ key: `recent-${i}`,
+ label: `mvn command-${i}`,
+ group: { id: "recent", label: "Recent" },
+ run: () => {},
+ })),
+ { key: "compile", label: "mvn compile", group: { id: "maven", label: "Maven Goals" }, run: () => {} },
+ ],
+ };
+ const rows = buildRows("mvn", [], [provider], () => {});
+ expect(rows.filter((r) => r.kind === "item" && r.group === "recent")).toHaveLength(8);
+ expect(rows.at(-1)).toMatchObject({ kind: "item", item: { label: "mvn compile" } });
+ });
+
+ it("keeps groups from different providers separate even when their local ids match", () => {
+ const providers: RunAnythingProvider[] = ["maven", "gradle"].map((id) => ({
+ id,
+ group: id,
+ items: () => [{ key: "build", label: `${id} build`, group: { id: "commands", label: id }, run: () => {} }],
+ }));
+ const rows = buildRows("", [], providers, () => {});
+ expect(rows.filter((r) => r.kind === "section").map((r) => r.key)).toEqual([
+ "s:maven:commands",
+ "s:gradle:commands",
+ ]);
+ });
+});
diff --git a/src/features/run-anything/model.ts b/src/features/run-anything/model.ts
index b454cfac5..0d77edeed 100644
--- a/src/features/run-anything/model.ts
+++ b/src/features/run-anything/model.ts
@@ -64,13 +64,18 @@ export function buildRows(
providers: readonly RunAnythingProvider[],
runLine: (line: string) => void,
): Row[] {
- const rows: Row[] = [];
- const section = (key: string, label: string, items: RunAnythingItem[]) => {
- if (!items.length) return;
- rows.push({ kind: "section", key: `s:${key}`, label });
- for (const item of items) rows.push({ kind: "item", key: `${key}:${item.key}`, item, group: key });
+ const groups = new Map }>();
+ const addItems = (key: string, label: string, items: RunAnythingItem[], deduplicate = false) => {
+ const group = groups.get(key) ?? { label, items: [], commands: new Set() };
+ for (const item of items) {
+ const command = (item.completion ?? item.label).trim();
+ if (deduplicate && group.commands.has(command)) continue;
+ group.commands.add(command);
+ group.items.push(item);
+ }
+ if (group.items.length) groups.set(key, group);
};
- section(
+ addItems(
"recent",
"Recent",
history
@@ -81,11 +86,21 @@ export function buildRows(
for (const p of providers) {
let items: RunAnythingItem[] = [];
try {
- items = p.items(query).slice(0, GROUP_LIMIT);
+ items = p.items(query);
} catch (e) {
console.error(`run anything provider ${p.id} failed`, e);
}
- section(p.id, p.group, items);
+ for (const item of items) {
+ const group = item.group;
+ const key = group?.id === "recent" ? "recent" : group ? `${p.id}:${group.id}` : p.id;
+ addItems(key, group?.label ?? p.group, [item], group?.id === "recent");
+ }
+ }
+ const rows: Row[] = [];
+ for (const [key, group] of groups) {
+ rows.push({ kind: "section", key: `s:${key}`, label: group.label });
+ for (const item of group.items.slice(0, GROUP_LIMIT))
+ rows.push({ kind: "item", key: `${key}:${item.key}`, item, group: key });
}
return rows;
}
diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json
index ef5b162e4..57654aaac 100644
--- a/src/platform/locales/text/en.json
+++ b/src/platform/locales/text/en.json
@@ -432,5 +432,7 @@
"Always allowed": "Always allowed",
"Declined": "Declined",
"Always declined": "Always declined",
- "Branch '{name}' is checked out in the worktree at {path}.\nDelete the worktree and the branch? Its folder is deleted.": "Branch '{name}' is checked out in the worktree at {path}.\nDelete the worktree and the branch? Its folder is deleted."
+ "Branch '{name}' is checked out in the worktree at {path}.\nDelete the worktree and the branch? Its folder is deleted.": "Branch '{name}' is checked out in the worktree at {path}.\nDelete the worktree and the branch? Its folder is deleted.",
+ "Maven Goals": "Maven Goals",
+ "Gradle Tasks": "Gradle Tasks"
}
diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json
index d81acbc2c..e529fb272 100644
--- a/src/platform/locales/text/zh.json
+++ b/src/platform/locales/text/zh.json
@@ -6784,5 +6784,7 @@
"Always allowed": "已设为始终允许",
"Declined": "已拒绝",
"Always declined": "已设为始终拒绝",
- "Branch '{name}' is checked out in the worktree at {path}.\nDelete the worktree and the branch? Its folder is deleted.": "分支 '{name}' 被位于 {path} 的工作树签出。\n删除该工作树和分支?其文件夹将被删除。"
+ "Branch '{name}' is checked out in the worktree at {path}.\nDelete the worktree and the branch? Its folder is deleted.": "分支 '{name}' 被位于 {path} 的工作树签出。\n删除该工作树和分支?其文件夹将被删除。",
+ "Maven Goals": "Maven 目标",
+ "Gradle Tasks": "Gradle 任务"
}
diff --git a/src/workbench/contributions.ts b/src/workbench/contributions.ts
index 51ac3246b..cefe7b722 100644
--- a/src/workbench/contributions.ts
+++ b/src/workbench/contributions.ts
@@ -1442,6 +1442,8 @@ export function localChangedPaths(
// running a whole command line; anything else runs as a shell command.
export interface RunAnythingItem {
+ /** Optional section within the provider; the shared `recent` id merges command histories. */
+ group?: { id: string; label: string };
/** Stable within the provider. */
key: string;
label: string;