Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit 9904021

Browse files
authored
Handle absolute paths in search globs (#4287)
* Improve search tools for multiroot Fix microsoft/vscode#292754 * Handle absolute paths in search globs * Get rid of '**' fallback * address comments * Improve test
1 parent 6c29c99 commit 9904021

8 files changed

Lines changed: 597 additions & 38 deletions

File tree

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@
256256
"toolReferenceName": "fileSearch",
257257
"displayName": "%copilot.tools.findFiles.name%",
258258
"userDescription": "%copilot.tools.findFiles.userDescription%",
259-
"modelDescription": "Search for files in the workspace by glob pattern. This only returns the paths of matching files. Use this tool when you know the exact filename pattern of the files you're searching for. Glob patterns match from the root of the workspace folder. Examples:\n- **/*.{js,ts} to match all js/ts files in the workspace.\n- src/** to match all files under the top-level src folder.\n- **/foo/**/*.js to match all js files under any foo folder in the workspace.",
259+
"modelDescription": "Search for files in the workspace by glob pattern. This only returns the paths of matching files. Use this tool when you know the exact filename pattern of the files you're searching for. Glob patterns match from the root of the workspace folder. Examples:\n- **/*.{js,ts} to match all js/ts files in the workspace.\n- src/** to match all files under the top-level src folder.\n- **/foo/**/*.js to match all js files under any foo folder in the workspace.\n\nIn a multi-root workspace, you can scope the search to a specific workspace folder by using the absolute path to the folder as the query, e.g. /path/to/folder/**/*.ts.",
260260
"tags": [
261261
"vscode_codesearch"
262262
],
@@ -265,7 +265,7 @@
265265
"properties": {
266266
"query": {
267267
"type": "string",
268-
"description": "Search for files with names or paths matching this glob pattern."
268+
"description": "Search for files with names or paths matching this glob pattern. Can also be an absolute path to a workspace folder to scope the search in a multi-root workspace."
269269
},
270270
"maxResults": {
271271
"type": "number",
@@ -282,7 +282,7 @@
282282
"toolReferenceName": "textSearch",
283283
"displayName": "%copilot.tools.findTextInFiles.name%",
284284
"userDescription": "%copilot.tools.findTextInFiles.userDescription%",
285-
"modelDescription": "Do a fast text search in the workspace. Use this tool when you want to search with an exact string or regex. If you are not sure what words will appear in the workspace, prefer using regex patterns with alternation (|) or character classes to search for multiple potential words at once instead of making separate searches. For example, use 'function|method|procedure' to look for all of those words at once. Use includePattern to search within files matching a specific pattern, or in a specific file, using a relative path. Use 'includeIgnoredFiles' to include files normally ignored by .gitignore, other ignore files, and `files.exclude` and `search.exclude` settings. Warning: using this may cause the search to be slower, only set it when you want to search in ignored folders like node_modules or build outputs. Use this tool when you want to see an overview of a particular file, instead of using read_file many times to look for code within a file.",
285+
"modelDescription": "Do a fast text search in the workspace. Use this tool when you want to search with an exact string or regex. If you are not sure what words will appear in the workspace, prefer using regex patterns with alternation (|) or character classes to search for multiple potential words at once instead of making separate searches. For example, use 'function|method|procedure' to look for all of those words at once. Use includePattern to search within files matching a specific pattern, or in a specific file, using a relative path. Use 'includeIgnoredFiles' to include files normally ignored by .gitignore, other ignore files, and `files.exclude` and `search.exclude` settings. Warning: using this may cause the search to be slower, only set it when you want to search in ignored folders like node_modules or build outputs. Use this tool when you want to see an overview of a particular file, instead of using read_file many times to look for code within a file.\n\nIn a multi-root workspace, you can scope the search to a specific workspace folder by using the absolute path to the folder as the includePattern, e.g. /path/to/folder.",
286286
"tags": [
287287
"vscode_codesearch"
288288
],
@@ -299,7 +299,7 @@
299299
},
300300
"includePattern": {
301301
"type": "string",
302-
"description": "Search files matching this glob pattern. Will be applied to the relative path of files within the workspace. To search recursively inside a folder, use a proper glob pattern like \"src/folder/**\". Do not use | in includePattern."
302+
"description": "Search files matching this glob pattern. Will be applied to the relative path of files within the workspace. To search recursively inside a folder, use a proper glob pattern like \"src/folder/**\". Do not use | in includePattern. Can also be an absolute path to a workspace folder to scope the search in a multi-root workspace."
303303
},
304304
"maxResults": {
305305
"type": "number",
@@ -6111,4 +6111,4 @@
61116111
"zod": "3.25.76"
61126112
},
61136113
"vscodeCommit": "1eb2581989c23047ff009b68e910e48f51d4d3d7"
6114-
}
6114+
}

src/extension/tools/node/findFilesTool.tsx

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@ import { URI } from '../../../util/vs/base/common/uri';
1111
import * as l10n from '@vscode/l10n';
1212
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
1313
import { ISearchService } from '../../../platform/search/common/searchService';
14+
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
1415
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
1516
import { raceTimeoutAndCancellationError } from '../../../util/common/racePromise';
1617
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
18+
import { isAbsolute } from '../../../util/vs/base/common/path';
1719
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
1820
import { ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString } from '../../../vscodeTypes';
1921
import { IBuildPromptContext } from '../../prompt/common/intents';
2022
import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer';
2123
import { ToolName } from '../common/toolNames';
2224
import { CopilotToolMode, ICopilotTool, ToolRegistry } from '../common/toolsRegistry';
23-
import { checkCancellation, inputGlobToPattern } from './toolUtils';
25+
import { checkCancellation, inputGlobToPattern, patternContainsWorkspaceFolderPath } from './toolUtils';
2426

2527
export interface IFindFilesToolParams {
2628
query: string;
@@ -35,6 +37,7 @@ export class FindFilesTool implements ICopilotTool<IFindFilesToolParams> {
3537
@ISearchService private readonly searchService: ISearchService,
3638
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
3739
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
40+
@ITelemetryService private readonly telemetryService: ITelemetryService,
3841
) { }
3942

4043
async invoke(options: vscode.LanguageModelToolInvocationOptions<IFindFilesToolParams>, token: CancellationToken) {
@@ -50,14 +53,16 @@ export class FindFilesTool implements ICopilotTool<IFindFilesToolParams> {
5053
const modelFamily = endpoint?.family;
5154

5255
// The input _should_ be a pattern matching inside a workspace, folder, but sometimes we get absolute paths, so try to resolve them
53-
const pattern = inputGlobToPattern(options.input.query, this.workspaceService, modelFamily);
56+
const globResult = inputGlobToPattern(options.input.query, this.workspaceService, modelFamily);
57+
58+
void this.sendSearchToolTelemetry(options, globResult.folderName);
5459

5560
// try find text with a timeout of 20s
5661
const timeoutInMs = 20_000;
5762

5863

5964
const results = await raceTimeoutAndCancellationError(
60-
(searchToken) => Promise.resolve(this.searchService.findFiles(pattern, undefined, searchToken)),
65+
(searchToken) => Promise.resolve(this.searchService.findFiles(globResult.patterns, undefined, searchToken)),
6166
token,
6267
timeoutInMs,
6368
'Timeout in searching files, try a more specific search pattern'
@@ -70,7 +75,7 @@ export class FindFilesTool implements ICopilotTool<IFindFilesToolParams> {
7075
// Render the prompt element with a timeout
7176
const prompt = await renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token);
7277
const result = new ExtendedLanguageModelToolResult([new LanguageModelPromptTsxPart(prompt)]);
73-
const query = `\`${options.input.query}\``;
78+
const query = this.formatQueryLabel(globResult, options.input.query);
7479
result.toolResultMessage = resultsToShow.length === 0 ?
7580
new MarkdownString(l10n.t`Searched for files matching ${query}, no matches`) :
7681
resultsToShow.length === 1 ?
@@ -80,16 +85,53 @@ export class FindFilesTool implements ICopilotTool<IFindFilesToolParams> {
8085
return result;
8186
}
8287

88+
private async sendSearchToolTelemetry(options: vscode.LanguageModelToolInvocationOptions<IFindFilesToolParams>, folderName: string | undefined): Promise<void> {
89+
const model = options.model && (await this.endpointProvider.getChatEndpoint(options.model)).model;
90+
const isMultiRoot = this.workspaceService.getWorkspaceFolders().length > 1;
91+
const query = options.input.query;
92+
/* __GDPR__
93+
"findFilesToolInvoked" : {
94+
"owner": "roblourens",
95+
"comment": "Telemetry for the findFiles tool in multi-root workspaces",
96+
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },
97+
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that invoked the tool" },
98+
"isMultiRoot": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the workspace has multiple root folders" },
99+
"queryScopedToFolder": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the query was resolved to a specific workspace folder" },
100+
"queryStartsWithFolderPath": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the raw query starts with a workspace folder absolute path" },
101+
"queryContainsFolderPath": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the raw query contains a workspace folder absolute path anywhere" }
102+
}
103+
*/
104+
this.telemetryService.sendMSFTTelemetryEvent('findFilesToolInvoked', {
105+
requestId: options.chatRequestId,
106+
model,
107+
isMultiRoot: String(isMultiRoot),
108+
queryScopedToFolder: String(!!folderName),
109+
queryStartsWithFolderPath: String(isAbsolute(query) && !!this.workspaceService.getWorkspaceFolder(URI.file(query))),
110+
queryContainsFolderPath: String(patternContainsWorkspaceFolderPath(query, this.workspaceService)),
111+
});
112+
}
113+
83114
prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IFindFilesToolParams>, token: vscode.CancellationToken): vscode.ProviderResult<vscode.PreparedToolInvocation> {
84-
const query = `\`${options.input.query}\``;
115+
const globResult = inputGlobToPattern(options.input.query, this.workspaceService, undefined);
116+
const query = this.formatQueryLabel(globResult, options.input.query);
85117
return {
86118
invocationMessage: new MarkdownString(l10n.t`Searching for files matching ${query}`)
87119
};
88120
}
89121

122+
private formatQueryLabel(globResult: { folderName?: string; folderRelativePattern?: string }, rawQuery: string): string {
123+
if (globResult.folderName) {
124+
if (globResult.folderRelativePattern && globResult.folderRelativePattern !== '**') {
125+
return `\`${globResult.folderName}\` \u00B7 \`${globResult.folderRelativePattern}\``;
126+
}
127+
return `\`${globResult.folderName}\``;
128+
}
129+
return `\`${rawQuery}\``;
130+
}
131+
90132
async resolveInput(input: IFindFilesToolParams, _promptContext: IBuildPromptContext, mode: CopilotToolMode): Promise<IFindFilesToolParams> {
91133
let query = input.query;
92-
if (!query.startsWith('**/')) {
134+
if (!query.startsWith('**/') && !query.startsWith('/') && !query.includes(':')) {
93135
query = `**/${query}`;
94136
}
95137

src/extension/tools/node/findTextInFilesTool.tsx

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import { OffsetLineColumnConverter } from '../../../platform/editing/common/offs
1111
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
1212
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
1313
import { ISearchService } from '../../../platform/search/common/searchService';
14+
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
1415
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
1516
import { raceTimeoutAndCancellationError } from '../../../util/common/racePromise';
1617
import { asArray } from '../../../util/vs/base/common/arrays';
1718
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
19+
import { isAbsolute } from '../../../util/vs/base/common/path';
1820
import { count } from '../../../util/vs/base/common/strings';
1921
import { URI } from '../../../util/vs/base/common/uri';
2022
import { Position as EditorPosition } from '../../../util/vs/editor/common/core/position';
@@ -25,7 +27,7 @@ import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer'
2527
import { Tag } from '../../prompts/node/base/tag';
2628
import { ToolName } from '../common/toolNames';
2729
import { CopilotToolMode, ICopilotTool, ToolRegistry } from '../common/toolsRegistry';
28-
import { checkCancellation, inputGlobToPattern } from './toolUtils';
30+
import { checkCancellation, InputGlobResult, inputGlobToPattern, patternContainsWorkspaceFolderPath } from './toolUtils';
2931

3032
interface IFindTextInFilesToolParams {
3133
query: string;
@@ -47,6 +49,7 @@ export class FindTextInFilesTool implements ICopilotTool<IFindTextInFilesToolPar
4749
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
4850
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
4951
@IConfigurationService private readonly configurationService: IConfigurationService,
52+
@ITelemetryService private readonly telemetryService: ITelemetryService,
5053
) { }
5154

5255
async invoke(options: vscode.LanguageModelToolInvocationOptions<IFindTextInFilesToolParams>, token: CancellationToken) {
@@ -60,7 +63,10 @@ export class FindTextInFilesTool implements ICopilotTool<IFindTextInFilesToolPar
6063
const modelFamily = endpoint?.family;
6164

6265
// The input _should_ be a pattern matching inside a workspace, folder, but sometimes we get absolute paths, so try to resolve them
63-
const patterns = options.input.includePattern ? inputGlobToPattern(options.input.includePattern, this.workspaceService, modelFamily) : undefined;
66+
const globResult = options.input.includePattern ? inputGlobToPattern(options.input.includePattern, this.workspaceService, modelFamily) : undefined;
67+
const patterns = globResult?.patterns;
68+
69+
void this.sendSearchToolTelemetry(options, globResult);
6470

6571
checkCancellation(token);
6672
const askedForTooManyResults = options.input.maxResults && options.input.maxResults > MaxResultsCap;
@@ -123,13 +129,39 @@ Then if you want to include those files you can call the tool again by setting "
123129

124130
return [];
125131
}).slice(0, maxResults);
126-
const query = this.formatQueryString(options.input);
132+
const query = this.formatQueryString(options.input, globResult);
127133
result.toolResultMessage = this.getResultMessage(isRegExp, query, textMatches.length);
128134

129135
result.toolResultDetails = textMatches;
130136
return result;
131137
}
132138

139+
private async sendSearchToolTelemetry(options: vscode.LanguageModelToolInvocationOptions<IFindTextInFilesToolParams>, globResult: InputGlobResult | undefined): Promise<void> {
140+
const model = options.model && (await this.endpointProvider.getChatEndpoint(options.model)).model;
141+
const isMultiRoot = this.workspaceService.getWorkspaceFolders().length > 1;
142+
const includePattern = options.input.includePattern;
143+
/* __GDPR__
144+
"findTextInFilesToolInvoked" : {
145+
"owner": "roblourens",
146+
"comment": "Telemetry for the findTextInFiles tool in multi-root workspaces",
147+
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },
148+
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model that invoked the tool" },
149+
"isMultiRoot": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the workspace has multiple root folders" },
150+
"patternScopedToFolder": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the includePattern was resolved to a specific workspace folder" },
151+
"patternStartsWithFolderPath": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the raw includePattern starts with a workspace folder absolute path" },
152+
"patternContainsFolderPath": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the raw includePattern contains a workspace folder absolute path anywhere" }
153+
}
154+
*/
155+
this.telemetryService.sendMSFTTelemetryEvent('findTextInFilesToolInvoked', {
156+
requestId: options.chatRequestId,
157+
model,
158+
isMultiRoot: String(isMultiRoot),
159+
patternScopedToFolder: String(!!globResult?.folderName),
160+
patternStartsWithFolderPath: String(!!includePattern && isAbsolute(includePattern) && !!this.workspaceService.getWorkspaceFolder(URI.file(includePattern))),
161+
patternContainsFolderPath: String(patternContainsWorkspaceFolderPath(includePattern, this.workspaceService)),
162+
});
163+
}
164+
133165
private getResultMessage(isRegExp: boolean, query: string, count: number): MarkdownString {
134166
if (count === 0) {
135167
return isRegExp
@@ -184,7 +216,8 @@ Then if you want to include those files you can call the tool again by setting "
184216

185217
prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IFindTextInFilesToolParams>, token: vscode.CancellationToken): vscode.ProviderResult<vscode.PreparedToolInvocation> {
186218
const isRegExp = options.input.isRegexp ?? true;
187-
const query = this.formatQueryString(options.input);
219+
const globResult = options.input.includePattern ? inputGlobToPattern(options.input.includePattern, this.workspaceService, undefined) : undefined;
220+
const query = this.formatQueryString(options.input, globResult);
188221
return {
189222
invocationMessage: isRegExp ?
190223
new MarkdownString(l10n.t`Searching for regex ${query}`) :
@@ -206,8 +239,14 @@ Then if you want to include those files you can call the tool again by setting "
206239
return `${fence}${inner}${fence}`;
207240
}
208241

209-
private formatQueryString(input: IFindTextInFilesToolParams): string {
242+
private formatQueryString(input: IFindTextInFilesToolParams, globResult?: InputGlobResult): string {
210243
const querySpan = this.formatCodeSpan(input.query);
244+
if (globResult?.folderName) {
245+
if (globResult.folderRelativePattern && globResult.folderRelativePattern !== '**') {
246+
return `${querySpan} (\`${globResult.folderName}\` \u00B7 ${this.formatCodeSpan(globResult.folderRelativePattern)})`;
247+
}
248+
return `${querySpan} (\`${globResult.folderName}\`)`;
249+
}
211250
if (input.includePattern && input.includePattern !== '**/*') {
212251
const patternSpan = this.formatCodeSpan(input.includePattern);
213252
return `${querySpan} (${patternSpan})`;
@@ -221,7 +260,7 @@ Then if you want to include those files you can call the tool again by setting "
221260
includePattern = undefined;
222261
}
223262

224-
if (includePattern && !includePattern.startsWith('**/')) {
263+
if (includePattern && !includePattern.startsWith('**/') && !includePattern.startsWith('/') && !includePattern.includes(':')) {
225264
includePattern = `**/${includePattern}`;
226265
}
227266
if (includePattern && includePattern.endsWith('/')) {

0 commit comments

Comments
 (0)