@@ -50,10 +50,8 @@ namespace FourSlash {
50
50
data ?: { } ;
51
51
}
52
52
53
- export interface Range {
53
+ export interface Range extends ts . TextRange {
54
54
fileName : string ;
55
- pos : number ;
56
- end : number ;
57
55
marker ?: Marker ;
58
56
}
59
57
@@ -1103,7 +1101,7 @@ namespace FourSlash {
1103
1101
return node ;
1104
1102
}
1105
1103
1106
- private verifyRange ( desc : string , expected : Range , actual : ts . Node ) {
1104
+ private verifyRange ( desc : string , expected : ts . TextRange , actual : ts . Node ) {
1107
1105
const actualStart = actual . getStart ( ) ;
1108
1106
const actualEnd = actual . getEnd ( ) ;
1109
1107
if ( actualStart !== expected . pos || actualEnd !== expected . end ) {
@@ -1713,11 +1711,8 @@ Actual: ${stringify(fullActual)}`);
1713
1711
}
1714
1712
1715
1713
public baselineQuickInfo ( ) {
1716
- let baselineFile = this . testData . globalOptions [ MetadataOptionNames . baselineFile ] ;
1717
- if ( ! baselineFile ) {
1718
- baselineFile = ts . getBaseFileName ( this . activeFile . fileName ) . replace ( ts . Extension . Ts , ".baseline" ) ;
1719
- }
1720
-
1714
+ const baselineFile = this . testData . globalOptions [ MetadataOptionNames . baselineFile ] ||
1715
+ ts . getBaseFileName ( this . activeFile . fileName ) . replace ( ts . Extension . Ts , ".baseline" ) ;
1721
1716
Harness . Baseline . runBaseline (
1722
1717
baselineFile ,
1723
1718
stringify (
@@ -1958,18 +1953,11 @@ Actual: ${stringify(fullActual)}`);
1958
1953
* May be negative.
1959
1954
*/
1960
1955
private applyEdits ( fileName : string , edits : ReadonlyArray < ts . TextChange > , isFormattingEdit : boolean ) : number {
1961
- // We get back a set of edits, but langSvc.editScript only accepts one at a time. Use this to keep track
1962
- // of the incremental offset from each edit to the next. We assume these edit ranges don't overlap
1963
-
1964
- // Copy this so we don't ruin someone else's copy
1965
- edits = JSON . parse ( JSON . stringify ( edits ) ) ;
1966
-
1967
1956
// Get a snapshot of the content of the file so we can make sure any formatting edits didn't destroy non-whitespace characters
1968
1957
const oldContent = this . getFileContent ( fileName ) ;
1969
1958
let runningOffset = 0 ;
1970
1959
1971
- for ( let i = 0 ; i < edits . length ; i ++ ) {
1972
- const edit = edits [ i ] ;
1960
+ forEachTextChange ( edits , edit => {
1973
1961
const offsetStart = edit . span . start ;
1974
1962
const offsetEnd = offsetStart + edit . span . length ;
1975
1963
this . editScriptAndUpdateMarkers ( fileName , offsetStart , offsetEnd , edit . newText ) ;
@@ -1985,14 +1973,7 @@ Actual: ${stringify(fullActual)}`);
1985
1973
}
1986
1974
}
1987
1975
runningOffset += editDelta ;
1988
-
1989
- // Update positions of any future edits affected by this change
1990
- for ( let j = i + 1 ; j < edits . length ; j ++ ) {
1991
- if ( edits [ j ] . span . start >= edits [ i ] . span . start ) {
1992
- edits [ j ] . span . start += editDelta ;
1993
- }
1994
- }
1995
- }
1976
+ } ) ;
1996
1977
1997
1978
if ( isFormattingEdit ) {
1998
1979
const newContent = this . getFileContent ( fileName ) ;
@@ -2034,30 +2015,14 @@ Actual: ${stringify(fullActual)}`);
2034
2015
this . languageServiceAdapterHost . editScript ( fileName , editStart , editEnd , newText ) ;
2035
2016
for ( const marker of this . testData . markers ) {
2036
2017
if ( marker . fileName === fileName ) {
2037
- marker . position = updatePosition ( marker . position ) ;
2018
+ marker . position = updatePosition ( marker . position , editStart , editEnd , newText ) ;
2038
2019
}
2039
2020
}
2040
2021
2041
2022
for ( const range of this . testData . ranges ) {
2042
2023
if ( range . fileName === fileName ) {
2043
- range . pos = updatePosition ( range . pos ) ;
2044
- range . end = updatePosition ( range . end ) ;
2045
- }
2046
- }
2047
-
2048
- function updatePosition ( position : number ) {
2049
- if ( position > editStart ) {
2050
- if ( position < editEnd ) {
2051
- // Inside the edit - mark it as invalidated (?)
2052
- return - 1 ;
2053
- }
2054
- else {
2055
- // Move marker back/forward by the appropriate amount
2056
- return position + ( editStart - editEnd ) + newText . length ;
2057
- }
2058
- }
2059
- else {
2060
- return position ;
2024
+ range . pos = updatePosition ( range . pos , editStart , editEnd , newText ) ;
2025
+ range . end = updatePosition ( range . end , editStart , editEnd , newText ) ;
2061
2026
}
2062
2027
}
2063
2028
}
@@ -2488,22 +2453,24 @@ Actual: ${stringify(fullActual)}`);
2488
2453
2489
2454
this . applyCodeActions ( codeActions ) ;
2490
2455
2491
- this . verifyNewContent ( options , ts . flatMap ( codeActions , a => a . changes . map ( c => c . fileName ) ) ) ;
2456
+ this . verifyNewContentAfterChange ( options , ts . flatMap ( codeActions , a => a . changes . map ( c => c . fileName ) ) ) ;
2492
2457
}
2493
2458
2494
2459
public verifyRangeIs ( expectedText : string , includeWhiteSpace ?: boolean ) {
2460
+ this . verifyTextMatches ( this . rangeText ( this . getOnlyRange ( ) ) , ! ! includeWhiteSpace , expectedText ) ;
2461
+ }
2462
+
2463
+ private getOnlyRange ( ) {
2495
2464
const ranges = this . getRanges ( ) ;
2496
2465
if ( ranges . length !== 1 ) {
2497
2466
this . raiseError ( "Exactly one range should be specified in the testfile." ) ;
2498
2467
}
2468
+ return ts . first ( ranges ) ;
2469
+ }
2499
2470
2500
- const actualText = this . rangeText ( ranges [ 0 ] ) ;
2501
-
2502
- const result = includeWhiteSpace
2503
- ? actualText === expectedText
2504
- : this . removeWhitespace ( actualText ) === this . removeWhitespace ( expectedText ) ;
2505
-
2506
- if ( ! result ) {
2471
+ private verifyTextMatches ( actualText : string , includeWhitespace : boolean , expectedText : string ) {
2472
+ const removeWhitespace = ( s : string ) : string => includeWhitespace ? s : this . removeWhitespace ( s ) ;
2473
+ if ( removeWhitespace ( actualText ) !== removeWhitespace ( expectedText ) ) {
2507
2474
this . raiseError ( `Actual range text doesn't match expected text.\n${ showTextDiff ( expectedText , actualText ) } ` ) ;
2508
2475
}
2509
2476
}
@@ -2570,33 +2537,68 @@ Actual: ${stringify(fullActual)}`);
2570
2537
const action = actions [ index ] ;
2571
2538
2572
2539
assert . equal ( action . description , options . description ) ;
2540
+ assert . deepEqual ( action . commands , options . commands ) ;
2573
2541
2574
- for ( const change of action . changes ) {
2575
- this . applyEdits ( change . fileName , change . textChanges , /*isFormattingEdit*/ false ) ;
2542
+ if ( options . applyChanges ) {
2543
+ for ( const change of action . changes ) {
2544
+ this . applyEdits ( change . fileName , change . textChanges , /*isFormattingEdit*/ false ) ;
2545
+ }
2546
+ this . verifyNewContentAfterChange ( options , action . changes . map ( c => c . fileName ) ) ;
2576
2547
}
2548
+ else {
2549
+ this . verifyNewContent ( options , action . changes ) ;
2550
+ }
2551
+ }
2577
2552
2578
- this . verifyNewContent ( options , action . changes . map ( c => c . fileName ) ) ;
2553
+ private verifyNewContent ( { newFileContent, newRangeContent } : FourSlashInterface . NewContentOptions , changes : ReadonlyArray < ts . FileTextChanges > ) : void {
2554
+ if ( newRangeContent !== undefined ) {
2555
+ assert ( newFileContent === undefined ) ;
2556
+ assert ( changes . length === 1 , "Affected 0 or more than 1 file, must use 'newFileContent' instead of 'newRangeContent'" ) ;
2557
+ const change = ts . first ( changes ) ;
2558
+ assert ( change . fileName = this . activeFile . fileName ) ;
2559
+ const newText = ts . textChanges . applyChanges ( this . getFileContent ( this . activeFile . fileName ) , change . textChanges ) ;
2560
+ const newRange = updateTextRangeForTextChanges ( this . getOnlyRange ( ) , change . textChanges ) ;
2561
+ const actualText = newText . slice ( newRange . pos , newRange . end ) ;
2562
+ this . verifyTextMatches ( actualText , /*includeWhitespace*/ true , newRangeContent ) ;
2563
+ }
2564
+ else {
2565
+ if ( newFileContent === undefined ) throw ts . Debug . fail ( ) ;
2566
+ if ( typeof newFileContent !== "object" ) newFileContent = { [ this . activeFile . fileName ] : newFileContent } ;
2567
+ for ( const change of changes ) {
2568
+ const expectedNewContent = newFileContent [ change . fileName ] ;
2569
+ if ( expectedNewContent === undefined ) {
2570
+ ts . Debug . fail ( `Did not expect a change in ${ change . fileName } ` ) ;
2571
+ }
2572
+ const oldText = this . tryGetFileContent ( change . fileName ) ;
2573
+ ts . Debug . assert ( ! ! change . isNewFile === ( oldText === undefined ) ) ;
2574
+ const newContent = change . isNewFile ? ts . first ( change . textChanges ) . newText : ts . textChanges . applyChanges ( oldText ! , change . textChanges ) ;
2575
+ assert . equal ( newContent , expectedNewContent ) ;
2576
+ }
2577
+ for ( const newFileName in newFileContent ) {
2578
+ ts . Debug . assert ( changes . some ( c => c . fileName === newFileName ) , "No change in file" , ( ) => newFileName ) ;
2579
+ }
2580
+ }
2579
2581
}
2580
2582
2581
- private verifyNewContent ( options : FourSlashInterface . NewContentOptions , changedFiles : ReadonlyArray < string > ) {
2582
- const assertedChangedFiles = ! options . newFileContent || typeof options . newFileContent === "string"
2583
+ private verifyNewContentAfterChange ( { newFileContent , newRangeContent } : FourSlashInterface . NewContentOptions , changedFiles : ReadonlyArray < string > ) {
2584
+ const assertedChangedFiles = ! newFileContent || typeof newFileContent === "string"
2583
2585
? [ this . activeFile . fileName ]
2584
- : ts . getOwnKeys ( options . newFileContent ) ;
2586
+ : ts . getOwnKeys ( newFileContent ) ;
2585
2587
assert . deepEqual ( assertedChangedFiles , changedFiles ) ;
2586
2588
2587
- if ( options . newFileContent !== undefined ) {
2588
- assert ( ! options . newRangeContent ) ;
2589
- if ( typeof options . newFileContent === "string" ) {
2590
- this . verifyCurrentFileContent ( options . newFileContent ) ;
2589
+ if ( newFileContent !== undefined ) {
2590
+ assert ( ! newRangeContent ) ;
2591
+ if ( typeof newFileContent === "string" ) {
2592
+ this . verifyCurrentFileContent ( newFileContent ) ;
2591
2593
}
2592
2594
else {
2593
- for ( const fileName in options . newFileContent ) {
2594
- this . verifyFileContent ( fileName , options . newFileContent [ fileName ] ) ;
2595
+ for ( const fileName in newFileContent ) {
2596
+ this . verifyFileContent ( fileName , newFileContent [ fileName ] ) ;
2595
2597
}
2596
2598
}
2597
2599
}
2598
2600
else {
2599
- this . verifyRangeIs ( options . newRangeContent ! , /*includeWhitespace*/ true ) ;
2601
+ this . verifyRangeIs ( newRangeContent ! , /*includeWhitespace*/ true ) ;
2600
2602
}
2601
2603
}
2602
2604
@@ -3114,7 +3116,7 @@ Actual: ${stringify(fullActual)}`);
3114
3116
assert ( action . name === "Move to a new file" && action . description === "Move to a new file" ) ;
3115
3117
3116
3118
const editInfo = this . languageService . getEditsForRefactor ( range . fileName , this . formatCodeSettings , range , refactor . name , action . name , options . preferences || ts . emptyOptions ) ! ;
3117
- this . testNewFileContents ( editInfo . edits , options . newFileContents , "move to new file" ) ;
3119
+ this . verifyNewContent ( { newFileContent : options . newFileContents } , editInfo . edits ) ;
3118
3120
}
3119
3121
3120
3122
private testNewFileContents ( edits : ReadonlyArray < ts . FileTextChanges > , newFileContents : { [ fileName : string ] : string } , description : string ) : void {
@@ -3380,6 +3382,36 @@ Actual: ${stringify(fullActual)}`);
3380
3382
}
3381
3383
}
3382
3384
3385
+ function updateTextRangeForTextChanges ( { pos, end } : ts . TextRange , textChanges : ReadonlyArray < ts . TextChange > ) : ts . TextRange {
3386
+ forEachTextChange ( textChanges , change => {
3387
+ const update = ( p : number ) : number => updatePosition ( p , change . span . start , ts . textSpanEnd ( change . span ) , change . newText ) ;
3388
+ pos = update ( pos ) ;
3389
+ end = update ( end ) ;
3390
+ } ) ;
3391
+ return { pos, end } ;
3392
+ }
3393
+
3394
+ /** Apply each textChange in order, updating future changes to account for the text offset of previous changes. */
3395
+ function forEachTextChange ( changes : ReadonlyArray < ts . TextChange > , cb : ( change : ts . TextChange ) => void ) : void {
3396
+ // Copy this so we don't ruin someone else's copy
3397
+ changes = JSON . parse ( JSON . stringify ( changes ) ) ;
3398
+ for ( let i = 0 ; i < changes . length ; i ++ ) {
3399
+ const change = changes [ i ] ;
3400
+ cb ( change ) ;
3401
+ const changeDelta = change . newText . length - change . span . length ;
3402
+ for ( let j = i + 1 ; j < changes . length ; j ++ ) {
3403
+ if ( changes [ j ] . span . start >= change . span . start ) {
3404
+ changes [ j ] . span . start += changeDelta ;
3405
+ }
3406
+ }
3407
+ }
3408
+ }
3409
+
3410
+ function updatePosition ( position : number , editStart : number , editEnd : number , { length } : string ) : number {
3411
+ // If inside the edit, return -1 to mark as invalid
3412
+ return position <= editStart ? position : position < editEnd ? - 1 : position + length - + ( editEnd - editStart ) ;
3413
+ }
3414
+
3383
3415
function renameKeys < T > ( obj : { readonly [ key : string ] : T } , renameKey : ( key : string ) => string ) : { readonly [ key : string ] : T } {
3384
3416
const res : { [ key : string ] : T } = { } ;
3385
3417
for ( const key in obj ) {
@@ -4842,10 +4874,12 @@ namespace FourSlashInterface {
4842
4874
}
4843
4875
4844
4876
export interface VerifyCodeFixOptions extends NewContentOptions {
4845
- description : string ;
4846
- errorCode ?: number ;
4847
- index ?: number ;
4848
- preferences ?: ts . UserPreferences ;
4877
+ readonly description : string ;
4878
+ readonly errorCode ?: number ;
4879
+ readonly index ?: number ;
4880
+ readonly preferences ?: ts . UserPreferences ;
4881
+ readonly applyChanges ?: boolean ;
4882
+ readonly commands ?: ReadonlyArray < ts . CodeActionCommand > ;
4849
4883
}
4850
4884
4851
4885
export interface VerifyCodeFixAvailableOptions {
0 commit comments