Skip to content

Commit 1084547

Browse files
authored
feat: add timeout in searching text in files (#1233)
* feat: add timeout handling for file and text search operations * feat: add timeout handling for search and rendering operations in FindTextInFilesTool * use raceTimeout and raceCancellation * Updated per comments * extract a raceTimeoutAndCancellationError() * enhance timeout handling and cancellation for file and text search operations * enhance raceTimeoutAndCancellationError * removed unnecessary check * refactor: streamline cancellation token usage in file and text search operations
1 parent 25c9004 commit 1084547

File tree

3 files changed

+88
-9
lines changed

3 files changed

+88
-9
lines changed

src/extension/tools/node/findFilesTool.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { URI } from '../../../util/vs/base/common/uri';
1111
import * as l10n from '@vscode/l10n';
1212
import { ISearchService } from '../../../platform/search/common/searchService';
1313
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
14+
import { raceTimeoutAndCancellationError } from '../../../util/common/racePromise';
1415
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
1516
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
1617
import { ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString } from '../../../vscodeTypes';
@@ -40,14 +41,24 @@ export class FindFilesTool implements ICopilotTool<IFindFilesToolParams> {
4041
// The input _should_ be a pattern matching inside a workspace, folder, but sometimes we get absolute paths, so try to resolve them
4142
const pattern = inputGlobToPattern(options.input.query, this.workspaceService);
4243

43-
const results = await this.searchService.findFiles(pattern, undefined, token);
44+
// try find text with a timeout of 20s
45+
const timeoutInMs = 20_000;
46+
47+
48+
const results = await raceTimeoutAndCancellationError(
49+
(searchToken) => Promise.resolve(this.searchService.findFiles(pattern, undefined, searchToken)),
50+
token,
51+
timeoutInMs,
52+
'Timeout in searching files, try a more specific search pattern'
53+
);
54+
4455
checkCancellation(token);
4556

4657
const maxResults = options.input.maxResults ?? 20;
4758
const resultsToShow = results.slice(0, maxResults);
48-
const result = new ExtendedLanguageModelToolResult([
49-
new LanguageModelPromptTsxPart(
50-
await renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token))]);
59+
// Render the prompt element with a timeout
60+
const prompt = await renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token);
61+
const result = new ExtendedLanguageModelToolResult([new LanguageModelPromptTsxPart(prompt)]);
5162
const query = `\`${options.input.query}\``;
5263
result.toolResultMessage = resultsToShow.length === 0 ?
5364
new MarkdownString(l10n.t`Searched for files matching ${query}, no matches`) :

src/extension/tools/node/findTextInFilesTool.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { OffsetLineColumnConverter } from '../../../platform/editing/common/offs
1010
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
1111
import { ISearchService } from '../../../platform/search/common/searchService';
1212
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
13+
import { raceTimeoutAndCancellationError } from '../../../util/common/racePromise';
1314
import { asArray } from '../../../util/vs/base/common/arrays';
1415
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
1516
import { count } from '../../../util/vs/base/common/strings';
@@ -51,14 +52,36 @@ export class FindTextInFilesTool implements ICopilotTool<IFindTextInFilesToolPar
5152
const maxResults = Math.min(options.input.maxResults ?? 20, MaxResultsCap);
5253
const isRegExp = options.input.isRegexp ?? true;
5354
const queryIsValidRegex = this.isValidRegex(options.input.query);
54-
let results = await this.searchAndCollectResults(options.input.query, isRegExp, patterns, maxResults, token);
55+
56+
// try find text with a timeout of 20s
57+
const timeoutInMs = 20_000;
58+
59+
let results = await raceTimeoutAndCancellationError(
60+
(searchToken) => this.searchAndCollectResults(options.input.query, isRegExp, patterns, maxResults, searchToken),
61+
token,
62+
timeoutInMs,
63+
// embed message to give LLM hint about what to do next
64+
`Timeout in searching text in files with ${isRegExp ? 'regex' : 'literal'} search, try a more specific search pattern or change regex/literal mode`
65+
);
66+
67+
// If we still have no results, we need to try the opposite regex mode
5568
if (!results.length && queryIsValidRegex) {
56-
results = await this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, token);
69+
results = await raceTimeoutAndCancellationError(
70+
(searchToken) => this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, searchToken),
71+
token,
72+
timeoutInMs,
73+
// embed message to give LLM hint about what to do next
74+
`Find ${results.length} results in searching text in files with ${isRegExp ? 'regex' : 'literal'} search, and then another searching hits timeout in with ${!isRegExp ? 'regex' : 'literal'} search, try a more specific search pattern`
75+
);
5776
}
5877

59-
const result = new ExtendedLanguageModelToolResult([
60-
new LanguageModelPromptTsxPart(
61-
await renderPromptElementJSON(this.instantiationService, FindTextInFilesResult, { textResults: results, maxResults, askedForTooManyResults: Boolean(askedForTooManyResults) }, options.tokenizationOptions, token))]);
78+
const prompt = await renderPromptElementJSON(this.instantiationService,
79+
FindTextInFilesResult,
80+
{ textResults: results, maxResults, askedForTooManyResults: Boolean(askedForTooManyResults) },
81+
options.tokenizationOptions,
82+
token);
83+
84+
const result = new ExtendedLanguageModelToolResult([new LanguageModelPromptTsxPart(prompt)]);
6285
const textMatches = results.flatMap(r => {
6386
if ('ranges' in r) {
6487
return asArray(r.ranges).map(rangeInfo => new Location(r.uri, rangeInfo.sourceRange));

src/util/common/racePromise.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { raceCancellation, raceTimeout } from '../vs/base/common/async';
7+
import { CancellationToken, CancellationTokenSource } from '../vs/base/common/cancellation';
8+
import { CancellationError } from '../vs/base/common/errors';
9+
10+
// sentinel value to indicate cancellation
11+
const CANCELLED = Symbol('cancelled');
12+
13+
/**
14+
* Races a promise against a cancellation token and a timeout.
15+
* @param promiseGenerator A function that generates the promise to race against cancellation and timeout.
16+
* @param parentToken The cancellation token to use.
17+
* @param timeoutInMs The timeout in milliseconds.
18+
* @param timeoutMessage The message to use for the timeout error.
19+
* @returns The result of the promise if it completes before the timeout, or throws an error if it times out or is cancelled.
20+
*/
21+
export async function raceTimeoutAndCancellationError<T>(
22+
promiseGenerator: (cancellationToken: CancellationToken) => Promise<T>,
23+
parentToken: CancellationToken,
24+
timeoutInMs: number,
25+
timeoutMessage: string): Promise<T> {
26+
const cancellationSource = new CancellationTokenSource(parentToken);
27+
try {
28+
const result = await raceTimeout(raceCancellation(promiseGenerator(cancellationSource.token), cancellationSource.token, CANCELLED as T), timeoutInMs);
29+
30+
if (result === CANCELLED) { // cancelled sentinel from raceCancellation
31+
throw new CancellationError();
32+
}
33+
34+
if (result === undefined) { // timeout sentinel from raceTimeout
35+
// signal ongoing work to cancel in the promise
36+
cancellationSource.cancel();
37+
throw new Error(timeoutMessage);
38+
}
39+
40+
return result;
41+
}
42+
finally {
43+
cancellationSource.dispose();
44+
}
45+
}

0 commit comments

Comments
 (0)