mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(java): resolve application classpaths independently of working directory (#493)
Run and Debug passed the configuration working directory to java_resolve_launch as the module, so changing the working directory also changed which jdtls project supplied the classpath and failed with "references non-existing project". Java application configurations now store their module at detection and creation, keep it when the working directory changes, and clear it when the main class changes. Older saved configurations resolve it from the main class source file. The Kotlin debugger check also uses the module directory. Fixes #435
This commit is contained in:
@@ -78,6 +78,7 @@ struct FileConfig {
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
cwd: Option<PathBuf>,
|
||||
module: Option<String>,
|
||||
#[serde(default)]
|
||||
env: BTreeMap<String, String>,
|
||||
#[serde(default, alias = "isTest", alias = "is_test")]
|
||||
@@ -161,6 +162,7 @@ fn convert(configs: Vec<FileConfig>, local: bool) -> Vec<RunConfig> {
|
||||
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::<Array>()));
|
||||
}
|
||||
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('\\', "/")));
|
||||
}
|
||||
|
||||
@@ -548,11 +548,11 @@ fn spring_boot_apps(root: &Path, build: &Path, out: &mut Vec<RunConfig>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
/// 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,
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ const config: RunConfig = {
|
||||
command: "java",
|
||||
args: ["-cp", "${classpath}", "Main"],
|
||||
cwd: null,
|
||||
module: "",
|
||||
env: {},
|
||||
detected: true,
|
||||
isTest: false,
|
||||
|
||||
@@ -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<LaunchConfig> {
|
||||
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,
|
||||
|
||||
@@ -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 });
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<typeof import("@/platform/ipc")>();
|
||||
return { ...real, isTauri: () => true, commands: { ...real.commands, ...ipc } };
|
||||
});
|
||||
vi.mock("@tauri-apps/api/core", async (original) => ({
|
||||
...(await original<typeof import("@tauri-apps/api/core")>()),
|
||||
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();
|
||||
});
|
||||
@@ -27,6 +27,7 @@ const config: RunConfig = {
|
||||
command: "java",
|
||||
args: ["-cp", "${classpath}", "Main"],
|
||||
cwd: null,
|
||||
module: "",
|
||||
env: {},
|
||||
detected: false,
|
||||
isTest: false,
|
||||
|
||||
@@ -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<void> {
|
||||
|
||||
/** Persists user configurations and reloads the merged list. */
|
||||
export async function saveUserConfigs(user: RunConfig[], removedDetectedIds: string[] = []): Promise<void> {
|
||||
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");
|
||||
|
||||
@@ -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<string, string>, 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 } : {}),
|
||||
};
|
||||
|
||||
@@ -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<string | null> {
|
||||
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<string>();
|
||||
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<string> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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} 语言服务器运行时",
|
||||
|
||||
Reference in New Issue
Block a user