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
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-31 00:59:22 +08:00
1 джерело 2754cf327f
коміт 0660f3c020
17 files changed
+265 -30

No files matched your search

+20
Переглянути файл
@@ -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}`);
}
});
+1
Переглянути файл
@@ -26,6 +26,7 @@ const config: RunConfig = {
command: "java",
args: ["-cp", "${classpath}", "Main"],
cwd: null,
module: "",
env: {},
detected: true,
isTest: false,
+5 -3
Переглянути файл
@@ -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,