11import { spawn } from 'child_process' ;
2+ import * as path from 'path' ;
23import type * as vscode from 'vscode' ;
3- import type { LocalTool } from './types' ;
4+ import type { LocalTool , ToolExecutionContext } from './types' ;
45import { textResult } from './result' ;
56import { getNumberArg } from './filesystemUtils' ;
67import { createSearchCodeFallbackNotice , searchCodeInProcess } from './searchCodeFallback' ;
@@ -18,7 +19,7 @@ import {
1819 MIN_MATCH_LINE_MAX_CHARS ,
1920} from './searchCodeUtils' ;
2021import type { SearchCodeOptions } from './searchCodeTypes' ;
21- import { WORKSPACE_SEARCH_PATH_DESCRIPTION , resolveWorkspaceRelativeDirectory } from './workspacePath' ;
22+ import { WORKSPACE_SEARCH_PATH_DESCRIPTION , getContextWorkspaceRoots , isDefaultSearchPath , resolveWorkspaceRelativeDirectory , toWorkspaceRootList } from './workspacePath' ;
2223
2324export const searchCodeTool : LocalTool = {
2425 serverId : 'internal' ,
@@ -83,26 +84,77 @@ export const searchCodeTool: LocalTool = {
8384 if ( signal ?. aborted ) {
8485 throw new Error ( `search_code aborted: ${ getAbortReasonText ( signal ) } ` ) ;
8586 }
86- const searchRoot = ( await resolveWorkspaceRelativeDirectory ( context . workspaceRoot , args . path ?? '.' ) ) . absolutePath ;
87+ const roots = toWorkspaceRootList ( getContextWorkspaceRoots ( context ) ) ;
88+ if ( roots . length > 1 && isDefaultSearchPath ( args . path ) ) {
89+ const result = await searchCodeAcrossRoots ( args , roots , context , signal ) ;
90+ return textResult ( formatSearchCodeOutput ( result , buildSearchCodeOptions ( args , roots [ 0 ] , roots [ 0 ] ) ) ) ;
91+ }
92+ const searchRoot = ( await resolveWorkspaceRelativeDirectory ( getContextWorkspaceRoots ( context ) , args . path ?? '.' ) ) . absolutePath ;
8793 const workspaceRoot = context . workspaceRoot ?? searchRoot ;
88- const query = String ( args . query ) ;
89- const maxResults = getNumberArg ( args . max_results , 100 ) ;
90- const options = {
91- searchRoot,
92- workspaceRoot,
93- query,
94- maxResults,
95- includePattern : typeof args . include === 'string' ? args . include : undefined ,
96- caseSensitive : args . case_sensitive === true ,
97- useRegex : getSearchCodeMatchMode ( args . match ) === 'regex' ,
98- matchLineMaxChars : getBoundedSearchLineMaxChars ( args . max_line_chars )
99- } ;
94+ const options = buildSearchCodeOptions ( args , searchRoot , workspaceRoot ) ;
10095 const result = await runRipgrepWithFallback ( options , context . outputChannel , signal ) ;
10196
10297 return textResult ( formatSearchCodeOutput ( result , options ) ) ;
10398 }
10499} ;
105100
101+ function buildSearchCodeOptions (
102+ args : Record < string , unknown > ,
103+ searchRoot : string ,
104+ workspaceRoot : string
105+ ) : SearchCodeOptions {
106+ return {
107+ searchRoot,
108+ workspaceRoot,
109+ query : String ( args . query ) ,
110+ maxResults : getNumberArg ( args . max_results , 100 ) ,
111+ includePattern : typeof args . include === 'string' ? args . include : undefined ,
112+ caseSensitive : args . case_sensitive === true ,
113+ useRegex : getSearchCodeMatchMode ( args . match ) === 'regex' ,
114+ matchLineMaxChars : getBoundedSearchLineMaxChars ( args . max_line_chars )
115+ } ;
116+ }
117+
118+ /**
119+ * Default whole-workspace search in a multi-root workspace: one pass per
120+ * folder, each match line prefixed with the folder name so paths stay
121+ * unambiguous ("micro-server/src/app.ts:12: ..."). max_results is shared
122+ * across folders.
123+ */
124+ async function searchCodeAcrossRoots (
125+ args : Record < string , unknown > ,
126+ roots : string [ ] ,
127+ context : ToolExecutionContext ,
128+ signal ?: AbortSignal
129+ ) : Promise < SearchCodeResult > {
130+ const maxResults = getNumberArg ( args . max_results , 100 ) ;
131+ const matches : string [ ] = [ ] ;
132+ const notices : string [ ] = [ ] ;
133+ let limited = false ;
134+
135+ for ( const root of roots ) {
136+ if ( matches . length >= maxResults ) {
137+ limited = true ;
138+ break ;
139+ }
140+ const options = buildSearchCodeOptions ( args , root , root ) ;
141+ options . maxResults = maxResults - matches . length ;
142+ const result = await runRipgrepWithFallback ( options , context . outputChannel , signal ) ;
143+ const prefix = `${ path . basename ( root ) } /` ;
144+ for ( const match of result . matches ) {
145+ matches . push ( prefix + match ) ;
146+ }
147+ for ( const notice of result . notices ?? [ ] ) {
148+ if ( ! notices . includes ( notice ) ) {
149+ notices . push ( notice ) ;
150+ }
151+ }
152+ limited = limited || result . limited ;
153+ }
154+
155+ return { matches, limited, notices } ;
156+ }
157+
106158async function runRipgrepWithFallback (
107159 options : SearchCodeOptions ,
108160 outputChannel : vscode . OutputChannel ,
0 commit comments