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
18 changes: 16 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,15 @@
{
"name": "lsp_java_getFileStructure",
"toolReferenceName": "javaFileStructure",
"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.",
"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.",
"displayName": "Java: Get File Structure",
"userDescription": "Get a Java file outline with classes, methods, fields, and line ranges.",
"tags": [
"java",
"lsp",
"code-navigation",
"file-outline"
],
"canBeReferencedInPrompt": true,
"icon": "$(symbol-class)",
"when": "config.vscode-java-dependency.enableLspTools && javaLSReady",
Expand All @@ -73,8 +80,15 @@
{
"name": "lsp_java_findSymbol",
"toolReferenceName": "javaFindSymbol",
"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)",
"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.",
Comment thread
wenytang-ms marked this conversation as resolved.
"displayName": "Java: Find Symbol",
"userDescription": "Find Java class, method, field, or interface definitions by name.",
"tags": [
"java",
"lsp",
"code-navigation",
"symbol-search"
],
"canBeReferencedInPrompt": true,
"icon": "$(search)",
"when": "config.vscode-java-dependency.enableLspTools && javaLSReady",
Expand Down
9 changes: 5 additions & 4 deletions resources/instruments/javaLspContext.instructions.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
---
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.
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.
applyTo: '**/*.java'
---

For Java files, two compiler-accurate `lsp_java_*` tools are available and return structured results in ~50 tokens vs ~500+ from generic search:
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:

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

These are deferred tools. Load them with `tool_search_tool_regex` using pattern `lsp_java_` before first use.
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`.

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.
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.

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.
20 changes: 12 additions & 8 deletions resources/skills/java-lsp-tools/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,45 @@
---
name: java-lsp-tools
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.
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.
---

# Java LSP Tools

Two compiler-accurate tools backed by the Java Language Server (jdtls). They return structured JSON with fewer tokens than `grep_search` or `read_file`.
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.

## Tools

### `lsp_java_findSymbol`
Search for Java symbol definitions (classes, methods, fields) by name across the workspace. Supports partial matching.
- Input: `{ query, limit? }` — limit defaults to 20, max 50
- Output: `{ name, kind, location }` per result (~60 tokens)
- **Use instead of** `grep_search` when looking for where a class/method is defined
- Output: `{ results: [{ name, kind, container?, location, range }], total }` (~60 tokens); `range` is `L start-end`
- **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
Comment thread
wenytang-ms marked this conversation as resolved.
- Do not repeat with the same or similar query after relevant results are returned

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

## When to Use

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

## Typical Workflow

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

If `findSymbol` returns relevant symbols, move forward to `getFileStructure` or `read_file`; do not call `findSymbol` again with the same or similar identifier.

## Fallback

- `findSymbol` returns empty → retry with shorter keyword, then fall back to `grep_search`
- Path error → use `findSymbol` to discover correct path first
- `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`
- Path error (`fileNotFound`) → use `findSymbol` to discover the correct path first; do not guess paths
- Tool error / jdtls not ready → fall back to `grep_search` + `read_file`, don't retry more than once
79 changes: 72 additions & 7 deletions src/copilot/tools/javaContextTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import * as path from "path";
import * as vscode from "vscode";
import { Commands } from "../../commands";
import { languageServerApiManager } from "../../languageServerApi/languageServerApiManager";
import { sendInfo } from "vscode-extension-telemetry-wrapper";

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

/**
* Normalize a workspace-symbol query for a single fallback retry.
* Strips a fully-qualified package prefix (com.foo.Bar -> Bar), generic parameters
* (List<String> -> List), and method parameter lists (foo() -> foo). jdtls already
* performs camel-hump matching, so the contiguous identifier is preserved.
*/
function normalizeSymbolQuery(query: string): string {
if (!query) {
return "";
}
let q = query.trim();
// Drop generic parameters and method parens: List<String> / foo(args) -> List / foo
q = q.replace(/[<(].*$/, "");
// Drop a fully-qualified package/qualifier prefix: com.foo.Bar / Foo#bar -> Bar / bar
const lastSep = Math.max(q.lastIndexOf("."), q.lastIndexOf("#"));
if (lastSep >= 0 && lastSep < q.length - 1) {
q = q.substring(lastSep + 1);
}
return q.trim();
}

function getToolErrorCode(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("No workspace folder")) {
Expand Down Expand Up @@ -125,7 +147,12 @@ const fileStructureTool: vscode.LanguageModelTool<FileStructureInput> = {
} catch {
status = "error";
errorCode = "fileNotFound";
const fileNotFoundPayload = { error: "File not found." };
// Most fileNotFound errors come from the model guessing a path. Return an
// actionable hint instead of a dead end so it can self-correct via findSymbol.
const fileNotFoundPayload = {
error: "File not found.",
hint: "Call lsp_java_findSymbol to obtain the exact workspace path before retrying. Do not guess file paths.",
};
responseCharCount = getResponseCharCount(fileNotFoundPayload);
return toResult(fileNotFoundPayload);
}
Expand All @@ -134,8 +161,13 @@ const fileStructureTool: vscode.LanguageModelTool<FileStructureInput> = {
);
if (!symbols || symbols.length === 0) {
status = "empty";
emptyReason = "documentSymbolProviderEmpty";
const noSymbolsPayload = { error: "No symbols found. The file may not be recognized by the Java language server." };
// Separate "index not ready yet" from a genuine no-symbol result so the model
// (and telemetry) can tell a transient state apart from an unrecognized file.
const indexing = !languageServerApiManager.isFullyReady();
emptyReason = indexing ? "indexingInProgress" : "documentSymbolProviderEmpty";
const noSymbolsPayload = indexing
? { error: "Java language server is still indexing. Retry shortly." }
: { error: "No symbols found. The file may not be recognized by the Java language server." };
responseCharCount = getResponseCharCount(noSymbolsPayload);
return toResult(noSymbolsPayload);
}
Expand Down Expand Up @@ -214,22 +246,54 @@ const findSymbolTool: vscode.LanguageModelTool<FindSymbolInput> = {
let errorCode = "";
let emptyReason = "";
let responseCharCount = 0;
let retried = false;
try {
const symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
"vscode.executeWorkspaceSymbolProvider", options.input.query,
const rawQuery = (options.input.query ?? "").trim();
// Reject blank/whitespace-only queries early: an empty query triggers an
// expensive workspace-wide symbol scan and can return a huge list.
if (!rawQuery) {
status = "error";
errorCode = "emptyQuery";
const emptyQueryPayload = {
error: "Query is empty. Provide a class, interface, method, or field name to search for.",
};
responseCharCount = getResponseCharCount(emptyQueryPayload);
return toResult(emptyQueryPayload);
}
let symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
"vscode.executeWorkspaceSymbolProvider", rawQuery,
);
// Server-side fallback: if the verbatim query misses, retry once with a
// normalized identifier (strip package qualifier, generics, and parameter
// lists) so the model does not have to chain repeated findSymbol calls itself.
if (!symbols || symbols.length === 0) {
const normalized = normalizeSymbolQuery(rawQuery);
if (normalized && normalized !== rawQuery) {
retried = true;
symbols = await vscode.commands.executeCommand<vscode.SymbolInformation[]>(
"vscode.executeWorkspaceSymbolProvider", normalized,
);
}
}
if (!symbols || symbols.length === 0) {
status = "empty";
emptyReason = "workspaceSymbolNoMatch";
const noMatchesPayload = { results: [], message: "No symbols found." };
// Distinguish a transient "index not ready" state from a real no-match so the
// model can retry later instead of concluding the symbol does not exist.
const indexing = !languageServerApiManager.isFullyReady();
emptyReason = indexing ? "indexingInProgress" : "workspaceSymbolNoMatch";
const noMatchesPayload = indexing
? { results: [], message: "Java language server is still indexing. Retry shortly or use grep_search as a fallback." }
: { results: [], message: "No symbols found." };
responseCharCount = getResponseCharCount(noMatchesPayload);
return toResult(noMatchesPayload);
}
totalResults = symbols.length;
const results = symbols.slice(0, limit).map(s => ({
name: s.name,
kind: vscode.SymbolKind[s.kind],
container: s.containerName || undefined,
location: `${vscode.workspace.asRelativePath(s.location.uri)}:${s.location.range.start.line + 1}`,
range: `L${s.location.range.start.line + 1}-${s.location.range.end.line + 1}`,
}));
resultCount = results.length;
const findSymbolPayload = { results, total: symbols.length };
Expand All @@ -245,6 +309,7 @@ const findSymbolTool: vscode.LanguageModelTool<FindSymbolInput> = {
status,
...(errorCode && { errorCode }),
...(emptyReason && { emptyReason }),
retried: retried ? "true" : "false",
limit,
resultCount,
totalResults,
Expand Down
Loading