@@ -44,6 +44,14 @@ namespace FourSlash {
4444
4545 markers : Marker [ ] ;
4646
47+ /**
48+ * Inserted in source files by surrounding desired text
49+ * in a range with `[|` and `|]`. For example,
50+ *
51+ * [|text in range|]
52+ *
53+ * is a range with `text in range` "selected".
54+ */
4755 ranges : Range [ ] ;
4856 }
4957
@@ -84,6 +92,15 @@ namespace FourSlash {
8492 end : number ;
8593 }
8694
95+ export interface ErrorIdentifier {
96+ code : number ;
97+ /**
98+ * In a file where there is more than one error with code `code`, `count` refers
99+ * to which 0-indexed error, sorted by order of occurence, to consider.
100+ */
101+ count : number ;
102+ }
103+
87104 export import IndentStyle = ts . IndentStyle ;
88105
89106 const entityMap = ts . createMap ( {
@@ -1575,11 +1592,11 @@ namespace FourSlash {
15751592 let runningOffset = 0 ;
15761593 edits = edits . sort ( ( a , b ) => a . span . start - b . span . start ) ;
15771594 // Get a snapshot of the content of the file so we can make sure any formatting edits didn't destroy non-whitespace characters
1578- const oldContent = this . getFileContent ( this . activeFile . fileName ) ;
1579- for ( let j = 0 ; j < edits . length ; j ++ ) {
1580- this . languageServiceAdapterHost . editScript ( fileName , edits [ j ] . span . start + runningOffset , ts . textSpanEnd ( edits [ j ] . span ) + runningOffset , edits [ j ] . newText ) ;
1581- this . updateMarkersForEdit ( fileName , edits [ j ] . span . start + runningOffset , ts . textSpanEnd ( edits [ j ] . span ) + runningOffset , edits [ j ] . newText ) ;
1582- const change = ( edits [ j ] . span . start - ts . textSpanEnd ( edits [ j ] . span ) ) + edits [ j ] . newText . length ;
1595+ const oldContent = this . getFileContent ( fileName ) ;
1596+ for ( const edit of edits ) {
1597+ this . languageServiceAdapterHost . editScript ( fileName , edit . span . start + runningOffset , ts . textSpanEnd ( edit . span ) + runningOffset , edit . newText ) ;
1598+ this . updateMarkersForEdit ( fileName , edit . span . start + runningOffset , ts . textSpanEnd ( edit . span ) + runningOffset , edit . newText ) ;
1599+ const change = ( edit . span . start - ts . textSpanEnd ( edit . span ) ) + edit . newText . length ;
15831600 runningOffset += change ;
15841601 // TODO: Consider doing this at least some of the time for higher fidelity. Currently causes a failure (bug 707150)
15851602 // this.languageService.getScriptLexicalStructure(fileName);
@@ -1595,6 +1612,12 @@ namespace FourSlash {
15951612 return runningOffset ;
15961613 }
15971614
1615+ private applyCodeAction ( action : ts . CodeAction ) : void {
1616+ for ( const filechange of action . changes ) {
1617+ this . applyEdits ( filechange . fileName , filechange . textChanges , /*isFormattingEdit*/ false ) ;
1618+ }
1619+ }
1620+
15981621 public copyFormatOptions ( ) : ts . FormatCodeSettings {
15991622 return ts . clone ( this . formatCodeSettings ) ;
16001623 }
@@ -2019,45 +2042,105 @@ namespace FourSlash {
20192042 }
20202043 }
20212044
2022- private getCodeFixes ( errorCode ?: number ) {
2045+ /**
2046+ * Compares expected text to the text that would be in the sole range
2047+ * (ie: [|...|]) in the file after applying the codefix corresponding
2048+ * to the error with errorCode, or of the sole error in the source file.
2049+ *
2050+ * Because codefixes are only applied on the working file, it is unsafe
2051+ * to apply this more than once (consider a refactoring across files).
2052+ */
2053+ public verifyCodeFixAtPosition ( expectedText : string , errorCode ?: number ) {
2054+ const ranges = this . getRanges ( ) ;
2055+ if ( ranges . length !== 1 ) {
2056+ this . raiseError ( "Exactly one range should be specified in the testfile." ) ;
2057+ }
2058+
20232059 const fileName = this . activeFile . fileName ;
2024- const diagnostics = this . getDiagnostics ( fileName ) ;
2060+ const codeFix : ts . CodeAction = this . getCodeFix ( fileName , errorCode ? { code : errorCode , count : 0 } : undefined ) ;
20252061
2026- if ( diagnostics . length === 0 ) {
2027- this . raiseError ( "Errors expected ." ) ;
2062+ if ( ! codeFix ) {
2063+ this . raiseError ( "Should find exactly one codefix ." ) ;
20282064 }
20292065
2030- if ( diagnostics . length > 1 && errorCode !== undefined ) {
2031- this . raiseError ( "When there's more than one error, you must specify the errror to fix." ) ;
2066+ const fileChange = ts . find ( codeFix . changes , change => change . fileName === fileName ) ;
2067+ if ( ! fileChange ) {
2068+ this . raiseError ( "CodeFix found doesn't provide any changes in this file." ) ;
20322069 }
20332070
2034- const diagnostic = ! errorCode ? diagnostics [ 0 ] : ts . find ( diagnostics , d => d . code == errorCode ) ;
2071+ this . applyEdits ( fileChange . fileName , fileChange . textChanges , /*isFormattingEdit*/ false ) ;
2072+ const actualText = this . rangeText ( ranges [ 0 ] ) ;
20352073
2036- return this . languageService . getCodeFixesAtPosition ( fileName , diagnostic . start , diagnostic . length , [ diagnostic . code ] ) ;
2074+ if ( this . removeWhitespace ( actualText ) !== this . removeWhitespace ( expectedText ) ) {
2075+ this . raiseError ( `Actual text doesn't match expected text. Actual: '${ actualText } ' Expected: '${ expectedText } '` ) ;
2076+ }
20372077 }
20382078
2039- public verifyCodeFixAtPosition ( expectedText : string , errorCode ?: number ) {
2040- const ranges = this . getRanges ( ) ;
2041- if ( ranges . length == 0 ) {
2042- this . raiseError ( "At least one range should be specified in the testfile." ) ;
2079+ /**
2080+ * Applies fixes for the errors in fileName and compares the results to
2081+ * expectedContents after all fixes have been applied.
2082+ *
2083+ * Note: applying one codefix may generate another (eg: remove duplicate implements
2084+ * may generate an extends -> interface conversion fix).
2085+ * @param expectedContents The contents of the file after the fixes are applied.
2086+ * @param fileName The file to check. If not supplied, the current open file is used.
2087+ * @param errorsToFix An array of errors for which quickfixes will be applied. If not
2088+ * supplied, all codefixes in the file are applied until none are left, starting from
2089+ * the first available codefix.
2090+ *
2091+ */
2092+ public verifyFileAfterCodeFix ( expectedContents : string , fileName ?: string , errorsToFix ?: ErrorIdentifier [ ] ) {
2093+ fileName = fileName ? fileName : this . activeFile . fileName ;
2094+
2095+ if ( errorsToFix ) {
2096+ for ( const error of errorsToFix ) {
2097+ const fix = this . getCodeFix ( fileName , error ) ;
2098+ if ( fix === undefined ) {
2099+ this . raiseError ( `Couldn't find the ${ error . count } 'th error with code ${ error . code } .` ) ;
2100+ }
2101+ this . applyCodeAction ( fix ) ;
2102+ }
20432103 }
2044-
2045- const actual = this . getCodeFixes ( errorCode ) ;
2046-
2047- if ( ! actual || actual . length == 0 ) {
2048- this . raiseError ( "No codefixes returned." ) ;
2104+ else {
2105+ let fix : ts . CodeAction ;
2106+ while ( fix = this . getCodeFix ( fileName ) ) {
2107+ this . applyCodeAction ( fix ) ;
2108+ }
20492109 }
20502110
2051- if ( actual . length > 1 ) {
2052- this . raiseError ( "More than 1 codefix returned." ) ;
2111+ const actualContents : string = this . getFileContent ( fileName ) ;
2112+ if ( this . removeWhitespace ( actualContents ) !== this . removeWhitespace ( expectedContents ) ) {
2113+ this . raiseError ( `Actual text doesn't match expected text. Actual:\n${ actualContents } \n\nExpected:\n${ expectedContents } ` ) ;
20532114 }
2115+ }
20542116
2055- this . applyEdits ( actual [ 0 ] . changes [ 0 ] . fileName , actual [ 0 ] . changes [ 0 ] . textChanges , /*isFormattingEdit*/ false ) ;
2056- const actualText = this . rangeText ( ranges [ 0 ] ) ;
2117+ /**
2118+ * Rerieves a codefix satisfying the parameters, or undefined if no such codefix is found.
2119+ * @param fileName Path to file where error should be retrieved from.
2120+ * @param error We get the `error.count`'th codefix with code `error.code`.
2121+ *
2122+ * If undefined, we get the first codefix available.
2123+ */
2124+ private getCodeFix ( fileName : string , error ?: ErrorIdentifier ) : ts . CodeAction | undefined {
2125+ const diagnostics : ts . Diagnostic [ ] = this . getDiagnostics ( fileName ) ;
2126+ const errorCount = error ? error . count : 0 ;
20572127
2058- if ( this . removeWhitespace ( actualText ) !== this . removeWhitespace ( expectedText ) ) {
2059- this . raiseError ( `Actual text doesn't match expected text. Actual: '${ actualText } ' Expected: '${ expectedText } '` ) ;
2128+ let countSeen = 0 ;
2129+ for ( const diagnostic of diagnostics ) {
2130+ if ( error && error . code !== diagnostic . code ) {
2131+ continue ;
2132+ }
2133+ const action = this . languageService . getCodeFixesAtPosition ( fileName , diagnostic . start , diagnostic . length , [ diagnostic . code ] ) ;
2134+ if ( action ) {
2135+ if ( action . length > errorCount - countSeen ) {
2136+ return action [ errorCount - countSeen ] ;
2137+ }
2138+ else {
2139+ countSeen += action . length ;
2140+ }
2141+ }
20602142 }
2143+ return undefined ;
20612144 }
20622145
20632146 public verifyDocCommentTemplate ( expected ?: ts . TextInsertion ) {
@@ -2344,14 +2427,14 @@ namespace FourSlash {
23442427 }
23452428
23462429 public verifyCodeFixAvailable ( negative : boolean , errorCode ?: number ) {
2347- const fixes = this . getCodeFixes ( errorCode ) ;
2430+ const codeFix = this . getCodeFix ( this . activeFile . fileName , errorCode ? { code : errorCode , count : 0 } : undefined ) ;
23482431
2349- if ( negative && fixes && fixes . length > 0 ) {
2350- this . raiseError ( `verifyCodeFixAvailable failed - expected no fixes, actual: ${ fixes . length } ` ) ;
2432+ if ( negative && codeFix ) {
2433+ this . raiseError ( `verifyCodeFixAvailable failed - expected no fixes but found one. ` ) ;
23512434 }
23522435
2353- if ( ! negative && ( fixes === undefined || fixes . length === 0 ) ) {
2354- this . raiseError ( `verifyCodeFixAvailable failed - expected code fixes, actual: 0 ` ) ;
2436+ if ( ! ( negative || codeFix ) ) {
2437+ this . raiseError ( `verifyCodeFixAvailable failed - expected code fixes but none found. ` ) ;
23552438 }
23562439 }
23572440
@@ -3329,6 +3412,10 @@ namespace FourSlashInterface {
33293412 this . state . verifyCodeFixAtPosition ( expectedText , errorCode ) ;
33303413 }
33313414
3415+ public fileAfterCodeFixes ( expectedContents : string , fileName ?: string , errorsToFix ?: FourSlash . ErrorIdentifier [ ] ) : void {
3416+ this . state . verifyFileAfterCodeFix ( expectedContents , fileName , errorsToFix ) ;
3417+ }
3418+
33323419 public navigationBar ( json : any ) {
33333420 this . state . verifyNavigationBar ( json ) ;
33343421 }
0 commit comments