From a601a51b94dbfb45c21eac079948ab1a53afa64f Mon Sep 17 00:00:00 2001 From: zhangdagou Date: Sun, 8 Nov 2026 22:21:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(build-tools):=20=E4=BC=98=E5=8C=96=20Maven?= =?UTF-8?q?=20=E5=91=BD=E4=BB=A4=E5=88=86=E7=BB=84=E4=B8=8E=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=E6=B5=8B=E8=AF=95=E7=9A=84=E8=A7=86=E8=A7=89=E5=8F=8D?= =?UTF-8?q?=E9=A6=88=20(#2095)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Maven command window listed recent commands and goals in one flat list, and Skip Tests gave no sign that the test phase would not run. Recent commands and goals now show as separate groups, in the Gradle entry point too, and with Skip Tests on the Maven test phase is greyed and struck through. Run Anything uses the UI font and group separators. Siblings covered: selection and Tab completion skip group headers and stay valid while typing, and Run Anything keeps results that other providers contribute. Co-authored-by: PLZY <41029728+PLZY@users.noreply.github.com> --- src/features/build-tools/BuildToolView.tsx | 11 +- .../build-tools/BuildTools.module.css | 47 +++++-- .../build-tools/ExecutePopup.test.tsx | 132 ++++++++++++++++++ src/features/build-tools/ExecutePopup.tsx | 99 +++++++++---- src/features/build-tools/actions.test.tsx | 47 ++++++- src/features/build-tools/runAnything.ts | 6 +- .../run-anything/RunAnything.module.css | 25 +++- src/features/run-anything/model.test.ts | 76 ++++++++++ src/features/run-anything/model.ts | 31 ++-- src/platform/locales/text/en.json | 4 +- src/platform/locales/text/zh.json | 4 +- src/workbench/contributions.ts | 2 + 12 files changed, 432 insertions(+), 52 deletions(-) create mode 100644 src/features/build-tools/ExecutePopup.test.tsx 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;