Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ The debugger will automatically attach. See [No-Config Debug Documentation](bund

No-Config Debug is enabled by default. To disable the terminal integration and the AI `debug_java_application` tool, set `"java.debug.settings.enableNoConfigDebug": false`, reload VS Code, and recreate existing terminals. Standard Java launch/attach debugging, including F5 and Run/Debug CodeLens, remains available.

Preparing the terminal integration does not start or wait for the Java language extension. The wrapper uses `JAVA_HOME`, then `VSCODE_JAVA_EXEC` when available, then `java` on `PATH`. If Java support activates later, recreate an existing terminal to use the newly discovered Java executable.

## AI-Assisted Debugging

When using GitHub Copilot Chat, you can now ask AI to help you debug Java applications! The extension provides a Language Model Tool that enables natural language debugging:
Expand Down
4 changes: 4 additions & 0 deletions bundled/scripts/noConfigScripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ When you open a terminal in VS Code with this extension installed, the following

Note: `JAVA_TOOL_OPTIONS` is NOT set globally to avoid affecting other Java tools (javac, maven, gradle). Instead, it's set only when you run the `debugjava` command.

Terminal setup does not activate or wait for Language Support for Java. Opening the debug configuration picker in a non-Java workspace therefore does not start Java just to discover its executable. If Java support is already active, its tooling JDK is contributed as `VSCODE_JAVA_EXEC`; otherwise, the integration observes activation in the background and adds it when available. Observation stops after activation or when the integration is disposed.

The wrapper keeps its selection order: `JAVA_HOME`, then `VSCODE_JAVA_EXEC`, then `java` on `PATH`. An existing cached `VSCODE_JAVA_EXEC` is retained while Java support is unavailable. Terminals created before Java executable discovery must be recreated to receive the new value. Actual Java launch/attach still requires Java support and may activate it.

## Disabling No-Config Debug

No-Config Debug is enabled by default. To opt out for all workspaces or just the current workspace, add this to the corresponding VS Code settings:
Expand Down
31 changes: 10 additions & 21 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { initializeThreadOperations } from "./threadOperations";
import * as utility from "./utility";
import { registerBreakpointCommands } from "./breakpointCommands";
import { registerVariableMenuCommands } from "./variableMenu";
import { promisify } from "util";

export async function activate(context: vscode.ExtensionContext): Promise<any> {
await initializeFromJsonFile(context.asAbsolutePath("./package.json"));
Expand Down Expand Up @@ -97,7 +96,7 @@ function initializeExtension(_operationId: string, context: vscode.ExtensionCont
initializeHotCodeReplace(context);
initializeCodeLensProvider(context);
initializeThreadOperations(context);
subscribeToJavaExtensionEvents();
subscribeToJavaExtensionEvents(context);

context.subscriptions.push(vscode.languages.registerInlineValuesProvider("java", new JavaInlineValuesProvider()));
return {
Expand All @@ -110,8 +109,6 @@ export async function deactivate() {
await disposeTelemetryWrapper();
}

const delay = promisify(setTimeout);

/**
* Register Language Model Tools after Java Language Server is ready.
* The debug tools depend on JDT.LS for compilation, classpath resolution,
Expand Down Expand Up @@ -157,22 +154,11 @@ async function registerLanguageModelToolsWhenReady(context: vscode.ExtensionCont
}
}

async function subscribeToJavaExtensionEvents(): Promise<void> {
const javaExt = vscode.extensions.getExtension("redhat.java");
if (!javaExt) {
return;
}

// wait javaExt to activate
const timeout = 30 * 60 * 1000; // wait 30 min at most
let count = 0;
while (!javaExt.isActive && count < timeout) {
await delay(1000);
count += 1000;
}

if (javaExt.isActive) {
javaExt.exports?.onDidSourceInvalidate?.((event: any) => {
function subscribeToJavaExtensionEvents(context: vscode.ExtensionContext): void {
context.subscriptions.push(utility.onDidActivateJavaExtension((api: {
onDidSourceInvalidate?: vscode.Event<{ affectedRootPaths?: string[] }>;
} | undefined) => {
const subscription = api?.onDidSourceInvalidate?.((event) => {
if (event?.affectedRootPaths?.length) {
const activeDebugSession = vscode.debug.activeDebugSession;
if (activeDebugSession?.type === "java") {
Expand All @@ -182,7 +168,10 @@ async function subscribeToJavaExtensionEvents(): Promise<void> {
}
}
});
}
if (subscription) {
context.subscriptions.push(subscription);
}
}));
}

function registerDebugEventListener(context: vscode.ExtensionContext) {
Expand Down
26 changes: 9 additions & 17 deletions src/noConfigDebugInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as path from 'path';
import * as vscode from 'vscode';

import { sendInfo, sendError } from "vscode-extension-telemetry-wrapper";
import { getJavaHome } from "./utility";
import { onDidActivateJavaExtension } from "./utility";
import { buildNoConfigPathAppendValue } from "./pathUtil";
import { applyAppendIfChanged, applyReplaceIfChanged } from "./envVarSync";

Expand Down Expand Up @@ -237,7 +237,7 @@ export async function registerNoConfigDebug(
});
};

// Listen before publishing the endpoint or awaiting Java/script setup.
// Listen before publishing the endpoint or awaiting script setup.
// Terminals surviving a reload may already have the stable endpoint path.
const fileCreationEvent = fileSystemWatcher.onDidCreate(handleEndpointFile);
const fileChangeEvent = fileSystemWatcher.onDidChange(handleEndpointFile);
Expand Down Expand Up @@ -267,21 +267,6 @@ export async function registerNoConfigDebug(
// only in the debugjava wrapper scripts (debugjava.ps1, debugjava.bat, debugjava)
applyReplaceIfChanged(collection, 'VSCODE_JDWP_ADAPTER_ENDPOINTS', tempFilePath);

// Try to get Java executable from Java Language Server
// This ensures we use the same Java version as the project is compiled with.
// If detection fails or returns nothing, we deliberately keep any previously
// set VSCODE_JAVA_EXEC to avoid churn from transient startup failures.
try {
const javaHome = await getJavaHome();
if (javaHome) {
const javaExec = path.join(javaHome, 'bin', 'java');
applyReplaceIfChanged(collection, 'VSCODE_JAVA_EXEC', javaExec);
}
} catch (error) {
// If we can't get Java from Language Server, that's okay
// The wrapper script will fall back to JAVA_HOME or PATH
}

const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts');
const debugJavaScriptPath = path.join(noConfigScriptsDir, "debugjava");
try {
Expand All @@ -294,9 +279,16 @@ export async function registerNoConfigDebug(
sendError(error);
}
applyAppendIfChanged(collection, 'PATH', buildNoConfigPathAppendValue(noConfigScriptsDir));
const javaExecutableSync = onDidActivateJavaExtension((api: { javaRequirement?: { java_home?: string } } | undefined) => {
const javaHome = api?.javaRequirement?.java_home;
if (javaHome) {
applyReplaceIfChanged(collection, "VSCODE_JAVA_EXEC", path.join(javaHome, "bin", "java"));
}
});

return Promise.resolve(
new vscode.Disposable(() => {
javaExecutableSync.dispose();
fileSystemWatcher.dispose();
fileCreationEvent.dispose();
fileChangeEvent.dispose();
Expand Down
16 changes: 7 additions & 9 deletions src/stackTraceLinkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
// Licensed under the MIT license.

import { CancellationToken, commands, DocumentLink, DocumentLinkProvider, DocumentSelector,
env, ExtensionContext, languages, Position, ProviderResult, Range, TextDocument, Uri,
env, Event, ExtensionContext, languages, Position, ProviderResult, Range, TextDocument, Uri,
window, workspace } from "vscode";
import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper";
import { resolveSourceUri } from "./languageServerPlugin";
import { parseJavaStackFrame } from "./stackFrameParser";
import { getJavaExtensionAPI, isJavaExtEnabled, ServerMode } from "./utility";
import { onDidActivateJavaExtension, ServerMode } from "./utility";

const ANALYZE_STACK_TRACE_COMMAND = "java.debug.analyzeStackTrace";
const NAVIGATE_TO_STACK_FRAME_COMMAND = "_java.debug.navigateToStackFrame";
Expand Down Expand Up @@ -177,16 +177,14 @@ export function registerStackTraceLinkProvider(context: ExtensionContext): void
}

function registerLinkProviderWhenReady(context: ExtensionContext): void {
// Without the Java language server, frames cannot be resolved to source - nothing to linkify.
if (!isJavaExtEnabled()) {
return;
}

const doRegister = () => context.subscriptions.push(
languages.registerDocumentLinkProvider(STACK_TRACE_DOCUMENT_SELECTOR, new JavaStackTraceLinkProvider()),
);

getJavaExtensionAPI().then((api) => {
context.subscriptions.push(onDidActivateJavaExtension((api: {
serverMode: string;
onDidServerModeChange: Event<string>;
} | undefined) => {
if (!api) {
return;
}
Expand All @@ -205,5 +203,5 @@ function registerLinkProviderWhenReady(context: ExtensionContext): void {
// Already in Standard mode.
doRegister();
}
});
}));
}
62 changes: 56 additions & 6 deletions src/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,69 @@ export function getJavaExtensionAPI(progressReporter?: IProgressReporter): Thena
throw new JavaExtensionNotEnabledError("VS Code Java Extension is not enabled.");
}

return new Promise<any>(async (resolve) => {
progressReporter?.getCancellationToken().onCancellationRequested(() => {
resolve(undefined);
});
const token = progressReporter?.getCancellationToken();
if (token?.isCancellationRequested) {
return Promise.resolve(undefined);
}

resolve(await extension.activate());
});
const activation = extension.activate();
if (!token) {
return activation;
}

let listener: vscode.Disposable | undefined;
return Promise.race([
activation,
new Promise<undefined>((resolve) => {
listener = token.onCancellationRequested(() => resolve(undefined));
}),
]).finally(() => listener?.dispose());
}

export function getJavaExtension(): vscode.Extension<any> | undefined {
return vscode.extensions.getExtension(JAVA_EXTENSION_ID);
}

export function onDidActivateJavaExtension<T>(listener: (api: T) => void): vscode.Disposable {
let disposed = false;
let timer: NodeJS.Timeout | undefined;
const update = (): boolean => {
if (disposed) {
return true;
}
try {
const extension = vscode.extensions.getExtension<T>(JAVA_EXTENSION_ID);
if (!extension?.isActive) {
return false;
}
listener(extension.exports);
} catch {
sendError({
name: "JavaExtensionError",
message: "Could not initialize integration with the active Java extension.",
});
}
return true;
};

// VS Code has no extension-activation event. Observe without activating Java,
// including when the first Java file is opened much later in this workspace.
if (!update()) {
timer = setInterval(() => {
if (update()) {
clearInterval(timer);
}
}, 1000);
timer.unref();
}
return new vscode.Disposable(() => {
disposed = true;
if (timer) {
clearInterval(timer);
}
});
}

export function isJavaExtEnabled(): boolean {
const javaExt = vscode.extensions.getExtension(JAVA_EXTENSION_ID);
return !!javaExt;
Expand Down
Loading
Loading