@@ -294,70 +294,35 @@ namespace ts.server {
294294 return deduplicate ( outputs , equateValues ) ;
295295 }
296296
297- type CombineOutputResult < T > = { project : Project ; result : readonly T [ ] ; } [ ] ;
298- function combineOutputResultContains < T > ( outputs : CombineOutputResult < T > , output : T , areEqual : ( a : T , b : T ) => boolean ) {
299- return outputs . some ( ( { result } ) => contains ( result , output , areEqual ) ) ;
300- }
301- function addToCombineOutputResult < T > ( outputs : CombineOutputResult < T > , project : Project , result : readonly T [ ] ) {
302- if ( result . length ) outputs . push ( { project, result } ) ;
303- }
304-
305- function combineProjectOutputFromEveryProject < T > ( projectService : ProjectService , action : ( project : Project ) => readonly T [ ] , areEqual : ( a : T , b : T ) => boolean ) : CombineOutputResult < T > {
306- const outputs : CombineOutputResult < T > = [ ] ;
307- projectService . loadAncestorProjectTree ( ) ;
308- projectService . forEachEnabledProject ( project => {
309- const theseOutputs = action ( project ) ;
310- addToCombineOutputResult ( outputs , project , filter ( theseOutputs , output => ! combineOutputResultContains ( outputs , output , areEqual ) ) ) ;
311- } ) ;
312- return outputs ;
313- }
314-
315- function flattenCombineOutputResult < T > ( outputs : CombineOutputResult < T > ) : readonly T [ ] {
316- return flatMap ( outputs , ( { result } ) => result ) ;
317- }
318-
319- function combineProjectOutputWhileOpeningReferencedProjects < T > (
320- projects : Projects ,
321- defaultProject : Project ,
322- action : ( project : Project ) => readonly T [ ] ,
323- getLocation : ( t : T ) => DocumentPosition ,
324- resultsEqual : ( a : T , b : T ) => boolean ,
325- ) : CombineOutputResult < T > {
326- const outputs : CombineOutputResult < T > = [ ] ;
327- combineProjectOutputWorker (
328- projects ,
329- defaultProject ,
330- /*initialLocation*/ undefined ,
331- ( project , _ , tryAddToTodo ) => {
332- const theseOutputs = action ( project ) ;
333- addToCombineOutputResult ( outputs , project , filter ( theseOutputs , output => ! combineOutputResultContains ( outputs , output , resultsEqual ) && ! tryAddToTodo ( project , getLocation ( output ) ) ) ) ;
334- } ,
335- ) ;
336- return outputs ;
337- }
297+ interface ProjectNavigateToItems {
298+ project : Project ;
299+ navigateToItems : readonly NavigateToItem [ ] ;
300+ } ;
338301
339302 function combineProjectOutputForRenameLocations (
340303 projects : Projects ,
341304 defaultProject : Project ,
342305 initialLocation : DocumentPosition ,
343306 findInStrings : boolean ,
344307 findInComments : boolean ,
345- hostPreferences : UserPreferences
308+ { providePrefixAndSuffixTextForRename } : UserPreferences
346309 ) : readonly RenameLocation [ ] {
347310 const outputs : RenameLocation [ ] = [ ] ;
348311 combineProjectOutputWorker (
349312 projects ,
350313 defaultProject ,
351314 initialLocation ,
352315 ( project , location , tryAddToTodo ) => {
353- for ( const output of project . getLanguageService ( ) . findRenameLocations ( location . fileName , location . pos , findInStrings , findInComments , hostPreferences . providePrefixAndSuffixTextForRename ) || emptyArray ) {
354- if ( ! contains ( outputs , output , documentSpansEqual ) && ! tryAddToTodo ( project , documentSpanLocation ( output ) ) ) {
355- outputs . push ( output ) ;
316+ const projectOutputs = project . getLanguageService ( ) . findRenameLocations ( location . fileName , location . pos , findInStrings , findInComments , providePrefixAndSuffixTextForRename ) ;
317+ if ( projectOutputs ) {
318+ for ( const output of projectOutputs ) {
319+ if ( ! contains ( outputs , output , documentSpansEqual ) && ! tryAddToTodo ( project , documentSpanLocation ( output ) ) ) {
320+ outputs . push ( output ) ;
321+ }
356322 }
357323 }
358324 } ,
359325 ) ;
360-
361326 return outputs ;
362327 }
363328
@@ -381,27 +346,30 @@ namespace ts.server {
381346 initialLocation ,
382347 ( project , location , getMappedLocation ) => {
383348 logger . info ( `Finding references to ${ location . fileName } position ${ location . pos } in project ${ project . getProjectName ( ) } ` ) ;
384- for ( const outputReferencedSymbol of project . getLanguageService ( ) . findReferences ( location . fileName , location . pos ) || emptyArray ) {
385- const mappedDefinitionFile = getMappedLocation ( project , documentSpanLocation ( outputReferencedSymbol . definition ) ) ;
386- const definition : ReferencedSymbolDefinitionInfo = mappedDefinitionFile === undefined ?
387- outputReferencedSymbol . definition :
388- {
389- ...outputReferencedSymbol . definition ,
390- textSpan : createTextSpan ( mappedDefinitionFile . pos , outputReferencedSymbol . definition . textSpan . length ) ,
391- fileName : mappedDefinitionFile . fileName ,
392- contextSpan : getMappedContextSpan ( outputReferencedSymbol . definition , project )
393- } ;
394-
395- let symbolToAddTo = find ( outputs , o => documentSpansEqual ( o . definition , definition ) ) ;
396- if ( ! symbolToAddTo ) {
397- symbolToAddTo = { definition, references : [ ] } ;
398- outputs . push ( symbolToAddTo ) ;
399- }
349+ const projectOutputs = project . getLanguageService ( ) . findReferences ( location . fileName , location . pos ) ;
350+ if ( projectOutputs ) {
351+ for ( const referencedSymbol of projectOutputs ) {
352+ const mappedDefinitionFile = getMappedLocation ( project , documentSpanLocation ( referencedSymbol . definition ) ) ;
353+ const definition : ReferencedSymbolDefinitionInfo = mappedDefinitionFile === undefined ?
354+ referencedSymbol . definition :
355+ {
356+ ...referencedSymbol . definition ,
357+ textSpan : createTextSpan ( mappedDefinitionFile . pos , referencedSymbol . definition . textSpan . length ) ,
358+ fileName : mappedDefinitionFile . fileName ,
359+ contextSpan : getMappedContextSpan ( referencedSymbol . definition , project )
360+ } ;
361+
362+ let symbolToAddTo = find ( outputs , o => documentSpansEqual ( o . definition , definition ) ) ;
363+ if ( ! symbolToAddTo ) {
364+ symbolToAddTo = { definition, references : [ ] } ;
365+ outputs . push ( symbolToAddTo ) ;
366+ }
400367
401- for ( const ref of outputReferencedSymbol . references ) {
402- // If it's in a mapped file, that is added to the todo list by `getMappedLocation`.
403- if ( ! contains ( symbolToAddTo . references , ref , documentSpansEqual ) && ! getMappedLocation ( project , documentSpanLocation ( ref ) ) ) {
404- symbolToAddTo . references . push ( ref ) ;
368+ for ( const ref of referencedSymbol . references ) {
369+ // If it's in a mapped file, that is added to the todo list by `getMappedLocation`.
370+ if ( ! contains ( symbolToAddTo . references , ref , documentSpansEqual ) && ! getMappedLocation ( project , documentSpanLocation ( ref ) ) ) {
371+ symbolToAddTo . references . push ( ref ) ;
372+ }
405373 }
406374 }
407375 }
@@ -411,29 +379,6 @@ namespace ts.server {
411379 return outputs . filter ( o => o . references . length !== 0 ) ;
412380 }
413381
414- function combineProjectOutputForFileReferences (
415- projects : Projects ,
416- defaultProject : Project ,
417- fileName : string
418- ) : readonly ReferenceEntry [ ] {
419- const outputs : ReferenceEntry [ ] = [ ] ;
420-
421- combineProjectOutputWorker (
422- projects ,
423- defaultProject ,
424- /*initialLocation*/ undefined ,
425- project => {
426- for ( const referenceEntry of project . getLanguageService ( ) . getFileReferences ( fileName ) || emptyArray ) {
427- if ( ! contains ( outputs , referenceEntry , documentSpansEqual ) ) {
428- outputs . push ( referenceEntry ) ;
429- }
430- }
431- } ,
432- ) ;
433-
434- return outputs ;
435- }
436-
437382 interface ProjectAndLocation < TLocation extends DocumentPosition | undefined > {
438383 readonly project : Project ;
439384 readonly location : TLocation ;
@@ -1610,11 +1555,22 @@ namespace ts.server {
16101555
16111556 private getFileReferences ( args : protocol . FileRequestArgs , simplifiedResult : boolean ) : protocol . FileReferencesResponseBody | readonly ReferenceEntry [ ] {
16121557 const projects = this . getProjects ( args ) ;
1613- const references = combineProjectOutputForFileReferences (
1614- projects ,
1615- this . getDefaultProject ( args ) ,
1616- args . file ,
1617- ) ;
1558+ const fileName = args . file ;
1559+
1560+ const references : ReferenceEntry [ ] = [ ] ;
1561+
1562+ forEachProjectInProjects ( projects , /*path*/ undefined , project => {
1563+ if ( project . getCancellationToken ( ) . isCancellationRequested ( ) ) return ;
1564+
1565+ const projectOutputs = project . getLanguageService ( ) . getFileReferences ( fileName ) ;
1566+ if ( projectOutputs ) {
1567+ for ( const referenceEntry of projectOutputs ) {
1568+ if ( ! contains ( references , referenceEntry , documentSpansEqual ) ) {
1569+ references . push ( referenceEntry ) ;
1570+ }
1571+ }
1572+ }
1573+ } ) ;
16181574
16191575 if ( ! simplifiedResult ) return references ;
16201576 const refs = references . map ( entry => referenceEntryToReferencesResponseItem ( this . projectService , entry ) ) ;
@@ -2092,10 +2048,10 @@ namespace ts.server {
20922048 private getNavigateToItems ( args : protocol . NavtoRequestArgs , simplifiedResult : boolean ) : readonly protocol . NavtoItem [ ] | readonly NavigateToItem [ ] {
20932049 const full = this . getFullNavigateToItems ( args ) ;
20942050 return ! simplifiedResult ?
2095- flattenCombineOutputResult ( full ) :
2051+ flatMap ( full , ( { navigateToItems } ) => navigateToItems ) :
20962052 flatMap (
20972053 full ,
2098- ( { project, result } ) => result . map ( navItem => {
2054+ ( { project, navigateToItems } ) => navigateToItems . map ( navItem => {
20992055 const scriptInfo = project . getScriptInfo ( navItem . fileName ) ! ;
21002056 const bakedItem : protocol . NavtoItem = {
21012057 name : navItem . name ,
@@ -2121,26 +2077,72 @@ namespace ts.server {
21212077 ) ;
21222078 }
21232079
2124- private getFullNavigateToItems ( args : protocol . NavtoRequestArgs ) : CombineOutputResult < NavigateToItem > {
2080+ private getFullNavigateToItems ( args : protocol . NavtoRequestArgs ) : ProjectNavigateToItems [ ] {
21252081 const { currentFileOnly, searchValue, maxResultCount, projectFileName } = args ;
2082+
21262083 if ( currentFileOnly ) {
21272084 Debug . assertIsDefined ( args . file ) ;
21282085 const { file, project } = this . getFileAndProject ( args as protocol . FileRequestArgs ) ;
2129- return [ { project, result : project . getLanguageService ( ) . getNavigateToItems ( searchValue , maxResultCount , file ) } ] ;
2130- }
2131- else if ( ! args . file && ! projectFileName ) {
2132- return combineProjectOutputFromEveryProject (
2133- this . projectService ,
2134- project => project . getLanguageService ( ) . getNavigateToItems ( searchValue , maxResultCount , /*filename*/ undefined , /*excludeDts*/ project . isNonTsProject ( ) ) ,
2135- navigateToItemIsEqualTo ) ;
2136- }
2137- const fileArgs = args as protocol . FileRequestArgs ;
2138- return combineProjectOutputWhileOpeningReferencedProjects < NavigateToItem > (
2139- this . getProjects ( fileArgs ) ,
2140- this . getDefaultProject ( fileArgs ) ,
2141- project => project . getLanguageService ( ) . getNavigateToItems ( searchValue , maxResultCount , /*fileName*/ undefined , /*excludeDts*/ project . isNonTsProject ( ) ) ,
2142- documentSpanLocation ,
2143- navigateToItemIsEqualTo ) ;
2086+ return [ { project, navigateToItems : project . getLanguageService ( ) . getNavigateToItems ( searchValue , maxResultCount , file ) } ] ;
2087+ }
2088+
2089+ const outputs : ProjectNavigateToItems [ ] = [ ] ;
2090+
2091+ // This is effectively a hashset with `name` as the custom hash and `navigateToItemIsEqualTo` as the custom equals.
2092+ // `name` is a very cheap hash function, but we could incorporate other properties to reduce collisions.
2093+ const seenItems = new Map < string , NavigateToItem [ ] > ( ) ; // name to items with that name
2094+
2095+ if ( ! args . file && ! projectFileName ) {
2096+ // VS Code's `Go to symbol in workspaces` sends request like this
2097+
2098+ // TODO (https://github.com/microsoft/TypeScript/issues/47839)
2099+ // This appears to have been intended to search all projects but, in practice, it seems to only search
2100+ // those that are downstream from already-loaded projects.
2101+ // Filtering by !isSourceOfProjectReferenceRedirect is new, but seems appropriate and consistent with
2102+ // the case below.
2103+ this . projectService . loadAncestorProjectTree ( ) ;
2104+ this . projectService . forEachEnabledProject ( project => addItemsForProject ( project ) ) ;
2105+ }
2106+ else {
2107+ // VS's `Go to symbol` sends requests with just a project and doesn't want cascading since it will
2108+ // send a separate request for each project of interest
2109+
2110+ // TODO (https://github.com/microsoft/TypeScript/issues/47839)
2111+ // This doesn't really make sense unless it's a single project matching `projectFileName`
2112+ const projects = this . getProjects ( args as protocol . FileRequestArgs ) ;
2113+ forEachProjectInProjects ( projects , /*path*/ undefined , project => addItemsForProject ( project ) ) ;
2114+ }
2115+
2116+ return outputs ;
2117+
2118+ // Mutates `outputs`
2119+ function addItemsForProject ( project : Project ) {
2120+ const projectItems = project . getLanguageService ( ) . getNavigateToItems ( searchValue , maxResultCount , /*filename*/ undefined , /*excludeDts*/ project . isNonTsProject ( ) ) ;
2121+ const unseenItems = filter ( projectItems , item => tryAddSeenItem ( item ) && ! getMappedLocation ( documentSpanLocation ( item ) , project ) ) ;
2122+ if ( unseenItems . length ) {
2123+ outputs . push ( { project, navigateToItems : unseenItems } ) ;
2124+ }
2125+ }
2126+
2127+ // Returns true if the item had not been seen before
2128+ // Mutates `seenItems`
2129+ function tryAddSeenItem ( item : NavigateToItem ) {
2130+ const name = item . name ;
2131+ if ( ! seenItems . has ( name ) ) {
2132+ seenItems . set ( name , [ item ] ) ;
2133+ return true ;
2134+ }
2135+
2136+ const seen = seenItems . get ( name ) ! ;
2137+ for ( const seenItem of seen ) {
2138+ if ( navigateToItemIsEqualTo ( seenItem , item ) ) {
2139+ return false ;
2140+ }
2141+ }
2142+
2143+ seen . push ( item ) ;
2144+ return true ;
2145+ }
21442146
21452147 function navigateToItemIsEqualTo ( a : NavigateToItem , b : NavigateToItem ) : boolean {
21462148 if ( a === b ) {
@@ -2255,14 +2257,29 @@ namespace ts.server {
22552257 const newPath = toNormalizedPath ( args . newFilePath ) ;
22562258 const formatOptions = this . getHostFormatOptions ( ) ;
22572259 const preferences = this . getHostPreferences ( ) ;
2258- const changes = flattenCombineOutputResult (
2259- combineProjectOutputFromEveryProject (
2260- this . projectService ,
2261- project => project . getLanguageService ( ) . getEditsForFileRename ( oldPath , newPath , formatOptions , preferences ) ,
2262- ( a , b ) => a . fileName === b . fileName
2263- )
2264- ) ;
2265- return simplifiedResult ? changes . map ( c => this . mapTextChangeToCodeEdit ( c ) ) : changes ;
2260+
2261+
2262+ const seenFiles = new Set < string > ( ) ;
2263+ const textChanges : FileTextChanges [ ] = [ ] ;
2264+ // TODO (https://github.com/microsoft/TypeScript/issues/47839)
2265+ // This appears to have been intended to search all projects but, in practice, it seems to only search
2266+ // those that are downstream from already-loaded projects.
2267+ this . projectService . loadAncestorProjectTree ( ) ;
2268+ this . projectService . forEachEnabledProject ( project => {
2269+ const projectTextChanges = project . getLanguageService ( ) . getEditsForFileRename ( oldPath , newPath , formatOptions , preferences ) ;
2270+ const projectFiles : string [ ] = [ ] ;
2271+ for ( const textChange of projectTextChanges ) {
2272+ if ( ! seenFiles . has ( textChange . fileName ) ) {
2273+ textChanges . push ( textChange ) ;
2274+ projectFiles . push ( textChange . fileName ) ;
2275+ }
2276+ }
2277+ for ( const file of projectFiles ) {
2278+ seenFiles . add ( file ) ;
2279+ }
2280+ } ) ;
2281+
2282+ return simplifiedResult ? textChanges . map ( c => this . mapTextChangeToCodeEdit ( c ) ) : textChanges ;
22662283 }
22672284
22682285 private getCodeFixes ( args : protocol . CodeFixRequestArgs , simplifiedResult : boolean ) : readonly protocol . CodeFixAction [ ] | readonly CodeFixAction [ ] | undefined {
0 commit comments