дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(run): recognise Windows launcher names in run configuration forms (#300)
Run configurations whose command is a Windows launcher (mvn.cmd, MVN.CMD, a full C:\\…\\mvn.cmd path, gradle.bat, pytest.exe, java.exe, godot.exe, npm.cmd) opened in the generic command fallback because the Maven/Gradle, npm, pytest, Godot and Application/Spring Boot forms compared executable names exactly. A shared commandName() (basename, either slash, .exe/.cmd/.bat/.com stripped case-insensitively) now matches them, and the original launcher text is kept so the command line round-trips unchanged. Refs #244
Цей коміт міститься в:
1 джерело
d44e2414ca
коміт
181a2ea03e
4 files changed
+93
-19
No files matched your search
@@ -0,0 +1,5 @@
|
||||
/** Executable identity for form matching; keep the original command for launching. */
|
||||
export function commandName(command: string): string {
|
||||
const name = command.replace(/\\/g, "/").split("/").pop() ?? command;
|
||||
return /\.(exe|cmd|bat|com)$/i.test(name) ? name.replace(/\.(exe|cmd|bat|com)$/i, "").toLowerCase() : name;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { call, commands, type RunConfig } from "@/platform/ipc";
|
||||
import { CLASSPATH, parseJavaArgs, TEST_CLASSPATH } from "@/platform/javaLaunch";
|
||||
import type { RunConfigTypeContribution, RunConfigTypeField } from "@/workbench/contributions";
|
||||
import { formatArgs, parseArgs } from "./configs";
|
||||
import { commandName } from "./commandName";
|
||||
|
||||
export const isWindows = () => typeof navigator !== "undefined" && /win/i.test(navigator.platform);
|
||||
|
||||
@@ -38,8 +39,6 @@ const profilesField = (hint: string, visible?: RunConfigTypeField["visible"]): R
|
||||
...(visible ? { visible } : {}),
|
||||
});
|
||||
|
||||
const isJava = (command: string) => /^java(\.exe)?$/i.test(command.replace(/\\/g, "/").split("/").pop() ?? "");
|
||||
|
||||
/** Main classes of the JVM configurations, offered while typing. */
|
||||
function mainClasses(configs: readonly RunConfig[]): string[] {
|
||||
const out = new Set<string>();
|
||||
@@ -85,7 +84,7 @@ function jvmApplicationType(spring: boolean): RunConfigTypeContribution {
|
||||
const main = p.mainClass?.trim() ?? "";
|
||||
const profiles = spring ? (p.activeProfiles?.trim() ?? "") : "";
|
||||
return {
|
||||
command: "java",
|
||||
command: p.executable || "java",
|
||||
args: [
|
||||
...splitJvmArgs(p.vmOptions),
|
||||
"-cp",
|
||||
@@ -98,7 +97,7 @@ function jvmApplicationType(spring: boolean): RunConfigTypeContribution {
|
||||
},
|
||||
// Configurations detected or saved before these fields existed: `java [vm] -cp <cp> Main [args]`.
|
||||
paramsFrom(c) {
|
||||
if (!isJava(c.command)) return null;
|
||||
if (!/^java$/i.test(commandName(c.command))) return null;
|
||||
const parsed = parseJavaArgs(c.args);
|
||||
const classpath =
|
||||
parsed.classpath === "runtime"
|
||||
@@ -114,6 +113,7 @@ function jvmApplicationType(spring: boolean): RunConfigTypeContribution {
|
||||
rest = rest.slice(1);
|
||||
}
|
||||
return {
|
||||
...(c.command !== "java" ? { executable: c.command } : {}),
|
||||
mainClass: parsed.mainClass,
|
||||
vmOptions: formatArgs(parsed.vmArgs),
|
||||
programArgs: formatArgs(rest),
|
||||
|
||||
@@ -71,6 +71,47 @@ describe("configuration types", () => {
|
||||
expect(runConfigTypes().map((t) => t.id)).toEqual(expect.arrayContaining(BUILTIN_TYPES.map((t) => t.id)));
|
||||
});
|
||||
|
||||
it.each([
|
||||
["maven", "mvn.cmd", ["clean", "test"]],
|
||||
["maven", "MVN.CMD", ["verify"]],
|
||||
["maven", "mvn.bat", ["test"]],
|
||||
["maven", "/opt/maven/bin/mvn", ["package"]],
|
||||
["maven", String.raw`C:\tools\maven\bin\mvn.cmd`, ["test", "-Dname=two words"]],
|
||||
["maven", "mvnw.cmd", ["test"]],
|
||||
["gradle", "gradle.bat", ["build"]],
|
||||
["gradle", "gradle.cmd", ["test"]],
|
||||
["gradle", "gradlew.bat", ["test"]],
|
||||
["pytest", "pytest.exe", ["tests/test_app.py", "-q"]],
|
||||
["pytest", String.raw`C:\project\.venv\Scripts\PYTEST.EXE`, ["-q"]],
|
||||
["pytest", "PYTHON.EXE", ["-m", "pytest", "tests"]],
|
||||
["npm", String.raw`C:\Program Files\nodejs\NPM.CMD`, ["run", "test", "--", "two words"]],
|
||||
["npm", "pnpm.COM", ["test"]],
|
||||
["application", String.raw`C:\jdk\bin\JAVA.EXE`, ["-cp", "lib/*", "a.Main"]],
|
||||
["spring-boot", "java.cmd", ["-cp", "lib/*", "a.App"]],
|
||||
["godot", "GODOT.EXE", ["--path", ".", "res://a.tscn"]],
|
||||
])("opens %s launcher %s in its form and preserves the command line", (kind, command, args) => {
|
||||
const original = cfg("windows", { kind, command, args, cwd: "C:/project" });
|
||||
const params = formParams(original);
|
||||
expect(params).not.toBeNull();
|
||||
expect(withParams(original, params!)).toMatchObject({ command, args, cwd: original.cwd });
|
||||
if (kind === "maven" || kind === "gradle") {
|
||||
expect(params!.wrapper === "true").toBe(/^(mvnw|gradlew)/.test(command));
|
||||
expect(withParams(original, { ...params!, tasks: "clean" })).toMatchObject({ command, args: ["clean"] });
|
||||
}
|
||||
});
|
||||
|
||||
it("switches launchers when the wrapper or package manager selection changes", () => {
|
||||
const maven = cfg("m", { kind: "maven", command: "mvnw.cmd", args: ["test"] });
|
||||
expect(withParams(maven, { ...formParams(maven)!, wrapper: "false" }).command).toBe("mvn");
|
||||
const npm = cfg("n", { kind: "npm", command: "NPM.CMD", args: ["test"] });
|
||||
expect(formParams(npm)!.packageManager).toBe("npm");
|
||||
expect(withParams(npm, { ...formParams(npm)!, packageManager: "pnpm" }).command).toBe("pnpm");
|
||||
});
|
||||
|
||||
it.each(["mvn.cmd.sh", "other.exe", "/bin/MVN"])("does not accept unrelated Maven commands: %s", (command) => {
|
||||
expect(formParams(cfg("m", { kind: "maven", command, args: ["test"] }))).toBeNull();
|
||||
});
|
||||
|
||||
it("old run.toml entries edit as a Shell Script command line", () => {
|
||||
const old = cfg("user:server", { command: "cargo", args: ["run", "--bin", "my app"] });
|
||||
const params = formParams(old)!;
|
||||
@@ -96,9 +137,19 @@ describe("configuration types", () => {
|
||||
command: "/Apps/Godot.app/Contents/MacOS/Godot",
|
||||
args: ["--path", "."],
|
||||
});
|
||||
expect(godot.paramsFrom!(detected)).toEqual({ projectDir: "", scene: "", arguments: "" });
|
||||
expect(godot.paramsFrom!(detected)).toEqual({
|
||||
projectDir: "",
|
||||
scene: "",
|
||||
arguments: "",
|
||||
executable: detected.command,
|
||||
});
|
||||
const scene = { ...detected, cwd: "demo", args: ["--path", ".", "res://a.tscn", "--verbose"] };
|
||||
expect(godot.paramsFrom!(scene)).toEqual({ projectDir: "demo", scene: "res://a.tscn", arguments: "--verbose" });
|
||||
expect(godot.paramsFrom!(scene)).toEqual({
|
||||
projectDir: "demo",
|
||||
scene: "res://a.tscn",
|
||||
arguments: "--verbose",
|
||||
executable: detected.command,
|
||||
});
|
||||
expect(godot.paramsFrom!(cfg("x", { command: "cargo" }))).toBeNull();
|
||||
});
|
||||
|
||||
@@ -221,8 +272,8 @@ describe("configuration types", () => {
|
||||
});
|
||||
const gradle = cfg("gradle:test", { kind: "gradle", command: "./gradlew", args: ["test"], detected: true });
|
||||
expect(formParams(gradle)).toMatchObject({ tasks: "test", wrapper: "true" });
|
||||
// A toolchain-specific command line the form can't reproduce stays a plain command.
|
||||
expect(formParams(cfg("m", { kind: "maven", command: "/opt/maven/bin/mvn", args: ["package"] }))).toBeNull();
|
||||
// An unrelated executable stays a plain command.
|
||||
expect(formParams(cfg("m", { kind: "maven", command: "/opt/tools/custom-build", args: ["package"] }))).toBeNull();
|
||||
expect(withParams(npm, { packageManager: "npm", command: "test", packageDir: "" })).toMatchObject({
|
||||
command: "npm",
|
||||
args: ["test"],
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { registerRunConfigType, useRunConfigTypes, type RunConfigTypeContribution } from "@/workbench/contributions";
|
||||
import { formatArgs, parseArgs, uid, uniqueName } from "./configs";
|
||||
import { commandName } from "./commandName";
|
||||
import {
|
||||
applicationType,
|
||||
gradleForkedJvm,
|
||||
@@ -19,7 +20,6 @@ import { tx } from "@/platform/i18n";
|
||||
export const COMPOUND = "compound";
|
||||
|
||||
const on = (v: string | undefined) => v === "true";
|
||||
const baseName = (p: string) => p.replace(/\\/g, "/").split("/").pop() ?? p;
|
||||
const dirOf = (cwd: string | null | undefined) => cwd ?? "";
|
||||
|
||||
/** Values of `args[index]` across detected configurations of `kind`. */
|
||||
@@ -115,7 +115,7 @@ const program: RunConfigTypeContribution = {
|
||||
};
|
||||
|
||||
const isScene = (a: string) => /\.t?scn$/i.test(a);
|
||||
const isGodot = (cmd: string) => /^godot([-.\w]*)$/i.test(baseName(cmd).replace(/\.exe$/i, ""));
|
||||
const isGodot = (cmd: string) => /^godot([-.\w]*)$/i.test(commandName(cmd));
|
||||
|
||||
/**
|
||||
* Godot: `godot --path <project>` runs the main scene, `… <scene>` one scene. The
|
||||
@@ -148,7 +148,7 @@ const godot: RunConfigTypeContribution = {
|
||||
},
|
||||
],
|
||||
commandLine: (p) => ({
|
||||
command: "godot",
|
||||
command: p.executable || "godot",
|
||||
args: ["--path", ".", ...(p.scene?.trim() ? [p.scene.trim()] : []), ...parseArgs(p.arguments ?? "")],
|
||||
cwd: p.projectDir?.trim() || null,
|
||||
}),
|
||||
@@ -158,7 +158,12 @@ const godot: RunConfigTypeContribution = {
|
||||
const path = rest.indexOf("--path");
|
||||
if (path >= 0) rest.splice(path, 2);
|
||||
const scene = rest.find(isScene) ?? "";
|
||||
return { projectDir: dirOf(c.cwd), scene, arguments: formatArgs(rest.filter((a) => a !== scene)) };
|
||||
return {
|
||||
projectDir: dirOf(c.cwd),
|
||||
scene,
|
||||
arguments: formatArgs(rest.filter((a) => a !== scene)),
|
||||
...(c.command !== "godot" ? { executable: c.command } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -194,13 +199,22 @@ const npm: RunConfigTypeContribution = {
|
||||
commandLine(p) {
|
||||
const command = p.command || "run";
|
||||
const args = [command, ...(command === "run" ? [p.script ?? ""] : []), ...parseArgs(p.arguments ?? "")];
|
||||
return { command: p.packageManager || "npm", args, cwd: p.packageDir?.trim() || null };
|
||||
const manager = p.packageManager || "npm";
|
||||
const executable = p.executable && commandName(p.executable) === manager ? p.executable : manager;
|
||||
return { command: executable, args, cwd: p.packageDir?.trim() || null };
|
||||
},
|
||||
paramsFrom(c) {
|
||||
const [command, ...rest] = c.args;
|
||||
if (!PACKAGE_MANAGERS.includes(c.command) || !command || !NPM_COMMANDS.includes(command)) return null;
|
||||
if (!PACKAGE_MANAGERS.includes(commandName(c.command)) || !command || !NPM_COMMANDS.includes(command)) return null;
|
||||
const script = command === "run" ? (rest.shift() ?? "") : "";
|
||||
return { packageManager: c.command, command, script, arguments: formatArgs(rest), packageDir: dirOf(c.cwd) };
|
||||
return {
|
||||
packageManager: commandName(c.command),
|
||||
command,
|
||||
script,
|
||||
arguments: formatArgs(rest),
|
||||
packageDir: dirOf(c.cwd),
|
||||
...(c.command !== commandName(c.command) ? { executable: c.command } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -238,16 +252,20 @@ function buildToolType(
|
||||
{ key: "wrapper", label: wrapper.label, control: "checkbox" },
|
||||
],
|
||||
commandLine: (p) => ({
|
||||
command: wrapperCommand(wrapper.unix, wrapper.windows, wrapper.plain, on(p.wrapper)),
|
||||
command:
|
||||
p.executable && commandName(p.executable) === (on(p.wrapper) ? wrapper.unix : wrapper.plain)
|
||||
? p.executable
|
||||
: wrapperCommand(wrapper.unix, wrapper.windows, wrapper.plain, on(p.wrapper)),
|
||||
args: [...parseArgs(p.tasks ?? ""), ...parseArgs(p.arguments ?? ""), ...jvm.args(p)],
|
||||
cwd: p.projectDir?.trim() || null,
|
||||
}),
|
||||
...(jvm.prepareLaunch ? { prepareLaunch: jvm.prepareLaunch } : {}),
|
||||
paramsFrom(c) {
|
||||
const name = baseName(c.command);
|
||||
const usesWrapper = name === wrapper.unix || name === wrapper.windows;
|
||||
const name = commandName(c.command);
|
||||
const usesWrapper = name === wrapper.unix;
|
||||
if (!usesWrapper && name !== wrapper.plain) return null;
|
||||
return {
|
||||
executable: c.command,
|
||||
projectDir: dirOf(c.cwd),
|
||||
tasks: formatArgs(c.args),
|
||||
arguments: "",
|
||||
@@ -350,7 +368,7 @@ const pytest: RunConfigTypeContribution = {
|
||||
? 2
|
||||
: m >= 0 && c.args[m + 1] === "pytest"
|
||||
? m + 2
|
||||
: baseName(c.command).replace(/\.exe$/i, "") === "pytest"
|
||||
: commandName(c.command) === "pytest"
|
||||
? 0
|
||||
: null;
|
||||
if (runner === null) return null;
|
||||
|
||||
Reference in new issue
Заблокувати користувача