diff --git a/package.json b/package.json index dd4626df5a..c3efacf113 100644 --- a/package.json +++ b/package.json @@ -268,7 +268,7 @@ "name": "copilot_findTextInFiles", "toolReferenceName": "textSearch", "displayName": "%copilot.tools.findTextInFiles.name%", - "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 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.", + "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.", "tags": [ "vscode_codesearch" ], @@ -290,6 +290,10 @@ "maxResults": { "type": "number", "description": "The maximum number of results to return. Do not use this unless necessary, it can slow things down. By default, only some matches are returned. If you use this and don't see what you're looking for, you can try again with a more specific query or a larger maxResults." + }, + "includeIgnoredFiles": { + "type": "boolean", + "description": "Whether to include files that would normally be ignored according to .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." } }, "required": [ @@ -4610,4 +4614,4 @@ "string_decoder": "npm:string_decoder@1.2.0", "node-gyp": "npm:node-gyp@10.3.1" } -} \ No newline at end of file +} diff --git a/src/extension/tools/node/findTextInFilesTool.tsx b/src/extension/tools/node/findTextInFilesTool.tsx index 337d3ad664..c4e5449570 100644 --- a/src/extension/tools/node/findTextInFilesTool.tsx +++ b/src/extension/tools/node/findTextInFilesTool.tsx @@ -6,6 +6,7 @@ import * as l10n from '@vscode/l10n'; import { BasePromptElementProps, PromptElement, PromptElementProps, PromptPiece, PromptReference, PromptSizing, TextChunk } from '@vscode/prompt-tsx'; import type * as vscode from 'vscode'; +import { IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { OffsetLineColumnConverter } from '../../../platform/editing/common/offsetLineColumnConverter'; import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider'; import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService'; @@ -18,7 +19,7 @@ import { count } from '../../../util/vs/base/common/strings'; import { URI } from '../../../util/vs/base/common/uri'; import { Position as EditorPosition } from '../../../util/vs/editor/common/core/position'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; -import { ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, Location, MarkdownString, Range } from '../../../vscodeTypes'; +import { ExcludeSettingOptions, ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, Location, MarkdownString, Range } from '../../../vscodeTypes'; import { IBuildPromptContext } from '../../prompt/common/intents'; import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer'; import { Tag } from '../../prompts/node/base/tag'; @@ -31,6 +32,8 @@ interface IFindTextInFilesToolParams { isRegexp?: boolean; includePattern?: string; maxResults?: number; + /** Whether to include files that would normally be ignored according to .gitignore, other ignore files and `files.exclude` and `search.exclude` settings. */ + includeIgnoredFiles?: boolean; } const MaxResultsCap = 200; @@ -43,6 +46,7 @@ export class FindTextInFilesTool implements ICopilotTool, token: CancellationToken) { @@ -57,12 +61,13 @@ export class FindTextInFilesTool implements ICopilotTool this.searchAndCollectResults(options.input.query, isRegExp, patterns, maxResults, searchToken), + (searchToken) => this.searchAndCollectResults(options.input.query, isRegExp, patterns, maxResults, includeIgnoredFiles, searchToken), token, timeoutInMs, // embed message to give LLM hint about what to do next @@ -72,7 +77,7 @@ export class FindTextInFilesTool implements ICopilotTool this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, searchToken), + (searchToken) => this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, includeIgnoredFiles, searchToken), token, timeoutInMs, // embed message to give LLM hint about what to do next @@ -80,9 +85,27 @@ export class FindTextInFilesTool implements ICopilotTool>('search.exclude'); + const excludePaths: string[] = []; + if (excludeSettings) { + for (const [path, isExcluded] of Object.entries(excludeSettings)) { + if (isExcluded) { + excludePaths.push(path); + } + } + } + + noMatchInstructions = `Your search pattern might be excluded completedly by either the search.exclude settings or .*ignore files. + If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[${excludePaths.join(';')}] separated by ';'). + Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.`; + } + const prompt = await renderPromptElementJSON(this.instantiationService, FindTextInFilesResult, - { textResults: results, maxResults, askedForTooManyResults: Boolean(askedForTooManyResults) }, + { textResults: results, maxResults, askedForTooManyResults: Boolean(askedForTooManyResults), noMatchInstructions }, options.tokenizationOptions, token); @@ -126,16 +149,20 @@ export class FindTextInFilesTool implements ICopilotTool { + private async searchAndCollectResults(query: string, isRegExp: boolean, patterns: vscode.GlobPattern[] | undefined, maxResults: number, includeIgnoredFiles: boolean | undefined, token: CancellationToken): Promise { + const findOptions: vscode.FindTextInFilesOptions2 = { + include: patterns ? patterns : undefined, + maxResults: maxResults + 1, + useExcludeSettings: includeIgnoredFiles ? ExcludeSettingOptions.None : ExcludeSettingOptions.SearchAndFilesExclude, + useIgnoreFiles: includeIgnoredFiles ? { local: false, parent: false, global: false } : undefined, + }; + const searchResult = this.searchService.findTextInFiles2( { pattern: query, isRegExp, }, - { - include: patterns ? patterns : undefined, - maxResults: maxResults + 1 - }, + findOptions, token); const results: vscode.TextSearchResult2[] = []; for await (const item of searchResult.results) { @@ -208,6 +235,7 @@ export interface FindTextInFilesResultProps extends BasePromptElementProps { textResults: vscode.TextSearchResult2[]; maxResults: number; askedForTooManyResults?: boolean; + noMatchInstructions?: string; } /** Max number of characters between matching ranges. */ @@ -220,14 +248,15 @@ export class FindTextInFilesResult extends PromptElement { const textMatches = this.props.textResults.filter(isTextSearchMatch); if (textMatches.length === 0) { - return <>No matches found; + const noMatchInstructions = this.props.noMatchInstructions ?? ''; + return <>No matches found.{noMatchInstructions}; } const numResults = textMatches.reduce((acc, result) => acc + result.ranges.length, 0); const resultCountToDisplay = Math.min(numResults, this.props.maxResults); const numResultsText = numResults === 1 ? '1 match' : `${resultCountToDisplay} matches`; - const maxResultsTooLargeText = this.props.askedForTooManyResults ? ` (maxResults capped at ${MaxResultsCap})` : ''; const maxResultsText = numResults > this.props.maxResults ? ` (more results are available)` : ''; + const maxResultsTooLargeText = this.props.askedForTooManyResults ? ` (maxResults capped at ${MaxResultsCap})` : ''; return <> {{numResultsText}{maxResultsText}{maxResultsTooLargeText}} {textMatches.flatMap(result => {