@@ -407,6 +407,21 @@ namespace ts.server {
407
407
}
408
408
}
409
409
410
+ /*@internal */
411
+ export interface OpenFileArguments {
412
+ fileName : string ;
413
+ content ?: string ;
414
+ scriptKind ?: protocol . ScriptKindName | ScriptKind ;
415
+ hasMixedContent ?: boolean ;
416
+ projectRootPath ?: string ;
417
+ }
418
+
419
+ /*@internal */
420
+ export interface ChangeFileArguments {
421
+ fileName : string ;
422
+ changes : Iterator < TextChange > ;
423
+ }
424
+
410
425
export class ProjectService {
411
426
412
427
/*@internal */
@@ -1128,11 +1143,22 @@ namespace ts.server {
1128
1143
return project ;
1129
1144
}
1130
1145
1146
+ private assignOrphanScriptInfosToInferredProject ( ) {
1147
+ // collect orphaned files and assign them to inferred project just like we treat open of a file
1148
+ this . openFiles . forEach ( ( projectRootPath , path ) => {
1149
+ const info = this . getScriptInfoForPath ( path as Path ) ! ;
1150
+ // collect all orphaned script infos from open files
1151
+ if ( info . isOrphan ( ) ) {
1152
+ this . assignOrphanScriptInfoToInferredProject ( info , projectRootPath ) ;
1153
+ }
1154
+ } ) ;
1155
+ }
1156
+
1131
1157
/**
1132
1158
* Remove this file from the set of open, non-configured files.
1133
1159
* @param info The file that has been closed or newly configured
1134
1160
*/
1135
- private closeOpenFile ( info : ScriptInfo ) : void {
1161
+ private closeOpenFile ( info : ScriptInfo , skipAssignOrphanScriptInfosToInferredProject ?: true ) {
1136
1162
// Closing file should trigger re-reading the file content from disk. This is
1137
1163
// because the user may chose to discard the buffer content before saving
1138
1164
// to the disk, and the server's version of the file can be out of sync.
@@ -1176,15 +1202,8 @@ namespace ts.server {
1176
1202
1177
1203
this . openFiles . delete ( info . path ) ;
1178
1204
1179
- if ( ensureProjectsForOpenFiles ) {
1180
- // collect orphaned files and assign them to inferred project just like we treat open of a file
1181
- this . openFiles . forEach ( ( projectRootPath , path ) => {
1182
- const info = this . getScriptInfoForPath ( path as Path ) ! ;
1183
- // collect all orphaned script infos from open files
1184
- if ( info . isOrphan ( ) ) {
1185
- this . assignOrphanScriptInfoToInferredProject ( info , projectRootPath ) ;
1186
- }
1187
- } ) ;
1205
+ if ( ! skipAssignOrphanScriptInfosToInferredProject && ensureProjectsForOpenFiles ) {
1206
+ this . assignOrphanScriptInfosToInferredProject ( ) ;
1188
1207
}
1189
1208
1190
1209
// Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project)
@@ -1199,6 +1218,8 @@ namespace ts.server {
1199
1218
else {
1200
1219
this . handleDeletedFile ( info ) ;
1201
1220
}
1221
+
1222
+ return ensureProjectsForOpenFiles ;
1202
1223
}
1203
1224
1204
1225
private deleteScriptInfo ( info : ScriptInfo ) {
@@ -2570,28 +2591,30 @@ namespace ts.server {
2570
2591
} ) ;
2571
2592
}
2572
2593
2573
- openClientFileWithNormalizedPath ( fileName : NormalizedPath , fileContent ?: string , scriptKind ?: ScriptKind , hasMixedContent ?: boolean , projectRootPath ?: NormalizedPath ) : OpenConfiguredProjectResult {
2574
- let configFileName : NormalizedPath | undefined ;
2575
- let configFileErrors : ReadonlyArray < Diagnostic > | undefined ;
2576
-
2594
+ private getOrCreateOpenScriptInfo ( fileName : NormalizedPath , fileContent : string | undefined , scriptKind : ScriptKind | undefined , hasMixedContent : boolean | undefined , projectRootPath : NormalizedPath | undefined ) {
2577
2595
const info = this . getOrCreateScriptInfoOpenedByClientForNormalizedPath ( fileName , projectRootPath ? this . getNormalizedAbsolutePath ( projectRootPath ) : this . currentDirectory , fileContent , scriptKind , hasMixedContent ) ! ; // TODO: GH#18217
2578
-
2579
2596
this . openFiles . set ( info . path , projectRootPath ) ;
2597
+ return info ;
2598
+ }
2599
+
2600
+ private assignProjectToOpenedScriptInfo ( info : ScriptInfo ) : OpenConfiguredProjectResult {
2601
+ let configFileName : NormalizedPath | undefined ;
2602
+ let configFileErrors : ReadonlyArray < Diagnostic > | undefined ;
2580
2603
let project : ConfiguredProject | ExternalProject | undefined = this . findExternalProjectContainingOpenScriptInfo ( info ) ;
2581
2604
if ( ! project && ! this . syntaxOnly ) { // Checking syntaxOnly is an optimization
2582
2605
configFileName = this . getConfigFileNameForFile ( info ) ;
2583
2606
if ( configFileName ) {
2584
2607
project = this . findConfiguredProjectByProjectName ( configFileName ) ;
2585
2608
if ( ! project ) {
2586
- project = this . createLoadAndUpdateConfiguredProject ( configFileName , `Creating possible configured project for ${ fileName } to open` ) ;
2609
+ project = this . createLoadAndUpdateConfiguredProject ( configFileName , `Creating possible configured project for ${ info . fileName } to open` ) ;
2587
2610
// Send the event only if the project got created as part of this open request and info is part of the project
2588
2611
if ( info . isOrphan ( ) ) {
2589
2612
// Since the file isnt part of configured project, do not send config file info
2590
2613
configFileName = undefined ;
2591
2614
}
2592
2615
else {
2593
2616
configFileErrors = project . getAllProjectErrors ( ) ;
2594
- this . sendConfigFileDiagEvent ( project , fileName ) ;
2617
+ this . sendConfigFileDiagEvent ( project , info . fileName ) ;
2595
2618
}
2596
2619
}
2597
2620
else {
@@ -2613,10 +2636,14 @@ namespace ts.server {
2613
2636
// At this point if file is part of any any configured or external project, then it would be present in the containing projects
2614
2637
// So if it still doesnt have any containing projects, it needs to be part of inferred project
2615
2638
if ( info . isOrphan ( ) ) {
2616
- this . assignOrphanScriptInfoToInferredProject ( info , projectRootPath ) ;
2639
+ Debug . assert ( this . openFiles . has ( info . path ) ) ;
2640
+ this . assignOrphanScriptInfoToInferredProject ( info , this . openFiles . get ( info . path ) ) ;
2617
2641
}
2618
2642
Debug . assert ( ! info . isOrphan ( ) ) ;
2643
+ return { configFileName, configFileErrors } ;
2644
+ }
2619
2645
2646
+ private cleanupAfterOpeningFile ( ) {
2620
2647
// This was postponed from closeOpenFile to after opening next file,
2621
2648
// so that we can reuse the project if we need to right away
2622
2649
this . removeOrphanConfiguredProjects ( ) ;
@@ -2636,9 +2663,14 @@ namespace ts.server {
2636
2663
this . removeOrphanScriptInfos ( ) ;
2637
2664
2638
2665
this . printProjects ( ) ;
2666
+ }
2639
2667
2668
+ openClientFileWithNormalizedPath ( fileName : NormalizedPath , fileContent ?: string , scriptKind ?: ScriptKind , hasMixedContent ?: boolean , projectRootPath ?: NormalizedPath ) : OpenConfiguredProjectResult {
2669
+ const info = this . getOrCreateOpenScriptInfo ( fileName , fileContent , scriptKind , hasMixedContent , projectRootPath ) ;
2670
+ const result = this . assignProjectToOpenedScriptInfo ( info ) ;
2671
+ this . cleanupAfterOpeningFile ( ) ;
2640
2672
this . telemetryOnOpenFile ( info ) ;
2641
- return { configFileName , configFileErrors } ;
2673
+ return result ;
2642
2674
}
2643
2675
2644
2676
private removeOrphanConfiguredProjects ( ) {
@@ -2745,12 +2777,16 @@ namespace ts.server {
2745
2777
* Close file whose contents is managed by the client
2746
2778
* @param filename is absolute pathname
2747
2779
*/
2748
- closeClientFile ( uncheckedFileName : string ) {
2780
+ closeClientFile ( uncheckedFileName : string ) : void ;
2781
+ /*@internal */
2782
+ closeClientFile ( uncheckedFileName : string , skipAssignOrphanScriptInfosToInferredProject : true ) : boolean ;
2783
+ closeClientFile ( uncheckedFileName : string , skipAssignOrphanScriptInfosToInferredProject ?: true ) {
2749
2784
const info = this . getScriptInfoForNormalizedPath ( toNormalizedPath ( uncheckedFileName ) ) ;
2750
- if ( info ) {
2751
- this . closeOpenFile ( info ) ;
2785
+ const result = info ? this . closeOpenFile ( info , skipAssignOrphanScriptInfosToInferredProject ) : false ;
2786
+ if ( ! skipAssignOrphanScriptInfosToInferredProject ) {
2787
+ this . printProjects ( ) ;
2752
2788
}
2753
- this . printProjects ( ) ;
2789
+ return result ;
2754
2790
}
2755
2791
2756
2792
private collectChanges ( lastKnownProjectVersions : protocol . ProjectVersionInfo [ ] , currentProjects : Project [ ] , result : ProjectFilesWithTSDiagnostics [ ] ) : void {
@@ -2770,36 +2806,68 @@ namespace ts.server {
2770
2806
}
2771
2807
2772
2808
/* @internal */
2773
- applyChangesInOpenFiles ( openFiles : protocol . ExternalFile [ ] | undefined , changedFiles : protocol . ChangedOpenFile [ ] | undefined , closedFiles : string [ ] | undefined ) : void {
2809
+ applyChangesInOpenFiles ( openFiles : Iterator < OpenFileArguments > | undefined , changedFiles ?: Iterator < ChangeFileArguments > , closedFiles ?: string [ ] ) : void {
2810
+ let openScriptInfos : ScriptInfo [ ] | undefined ;
2811
+ let assignOrphanScriptInfosToInferredProject = false ;
2774
2812
if ( openFiles ) {
2775
- for ( const file of openFiles ) {
2813
+ while ( true ) {
2814
+ const { value : file , done } = openFiles . next ( ) ;
2815
+ if ( done ) break ;
2776
2816
const scriptInfo = this . getScriptInfo ( file . fileName ) ;
2777
2817
Debug . assert ( ! scriptInfo || ! scriptInfo . isScriptOpen ( ) , "Script should not exist and not be open already" ) ;
2778
- const normalizedPath = scriptInfo ? scriptInfo . fileName : toNormalizedPath ( file . fileName ) ;
2779
- this . openClientFileWithNormalizedPath ( normalizedPath , file . content , tryConvertScriptKindName ( file . scriptKind ! ) , file . hasMixedContent ) ; // TODO: GH#18217
2818
+ // Create script infos so we have the new content for all the open files before we do any updates to projects
2819
+ const info = this . getOrCreateOpenScriptInfo (
2820
+ scriptInfo ? scriptInfo . fileName : toNormalizedPath ( file . fileName ) ,
2821
+ file . content ,
2822
+ tryConvertScriptKindName ( file . scriptKind ! ) ,
2823
+ file . hasMixedContent ,
2824
+ file . projectRootPath ? toNormalizedPath ( file . projectRootPath ) : undefined
2825
+ ) ;
2826
+ ( openScriptInfos || ( openScriptInfos = [ ] ) ) . push ( info ) ;
2780
2827
}
2781
2828
}
2782
2829
2783
2830
if ( changedFiles ) {
2784
- for ( const file of changedFiles ) {
2831
+ while ( true ) {
2832
+ const { value : file , done } = changedFiles . next ( ) ;
2833
+ if ( done ) break ;
2785
2834
const scriptInfo = this . getScriptInfo ( file . fileName ) ! ;
2786
2835
Debug . assert ( ! ! scriptInfo ) ;
2836
+ // Make edits to script infos and marks containing project as dirty
2787
2837
this . applyChangesToFile ( scriptInfo , file . changes ) ;
2788
2838
}
2789
2839
}
2790
2840
2791
2841
if ( closedFiles ) {
2792
2842
for ( const file of closedFiles ) {
2793
- this . closeClientFile ( file ) ;
2843
+ // Close files, but dont assign projects to orphan open script infos, that part comes later
2844
+ assignOrphanScriptInfosToInferredProject = this . closeClientFile ( file , /*skipAssignOrphanScriptInfosToInferredProject*/ true ) || assignOrphanScriptInfosToInferredProject ;
2794
2845
}
2795
2846
}
2847
+
2848
+ // All the script infos now exist, so ok to go update projects for open files
2849
+ if ( openScriptInfos ) {
2850
+ openScriptInfos . forEach ( info => this . assignProjectToOpenedScriptInfo ( info ) ) ;
2851
+ }
2852
+
2853
+ // While closing files there could be open files that needed assigning new inferred projects, do it now
2854
+ if ( assignOrphanScriptInfosToInferredProject ) {
2855
+ this . assignOrphanScriptInfosToInferredProject ( ) ;
2856
+ }
2857
+
2858
+ // Cleanup projects
2859
+ this . cleanupAfterOpeningFile ( ) ;
2860
+
2861
+ // Telemetry
2862
+ forEach ( openScriptInfos , info => this . telemetryOnOpenFile ( info ) ) ;
2863
+ this . printProjects ( ) ;
2796
2864
}
2797
2865
2798
2866
/* @internal */
2799
- applyChangesToFile ( scriptInfo : ScriptInfo , changes : TextChange [ ] ) {
2800
- // apply changes in reverse order
2801
- for ( let i = changes . length - 1 ; i >= 0 ; i -- ) {
2802
- const change = changes [ i ] ;
2867
+ applyChangesToFile ( scriptInfo : ScriptInfo , changes : Iterator < TextChange > ) {
2868
+ while ( true ) {
2869
+ const { value : change , done } = changes . next ( ) ;
2870
+ if ( done ) break ;
2803
2871
scriptInfo . editContent ( change . span . start , change . span . start + change . span . length , change . newText ) ;
2804
2872
}
2805
2873
}
0 commit comments