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;