@@ -383,10 +383,70 @@ namespace ts.server {
383383 }
384384
385385 interface OriginalFileInfo { fileName : NormalizedPath ; path : Path ; }
386+ interface AncestorConfigFileInfo {
387+ /** config file name */ fileName : string ;
388+ /** path of open file so we can look at correct root */ path : Path ;
389+ configFileInfo : true ;
390+ }
386391 type OpenScriptInfoOrClosedFileInfo = ScriptInfo | OriginalFileInfo ;
392+ type OpenScriptInfoOrClosedOrConfigFileInfo = OpenScriptInfoOrClosedFileInfo | AncestorConfigFileInfo ;
393+
394+ function isOpenScriptInfo ( infoOrFileNameOrConfig : OpenScriptInfoOrClosedOrConfigFileInfo ) : infoOrFileNameOrConfig is ScriptInfo {
395+ return ! ! ( infoOrFileNameOrConfig as ScriptInfo ) . containingProjects ;
396+ }
397+
398+ function isAncestorConfigFileInfo ( infoOrFileNameOrConfig : OpenScriptInfoOrClosedOrConfigFileInfo ) : infoOrFileNameOrConfig is AncestorConfigFileInfo {
399+ return ! ! ( infoOrFileNameOrConfig as AncestorConfigFileInfo ) . configFileInfo ;
400+ }
401+
402+ function forEachResolvedProjectReference < T > (
403+ project : ConfiguredProject ,
404+ cb : ( resolvedProjectReference : ResolvedProjectReference | undefined , resolvedProjectReferencePath : Path ) => T | undefined
405+ ) : T | undefined {
406+ const program = project . getCurrentProgram ( ) ;
407+ return program && program . forEachResolvedProjectReference ( cb ) ;
408+ }
409+
410+ function forEachPotentialProjectReference < T > (
411+ project : ConfiguredProject ,
412+ cb : ( potentialProjectReference : Path ) => T | undefined
413+ ) : T | undefined {
414+ return project . potentialProjectReferences &&
415+ forEachKey ( project . potentialProjectReferences , cb ) ;
416+ }
417+
418+ function forEachAnyProjectReferenceKind < T > (
419+ project : ConfiguredProject ,
420+ cb : ( resolvedProjectReference : ResolvedProjectReference | undefined , resolvedProjectReferencePath : Path ) => T | undefined ,
421+ cbProjectRef : ( projectReference : ProjectReference ) => T | undefined ,
422+ cbPotentialProjectRef : ( potentialProjectReference : Path ) => T | undefined
423+ ) : T | undefined {
424+ return project . getCurrentProgram ( ) ?
425+ forEachResolvedProjectReference ( project , cb ) :
426+ project . isInitialLoadPending ( ) ?
427+ forEachPotentialProjectReference ( project , cbPotentialProjectRef ) :
428+ forEach ( project . getProjectReferences ( ) , cbProjectRef ) ;
429+ }
430+
431+ function callbackRefProject < T > (
432+ project : ConfiguredProject ,
433+ cb : ( refProj : ConfiguredProject ) => T | undefined ,
434+ refPath : Path | undefined
435+ ) {
436+ const refProject = refPath && project . projectService . configuredProjects . get ( refPath ) ;
437+ return refProject && cb ( refProject ) ;
438+ }
387439
388- function isOpenScriptInfo ( infoOrFileName : OpenScriptInfoOrClosedFileInfo ) : infoOrFileName is ScriptInfo {
389- return ! ! ( infoOrFileName as ScriptInfo ) . containingProjects ;
440+ function forEachReferencedProject < T > (
441+ project : ConfiguredProject ,
442+ cb : ( refProj : ConfiguredProject ) => T | undefined
443+ ) : T | undefined {
444+ return forEachAnyProjectReferenceKind (
445+ project ,
446+ resolvedRef => callbackRefProject ( project , cb , resolvedRef && resolvedRef . sourceFile . path ) ,
447+ projectRef => callbackRefProject ( project , cb , project . toPath ( projectRef . path ) ) ,
448+ potentialProjectRef => callbackRefProject ( project , cb , potentialProjectRef )
449+ ) ;
390450 }
391451
392452 interface ScriptInfoInNodeModulesWatcher extends FileWatcher {
@@ -1261,7 +1321,7 @@ namespace ts.server {
12611321 }
12621322 }
12631323
1264- private configFileExists ( configFileName : NormalizedPath , canonicalConfigFilePath : string , info : OpenScriptInfoOrClosedFileInfo ) {
1324+ private configFileExists ( configFileName : NormalizedPath , canonicalConfigFilePath : string , info : OpenScriptInfoOrClosedOrConfigFileInfo ) {
12651325 let configFileExistenceInfo = this . configFileExistenceInfoCache . get ( canonicalConfigFilePath ) ;
12661326 if ( configFileExistenceInfo ) {
12671327 // By default the info would get impacted by presence of config file since its in the detection path
@@ -1492,7 +1552,7 @@ namespace ts.server {
14921552 * The server must start searching from the directory containing
14931553 * the newly opened file.
14941554 */
1495- private forEachConfigFileLocation ( info : OpenScriptInfoOrClosedFileInfo , action : ( configFileName : NormalizedPath , canonicalConfigFilePath : string ) => boolean | void ) {
1555+ private forEachConfigFileLocation ( info : OpenScriptInfoOrClosedOrConfigFileInfo , action : ( configFileName : NormalizedPath , canonicalConfigFilePath : string ) => boolean | void ) {
14961556 if ( this . syntaxOnly ) {
14971557 return undefined ;
14981558 }
@@ -1505,25 +1565,24 @@ namespace ts.server {
15051565
15061566 // If projectRootPath doesn't contain info.path, then do normal search for config file
15071567 const anySearchPathOk = ! projectRootPath || ! isSearchPathInProjectRoot ( ) ;
1568+ // For ancestor of config file always ignore its own directory since its going to result in itself
1569+ let searchInDirectory = ! isAncestorConfigFileInfo ( info ) ;
15081570 do {
1509- const canonicalSearchPath = normalizedPathToPath ( searchPath , this . currentDirectory , this . toCanonicalFileName ) ;
1510- const tsconfigFileName = asNormalizedPath ( combinePaths ( searchPath , "tsconfig.json" ) ) ;
1511- let result = action ( tsconfigFileName , combinePaths ( canonicalSearchPath , "tsconfig.json" ) ) ;
1512- if ( result ) {
1513- return tsconfigFileName ;
1514- }
1571+ if ( searchInDirectory ) {
1572+ const canonicalSearchPath = normalizedPathToPath ( searchPath , this . currentDirectory , this . toCanonicalFileName ) ;
1573+ const tsconfigFileName = asNormalizedPath ( combinePaths ( searchPath , "tsconfig.json" ) ) ;
1574+ let result = action ( tsconfigFileName , combinePaths ( canonicalSearchPath , "tsconfig.json" ) ) ;
1575+ if ( result ) return tsconfigFileName ;
15151576
1516- const jsconfigFileName = asNormalizedPath ( combinePaths ( searchPath , "jsconfig.json" ) ) ;
1517- result = action ( jsconfigFileName , combinePaths ( canonicalSearchPath , "jsconfig.json" ) ) ;
1518- if ( result ) {
1519- return jsconfigFileName ;
1577+ const jsconfigFileName = asNormalizedPath ( combinePaths ( searchPath , "jsconfig.json" ) ) ;
1578+ result = action ( jsconfigFileName , combinePaths ( canonicalSearchPath , "jsconfig.json" ) ) ;
1579+ if ( result ) return jsconfigFileName ;
15201580 }
15211581
15221582 const parentPath = asNormalizedPath ( getDirectoryPath ( searchPath ) ) ;
1523- if ( parentPath === searchPath ) {
1524- break ;
1525- }
1583+ if ( parentPath === searchPath ) break ;
15261584 searchPath = parentPath ;
1585+ searchInDirectory = true ;
15271586 } while ( anySearchPathOk || isSearchPathInProjectRoot ( ) ) ;
15281587
15291588 return undefined ;
@@ -1539,7 +1598,7 @@ namespace ts.server {
15391598 * If script info is passed in, it is asserted to be open script info
15401599 * otherwise just file name
15411600 */
1542- private getConfigFileNameForFile ( info : OpenScriptInfoOrClosedFileInfo ) {
1601+ private getConfigFileNameForFile ( info : OpenScriptInfoOrClosedOrConfigFileInfo ) {
15431602 if ( isOpenScriptInfo ( info ) ) Debug . assert ( info . isScriptOpen ( ) ) ;
15441603 this . logger . info ( `Search path: ${ getDirectoryPath ( info . fileName ) } ` ) ;
15451604 const configFileName = this . forEachConfigFileLocation ( info , ( configFileName , canonicalConfigFilePath ) =>
@@ -2062,7 +2121,7 @@ namespace ts.server {
20622121 if ( project . languageServiceEnabled &&
20632122 ! project . isOrphan ( ) &&
20642123 ! project . getCompilerOptions ( ) . preserveSymlinks &&
2065- ! contains ( info . containingProjects , project ) ) {
2124+ ! info . isAttached ( project ) ) {
20662125 if ( ! projects ) {
20672126 projects = createMultiMap ( ) ;
20682127 projects . add ( toAddInfo . path , project ) ;
@@ -2662,7 +2721,7 @@ namespace ts.server {
26622721 if ( ! project ) {
26632722 project = this . createLoadAndUpdateConfiguredProject ( configFileName , `Creating possible configured project for ${ info . fileName } to open` ) ;
26642723 // Send the event only if the project got created as part of this open request and info is part of the project
2665- if ( info . isOrphan ( ) ) {
2724+ if ( ! project . containsScriptInfo ( info ) ) {
26662725 // Since the file isnt part of configured project, do not send config file info
26672726 configFileName = undefined ;
26682727 }
@@ -2676,6 +2735,8 @@ namespace ts.server {
26762735 updateProjectIfDirty ( project ) ;
26772736 }
26782737 defaultConfigProject = project ;
2738+ // Create ancestor configured project
2739+ this . createAncestorProjects ( info , defaultConfigProject ) ;
26792740 }
26802741 }
26812742
@@ -2698,6 +2759,74 @@ namespace ts.server {
26982759 return { configFileName, configFileErrors, defaultConfigProject } ;
26992760 }
27002761
2762+ private createAncestorProjects ( info : ScriptInfo , project : ConfiguredProject ) {
2763+ // Skip if info is not part of default configured project
2764+ if ( ! info . isAttached ( project ) ) return ;
2765+
2766+ // Create configured project till project root
2767+ while ( true ) {
2768+ // Skip if project is not composite
2769+ if ( ! project . isInitialLoadPending ( ) && ! project . getCompilerOptions ( ) . composite ) return ;
2770+
2771+ // Get config file name
2772+ const configFileName = this . getConfigFileNameForFile ( {
2773+ fileName : project . getConfigFilePath ( ) ,
2774+ path : info . path ,
2775+ configFileInfo : true
2776+ } ) ;
2777+ if ( ! configFileName ) return ;
2778+
2779+ // find or delay load the project
2780+ const ancestor = this . findConfiguredProjectByProjectName ( configFileName ) ||
2781+ this . createConfiguredProjectWithDelayLoad ( configFileName , `Creating project possibly referencing default composite project ${ project . getProjectName ( ) } of open file ${ info . fileName } ` ) ;
2782+ if ( ancestor . isInitialLoadPending ( ) ) {
2783+ // Set a potential project reference
2784+ ancestor . setPotentialProjectReference ( project . canonicalConfigFilePath ) ;
2785+ }
2786+ project = ancestor ;
2787+ }
2788+ }
2789+
2790+ /*@internal */
2791+ loadAncestorProjectTree ( forProjects ?: ReadonlyMap < true > ) {
2792+ forProjects = forProjects || mapDefinedMap (
2793+ this . configuredProjects ,
2794+ project => ! project . isInitialLoadPending ( ) || undefined
2795+ ) ;
2796+
2797+ const seenProjects = createMap < true > ( ) ;
2798+ // Work on array copy as we could add more projects as part of callback
2799+ for ( const project of arrayFrom ( this . configuredProjects . values ( ) ) ) {
2800+ // If this project has potential project reference for any of the project we are loading ancestor tree for
2801+ // we need to load this project tree
2802+ if ( forEachPotentialProjectReference (
2803+ project ,
2804+ potentialRefPath => forProjects ! . has ( potentialRefPath )
2805+ ) ) {
2806+ // Load children
2807+ this . ensureProjectChildren ( project , seenProjects ) ;
2808+ }
2809+ }
2810+ }
2811+
2812+ private ensureProjectChildren ( project : ConfiguredProject , seenProjects : Map < true > ) {
2813+ if ( ! addToSeen ( seenProjects , project . canonicalConfigFilePath ) ) return ;
2814+ // Update the project
2815+ updateProjectIfDirty ( project ) ;
2816+
2817+ // Create tree because project is uptodate we only care of resolved references
2818+ forEachResolvedProjectReference (
2819+ project ,
2820+ ref => {
2821+ if ( ! ref ) return ;
2822+ const configFileName = toNormalizedPath ( ref . sourceFile . fileName ) ;
2823+ const child = this . findConfiguredProjectByProjectName ( configFileName ) ||
2824+ this . createAndLoadConfiguredProject ( configFileName , `Creating project for reference of project: ${ project . projectName } ` ) ;
2825+ this . ensureProjectChildren ( child , seenProjects ) ;
2826+ }
2827+ ) ;
2828+ }
2829+
27012830 private cleanupAfterOpeningFile ( toRetainConfigProjects : ConfiguredProject [ ] | ConfiguredProject | undefined ) {
27022831 // This was postponed from closeOpenFile to after opening next file,
27032832 // so that we can reuse the project if we need to right away
@@ -2729,6 +2858,16 @@ namespace ts.server {
27292858
27302859 private removeOrphanConfiguredProjects ( toRetainConfiguredProjects : ConfiguredProject [ ] | ConfiguredProject | undefined ) {
27312860 const toRemoveConfiguredProjects = cloneMap ( this . configuredProjects ) ;
2861+ const markOriginalProjectsAsUsed = ( project : Project ) => {
2862+ if ( ! project . isOrphan ( ) && project . originalConfiguredProjects ) {
2863+ project . originalConfiguredProjects . forEach (
2864+ ( _value , configuredProjectPath ) => {
2865+ const project = this . getConfiguredProjectByCanonicalConfigFilePath ( configuredProjectPath ) ;
2866+ return project && retainConfiguredProject ( project ) ;
2867+ }
2868+ ) ;
2869+ }
2870+ } ;
27322871 if ( toRetainConfiguredProjects ) {
27332872 if ( isArray ( toRetainConfiguredProjects ) ) {
27342873 toRetainConfiguredProjects . forEach ( retainConfiguredProject ) ;
@@ -2745,32 +2884,30 @@ namespace ts.server {
27452884 // If project has open ref (there are more than zero references from external project/open file), keep it alive as well as any project it references
27462885 if ( project . hasOpenRef ( ) ) {
27472886 retainConfiguredProject ( project ) ;
2748- markOriginalProjectsAsUsed ( project ) ;
27492887 }
2750- else {
2888+ else if ( toRemoveConfiguredProjects . has ( project . canonicalConfigFilePath ) ) {
27512889 // If the configured project for project reference has more than zero references, keep it alive
2752- project . forEachResolvedProjectReference ( ref => {
2753- if ( ref ) {
2754- const refProject = this . configuredProjects . get ( ref . sourceFile . path ) ;
2755- if ( refProject && refProject . hasOpenRef ( ) ) {
2756- retainConfiguredProject ( project ) ;
2757- }
2758- }
2759- } ) ;
2890+ forEachReferencedProject (
2891+ project ,
2892+ ref => isRetained ( ref ) && retainConfiguredProject ( project )
2893+ ) ;
27602894 }
27612895 } ) ;
27622896
27632897 // Remove all the non marked projects
27642898 toRemoveConfiguredProjects . forEach ( project => this . removeProject ( project ) ) ;
27652899
2766- function markOriginalProjectsAsUsed ( project : Project ) {
2767- if ( ! project . isOrphan ( ) && project . originalConfiguredProjects ) {
2768- project . originalConfiguredProjects . forEach ( ( _value , configuredProjectPath ) => toRemoveConfiguredProjects . delete ( configuredProjectPath ) ) ;
2769- }
2900+ function isRetained ( project : ConfiguredProject ) {
2901+ return project . hasOpenRef ( ) || ! toRemoveConfiguredProjects . has ( project . canonicalConfigFilePath ) ;
27702902 }
27712903
27722904 function retainConfiguredProject ( project : ConfiguredProject ) {
2773- toRemoveConfiguredProjects . delete ( project . canonicalConfigFilePath ) ;
2905+ if ( toRemoveConfiguredProjects . delete ( project . canonicalConfigFilePath ) ) {
2906+ // Keep original projects used
2907+ markOriginalProjectsAsUsed ( project ) ;
2908+ // Keep all the references alive
2909+ forEachReferencedProject ( project , retainConfiguredProject ) ;
2910+ }
27742911 }
27752912 }
27762913
0 commit comments