Skip to content

Commit 7dbc48a

Browse files
wenytang-msCopilot
andauthored
perf: tune Java LSP tool selection guidance (#1020)
* Tune Java LSP tool selection guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * pref: adjust the invoke chain * perf: update to comments --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8b89050 commit 7dbc48a

4 files changed

Lines changed: 105 additions & 21 deletions

File tree

package.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,15 @@
5252
{
5353
"name": "lsp_java_getFileStructure",
5454
"toolReferenceName": "javaFileStructure",
55-
"modelDescription": "Get the outline (classes, methods, fields) of a Java file with symbol kinds and line ranges.\n\nUse before read_file to find specific line ranges. For searching across files, use lsp_java_findSymbol instead.\n\nOnly use file paths confirmed from prior tool results or user input. If unsure, call lsp_java_findSymbol first.",
55+
"modelDescription": "Get a known Java file's outline: classes, interfaces, methods, fields, symbol kinds, and line ranges, to pick a precise read_file range instead of reading the whole file.\n\nUse after lsp_java_findSymbol returns a file, or when the user gave a Java file path; do not guess paths. Not for workspace-wide search\u2014use lsp_java_findSymbol for that. Do not re-call for the same file unless the first result was empty.",
5656
"displayName": "Java: Get File Structure",
57+
"userDescription": "Get a Java file outline with classes, methods, fields, and line ranges.",
58+
"tags": [
59+
"java",
60+
"lsp",
61+
"code-navigation",
62+
"file-outline"
63+
],
5764
"canBeReferencedInPrompt": true,
5865
"icon": "$(symbol-class)",
5966
"when": "config.vscode-java-dependency.enableLspTools && javaLSReady",
@@ -73,8 +80,15 @@
7380
{
7481
"name": "lsp_java_findSymbol",
7582
"toolReferenceName": "javaFindSymbol",
76-
"modelDescription": "Search for Java symbol definitions (classes, interfaces, methods, fields) across the entire workspace by name. Returns precise locations in ~60 tokens vs ~500+ tokens from grep_search.\n\nWhen to use:\n- To find where a class, method, or field is defined — faster and more precise than grep_search\n- To discover file paths before using other tools\n- Supports partial matching (e.g. 'UserSvc' finds 'UserServiceImpl')\n\nWhen NOT to use:\n- For non-Java files\n- To search for string literals, comments, or non-symbol text (use grep_search)",
83+
"modelDescription": "Find Java class, interface, method, or field definitions across the workspace by name or partial identifier. Prefer over grep_search, file_search, semantic_search, or search subagents for Java symbol lookup.\n\nOn relevant results, do not repeat with a similar query; continue with lsp_java_getFileStructure or read_file on the returned line range. The tool retries internally, so on an empty result do not re-search\u2014retry once only if it reports indexing in progress, otherwise use generic search.\n\nDo not use for non-Java files, literals, comments, build/XML files, or conceptual exploration.",
7784
"displayName": "Java: Find Symbol",
85+
"userDescription": "Find Java class, method, field, or interface definitions by name.",
86+
"tags": [
87+
"java",
88+
"lsp",
89+
"code-navigation",
90+
"symbol-search"
91+
],
7892
"canBeReferencedInPrompt": true,
7993
"icon": "$(search)",
8094
"when": "config.vscode-java-dependency.enableLspTools && javaLSReady",
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
---
2-
description: REQUIRED for Java files. Provides compiler-accurate lsp_java_findSymbol and lsp_java_getFileStructure tools that replace grep_search, search_subagent, semantic_search, and file_search for Java symbol navigation.
2+
description: Use Java LSP tools for precise Java symbol navigation. Prefer lsp_java_findSymbol and lsp_java_getFileStructure over generic search only when locating Java classes, methods, fields, or file outlines.
33
applyTo: '**/*.java'
44
---
55

6-
For Java files, two compiler-accurate `lsp_java_*` tools are available and return structured results in ~50 tokens vs ~500+ from generic search:
6+
For Java symbol navigation, two compiler-accurate `lsp_java_*` tools are available and return structured results with smaller, easier-to-interpret payloads than generic search:
77

88
- `lsp_java_findSymbol(query)` — find class/method/field definitions by name across the workspace
99
- `lsp_java_getFileStructure(uri)` — get file outline (classes, methods, fields) with line ranges
1010

11-
These are deferred tools. Load them with `tool_search_tool_regex` using pattern `lsp_java_` before first use.
11+
If these tools are not already available in the current tool list, load them with `tool_search` using a query such as `Java LSP symbol navigation lsp_java`.
1212

13-
Prefer these over `grep_search`, `search_subagent`, `semantic_search`, `file_search`, or full-file `read_file` when navigating Java symbols. Always use `findSymbol` to discover file paths before passing them to `getFileStructure` — do not guess paths. Fall back to `grep_search` if a tool returns empty or errors.
13+
Use `lsp_java_findSymbol` before `grep_search`, `search_subagent`, `semantic_search`, or `file_search` only when the task is to locate Java symbols by name or partial identifier. If it returns relevant symbols, do not call it again with the same or similar query; next use `lsp_java_getFileStructure` for the returned file or `read_file` on the smallest useful line range.
1414

15+
Use `lsp_java_getFileStructure` only with a path confirmed by the user or a previous tool result. Do not guess paths. Use generic search for string literals, comments, XML, Gradle/Maven files, non-Java files, or broad conceptual exploration. `findSymbol` already retries internally with a normalized identifier, so do not re-issue the same search on an empty result: if it reports indexing in progress, retry once after a short pause; otherwise fall back to generic search.
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,45 @@
11
---
22
name: java-lsp-tools
3-
description: Compiler-accurate Java code navigation via the Java Language Server. Use lsp_java_findSymbol to locate symbols and lsp_java_getFileStructure to inspect file outlines. Prefer over grep_search for Java symbol navigation.
3+
description: Compiler-accurate Java symbol navigation via the Java Language Server. Use lsp_java_findSymbol for Java identifiers and lsp_java_getFileStructure for known Java files; prefer them over generic search only for symbol/file-outline navigation.
44
---
55

66
# Java LSP Tools
77

8-
Two compiler-accurate tools backed by the Java Language Server (jdtls). They return structured JSON with fewer tokens than `grep_search` or `read_file`.
8+
Two compiler-accurate tools backed by the Java Language Server (jdtls). They return structured JSON that is easier to interpret than generic search results for Java symbol navigation.
99

1010
## Tools
1111

1212
### `lsp_java_findSymbol`
1313
Search for Java symbol definitions (classes, methods, fields) by name across the workspace. Supports partial matching.
1414
- Input: `{ query, limit? }` — limit defaults to 20, max 50
15-
- Output: `{ name, kind, location }` per result (~60 tokens)
16-
- **Use instead of** `grep_search` when looking for where a class/method is defined
15+
- Output: `{ results: [{ name, kind, container?, location, range }], total }` (~60 tokens); `range` is `L start-end`
16+
- **Use instead of** `grep_search`, `file_search`, `semantic_search`, or `search_subagent` when looking for where a Java class/method/field is defined by identifier
17+
- Do not repeat with the same or similar query after relevant results are returned
1718

1819
### `lsp_java_getFileStructure`
1920
Get hierarchical outline of a Java file (classes, methods, fields) with line ranges.
2021
- Input: `{ uri }` — workspace-relative path. Must be a known path from prior tool results or user input — do not guess
2122
- Output: symbol tree with `L start-end` ranges (~100 tokens)
22-
- **Use instead of** `read_file` full scan when you need to understand a file's layout
23+
- **Use before** `read_file` when you need to choose a precise line range in a known Java file
2324

2425
## When to Use
2526

2627
| Task | Use | Not |
2728
|---|---|---|
2829
| Find class/method/field definition | `lsp_java_findSymbol` | `grep_search` |
29-
| See file outline before reading | `lsp_java_getFileStructure` | `read_file` full file |
30+
| See known Java file outline before reading | `lsp_java_getFileStructure` | `read_file` full file |
3031
| Search non-Java files (xml, gradle) | `grep_search` | lsp tools |
3132
| Search string literals or comments | `grep_search` | lsp tools |
33+
| Explore broad concepts without identifiers | `semantic_search` or `search_subagent` | lsp tools |
3234

3335
## Typical Workflow
3436

3537
**findSymbol → getFileStructure → read_file (specific lines only)**
3638

39+
If `findSymbol` returns relevant symbols, move forward to `getFileStructure` or `read_file`; do not call `findSymbol` again with the same or similar identifier.
40+
3741
## Fallback
3842

39-
- `findSymbol` returns empty → retry with shorter keyword, then fall back to `grep_search`
40-
- Path error → use `findSymbol` to discover correct path first
43+
- `findSymbol` returns empty → it already retried internally with a normalized identifier, so do not re-issue the same search. If the result says indexing is in progress, retry once after a short pause; otherwise fall back to `grep_search`
44+
- Path error (`fileNotFound`) → use `findSymbol` to discover the correct path first; do not guess paths
4145
- Tool error / jdtls not ready → fall back to `grep_search` + `read_file`, don't retry more than once

src/copilot/tools/javaContextTools.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import * as path from "path";
2424
import * as vscode from "vscode";
2525
import { Commands } from "../../commands";
26+
import { languageServerApiManager } from "../../languageServerApi/languageServerApiManager";
2627
import { sendInfo } from "vscode-extension-telemetry-wrapper";
2728

2829
// Hard caps to keep tool responses within the < 200 token budget.
@@ -43,6 +44,27 @@ function getResponseCharCount(data: unknown): number {
4344
return typeof data === "string" ? data.length : JSON.stringify(data, null, 2).length;
4445
}
4546

47+
/**
48+
* Normalize a workspace-symbol query for a single fallback retry.
49+
* Strips a fully-qualified package prefix (com.foo.Bar -> Bar), generic parameters
50+
* (List<String> -> List), and method parameter lists (foo() -> foo). jdtls already
51+
* performs camel-hump matching, so the contiguous identifier is preserved.
52+
*/
53+
function normalizeSymbolQuery(query: string): string {
54+
if (!query) {
55+
return "";
56+
}
57+
let q = query.trim();
58+
// Drop generic parameters and method parens: List<String> / foo(args) -> List / foo
59+
q = q.replace(/[<(].*$/, "");
60+
// Drop a fully-qualified package/qualifier prefix: com.foo.Bar / Foo#bar -> Bar / bar
61+
const lastSep = Math.max(q.lastIndexOf("."), q.lastIndexOf("#"));
62+
if (lastSep >= 0 && lastSep < q.length - 1) {
63+
q = q.substring(lastSep + 1);
64+
}
65+
return q.trim();
66+
}
67+
4668
function getToolErrorCode(error: unknown): string {
4769
const message = error instanceof Error ? error.message : String(error);
4870
if (message.includes("No workspace folder")) {
@@ -125,7 +147,12 @@ const fileStructureTool: vscode.LanguageModelTool<FileStructureInput> = {
125147
} catch {
126148
status = "error";
127149
errorCode = "fileNotFound";
128-
const fileNotFoundPayload = { error: "File not found." };
150+
// Most fileNotFound errors come from the model guessing a path. Return an
151+
// actionable hint instead of a dead end so it can self-correct via findSymbol.
152+
const fileNotFoundPayload = {
153+
error: "File not found.",
154+
hint: "Call lsp_java_findSymbol to obtain the exact workspace path before retrying. Do not guess file paths.",
155+
};
129156
responseCharCount = getResponseCharCount(fileNotFoundPayload);
130157
return toResult(fileNotFoundPayload);
131158
}
@@ -134,8 +161,13 @@ const fileStructureTool: vscode.LanguageModelTool<FileStructureInput> = {
134161
);
135162
if (!symbols || symbols.length === 0) {
136163
status = "empty";
137-
emptyReason = "documentSymbolProviderEmpty";
138-
const noSymbolsPayload = { error: "No symbols found. The file may not be recognized by the Java language server." };
164+
// Separate "index not ready yet" from a genuine no-symbol result so the model
165+
// (and telemetry) can tell a transient state apart from an unrecognized file.
166+
const indexing = !languageServerApiManager.isFullyReady();
167+
emptyReason = indexing ? "indexingInProgress" : "documentSymbolProviderEmpty";
168+
const noSymbolsPayload = indexing
169+
? { error: "Java language server is still indexing. Retry shortly." }
170+
: { error: "No symbols found. The file may not be recognized by the Java language server." };
139171
responseCharCount = getResponseCharCount(noSymbolsPayload);
140172
return toResult(noSymbolsPayload);
141173
}
@@ -214,22 +246,54 @@ const findSymbolTool: vscode.LanguageModelTool<FindSymbolInput> = {
214246
let errorCode = "";
215247
let emptyReason = "";
216248
let responseCharCount = 0;
249+
let retried = false;
217250
try {
218-
const symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
219-
"vscode.executeWorkspaceSymbolProvider", options.input.query,
251+
const rawQuery = (options.input.query ?? "").trim();
252+
// Reject blank/whitespace-only queries early: an empty query triggers an
253+
// expensive workspace-wide symbol scan and can return a huge list.
254+
if (!rawQuery) {
255+
status = "error";
256+
errorCode = "emptyQuery";
257+
const emptyQueryPayload = {
258+
error: "Query is empty. Provide a class, interface, method, or field name to search for.",
259+
};
260+
responseCharCount = getResponseCharCount(emptyQueryPayload);
261+
return toResult(emptyQueryPayload);
262+
}
263+
let symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
264+
"vscode.executeWorkspaceSymbolProvider", rawQuery,
220265
);
266+
// Server-side fallback: if the verbatim query misses, retry once with a
267+
// normalized identifier (strip package qualifier, generics, and parameter
268+
// lists) so the model does not have to chain repeated findSymbol calls itself.
269+
if (!symbols || symbols.length === 0) {
270+
const normalized = normalizeSymbolQuery(rawQuery);
271+
if (normalized && normalized !== rawQuery) {
272+
retried = true;
273+
symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
274+
"vscode.executeWorkspaceSymbolProvider", normalized,
275+
);
276+
}
277+
}
221278
if (!symbols || symbols.length === 0) {
222279
status = "empty";
223-
emptyReason = "workspaceSymbolNoMatch";
224-
const noMatchesPayload = { results: [], message: "No symbols found." };
280+
// Distinguish a transient "index not ready" state from a real no-match so the
281+
// model can retry later instead of concluding the symbol does not exist.
282+
const indexing = !languageServerApiManager.isFullyReady();
283+
emptyReason = indexing ? "indexingInProgress" : "workspaceSymbolNoMatch";
284+
const noMatchesPayload = indexing
285+
? { results: [], message: "Java language server is still indexing. Retry shortly or use grep_search as a fallback." }
286+
: { results: [], message: "No symbols found." };
225287
responseCharCount = getResponseCharCount(noMatchesPayload);
226288
return toResult(noMatchesPayload);
227289
}
228290
totalResults = symbols.length;
229291
const results = symbols.slice(0, limit).map(s => ({
230292
name: s.name,
231293
kind: vscode.SymbolKind[s.kind],
294+
container: s.containerName || undefined,
232295
location: `${vscode.workspace.asRelativePath(s.location.uri)}:${s.location.range.start.line + 1}`,
296+
range: `L${s.location.range.start.line + 1}-${s.location.range.end.line + 1}`,
233297
}));
234298
resultCount = results.length;
235299
const findSymbolPayload = { results, total: symbols.length };
@@ -245,6 +309,7 @@ const findSymbolTool: vscode.LanguageModelTool<FindSymbolInput> = {
245309
status,
246310
...(errorCode && { errorCode }),
247311
...(emptyReason && { emptyReason }),
312+
retried: retried ? "true" : "false",
248313
limit,
249314
resultCount,
250315
totalResults,

0 commit comments

Comments
 (0)