diff --git a/crates/ide-run/src/config.rs b/crates/ide-run/src/config.rs index ada02e1c..7855f074 100644 --- a/crates/ide-run/src/config.rs +++ b/crates/ide-run/src/config.rs @@ -78,6 +78,7 @@ struct FileConfig { #[serde(default)] args: Vec, cwd: Option, + module: Option, #[serde(default)] env: BTreeMap, #[serde(default, alias = "isTest", alias = "is_test")] @@ -161,6 +162,7 @@ fn convert(configs: Vec, local: bool) -> Vec { debug_adapter: c.debug_adapter.filter(|d| !d.is_empty()), allow_parallel: c.allow_parallel, extras: RunExtras { + module: c.module, params: c.params, folder: non_empty(c.folder), local, @@ -210,6 +212,9 @@ fn to_table(c: &RunConfig) -> Table { if !c.args.is_empty() { t.insert("args", value(c.args.iter().collect::())); } + if let Some(module) = &c.extras.module { + t.insert("module", value(module)); + } if let Some(cwd) = &c.cwd { t.insert("cwd", value(cwd.to_string_lossy().replace('\\', "/"))); } diff --git a/crates/ide-run/src/detect.rs b/crates/ide-run/src/detect.rs index b6b0c89e..a56fec5d 100644 --- a/crates/ide-run/src/detect.rs +++ b/crates/ide-run/src/detect.rs @@ -548,11 +548,11 @@ fn spring_boot_apps(root: &Path, build: &Path, out: &mut Vec) { Some(m) => format!("application:{m}:{class}"), None => format!("application:{class}"), }; - out.push( - config(id, simple, "spring-boot", "java", &["-cp", "${classpath}", &class]) - .in_dir(module_rel.as_ref().map(PathBuf::from)) - .debug("java-debug"), - ); + let mut app = config(id, simple, "spring-boot", "java", &["-cp", "${classpath}", &class]) + .in_dir(module_rel.as_ref().map(PathBuf::from)) + .debug("java-debug"); + app.extras.module = Some(module_rel.clone().unwrap_or_default()); + out.push(app); } } } diff --git a/crates/ide-run/src/lib.rs b/crates/ide-run/src/lib.rs index 97eb4afd..b32ec8b5 100644 --- a/crates/ide-run/src/lib.rs +++ b/crates/ide-run/src/lib.rs @@ -70,6 +70,10 @@ fn is_true(b: &bool) -> bool { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct RunExtras { + /// Module supplying a JVM application's classpath; empty means the project root. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[specta(optional)] + pub module: Option, /// Values of the configuration type's own form fields (the command line derives from them). #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] #[specta(optional)] @@ -137,6 +141,7 @@ pub struct RunExtras { impl Default for RunExtras { fn default() -> Self { Self { + module: None, params: BTreeMap::new(), folder: None, local: false, diff --git a/crates/ide-run/tests/config.rs b/crates/ide-run/tests/config.rs index 17761870..7ba18d86 100644 --- a/crates/ide-run/tests/config.rs +++ b/crates/ide-run/tests/config.rs @@ -234,3 +234,31 @@ fn suppression_round_trip_and_resaving() { assert_eq!(load_suppressed_config_ids(dir.path()).unwrap(), vec![ids[1].clone()]); assert!(fs::read_to_string(dir.path().join(".openidea/.gitignore")).unwrap().contains("/run.local.toml")); } + +#[test] +fn java_module_is_optional_and_independent_of_working_directory() { + let dir = tempfile::tempdir().unwrap(); + let path = user_configs_path(dir.path()); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write( + &path, + r#"[[configurations]] +id = "user:main" +name = "Main" +kind = "application" +command = "java" +args = ["-cp", "${classpath}", "a.Main"] +cwd = "sibling" +"#, + ) + .unwrap(); + let mut configs = load_user_configs(dir.path()).unwrap(); + assert_eq!(configs[0].extras.module, None); + for module in ["app", ""] { + configs[0].extras.module = Some(module.into()); + save_user_configs(dir.path(), &configs).unwrap(); + let loaded = load_user_configs(dir.path()).unwrap(); + assert_eq!(loaded, configs); + assert_eq!(loaded[0].cwd.as_deref(), Some(std::path::Path::new("sibling"))); + } +} diff --git a/crates/ide-run/tests/detect.rs b/crates/ide-run/tests/detect.rs index 5567466c..31a672a3 100644 --- a/crates/ide-run/tests/detect.rs +++ b/crates/ide-run/tests/detect.rs @@ -216,6 +216,7 @@ fn builds_nested_in_a_folder_of_repositories() { assert_eq!(app.kind, "spring-boot"); assert_eq!(cmdline(app), "java -cp ${classpath} org.dromara.DromaraApplication"); assert_eq!(app.cwd.as_deref(), Some(Path::new("RuoYi-Vue-Plus/ruoyi-admin"))); + assert_eq!(app.extras.module.as_deref(), Some("RuoYi-Vue-Plus/ruoyi-admin")); assert_eq!(app.debug_adapter.as_deref(), Some("java-debug")); let package = find(&c, "maven:package:RuoYi-Vue-Plus"); assert_eq!(package.name, "RuoYi-Vue-Plus: mvn package"); diff --git a/src/bindings.ts b/src/bindings.ts index 4f0982ad..b62486f8 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -5076,6 +5076,8 @@ export type RunConfig_Serialize = { export type RunExtras = RunExtras_Serialize | RunExtras_Deserialize; export type RunExtras_Deserialize = { + /** Module supplying a JVM application's classpath; empty means the project root. */ + module?: string | null, /** Values of the configuration type's own form fields (the command line derives from them). */ params?: { [key in string]: string }, /** Folder in the Run/Debug Configurations tree. */ @@ -5113,6 +5115,8 @@ export type RunExtras_Deserialize = { }; export type RunExtras_Serialize = { + /** Module supplying a JVM application's classpath; empty means the project root. */ + module?: string | null, /** Values of the configuration type's own form fields (the command line derives from them). */ params?: { [key in string]: string }, /** Folder in the Run/Debug Configurations tree. */ diff --git a/src/features/debugger/__tests__/java.test.ts b/src/features/debugger/__tests__/java.test.ts index 25365626..3c5743a2 100644 --- a/src/features/debugger/__tests__/java.test.ts +++ b/src/features/debugger/__tests__/java.test.ts @@ -11,6 +11,14 @@ vi.mock("@/platform/ipc", async (orig) => { ...real, commands: { ...real.commands, + findFiles: (name: string) => + ok( + name === "Main.java" + ? [{ path: "/p/api/src/main/java/a/Main.java", relPath: "api/src/main/java/a/Main.java" }] + : [], + ), + fsReadText: () => ok("package a;\npublic class Main {}"), + runConfigs: () => ok([]), fsReadDir: (path: string) => ok( kotlinFiles && path.endsWith("/src") @@ -49,6 +57,7 @@ const app: RunConfig = { command: "java", args: ["-Xmx1g", "-cp", "${classpath}", "a.Main", "--port", "8081 x"], cwd: "api", + module: "api", env: { A: "1" }, detected: true, isTest: false, @@ -144,3 +153,14 @@ describe("Java debugging (java-debug through jdtls)", () => { expect(JSON.parse(l.extraJson!)).toMatchObject({ debugServer: 41234, hostName: "127.0.0.1", port: 5005 }); }); }); + +it.each(["sibling", "/outside/modules"])("debugs from %s with the main class module", async (cwd) => { + kotlinFiles = false; + for (const module of ["api", undefined]) { + invoked.length = 0; + const launch = await javaLaunchConfig({ ...app, module, cwd }, "/p"); + expect(invoked).toContainEqual(["javaResolveLaunch", ["api", false, true, expect.any(Number)]]); + expect(invoked).toContainEqual(["javaDebugServer", ["api"]]); + expect(launch.cwd).toBe(cwd.startsWith("/") ? cwd : `/p/${cwd}`); + } +}); diff --git a/src/features/debugger/__tests__/javaCancel.test.ts b/src/features/debugger/__tests__/javaCancel.test.ts index edbb3dfc..4b8addb5 100644 --- a/src/features/debugger/__tests__/javaCancel.test.ts +++ b/src/features/debugger/__tests__/javaCancel.test.ts @@ -26,6 +26,7 @@ const config: RunConfig = { command: "java", args: ["-cp", "${classpath}", "Main"], cwd: null, + module: "", env: {}, detected: true, isTest: false, diff --git a/src/features/debugger/java.ts b/src/features/debugger/java.ts index 3ad22fbe..459daca8 100644 --- a/src/features/debugger/java.ts +++ b/src/features/debugger/java.ts @@ -20,6 +20,7 @@ import { joinJavaArgs, parseJavaArgs, resolveJavaCommand, + resolveJavaModule, } from "@/platform/javaLaunch"; import { notifications } from "@/workbench/contributions"; @@ -85,11 +86,12 @@ export async function javaLaunchConfig( root: string, preparation?: { request: RequestId; signal: AbortSignal }, ): Promise { - const module = rc.cwd ?? ""; const cwd = rc.cwd ? join(root, rc.cwd) : root; const parsed = parseJavaArgs(rc.args); if (!parsed.mainClass) throw new Error(`No main class in '${rc.name}'`); preparation?.signal.throwIfAborted(); + const module = await resolveJavaModule(rc); + preparation?.signal.throwIfAborted(); const info = await call( commands.javaResolveLaunch(module, parsed.classpath === "test", true, preparation?.request ?? nextRequestId()), ); @@ -103,7 +105,7 @@ export async function javaLaunchConfig( body: formatCompileErrors(info).trimEnd(), }); const resolved = resolveJavaCommand(rc, info); - const kotlin = rc.debugAdapter === KOTLIN_ADAPTER || (await moduleHasKotlinSources(cwd)); + const kotlin = rc.debugAdapter === KOTLIN_ADAPTER || (await moduleHasKotlinSources(join(root, module))); preparation?.signal.throwIfAborted(); const classPaths = parsed.explicitClasspath ? parsed.explicitClasspath.split(/[:;]/) : info.classPaths; if (kotlin) { @@ -116,7 +118,7 @@ export async function javaLaunchConfig( cwd, env: { ...rc.env }, extraJson: JSON.stringify({ - projectRoot: cwd, + projectRoot: join(root, module), mainClass: parsed.mainClass, classPaths, modulePaths: info.modulePaths, diff --git a/src/features/run/ConfigForm.tsx b/src/features/run/ConfigForm.tsx index 34c9f94b..1d11ed2d 100644 --- a/src/features/run/ConfigForm.tsx +++ b/src/features/run/ConfigForm.tsx @@ -3,6 +3,7 @@ * common ones, the options added through "Modify options" (⌥M) and the * Before launch list. */ +import { parseJavaArgs } from "@/platform/javaLaunch"; import { useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from "react"; import clsx from "clsx"; import type { BeforeLaunchTask, RunConfig } from "@/platform/ipc"; @@ -428,7 +429,10 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi placeholder="Space separated; quote with ' or "" onChange={(e) => { setArgsText(e.target.value); - update({ args: parseArgs(e.target.value), params: {} }); + const args = parseArgs(e.target.value); + const module = + parseJavaArgs(args).mainClass === parseJavaArgs(config.args).mainClass ? config.module : undefined; + update({ args, params: {}, module }); }} /> diff --git a/src/features/run/application.ts b/src/features/run/application.ts index 681ac8e5..9be6396a 100644 --- a/src/features/run/application.ts +++ b/src/features/run/application.ts @@ -4,26 +4,12 @@ * belongs to, and test configurations scoped to one class or method. */ import type { RunConfig } from "@/platform/ipc"; -import { CLASSPATH, TEST_CLASSPATH } from "@/platform/javaLaunch"; +import { CLASSPATH, TEST_CLASSPATH, jvmModuleDir } from "@/platform/javaLaunch"; import { isTestPath } from "@/platform/languageSettings"; import { currentRoots } from "@/platform/projectRoots"; import { simpleClassName } from "./jvmTargets"; -/** - * Module directory (relative to the project root, `""` for the root) of a JVM - * source file: the part before `src/{main,test}/…`, else the working directory - * of the closest Maven / Gradle configuration. - */ -export function jvmModuleDir(relPath: string, configs: readonly RunConfig[] = []): string { - const m = /^(?:(.*)\/)?src\/(?:main|test|[\w-]+)\/(?:java|kotlin|scala|groovy)\//.exec(relPath); - if (m) return m[1] ?? ""; - const cwds = configs - .filter((c) => c.kind === "maven" || c.kind === "gradle") - .map((c) => (c.cwd ?? "").replace(/^\.\/?/, "").replace(/\/+$/, "")) - .filter((cwd) => !cwd || relPath.startsWith(cwd + "/")) - .sort((a, b) => b.length - a.length); - return cwds[0] ?? ""; -} +export { jvmModuleDir } from "@/platform/javaLaunch"; /** In a test source root (detected or marked in Project Structure), or Maven's `src/test`. */ const isTestSource = (relPath: string) => /(^|\/)src\/test\//.test(relPath) || isTestPath(currentRoots(), relPath); @@ -42,6 +28,7 @@ export function applicationConfig( command: "java", args: ["-cp", isTestSource(relPath) ? TEST_CLASSPATH : CLASSPATH, target.className], cwd: module || null, + module, env: {}, detected: true, isTest: false, diff --git a/src/features/run/javaModule.test.ts b/src/features/run/javaModule.test.ts new file mode 100644 index 00000000..4856b831 --- /dev/null +++ b/src/features/run/javaModule.test.ts @@ -0,0 +1,114 @@ +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import type { RunConfig } from "@/platform/ipc"; +import { findJavaModule } from "@/platform/javaLaunch"; +import { provideNotificationService } from "@/workbench/contributions"; +import { applicationConfig } from "./application"; +import { resetRuns, saveUserConfigs, startRun } from "./store"; +import { applicationType } from "./jvmTypes"; +import { withParams } from "./types"; + +const ipc = vi.hoisted(() => { + const ok = (data: unknown) => Promise.resolve({ status: "ok", data }); + return { + findFiles: vi.fn((name: string) => + ok( + name === "Main.java" + ? [{ path: "/p/app/src/main/java/a/Main.java", relPath: "app/src/main/java/a/Main.java" }] + : [], + ), + ), + fsReadText: vi.fn(() => ok("package a;\npublic class Main {}")), + runConfigs: vi.fn(() => ok([])), + runSaveConfigs: vi.fn(() => ok(null)), + runStart: vi.fn(() => ok({ id: 1, pty: false })), + javaResolveLaunch: vi.fn(() => + ok({ + build: "succeeded", + projectRoot: "/p", + classPaths: ["/p/app/target/classes"], + modulePaths: [], + javaHome: null, + javaExec: "java", + shortenCommandLine: null, + errors: [], + errorCount: 0, + hints: [], + }), + ), + }; +}); +vi.mock("@/platform/ipc", async (original) => { + const real = await original(); + return { ...real, isTauri: () => true, commands: { ...real.commands, ...ipc } }; +}); +vi.mock("@tauri-apps/api/core", async (original) => ({ + ...(await original()), + Channel: class { + onmessage = null; + }, +})); +const config: RunConfig = { + id: "user:main", + name: "Main", + kind: "application", + command: "java", + args: ["-cp", "${classpath}", "a.Main"], + cwd: "app", + env: {}, + detected: false, + isTest: false, + debugAdapter: "java-debug", +}; +beforeEach(() => { + vi.clearAllMocks(); + provideNotificationService({ notify: vi.fn() }); +}); +afterEach(() => resetRuns()); + +it.each(["sibling", "/outside/modules"])("runs from %s using the stored module's classpath", async (cwd) => { + const session = await startRun({ ...config, module: "app", cwd }); + expect(session?.status).toBe("running"); + expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, true, expect.any(Number)); + expect(ipc.runStart).toHaveBeenCalledWith( + expect.objectContaining({ cwd, args: ["-cp", "/p/app/target/classes", "a.Main"] }), + expect.anything(), + ); + expect(ipc.findFiles).not.toHaveBeenCalled(); +}); + +it("finds the source module for an old saved configuration without a module", async () => { + await startRun({ ...config, cwd: "/outside/modules" }); + expect(ipc.findFiles).toHaveBeenCalledWith("Main.java", 1000); + expect(ipc.javaResolveLaunch).toHaveBeenCalledWith("app", false, true, expect.any(Number)); + expect(ipc.runStart).toHaveBeenCalledWith(expect.objectContaining({ cwd: "/outside/modules" }), expect.anything()); +}); + +it("keeps an explicitly stored root module", async () => { + expect(await findJavaModule({ ...config, module: "", cwd: "sibling" })).toBe(""); + expect(ipc.findFiles).not.toHaveBeenCalled(); +}); + +it("stores the source module when a manually created application is saved", async () => { + await saveUserConfigs([{ ...config, cwd: "sibling" }]); + expect(ipc.runSaveConfigs).toHaveBeenCalledWith([expect.objectContaining({ module: "app", cwd: "sibling" })], []); +}); + +it("stores the module at context creation and forgets it when the main class changes", () => { + const detected = applicationConfig({ className: "a.Main" }, "app/src/main/java/a/Main.java"); + expect(detected.module).toBe("app"); + expect(withParams(detected, { mainClass: "b.Other" }, applicationType).module).toBeUndefined(); + expect(withParams(detected, { mainClass: "a.Main", programArgs: "x" }, applicationType).module).toBe("app"); +}); + +it("does not substitute the working directory when sources are missing or ambiguous", async () => { + ipc.findFiles.mockResolvedValueOnce({ status: "ok", data: [] }); + expect(await findJavaModule({ ...config, cwd: "sibling" })).toBeNull(); + ipc.findFiles.mockResolvedValueOnce({ + status: "ok", + data: [ + { path: "/p/app/src/main/java/a/Main.java", relPath: "app/src/main/java/a/Main.java" }, + { path: "/p/other/src/main/java/a/Main.java", relPath: "other/src/main/java/a/Main.java" }, + ], + }); + expect(await findJavaModule(config)).toBeNull(); +}); diff --git a/src/features/run/resolveCancellation.test.ts b/src/features/run/resolveCancellation.test.ts index ab16fa39..2cb382ac 100644 --- a/src/features/run/resolveCancellation.test.ts +++ b/src/features/run/resolveCancellation.test.ts @@ -27,6 +27,7 @@ const config: RunConfig = { command: "java", args: ["-cp", "${classpath}", "Main"], cwd: null, + module: "", env: {}, detected: false, isTest: false, diff --git a/src/features/run/store.ts b/src/features/run/store.ts index 04ff9ad1..e4eb58f2 100644 --- a/src/features/run/store.ts +++ b/src/features/run/store.ts @@ -17,7 +17,14 @@ import { type RunId, type TestCaseReport, } from "@/platform/ipc"; -import { formatCompileErrors, isJvmApplication, parseJavaArgs, resolveJavaCommand } from "@/platform/javaLaunch"; +import { + findJavaModule, + formatCompileErrors, + isJvmApplication, + parseJavaArgs, + resolveJavaCommand, + resolveJavaModule, +} from "@/platform/javaLaunch"; import { downloadJdkActions } from "@/platform/jdk"; import { useProject } from "@/platform/project"; import { executeAction } from "@/platform/actions"; @@ -319,6 +326,11 @@ export async function deleteConfig(id: string): Promise { /** Persists user configurations and reloads the merged list. */ export async function saveUserConfigs(user: RunConfig[], removedDetectedIds: string[] = []): Promise { + user = await Promise.all( + user.map(async (config) => + isJvmApplication(config) && config.module == null ? { ...config, module: await findJavaModule(config) } : config, + ), + ); await call( commands.runSaveConfigs( user.map((c) => ({ ...c, detected: false })), @@ -701,9 +713,9 @@ async function resolveJvmApplication(sessionId: string, config: RunConfig): Prom const request = { id: nextRequestId(), cancelled: false }; rt.resolveRequest = request; try { - const info = await call( - commands.javaResolveLaunch(config.cwd ?? "", parsed.classpath === "test", true, request.id), - ); + const module = await resolveJavaModule(config); + if (request.cancelled) return null; + const info = await call(commands.javaResolveLaunch(module, parsed.classpath === "test", true, request.id)); if (request.cancelled) return null; if (info.build === "withErrors") say("Build completed with errors; launching with the last compiled classes.\n", "stderr"); diff --git a/src/features/run/types.ts b/src/features/run/types.ts index 11333694..c531a11e 100644 --- a/src/features/run/types.ts +++ b/src/features/run/types.ts @@ -3,6 +3,7 @@ * Application, Spring Boot, Compound) and the helpers that turn a type's params into the launched * command line. Other features contribute types with `registerRunConfigType`. */ +import { isJvmApplication, parseJavaArgs } from "@/platform/javaLaunch"; import type { RunConfig } from "@/platform/ipc"; import { registerRunConfigType, useRunConfigTypes, type RunConfigTypeContribution } from "@/workbench/contributions"; import { formatArgs, parseArgs, uid, uniqueName } from "./configs"; @@ -441,6 +442,9 @@ export function withParams(config: RunConfig, params: Record, ty params, command: line.command, args: line.args, + ...(isJvmApplication(config) && parseJavaArgs(config.args).mainClass !== parseJavaArgs(line.args).mainClass + ? { module: undefined } + : {}), ...(type?.ownsWorkingDirectory ? { cwd: line.cwd ?? null } : {}), ...(line.stop ? { stopCommand: line.stop } : {}), }; diff --git a/src/platform/javaLaunch.ts b/src/platform/javaLaunch.ts index e2136366..35a8db44 100644 --- a/src/platform/javaLaunch.ts +++ b/src/platform/javaLaunch.ts @@ -4,7 +4,8 @@ * `${classpath}` (or `${testClasspath}`) and the `java` launcher are resolved * through jdtls when the configuration starts. */ -import type { JavaLaunchInfo, RunConfig } from "@/platform/ipc"; +import { call, commands, type JavaLaunchInfo, type RunConfig } from "@/platform/ipc"; +import { tx } from "@/platform/i18n"; export const CLASSPATH = "${classpath}"; export const TEST_CLASSPATH = "${testClasspath}"; @@ -103,3 +104,48 @@ export function jdwpListeningPort(line: string): number | null { const m = /Listening for transport dt_socket at address:\s*(?:[\w.[\]:-]*:)?(\d+)\s*$/.exec(line.trim()); return m ? Number(m[1]) : null; } + +/** + * Module directory (relative to the project root, `""` for the root) of a JVM + * source file: the part before `src/{main,test}/…`, else the working directory + * of the closest Maven / Gradle configuration. + */ +export function jvmModuleDir(relPath: string, configs: readonly RunConfig[] = []): string { + const m = /^(?:(.*)\/)?src\/(?:main|test|[\w-]+)\/(?:java|kotlin|scala|groovy)\//.exec(relPath); + if (m) return m[1] ?? ""; + const cwds = configs + .filter((c) => c.kind === "maven" || c.kind === "gradle") + .map((c) => (c.cwd ?? "").replace(/^\.\/?/, "").replace(/\/+$/, "")) + .filter((cwd) => !cwd || relPath.startsWith(cwd + "/")) + .sort((a, b) => b.length - a.length); + return cwds[0] ?? ""; +} + +/** Recover the classpath module of configurations saved before it was stored separately. */ +export async function findJavaModule(config: RunConfig): Promise { + if (config.module != null) return config.module; + const main = parseJavaArgs(config.args).mainClass; + if (!main) return null; + const outer = main.split("$")[0]!; + const simple = outer.slice(outer.lastIndexOf(".") + 1); + const pkg = outer.slice(0, Math.max(0, outer.lastIndexOf("."))); + const names = [`${simple}.java`, `${simple}.kt`, ...(simple.endsWith("Kt") ? [`${simple.slice(0, -2)}.kt`] : [])]; + const matches = (await Promise.all(names.map((name) => call(commands.findFiles(name, 1000))))) + .flat() + .filter((file) => names.includes(file.relPath.split("/").pop()!)); + const configs = await call(commands.runConfigs()); + const modules = new Set(); + for (const file of matches) { + const text = await call(commands.fsReadText(file.path)); + const sourcePackage = /^\s*package\s+([\w.]+)\s*[;\r\n]/m.exec(text)?.[1] ?? ""; + if (sourcePackage === pkg) modules.add(jvmModuleDir(file.relPath, configs)); + } + return modules.size === 1 ? [...modules][0]! : null; +} + +export async function resolveJavaModule(config: RunConfig): Promise { + const module = await findJavaModule(config); + if (module === null) + throw new Error(tx`Cannot determine the module for main class '${parseJavaArgs(config.args).mainClass}'`); + return module; +} diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 227a177f..c09e63d1 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -1,4 +1,5 @@ { + "Cannot determine the module for main class '{value}'": "无法确定主类 '{value}' 所属的模块", "Attach": "附加", "Remove from Workspace": "从工作区移除", "Downloading Java {0} language server runtime": "正在下载 Java {0} 语言服务器运行时",