mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
feat(run): choose the JRE and the module of an Application configuration (#1172)
Application / Spring Boot configurations found their module only from the main class, so the same class in two modules failed with "Cannot determine the module for main class", and the JVM was always the module's. The form now has JRE (the module's JDK or an installed JDK) and Use classpath of module (found by main class, or a project module, stored as RunConfig.module). Run configuration selects can load their options when shown. Fixes #1153
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* Before launch list.
|
||||
*/
|
||||
import { parseJavaArgs } from "@/platform/javaLaunch";
|
||||
import { useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from "react";
|
||||
import clsx from "clsx";
|
||||
import { isTauri, type BeforeLaunchTask, type RunConfig } from "@/platform/ipc";
|
||||
import { useWorkflowSettings } from "@/platform/workflowSettings";
|
||||
@@ -729,9 +729,20 @@ function TypeField({
|
||||
const tx = useTx();
|
||||
const root = useProject((s) => s.project?.root ?? null);
|
||||
const listId = `run-field-${field.key}`;
|
||||
const [loaded, setLoaded] = useState<ReadonlyArray<{ value: string; label: string; detail?: string }>>([]);
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
void field
|
||||
.loadOptions?.()
|
||||
.then((options) => live && setLoaded(options))
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, [field]);
|
||||
if (field.control === "output") return <OutputField label={field.label} text={field.value?.(params) ?? ""} />;
|
||||
if (field.control === "select") {
|
||||
const options = [...(field.options ?? [])];
|
||||
const options = [...(field.options ?? []), ...loaded];
|
||||
if (value && !options.some((o) => o.value === value)) options.push({ value, label: value });
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
*/
|
||||
import { call, commands, type RunConfig } from "@/platform/ipc";
|
||||
import { CLASSPATH, parseJavaArgs, TEST_CLASSPATH } from "@/platform/javaLaunch";
|
||||
import { basename, useProject } from "@/platform/project";
|
||||
import { useBuildPresentation } from "@/platform/projectPresentation";
|
||||
import type { RunConfigTypeContribution, RunConfigTypeField } from "@/workbench/contributions";
|
||||
import { formatArgs, parseArgs } from "./configs";
|
||||
import { commandName } from "./commandName";
|
||||
@@ -51,6 +53,45 @@ function mainClasses(configs: readonly RunConfig[]): string[] {
|
||||
return [...out];
|
||||
}
|
||||
|
||||
/** The module of the project root in the `module` param (`RunConfig.module` is `""` for it). */
|
||||
export const ROOT_MODULE = ".";
|
||||
|
||||
/** `java` in a JDK home (`params.jre`); the home itself when the path names `bin/java` already. */
|
||||
export function jreJava(home: string, windows = isWindows()): string {
|
||||
const java = windows ? "java.exe" : "java";
|
||||
return /[\\/]bin[\\/]java(\.exe)?$/i.test(home) ? home : `${home.replace(/[\\/]+$/, "")}/bin/${java}`;
|
||||
}
|
||||
|
||||
/** IDEA's JRE: the module's JDK unless one installed JDK is chosen. */
|
||||
const jreField: RunConfigTypeField = {
|
||||
key: "jre",
|
||||
label: "JRE",
|
||||
control: "select",
|
||||
options: [{ value: "", label: "Default (the module's JDK)" }],
|
||||
loadOptions: async () =>
|
||||
(await call(commands.sdkDetect("jdk"))).map((jdk) => ({
|
||||
value: String(jdk.path),
|
||||
label: jdk.name,
|
||||
detail: String(jdk.path),
|
||||
})),
|
||||
};
|
||||
|
||||
/** IDEA's "Use classpath of module": the module whose classpath and build the run uses. */
|
||||
const moduleField: RunConfigTypeField = {
|
||||
key: "module",
|
||||
label: "Use classpath of module",
|
||||
control: "select",
|
||||
options: [{ value: "", label: "Find by main class" }],
|
||||
loadOptions: async () => {
|
||||
const root = useProject.getState().project?.root ?? "";
|
||||
return [...useBuildPresentation.getState().modules.keys()].sort().map((dir) => ({
|
||||
value: dir || ROOT_MODULE,
|
||||
label: dir ? basename(dir) : basename(root),
|
||||
detail: dir.includes("/") ? dir : undefined,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
function jvmApplicationType(spring: boolean): RunConfigTypeContribution {
|
||||
return {
|
||||
id: spring ? "spring-boot" : "application",
|
||||
@@ -72,6 +113,8 @@ function jvmApplicationType(spring: boolean): RunConfigTypeContribution {
|
||||
{ key: "programArgs", label: "Program arguments", mono: true },
|
||||
...(spring ? [profilesField("Comma separated; passed as --spring.profiles.active")] : []),
|
||||
...(spring ? updatePolicyFields(false) : []),
|
||||
jreField,
|
||||
moduleField,
|
||||
{
|
||||
key: "classpath",
|
||||
label: "Classpath",
|
||||
@@ -86,7 +129,7 @@ function jvmApplicationType(spring: boolean): RunConfigTypeContribution {
|
||||
const main = p.mainClass?.trim() ?? "";
|
||||
const profiles = spring ? (p.activeProfiles?.trim() ?? "") : "";
|
||||
return {
|
||||
command: p.executable || "java",
|
||||
command: p.jre ? jreJava(p.jre) : p.executable || "java",
|
||||
args: [
|
||||
...splitJvmArgs(p.vmOptions),
|
||||
"-cp",
|
||||
@@ -116,6 +159,7 @@ function jvmApplicationType(spring: boolean): RunConfigTypeContribution {
|
||||
}
|
||||
return {
|
||||
...(c.command !== "java" ? { executable: c.command } : {}),
|
||||
...(c.module != null ? { module: c.module || ROOT_MODULE } : {}),
|
||||
mainClass: parsed.mainClass,
|
||||
vmOptions: formatArgs(parsed.vmArgs),
|
||||
programArgs: formatArgs(rest),
|
||||
|
||||
@@ -70,6 +70,32 @@ beforeEach(() => {
|
||||
afterEach(() => disposeTypes());
|
||||
|
||||
describe("configuration types", () => {
|
||||
it("runs an Application with the chosen JRE and the classpath of the chosen module", () => {
|
||||
const type = typeFor("application")!;
|
||||
const base = withParams(newConfigOfType(type, []), { ...newConfigOfType(type, []).params, mainClass: "a.Main" });
|
||||
expect(base.command).toBe("java");
|
||||
expect(base.module).toBeUndefined();
|
||||
|
||||
const chosen = withParams(base, { ...base.params, jre: "/jdks/temurin-8", module: "server" });
|
||||
expect(chosen.command).toBe("/jdks/temurin-8/bin/java");
|
||||
expect(chosen.module).toBe("server");
|
||||
// The project root is a module too.
|
||||
expect(withParams(chosen, { ...chosen.params, module: "." }).module).toBe("");
|
||||
// Back to the defaults: the module's JDK, and the module found again for another main class.
|
||||
const auto = withParams(chosen, { ...chosen.params, jre: "", module: "", mainClass: "b.Main" });
|
||||
expect(auto.command).toBe("java");
|
||||
expect(auto.module).toBeUndefined();
|
||||
|
||||
// A configuration saved with its module shows it in the form.
|
||||
const saved = cfg("saved", {
|
||||
kind: "application",
|
||||
command: "java",
|
||||
args: ["-cp", "${classpath}", "a.Main"],
|
||||
module: "",
|
||||
});
|
||||
expect(formParams(saved)).toMatchObject({ mainClass: "a.Main", module: "." });
|
||||
});
|
||||
|
||||
it.each(["spring-boot", "tomcat"])("edits shared update policies in the %s form", async (kind) => {
|
||||
const type = typeFor(kind)!;
|
||||
const original = newConfigOfType(type, []);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
gradleForkedJvm,
|
||||
isWindows,
|
||||
mavenForkedJvm,
|
||||
ROOT_MODULE,
|
||||
springBootType,
|
||||
type ForkedJvm,
|
||||
} from "./jvmTypes";
|
||||
@@ -505,9 +506,12 @@ export function withParams(config: RunConfig, params: Record<string, string>, ty
|
||||
...(params.goKind ? { isTest: params.goKind === "test" } : {}),
|
||||
command: line.command,
|
||||
args: line.args,
|
||||
...(isJvmApplication(config) && parseJavaArgs(config.args).mainClass !== parseJavaArgs(line.args).mainClass
|
||||
? { module: undefined }
|
||||
: {}),
|
||||
// "Use classpath of module": the chosen module; else it is found again for another main class.
|
||||
...(isJvmApplication(config) && params.module
|
||||
? { module: params.module === ROOT_MODULE ? "" : params.module }
|
||||
: isJvmApplication(config) && parseJavaArgs(config.args).mainClass !== parseJavaArgs(line.args).mainClass
|
||||
? { module: undefined }
|
||||
: {}),
|
||||
...(type?.ownsWorkingDirectory ? { cwd: line.cwd ?? null } : {}),
|
||||
...(line.stop ? { stopCommand: line.stop } : {}),
|
||||
};
|
||||
|
||||
@@ -5959,5 +5959,8 @@
|
||||
"Could not close comparison": "无法关闭比较窗口",
|
||||
"Could not open comparison": "无法打开比较窗口",
|
||||
"Could not show diff": "无法显示差异",
|
||||
"Back to Changed Files": "返回更改文件列表"
|
||||
"Back to Changed Files": "返回更改文件列表",
|
||||
"Default (the module's JDK)": "默认(模块的 JDK)",
|
||||
"Use classpath of module": "使用模块的类路径",
|
||||
"Find by main class": "按主类查找"
|
||||
}
|
||||
|
||||
@@ -1107,7 +1107,9 @@ export interface RunConfigTypeField {
|
||||
/** Visible lines of a `textarea` (default 4). */
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
options?: ReadonlyArray<{ value: string; label: string }>;
|
||||
options?: ReadonlyArray<{ value: string; label: string; detail?: string }>;
|
||||
/** A `select`'s further options, read when the form shows it (installed JDKs, build modules). */
|
||||
loadOptions?(): Promise<ReadonlyArray<{ value: string; label: string; detail?: string }>>;
|
||||
/** Values offered while typing (e.g. the scripts of detected configurations). */
|
||||
suggestions?(configs: readonly RunConfig[], params: Readonly<Record<string, string>>): string[];
|
||||
required?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user