Files
openidea/src/platform/javaLaunch.ts
T
l0ng-ai acf6931017 fix(java): Run jdtls with a Lombok agent and show build errors at launch (#9)
* fix(java): Run jdtls with a Lombok agent and show build errors at launch

jdtls compiles with ECJ, which only generates Lombok members when the
server JVM runs -javaagent:lombok.jar. Nothing put it there, so @Data /
@Setter classes lost their accessors and Run launched stale classes
with "Unresolved compilation problem" stubs and no explanation.

- Projects whose build files mention Lombok get the newest jar: the
  IDE-managed one (new "Lombok (jdtls agent)" tool, latest release from
  Maven Central), else ~/.m2 (incl. settings.xml localRepository) or the
  Gradle cache. Each agent gets its own jdtls workspace so everything is
  rebuilt with it.
- Settings › Languages & Frameworks › Java › JVM options
  (java.jdt.ls.vmargs); a Lombok agent there wins.
- Run (and Debug, as a notification) list the first compile errors when
  the build has errors, plus a hint when Lombok is missing or older than
  1.18.34.

Fixes #3

* fix(java): Base the Lombok hint on the agent jdtls actually runs

A Lombok agent in Settings › Java › JVM options wins over the managed
and cached jars, but the launch hint still looked only at those: with
no cached jar it said Lombok was missing, and with an old cached one it
blamed that version although jdtls ran the user's. The hint and the
startup warning now use the user's agent (version from its file name).
2026-10-23 18:46:13 +08:00

106 lines
4.3 KiB
TypeScript

/**
* Java "Application" run configurations, shared by Run and Debug: the command
* line is `java [vm options] -cp ${classpath} <main class> [program args]`;
* `${classpath}` (or `${testClasspath}`) and the `java` launcher are resolved
* through jdtls when the configuration starts.
*/
import type { JavaLaunchInfo, RunConfig } from "@/platform/ipc";
export const CLASSPATH = "${classpath}";
export const TEST_CLASSPATH = "${testClasspath}";
/** Run configuration kinds launched as a JVM main class. */
export const JVM_APPLICATION_KINDS = ["application", "spring-boot"];
export function isJvmApplication(config: Pick<RunConfig, "kind">): boolean {
return JVM_APPLICATION_KINDS.includes(config.kind);
}
/** `java` options whose value is the next argument. */
const WITH_VALUE = new Set([
"-cp",
"-classpath",
"--class-path",
"-p",
"--module-path",
"--upgrade-module-path",
"--add-modules",
"--add-opens",
"--add-exports",
"--add-reads",
"--patch-module",
"--limit-modules",
"--enable-native-access",
]);
export interface JavaCommandLine {
/** VM options, without the classpath. */
vmArgs: string[];
mainClass: string | null;
programArgs: string[];
/** Which classpath placeholder the configuration uses (`null`: an explicit classpath or none). */
classpath: "runtime" | "test" | null;
/** An explicit `-cp` value other than a placeholder. */
explicitClasspath: string | null;
}
/** Splits `java` arguments into VM options, main class and program arguments. */
export function parseJavaArgs(args: readonly string[]): JavaCommandLine {
const vmArgs: string[] = [];
let classpath: JavaCommandLine["classpath"] = null;
let explicitClasspath: string | null = null;
for (let i = 0; i < args.length; i++) {
const a = args[i]!;
if (a === "-cp" || a === "-classpath" || a === "--class-path") {
const value = args[++i] ?? "";
if (value === CLASSPATH) classpath = "runtime";
else if (value === TEST_CLASSPATH) classpath = "test";
else explicitClasspath = value;
continue;
}
if (WITH_VALUE.has(a)) {
vmArgs.push(a, args[++i] ?? "");
continue;
}
if (a.startsWith("-")) {
vmArgs.push(a);
continue;
}
return { vmArgs, mainClass: a, programArgs: args.slice(i + 1), classpath, explicitClasspath };
}
return { vmArgs, mainClass: null, programArgs: [], classpath, explicitClasspath };
}
/** The configuration with its placeholders and launcher resolved. */
export function resolveJavaCommand(config: RunConfig, info: JavaLaunchInfo, pathSeparator = ":"): RunConfig {
const cp = info.classPaths.join(pathSeparator);
const args: string[] = [];
if (info.modulePaths.length && !config.args.some((a) => a === "-p" || a === "--module-path"))
args.push("--module-path", info.modulePaths.join(pathSeparator));
args.push(...config.args.map((a) => (a === CLASSPATH || a === TEST_CLASSPATH ? cp : a)));
const command = config.command === "java" && info.javaExec ? info.javaExec : config.command;
return { ...config, command, args };
}
/** Console report of a launch's compile errors (`path:line:col: error: …`) and their likely causes; "" when none. */
export function formatCompileErrors(info: JavaLaunchInfo): string {
const lines = info.errors.map((e) => `${e.path}:${e.line}:${e.column}: error: ${e.message}`);
const more = info.errorCount - info.errors.length;
if (more > 0) lines.push(`… and ${more} more error${more === 1 ? "" : "s"} (see the Problems tool window)`);
lines.push(...info.hints);
return lines.length ? `${lines.join("\n")}\n` : "";
}
/** Joins arguments into one string for adapters that take `args` / `vmArgs` as text (java-debug). */
export function joinJavaArgs(args: readonly string[]): string {
return args.map((a) => (a === "" ? '""' : /[\s"]/.test(a) ? `"${a.replace(/(["\\])/g, "\\$1")}"` : a)).join(" ");
}
/** JDWP agent options: listen on a free local port and wait for the debugger. */
export const JDWP_AGENT = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0";
/** The port of `Listening for transport dt_socket at address: 5005` (JDWP's start-up line). */
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;
}