@@ -263,22 +263,31 @@ namespace FourSlash {
263263 constructor ( private basePath : string , private testType : FourSlashTestType , public testData : FourSlashData ) {
264264 // Create a new Services Adapter
265265 this . cancellationToken = new TestCancellationToken ( ) ;
266- const compilationOptions = convertGlobalOptionsToCompilerOptions ( this . testData . globalOptions ) ;
267- if ( compilationOptions . typeRoots ) {
268- compilationOptions . typeRoots = compilationOptions . typeRoots . map ( p => ts . getNormalizedAbsolutePath ( p , this . basePath ) ) ;
269- }
266+ let compilationOptions = convertGlobalOptionsToCompilerOptions ( this . testData . globalOptions ) ;
270267 compilationOptions . skipDefaultLibCheck = true ;
271268
272- const languageServiceAdapter = this . getLanguageServiceAdapter ( testType , this . cancellationToken , compilationOptions ) ;
273- this . languageServiceAdapterHost = languageServiceAdapter . getHost ( ) ;
274- this . languageService = languageServiceAdapter . getLanguageService ( ) ;
275-
276269 // Initialize the language service with all the scripts
277270 let startResolveFileRef : FourSlashFile ;
278271
279272 ts . forEach ( testData . files , file => {
280273 // Create map between fileName and its content for easily looking up when resolveReference flag is specified
281274 this . inputFiles [ file . fileName ] = file . content ;
275+
276+ if ( ts . getBaseFileName ( file . fileName ) . toLowerCase ( ) === "tsconfig.json" ) {
277+ const configJson = ts . parseConfigFileTextToJson ( file . fileName , file . content ) ;
278+ assert . isTrue ( configJson . config !== undefined ) ;
279+
280+ // Extend our existing compiler options so that we can also support tsconfig only options
281+ if ( configJson . config . compilerOptions ) {
282+ const baseDirectory = ts . normalizePath ( ts . getDirectoryPath ( file . fileName ) ) ;
283+ const tsConfig = ts . convertCompilerOptionsFromJson ( configJson . config . compilerOptions , baseDirectory , file . fileName ) ;
284+
285+ if ( ! tsConfig . errors || ! tsConfig . errors . length ) {
286+ compilationOptions = ts . extend ( compilationOptions , tsConfig . options ) ;
287+ }
288+ }
289+ }
290+
282291 if ( ! startResolveFileRef && file . fileOptions [ metadataOptionNames . resolveReference ] === "true" ) {
283292 startResolveFileRef = file ;
284293 }
@@ -288,6 +297,15 @@ namespace FourSlash {
288297 }
289298 } ) ;
290299
300+
301+ if ( compilationOptions . typeRoots ) {
302+ compilationOptions . typeRoots = compilationOptions . typeRoots . map ( p => ts . getNormalizedAbsolutePath ( p , this . basePath ) ) ;
303+ }
304+
305+ const languageServiceAdapter = this . getLanguageServiceAdapter ( testType , this . cancellationToken , compilationOptions ) ;
306+ this . languageServiceAdapterHost = languageServiceAdapter . getHost ( ) ;
307+ this . languageService = languageServiceAdapter . getLanguageService ( ) ;
308+
291309 if ( startResolveFileRef ) {
292310 // Add the entry-point file itself into the languageServiceShimHost
293311 this . languageServiceAdapterHost . addScript ( startResolveFileRef . fileName , startResolveFileRef . content , /*isRootFile*/ true ) ;
@@ -730,10 +748,10 @@ namespace FourSlash {
730748 }
731749 }
732750
733- public verifyCompletionListContains ( symbol : string , text ?: string , documentation ?: string , kind ?: string ) {
751+ public verifyCompletionListContains ( symbol : string , text ?: string , documentation ?: string , kind ?: string , spanIndex ?: number ) {
734752 const completions = this . getCompletionListAtCaret ( ) ;
735753 if ( completions ) {
736- this . assertItemInCompletionList ( completions . entries , symbol , text , documentation , kind ) ;
754+ this . assertItemInCompletionList ( completions . entries , symbol , text , documentation , kind , spanIndex ) ;
737755 }
738756 else {
739757 this . raiseError ( `No completions at position '${ this . currentCaretPosition } ' when looking for '${ symbol } '.` ) ;
@@ -749,25 +767,32 @@ namespace FourSlash {
749767 * @param expectedText the text associated with the symbol
750768 * @param expectedDocumentation the documentation text associated with the symbol
751769 * @param expectedKind the kind of symbol (see ScriptElementKind)
770+ * @param spanIndex the index of the range that the completion item's replacement text span should match
752771 */
753- public verifyCompletionListDoesNotContain ( symbol : string , expectedText ?: string , expectedDocumentation ?: string , expectedKind ?: string ) {
772+ public verifyCompletionListDoesNotContain ( symbol : string , expectedText ?: string , expectedDocumentation ?: string , expectedKind ?: string , spanIndex ?: number ) {
754773 const that = this ;
774+ let replacementSpan : ts . TextSpan ;
775+ if ( spanIndex !== undefined ) {
776+ replacementSpan = this . getTextSpanForRangeAtIndex ( spanIndex ) ;
777+ }
778+
755779 function filterByTextOrDocumentation ( entry : ts . CompletionEntry ) {
756780 const details = that . getCompletionEntryDetails ( entry . name ) ;
757781 const documentation = ts . displayPartsToString ( details . documentation ) ;
758782 const text = ts . displayPartsToString ( details . displayParts ) ;
759- if ( expectedText && expectedDocumentation ) {
760- return ( documentation === expectedDocumentation && text === expectedText ) ? true : false ;
783+
784+ // If any of the expected values are undefined, assume that users don't
785+ // care about them.
786+ if ( replacementSpan && ! TestState . textSpansEqual ( replacementSpan , entry . replacementSpan ) ) {
787+ return false ;
761788 }
762- else if ( expectedText && ! expectedDocumentation ) {
763- return text === expectedText ? true : false ;
789+ else if ( expectedText && text !== expectedText ) {
790+ return false ;
764791 }
765- else if ( expectedDocumentation && ! expectedText ) {
766- return documentation === expectedDocumentation ? true : false ;
792+ else if ( expectedDocumentation && documentation !== expectedDocumentation ) {
793+ return false ;
767794 }
768- // Because expectedText and expectedDocumentation are undefined, we assume that
769- // users don"t care to compare them so we will treat that entry as if the entry has matching text and documentation
770- // and keep it in the list of filtered entry.
795+
771796 return true ;
772797 }
773798
@@ -791,6 +816,10 @@ namespace FourSlash {
791816 if ( expectedKind ) {
792817 error += "Expected kind: " + expectedKind + " to equal: " + filterCompletions [ 0 ] . kind + "." ;
793818 }
819+ if ( replacementSpan ) {
820+ const spanText = filterCompletions [ 0 ] . replacementSpan ? stringify ( filterCompletions [ 0 ] . replacementSpan ) : undefined ;
821+ error += "Expected replacement span: " + stringify ( replacementSpan ) + " to equal: " + spanText + "." ;
822+ }
794823 this . raiseError ( error ) ;
795824 }
796825 }
@@ -2188,7 +2217,7 @@ namespace FourSlash {
21882217 return text . substring ( startPos , endPos ) ;
21892218 }
21902219
2191- private assertItemInCompletionList ( items : ts . CompletionEntry [ ] , name : string , text ?: string , documentation ?: string , kind ?: string ) {
2220+ private assertItemInCompletionList ( items : ts . CompletionEntry [ ] , name : string , text ?: string , documentation ?: string , kind ?: string , spanIndex ?: number ) {
21922221 for ( let i = 0 ; i < items . length ; i ++ ) {
21932222 const item = items [ i ] ;
21942223 if ( item . name === name ) {
@@ -2207,6 +2236,11 @@ namespace FourSlash {
22072236 assert . equal ( item . kind , kind , this . assertionMessageAtLastKnownMarker ( "completion item kind for " + name ) ) ;
22082237 }
22092238
2239+ if ( spanIndex !== undefined ) {
2240+ const span = this . getTextSpanForRangeAtIndex ( spanIndex ) ;
2241+ assert . isTrue ( TestState . textSpansEqual ( span , item . replacementSpan ) , this . assertionMessageAtLastKnownMarker ( stringify ( span ) + " does not equal " + stringify ( item . replacementSpan ) + " replacement span for " + name ) ) ;
2242+ }
2243+
22102244 return ;
22112245 }
22122246 }
@@ -2263,6 +2297,17 @@ namespace FourSlash {
22632297 return `line ${ ( pos . line + 1 ) } , col ${ pos . character } ` ;
22642298 }
22652299
2300+ private getTextSpanForRangeAtIndex ( index : number ) : ts . TextSpan {
2301+ const ranges = this . getRanges ( ) ;
2302+ if ( ranges && ranges . length > index ) {
2303+ const range = ranges [ index ] ;
2304+ return { start : range . start , length : range . end - range . start } ;
2305+ }
2306+ else {
2307+ this . raiseError ( "Supplied span index: " + index + " does not exist in range list of size: " + ( ranges ? 0 : ranges . length ) ) ;
2308+ }
2309+ }
2310+
22662311 public getMarkerByName ( markerName : string ) {
22672312 const markerPos = this . testData . markerPositions [ markerName ] ;
22682313 if ( markerPos === undefined ) {
@@ -2286,6 +2331,10 @@ namespace FourSlash {
22862331 public resetCancelled ( ) : void {
22872332 this . cancellationToken . resetCancelled ( ) ;
22882333 }
2334+
2335+ private static textSpansEqual ( a : ts . TextSpan , b : ts . TextSpan ) {
2336+ return a && b && a . start === b . start && a . length === b . length ;
2337+ }
22892338 }
22902339
22912340 export function runFourSlashTest ( basePath : string , testType : FourSlashTestType , fileName : string ) {
@@ -2294,12 +2343,16 @@ namespace FourSlash {
22942343 }
22952344
22962345 export function runFourSlashTestContent ( basePath : string , testType : FourSlashTestType , content : string , fileName : string ) : void {
2346+ // Give file paths an absolute path for the virtual file system
2347+ const absoluteBasePath = ts . combinePaths ( Harness . virtualFileSystemRoot , basePath ) ;
2348+ const absoluteFileName = ts . combinePaths ( Harness . virtualFileSystemRoot , fileName ) ;
2349+
22972350 // Parse out the files and their metadata
2298- const testData = parseTestData ( basePath , content , fileName ) ;
2299- const state = new TestState ( basePath , testType , testData ) ;
2351+ const testData = parseTestData ( absoluteBasePath , content , absoluteFileName ) ;
2352+ const state = new TestState ( absoluteBasePath , testType , testData ) ;
23002353 const output = ts . transpileModule ( content , { reportDiagnostics : true } ) ;
23012354 if ( output . diagnostics . length > 0 ) {
2302- throw new Error ( `Syntax error in ${ basePath } : ${ output . diagnostics [ 0 ] . messageText } ` ) ;
2355+ throw new Error ( `Syntax error in ${ absoluteBasePath } : ${ output . diagnostics [ 0 ] . messageText } ` ) ;
23032356 }
23042357 runCode ( output . outputText , state ) ;
23052358 }
@@ -2852,12 +2905,12 @@ namespace FourSlashInterface {
28522905
28532906 // Verifies the completion list contains the specified symbol. The
28542907 // completion list is brought up if necessary
2855- public completionListContains ( symbol : string , text ?: string , documentation ?: string , kind ?: string ) {
2908+ public completionListContains ( symbol : string , text ?: string , documentation ?: string , kind ?: string , spanIndex ?: number ) {
28562909 if ( this . negative ) {
2857- this . state . verifyCompletionListDoesNotContain ( symbol , text , documentation , kind ) ;
2910+ this . state . verifyCompletionListDoesNotContain ( symbol , text , documentation , kind , spanIndex ) ;
28582911 }
28592912 else {
2860- this . state . verifyCompletionListContains ( symbol , text , documentation , kind ) ;
2913+ this . state . verifyCompletionListContains ( symbol , text , documentation , kind , spanIndex ) ;
28612914 }
28622915 }
28632916
0 commit comments