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