From e98a5605234894cfc03078f7d400a2b3fcce9fb7 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:21:53 +0800 Subject: [PATCH] fix(run): open .NET Project configurations as the form with a project field (#2757) The .NET Project type had no way back from a command line to its fields, and a new configuration starts without params, so the editor fell back to the plain Command/Arguments form: there was no .NET project field, and Debug failed with "Select a .NET project". dotnet run command lines the form can express now map back to its fields, for new configurations and ones typed as a command. Fixes #2733 --- src/features/run/dotnetTypes.test.tsx | 32 +++++++++++++++++++++++- src/features/run/dotnetTypes.ts | 36 ++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/features/run/dotnetTypes.test.tsx b/src/features/run/dotnetTypes.test.tsx index 93d5f16a9..aef05b425 100644 --- a/src/features/run/dotnetTypes.test.tsx +++ b/src/features/run/dotnetTypes.test.tsx @@ -6,7 +6,7 @@ import { useProject } from "@/platform/project"; import { dotnetBuildConfig, dotnetProjectType, dotnetTestType } from "./dotnetTypes"; import { ConfigForm } from "./ConfigForm"; import { validateConfig } from "./configs"; -import { newConfigOfType, registerBuiltinTypes, withParams } from "./types"; +import { formParams, newConfigOfType, registerBuiltinTypes, withParams } from "./types"; const resolveDotnet = vi.hoisted(() => vi.fn()); vi.mock("@/platform/ipc", async (importOriginal) => { @@ -69,6 +69,36 @@ describe(".NET configurations", () => { ]); }); + it("opens a new configuration, and a typed dotnet run command, as the form with its project field", () => { + const fresh = newConfigOfType(dotnetProjectType, []); + expect(formParams(fresh, dotnetProjectType)).toEqual({ project: "" }); + render( + , + ); + expect(screen.getByLabelText(".NET project")).toBeTruthy(); + // #2733: typed into the command fields of a configuration without params. + const typed: RunConfig = { + ...fresh, + params: {}, + command: "dotnet", + args: ["run", "--project", "Worker.Service", "--no-launch-profile", "--", "--verbose"], + }; + expect(formParams(typed, dotnetProjectType)).toEqual({ + project: "Worker.Service", + programArgs: "--verbose", + }); + // Options the form has no field for keep the command editor. + expect(formParams({ ...typed, args: ["run", "--project", "A", "--no-build"] }, dotnetProjectType)).toBeNull(); + expect(formParams({ ...typed, args: ["build", "A"] }, dotnetProjectType)).toBeNull(); + }); + it("builds edited project/framework from the open root instead of a profile cwd", () => { useProject.setState({ project: { root: "/repo" } as never }); const c = withParams( diff --git a/src/features/run/dotnetTypes.ts b/src/features/run/dotnetTypes.ts index e9ca7a8c5..1eb0b93bb 100644 --- a/src/features/run/dotnetTypes.ts +++ b/src/features/run/dotnetTypes.ts @@ -1,7 +1,8 @@ import { call, commands, type RunConfig } from "@/platform/ipc"; import { useProject } from "@/platform/project"; import type { RunConfigTypeContribution } from "@/workbench/contributions"; -import { parseArgs } from "./configs"; +import { formatArgs, parseArgs } from "./configs"; +import { commandName } from "./commandName"; import { prepareDotnetTest } from "@/platform/dotnet"; function profile(p: Readonly>): { @@ -28,6 +29,38 @@ export function dotnetCommand(p: Readonly>, test = false) }; } +/** + * The form fields of a `dotnet run` command line ([`dotnetCommand`] reversed), or `null` when it + * uses anything the form can't show. Lets a new configuration, and one typed as a command, open + * as the form with its project field. + */ +function dotnetRunParams(c: RunConfig): Record | null { + if (!/^dotnet(\.exe)?$/i.test(commandName(c.command))) return null; + const [verb, ...rest] = c.args; + if (verb !== "run") return null; + const p: Record = c.command === "dotnet" ? {} : { executable: c.command }; + const flags: Record = { + "--project": "project", + "--framework": "framework", + "--configuration": "configuration", + "--launch-profile": "launchProfile", + }; + for (let i = 0; i < rest.length; i++) { + const arg = rest[i]!; + if (arg === "--") { + p.programArgs = formatArgs(rest.slice(i + 1)); + break; + } + if (arg === "--no-launch-profile") continue; + const key = flags[arg]; + const value = rest[i + 1]; + if (!key || value === undefined) return null; + p[key] = value; + i++; + } + return { project: "", ...p }; +} + /** Build the current project/framework, independently of the run's profile working directory. */ export function dotnetBuildConfig(config: RunConfig): RunConfig { const p = config.params ?? {}; @@ -104,6 +137,7 @@ export const dotnetProjectType: RunConfigTypeContribution = { }, ], commandLine: (p) => dotnetCommand(p), + paramsFrom: dotnetRunParams, async prepareLaunch(config) { const resolved = await call(commands.runDotnetResolve(config, false)); const p = resolved.params ?? {};