fix(debugger): name the Java project in debug requests (#1866)

java-debug resolves the Java project for Evaluate from `mainClass` only when the class
belongs to a project jdtls knows, and a JDWP attach cannot even see the main class, so
every expression failed with "Cannot evaluate, please specify projectName in launch.json".

Both builders now send `projectName`: the launch one for the project root jdtls resolved,
the attach one for the module directory. The name is read from Eclipse's `.project`
descriptor, else the Maven `artifactId` (jdtls names a module after its artifact, not its
folder), else the folder name.

Co-authored-by: chao <[email protected]>
Цей коміт міститься в:
WangTongchaoтаchao authored and GitHub committed 2026-11-07 03:10:52 +08:00
1 джерело fa36733f80
коміт ab9bb7c266
2 files changed
+82 -4

No files matched your search

+35 -3
Переглянути файл
@@ -4,6 +4,7 @@ import type { RunConfig } from "@/platform/ipc";
const invoked: Array<[string, unknown[]]> = [];
let shortenCommandLine: string | null = null;
let kotlinFiles = false;
const files: Record<string, string> = {};
vi.mock("@/platform/ipc", async (orig) => {
const real = await orig<typeof import("@/platform/ipc")>();
const ok = (data: unknown) => Promise.resolve({ status: "ok", data });
@@ -17,7 +18,7 @@ vi.mock("@/platform/ipc", async (orig) => {
? [{ path: "/p/api/src/main/java/a/Main.java", relPath: "api/src/main/java/a/Main.java" }]
: [],
),
fsReadText: () => ok("package a;\npublic class Main {}"),
fsReadText: (path: string) => ok(files[path] ?? "package a;\npublic class Main {}"),
runConfigs: () => ok([]),
fsReadDir: (path: string) =>
ok(
@@ -49,7 +50,7 @@ vi.mock("@/platform/ipc", async (orig) => {
};
});
const { isJavaDebuggable, javaAttachConfig, javaLaunchConfig } = await import("../java");
const { isJavaDebuggable, javaAttachConfig, javaLaunchConfig, javaProjectName } = await import("../java");
const app: RunConfig = {
id: "application:a.Main",
@@ -90,6 +91,7 @@ describe("Java debugging (java-debug through jdtls)", () => {
args: '--port "8081 x"',
console: "internalConsole",
shortenCommandLine: "none",
projectName: "p",
});
});
@@ -162,7 +164,12 @@ describe("Java debugging (java-debug through jdtls)", () => {
it("attaches to a JDWP agent", async () => {
const l = await javaAttachConfig({ name: "mvn test", module: "", hostName: "127.0.0.1", port: 5005 }, "/p");
expect(l).toMatchObject({ request: "attach", cwd: "/p" });
expect(JSON.parse(l.extraJson!)).toMatchObject({ debugServer: 41234, hostName: "127.0.0.1", port: 5005 });
expect(JSON.parse(l.extraJson!)).toMatchObject({
debugServer: 41234,
hostName: "127.0.0.1",
port: 5005,
projectName: "p",
});
});
});
@@ -176,3 +183,28 @@ it.each(["sibling", "/outside/modules"])("debugs from %s with the main class mod
expect(launch.cwd).toBe(cwd.startsWith("/") ? cwd : `/p/${cwd}`);
}
});
describe("naming the Java project jdtls imported", () => {
beforeEach(() => {
invoked.length = 0;
for (const key of Object.keys(files)) delete files[key];
});
it("uses the Maven artifactId, not the folder name, and skips the parent's", async () => {
files["/p/pom.xml"] =
"<project><parent><artifactId>orders-parent</artifactId></parent><artifactId>openidea-fixture-orders</artifactId></project>";
expect(await javaProjectName("/p")).toBe("openidea-fixture-orders");
});
it("prefers the Eclipse project descriptor", async () => {
files["/p/.project"] = "<projectDescription><name>taihe</name></projectDescription>";
files["/p/pom.xml"] = "<project><artifactId>orders</artifactId></project>";
expect(await javaProjectName("/p")).toBe("taihe");
});
it("falls back to the folder name when the project says nothing, including unresolved properties", async () => {
expect(await javaProjectName("/p/api")).toBe("api");
files["/p/pom.xml"] = "<project><artifactId>${revision}</artifactId></project>";
expect(await javaProjectName("/p")).toBe("p");
});
});
+47 -1
Переглянути файл
@@ -30,6 +30,37 @@ function join(base: string, p: string) {
return rel === "." || rel === "" ? base : `${base.replace(/\/$/, "")}/${rel}`;
}
/** Last path segment: jdtls names a Java project after the folder it imported. */
function baseName(p: string): string {
const s = p.replace(/[\\/]+$/, "");
const cut = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
return cut >= 0 ? s.slice(cut + 1) : s;
}
/** The name jdtls knows this folder's Java project by: Eclipse's project descriptor for an Eclipse
* project, the Maven artifactId for a module (jdtls names those after the artifact, not the
* folder), and the folder name when neither says anything.
*/
export async function javaProjectName(root: string): Promise<string> {
const descriptor = await readText(join(root, ".project"));
const eclipse = /<name>([^<]+)<\/name>/.exec(descriptor)?.[1]?.trim();
if (eclipse) return eclipse;
const pom = await readText(join(root, "pom.xml"));
// A child module's own <artifactId> follows the <parent> block, which names the parent.
const own = pom.replace(/<parent>[\s\S]*?<\/parent>/, "");
const artifact = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(own)?.[1];
if (artifact && !artifact.includes("${")) return artifact;
return baseName(root);
}
/** A file of the project, or "" when it is missing (or when there is no filesystem bridge). */
async function readText(path: string): Promise<string> {
try {
return await call(commands.fsReadText(path));
} catch {
return "";
}
}
/** Conventional JVM source sets, including Kotlin kept in a Java source directory.
* A mixed module uses Kotlin's adapter, which also accepts Java line breakpoints.
*/
@@ -125,6 +156,10 @@ export async function javaLaunchConfig(
}
const port = await debugServer(module, preparation?.signal);
preparation?.signal.throwIfAborted();
// java-debug resolves the Java project for Evaluate on its own from `mainClass` only when
// the class belongs to a project it knows; name the project it imported so that works for
// classes compiled outside the language server's build (plain folders, custom outputs).
const project = await javaProjectName(info.projectRoot ?? root);
return {
name: rc.name,
adapter: JAVA_ADAPTER,
@@ -139,6 +174,7 @@ export async function javaLaunchConfig(
mainClass: parsed.mainClass,
classPaths,
modulePaths: info.modulePaths,
projectName: project,
javaExec: resolved.command === "java" ? undefined : resolved.command,
vmArgs: joinJavaArgs([...consoleVmArgs(rc), ...parsed.vmArgs]),
args: joinJavaArgs(parsed.programArgs),
@@ -180,6 +216,10 @@ export async function javaAttachConfig(target: JavaAttachTarget, root: string):
};
}
const port = await debugServer(target.module);
// java-debug resolves the Java project for Evaluate from `mainClass` only when the class is in
// a project it imported; a JDWP attach cannot even see the main class, so name the project the
// language server imported (the directory name of the module it opened).
const project = await javaProjectName(cwd);
return {
name: target.name,
adapter: JAVA_ADAPTER,
@@ -188,6 +228,12 @@ export async function javaAttachConfig(target: JavaAttachTarget, root: string):
args: [],
cwd: target.module ? join(root, target.module) : root,
env: {},
extraJson: JSON.stringify({ debugServer: port, hostName: target.hostName, port: target.port, timeout: 30_000 }),
extraJson: JSON.stringify({
debugServer: port,
hostName: target.hostName,
port: target.port,
timeout: 30_000,
projectName: project,
}),
};
}