@@ -391,8 +391,11 @@ namespace FourSlash {
391
391
}
392
392
393
393
private getFileContent ( fileName : string ) : string {
394
- const script = this . languageServiceAdapterHost . getScriptInfo ( fileName ) ! ;
395
- return script . content ;
394
+ return ts . Debug . assertDefined ( this . tryGetFileContent ( fileName ) ) ;
395
+ }
396
+ private tryGetFileContent ( fileName : string ) : string | undefined {
397
+ const script = this . languageServiceAdapterHost . getScriptInfo ( fileName ) ;
398
+ return script && script . content ;
396
399
}
397
400
398
401
// Entry points from fourslash.ts
@@ -1935,7 +1938,7 @@ Actual: ${stringify(fullActual)}`);
1935
1938
* @returns The number of characters added to the file as a result of the edits.
1936
1939
* May be negative.
1937
1940
*/
1938
- private applyEdits ( fileName : string , edits : ts . TextChange [ ] , isFormattingEdit : boolean ) : number {
1941
+ private applyEdits ( fileName : string , edits : ReadonlyArray < ts . TextChange > , isFormattingEdit : boolean ) : number {
1939
1942
// We get back a set of edits, but langSvc.editScript only accepts one at a time. Use this to keep track
1940
1943
// of the incremental offset from each edit to the next. We assume these edit ranges don't overlap
1941
1944
@@ -3129,30 +3132,34 @@ Actual: ${stringify(fullActual)}`);
3129
3132
assert ( action . name === "Move to a new file" && action . description === "Move to a new file" ) ;
3130
3133
3131
3134
const editInfo = this . languageService . getEditsForRefactor ( this . activeFile . fileName , this . formatCodeSettings , range , refactor . name , action . name , options . preferences || ts . defaultPreferences ) ! ;
3132
- this . testNewFileContents ( editInfo . edits , options . newFileContents ) ;
3135
+ this . testNewFileContents ( editInfo . edits , options . newFileContents , "move to new file" ) ;
3133
3136
}
3134
3137
3135
- private testNewFileContents ( edits : ReadonlyArray < ts . FileTextChanges > , newFileContents : { [ fileName : string ] : string } ) : void {
3136
- for ( const edit of edits ) {
3137
- const newContent = newFileContents [ edit . fileName ] ;
3138
+ private testNewFileContents ( edits : ReadonlyArray < ts . FileTextChanges > , newFileContents : { [ fileName : string ] : string } , description : string ) : void {
3139
+ for ( const { fileName , textChanges } of edits ) {
3140
+ const newContent = newFileContents [ fileName ] ;
3138
3141
if ( newContent === undefined ) {
3139
- this . raiseError ( `There was an edit in ${ edit . fileName } but new content was not specified.` ) ;
3142
+ this . raiseError ( `${ description } - There was an edit in ${ fileName } but new content was not specified.` ) ;
3140
3143
}
3141
- if ( this . testData . files . some ( f => f . fileName === edit . fileName ) ) {
3142
- this . applyEdits ( edit . fileName , edit . textChanges , /*isFormattingEdit*/ false ) ;
3143
- this . openFile ( edit . fileName ) ;
3144
- this . verifyCurrentFileContent ( newContent ) ;
3144
+
3145
+ const fileContent = this . tryGetFileContent ( fileName ) ;
3146
+ if ( fileContent !== undefined ) {
3147
+ const actualNewContent = ts . textChanges . applyChanges ( fileContent , textChanges ) ;
3148
+ assert . equal ( actualNewContent , newContent , `new content for ${ fileName } ` ) ;
3145
3149
}
3146
3150
else {
3147
- assert ( edit . textChanges . length === 1 ) ;
3148
- const change = ts . first ( edit . textChanges ) ;
3151
+ // Creates a new file.
3152
+ assert ( textChanges . length === 1 ) ;
3153
+ const change = ts . first ( textChanges ) ;
3149
3154
assert . deepEqual ( change . span , ts . createTextSpan ( 0 , 0 ) ) ;
3150
- assert . equal ( change . newText , newContent , `Content for ${ edit . fileName } ` ) ;
3155
+ assert . equal ( change . newText , newContent , `${ description } - Content for ${ fileName } ` ) ;
3151
3156
}
3152
3157
}
3153
3158
3154
3159
for ( const fileName in newFileContents ) {
3155
- assert ( edits . some ( e => e . fileName === fileName ) ) ;
3160
+ if ( ! edits . some ( e => e . fileName === fileName ) ) {
3161
+ ts . Debug . fail ( `${ description } - Asserted new contents of ${ fileName } but there were no edits` ) ;
3162
+ }
3156
3163
}
3157
3164
}
3158
3165
@@ -3287,7 +3294,7 @@ Actual: ${stringify(fullActual)}`);
3287
3294
eq ( item . replacementSpan , options && options . replacementSpan && ts . createTextSpanFromRange ( options . replacementSpan ) , "replacementSpan" ) ;
3288
3295
}
3289
3296
3290
- private findFile ( indexOrName : string | number ) {
3297
+ private findFile ( indexOrName : string | number ) : FourSlashFile {
3291
3298
if ( typeof indexOrName === "number" ) {
3292
3299
const index = indexOrName ;
3293
3300
if ( index >= this . testData . files . length ) {
@@ -3298,32 +3305,39 @@ Actual: ${stringify(fullActual)}`);
3298
3305
}
3299
3306
}
3300
3307
else if ( ts . isString ( indexOrName ) ) {
3301
- let name = ts . normalizePath ( indexOrName ) ;
3302
-
3303
- // names are stored in the compiler with this relative path, this allows people to use goTo.file on just the fileName
3304
- name = name . indexOf ( "/" ) === - 1 ? ( this . basePath + "/" + name ) : name ;
3305
-
3306
- const availableNames : string [ ] = [ ] ;
3307
- const result = ts . forEach ( this . testData . files , file => {
3308
- const fn = ts . normalizePath ( file . fileName ) ;
3309
- if ( fn ) {
3310
- if ( fn === name ) {
3311
- return file ;
3312
- }
3313
- availableNames . push ( fn ) ;
3314
- }
3315
- } ) ;
3316
-
3317
- if ( ! result ) {
3318
- throw new Error ( `No test file named "${ name } " exists. Available file names are: ${ availableNames . join ( ", " ) } ` ) ;
3308
+ const { file, availableNames } = this . tryFindFileWorker ( indexOrName ) ;
3309
+ if ( ! file ) {
3310
+ throw new Error ( `No test file named "${ indexOrName } " exists. Available file names are: ${ availableNames . join ( ", " ) } ` ) ;
3319
3311
}
3320
- return result ;
3312
+ return file ;
3321
3313
}
3322
3314
else {
3323
3315
return ts . Debug . assertNever ( indexOrName ) ;
3324
3316
}
3325
3317
}
3326
3318
3319
+ private tryFindFileWorker ( name : string ) : { readonly file : FourSlashFile | undefined ; readonly availableNames : ReadonlyArray < string > ; } {
3320
+ name = ts . normalizePath ( name ) ;
3321
+ // names are stored in the compiler with this relative path, this allows people to use goTo.file on just the fileName
3322
+ name = name . indexOf ( "/" ) === - 1 ? ( this . basePath + "/" + name ) : name ;
3323
+
3324
+ const availableNames : string [ ] = [ ] ;
3325
+ const file = ts . forEach ( this . testData . files , file => {
3326
+ const fn = ts . normalizePath ( file . fileName ) ;
3327
+ if ( fn ) {
3328
+ if ( fn === name ) {
3329
+ return file ;
3330
+ }
3331
+ availableNames . push ( fn ) ;
3332
+ }
3333
+ } ) ;
3334
+ return { file, availableNames } ;
3335
+ }
3336
+
3337
+ private hasFile ( name : string ) : boolean {
3338
+ return this . tryFindFileWorker ( name ) . file !== undefined ;
3339
+ }
3340
+
3327
3341
private getLineColStringAtPosition ( position : number ) {
3328
3342
const pos = this . languageServiceAdapterHost . positionToLineAndCharacter ( this . activeFile . fileName , position ) ;
3329
3343
return `line ${ ( pos . line + 1 ) } , col ${ pos . character } ` ;
@@ -3361,16 +3375,35 @@ Actual: ${stringify(fullActual)}`);
3361
3375
return ! ! a && ! ! b && a . start === b . start && a . length === b . length ;
3362
3376
}
3363
3377
3364
- public getEditsForFileRename ( options : FourSlashInterface . GetEditsForFileRenameOptions ) : void {
3365
- const changes = this . languageService . getEditsForFileRename ( options . oldPath , options . newPath , this . formatCodeSettings , ts . defaultPreferences ) ;
3366
- this . testNewFileContents ( changes , options . newFileContents ) ;
3378
+ public getEditsForFileRename ( { oldPath, newPath, newFileContents } : FourSlashInterface . GetEditsForFileRenameOptions ) : void {
3379
+ const test = ( fileContents : { readonly [ fileName : string ] : string } , description : string ) : void => {
3380
+ const changes = this . languageService . getEditsForFileRename ( oldPath , newPath , this . formatCodeSettings , ts . defaultPreferences ) ;
3381
+ this . testNewFileContents ( changes , fileContents , description ) ;
3382
+ } ;
3383
+
3384
+ ts . Debug . assert ( ! this . hasFile ( newPath ) , "initially, newPath should not exist" ) ;
3385
+
3386
+ test ( newFileContents , "with file not yet moved" ) ;
3387
+
3388
+ this . languageServiceAdapterHost . renameFileOrDirectory ( oldPath , newPath ) ;
3389
+ this . languageService . cleanupSemanticCache ( ) ;
3390
+ const pathUpdater = ts . getPathUpdater ( oldPath , newPath , ts . createGetCanonicalFileName ( /*useCaseSensitiveFileNames*/ false ) ) ;
3391
+ test ( renameKeys ( newFileContents , key => pathUpdater ( key ) || key ) , "with file moved" ) ;
3367
3392
}
3368
3393
3369
3394
private getApplicableRefactors ( positionOrRange : number | ts . TextRange , preferences = ts . defaultPreferences ) : ReadonlyArray < ts . ApplicableRefactorInfo > {
3370
3395
return this . languageService . getApplicableRefactors ( this . activeFile . fileName , positionOrRange , preferences ) || ts . emptyArray ;
3371
3396
}
3372
3397
}
3373
3398
3399
+ function renameKeys < T > ( obj : { readonly [ key : string ] : T } , renameKey : ( key : string ) => string ) : { readonly [ key : string ] : T } {
3400
+ const res : { [ key : string ] : T } = { } ;
3401
+ for ( const key in obj ) {
3402
+ res [ renameKey ( key ) ] = obj [ key ] ;
3403
+ }
3404
+ return res ;
3405
+ }
3406
+
3374
3407
export function runFourSlashTest ( basePath : string , testType : FourSlashTestType , fileName : string ) {
3375
3408
const content = Harness . IO . readFile ( fileName ) ! ;
3376
3409
runFourSlashTestContent ( basePath , testType , content , fileName ) ;
0 commit comments