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
This commit is contained in:
l0ng-ai
2026-11-11 23:21:53 +08:00
committed by GitHub
parent 6983e8ec2f
commit e98a560523
2 changed files with 66 additions and 2 deletions
+31 -1
View File
@@ -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(
<ConfigForm
config={fresh}
all={[fresh]}
type={dotnetProjectType}
errors={{}}
modifyRequest={0}
onChange={vi.fn()}
/>,
);
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(
+35 -1
View File
@@ -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<Record<string, string>>): {
@@ -28,6 +29,38 @@ export function dotnetCommand(p: Readonly<Record<string, string>>, 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<string, string> | null {
if (!/^dotnet(\.exe)?$/i.test(commandName(c.command))) return null;
const [verb, ...rest] = c.args;
if (verb !== "run") return null;
const p: Record<string, string> = c.command === "dotnet" ? {} : { executable: c.command };
const flags: Record<string, string> = {
"--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 ?? {};