Skip to content

Commit cd3a819

Browse files
authored
Merge branch 'main' into haital-microsoft/prefer-java-upgrade-over-cve
2 parents ba2b719 + 395d5cc commit cd3a819

4 files changed

Lines changed: 106 additions & 34 deletions

File tree

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
{
5353
"name": "lsp_java_getFileStructure",
5454
"toolReferenceName": "javaFileStructure",
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.",
55+
"modelDescription": "Outline a known Java file (classes, methods, fields with line ranges) to pick a precise read_file range instead of reading the whole file. Needs a path from lsp_java_findSymbol or the user do not guess. Returns file plus per-symbol readFileRange ({ offset, limit }) for read_file. Use limit to cap outline items (default 20, max 60). Not for workspace search (use lsp_java_findSymbol).",
5656
"displayName": "Java: Get File Structure",
5757
"userDescription": "Get a Java file outline with classes, methods, fields, and line ranges.",
5858
"tags": [
@@ -69,7 +69,11 @@
6969
"properties": {
7070
"uri": {
7171
"type": "string",
72-
"description": "Workspace-relative path to a Java file. Must be a known path from prior tool results or user input — do not guess."
72+
"description": "Workspace-relative path to a Java file, from lsp_java_findSymbol or user input — do not guess."
73+
},
74+
"limit": {
75+
"type": "number",
76+
"description": "Maximum outline items to return (default: 20, max: 60). Use a smaller value when only top-level context is needed."
7377
}
7478
},
7579
"required": [
@@ -80,7 +84,7 @@
8084
{
8185
"name": "lsp_java_findSymbol",
8286
"toolReferenceName": "javaFindSymbol",
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.",
87+
"modelDescription": "Find Java class/interface/method/field definitions across the workspace by name or partial identifier. Prefer over grep_search, file_search, or semantic_search for Java symbol lookup. Each result has file and readFileInput ({ filePath, offset, limit }) for read_file; use it when source is needed, or lsp_java_getFileStructure with file for broader context. On empty results don't re-search (it retries internally); retry once only if it reports indexing in progress, else use generic search. Not for non-Java files, literals, comments, or build/XML files.",
8488
"displayName": "Java: Find Symbol",
8589
"userDescription": "Find Java class, method, field, or interface definitions by name.",
8690
"tags": [

resources/instruments/javaLspContext.instructions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ For Java symbol navigation, two compiler-accurate `lsp_java_*` tools are availab
1010

1111
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-
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.
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 and source is needed, call `read_file` with the returned `readFileInput`, or call `lsp_java_getFileStructure` with the returned `file` when broader file context is needed.
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.
15+
Use `lsp_java_getFileStructure` only with a path confirmed by the user or a previous tool result. Prefer `file` from `lsp_java_findSymbol`; do not guess paths. Its output includes a top-level `file` and per-symbol `readFileRange`; to read a selected symbol, call `read_file` with `filePath=file` and that `readFileRange`. Use `limit` to keep large outlines small. Use generic search for string literals, comments, XML, Gradle/Maven files, non-Java files, or broad conceptual exploration. `lsp_java_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.

resources/skills/java-lsp-tools/SKILL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ Two compiler-accurate tools backed by the Java Language Server (jdtls). They ret
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: `{ results: [{ name, kind, container?, location, range }], total }` (~60 tokens); `range` is `L start-end`
15+
- Output: `{ results: [{ name, kind, container?, file, startLine, endLine, readFileInput, range }], total }`; `readFileInput` is `{ filePath, offset, limit }` for `read_file`, and `file` can be passed to `lsp_java_getFileStructure`
1616
- **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
17+
- When source is needed for a returned symbol, use its `readFileInput` directly
1818

1919
### `lsp_java_getFileStructure`
2020
Get hierarchical outline of a Java file (classes, methods, fields) with line ranges.
21-
- Input: `{ uri }` — workspace-relative path. Must be a known path from prior tool results or user input — do not guess
22-
- Output: symbol tree with `L start-end` ranges (~100 tokens)
21+
- Input: `{ uri, limit? }` — workspace-relative path plus max outline items. Prefer `file` from `lsp_java_findSymbol`; limit defaults to 20, max 60. Must be a known path from prior tool results or user input — do not guess
22+
- Output: `{ file, symbols: [{ name, kind, startLine, endLine, readFileRange, range, detail?, children? }], truncated? }`; call `read_file` with `filePath=file` and the selected symbol's `readFileRange`
2323
- **Use before** `read_file` when you need to choose a precise line range in a known Java file
2424

2525
## When to Use
@@ -34,9 +34,9 @@ Get hierarchical outline of a Java file (classes, methods, fields) with line ran
3434

3535
## Typical Workflow
3636

37-
**findSymbolgetFileStructure → read_file (specific lines only)**
37+
**lsp_java_findSymbollsp_java_getFileStructure → read_file (specific lines only)**
3838

39-
If `findSymbol` returns relevant symbols, move forward to `getFileStructure` or `read_file`; do not call `findSymbol` again with the same or similar identifier.
39+
If `lsp_java_findSymbol` returns relevant symbols and source is needed, call `read_file` with the returned `readFileInput`, or call `lsp_java_getFileStructure` with the returned `file` when broader file context is needed.
4040

4141
## Fallback
4242

src/copilot/tools/javaContextTools.ts

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import { sendInfo } from "vscode-extension-telemetry-wrapper";
2828

2929
// Hard caps to keep tool responses within the < 200 token budget.
3030
const MAX_SYMBOL_DEPTH = 3;
31-
const MAX_SYMBOL_NODES = 80;
31+
const MAX_FILE_STRUCTURE_SYMBOL_NODES = 60;
32+
const DEFAULT_FILE_STRUCTURE_SYMBOL_NODES = 20;
3233
const MAX_CALL_RESULTS = 50;
3334
const MAX_TYPE_RESULTS = 50;
3435
const MAX_IMPORTS = 50;
@@ -44,6 +45,39 @@ function getResponseCharCount(data: unknown): number {
4445
return typeof data === "string" ? data.length : JSON.stringify(data, null, 2).length;
4546
}
4647

48+
interface ReadFileInput {
49+
filePath: string;
50+
offset: number;
51+
limit: number;
52+
}
53+
54+
interface ReadFileRange {
55+
offset: number;
56+
limit: number;
57+
}
58+
59+
function toInclusiveLineRange(range: vscode.Range): { startLine: number; endLine: number } {
60+
const startLine = range.start.line + 1;
61+
const endLine = Math.max(startLine, range.end.character === 0 && range.end.line > range.start.line
62+
? range.end.line
63+
: range.end.line + 1);
64+
return { startLine, endLine };
65+
}
66+
67+
function toReadFileRange(startLine: number, endLine: number): ReadFileRange {
68+
return {
69+
offset: startLine,
70+
limit: endLine - startLine + 1,
71+
};
72+
}
73+
74+
function toReadFileInput(filePath: string, startLine: number, endLine: number): ReadFileInput {
75+
return {
76+
filePath,
77+
...toReadFileRange(startLine, endLine),
78+
};
79+
}
80+
4781
/**
4882
* Normalize a workspace-symbol query for a single fallback retry.
4983
* Strips a fully-qualified package prefix (com.foo.Bar -> Bar), generic parameters
@@ -86,7 +120,8 @@ function getToolErrorCode(error: unknown): string {
86120
* - Relative path: "src/main/java/Main.java"
87121
* - Absolute path: "/home/user/project/src/Main.java" or "C:\\Users\\...\\Main.java"
88122
*
89-
* Relative paths are resolved against the first workspace folder.
123+
* Relative paths are resolved against the first workspace folder unless they
124+
* start with a workspace folder name in a multi-root workspace.
90125
* The resolved URI must use the file: scheme and fall under a workspace folder.
91126
*/
92127
function resolveFileUri(input: string): vscode.Uri {
@@ -96,19 +131,31 @@ function resolveFileUri(input: string): vscode.Uri {
96131
}
97132

98133
let uri: vscode.Uri;
134+
const normalizedInput = input.trim();
99135

100-
if (input.includes("://")) {
136+
if (normalizedInput.includes("://")) {
101137
// URI string (e.g. "file:///home/user/project/src/Main.java")
102-
uri = vscode.Uri.parse(input);
138+
uri = vscode.Uri.parse(normalizedInput);
103139
if (uri.scheme !== "file") {
104140
throw new Error(`Unsupported URI scheme "${uri.scheme}". Only file: URIs are allowed.`);
105141
}
106-
} else if (path.isAbsolute(input)) {
142+
} else if (path.isAbsolute(normalizedInput)) {
107143
// Absolute filesystem path (Unix or Windows)
108-
uri = vscode.Uri.file(input);
144+
uri = vscode.Uri.file(normalizedInput);
109145
} else {
110-
// Relative path — resolve against first workspace folder
111-
uri = vscode.Uri.joinPath(folders[0].uri, input);
146+
// Relative path — resolve against a matching workspace folder when
147+
// asRelativePath included the folder name, otherwise use the first root.
148+
const normalizedRelativePath = normalizedInput.replace(/\\/g, "/");
149+
const matchingFolder = folders.find(folder =>
150+
normalizedRelativePath === folder.name || normalizedRelativePath.startsWith(`${folder.name}/`));
151+
if (matchingFolder) {
152+
const pathInFolder = normalizedRelativePath === matchingFolder.name
153+
? ""
154+
: normalizedRelativePath.substring(matchingFolder.name.length + 1);
155+
uri = vscode.Uri.joinPath(matchingFolder.uri, pathInFolder);
156+
} else {
157+
uri = vscode.Uri.joinPath(folders[0].uri, normalizedRelativePath);
158+
}
112159
}
113160

114161
// Ensure the resolved path is under a workspace folder
@@ -130,16 +177,19 @@ function resolveFileUri(input: string): vscode.Uri {
130177

131178
interface FileStructureInput {
132179
uri: string;
180+
limit?: number;
133181
}
134182

135183
const fileStructureTool: vscode.LanguageModelTool<FileStructureInput> = {
136184
async invoke(options, _token) {
137185
const startTime = Date.now();
186+
const limit = Math.min(Math.max(Math.floor(options.input.limit ?? DEFAULT_FILE_STRUCTURE_SYMBOL_NODES), 1), MAX_FILE_STRUCTURE_SYMBOL_NODES);
138187
let resultCount = 0;
139188
let status = "success";
140189
let errorCode = "";
141190
let emptyReason = "";
142191
let responseCharCount = 0;
192+
let truncated = false;
143193
try {
144194
const uri = resolveFileUri(options.input.uri);
145195
try {
@@ -171,11 +221,12 @@ const fileStructureTool: vscode.LanguageModelTool<FileStructureInput> = {
171221
responseCharCount = getResponseCharCount(noSymbolsPayload);
172222
return toResult(noSymbolsPayload);
173223
}
174-
const counter = { count: 0 };
175-
const result = symbolsToJson(symbols, 0, counter);
224+
const counter = { count: 0, truncated: false };
225+
const result = symbolsToJson(symbols, 0, counter, limit);
176226
resultCount = counter.count;
177-
const truncated = counter.count >= MAX_SYMBOL_NODES;
178-
const fileStructurePayload = { symbols: result, ...(truncated && { truncated: true }) };
227+
truncated = counter.truncated;
228+
const file = vscode.workspace.asRelativePath(uri);
229+
const fileStructurePayload = { file, symbols: result, ...(truncated && { truncated: true }) };
179230
responseCharCount = getResponseCharCount(fileStructurePayload);
180231
return toResult(fileStructurePayload);
181232
} catch (e) {
@@ -188,6 +239,8 @@ const fileStructureTool: vscode.LanguageModelTool<FileStructureInput> = {
188239
status,
189240
...(errorCode && { errorCode }),
190241
...(emptyReason && { emptyReason }),
242+
truncated: truncated ? "true" : "false",
243+
limit,
191244
resultCount,
192245
responseCharCount,
193246
durationMs: Date.now() - startTime,
@@ -199,28 +252,36 @@ const fileStructureTool: vscode.LanguageModelTool<FileStructureInput> = {
199252
interface SymbolNode {
200253
name: string;
201254
kind: string;
255+
startLine: number;
256+
endLine: number;
257+
readFileRange: ReadFileRange;
202258
range: string;
203259
detail?: string;
204260
children?: SymbolNode[];
205261
}
206262

207-
function symbolsToJson(symbols: vscode.DocumentSymbol[], depth: number, counter: { count: number }): SymbolNode[] {
263+
function symbolsToJson(symbols: vscode.DocumentSymbol[], depth: number, counter: { count: number; truncated: boolean }, limit: number): SymbolNode[] {
208264
const result: SymbolNode[] = [];
209265
for (const s of symbols) {
210-
if (counter.count >= MAX_SYMBOL_NODES) {
266+
if (counter.count >= limit) {
267+
counter.truncated = true;
211268
break;
212269
}
213270
counter.count++;
271+
const { startLine, endLine } = toInclusiveLineRange(s.range);
214272
const node: SymbolNode = {
215273
name: s.name,
216274
kind: vscode.SymbolKind[s.kind],
217-
range: `L${s.range.start.line + 1}-${s.range.end.line + 1}`,
275+
startLine,
276+
endLine,
277+
readFileRange: toReadFileRange(startLine, endLine),
278+
range: `L${startLine}-${endLine}`,
218279
};
219280
if (s.detail) {
220281
node.detail = s.detail;
221282
}
222283
if (s.children?.length && depth < MAX_SYMBOL_DEPTH) {
223-
node.children = symbolsToJson(s.children, depth + 1, counter);
284+
node.children = symbolsToJson(s.children, depth + 1, counter, limit);
224285
}
225286
result.push(node);
226287
}
@@ -288,13 +349,20 @@ const findSymbolTool: vscode.LanguageModelTool<FindSymbolInput> = {
288349
return toResult(noMatchesPayload);
289350
}
290351
totalResults = symbols.length;
291-
const results = symbols.slice(0, limit).map(s => ({
292-
name: s.name,
293-
kind: vscode.SymbolKind[s.kind],
294-
container: s.containerName || undefined,
295-
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}`,
297-
}));
352+
const results = symbols.slice(0, limit).map(s => {
353+
const file = vscode.workspace.asRelativePath(s.location.uri);
354+
const { startLine, endLine } = toInclusiveLineRange(s.location.range);
355+
return {
356+
name: s.name,
357+
kind: vscode.SymbolKind[s.kind],
358+
container: s.containerName || undefined,
359+
file,
360+
startLine,
361+
endLine,
362+
readFileInput: toReadFileInput(file, startLine, endLine),
363+
range: `L${startLine}-${endLine}`,
364+
};
365+
});
298366
resultCount = results.length;
299367
const findSymbolPayload = { results, total: symbols.length };
300368
responseCharCount = getResponseCharCount(findSymbolPayload);

0 commit comments

Comments
 (0)