@@ -391,8 +391,11 @@ namespace FourSlash {
391391 }
392392
393393 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 ;
396399 }
397400
398401 // Entry points from fourslash.ts
@@ -1935,7 +1938,7 @@ Actual: ${stringify(fullActual)}`);
19351938 * @returns The number of characters added to the file as a result of the edits.
19361939 * May be negative.
19371940 */
1938- private applyEdits ( fileName : string , edits : ts . TextChange [ ] , isFormattingEdit : boolean ) : number {
1941+ private applyEdits ( fileName : string , edits : ReadonlyArray < ts . TextChange > , isFormattingEdit : boolean ) : number {
19391942 // We get back a set of edits, but langSvc.editScript only accepts one at a time. Use this to keep track
19401943 // of the incremental offset from each edit to the next. We assume these edit ranges don't overlap
19411944
@@ -3129,30 +3132,34 @@ Actual: ${stringify(fullActual)}`);
31293132 assert ( action . name === "Move to a new file" && action . description === "Move to a new file" ) ;
31303133
31313134 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" ) ;
31333136 }
31343137
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 ] ;
31383141 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.` ) ;
31403143 }
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 } ` ) ;
31453149 }
31463150 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 ) ;
31493154 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 } ` ) ;
31513156 }
31523157 }
31533158
31543159 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+ }
31563163 }
31573164 }
31583165
@@ -3287,7 +3294,7 @@ Actual: ${stringify(fullActual)}`);
32873294 eq ( item . replacementSpan , options && options . replacementSpan && ts . createTextSpanFromRange ( options . replacementSpan ) , "replacementSpan" ) ;
32883295 }
32893296
3290- private findFile ( indexOrName : string | number ) {
3297+ private findFile ( indexOrName : string | number ) : FourSlashFile {
32913298 if ( typeof indexOrName === "number" ) {
32923299 const index = indexOrName ;
32933300 if ( index >= this . testData . files . length ) {
@@ -3298,32 +3305,39 @@ Actual: ${stringify(fullActual)}`);
32983305 }
32993306 }
33003307 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 ( ", " ) } ` ) ;
33193311 }
3320- return result ;
3312+ return file ;
33213313 }
33223314 else {
33233315 return ts . Debug . assertNever ( indexOrName ) ;
33243316 }
33253317 }
33263318
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+
33273341 private getLineColStringAtPosition ( position : number ) {
33283342 const pos = this . languageServiceAdapterHost . positionToLineAndCharacter ( this . activeFile . fileName , position ) ;
33293343 return `line ${ ( pos . line + 1 ) } , col ${ pos . character } ` ;
@@ -3361,16 +3375,35 @@ Actual: ${stringify(fullActual)}`);
33613375 return ! ! a && ! ! b && a . start === b . start && a . length === b . length ;
33623376 }
33633377
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" ) ;
33673392 }
33683393
33693394 private getApplicableRefactors ( positionOrRange : number | ts . TextRange , preferences = ts . defaultPreferences ) : ReadonlyArray < ts . ApplicableRefactorInfo > {
33703395 return this . languageService . getApplicableRefactors ( this . activeFile . fileName , positionOrRange , preferences ) || ts . emptyArray ;
33713396 }
33723397 }
33733398
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+
33743407 export function runFourSlashTest ( basePath : string , testType : FourSlashTestType , fileName : string ) {
33753408 const content = Harness . IO . readFile ( fileName ) ! ;
33763409 runFourSlashTestContent ( basePath , testType , content , fileName ) ;
0 commit comments