Skip to content

Commit 867f458

Browse files
committed
refactor: gate palette command on javaLSReady, share frame parser, harden navigate
- Add a commandPalette when-clause (javaLSReady) to java.debug.analyzeStackTrace so it only shows once the Java language server is ready - matching where the linkify provider actually registers, and avoiding a dead-end scratch doc in non-Java windows. - Extract the duplicated frame regex + parsing into stackFrameParser.ts, shared by the terminal and document link providers so matching stays identical. The helper builds a fresh RegExp per call, preserving the terminal provider's no-shared-stateful-RegExp contract. - Wrap resolveSourceUri/navigation in navigateToStackFrame in try/catch (and await the navigation) so a not-ready/restarting language server can't surface an unhandled rejection. Copilot-Session: d80e7a97-119d-4f89-a02a-be25b076e806
1 parent 8565a6b commit 867f458

4 files changed

Lines changed: 90 additions & 37 deletions

File tree

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@
239239
}
240240
],
241241
"commandPalette": [
242+
{
243+
"command": "java.debug.analyzeStackTrace",
244+
"when": "javaLSReady"
245+
},
242246
{
243247
"command": "java.debug.hotCodeReplace",
244248
"when": "false"

src/stackFrameParser.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
// The single source of truth for parsing a Java stack frame, shared by the terminal link provider
5+
// and the document (stack-trace) link provider so the matching stays identical across surfaces.
6+
7+
export interface IParsedStackFrame {
8+
// The `com.foo.Bar.baz(Bar.java:42)` text handed to `resolveSourceUri` for source resolution.
9+
stackTrace: string;
10+
// The fully-qualified method (`com.foo.Bar.baz`), used for the class-name quick-pick fallback.
11+
methodName: string;
12+
// The 1-based source line number parsed from the frame.
13+
lineNumber: number;
14+
// Offset of the frame within the input line (points at the class name, past the leading `at `).
15+
startIndex: number;
16+
// Length of the linkifiable frame text (equals `stackTrace.length`).
17+
length: number;
18+
}
19+
20+
/**
21+
* Parses the first Java stack frame (e.g. `at module/com.foo.Bar.baz(Bar.java:42)`) out of a line,
22+
* or returns undefined when none is present.
23+
*
24+
* A fresh `RegExp` is created per call on purpose: provider callbacks can overlap asynchronously,
25+
* so a shared stateful `RegExp` (were a `g`/`y` flag ever added) could corrupt `lastIndex`.
26+
*/
27+
export function parseJavaStackFrame(line: string): IParsedStackFrame | undefined {
28+
// Group 2: optional module prefix, group 3: fully-qualified method, group 5: `File.java:line`.
29+
const regex = /(\sat\s+)([\w$.]+\/)?(([\w$]+\.)+[<\w$>]+)\(([\w-$]+\.java:\d+)\)/;
30+
const result = regex.exec(line);
31+
if (!result || !result.length) {
32+
return undefined;
33+
}
34+
35+
const stackTrace = `${result[2] || ""}${result[3]}(${result[5]})`;
36+
return {
37+
stackTrace,
38+
methodName: result[3],
39+
lineNumber: Number(result[5].split(":")[1]),
40+
startIndex: result.index + result[1].length,
41+
length: stackTrace.length,
42+
};
43+
}

src/stackTraceLinkProvider.ts

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CancellationToken, commands, DocumentLink, DocumentLinkProvider, Docume
66
window, workspace } from "vscode";
77
import { instrumentOperationAsVsCodeCommand, sendInfo } from "vscode-extension-telemetry-wrapper";
88
import { resolveSourceUri } from "./languageServerPlugin";
9+
import { parseJavaStackFrame } from "./stackFrameParser";
910
import { getJavaExtensionAPI, isJavaExtEnabled, ServerMode } from "./utility";
1011

1112
const ANALYZE_STACK_TRACE_COMMAND = "java.debug.analyzeStackTrace";
@@ -18,10 +19,6 @@ const STACK_TRACE_DOCUMENT_SELECTOR: DocumentSelector = [
1819
{ scheme: "untitled" },
1920
];
2021

21-
// Matches a Java stack frame such as `at module/com.foo.Bar.baz(Bar.java:42)`.
22-
// Group 2: optional module prefix, group 3: fully-qualified method, group 5: `File.java:line`.
23-
const STACK_FRAME_REGEX = /(\sat\s+)([\w$.]+\/)?(([\w$]+\.)+[<\w$>]+)\(([\w-$]+\.java:\d+)\)/;
24-
2522
// Guard against pathological input: cap the length of a scanned line (mitigates ReDoS on the
2623
// nested-quantifier regex) and the number of links produced for very large pasted traces.
2724
const MAX_SCANNED_LINE_LENGTH = 1000;
@@ -58,17 +55,21 @@ export class JavaStackTraceLinkProvider implements DocumentLinkProvider {
5855
continue;
5956
}
6057

61-
const result = STACK_FRAME_REGEX.exec(lineText);
62-
if (!result || !result.length) {
58+
const frame = parseJavaStackFrame(lineText);
59+
if (!frame) {
6360
continue;
6461
}
6562

66-
const stackTrace = `${result[2] || ""}${result[3]}(${result[5]})`;
67-
const lineNumber = Number(result[5].split(":")[1]);
68-
const startIndex = result.index + result[1].length;
69-
const range = new Range(new Position(i, startIndex), new Position(i, startIndex + stackTrace.length));
63+
const range = new Range(
64+
new Position(i, frame.startIndex),
65+
new Position(i, frame.startIndex + frame.length),
66+
);
7067

71-
const args: IStackFrameLinkArgs = { stackTrace, methodName: result[3], lineNumber };
68+
const args: IStackFrameLinkArgs = {
69+
stackTrace: frame.stackTrace,
70+
methodName: frame.methodName,
71+
lineNumber: frame.lineNumber,
72+
};
7273
const target = Uri.parse(`command:${NAVIGATE_TO_STACK_FRAME_COMMAND}?${encodeURIComponent(JSON.stringify(args))}`);
7374
links.push(new DocumentLink(range, target));
7475
}
@@ -97,22 +98,28 @@ async function navigateToStackFrame(args: IStackFrameLinkArgs): Promise<void> {
9798
*/
9899
sendInfo("", { operationName: "navigateToJavaStackFrame" });
99100

100-
const uri = await resolveSourceUri(args.stackTrace);
101-
if (uri) {
102-
const parsed = Uri.parse(uri);
103-
if (!ALLOWED_SOURCE_SCHEMES.has(parsed.scheme)) {
104-
return;
101+
try {
102+
const uri = await resolveSourceUri(args.stackTrace);
103+
if (uri) {
104+
const parsed = Uri.parse(uri);
105+
if (!ALLOWED_SOURCE_SCHEMES.has(parsed.scheme)) {
106+
return;
107+
}
108+
const targetLine = Math.max(args.lineNumber - 1, 0);
109+
await window.showTextDocument(parsed, {
110+
preserveFocus: true,
111+
selection: new Range(new Position(targetLine, 0), new Position(targetLine, 0)),
112+
});
113+
} else {
114+
// No source found: open the symbol quick pick scoped to the class name.
115+
const fullyQualifiedName = args.methodName.substring(0, args.methodName.lastIndexOf("."));
116+
const className = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf(".") + 1);
117+
await commands.executeCommand("workbench.action.quickOpen", "#" + className);
105118
}
106-
const targetLine = Math.max(args.lineNumber - 1, 0);
107-
window.showTextDocument(parsed, {
108-
preserveFocus: true,
109-
selection: new Range(new Position(targetLine, 0), new Position(targetLine, 0)),
110-
});
111-
} else {
112-
// No source found: open the symbol quick pick scoped to the class name.
113-
const fullyQualifiedName = args.methodName.substring(0, args.methodName.lastIndexOf("."));
114-
const className = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf(".") + 1);
115-
commands.executeCommand("workbench.action.quickOpen", "#" + className);
119+
} catch {
120+
// The internal navigate command is always registered, but resolving a frame needs the Java
121+
// language server in Standard mode. If it isn't (e.g. server restarting/downgraded) or the
122+
// resolved document fails to open, fail quietly instead of surfacing an unhandled rejection.
116123
}
117124
}
118125

@@ -124,7 +131,8 @@ async function analyzeStackTrace(): Promise<void> {
124131
// The command itself is auto-instrumented via instrumentOperationAsVsCodeCommand, so no
125132
// manual telemetry is needed here to track invocations.
126133
const clipboard = await env.clipboard.readText();
127-
const content = STACK_FRAME_REGEX.test(clipboard.slice(0, MAX_CLIPBOARD_SCAN_LENGTH)) ? clipboard : "";
134+
const looksLikeTrace = parseJavaStackFrame(clipboard.slice(0, MAX_CLIPBOARD_SCAN_LENGTH)) !== undefined;
135+
const content = looksLikeTrace ? clipboard : "";
128136
const document = await workspace.openTextDocument({ language: "log", content });
129137
await window.showTextDocument(document);
130138
}

src/terminalLinkProvider.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CancellationToken, commands, Position, ProviderResult, Range, TerminalL
55
TerminalLinkProvider, Uri, window } from "vscode";
66
import { sendInfo } from "vscode-extension-telemetry-wrapper";
77
import { resolveSourceUri } from "./languageServerPlugin";
8+
import { parseJavaStackFrame } from "./stackFrameParser";
89

910
export class JavaTerminalLinkProvder implements TerminalLinkProvider<IJavaTerminalLink> {
1011
/**
@@ -17,17 +18,14 @@ export class JavaTerminalLinkProvder implements TerminalLinkProvider<IJavaTermin
1718
*/
1819
public provideTerminalLinks(context: TerminalLinkContext, _token: CancellationToken): ProviderResult<IJavaTerminalLink[]> {
1920
const isDebuggerTerminal: boolean = context.terminal.name.startsWith("Run:") || context.terminal.name.startsWith("Debug:");
20-
const regex = new RegExp("(\\sat\\s+)([\\w$\\.]+\\/)?(([\\w$]+\\.)+[<\\w$>]+)\\(([\\w-$]+\\.java:\\d+)\\)");
21-
const result: RegExpExecArray | null = regex.exec(context.line);
22-
if (result && result.length) {
23-
const stackTrace = `${result[2] || ""}${result[3]}(${result[5]})`;
24-
const sourceLineNumber = Number(result[5].split(":")[1]);
21+
const frame = parseJavaStackFrame(context.line);
22+
if (frame) {
2523
return [{
26-
startIndex: result.index + result[1].length,
27-
length: stackTrace.length,
28-
methodName: result[3],
29-
stackTrace,
30-
lineNumber: sourceLineNumber,
24+
startIndex: frame.startIndex,
25+
length: frame.length,
26+
methodName: frame.methodName,
27+
stackTrace: frame.stackTrace,
28+
lineNumber: frame.lineNumber,
3129
isDebuggerTerminal,
3230
}];
3331
}

0 commit comments

Comments
 (0)