mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
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]>
240 lines
8.8 KiB
TypeScript
240 lines
8.8 KiB
TypeScript
/**
|
|
* Java debugging through Microsoft java-debug, a jdtls bundle: jdtls starts its
|
|
* DAP server (`vscode.java.startDebugSession`) and the session connects to that
|
|
* port (`debugServer`). Application configurations are launched by java-debug;
|
|
* JVMs started by Maven / Gradle with a JDWP agent are attached to.
|
|
*/
|
|
import { executeAction } from "@/platform/actions";
|
|
import { call, commands, IpcError, nextRequestId, type LaunchConfig, type RunConfig } from "@/platform/ipc";
|
|
import {
|
|
consoleVmArgs,
|
|
formatCompileErrors,
|
|
isJvmApplication,
|
|
joinJavaArgs,
|
|
parseJavaArgs,
|
|
prepareJavaLaunch,
|
|
type JavaPreparation,
|
|
} from "@/platform/javaLaunch";
|
|
import { notifications } from "@/workbench/contributions";
|
|
|
|
export const JAVA_ADAPTER = "java-debug";
|
|
export const KOTLIN_ADAPTER = "kotlin-debug-adapter";
|
|
|
|
export function isJavaDebuggable(rc: RunConfig): boolean {
|
|
return isJvmApplication(rc) && [JAVA_ADAPTER, KOTLIN_ADAPTER].includes(rc.debugAdapter ?? JAVA_ADAPTER);
|
|
}
|
|
|
|
function join(base: string, p: string) {
|
|
if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) return p;
|
|
const rel = p.replace(/^\.\//, "");
|
|
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.
|
|
*/
|
|
export async function moduleHasKotlinSources(cwd: string): Promise<boolean> {
|
|
const pending = [join(cwd, "src")];
|
|
while (pending.length) {
|
|
const path = pending.pop()!;
|
|
const entries = await call(commands.fsReadDir(path)).catch(() => []);
|
|
for (const entry of entries) {
|
|
if (entry.excluded) continue;
|
|
if ((entry.kind === "file" || entry.targetKind === "file") && entry.name.endsWith(".kt")) return true;
|
|
// Do not follow directory symlinks: a source tree can link back to a parent.
|
|
if (entry.kind === "dir") pending.push(entry.path);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** java-debug missing from jdtls: offer the one-click install (jdtls restarts with the bundle). */
|
|
function offerInstall(e: unknown): never {
|
|
if (e instanceof IpcError && e.kind === "unavailable" && /java-debug|debugger/i.test(e.message)) {
|
|
notifications().notify({
|
|
group: "run",
|
|
level: "warning",
|
|
key: "debug.java-debug-missing",
|
|
title: "The Java debugger is not installed",
|
|
body: "Debugging Java needs Microsoft java-debug, loaded into the Java language server. Install it and debug again once jdtls has restarted.",
|
|
sticky: true,
|
|
actions: [{ label: "Install", run: () => void executeAction("tools.install", JAVA_ADAPTER) }],
|
|
});
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
async function debugServer(module: string, signal?: AbortSignal): Promise<number> {
|
|
try {
|
|
return await call(commands.javaDebugServer(module));
|
|
} catch (e) {
|
|
signal?.throwIfAborted();
|
|
offerInstall(e);
|
|
}
|
|
}
|
|
|
|
/** Launch request for an Application configuration (`java … -cp ${classpath} Main args`). */
|
|
export async function javaLaunchConfig(
|
|
rc: RunConfig,
|
|
root: string,
|
|
preparation?: JavaPreparation,
|
|
): Promise<LaunchConfig> {
|
|
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, info, resolved } = await prepareJavaLaunch(
|
|
rc,
|
|
preparation ?? {
|
|
request: nextRequestId(),
|
|
signal: new AbortController().signal,
|
|
},
|
|
);
|
|
if (info.errorCount > 0 && !preparation?.report)
|
|
notifications().notify({
|
|
group: "run",
|
|
level: "warning",
|
|
key: "debug.java-compile-errors",
|
|
title: `'${rc.name}' has compile errors; debugging the last compiled classes`,
|
|
body: formatCompileErrors(info).trimEnd(),
|
|
});
|
|
const kotlin = rc.debugAdapter === KOTLIN_ADAPTER || (await moduleHasKotlinSources(join(root, module)));
|
|
preparation?.signal.throwIfAborted();
|
|
const classPaths = parsed.explicitClasspath
|
|
? parsed.explicitClasspath.split(info.pathListSeparator)
|
|
: info.classPaths;
|
|
if (kotlin) {
|
|
return {
|
|
name: rc.name,
|
|
adapter: KOTLIN_ADAPTER,
|
|
request: "launch",
|
|
program: null,
|
|
args: [],
|
|
cwd,
|
|
env: { ...rc.env },
|
|
extraJson: JSON.stringify({
|
|
projectRoot: join(root, module),
|
|
mainClass: parsed.mainClass,
|
|
classPaths,
|
|
modulePaths: info.modulePaths,
|
|
javaExec: resolved.command,
|
|
vmArguments: [...consoleVmArgs(rc), ...parsed.vmArgs],
|
|
programArgs: parsed.programArgs,
|
|
}),
|
|
};
|
|
}
|
|
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,
|
|
request: "launch",
|
|
program: null,
|
|
// java-debug takes `args` / `vmArgs` as strings.
|
|
args: [],
|
|
cwd,
|
|
env: { ...rc.env },
|
|
extraJson: JSON.stringify({
|
|
debugServer: port,
|
|
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),
|
|
// java-debug spawns the JVM itself: it must shorten a classpath too long for the OS the same way Run does.
|
|
shortenCommandLine: info.shortenCommandLine ?? "none",
|
|
console: "internalConsole",
|
|
stopOnEntry: false,
|
|
}),
|
|
};
|
|
}
|
|
|
|
export interface JavaAttachTarget {
|
|
name: string;
|
|
/** Module directory (relative to the project root) whose jdtls resolves sources. */
|
|
module: string;
|
|
hostName: string;
|
|
port: number;
|
|
}
|
|
|
|
export function isJavaAttachTarget(
|
|
arg: unknown,
|
|
): arg is JavaAttachTarget & { process?: number | null; runConfig?: RunConfig } {
|
|
return !!arg && typeof arg === "object" && "port" in arg && typeof (arg as { port: unknown }).port === "number";
|
|
}
|
|
|
|
/** Attach request for a JVM listening with a JDWP agent. */
|
|
export async function javaAttachConfig(target: JavaAttachTarget, root: string): Promise<LaunchConfig> {
|
|
const cwd = target.module ? join(root, target.module) : root;
|
|
if (await moduleHasKotlinSources(cwd)) {
|
|
return {
|
|
name: target.name,
|
|
adapter: KOTLIN_ADAPTER,
|
|
request: "attach",
|
|
program: null,
|
|
args: [],
|
|
cwd,
|
|
env: {},
|
|
extraJson: JSON.stringify({ projectRoot: cwd, hostName: target.hostName, port: target.port, timeout: 30_000 }),
|
|
};
|
|
}
|
|
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,
|
|
request: "attach",
|
|
program: null,
|
|
args: [],
|
|
cwd: target.module ? join(root, target.module) : root,
|
|
env: {},
|
|
extraJson: JSON.stringify({
|
|
debugServer: port,
|
|
hostName: target.hostName,
|
|
port: target.port,
|
|
timeout: 30_000,
|
|
projectName: project,
|
|
}),
|
|
};
|
|
}
|