• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace FourSlash {
2    import ArrayOrSingle = FourSlashInterface.ArrayOrSingle;
3
4    export const enum FourSlashTestType {
5        Native,
6        Shims,
7        ShimsWithPreprocess,
8        Server
9    }
10
11    // Represents a parsed source file with metadata
12    interface FourSlashFile {
13        // The contents of the file (with markers, etc stripped out)
14        content: string;
15        fileName: string;
16        symlinks?: string[];
17        version: number;
18        // File-specific options (name/value pairs)
19        fileOptions: Harness.TestCaseParser.CompilerSettings;
20    }
21
22    // Represents a set of parsed source files and options
23    interface FourSlashData {
24        // Global options (name/value pairs)
25        globalOptions: Harness.TestCaseParser.CompilerSettings;
26
27        files: FourSlashFile[];
28
29        symlinks: vfs.FileSet | undefined;
30
31        // A mapping from marker names to name/position pairs
32        markerPositions: ts.ESMap<string, Marker>;
33
34        markers: Marker[];
35
36        /**
37         * Inserted in source files by surrounding desired text
38         * in a range with `[|` and `|]`. For example,
39         *
40         * [|text in range|]
41         *
42         * is a range with `text in range` "selected".
43         */
44        ranges: Range[];
45        rangesByText?: ts.MultiMap<string, Range>;
46    }
47
48    export interface Marker {
49        fileName: string;
50        position: number;
51        data?: {};
52    }
53
54    export interface Range extends ts.TextRange {
55        fileName: string;
56        marker?: Marker;
57    }
58
59    interface LocationInformation {
60        position: number;
61        sourcePosition: number;
62        sourceLine: number;
63        sourceColumn: number;
64    }
65
66    interface RangeLocationInformation extends LocationInformation {
67        marker?: Marker;
68    }
69
70    interface ImplementationLocationInformation extends ts.ImplementationLocation {
71        matched?: boolean;
72    }
73
74    export interface TextSpan {
75        start: number;
76        end: number;
77    }
78
79    // Name of testcase metadata including ts.CompilerOptions properties that will be used by globalOptions
80    // To add additional option, add property into the testOptMetadataNames, refer the property in either globalMetadataNames or fileMetadataNames
81    // Add cases into convertGlobalOptionsToCompilationsSettings function for the compiler to acknowledge such option from meta data
82    const enum MetadataOptionNames {
83        baselineFile = "baselinefile",
84        emitThisFile = "emitthisfile",  // This flag is used for testing getEmitOutput feature. It allows test-cases to indicate what file to be output in multiple files project
85        fileName = "filename",
86        resolveReference = "resolvereference",  // This flag is used to specify entry file for resolve file references. The flag is only allow once per test file
87        symlink = "symlink",
88    }
89
90    // List of allowed metadata names
91    const fileMetadataNames = [MetadataOptionNames.fileName, MetadataOptionNames.emitThisFile, MetadataOptionNames.resolveReference, MetadataOptionNames.symlink];
92
93    function convertGlobalOptionsToCompilerOptions(globalOptions: Harness.TestCaseParser.CompilerSettings): ts.CompilerOptions {
94        const settings: ts.CompilerOptions = { target: ts.ScriptTarget.ES5 };
95        Harness.Compiler.setCompilerOptionsFromHarnessSetting(globalOptions, settings);
96        return settings;
97    }
98
99    export class TestCancellationToken implements ts.HostCancellationToken {
100        // 0 - cancelled
101        // >0 - not cancelled
102        // <0 - not cancelled and value denotes number of isCancellationRequested after which token become cancelled
103        private static readonly notCanceled = -1;
104        private numberOfCallsBeforeCancellation = TestCancellationToken.notCanceled;
105
106        public isCancellationRequested(): boolean {
107            if (this.numberOfCallsBeforeCancellation < 0) {
108                return false;
109            }
110
111            if (this.numberOfCallsBeforeCancellation > 0) {
112                this.numberOfCallsBeforeCancellation--;
113                return false;
114            }
115
116            return true;
117        }
118
119        public setCancelled(numberOfCalls = 0): void {
120            ts.Debug.assert(numberOfCalls >= 0);
121            this.numberOfCallsBeforeCancellation = numberOfCalls;
122        }
123
124        public resetCancelled(): void {
125            this.numberOfCallsBeforeCancellation = TestCancellationToken.notCanceled;
126        }
127    }
128
129    export function verifyOperationIsCancelled(f: () => void) {
130        try {
131            f();
132        }
133        catch (e) {
134            if (e instanceof ts.OperationCanceledException) {
135                return;
136            }
137        }
138
139        throw new Error("Operation should be cancelled");
140    }
141
142    export function ignoreInterpolations(diagnostic: string | ts.DiagnosticMessage): FourSlashInterface.DiagnosticIgnoredInterpolations {
143        return { template: typeof diagnostic === "string" ? diagnostic : diagnostic.message };
144    }
145
146    // This function creates IScriptSnapshot object for testing getPreProcessedFileInfo
147    // Return object may lack some functionalities for other purposes.
148    function createScriptSnapShot(sourceText: string): ts.IScriptSnapshot {
149        return ts.ScriptSnapshot.fromString(sourceText);
150    }
151
152    const enum CallHierarchyItemDirection {
153        Root,
154        Incoming,
155        Outgoing
156    }
157
158    export class TestState {
159        // Language service instance
160        private languageServiceAdapterHost: Harness.LanguageService.LanguageServiceAdapterHost;
161        private languageService: ts.LanguageService;
162        private cancellationToken: TestCancellationToken;
163        private assertTextConsistent: ((fileName: string) => void) | undefined;
164
165        // The current caret position in the active file
166        public currentCaretPosition = 0;
167        // The position of the end of the current selection, or -1 if nothing is selected
168        public selectionEnd = -1;
169
170        public lastKnownMarker = "";
171
172        // The file that's currently 'opened'
173        public activeFile!: FourSlashFile;
174
175        // Whether or not we should format on keystrokes
176        public enableFormatting = true;
177
178        public formatCodeSettings: ts.FormatCodeSettings;
179
180        private inputFiles = new ts.Map<string, string>();  // Map between inputFile's fileName and its content for easily looking up when resolving references
181
182        private static getDisplayPartsJson(displayParts: ts.SymbolDisplayPart[] | undefined) {
183            let result = "";
184            ts.forEach(displayParts, part => {
185                if (result) {
186                    result += ",\n    ";
187                }
188                else {
189                    result = "[\n    ";
190                }
191                result += JSON.stringify(part);
192            });
193            if (result) {
194                result += "\n]";
195            }
196
197            return result;
198        }
199
200        // Add input file which has matched file name with the given reference-file path.
201        // This is necessary when resolveReference flag is specified
202        private addMatchedInputFile(referenceFilePath: string, extensions: readonly string[] | undefined) {
203            const inputFiles = this.inputFiles;
204            const languageServiceAdapterHost = this.languageServiceAdapterHost;
205            const didAdd = tryAdd(referenceFilePath);
206            if (extensions && !didAdd) {
207                ts.forEach(extensions, ext => tryAdd(referenceFilePath + ext));
208            }
209
210            function tryAdd(path: string) {
211                const inputFile = inputFiles.get(path);
212                if (inputFile && !Harness.isDefaultLibraryFile(path)) {
213                    languageServiceAdapterHost.addScript(path, inputFile, /*isRootFile*/ true);
214                    return true;
215                }
216            }
217        }
218
219        private getLanguageServiceAdapter(testType: FourSlashTestType, cancellationToken: TestCancellationToken, compilationOptions: ts.CompilerOptions): Harness.LanguageService.LanguageServiceAdapter {
220            switch (testType) {
221                case FourSlashTestType.Native:
222                    return new Harness.LanguageService.NativeLanguageServiceAdapter(cancellationToken, compilationOptions);
223                case FourSlashTestType.Shims:
224                    return new Harness.LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ false, cancellationToken, compilationOptions);
225                case FourSlashTestType.ShimsWithPreprocess:
226                    return new Harness.LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ true, cancellationToken, compilationOptions);
227                case FourSlashTestType.Server:
228                    return new Harness.LanguageService.ServerLanguageServiceAdapter(cancellationToken, compilationOptions);
229                default:
230                    throw new Error("Unknown FourSlash test type: ");
231            }
232        }
233
234        constructor(private originalInputFileName: string, private basePath: string, private testType: FourSlashTestType, public testData: FourSlashData) {
235            // Create a new Services Adapter
236            this.cancellationToken = new TestCancellationToken();
237            let compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions);
238            compilationOptions.skipDefaultLibCheck = true;
239
240            // Initialize the language service with all the scripts
241            let startResolveFileRef: FourSlashFile | undefined;
242
243            let configFileName: string | undefined;
244            for (const file of testData.files) {
245                // Create map between fileName and its content for easily looking up when resolveReference flag is specified
246                this.inputFiles.set(file.fileName, file.content);
247                if (isConfig(file)) {
248                    const configJson = ts.parseConfigFileTextToJson(file.fileName, file.content);
249                    if (configJson.config === undefined) {
250                        throw new Error(`Failed to parse test ${file.fileName}: ${configJson.error!.messageText}`);
251                    }
252
253                    // Extend our existing compiler options so that we can also support tsconfig only options
254                    if (configJson.config.compilerOptions) {
255                        const baseDirectory = ts.normalizePath(ts.getDirectoryPath(file.fileName));
256                        const tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDirectory, file.fileName);
257
258                        if (!tsConfig.errors || !tsConfig.errors.length) {
259                            compilationOptions = ts.extend(tsConfig.options, compilationOptions);
260                        }
261                    }
262                    configFileName = file.fileName;
263                }
264
265                if (!startResolveFileRef && file.fileOptions[MetadataOptionNames.resolveReference] === "true") {
266                    startResolveFileRef = file;
267                }
268                else if (startResolveFileRef) {
269                    // If entry point for resolving file references is already specified, report duplication error
270                    throw new Error("There exists a Fourslash file which has resolveReference flag specified; remove duplicated resolveReference flag");
271                }
272            }
273
274            let configParseResult: ts.ParsedCommandLine | undefined;
275            if (configFileName) {
276                const baseDir = ts.normalizePath(ts.getDirectoryPath(configFileName));
277                const files: vfs.FileSet = { [baseDir]: {} };
278                this.inputFiles.forEach((data, path) => {
279                    const scriptInfo = new Harness.LanguageService.ScriptInfo(path, undefined!, /*isRootFile*/ false); // TODO: GH#18217
280                    files[path] = new vfs.File(data, { meta: { scriptInfo } });
281                });
282                const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd: baseDir, files });
283                const host = new fakes.ParseConfigHost(fs);
284                const jsonSourceFile = ts.parseJsonText(configFileName, this.inputFiles.get(configFileName)!);
285                configParseResult = ts.parseJsonSourceFileConfigFileContent(jsonSourceFile, host, baseDir, compilationOptions, configFileName);
286                compilationOptions = configParseResult.options;
287            }
288
289            if (compilationOptions.typeRoots) {
290                compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath));
291            }
292
293            const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions);
294            this.languageServiceAdapterHost = languageServiceAdapter.getHost();
295            this.languageService = memoWrap(languageServiceAdapter.getLanguageService(), this); // Wrap the LS to cache some expensive operations certain tests call repeatedly
296            if (this.testType === FourSlashTestType.Server) {
297                this.assertTextConsistent = fileName => (languageServiceAdapter as Harness.LanguageService.ServerLanguageServiceAdapter).assertTextConsistent(fileName);
298            }
299
300            if (startResolveFileRef) {
301                // Add the entry-point file itself into the languageServiceShimHost
302                this.languageServiceAdapterHost.addScript(startResolveFileRef.fileName, startResolveFileRef.content, /*isRootFile*/ true);
303
304                const resolvedResult = languageServiceAdapter.getPreProcessedFileInfo(startResolveFileRef.fileName, startResolveFileRef.content);
305                const referencedFiles: ts.FileReference[] = resolvedResult.referencedFiles;
306                const importedFiles: ts.FileReference[] = resolvedResult.importedFiles;
307
308                // Add triple reference files into language-service host
309                ts.forEach(referencedFiles, referenceFile => {
310                    // Fourslash insert tests/cases/fourslash into inputFile.unitName so we will properly append the same base directory to refFile path
311                    const referenceFilePath = this.basePath + "/" + referenceFile.fileName;
312                    this.addMatchedInputFile(referenceFilePath, /* extensions */ undefined);
313                });
314
315                // Add import files into language-service host
316                ts.forEach(importedFiles, importedFile => {
317                    // Fourslash insert tests/cases/fourslash into inputFile.unitName and import statement doesn't require ".ts"
318                    // so convert them before making appropriate comparison
319                    const importedFilePath = this.basePath + "/" + importedFile.fileName;
320                    this.addMatchedInputFile(importedFilePath, ts.getSupportedExtensions(compilationOptions));
321                });
322
323                // Check if no-default-lib flag is false and if so add default library
324                if (!resolvedResult.isLibFile) {
325                    this.languageServiceAdapterHost.addScript(Harness.Compiler.defaultLibFileName,
326                        Harness.Compiler.getDefaultLibrarySourceFile()!.text, /*isRootFile*/ false);
327
328                    compilationOptions.lib?.forEach(fileName => {
329                        const libFile = Harness.Compiler.getDefaultLibrarySourceFile(fileName);
330                        ts.Debug.assertIsDefined(libFile, `Could not find lib file '${fileName}'`);
331                        if (libFile) {
332                            this.languageServiceAdapterHost.addScript(fileName, libFile.text, /*isRootFile*/ false);
333                        }
334                    });
335                }
336            }
337            else {
338                // resolveReference file-option is not specified then do not resolve any files and include all inputFiles
339                this.inputFiles.forEach((file, fileName) => {
340                    if (!Harness.isDefaultLibraryFile(fileName)) {
341                        // all files if config file not specified, otherwise root files from the config and typings cache files are root files
342                        const isRootFile = !configParseResult ||
343                            ts.contains(configParseResult.fileNames, fileName) ||
344                            (ts.isDeclarationFileName(fileName) && ts.containsPath("/Library/Caches/typescript", fileName));
345                        this.languageServiceAdapterHost.addScript(fileName, file, isRootFile);
346                    }
347                });
348
349                if (!compilationOptions.noLib) {
350                    const seen = new Set<string>();
351                    const addSourceFile = (fileName: string) => {
352                        if (seen.has(fileName)) return;
353                        seen.add(fileName);
354                        const libFile = Harness.Compiler.getDefaultLibrarySourceFile(fileName);
355                        ts.Debug.assertIsDefined(libFile, `Could not find lib file '${fileName}'`);
356                        this.languageServiceAdapterHost.addScript(fileName, libFile.text, /*isRootFile*/ false);
357                        if (!ts.some(libFile.libReferenceDirectives)) return;
358                        for (const directive of libFile.libReferenceDirectives) {
359                            addSourceFile(`lib.${directive.fileName}.d.ts`);
360                        }
361                    };
362
363                    addSourceFile(Harness.Compiler.defaultLibFileName);
364                    compilationOptions.lib?.forEach(addSourceFile);
365                }
366
367                compilationOptions.ets?.libs.forEach(fileName => {
368                    const content = ts.sys.readFile(fileName);
369                    if (content) {
370                        this.languageServiceAdapterHost.addScript(fileName, content, /*isRootFile*/ false);
371                    }
372                });
373            }
374
375            for (const file of testData.files) {
376                ts.forEach(file.symlinks, link => {
377                    this.languageServiceAdapterHost.vfs.mkdirpSync(vpath.dirname(link));
378                    this.languageServiceAdapterHost.vfs.symlinkSync(file.fileName, link);
379                });
380            }
381
382            if (testData.symlinks) {
383                this.languageServiceAdapterHost.vfs.apply(testData.symlinks);
384            }
385
386            this.formatCodeSettings = ts.testFormatSettings;
387
388            // Open the first file by default
389            this.openFile(0);
390
391            function memoWrap(ls: ts.LanguageService, target: TestState): ts.LanguageService {
392                const cacheableMembers: (keyof typeof ls)[] = [
393                    "getCompletionEntryDetails",
394                    "getCompletionEntrySymbol",
395                    "getQuickInfoAtPosition",
396                    "getReferencesAtPosition",
397                    "getDocumentHighlights",
398                ];
399                const proxy = {} as ts.LanguageService;
400                const keys = ts.getAllKeys(ls);
401                for (const k of keys) {
402                    const key = k as keyof typeof ls;
403                    if (cacheableMembers.indexOf(key) === -1) {
404                        proxy[key] = (...args: any[]) => (ls[key] as Function)(...args);
405                        continue;
406                    }
407                    const memo = Utils.memoize(
408                        (_version: number, _active: string, _caret: number, _selectEnd: number, _marker: string, ...args: any[]) => (ls[key] as Function)(...args),
409                        (...args) => args.join("|,|")
410                    );
411                    proxy[key] = (...args: any[]) => memo(
412                        target.languageServiceAdapterHost.getScriptInfo(target.activeFile.fileName)!.version,
413                        target.activeFile.fileName,
414                        target.currentCaretPosition,
415                        target.selectionEnd,
416                        target.lastKnownMarker,
417                        ...args
418                    );
419                }
420                return proxy;
421            }
422        }
423
424        private getFileContent(fileName: string): string {
425            return ts.Debug.checkDefined(this.tryGetFileContent(fileName));
426        }
427        private tryGetFileContent(fileName: string): string | undefined {
428            const script = this.languageServiceAdapterHost.getScriptInfo(fileName);
429            return script && script.content;
430        }
431
432        // Entry points from fourslash.ts
433        public goToMarker(name: string | Marker = "") {
434            const marker = ts.isString(name) ? this.getMarkerByName(name) : name;
435            if (this.activeFile.fileName !== marker.fileName) {
436                this.openFile(marker.fileName);
437            }
438
439            const content = this.getFileContent(marker.fileName);
440            if (marker.position === -1 || marker.position > content.length) {
441                throw new Error(`Marker "${name}" has been invalidated by unrecoverable edits to the file.`);
442            }
443            const mName = ts.isString(name) ? name : this.markerName(marker);
444            this.lastKnownMarker = mName;
445            this.goToPosition(marker.position);
446        }
447
448        public goToEachMarker(markers: readonly Marker[], action: (marker: Marker, index: number) => void) {
449            assert(markers.length);
450            for (let i = 0; i < markers.length; i++) {
451                this.goToMarker(markers[i]);
452                action(markers[i], i);
453            }
454        }
455
456        public goToEachRange(action: (range: Range) => void) {
457            const ranges = this.getRanges();
458            assert(ranges.length);
459            for (const range of ranges) {
460                this.selectRange(range);
461                action(range);
462            }
463        }
464
465        public markerName(m: Marker): string {
466            return ts.forEachEntry(this.testData.markerPositions, (marker, name) => {
467                if (marker === m) {
468                    return name;
469                }
470            })!;
471        }
472
473        public goToPosition(positionOrLineAndCharacter: number | ts.LineAndCharacter) {
474            const pos = typeof positionOrLineAndCharacter === "number"
475                ? positionOrLineAndCharacter
476                : this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, positionOrLineAndCharacter);
477            this.currentCaretPosition = pos;
478            this.selectionEnd = -1;
479        }
480
481        public select(startMarker: string, endMarker: string) {
482            const start = this.getMarkerByName(startMarker), end = this.getMarkerByName(endMarker);
483            ts.Debug.assert(start.fileName === end.fileName);
484            if (this.activeFile.fileName !== start.fileName) {
485                this.openFile(start.fileName);
486            }
487            this.goToPosition(start.position);
488            this.selectionEnd = end.position;
489        }
490
491        public selectAllInFile(fileName: string) {
492            this.openFile(fileName);
493            this.goToPosition(0);
494            this.selectionEnd = this.activeFile.content.length;
495        }
496
497        public selectRange(range: Range): void {
498            this.goToRangeStart(range);
499            this.selectionEnd = range.end;
500        }
501
502        public selectLine(index: number) {
503            const lineStart = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: index, character: 0 });
504            const lineEnd = lineStart + this.getLineContent(index).length;
505            this.selectRange({ fileName: this.activeFile.fileName, pos: lineStart, end: lineEnd });
506        }
507
508        public moveCaretRight(count = 1) {
509            this.currentCaretPosition += count;
510            this.currentCaretPosition = Math.min(this.currentCaretPosition, this.getFileContent(this.activeFile.fileName).length);
511            this.selectionEnd = -1;
512        }
513
514        // Opens a file given its 0-based index or fileName
515        public openFile(indexOrName: number | string, content?: string, scriptKindName?: string): void {
516            const fileToOpen: FourSlashFile = this.findFile(indexOrName);
517            fileToOpen.fileName = ts.normalizeSlashes(fileToOpen.fileName);
518            this.activeFile = fileToOpen;
519            // Let the host know that this file is now open
520            this.languageServiceAdapterHost.openFile(fileToOpen.fileName, content, scriptKindName);
521        }
522
523        public verifyErrorExistsBetweenMarkers(startMarkerName: string, endMarkerName: string, shouldExist: boolean) {
524            const startMarker = this.getMarkerByName(startMarkerName);
525            const endMarker = this.getMarkerByName(endMarkerName);
526            const predicate = (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) =>
527                ((errorMinChar === startPos) && (errorLimChar === endPos)) ? true : false;
528
529            const exists = this.anyErrorInRange(predicate, startMarker, endMarker);
530
531            if (exists !== shouldExist) {
532                this.printErrorLog(shouldExist, this.getAllDiagnostics());
533                throw new Error(`${shouldExist ? "Expected" : "Did not expect"} failure between markers: '${startMarkerName}', '${endMarkerName}'`);
534            }
535        }
536
537        public verifyOrganizeImports(newContent: string) {
538            const changes = this.languageService.organizeImports({ fileName: this.activeFile.fileName, type: "file" }, this.formatCodeSettings, ts.emptyOptions);
539            this.applyChanges(changes);
540            this.verifyFileContent(this.activeFile.fileName, newContent);
541        }
542
543        private raiseError(message: string): never {
544            throw new Error(this.messageAtLastKnownMarker(message));
545        }
546
547        private messageAtLastKnownMarker(message: string) {
548            const locationDescription = this.lastKnownMarker ? this.lastKnownMarker : this.getLineColStringAtPosition(this.currentCaretPosition);
549            return `At ${locationDescription}: ${message}`;
550        }
551
552        private assertionMessageAtLastKnownMarker(msg: string) {
553            return "\nMarker: " + this.lastKnownMarker + "\nChecking: " + msg + "\n\n";
554        }
555
556        private getDiagnostics(fileName: string, includeSuggestions = false): ts.Diagnostic[] {
557            return [
558                ...this.languageService.getSyntacticDiagnostics(fileName),
559                ...this.languageService.getSemanticDiagnostics(fileName),
560                ...(includeSuggestions ? this.languageService.getSuggestionDiagnostics(fileName) : ts.emptyArray),
561            ];
562        }
563
564        private getAllDiagnostics(): readonly ts.Diagnostic[] {
565            return ts.flatMap(this.languageServiceAdapterHost.getFilenames(), fileName => {
566                if (!ts.isAnySupportedFileExtension(fileName)) {
567                    return [];
568                }
569
570                const baseName = ts.getBaseFileName(fileName);
571                if (baseName === "package.json" || baseName === "tsconfig.json" || baseName === "jsconfig.json") {
572                    return [];
573                }
574                return this.getDiagnostics(fileName);
575            });
576        }
577
578        public verifyErrorExistsAfterMarker(markerName: string, shouldExist: boolean, after: boolean) {
579            const marker: Marker = this.getMarkerByName(markerName);
580            let predicate: (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number) => boolean;
581
582            if (after) {
583                predicate = (errorMinChar: number, errorLimChar: number, startPos: number) =>
584                    ((errorMinChar >= startPos) && (errorLimChar >= startPos)) ? true : false;
585            }
586            else {
587                predicate = (errorMinChar: number, errorLimChar: number, startPos: number) =>
588                    ((errorMinChar <= startPos) && (errorLimChar <= startPos)) ? true : false;
589            }
590
591            const exists = this.anyErrorInRange(predicate, marker);
592            const diagnostics = this.getAllDiagnostics();
593
594            if (exists !== shouldExist) {
595                this.printErrorLog(shouldExist, diagnostics);
596                throw new Error(`${shouldExist ? "Expected" : "Did not expect"} failure at marker '${markerName}'`);
597            }
598        }
599
600        private anyErrorInRange(predicate: (errorMinChar: number, errorLimChar: number, startPos: number, endPos: number | undefined) => boolean, startMarker: Marker, endMarker?: Marker): boolean {
601            return this.getDiagnostics(startMarker.fileName).some(({ start, length }) =>
602                predicate(start!, start! + length!, startMarker.position, endMarker === undefined ? undefined : endMarker.position)); // TODO: GH#18217
603        }
604
605        private printErrorLog(expectErrors: boolean, errors: readonly ts.Diagnostic[]): void {
606            if (expectErrors) {
607                Harness.IO.log("Expected error not found.  Error list is:");
608            }
609            else {
610                Harness.IO.log("Unexpected error(s) found.  Error list is:");
611            }
612
613            for (const { start, length, messageText, file } of errors) {
614                Harness.IO.log("  " + this.formatRange(file, start!, length!) + // TODO: GH#18217
615                    ", message: " + ts.flattenDiagnosticMessageText(messageText, Harness.IO.newLine()) + "\n");
616            }
617        }
618
619        private formatRange(file: ts.SourceFile | undefined, start: number, length: number) {
620            if (file) {
621                return `from: ${this.formatLineAndCharacterOfPosition(file, start)}, to: ${this.formatLineAndCharacterOfPosition(file, start + length)}`;
622            }
623            return "global";
624        }
625
626        private formatLineAndCharacterOfPosition(file: ts.SourceFile, pos: number) {
627            if (file) {
628                const { line, character } = ts.getLineAndCharacterOfPosition(file, pos);
629                return `${line}:${character}`;
630            }
631            return "global";
632        }
633
634        private formatPosition(file: ts.SourceFile, pos: number) {
635            if (file) {
636                return file.fileName + "@" + pos;
637            }
638            return "global";
639        }
640
641        public verifyNoErrors() {
642            ts.forEachKey(this.inputFiles, fileName => {
643                if (!ts.isAnySupportedFileExtension(fileName)
644                    || Harness.getConfigNameFromFileName(fileName)
645                    || !ts.getAllowJSCompilerOption(this.getProgram().getCompilerOptions()) && !ts.resolutionExtensionIsTSOrJson(ts.extensionFromPath(fileName))) return;
646                const errors = this.getDiagnostics(fileName).filter(e => e.category !== ts.DiagnosticCategory.Suggestion);
647                if (errors.length) {
648                    this.printErrorLog(/*expectErrors*/ false, errors);
649                    const error = errors[0];
650                    this.raiseError(`Found an error: ${this.formatPosition(error.file!, error.start!)}: ${error.messageText}`);
651                }
652            });
653        }
654
655        public verifyErrorExistsAtRange(range: Range, code: number, expectedMessage?: string) {
656            const span = ts.createTextSpanFromRange(range);
657            const hasMatchingError = ts.some(
658                this.getDiagnostics(range.fileName),
659                ({ code, messageText, start, length }) =>
660                    code === code &&
661                    (!expectedMessage || expectedMessage === messageText) &&
662                    ts.isNumber(start) && ts.isNumber(length) &&
663                    ts.textSpansEqual(span, { start, length }));
664
665            if (!hasMatchingError) {
666                this.raiseError(`No error with code ${code} found at provided range.`);
667            }
668        }
669
670        public verifyNumberOfErrorsInCurrentFile(expected: number) {
671            const errors = this.getDiagnostics(this.activeFile.fileName);
672            const actual = errors.length;
673
674            if (actual !== expected) {
675                this.printErrorLog(/*expectErrors*/ false, errors);
676                const errorMsg = "Actual number of errors (" + actual + ") does not match expected number (" + expected + ")";
677                Harness.IO.log(errorMsg);
678                this.raiseError(errorMsg);
679            }
680        }
681
682        public verifyEval(expr: string, value: any) {
683            const emit = this.languageService.getEmitOutput(this.activeFile.fileName);
684            if (emit.outputFiles.length !== 1) {
685                throw new Error("Expected exactly one output from emit of " + this.activeFile.fileName);
686            }
687
688            const evaluation = new Function(`${emit.outputFiles[0].text};\r\nreturn (${expr});`)(); // eslint-disable-line no-new-func
689            if (evaluation !== value) {
690                this.raiseError(`Expected evaluation of expression "${expr}" to equal "${value}", but got "${evaluation}"`);
691            }
692        }
693
694        public verifyGoToDefinitionIs(endMarker: ArrayOrSingle<string>) {
695            this.verifyGoToXWorker(toArray(endMarker), () => this.getGoToDefinition());
696        }
697
698        public verifyGoToDefinition(arg0: any, endMarkerNames?: ArrayOrSingle<string>) {
699            this.verifyGoToX(arg0, endMarkerNames, () => this.getGoToDefinitionAndBoundSpan());
700        }
701
702        private getGoToDefinition(): readonly ts.DefinitionInfo[] {
703            return this.languageService.getDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition)!;
704        }
705
706        private getGoToDefinitionAndBoundSpan(): ts.DefinitionInfoAndBoundSpan {
707            return this.languageService.getDefinitionAndBoundSpan(this.activeFile.fileName, this.currentCaretPosition)!;
708        }
709
710        public verifyGoToType(arg0: any, endMarkerNames?: ArrayOrSingle<string>) {
711            this.verifyGoToX(arg0, endMarkerNames, () =>
712                this.languageService.getTypeDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition));
713        }
714
715        private verifyGoToX(arg0: any, endMarkerNames: ArrayOrSingle<string> | undefined, getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined) {
716            if (endMarkerNames) {
717                this.verifyGoToXPlain(arg0, endMarkerNames, getDefs);
718            }
719            else if (ts.isArray(arg0)) {
720                const pairs = arg0 as readonly [ArrayOrSingle<string>, ArrayOrSingle<string>][];
721                for (const [start, end] of pairs) {
722                    this.verifyGoToXPlain(start, end, getDefs);
723                }
724            }
725            else {
726                const obj: { [startMarkerName: string]: ArrayOrSingle<string> } = arg0;
727                for (const startMarkerName in obj) {
728                    if (ts.hasProperty(obj, startMarkerName)) {
729                        this.verifyGoToXPlain(startMarkerName, obj[startMarkerName], getDefs);
730                    }
731                }
732            }
733        }
734
735        private verifyGoToXPlain(startMarkerNames: ArrayOrSingle<string>, endMarkerNames: ArrayOrSingle<string>, getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined) {
736            for (const start of toArray(startMarkerNames)) {
737                this.verifyGoToXSingle(start, endMarkerNames, getDefs);
738            }
739        }
740
741        public verifyGoToDefinitionForMarkers(markerNames: string[]) {
742            for (const markerName of markerNames) {
743                this.verifyGoToXSingle(`${markerName}Reference`, `${markerName}Definition`, () => this.getGoToDefinition());
744            }
745        }
746
747        private verifyGoToXSingle(startMarkerName: string, endMarkerNames: ArrayOrSingle<string>, getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined) {
748            this.goToMarker(startMarkerName);
749            this.verifyGoToXWorker(toArray(endMarkerNames), getDefs, startMarkerName);
750        }
751
752        private verifyGoToXWorker(endMarkers: readonly string[], getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined, startMarkerName?: string) {
753            const defs = getDefs();
754            let definitions: readonly ts.DefinitionInfo[];
755            let testName: string;
756
757            if (!defs || ts.isArray(defs)) {
758                definitions = defs as ts.DefinitionInfo[] || [];
759                testName = "goToDefinitions";
760            }
761            else {
762                this.verifyDefinitionTextSpan(defs, startMarkerName!);
763
764                definitions = defs.definitions!; // TODO: GH#18217
765                testName = "goToDefinitionsAndBoundSpan";
766            }
767
768            if (endMarkers.length !== definitions.length) {
769                this.raiseError(`${testName} failed - expected to find ${endMarkers.length} definitions but got ${definitions.length}`);
770            }
771
772            ts.zipWith(endMarkers, definitions, (endMarker, definition, i) => {
773                const marker = this.getMarkerByName(endMarker);
774                if (ts.comparePaths(marker.fileName, definition.fileName, /*ignoreCase*/ true) !== ts.Comparison.EqualTo || marker.position !== definition.textSpan.start) {
775                    const filesToDisplay = ts.deduplicate([marker.fileName, definition.fileName], ts.equateValues);
776                    const markers = [{ text: "EXPECTED", fileName: marker.fileName, position: marker.position }, { text: "ACTUAL", fileName: definition.fileName, position: definition.textSpan.start }];
777                    const text = filesToDisplay.map(fileName => {
778                        const markersToRender = markers.filter(m => m.fileName === fileName).sort((a, b) => b.position - a.position);
779                        let fileContent = this.getFileContent(fileName);
780                        for (const marker of markersToRender) {
781                            fileContent = fileContent.slice(0, marker.position) + `\x1b[1;4m/*${marker.text}*/\x1b[0;31m` + fileContent.slice(marker.position);
782                        }
783                        return `// @Filename: ${fileName}\n${fileContent}`;
784                    }).join("\n\n");
785
786                    this.raiseError(`${testName} failed for definition ${endMarker} (${i}): expected ${marker.fileName} at ${marker.position}, got ${definition.fileName} at ${definition.textSpan.start}\n\n${text}\n`);
787                }
788            });
789        }
790
791        private verifyDefinitionTextSpan(defs: ts.DefinitionInfoAndBoundSpan, startMarkerName: string) {
792            const range = this.testData.ranges.find(range => this.markerName(range.marker!) === startMarkerName);
793
794            if (!range && !defs.textSpan) {
795                return;
796            }
797
798            if (!range) {
799                const marker = this.getMarkerByName(startMarkerName);
800                const startFile = marker.fileName;
801                const fileContent = this.getFileContent(startFile);
802                const spanContent = fileContent.slice(defs.textSpan.start, ts.textSpanEnd(defs.textSpan));
803                const spanContentWithMarker = spanContent.slice(0, marker.position - defs.textSpan.start) + `/*${startMarkerName}*/` + spanContent.slice(marker.position - defs.textSpan.start);
804                const suggestedFileContent = (fileContent.slice(0, defs.textSpan.start) + `\x1b[1;4m[|${spanContentWithMarker}|]\x1b[0;31m` + fileContent.slice(ts.textSpanEnd(defs.textSpan)))
805                    .split(/\r?\n/).map(line => " ".repeat(6) + line).join(ts.sys.newLine);
806                this.raiseError(`goToDefinitionsAndBoundSpan failed. Found a starting TextSpan around '${spanContent}' in '${startFile}' (at position ${defs.textSpan.start}). `
807                    + `If this is the correct input span, put a fourslash range around it: \n\n${suggestedFileContent}\n`);
808            }
809            else {
810                this.assertTextSpanEqualsRange(defs.textSpan, range, "goToDefinitionsAndBoundSpan failed");
811            }
812        }
813
814        public verifyGetEmitOutputForCurrentFile(expected: string): void {
815            const emit = this.languageService.getEmitOutput(this.activeFile.fileName);
816            if (emit.outputFiles.length !== 1) {
817                throw new Error("Expected exactly one output from emit of " + this.activeFile.fileName);
818            }
819            const actual = emit.outputFiles[0].text;
820            if (actual !== expected) {
821                this.raiseError(`Expected emit output to be "${expected}", but got "${actual}"`);
822            }
823        }
824
825        public verifyGetEmitOutputContentsForCurrentFile(expected: ts.OutputFile[]): void {
826            const emit = this.languageService.getEmitOutput(this.activeFile.fileName);
827            assert.equal(emit.outputFiles.length, expected.length, "Number of emit output files");
828            ts.zipWith(emit.outputFiles, expected, (outputFile, expected) => {
829                assert.equal(outputFile.name, expected.name, "FileName");
830                assert.equal(outputFile.text, expected.text, "Content");
831            });
832        }
833
834        public verifyCompletions(options: FourSlashInterface.VerifyCompletionsOptions) {
835            if (options.marker === undefined) {
836                this.verifyCompletionsWorker(options);
837            }
838            else {
839                for (const marker of toArray(options.marker)) {
840                    this.goToMarker(marker);
841                    this.verifyCompletionsWorker(options);
842                }
843            }
844        }
845
846        private verifyCompletionsWorker(options: FourSlashInterface.VerifyCompletionsOptions): void {
847            const actualCompletions = this.getCompletionListAtCaret({ ...options.preferences, triggerCharacter: options.triggerCharacter })!;
848            if (!actualCompletions) {
849                if (ts.hasProperty(options, "exact") && (options.exact === undefined || ts.isArray(options.exact) && !options.exact.length)) {
850                    return;
851                }
852                this.raiseError(`No completions at position '${this.currentCaretPosition}'.`);
853            }
854
855            if (actualCompletions.isNewIdentifierLocation !== (options.isNewIdentifierLocation || false)) {
856                this.raiseError(`Expected 'isNewIdentifierLocation' to be ${options.isNewIdentifierLocation || false}, got ${actualCompletions.isNewIdentifierLocation}`);
857            }
858
859            if (ts.hasProperty(options, "isGlobalCompletion") && actualCompletions.isGlobalCompletion !== options.isGlobalCompletion) {
860                this.raiseError(`Expected 'isGlobalCompletion to be ${options.isGlobalCompletion}, got ${actualCompletions.isGlobalCompletion}`);
861            }
862
863            if (ts.hasProperty(options, "optionalReplacementSpan")) {
864                assert.deepEqual(
865                    actualCompletions.optionalReplacementSpan && actualCompletions.optionalReplacementSpan,
866                    options.optionalReplacementSpan && ts.createTextSpanFromRange(options.optionalReplacementSpan),
867                    "Expected 'optionalReplacementSpan' properties to match");
868            }
869
870            const nameToEntries = new ts.Map<string, ts.CompletionEntry[]>();
871            for (const entry of actualCompletions.entries) {
872                const entries = nameToEntries.get(entry.name);
873                if (!entries) {
874                    nameToEntries.set(entry.name, [entry]);
875                }
876                else {
877                    if (entries.some(e => e.source === entry.source)) {
878                        this.raiseError(`Duplicate completions for ${entry.name}`);
879                    }
880                    entries.push(entry);
881                }
882            }
883
884            if (ts.hasProperty(options, "exact")) {
885                ts.Debug.assert(!ts.hasProperty(options, "includes") && !ts.hasProperty(options, "excludes"));
886                if (options.exact === undefined) throw this.raiseError("Expected no completions");
887                this.verifyCompletionsAreExactly(actualCompletions.entries, toArray(options.exact), options.marker);
888            }
889            else {
890                if (options.includes) {
891                    for (const include of toArray(options.includes)) {
892                        const name = typeof include === "string" ? include : include.name;
893                        const found = nameToEntries.get(name);
894                        if (!found) throw this.raiseError(`Includes: completion '${name}' not found.`);
895                        assert(found.length === 1, `Must use 'exact' for multiple completions with same name: '${name}'`);
896                        this.verifyCompletionEntry(ts.first(found), include);
897                    }
898                }
899                if (options.excludes) {
900                    for (const exclude of toArray(options.excludes)) {
901                        assert(typeof exclude === "string");
902                        if (nameToEntries.has(exclude)) {
903                            this.raiseError(`Excludes: unexpected completion '${exclude}' found.`);
904                        }
905                    }
906                }
907            }
908        }
909
910        private verifyCompletionEntry(actual: ts.CompletionEntry, expected: FourSlashInterface.ExpectedCompletionEntry) {
911            expected = typeof expected === "string" ? { name: expected } : expected;
912
913            if (actual.insertText !== expected.insertText) {
914                this.raiseError(`Expected completion insert text to be ${expected.insertText}, got ${actual.insertText}`);
915            }
916            const convertedReplacementSpan = expected.replacementSpan && ts.createTextSpanFromRange(expected.replacementSpan);
917            if (convertedReplacementSpan?.length) {
918                try {
919                    assert.deepEqual(actual.replacementSpan, convertedReplacementSpan);
920                }
921                catch {
922                    this.raiseError(`Expected completion replacementSpan to be ${stringify(convertedReplacementSpan)}, got ${stringify(actual.replacementSpan)}`);
923                }
924            }
925
926            if (expected.kind !== undefined || expected.kindModifiers !== undefined) {
927                assert.equal(actual.kind, expected.kind, `Expected 'kind' for ${actual.name} to match`);
928                assert.equal(actual.kindModifiers, expected.kindModifiers || "", `Expected 'kindModifiers' for ${actual.name} to match`);
929            }
930            if (expected.isFromUncheckedFile !== undefined) {
931                assert.equal<boolean | undefined>(actual.isFromUncheckedFile, expected.isFromUncheckedFile, "Expected 'isFromUncheckedFile' properties to match");
932            }
933            if (expected.isPackageJsonImport !== undefined) {
934                assert.equal<boolean | undefined>(actual.isPackageJsonImport, expected.isPackageJsonImport, "Expected 'isPackageJsonImport' properties to match");
935            }
936
937            assert.equal(actual.hasAction, expected.hasAction, `Expected 'hasAction' properties to match`);
938            assert.equal(actual.isRecommended, expected.isRecommended, `Expected 'isRecommended' properties to match'`);
939            assert.equal(actual.source, expected.source, `Expected 'source' values to match`);
940            assert.equal(actual.sortText, expected.sortText || ts.Completions.SortText.LocationPriority, this.messageAtLastKnownMarker(`Actual entry: ${JSON.stringify(actual)}`));
941
942            if (expected.text !== undefined) {
943                const actualDetails = this.getCompletionEntryDetails(actual.name, actual.source)!;
944                assert.equal(ts.displayPartsToString(actualDetails.displayParts), expected.text, "Expected 'text' property to match 'displayParts' string");
945                assert.equal(ts.displayPartsToString(actualDetails.documentation), expected.documentation || "", "Expected 'documentation' property to match 'documentation' display parts string");
946                // TODO: GH#23587
947                // assert.equal(actualDetails.kind, actual.kind);
948                assert.equal(actualDetails.kindModifiers, actual.kindModifiers, "Expected 'kindModifiers' properties to match");
949                assert.equal(actualDetails.source && ts.displayPartsToString(actualDetails.source), expected.sourceDisplay, "Expected 'sourceDisplay' property to match 'source' display parts string");
950                assert.deepEqual(actualDetails.tags, expected.tags);
951            }
952            else {
953                assert(expected.documentation === undefined && expected.tags === undefined && expected.sourceDisplay === undefined, "If specifying completion details, should specify 'text'");
954            }
955        }
956
957        private verifyCompletionsAreExactly(actual: readonly ts.CompletionEntry[], expected: readonly FourSlashInterface.ExpectedCompletionEntry[], marker?: ArrayOrSingle<string | Marker>) {
958            // First pass: test that names are right. Then we'll test details.
959            assert.deepEqual(actual.map(a => a.name), expected.map(e => typeof e === "string" ? e : e.name), marker ? "At marker " + JSON.stringify(marker) : undefined);
960
961            ts.zipWith(actual, expected, (completion, expectedCompletion, index) => {
962                const name = typeof expectedCompletion === "string" ? expectedCompletion : expectedCompletion.name;
963                if (completion.name !== name) {
964                    this.raiseError(`${marker ? JSON.stringify(marker) : ""} Expected completion at index ${index} to be ${name}, got ${completion.name}`);
965                }
966                this.verifyCompletionEntry(completion, expectedCompletion);
967            });
968        }
969
970        /** Use `getProgram` instead of accessing this directly. */
971        private _program: ts.Program | undefined;
972        /** Use `getChecker` instead of accessing this directly. */
973        private _checker: ts.TypeChecker | undefined;
974
975        private getProgram(): ts.Program {
976            return this._program || (this._program = this.languageService.getProgram()!); // TODO: GH#18217
977        }
978
979        private getChecker() {
980            return this._checker || (this._checker = this.getProgram().getTypeChecker());
981        }
982
983        private getSourceFile(): ts.SourceFile {
984            const { fileName } = this.activeFile;
985            const result = this.getProgram().getSourceFile(fileName);
986            if (!result) {
987                throw new Error(`Could not get source file ${fileName}`);
988            }
989            return result;
990        }
991
992        private getNode(): ts.Node {
993            return ts.getTouchingPropertyName(this.getSourceFile(), this.currentCaretPosition);
994        }
995
996        private goToAndGetNode(range: Range): ts.Node {
997            this.goToRangeStart(range);
998            const node = this.getNode();
999            this.verifyRange("touching property name", range, node);
1000            return node;
1001        }
1002
1003        private verifyRange(desc: string, expected: ts.TextRange, actual: ts.Node) {
1004            const actualStart = actual.getStart();
1005            const actualEnd = actual.getEnd();
1006            if (actualStart !== expected.pos || actualEnd !== expected.end) {
1007                this.raiseError(`${desc} should be ${expected.pos}-${expected.end}, got ${actualStart}-${actualEnd}`);
1008            }
1009        }
1010
1011        private verifySymbol(symbol: ts.Symbol, declarationRanges: Range[]) {
1012            const { declarations } = symbol;
1013            if (declarations.length !== declarationRanges.length) {
1014                this.raiseError(`Expected to get ${declarationRanges.length} declarations, got ${declarations.length}`);
1015            }
1016
1017            ts.zipWith(declarations, declarationRanges, (decl, range) => {
1018                this.verifyRange("symbol declaration", range, decl);
1019            });
1020        }
1021
1022        public verifySymbolAtLocation(startRange: Range, declarationRanges: Range[]): void {
1023            const node = this.goToAndGetNode(startRange);
1024            const symbol = this.getChecker().getSymbolAtLocation(node)!;
1025            if (!symbol) {
1026                this.raiseError("Could not get symbol at location");
1027            }
1028            this.verifySymbol(symbol, declarationRanges);
1029        }
1030
1031        public symbolsInScope(range: Range): ts.Symbol[] {
1032            const node = this.goToAndGetNode(range);
1033            return this.getChecker().getSymbolsInScope(node, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace);
1034        }
1035
1036        public setTypesRegistry(map: ts.MapLike<void>): void {
1037            this.languageServiceAdapterHost.typesRegistry = new ts.Map(ts.getEntries(map));
1038        }
1039
1040        public verifyTypeOfSymbolAtLocation(range: Range, symbol: ts.Symbol, expected: string): void {
1041            const node = this.goToAndGetNode(range);
1042            const checker = this.getChecker();
1043            const type = checker.getTypeOfSymbolAtLocation(symbol, node);
1044
1045            const actual = checker.typeToString(type);
1046            if (actual !== expected) {
1047                this.raiseError(displayExpectedAndActualString(expected, actual));
1048            }
1049        }
1050
1051        private verifyDocumentHighlightsRespectFilesList(files: readonly string[]): void {
1052            const startFile = this.activeFile.fileName;
1053            for (const fileName of files) {
1054                const searchFileNames = startFile === fileName ? [startFile] : [startFile, fileName];
1055                const highlights = this.getDocumentHighlightsAtCurrentPosition(searchFileNames);
1056                if (highlights && !highlights.every(dh => ts.contains(searchFileNames, dh.fileName))) {
1057                    this.raiseError(`When asking for document highlights only in files ${searchFileNames}, got document highlights in ${unique(highlights, dh => dh.fileName)}`);
1058                }
1059            }
1060        }
1061
1062        public verifyReferenceGroups(starts: ArrayOrSingle<string> | ArrayOrSingle<Range>, parts: readonly FourSlashInterface.ReferenceGroup[]): void {
1063            interface ReferenceGroupJson {
1064                definition: string | { text: string, range: ts.TextSpan };
1065                references: ts.ReferenceEntry[];
1066            }
1067            interface RangeMarkerData {
1068                id?: string;
1069                isWriteAccess?: boolean,
1070                isDefinition?: boolean,
1071                isInString?: true,
1072                contextRangeIndex?: number,
1073                contextRangeDelta?: number,
1074                contextRangeId?: string
1075            }
1076            const fullExpected = ts.map<FourSlashInterface.ReferenceGroup, ReferenceGroupJson>(parts, ({ definition, ranges }) => ({
1077                definition: typeof definition === "string" ? definition : { ...definition, range: ts.createTextSpanFromRange(definition.range) },
1078                references: ranges.map<ts.ReferenceEntry>(r => {
1079                    const { isWriteAccess = false, isDefinition = false, isInString, contextRangeIndex, contextRangeDelta, contextRangeId } = (r.marker && r.marker.data || {}) as RangeMarkerData;
1080                    let contextSpan: ts.TextSpan | undefined;
1081                    if (contextRangeDelta !== undefined) {
1082                        const allRanges = this.getRanges();
1083                        const index = allRanges.indexOf(r);
1084                        if (index !== -1) {
1085                            contextSpan = ts.createTextSpanFromRange(allRanges[index + contextRangeDelta]);
1086                        }
1087                    }
1088                    else if (contextRangeId !== undefined) {
1089                        const allRanges = this.getRanges();
1090                        const contextRange = ts.find(allRanges, range => (range.marker?.data as RangeMarkerData)?.id === contextRangeId);
1091                        if (contextRange) {
1092                            contextSpan = ts.createTextSpanFromRange(contextRange);
1093                        }
1094                    }
1095                    else if (contextRangeIndex !== undefined) {
1096                        contextSpan = ts.createTextSpanFromRange(this.getRanges()[contextRangeIndex]);
1097                    }
1098                    return {
1099                        textSpan: ts.createTextSpanFromRange(r),
1100                        fileName: r.fileName,
1101                        ...(contextSpan ? { contextSpan } : undefined),
1102                        isWriteAccess,
1103                        isDefinition,
1104                        ...(isInString ? { isInString: true } : undefined),
1105                    };
1106                }),
1107            }));
1108
1109            for (const start of toArray<string | Range>(starts)) {
1110                this.goToMarkerOrRange(start);
1111                const fullActual = ts.map<ts.ReferencedSymbol, ReferenceGroupJson>(this.findReferencesAtCaret(), ({ definition, references }, i) => {
1112                    const text = definition.displayParts.map(d => d.text).join("");
1113                    return {
1114                        definition: fullExpected.length > i && typeof fullExpected[i].definition === "string" ? text : { text, range: definition.textSpan },
1115                        references,
1116                    };
1117                });
1118                this.assertObjectsEqual(fullActual, fullExpected);
1119
1120                if (parts) {
1121                    this.verifyDocumentHighlightsRespectFilesList(unique(ts.flatMap(parts, p => p.ranges), r => r.fileName));
1122                }
1123            }
1124        }
1125
1126        public verifyBaselineFindAllReferences(...markerNames: string[]) {
1127            ts.Debug.assert(markerNames.length > 0, "Must pass at least one marker name to `baselineFindAllReferences()`");
1128            const baseline = markerNames.map(markerName => {
1129                this.goToMarker(markerName);
1130                const marker = this.getMarkerByName(markerName);
1131                const references = this.languageService.findReferences(marker.fileName, marker.position);
1132                const refsByFile = references
1133                    ? ts.group(ts.sort(ts.flatMap(references, r => r.references), (a, b) => a.textSpan.start - b.textSpan.start), ref => ref.fileName)
1134                    : ts.emptyArray;
1135
1136                // Write input files
1137                const baselineContent = this.getBaselineContentForGroupedReferences(refsByFile, markerName);
1138
1139                // Write response JSON
1140                return baselineContent + JSON.stringify(references, undefined, 2);
1141            }).join("\n\n");
1142            Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(".baseline.jsonc"), baseline);
1143        }
1144
1145        public verifyBaselineGetFileReferences(fileName: string) {
1146            const references = this.languageService.getFileReferences(fileName);
1147            const refsByFile = references
1148                ? ts.group(ts.sort(references, (a, b) => a.textSpan.start - b.textSpan.start), ref => ref.fileName)
1149                : ts.emptyArray;
1150
1151            // Write input files
1152            let baselineContent = this.getBaselineContentForGroupedReferences(refsByFile);
1153
1154            // Write response JSON
1155            baselineContent += JSON.stringify(references, undefined, 2);
1156            Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(".baseline.jsonc"), baselineContent);
1157        }
1158
1159        private getBaselineContentForGroupedReferences(refsByFile: readonly (readonly ts.ReferenceEntry[])[], markerName?: string) {
1160            const marker = markerName !== undefined ? this.getMarkerByName(markerName) : undefined;
1161            let baselineContent = "";
1162            for (const group of refsByFile) {
1163                baselineContent += getBaselineContentForFile(group[0].fileName, this.getFileContent(group[0].fileName));
1164                baselineContent += "\n\n";
1165            }
1166            return baselineContent;
1167
1168            function getBaselineContentForFile(fileName: string, content: string) {
1169                let newContent = `=== ${fileName} ===\n`;
1170                let pos = 0;
1171                for (const { textSpan } of refsByFile.find(refs => refs[0].fileName === fileName) ?? ts.emptyArray) {
1172                    const end = textSpan.start + textSpan.length;
1173                    newContent += content.slice(pos, textSpan.start);
1174                    pos = textSpan.start;
1175                    // It's easier to read if the /*FIND ALL REFS*/ comment is outside the range markers, which makes
1176                    // this code a bit more verbose than it would be if I were less picky about the baseline format.
1177                    if (fileName === marker?.fileName && marker.position === textSpan.start) {
1178                        newContent += "/*FIND ALL REFS*/";
1179                        newContent += "[|";
1180                    }
1181                    else if (fileName === marker?.fileName && ts.textSpanContainsPosition(textSpan, marker.position)) {
1182                        newContent += "[|";
1183                        newContent += content.slice(pos, marker.position);
1184                        newContent += "/*FIND ALL REFS*/";
1185                        pos = marker.position;
1186                    }
1187                    else {
1188                        newContent += "[|";
1189                    }
1190                    newContent += content.slice(pos, end);
1191                    newContent += "|]";
1192                    pos = end;
1193                }
1194                if (marker?.fileName === fileName && marker.position >= pos) {
1195                    newContent += content.slice(pos, marker.position);
1196                    newContent += "/*FIND ALL REFS*/";
1197                    pos = marker.position;
1198                }
1199                newContent += content.slice(pos);
1200                return newContent.split(/\r?\n/).map(l => "// " + l).join("\n");
1201            }
1202        }
1203
1204        public verifyNoReferences(markerNameOrRange?: string | Range) {
1205            if (markerNameOrRange !== undefined) this.goToMarkerOrRange(markerNameOrRange);
1206            const refs = this.getReferencesAtCaret();
1207            if (refs && refs.length) {
1208                this.raiseError(`Expected getReferences to fail, but saw references: ${stringify(refs)}`);
1209            }
1210        }
1211
1212        /** @deprecated - use `verify.baselineFindAllReferences()` instead. */
1213        public verifyGetReferencesForServerTest(expected: readonly ts.ReferenceEntry[]): void {
1214            const refs = this.getReferencesAtCaret();
1215            assert.deepEqual<readonly ts.ReferenceEntry[] | undefined>(refs, expected);
1216        }
1217
1218        public verifySingleReferenceGroup(definition: FourSlashInterface.ReferenceGroupDefinition, ranges?: Range[] | string) {
1219            ranges = ts.isString(ranges) ? this.rangesByText().get(ranges)! : ranges || this.getRanges();
1220            this.verifyReferenceGroups(ranges, [{ definition, ranges }]);
1221        }
1222
1223        private assertObjectsEqual<T>(fullActual: T, fullExpected: T, msgPrefix = ""): void {
1224            const recur = <U>(actual: U, expected: U, path: string) => {
1225                const fail = (msg: string) => {
1226                    this.raiseError(`${msgPrefix} At ${path}: ${msg} ${displayExpectedAndActualString(stringify(fullExpected), stringify(fullActual))}`);
1227                };
1228
1229                if ((actual === undefined) !== (expected === undefined)) {
1230                    fail(`Expected ${stringify(expected)}, got ${stringify(actual)}`);
1231                }
1232
1233                for (const key in actual) {
1234                    if (ts.hasProperty(actual as any, key)) {
1235                        const ak = actual[key], ek = expected[key];
1236                        if (typeof ak === "object" && typeof ek === "object") {
1237                            recur(ak, ek, path ? path + "." + key : key);
1238                        }
1239                        else if (ak !== ek) {
1240                            fail(`Expected '${key}' to be '${stringify(ek)}', got '${stringify(ak)}'`);
1241                        }
1242                    }
1243                }
1244
1245                for (const key in expected) {
1246                    if (ts.hasProperty(expected as any, key)) {
1247                        if (!ts.hasProperty(actual as any, key)) {
1248                            fail(`${msgPrefix}Missing property '${key}'`);
1249                        }
1250                    }
1251                }
1252            };
1253
1254            if (fullActual === undefined || fullExpected === undefined) {
1255                if (fullActual === fullExpected) {
1256                    return;
1257                }
1258                this.raiseError(`${msgPrefix} ${displayExpectedAndActualString(stringify(fullExpected), stringify(fullActual))}`);
1259            }
1260            recur(fullActual, fullExpected, "");
1261
1262        }
1263
1264        public verifyDisplayPartsOfReferencedSymbol(expected: ts.SymbolDisplayPart[]) {
1265            const referencedSymbols = this.findReferencesAtCaret()!;
1266
1267            if (referencedSymbols.length === 0) {
1268                this.raiseError("No referenced symbols found at current caret position");
1269            }
1270            else if (referencedSymbols.length > 1) {
1271                this.raiseError("More than one referenced symbol found");
1272            }
1273
1274            assert.equal(TestState.getDisplayPartsJson(referencedSymbols[0].definition.displayParts),
1275                TestState.getDisplayPartsJson(expected), this.messageAtLastKnownMarker("referenced symbol definition display parts"));
1276        }
1277
1278        private configure(preferences: ts.UserPreferences) {
1279            if (this.testType === FourSlashTestType.Server) {
1280                (this.languageService as ts.server.SessionClient).configure(preferences);
1281            }
1282        }
1283
1284        private getCompletionListAtCaret(options?: ts.GetCompletionsAtPositionOptions): ts.CompletionInfo | undefined {
1285            if (options) {
1286                this.configure(options);
1287            }
1288            return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, options);
1289        }
1290
1291        private getCompletionEntryDetails(entryName: string, source?: string, preferences?: ts.UserPreferences): ts.CompletionEntryDetails | undefined {
1292            if (preferences) {
1293                this.configure(preferences);
1294            }
1295            return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source, preferences);
1296        }
1297
1298        private getReferencesAtCaret() {
1299            return this.languageService.getReferencesAtPosition(this.activeFile.fileName, this.currentCaretPosition);
1300        }
1301
1302        private findReferencesAtCaret() {
1303            return this.languageService.findReferences(this.activeFile.fileName, this.currentCaretPosition);
1304        }
1305
1306        public getSyntacticDiagnostics(expected: readonly FourSlashInterface.Diagnostic[]) {
1307            const diagnostics = this.languageService.getSyntacticDiagnostics(this.activeFile.fileName);
1308            this.testDiagnostics(expected, diagnostics, "error");
1309        }
1310
1311        public getSemanticDiagnostics(expected: readonly FourSlashInterface.Diagnostic[]) {
1312            const diagnostics = this.languageService.getSemanticDiagnostics(this.activeFile.fileName);
1313            this.testDiagnostics(expected, diagnostics, "error");
1314        }
1315
1316        public getSuggestionDiagnostics(expected: readonly FourSlashInterface.Diagnostic[]): void {
1317            this.testDiagnostics(expected, this.languageService.getSuggestionDiagnostics(this.activeFile.fileName), "suggestion");
1318        }
1319
1320        private testDiagnostics(expected: readonly FourSlashInterface.Diagnostic[], diagnostics: readonly ts.Diagnostic[], category: string) {
1321            assert.deepEqual(ts.realizeDiagnostics(diagnostics, "\n"), expected.map((e): ts.RealizedDiagnostic => {
1322                const range = e.range || this.getRangesInFile()[0];
1323                if (!range) {
1324                    this.raiseError("Must provide a range for each expected diagnostic, or have one range in the fourslash source.");
1325                }
1326                return {
1327                    message: e.message,
1328                    category,
1329                    code: e.code,
1330                    ...ts.createTextSpanFromRange(range),
1331                    reportsUnnecessary: e.reportsUnnecessary,
1332                    reportsDeprecated: e.reportsDeprecated
1333                };
1334            }));
1335        }
1336
1337        public verifyQuickInfoAt(markerName: string | Range, expectedText: string, expectedDocumentation?: string) {
1338            if (typeof markerName === "string") this.goToMarker(markerName);
1339            else this.goToRangeStart(markerName);
1340
1341            this.verifyQuickInfoString(expectedText, expectedDocumentation);
1342        }
1343
1344        public verifyQuickInfos(namesAndTexts: { [name: string]: string | [string, string] }) {
1345            for (const name in namesAndTexts) {
1346                if (ts.hasProperty(namesAndTexts, name)) {
1347                    const text = namesAndTexts[name];
1348                    if (ts.isArray(text)) {
1349                        assert(text.length === 2);
1350                        const [expectedText, expectedDocumentation] = text;
1351                        this.verifyQuickInfoAt(name, expectedText, expectedDocumentation);
1352                    }
1353                    else {
1354                        this.verifyQuickInfoAt(name, text);
1355                    }
1356                }
1357            }
1358        }
1359
1360        public verifyQuickInfoString(expectedText: string, expectedDocumentation?: string) {
1361            if (expectedDocumentation === "") {
1362                throw new Error("Use 'undefined' instead");
1363            }
1364            const actualQuickInfo = this.languageService.getQuickInfoAtPosition(this.activeFile.fileName, this.currentCaretPosition);
1365            const actualQuickInfoText = actualQuickInfo ? ts.displayPartsToString(actualQuickInfo.displayParts) : "";
1366            const actualQuickInfoDocumentation = actualQuickInfo ? ts.displayPartsToString(actualQuickInfo.documentation) : "";
1367
1368            assert.equal(actualQuickInfoText, expectedText, this.messageAtLastKnownMarker("quick info text"));
1369            assert.equal(actualQuickInfoDocumentation, expectedDocumentation || "", this.assertionMessageAtLastKnownMarker("quick info doc"));
1370        }
1371
1372        public verifyQuickInfoDisplayParts(kind: string, kindModifiers: string, textSpan: TextSpan,
1373            displayParts: ts.SymbolDisplayPart[],
1374            documentation: ts.SymbolDisplayPart[],
1375            tags: ts.JSDocTagInfo[] | undefined
1376        ) {
1377
1378            const actualQuickInfo = this.languageService.getQuickInfoAtPosition(this.activeFile.fileName, this.currentCaretPosition)!;
1379            assert.equal(actualQuickInfo.kind, kind, this.messageAtLastKnownMarker("QuickInfo kind"));
1380            assert.equal(actualQuickInfo.kindModifiers, kindModifiers, this.messageAtLastKnownMarker("QuickInfo kindModifiers"));
1381            assert.equal(JSON.stringify(actualQuickInfo.textSpan), JSON.stringify(textSpan), this.messageAtLastKnownMarker("QuickInfo textSpan"));
1382            assert.equal(TestState.getDisplayPartsJson(actualQuickInfo.displayParts), TestState.getDisplayPartsJson(displayParts), this.messageAtLastKnownMarker("QuickInfo displayParts"));
1383            assert.equal(TestState.getDisplayPartsJson(actualQuickInfo.documentation), TestState.getDisplayPartsJson(documentation), this.messageAtLastKnownMarker("QuickInfo documentation"));
1384            if (!actualQuickInfo.tags || !tags) {
1385                assert.equal(actualQuickInfo.tags, tags, this.messageAtLastKnownMarker("QuickInfo tags"));
1386            }
1387            else {
1388                assert.equal(actualQuickInfo.tags.length, tags.length, this.messageAtLastKnownMarker("QuickInfo tags"));
1389                ts.zipWith(tags, actualQuickInfo.tags, (expectedTag, actualTag) => {
1390                    assert.equal(expectedTag.name, actualTag.name);
1391                    assert.equal(expectedTag.text, actualTag.text, this.messageAtLastKnownMarker("QuickInfo tag " + actualTag.name));
1392                });
1393            }
1394        }
1395
1396        public verifyRangesAreRenameLocations(options?: Range[] | { findInStrings?: boolean, findInComments?: boolean, ranges?: Range[] }) {
1397            if (ts.isArray(options)) {
1398                this.verifyRenameLocations(options, options);
1399            }
1400            else {
1401                const ranges = options && options.ranges || this.getRanges();
1402                this.verifyRenameLocations(ranges, { ranges, ...options });
1403            }
1404        }
1405
1406        public verifyRenameLocations(startRanges: ArrayOrSingle<Range>, options: FourSlashInterface.RenameLocationsOptions) {
1407            interface RangeMarkerData {
1408                id?: string;
1409                contextRangeIndex?: number,
1410                contextRangeDelta?: number
1411                contextRangeId?: string;
1412            }
1413            const { findInStrings = false, findInComments = false, ranges = this.getRanges(), providePrefixAndSuffixTextForRename = true } = ts.isArray(options) ? { findInStrings: false, findInComments: false, ranges: options, providePrefixAndSuffixTextForRename: true } : options;
1414
1415            const _startRanges = toArray(startRanges);
1416            assert(_startRanges.length);
1417            for (const startRange of _startRanges) {
1418                this.goToRangeStart(startRange);
1419
1420                const renameInfo = this.languageService.getRenameInfo(this.activeFile.fileName, this.currentCaretPosition);
1421                if (!renameInfo.canRename) {
1422                    this.raiseError("Expected rename to succeed, but it actually failed.");
1423                    break;
1424                }
1425
1426                const references = this.languageService.findRenameLocations(
1427                    this.activeFile.fileName, this.currentCaretPosition, findInStrings, findInComments, providePrefixAndSuffixTextForRename);
1428
1429                const sort = (locations: readonly ts.RenameLocation[] | undefined) =>
1430                    locations && ts.sort(locations, (r1, r2) => ts.compareStringsCaseSensitive(r1.fileName, r2.fileName) || r1.textSpan.start - r2.textSpan.start);
1431                assert.deepEqual(sort(references), sort(ranges.map((rangeOrOptions): ts.RenameLocation => {
1432                    const { range, ...prefixSuffixText } = "range" in rangeOrOptions ? rangeOrOptions : { range: rangeOrOptions }; // eslint-disable-line no-in-operator
1433                    const { contextRangeIndex, contextRangeDelta, contextRangeId } = (range.marker && range.marker.data || {}) as RangeMarkerData;
1434                    let contextSpan: ts.TextSpan | undefined;
1435                    if (contextRangeDelta !== undefined) {
1436                        const allRanges = this.getRanges();
1437                        const index = allRanges.indexOf(range);
1438                        if (index !== -1) {
1439                            contextSpan = ts.createTextSpanFromRange(allRanges[index + contextRangeDelta]);
1440                        }
1441                    }
1442                    else if (contextRangeId !== undefined) {
1443                        const allRanges = this.getRanges();
1444                        const contextRange = ts.find(allRanges, range => (range.marker?.data as RangeMarkerData)?.id === contextRangeId);
1445                        if (contextRange) {
1446                            contextSpan = ts.createTextSpanFromRange(contextRange);
1447                        }
1448                    }
1449                    else if (contextRangeIndex !== undefined) {
1450                        contextSpan = ts.createTextSpanFromRange(this.getRanges()[contextRangeIndex]);
1451                    }
1452                    return {
1453                        fileName: range.fileName,
1454                        textSpan: ts.createTextSpanFromRange(range),
1455                        ...(contextSpan ? { contextSpan } : undefined),
1456                        ...prefixSuffixText
1457                    };
1458                })));
1459            }
1460        }
1461
1462        public baselineRename(marker: string, options: FourSlashInterface.RenameOptions) {
1463            const { fileName, position } = this.getMarkerByName(marker);
1464            const locations = this.languageService.findRenameLocations(
1465                fileName,
1466                position,
1467                options.findInStrings ?? false,
1468                options.findInComments ?? false,
1469                options.providePrefixAndSuffixTextForRename);
1470
1471            if (!locations) {
1472                this.raiseError(`baselineRename failed. Could not rename at the provided position.`);
1473            }
1474
1475            const renamesByFile = ts.group(locations, l => l.fileName);
1476            const baselineContent = renamesByFile.map(renames => {
1477                const { fileName } = renames[0];
1478                const sortedRenames = ts.sort(renames, (a, b) => b.textSpan.start - a.textSpan.start);
1479                let baselineFileContent = this.getFileContent(fileName);
1480                for (const { textSpan } of sortedRenames) {
1481                    const isOriginalSpan = fileName === this.activeFile.fileName && ts.textSpanIntersectsWithPosition(textSpan, position);
1482                    baselineFileContent =
1483                        baselineFileContent.slice(0, textSpan.start) +
1484                        (isOriginalSpan ? "[|RENAME|]" : "RENAME") +
1485                        baselineFileContent.slice(textSpan.start + textSpan.length);
1486                }
1487                return `/*====== ${fileName} ======*/\n\n${baselineFileContent}`;
1488            }).join("\n\n") + "\n";
1489
1490            Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(), baselineContent);
1491        }
1492
1493        public verifyQuickInfoExists(negative: boolean) {
1494            const actualQuickInfo = this.languageService.getQuickInfoAtPosition(this.activeFile.fileName, this.currentCaretPosition);
1495            if (negative) {
1496                if (actualQuickInfo) {
1497                    this.raiseError("verifyQuickInfoExists failed. Expected quick info NOT to exist");
1498                }
1499            }
1500            else {
1501                if (!actualQuickInfo) {
1502                    this.raiseError("verifyQuickInfoExists failed. Expected quick info to exist");
1503                }
1504            }
1505        }
1506
1507        public verifySignatureHelpPresence(expectPresent: boolean, triggerReason: ts.SignatureHelpTriggerReason | undefined, markers: readonly (string | Marker)[]) {
1508            if (markers.length) {
1509                for (const marker of markers) {
1510                    this.goToMarker(marker);
1511                    this.verifySignatureHelpPresence(expectPresent, triggerReason, ts.emptyArray);
1512                }
1513                return;
1514            }
1515            const actual = this.getSignatureHelp({ triggerReason });
1516            if (expectPresent !== !!actual) {
1517                if (actual) {
1518                    this.raiseError(`Expected no signature help, but got "${stringify(actual)}"`);
1519                }
1520                else {
1521                    this.raiseError("Expected signature help, but none was returned.");
1522                }
1523            }
1524        }
1525
1526        public verifySignatureHelp(optionses: readonly FourSlashInterface.VerifySignatureHelpOptions[]) {
1527            for (const options of optionses) {
1528                if (options.marker === undefined) {
1529                    this.verifySignatureHelpWorker(options);
1530                }
1531                else {
1532                    for (const marker of toArray(options.marker)) {
1533                        this.goToMarker(marker);
1534                        this.verifySignatureHelpWorker(options);
1535                    }
1536                }
1537            }
1538        }
1539
1540        private verifySignatureHelpWorker(options: FourSlashInterface.VerifySignatureHelpOptions) {
1541            const help = this.getSignatureHelp({ triggerReason: options.triggerReason })!;
1542            if (!help) {
1543                this.raiseError("Could not get a help signature");
1544            }
1545
1546            const selectedItem = help.items[options.overrideSelectedItemIndex ?? help.selectedItemIndex];
1547            // Argument index may exceed number of parameters
1548            const currentParameter = selectedItem.parameters[help.argumentIndex] as ts.SignatureHelpParameter | undefined;
1549
1550            assert.equal(help.items.length, options.overloadsCount || 1, this.assertionMessageAtLastKnownMarker("signature help overloads count"));
1551
1552            assert.equal(ts.displayPartsToString(selectedItem.documentation), options.docComment || "", this.assertionMessageAtLastKnownMarker("current signature help doc comment"));
1553
1554            if (options.text !== undefined) {
1555                assert.equal(
1556                    ts.displayPartsToString(selectedItem.prefixDisplayParts) +
1557                    selectedItem.parameters.map(p => ts.displayPartsToString(p.displayParts)).join(ts.displayPartsToString(selectedItem.separatorDisplayParts)) +
1558                    ts.displayPartsToString(selectedItem.suffixDisplayParts), options.text);
1559            }
1560            if (options.parameterName !== undefined) {
1561                assert.equal(currentParameter!.name, options.parameterName);
1562            }
1563            if (options.parameterSpan !== undefined) {
1564                assert.equal(ts.displayPartsToString(currentParameter!.displayParts), options.parameterSpan);
1565            }
1566            if (currentParameter) {
1567                assert.equal(ts.displayPartsToString(currentParameter.documentation), options.parameterDocComment || "", this.assertionMessageAtLastKnownMarker("current parameter Help DocComment"));
1568            }
1569            if (options.parameterCount !== undefined) {
1570                assert.equal(selectedItem.parameters.length, options.parameterCount);
1571            }
1572            if (options.argumentCount !== undefined) {
1573                assert.equal(help.argumentCount, options.argumentCount);
1574            }
1575
1576            assert.equal(selectedItem.isVariadic, !!options.isVariadic);
1577
1578            const actualTags = selectedItem.tags;
1579            assert.equal(actualTags.length, (options.tags || ts.emptyArray).length, this.assertionMessageAtLastKnownMarker("signature help tags"));
1580            ts.zipWith((options.tags || ts.emptyArray), actualTags, (expectedTag, actualTag) => {
1581                assert.equal(actualTag.name, expectedTag.name);
1582                assert.equal(actualTag.text, expectedTag.text, this.assertionMessageAtLastKnownMarker("signature help tag " + actualTag.name));
1583            });
1584
1585            const allKeys: readonly (keyof FourSlashInterface.VerifySignatureHelpOptions)[] = [
1586                "marker",
1587                "triggerReason",
1588                "overloadsCount",
1589                "docComment",
1590                "text",
1591                "parameterName",
1592                "parameterSpan",
1593                "parameterDocComment",
1594                "parameterCount",
1595                "isVariadic",
1596                "tags",
1597                "argumentCount",
1598                "overrideSelectedItemIndex"
1599            ];
1600            for (const key in options) {
1601                if (!ts.contains(allKeys, key)) {
1602                    ts.Debug.fail("Unexpected key " + key);
1603                }
1604            }
1605        }
1606
1607        private validate(name: string, expected: string | undefined, actual: string | undefined) {
1608            if (expected && expected !== actual) {
1609                this.raiseError("Expected " + name + " '" + expected + "'.  Got '" + actual + "' instead.");
1610            }
1611        }
1612
1613        public verifyRenameInfoSucceeded(displayName: string | undefined, fullDisplayName: string | undefined, kind: string | undefined, kindModifiers: string | undefined, fileToRename: string | undefined, expectedRange: Range | undefined, renameInfoOptions: ts.RenameInfoOptions | undefined): void {
1614            const renameInfo = this.languageService.getRenameInfo(this.activeFile.fileName, this.currentCaretPosition, renameInfoOptions || { allowRenameOfImportPath: true });
1615            if (!renameInfo.canRename) {
1616                throw this.raiseError("Rename did not succeed");
1617            }
1618
1619            this.validate("displayName", displayName, renameInfo.displayName);
1620            this.validate("fullDisplayName", fullDisplayName, renameInfo.fullDisplayName);
1621            this.validate("kind", kind, renameInfo.kind);
1622            this.validate("kindModifiers", kindModifiers, renameInfo.kindModifiers);
1623            this.validate("fileToRename", fileToRename, renameInfo.fileToRename);
1624
1625            if (!expectedRange) {
1626                if (this.getRanges().length !== 1) {
1627                    this.raiseError("Expected a single range to be selected in the test file.");
1628                }
1629                expectedRange = this.getRanges()[0];
1630            }
1631
1632            if (renameInfo.triggerSpan.start !== expectedRange.pos ||
1633                ts.textSpanEnd(renameInfo.triggerSpan) !== expectedRange.end) {
1634                this.raiseError("Expected triggerSpan [" + expectedRange.pos + "," + expectedRange.end + ").  Got [" +
1635                    renameInfo.triggerSpan.start + "," + ts.textSpanEnd(renameInfo.triggerSpan) + ") instead.");
1636            }
1637        }
1638
1639        public verifyRenameInfoFailed(message?: string, allowRenameOfImportPath?: boolean) {
1640            allowRenameOfImportPath = allowRenameOfImportPath === undefined ? true : allowRenameOfImportPath;
1641            const renameInfo = this.languageService.getRenameInfo(this.activeFile.fileName, this.currentCaretPosition, { allowRenameOfImportPath });
1642            if (renameInfo.canRename) {
1643                throw this.raiseError("Rename was expected to fail");
1644            }
1645            this.validate("error", message, renameInfo.localizedErrorMessage);
1646        }
1647
1648        private alignmentForExtraInfo = 50;
1649
1650        private spanLines(file: FourSlashFile, spanInfo: ts.TextSpan, { selection = false, fullLines = false, lineNumbers = false } = {}) {
1651            if (selection) {
1652                fullLines = true;
1653            }
1654
1655            let contextStartPos = spanInfo.start;
1656            let contextEndPos = contextStartPos + spanInfo.length;
1657            if (fullLines) {
1658                if (contextStartPos > 0) {
1659                    while (contextStartPos > 1) {
1660                        const ch = file.content.charCodeAt(contextStartPos - 1);
1661                        if (ch === ts.CharacterCodes.lineFeed || ch === ts.CharacterCodes.carriageReturn) {
1662                            break;
1663                        }
1664                        contextStartPos--;
1665                    }
1666                }
1667                if (contextEndPos < file.content.length) {
1668                    while (contextEndPos < file.content.length - 1) {
1669                        const ch = file.content.charCodeAt(contextEndPos);
1670                        if (ch === ts.CharacterCodes.lineFeed || ch === ts.CharacterCodes.carriageReturn) {
1671                            break;
1672                        }
1673                        contextEndPos++;
1674                    }
1675                }
1676            }
1677
1678            let contextString: string;
1679            let contextLineMap: number[];
1680            let contextStart: ts.LineAndCharacter;
1681            let contextEnd: ts.LineAndCharacter;
1682            let selectionStart: ts.LineAndCharacter;
1683            let selectionEnd: ts.LineAndCharacter;
1684            let lineNumberPrefixLength: number;
1685            if (lineNumbers) {
1686                contextString = file.content;
1687                contextLineMap = ts.computeLineStarts(contextString);
1688                contextStart = ts.computeLineAndCharacterOfPosition(contextLineMap, contextStartPos);
1689                contextEnd = ts.computeLineAndCharacterOfPosition(contextLineMap, contextEndPos);
1690                selectionStart = ts.computeLineAndCharacterOfPosition(contextLineMap, spanInfo.start);
1691                selectionEnd = ts.computeLineAndCharacterOfPosition(contextLineMap, ts.textSpanEnd(spanInfo));
1692                lineNumberPrefixLength = (contextEnd.line + 1).toString().length + 2;
1693            }
1694            else {
1695                contextString = file.content.substring(contextStartPos, contextEndPos);
1696                contextLineMap = ts.computeLineStarts(contextString);
1697                contextStart = { line: 0, character: 0 };
1698                contextEnd = { line: contextLineMap.length - 1, character: 0 };
1699                selectionStart = selection ? ts.computeLineAndCharacterOfPosition(contextLineMap, spanInfo.start - contextStartPos) : contextStart;
1700                selectionEnd = selection ? ts.computeLineAndCharacterOfPosition(contextLineMap, ts.textSpanEnd(spanInfo) - contextStartPos) : contextEnd;
1701                lineNumberPrefixLength = 0;
1702            }
1703
1704            const output: string[] = [];
1705            for (let lineNumber = contextStart.line; lineNumber <= contextEnd.line; lineNumber++) {
1706                const spanLine = contextString.substring(contextLineMap[lineNumber], contextLineMap[lineNumber + 1]);
1707                output.push(lineNumbers ? `${ts.padLeft(`${lineNumber + 1}: `, lineNumberPrefixLength)}${spanLine}` : spanLine);
1708                if (selection) {
1709                    if (lineNumber < selectionStart.line || lineNumber > selectionEnd.line) {
1710                        continue;
1711                    }
1712
1713                    const isEmpty = selectionStart.line === selectionEnd.line && selectionStart.character === selectionEnd.character;
1714                    const selectionPadLength = lineNumber === selectionStart.line ? selectionStart.character : 0;
1715                    const selectionPad = " ".repeat(selectionPadLength + lineNumberPrefixLength);
1716                    const selectionLength = isEmpty ? 0 : Math.max(lineNumber < selectionEnd.line ? spanLine.trimRight().length - selectionPadLength : selectionEnd.character - selectionPadLength, 1);
1717                    const selectionLine = isEmpty ? "<" : "^".repeat(selectionLength);
1718                    output.push(`${selectionPad}${selectionLine}`);
1719                }
1720            }
1721            return output;
1722        }
1723
1724        private spanInfoToString(spanInfo: ts.TextSpan, prefixString: string, file: FourSlashFile = this.activeFile) {
1725            let resultString = "SpanInfo: " + JSON.stringify(spanInfo);
1726            if (spanInfo) {
1727                const spanLines = this.spanLines(file, spanInfo);
1728                for (let i = 0; i < spanLines.length; i++) {
1729                    if (!i) {
1730                        resultString += "\n";
1731                    }
1732                    resultString += prefixString + spanLines[i];
1733                }
1734                resultString += "\n" + prefixString + ":=> (" + this.getLineColStringAtPosition(spanInfo.start, file) + ") to (" + this.getLineColStringAtPosition(ts.textSpanEnd(spanInfo), file) + ")";
1735            }
1736
1737            return resultString;
1738        }
1739
1740        private baselineCurrentFileLocations(getSpanAtPos: (pos: number) => ts.TextSpan): string {
1741            const fileLineMap = ts.computeLineStarts(this.activeFile.content);
1742            let nextLine = 0;
1743            let resultString = "";
1744            let currentLine: string;
1745            let previousSpanInfo: string | undefined;
1746            let startColumn: number | undefined;
1747            let length: number | undefined;
1748            const prefixString = "    >";
1749
1750            let pos = 0;
1751            const addSpanInfoString = () => {
1752                if (previousSpanInfo) {
1753                    resultString += currentLine;
1754                    let thisLineMarker = ts.repeatString(" ", startColumn!) + ts.repeatString("~", length!);
1755                    thisLineMarker += ts.repeatString(" ", this.alignmentForExtraInfo - thisLineMarker.length - prefixString.length + 1);
1756                    resultString += thisLineMarker;
1757                    resultString += "=> Pos: (" + (pos - length!) + " to " + (pos - 1) + ") ";
1758                    resultString += " " + previousSpanInfo;
1759                    previousSpanInfo = undefined;
1760                }
1761            };
1762
1763            for (; pos < this.activeFile.content.length; pos++) {
1764                if (pos === 0 || pos === fileLineMap[nextLine]) {
1765                    nextLine++;
1766                    addSpanInfoString();
1767                    if (resultString.length) {
1768                        resultString += "\n--------------------------------";
1769                    }
1770                    currentLine = "\n" + nextLine.toString() + ts.repeatString(" ", 3 - nextLine.toString().length) + ">" + this.activeFile.content.substring(pos, fileLineMap[nextLine]) + "\n    ";
1771                    startColumn = 0;
1772                    length = 0;
1773                }
1774                const spanInfo = this.spanInfoToString(getSpanAtPos(pos), prefixString);
1775                if (previousSpanInfo && previousSpanInfo !== spanInfo) {
1776                    addSpanInfoString();
1777                    previousSpanInfo = spanInfo;
1778                    startColumn = startColumn! + length!;
1779                    length = 1;
1780                }
1781                else {
1782                    previousSpanInfo = spanInfo;
1783                    length!++;
1784                }
1785            }
1786            addSpanInfoString();
1787            return resultString;
1788        }
1789
1790        public getBreakpointStatementLocation(pos: number) {
1791            return this.languageService.getBreakpointStatementAtPosition(this.activeFile.fileName, pos);
1792        }
1793
1794        public baselineCurrentFileBreakpointLocations() {
1795            const baselineFile = this.getBaselineFileNameForInternalFourslashFile().replace("breakpointValidation", "bpSpan");
1796            Harness.Baseline.runBaseline(baselineFile, this.baselineCurrentFileLocations(pos => this.getBreakpointStatementLocation(pos)!));
1797        }
1798
1799        private getEmitFiles(): readonly FourSlashFile[] {
1800            // Find file to be emitted
1801            const emitFiles: FourSlashFile[] = [];  // List of FourSlashFile that has emitThisFile flag on
1802
1803            const allFourSlashFiles = this.testData.files;
1804            for (const file of allFourSlashFiles) {
1805                if (file.fileOptions[MetadataOptionNames.emitThisFile] === "true") {
1806                    // Find a file with the flag emitThisFile turned on
1807                    emitFiles.push(file);
1808                }
1809            }
1810
1811            // If there is not emiThisFile flag specified in the test file, throw an error
1812            if (emitFiles.length === 0) {
1813                this.raiseError("No emitThisFile is specified in the test file");
1814            }
1815
1816            return emitFiles;
1817        }
1818
1819        public verifyGetEmitOutput(expectedOutputFiles: readonly string[]): void {
1820            const outputFiles = ts.flatMap(this.getEmitFiles(), e => this.languageService.getEmitOutput(e.fileName).outputFiles);
1821
1822            assert.deepEqual(outputFiles.map(f => f.name), expectedOutputFiles);
1823
1824            for (const { name, text } of outputFiles) {
1825                const fromTestFile = this.getFileContent(name);
1826                if (fromTestFile !== text) {
1827                    this.raiseError(`Emit output for ${name} is not as expected: ${showTextDiff(fromTestFile, text)}`);
1828                }
1829            }
1830        }
1831
1832        public baselineGetEmitOutput(): void {
1833            let resultString = "";
1834            // Loop through all the emittedFiles and emit them one by one
1835            for (const emitFile of this.getEmitFiles()) {
1836                const emitOutput = this.languageService.getEmitOutput(emitFile.fileName);
1837                // Print emitOutputStatus in readable format
1838                resultString += "EmitSkipped: " + emitOutput.emitSkipped + Harness.IO.newLine();
1839
1840                if (emitOutput.emitSkipped) {
1841                    resultString += "Diagnostics:" + Harness.IO.newLine();
1842                    const diagnostics = ts.getPreEmitDiagnostics(this.languageService.getProgram()!); // TODO: GH#18217
1843                    for (const diagnostic of diagnostics) {
1844                        if (!ts.isString(diagnostic.messageText)) {
1845                            resultString += this.flattenChainedMessage(diagnostic.messageText);
1846                        }
1847                        else {
1848                            resultString += "  " + diagnostic.messageText + Harness.IO.newLine();
1849                        }
1850                    }
1851                }
1852
1853                for (const outputFile of emitOutput.outputFiles) {
1854                    const fileName = "FileName : " + outputFile.name + Harness.IO.newLine();
1855                    resultString = resultString + Harness.IO.newLine() + fileName + outputFile.text;
1856                }
1857                resultString += Harness.IO.newLine();
1858            }
1859
1860            Harness.Baseline.runBaseline(ts.Debug.checkDefined(this.testData.globalOptions[MetadataOptionNames.baselineFile]), resultString);
1861        }
1862
1863        private flattenChainedMessage(diag: ts.DiagnosticMessageChain, indent = " ") {
1864            let result = "";
1865            result += indent + diag.messageText + Harness.IO.newLine();
1866            if (diag.next) {
1867                for (const kid of diag.next) {
1868                    result += this.flattenChainedMessage(kid, indent + " ");
1869                }
1870            }
1871            return result;
1872        }
1873
1874        public baselineSyntacticDiagnostics() {
1875            const files = this.getCompilerTestFiles();
1876            const result = this.getSyntacticDiagnosticBaselineText(files);
1877            Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(), result);
1878        }
1879
1880        private getCompilerTestFiles() {
1881            return ts.map(this.testData.files, ({ content, fileName }) => ({
1882                content, unitName: fileName
1883            }));
1884        }
1885
1886        public baselineSyntacticAndSemanticDiagnostics() {
1887            const files = this.getCompilerTestFiles();
1888            const result = this.getSyntacticDiagnosticBaselineText(files)
1889                + Harness.IO.newLine()
1890                + Harness.IO.newLine()
1891                + this.getSemanticDiagnosticBaselineText(files);
1892            Harness.Baseline.runBaseline(this.getBaselineFileNameForContainingTestFile(), result);
1893        }
1894
1895        private getSyntacticDiagnosticBaselineText(files: Harness.Compiler.TestFile[]) {
1896            const diagnostics = ts.flatMap(files,
1897                file => this.languageService.getSyntacticDiagnostics(file.unitName)
1898            );
1899            const result = `Syntactic Diagnostics for file '${this.originalInputFileName}':`
1900                + Harness.IO.newLine()
1901                + Harness.Compiler.getErrorBaseline(files, diagnostics, /*pretty*/ false);
1902            return result;
1903        }
1904
1905        private getSemanticDiagnosticBaselineText(files: Harness.Compiler.TestFile[]) {
1906            const diagnostics = ts.flatMap(files,
1907                file => this.languageService.getSemanticDiagnostics(file.unitName)
1908            );
1909            const result = `Semantic Diagnostics for file '${this.originalInputFileName}':`
1910                + Harness.IO.newLine()
1911                + Harness.Compiler.getErrorBaseline(files, diagnostics, /*pretty*/ false);
1912            return result;
1913        }
1914
1915        public baselineQuickInfo() {
1916            const baselineFile = this.getBaselineFileNameForInternalFourslashFile();
1917            Harness.Baseline.runBaseline(
1918                baselineFile,
1919                stringify(
1920                    this.testData.markers.map(marker => ({
1921                        marker,
1922                        quickInfo: this.languageService.getQuickInfoAtPosition(marker.fileName, marker.position)
1923                    }))));
1924        }
1925
1926        public baselineSmartSelection() {
1927            const n = "\n";
1928            const baselineFile = this.getBaselineFileNameForInternalFourslashFile();
1929            const markers = this.getMarkers();
1930            const fileContent = this.activeFile.content;
1931            const text = markers.map(marker => {
1932                const baselineContent = [fileContent.slice(0, marker.position) + "/**/" + fileContent.slice(marker.position) + n];
1933                let selectionRange: ts.SelectionRange | undefined = this.languageService.getSmartSelectionRange(this.activeFile.fileName, marker.position);
1934                while (selectionRange) {
1935                    const { textSpan } = selectionRange;
1936                    let masked = Array.from(fileContent).map((char, index) => {
1937                        const charCode = char.charCodeAt(0);
1938                        if (index >= textSpan.start && index < ts.textSpanEnd(textSpan)) {
1939                            return char === " " ? "•" : ts.isLineBreak(charCode) ? `↲${n}` : char;
1940                        }
1941                        return ts.isLineBreak(charCode) ? char : " ";
1942                    }).join("");
1943                    masked = masked.replace(/^\s*$\r?\n?/gm, ""); // Remove blank lines
1944                    const isRealCharacter = (char: string) => char !== "•" && char !== "↲" && !ts.isWhiteSpaceLike(char.charCodeAt(0));
1945                    const leadingWidth = Array.from(masked).findIndex(isRealCharacter);
1946                    const trailingWidth = ts.findLastIndex(Array.from(masked), isRealCharacter);
1947                    masked = masked.slice(0, leadingWidth)
1948                        + masked.slice(leadingWidth, trailingWidth).replace(/•/g, " ").replace(/↲/g, "")
1949                        + masked.slice(trailingWidth);
1950                    baselineContent.push(masked);
1951                    selectionRange = selectionRange.parent;
1952                }
1953                return baselineContent.join(fileContent.includes("\n") ? n + n : n);
1954            }).join(n.repeat(2) + "=".repeat(80) + n.repeat(2));
1955
1956            Harness.Baseline.runBaseline(baselineFile, text);
1957        }
1958
1959        public printBreakpointLocation(pos: number) {
1960            Harness.IO.log("\n**Pos: " + pos + " " + this.spanInfoToString(this.getBreakpointStatementLocation(pos)!, "  "));
1961        }
1962
1963        public printBreakpointAtCurrentLocation() {
1964            this.printBreakpointLocation(this.currentCaretPosition);
1965        }
1966
1967        public printCurrentParameterHelp() {
1968            const help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition, /*options*/ undefined);
1969            Harness.IO.log(stringify(help));
1970        }
1971
1972        public printCurrentQuickInfo() {
1973            const quickInfo = this.languageService.getQuickInfoAtPosition(this.activeFile.fileName, this.currentCaretPosition)!;
1974            Harness.IO.log("Quick Info: " + quickInfo.displayParts!.map(part => part.text).join(""));
1975        }
1976
1977        public printErrorList() {
1978            const syntacticErrors = this.languageService.getSyntacticDiagnostics(this.activeFile.fileName);
1979            const semanticErrors = this.languageService.getSemanticDiagnostics(this.activeFile.fileName);
1980            const errorList = ts.concatenate(syntacticErrors, semanticErrors);
1981            Harness.IO.log(`Error list (${errorList.length} errors)`);
1982
1983            if (errorList.length) {
1984                errorList.forEach(err => {
1985                    Harness.IO.log(
1986                        "start: " + err.start +
1987                        ", length: " + err.length +
1988                        ", message: " + ts.flattenDiagnosticMessageText(err.messageText, Harness.IO.newLine()));
1989                });
1990            }
1991        }
1992
1993        public printCurrentFileState(showWhitespace: boolean, makeCaretVisible: boolean) {
1994            for (const file of this.testData.files) {
1995                const active = (this.activeFile === file);
1996                Harness.IO.log(`=== Script (${file.fileName}) ${(active ? "(active, cursor at |)" : "")} ===`);
1997                let content = this.getFileContent(file.fileName);
1998                if (active) {
1999                    content = content.substr(0, this.currentCaretPosition) + (makeCaretVisible ? "|" : "") + content.substr(this.currentCaretPosition);
2000                }
2001                if (showWhitespace) {
2002                    content = makeWhitespaceVisible(content);
2003                }
2004                Harness.IO.log(content);
2005            }
2006        }
2007
2008        public printCurrentSignatureHelp() {
2009            const help = this.getSignatureHelp(ts.emptyOptions)!;
2010            Harness.IO.log(stringify(help.items[help.selectedItemIndex]));
2011        }
2012
2013        private getBaselineFileNameForInternalFourslashFile(ext = ".baseline") {
2014            return this.testData.globalOptions[MetadataOptionNames.baselineFile] ||
2015                ts.getBaseFileName(this.activeFile.fileName).replace(ts.Extension.Ts, ext);
2016        }
2017
2018        private getBaselineFileNameForContainingTestFile(ext = ".baseline") {
2019            return ts.getBaseFileName(this.originalInputFileName).replace(ts.Extension.Ts, ext);
2020        }
2021
2022        private getSignatureHelp({ triggerReason }: FourSlashInterface.VerifySignatureHelpOptions): ts.SignatureHelpItems | undefined {
2023            return this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition, {
2024                triggerReason
2025            });
2026        }
2027
2028        public printCompletionListMembers(preferences: ts.UserPreferences | undefined) {
2029            const completions = this.getCompletionListAtCaret(preferences);
2030            this.printMembersOrCompletions(completions);
2031        }
2032
2033        private printMembersOrCompletions(info: ts.CompletionInfo | undefined) {
2034            if (info === undefined) { return "No completion info."; }
2035            const { entries } = info;
2036
2037            function pad(s: string, length: number) {
2038                return s + new Array(length - s.length + 1).join(" ");
2039            }
2040            function max<T>(arr: T[], selector: (x: T) => number): number {
2041                return arr.reduce((prev, x) => Math.max(prev, selector(x)), 0);
2042            }
2043            const longestNameLength = max(entries, m => m.name.length);
2044            const longestKindLength = max(entries, m => m.kind.length);
2045            entries.sort((m, n) => m.sortText > n.sortText ? 1 : m.sortText < n.sortText ? -1 : m.name > n.name ? 1 : m.name < n.name ? -1 : 0);
2046            const membersString = entries.map(m => `${pad(m.name, longestNameLength)} ${pad(m.kind, longestKindLength)} ${m.kindModifiers} ${m.isRecommended ? "recommended " : ""}${m.source === undefined ? "" : m.source}`).join("\n");
2047            Harness.IO.log(membersString);
2048        }
2049
2050        public printContext() {
2051            ts.forEach(this.languageServiceAdapterHost.getFilenames(), Harness.IO.log);
2052        }
2053
2054        public deleteChar(count = 1) {
2055            let offset = this.currentCaretPosition;
2056            const ch = "";
2057
2058            const checkCadence = (count >> 2) + 1;
2059
2060            for (let i = 0; i < count; i++) {
2061                this.editScriptAndUpdateMarkers(this.activeFile.fileName, offset, offset + 1, ch);
2062
2063                if (i % checkCadence === 0) {
2064                    this.checkPostEditInvariants();
2065                }
2066
2067                // Handle post-keystroke formatting
2068                if (this.enableFormatting) {
2069                    const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeSettings);
2070                    if (edits.length) {
2071                        offset += this.applyEdits(this.activeFile.fileName, edits);
2072                    }
2073                }
2074            }
2075
2076            this.checkPostEditInvariants();
2077        }
2078
2079        public replace(start: number, length: number, text: string) {
2080            this.editScriptAndUpdateMarkers(this.activeFile.fileName, start, start + length, text);
2081            this.checkPostEditInvariants();
2082        }
2083
2084        public deleteLineRange(startIndex: number, endIndexInclusive: number) {
2085            const startPos = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: startIndex, character: 0 });
2086            const endPos = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: endIndexInclusive + 1, character: 0 });
2087            this.replace(startPos, endPos - startPos, "");
2088        }
2089
2090        public deleteCharBehindMarker(count = 1) {
2091            let offset = this.currentCaretPosition;
2092            const ch = "";
2093            const checkCadence = (count >> 2) + 1;
2094
2095            for (let i = 0; i < count; i++) {
2096                this.currentCaretPosition--;
2097                offset--;
2098                this.editScriptAndUpdateMarkers(this.activeFile.fileName, offset, offset + 1, ch);
2099
2100                if (i % checkCadence === 0) {
2101                    this.checkPostEditInvariants();
2102                }
2103
2104                // Don't need to examine formatting because there are no formatting changes on backspace.
2105            }
2106
2107            this.checkPostEditInvariants();
2108        }
2109
2110        // Enters lines of text at the current caret position
2111        public type(text: string, highFidelity = false) {
2112            let offset = this.currentCaretPosition;
2113            const prevChar = " ";
2114            const checkCadence = (text.length >> 2) + 1;
2115            const selection = this.getSelection();
2116            this.replace(selection.pos, selection.end - selection.pos, "");
2117
2118            for (let i = 0; i < text.length; i++) {
2119                const ch = text.charAt(i);
2120                this.editScriptAndUpdateMarkers(this.activeFile.fileName, offset, offset, ch);
2121                if (highFidelity) {
2122                    this.languageService.getBraceMatchingAtPosition(this.activeFile.fileName, offset);
2123                }
2124
2125                this.currentCaretPosition++;
2126                offset++;
2127
2128                if (highFidelity) {
2129                    if (ch === "(" || ch === "," || ch === "<") {
2130                        /* Signature help*/
2131                        this.languageService.getSignatureHelpItems(this.activeFile.fileName, offset, {
2132                            triggerReason: {
2133                                kind: "characterTyped",
2134                                triggerCharacter: ch
2135                            }
2136                        });
2137                    }
2138                    else if (prevChar === " " && /A-Za-z_/.test(ch)) {
2139                        /* Completions */
2140                        this.languageService.getCompletionsAtPosition(this.activeFile.fileName, offset, ts.emptyOptions);
2141                    }
2142
2143                    if (i % checkCadence === 0) {
2144                        this.checkPostEditInvariants();
2145                    }
2146                }
2147
2148                // Handle post-keystroke formatting
2149                if (this.enableFormatting) {
2150                    const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeSettings);
2151                    if (edits.length) {
2152                        offset += this.applyEdits(this.activeFile.fileName, edits);
2153                    }
2154                }
2155            }
2156
2157            this.checkPostEditInvariants();
2158        }
2159
2160        // Enters text as if the user had pasted it
2161        public paste(text: string) {
2162            const start = this.currentCaretPosition;
2163            this.editScriptAndUpdateMarkers(this.activeFile.fileName, this.currentCaretPosition, this.currentCaretPosition, text);
2164            this.checkPostEditInvariants();
2165            const offset = this.currentCaretPosition += text.length;
2166
2167            // Handle formatting
2168            if (this.enableFormatting) {
2169                const edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, offset, this.formatCodeSettings);
2170                if (edits.length) {
2171                    this.applyEdits(this.activeFile.fileName, edits);
2172                }
2173            }
2174
2175
2176            this.checkPostEditInvariants();
2177        }
2178
2179        private checkPostEditInvariants() {
2180            if (this.testType !== FourSlashTestType.Native) {
2181                // getSourcefile() results can not be serialized. Only perform these verifications
2182                // if running against a native LS object.
2183                return;
2184            }
2185
2186            const incrementalSourceFile = this.languageService.getNonBoundSourceFile(this.activeFile.fileName);
2187            Utils.assertInvariants(incrementalSourceFile, /*parent:*/ undefined);
2188
2189            const incrementalSyntaxDiagnostics = incrementalSourceFile.parseDiagnostics;
2190
2191            // Check syntactic structure
2192            const content = this.getFileContent(this.activeFile.fileName);
2193
2194            const referenceSourceFile = ts.createLanguageServiceSourceFile(
2195                this.activeFile.fileName, createScriptSnapShot(content), ts.ScriptTarget.Latest, /*version:*/ "0", /*setNodeParents:*/ false);
2196            const referenceSyntaxDiagnostics = referenceSourceFile.parseDiagnostics;
2197
2198            Utils.assertDiagnosticsEquals(incrementalSyntaxDiagnostics, referenceSyntaxDiagnostics);
2199            Utils.assertStructuralEquals(incrementalSourceFile, referenceSourceFile);
2200        }
2201
2202        /**
2203         * @returns The number of characters added to the file as a result of the edits.
2204         * May be negative.
2205         */
2206        private applyEdits(fileName: string, edits: readonly ts.TextChange[]): number {
2207            let runningOffset = 0;
2208
2209            forEachTextChange(edits, edit => {
2210                const offsetStart = edit.span.start;
2211                const offsetEnd = offsetStart + edit.span.length;
2212                this.editScriptAndUpdateMarkers(fileName, offsetStart, offsetEnd, edit.newText);
2213                const editDelta = edit.newText.length - edit.span.length;
2214                if (offsetStart <= this.currentCaretPosition) {
2215                    if (offsetEnd <= this.currentCaretPosition) {
2216                        // The entirety of the edit span falls before the caret position, shift the caret accordingly
2217                        this.currentCaretPosition += editDelta;
2218                    }
2219                    else {
2220                        // The span being replaced includes the caret position, place the caret at the beginning of the span
2221                        this.currentCaretPosition = offsetStart;
2222                    }
2223                }
2224                runningOffset += editDelta;
2225            });
2226
2227            return runningOffset;
2228        }
2229
2230        public copyFormatOptions(): ts.FormatCodeSettings {
2231            return ts.clone(this.formatCodeSettings);
2232        }
2233
2234        public setFormatOptions(formatCodeOptions: ts.FormatCodeOptions | ts.FormatCodeSettings): ts.FormatCodeSettings {
2235            const oldFormatCodeOptions = this.formatCodeSettings;
2236            this.formatCodeSettings = ts.toEditorSettings(formatCodeOptions);
2237            if (this.testType === FourSlashTestType.Server) {
2238                (this.languageService as ts.server.SessionClient).setFormattingOptions(this.formatCodeSettings);
2239            }
2240            return oldFormatCodeOptions;
2241        }
2242
2243        public formatDocument() {
2244            const edits = this.languageService.getFormattingEditsForDocument(this.activeFile.fileName, this.formatCodeSettings);
2245            this.applyEdits(this.activeFile.fileName, edits);
2246        }
2247
2248        public formatSelection(start: number, end: number) {
2249            const edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, end, this.formatCodeSettings);
2250            this.applyEdits(this.activeFile.fileName, edits);
2251        }
2252
2253        public formatOnType(pos: number, key: string) {
2254            const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, pos, key, this.formatCodeSettings);
2255            this.applyEdits(this.activeFile.fileName, edits);
2256        }
2257
2258        private editScriptAndUpdateMarkers(fileName: string, editStart: number, editEnd: number, newText: string) {
2259            this.languageServiceAdapterHost.editScript(fileName, editStart, editEnd, newText);
2260            if (this.assertTextConsistent) {
2261                this.assertTextConsistent(fileName);
2262            }
2263            for (const marker of this.testData.markers) {
2264                if (marker.fileName === fileName) {
2265                    marker.position = updatePosition(marker.position, editStart, editEnd, newText);
2266                }
2267            }
2268
2269            for (const range of this.testData.ranges) {
2270                if (range.fileName === fileName) {
2271                    range.pos = updatePosition(range.pos, editStart, editEnd, newText);
2272                    range.end = updatePosition(range.end, editStart, editEnd, newText);
2273                }
2274            }
2275            this.testData.rangesByText = undefined;
2276        }
2277
2278        private removeWhitespace(text: string): string {
2279            return text.replace(/\s/g, "");
2280        }
2281
2282        public goToBOF() {
2283            this.goToPosition(0);
2284        }
2285
2286        public goToEOF() {
2287            const len = this.getFileContent(this.activeFile.fileName).length;
2288            this.goToPosition(len);
2289        }
2290
2291        private goToMarkerOrRange(markerOrRange: string | Range) {
2292            if (typeof markerOrRange === "string") {
2293                this.goToMarker(markerOrRange);
2294            }
2295            else {
2296                this.goToRangeStart(markerOrRange);
2297            }
2298        }
2299
2300        public goToRangeStart({ fileName, pos }: Range) {
2301            this.openFile(fileName);
2302            this.goToPosition(pos);
2303        }
2304
2305        public goToTypeDefinition(definitionIndex: number) {
2306            const definitions = this.languageService.getTypeDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition)!;
2307            if (!definitions || !definitions.length) {
2308                this.raiseError("goToTypeDefinition failed - expected to find at least one definition location but got 0");
2309            }
2310
2311            if (definitionIndex >= definitions.length) {
2312                this.raiseError(`goToTypeDefinition failed - definitionIndex value (${definitionIndex}) exceeds definition list size (${definitions.length})`);
2313            }
2314
2315            const definition = definitions[definitionIndex];
2316            this.openFile(definition.fileName);
2317            this.currentCaretPosition = definition.textSpan.start;
2318        }
2319
2320        public verifyTypeDefinitionsCount(negative: boolean, expectedCount: number) {
2321            const assertFn = negative ? assert.notEqual : assert.equal;
2322
2323            const definitions = this.languageService.getTypeDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition);
2324            const actualCount = definitions && definitions.length || 0;
2325
2326            assertFn(actualCount, expectedCount, this.messageAtLastKnownMarker("Type definitions Count"));
2327        }
2328
2329        public verifyImplementationListIsEmpty(negative: boolean) {
2330            const implementations = this.languageService.getImplementationAtPosition(this.activeFile.fileName, this.currentCaretPosition);
2331
2332            if (negative) {
2333                assert.isTrue(implementations && implementations.length > 0, "Expected at least one implementation but got 0");
2334            }
2335            else {
2336                assert.isUndefined(implementations, "Expected implementation list to be empty but implementations returned");
2337            }
2338        }
2339
2340        public verifyGoToDefinitionName(expectedName: string, expectedContainerName: string) {
2341            const definitions = this.languageService.getDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition);
2342            const actualDefinitionName = definitions && definitions.length ? definitions[0].name : "";
2343            const actualDefinitionContainerName = definitions && definitions.length ? definitions[0].containerName : "";
2344            assert.equal(actualDefinitionName, expectedName, this.messageAtLastKnownMarker("Definition Info Name"));
2345            assert.equal(actualDefinitionContainerName, expectedContainerName, this.messageAtLastKnownMarker("Definition Info Container Name"));
2346        }
2347
2348        public goToImplementation() {
2349            const implementations = this.languageService.getImplementationAtPosition(this.activeFile.fileName, this.currentCaretPosition)!;
2350            if (!implementations || !implementations.length) {
2351                this.raiseError("goToImplementation failed - expected to find at least one implementation location but got 0");
2352            }
2353            if (implementations.length > 1) {
2354                this.raiseError(`goToImplementation failed - more than 1 implementation returned (${implementations.length})`);
2355            }
2356
2357            const implementation = implementations[0];
2358            this.openFile(implementation.fileName);
2359            this.currentCaretPosition = implementation.textSpan.start;
2360        }
2361
2362        public verifyRangesInImplementationList(markerName: string) {
2363            this.goToMarker(markerName);
2364            const implementations: readonly ImplementationLocationInformation[] = this.languageService.getImplementationAtPosition(this.activeFile.fileName, this.currentCaretPosition)!;
2365            if (!implementations || !implementations.length) {
2366                this.raiseError("verifyRangesInImplementationList failed - expected to find at least one implementation location but got 0");
2367            }
2368
2369            const duplicate = findDuplicatedElement(implementations, ts.documentSpansEqual);
2370            if (duplicate) {
2371                const { textSpan, fileName } = duplicate;
2372                this.raiseError(`Duplicate implementations returned for range (${textSpan.start}, ${ts.textSpanEnd(textSpan)}) in ${fileName}`);
2373            }
2374
2375            const ranges = this.getRanges();
2376
2377            if (!ranges || !ranges.length) {
2378                this.raiseError("verifyRangesInImplementationList failed - expected to find at least one range in test source");
2379            }
2380
2381            const unsatisfiedRanges: Range[] = [];
2382
2383            const delayedErrors: string[] = [];
2384            for (const range of ranges) {
2385                const length = range.end - range.pos;
2386                const matchingImpl = ts.find(implementations, impl =>
2387                    range.fileName === impl.fileName && range.pos === impl.textSpan.start && length === impl.textSpan.length);
2388                if (matchingImpl) {
2389                    if (range.marker && range.marker.data) {
2390                        const expected = <{ displayParts?: ts.SymbolDisplayPart[], parts: string[], kind?: string }>range.marker.data;
2391                        if (expected.displayParts) {
2392                            if (!ts.arrayIsEqualTo(expected.displayParts, matchingImpl.displayParts, displayPartIsEqualTo)) {
2393                                delayedErrors.push(`Mismatched display parts: expected ${JSON.stringify(expected.displayParts)}, actual ${JSON.stringify(matchingImpl.displayParts)}`);
2394                            }
2395                        }
2396                        else if (expected.parts) {
2397                            const actualParts = matchingImpl.displayParts.map(p => p.text);
2398                            if (!ts.arrayIsEqualTo(expected.parts, actualParts)) {
2399                                delayedErrors.push(`Mismatched non-tagged display parts: expected ${JSON.stringify(expected.parts)}, actual ${JSON.stringify(actualParts)}`);
2400                            }
2401                        }
2402                        if (expected.kind !== undefined) {
2403                            if (expected.kind !== matchingImpl.kind) {
2404                                delayedErrors.push(`Mismatched kind: expected ${JSON.stringify(expected.kind)}, actual ${JSON.stringify(matchingImpl.kind)}`);
2405                            }
2406                        }
2407                    }
2408
2409                    matchingImpl.matched = true;
2410                }
2411                else {
2412                    unsatisfiedRanges.push(range);
2413                }
2414            }
2415            if (delayedErrors.length) {
2416                this.raiseError(delayedErrors.join("\n"));
2417            }
2418
2419            const unmatchedImplementations = implementations.filter(impl => !impl.matched);
2420            if (unmatchedImplementations.length || unsatisfiedRanges.length) {
2421                let error = "Not all ranges or implementations are satisfied";
2422                if (unsatisfiedRanges.length) {
2423                    error += "\nUnsatisfied ranges:";
2424                    for (const range of unsatisfiedRanges) {
2425                        error += `\n    (${range.pos}, ${range.end}) in ${range.fileName}: ${this.rangeText(range)}`;
2426                    }
2427                }
2428
2429                if (unmatchedImplementations.length) {
2430                    error += "\nUnmatched implementations:";
2431                    for (const impl of unmatchedImplementations) {
2432                        const end = impl.textSpan.start + impl.textSpan.length;
2433                        error += `\n    (${impl.textSpan.start}, ${end}) in ${impl.fileName}: ${this.getFileContent(impl.fileName).slice(impl.textSpan.start, end)}`;
2434                    }
2435                }
2436                this.raiseError(error);
2437            }
2438
2439            function displayPartIsEqualTo(a: ts.SymbolDisplayPart, b: ts.SymbolDisplayPart): boolean {
2440                return a.kind === b.kind && a.text === b.text;
2441            }
2442        }
2443
2444        public getMarkers(): Marker[] {
2445            //  Return a copy of the list
2446            return this.testData.markers.slice(0);
2447        }
2448
2449        public getMarkerNames(): string[] {
2450            return ts.arrayFrom(this.testData.markerPositions.keys());
2451        }
2452
2453        public getRanges(): Range[] {
2454            return this.testData.ranges;
2455        }
2456
2457        public getRangesInFile(fileName = this.activeFile.fileName) {
2458            return this.getRanges().filter(r => r.fileName === fileName);
2459        }
2460
2461        public rangesByText(): ts.ESMap<string, Range[]> {
2462            if (this.testData.rangesByText) return this.testData.rangesByText;
2463            const result = ts.createMultiMap<Range>();
2464            this.testData.rangesByText = result;
2465            for (const range of this.getRanges()) {
2466                const text = this.rangeText(range);
2467                result.add(text, range);
2468            }
2469            return result;
2470        }
2471
2472        private rangeText({ fileName, pos, end }: Range): string {
2473            return this.getFileContent(fileName).slice(pos, end);
2474        }
2475
2476        public verifyCaretAtMarker(markerName = "") {
2477            const pos = this.getMarkerByName(markerName);
2478            if (pos.fileName !== this.activeFile.fileName) {
2479                throw new Error(`verifyCaretAtMarker failed - expected to be in file "${pos.fileName}", but was in file "${this.activeFile.fileName}"`);
2480            }
2481            if (pos.position !== this.currentCaretPosition) {
2482                throw new Error(`verifyCaretAtMarker failed - expected to be at marker "/*${markerName}*/, but was at position ${this.currentCaretPosition}(${this.getLineColStringAtPosition(this.currentCaretPosition)})`);
2483            }
2484        }
2485
2486        private getIndentation(fileName: string, position: number, indentStyle: ts.IndentStyle, baseIndentSize: number): number {
2487            const formatOptions = ts.clone(this.formatCodeSettings);
2488            formatOptions.indentStyle = indentStyle;
2489            formatOptions.baseIndentSize = baseIndentSize;
2490            return this.languageService.getIndentationAtPosition(fileName, position, formatOptions);
2491        }
2492
2493        public verifyIndentationAtCurrentPosition(numberOfSpaces: number, indentStyle: ts.IndentStyle = ts.IndentStyle.Smart, baseIndentSize = 0) {
2494            const actual = this.getIndentation(this.activeFile.fileName, this.currentCaretPosition, indentStyle, baseIndentSize);
2495            const lineCol = this.getLineColStringAtPosition(this.currentCaretPosition);
2496            if (actual !== numberOfSpaces) {
2497                this.raiseError(`verifyIndentationAtCurrentPosition failed at ${lineCol} - expected: ${numberOfSpaces}, actual: ${actual}`);
2498            }
2499        }
2500
2501        public verifyIndentationAtPosition(fileName: string, position: number, numberOfSpaces: number, indentStyle: ts.IndentStyle = ts.IndentStyle.Smart, baseIndentSize = 0) {
2502            const actual = this.getIndentation(fileName, position, indentStyle, baseIndentSize);
2503            const lineCol = this.getLineColStringAtPosition(position);
2504            if (actual !== numberOfSpaces) {
2505                this.raiseError(`verifyIndentationAtPosition failed at ${lineCol} - expected: ${numberOfSpaces}, actual: ${actual}`);
2506            }
2507        }
2508
2509        public verifyCurrentLineContent(text: string) {
2510            const actual = this.getCurrentLineContent();
2511            if (actual !== text) {
2512                throw new Error("verifyCurrentLineContent\n" + displayExpectedAndActualString(text, actual, /* quoted */ true));
2513            }
2514        }
2515
2516        public verifyCurrentFileContent(text: string) {
2517            this.verifyFileContent(this.activeFile.fileName, text);
2518        }
2519
2520        private verifyFileContent(fileName: string, text: string) {
2521            const actual = this.getFileContent(fileName);
2522            if (actual !== text) {
2523                throw new Error(`verifyFileContent failed:\n${showTextDiff(text, actual)}`);
2524            }
2525        }
2526
2527        public verifyFormatDocumentChangesNothing(): void {
2528            const { fileName } = this.activeFile;
2529            const before = this.getFileContent(fileName);
2530            this.formatDocument();
2531            this.verifyFileContent(fileName, before);
2532        }
2533
2534        public verifyTextAtCaretIs(text: string) {
2535            const actual = this.getFileContent(this.activeFile.fileName).substring(this.currentCaretPosition, this.currentCaretPosition + text.length);
2536            if (actual !== text) {
2537                throw new Error("verifyTextAtCaretIs\n" + displayExpectedAndActualString(text, actual, /* quoted */ true));
2538            }
2539        }
2540
2541        public verifyCurrentNameOrDottedNameSpanText(text: string) {
2542            const span = this.languageService.getNameOrDottedNameSpan(this.activeFile.fileName, this.currentCaretPosition, this.currentCaretPosition);
2543            if (!span) {
2544                return this.raiseError("verifyCurrentNameOrDottedNameSpanText\n" + displayExpectedAndActualString("\"" + text + "\"", "undefined"));
2545            }
2546
2547            const actual = this.getFileContent(this.activeFile.fileName).substring(span.start, ts.textSpanEnd(span));
2548            if (actual !== text) {
2549                this.raiseError("verifyCurrentNameOrDottedNameSpanText\n" + displayExpectedAndActualString(text, actual, /* quoted */ true));
2550            }
2551        }
2552
2553        private getNameOrDottedNameSpan(pos: number) {
2554            return this.languageService.getNameOrDottedNameSpan(this.activeFile.fileName, pos, pos);
2555        }
2556
2557        public baselineCurrentFileNameOrDottedNameSpans() {
2558            Harness.Baseline.runBaseline(
2559                this.testData.globalOptions[MetadataOptionNames.baselineFile],
2560                this.baselineCurrentFileLocations(pos => this.getNameOrDottedNameSpan(pos)!));
2561        }
2562
2563        public printNameOrDottedNameSpans(pos: number) {
2564            Harness.IO.log(this.spanInfoToString(this.getNameOrDottedNameSpan(pos)!, "**"));
2565        }
2566
2567        private classificationToIdentifier(classification: number){
2568
2569            const tokenTypes: string[] = [];
2570            tokenTypes[ts.classifier.v2020.TokenType.class] = "class";
2571            tokenTypes[ts.classifier.v2020.TokenType.enum] = "enum";
2572            tokenTypes[ts.classifier.v2020.TokenType.interface] = "interface";
2573            tokenTypes[ts.classifier.v2020.TokenType.namespace] = "namespace";
2574            tokenTypes[ts.classifier.v2020.TokenType.typeParameter] = "typeParameter";
2575            tokenTypes[ts.classifier.v2020.TokenType.type] = "type";
2576            tokenTypes[ts.classifier.v2020.TokenType.parameter] = "parameter";
2577            tokenTypes[ts.classifier.v2020.TokenType.variable] = "variable";
2578            tokenTypes[ts.classifier.v2020.TokenType.enumMember] = "enumMember";
2579            tokenTypes[ts.classifier.v2020.TokenType.property] = "property";
2580            tokenTypes[ts.classifier.v2020.TokenType.function] = "function";
2581            tokenTypes[ts.classifier.v2020.TokenType.member] = "member";
2582
2583            const tokenModifiers: string[] = [];
2584            tokenModifiers[ts.classifier.v2020.TokenModifier.async] = "async";
2585            tokenModifiers[ts.classifier.v2020.TokenModifier.declaration] = "declaration";
2586            tokenModifiers[ts.classifier.v2020.TokenModifier.readonly] = "readonly";
2587            tokenModifiers[ts.classifier.v2020.TokenModifier.static] = "static";
2588            tokenModifiers[ts.classifier.v2020.TokenModifier.local] = "local";
2589            tokenModifiers[ts.classifier.v2020.TokenModifier.defaultLibrary] = "defaultLibrary";
2590
2591
2592            function getTokenTypeFromClassification(tsClassification: number): number | undefined {
2593                if (tsClassification > ts.classifier.v2020.TokenEncodingConsts.modifierMask) {
2594                    return (tsClassification >> ts.classifier.v2020.TokenEncodingConsts.typeOffset) - 1;
2595                }
2596                return undefined;
2597            }
2598
2599            function getTokenModifierFromClassification(tsClassification: number) {
2600                return tsClassification & ts.classifier.v2020.TokenEncodingConsts.modifierMask;
2601            }
2602
2603            const typeIdx = getTokenTypeFromClassification(classification) || 0;
2604            const modSet = getTokenModifierFromClassification(classification);
2605
2606            return [tokenTypes[typeIdx], ...tokenModifiers.filter((_, i) => modSet & 1 << i)].join(".");
2607        }
2608
2609        private verifyClassifications(expected: { classificationType: string | number, text?: string; textSpan?: TextSpan }[], actual: (ts.ClassifiedSpan | ts.ClassifiedSpan2020)[] , sourceFileText: string) {
2610            if (actual.length !== expected.length) {
2611                this.raiseError("verifyClassifications failed - expected total classifications to be " + expected.length +
2612                    ", but was " + actual.length +
2613                    jsonMismatchString());
2614            }
2615
2616            ts.zipWith(expected, actual, (expectedClassification, actualClassification) => {
2617                const expectedType = expectedClassification.classificationType;
2618                const actualType = typeof actualClassification.classificationType === "number"  ? this.classificationToIdentifier(actualClassification.classificationType) : actualClassification.classificationType;
2619
2620                if (expectedType !== actualType) {
2621                    this.raiseError("verifyClassifications failed - expected classifications type to be " +
2622                        expectedType + ", but was " +
2623                        actualType +
2624                        jsonMismatchString());
2625                }
2626
2627                const expectedSpan = expectedClassification.textSpan;
2628                const actualSpan = actualClassification.textSpan;
2629
2630                if (expectedSpan) {
2631                    const expectedLength = expectedSpan.end - expectedSpan.start;
2632
2633                    if (expectedSpan.start !== actualSpan.start || expectedLength !== actualSpan.length) {
2634                        this.raiseError("verifyClassifications failed - expected span of text to be " +
2635                            "{start=" + expectedSpan.start + ", length=" + expectedLength + "}, but was " +
2636                            "{start=" + actualSpan.start + ", length=" + actualSpan.length + "}" +
2637                            jsonMismatchString());
2638                    }
2639                }
2640
2641                const actualText = this.activeFile.content.substr(actualSpan.start, actualSpan.length);
2642                if (expectedClassification.text !== actualText) {
2643                    this.raiseError("verifyClassifications failed - expected classified text to be " +
2644                        expectedClassification.text + ", but was " +
2645                        actualText +
2646                        jsonMismatchString());
2647                }
2648            });
2649
2650            function jsonMismatchString() {
2651                const showActual = actual.map(({ classificationType, textSpan }) =>
2652                    ({ classificationType, text: sourceFileText.slice(textSpan.start, textSpan.start + textSpan.length) }));
2653                return Harness.IO.newLine() +
2654                    "expected: '" + Harness.IO.newLine() + stringify(expected) + "'" + Harness.IO.newLine() +
2655                    "actual:   '" + Harness.IO.newLine() + stringify(showActual) + "'";
2656            }
2657        }
2658
2659        public verifyProjectInfo(expected: string[]) {
2660            if (this.testType === FourSlashTestType.Server) {
2661                const actual = (<ts.server.SessionClient>this.languageService).getProjectInfo(
2662                    this.activeFile.fileName,
2663                    /* needFileNameList */ true
2664                );
2665                assert.equal(
2666                    expected.join(","),
2667                    actual.fileNames!.map(file => {
2668                        return file.replace(this.basePath + "/", "");
2669                    }).join(",")
2670                );
2671            }
2672        }
2673
2674        public replaceWithSemanticClassifications(format: ts.SemanticClassificationFormat.TwentyTwenty) {
2675            const actual = this.languageService.getSemanticClassifications(this.activeFile.fileName,
2676                ts.createTextSpan(0, this.activeFile.content.length), format);
2677            const replacement = [`const c2 = classification("2020");`,`verify.semanticClassificationsAre("2020",`];
2678            for (const a of actual) {
2679                const identifier = this.classificationToIdentifier(a.classificationType as number);
2680                const text = this.activeFile.content.slice(a.textSpan.start, a.textSpan.start + a.textSpan.length);
2681                replacement.push(`    c2.semanticToken("${identifier}", "${text}"), `);
2682            };
2683            replacement.push(");");
2684
2685            throw new Error("You need to change the source code of fourslash test to use replaceWithSemanticClassifications");
2686
2687            // const fs = require("fs");
2688            // const testfilePath = this.originalInputFileName.slice(1);
2689            // const testfile = fs.readFileSync(testfilePath, "utf8");
2690            // const newfile = testfile.replace("verify.replaceWithSemanticClassifications(\"2020\")", replacement.join("\n"));
2691            // fs.writeFileSync(testfilePath, newfile);
2692        }
2693
2694
2695        public verifySemanticClassifications(format: ts.SemanticClassificationFormat, expected: { classificationType: string | number; text?: string }[]) {
2696            const actual = this.languageService.getSemanticClassifications(this.activeFile.fileName,
2697                ts.createTextSpan(0, this.activeFile.content.length), format);
2698            this.verifyClassifications(expected, actual, this.activeFile.content);
2699        }
2700
2701        public verifySyntacticClassifications(expected: { classificationType: string; text: string }[]) {
2702            const actual = this.languageService.getSyntacticClassifications(this.activeFile.fileName,
2703                ts.createTextSpan(0, this.activeFile.content.length));
2704
2705            this.verifyClassifications(expected, actual, this.activeFile.content);
2706        }
2707
2708        public printOutliningSpans() {
2709            const spans = this.languageService.getOutliningSpans(this.activeFile.fileName);
2710            Harness.IO.log(`Outlining spans (${spans.length} items)\nResults:`);
2711            Harness.IO.log(stringify(spans));
2712            this.printOutliningSpansInline(spans);
2713        }
2714
2715        private printOutliningSpansInline(spans: ts.OutliningSpan[]) {
2716            const allSpanInsets = [] as { text: string, pos: number }[];
2717            let annotated = this.activeFile.content;
2718            ts.forEach(spans, span => {
2719                allSpanInsets.push({ text: "[|", pos: span.textSpan.start });
2720                allSpanInsets.push({ text: "|]", pos: span.textSpan.start + span.textSpan.length });
2721            });
2722
2723            const reverseSpans = allSpanInsets.sort((l, r) => r.pos - l.pos);
2724            ts.forEach(reverseSpans, span => {
2725               annotated = annotated.slice(0, span.pos) + span.text + annotated.slice(span.pos);
2726            });
2727            Harness.IO.log(`\nMockup:\n${annotated}`);
2728        }
2729
2730        public verifyOutliningSpans(spans: Range[], kind?: "comment" | "region" | "code" | "imports") {
2731            const actual = this.languageService.getOutliningSpans(this.activeFile.fileName);
2732
2733            const filterActual = ts.filter(actual, f => kind === undefined ? true : f.kind === kind);
2734            if (filterActual.length !== spans.length) {
2735                this.raiseError(`verifyOutliningSpans failed - expected total spans to be ${spans.length}, but was ${actual.length}\n\nFound Spans:\n\n${this.printOutliningSpansInline(actual)}`);
2736            }
2737
2738            ts.zipWith(spans, filterActual, (expectedSpan, actualSpan, i) => {
2739                if (expectedSpan.pos !== actualSpan.textSpan.start || expectedSpan.end !== ts.textSpanEnd(actualSpan.textSpan)) {
2740                    return this.raiseError(`verifyOutliningSpans failed - span ${(i + 1)} expected: (${expectedSpan.pos},${expectedSpan.end}),  actual: (${actualSpan.textSpan.start},${ts.textSpanEnd(actualSpan.textSpan)})`);
2741                }
2742                if (kind !== undefined && actualSpan.kind !== kind) {
2743                    return this.raiseError(`verifyOutliningSpans failed - span ${(i + 1)} expected kind: ('${kind}'),  actual: ('${actualSpan.kind}')`);
2744                }
2745            });
2746        }
2747
2748        public verifyOutliningHintSpans(spans: Range[]) {
2749            const actual = this.languageService.getOutliningSpans(this.activeFile.fileName);
2750
2751            if (actual.length !== spans.length) {
2752                this.raiseError(`verifyOutliningHintSpans failed - expected total spans to be ${spans.length}, but was ${actual.length}`);
2753            }
2754
2755            ts.zipWith(spans, actual, (expectedSpan, actualSpan, i) => {
2756                if (expectedSpan.pos !== actualSpan.hintSpan.start || expectedSpan.end !== ts.textSpanEnd(actualSpan.hintSpan)) {
2757                    return this.raiseError(`verifyOutliningSpans failed - span ${(i + 1)} expected: (${expectedSpan.pos},${expectedSpan.end}),  actual: (${actualSpan.hintSpan.start},${ts.textSpanEnd(actualSpan.hintSpan)})`);
2758                }
2759            });
2760        }
2761
2762        public verifyTodoComments(descriptors: string[], spans: Range[]) {
2763            const actual = this.languageService.getTodoComments(this.activeFile.fileName,
2764                descriptors.map(d => { return { text: d, priority: 0 }; }));
2765
2766            if (actual.length !== spans.length) {
2767                this.raiseError(`verifyTodoComments failed - expected total spans to be ${spans.length}, but was ${actual.length}`);
2768            }
2769
2770            ts.zipWith(spans, actual, (expectedSpan, actualComment, i) => {
2771                const actualCommentSpan = ts.createTextSpan(actualComment.position, actualComment.message.length);
2772
2773                if (expectedSpan.pos !== actualCommentSpan.start || expectedSpan.end !== ts.textSpanEnd(actualCommentSpan)) {
2774                    this.raiseError(`verifyOutliningSpans failed - span ${(i + 1)} expected: (${expectedSpan.pos},${expectedSpan.end}),  actual: (${actualCommentSpan.start},${ts.textSpanEnd(actualCommentSpan)})`);
2775                }
2776            });
2777        }
2778
2779        /**
2780         * Finds and applies a code action corresponding to the supplied parameters.
2781         * If index is undefined, applies the unique code action available.
2782         * @param errorCode The error code that generated the code action.
2783         * @param index The nth (0-index-based) codeaction available generated by errorCode.
2784         */
2785        public getAndApplyCodeActions(errorCode?: number, index?: number) {
2786            const fileName = this.activeFile.fileName;
2787            const fixes = this.getCodeFixes(fileName, errorCode);
2788            if (index === undefined) {
2789                if (!(fixes && fixes.length === 1)) {
2790                    this.raiseError(`Should find exactly one codefix, but ${fixes ? fixes.length : "none"} found. ${fixes ? fixes.map(a => `${Harness.IO.newLine()} "${a.description}"`) : ""}`);
2791                }
2792                index = 0;
2793            }
2794            else {
2795                if (!(fixes && fixes.length >= index + 1)) {
2796                    this.raiseError(`Should find at least ${index + 1} codefix(es), but ${fixes ? fixes.length : "none"} found.`);
2797                }
2798            }
2799
2800            this.applyChanges(fixes[index].changes);
2801        }
2802
2803        public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
2804            this.goToMarker(markerName);
2805
2806            const details = this.getCompletionEntryDetails(options.name, options.source, options.preferences);
2807            if (!details) {
2808                const completions = this.getCompletionListAtCaret(options.preferences)?.entries;
2809                const matchingName = completions?.filter(e => e.name === options.name);
2810                const detailMessage = matchingName?.length
2811                    ? `\n  Found ${matchingName.length} with name '${options.name}' from source(s) ${matchingName.map(e => `'${e.source}'`).join(", ")}.`
2812                    : ` (In fact, there were no completions with name '${options.name}' at all.)`;
2813                return this.raiseError(`No completions were found for the given name, source, and preferences.` + detailMessage);
2814            }
2815            const codeActions = details.codeActions;
2816            if (codeActions?.length !== 1) {
2817                this.raiseError(`Expected one code action, got ${codeActions?.length ?? 0}`);
2818            }
2819            const codeAction = ts.first(codeActions);
2820
2821            if (codeAction.description !== options.description) {
2822                this.raiseError(`Expected description to be:\n${options.description}\ngot:\n${codeActions[0].description}`);
2823            }
2824            this.applyChanges(codeAction.changes);
2825
2826            this.verifyNewContentAfterChange(options, ts.flatMap(codeActions, a => a.changes.map(c => c.fileName)));
2827        }
2828
2829        public verifyRangeIs(expectedText: string, includeWhiteSpace?: boolean) {
2830            this.verifyTextMatches(this.rangeText(this.getOnlyRange()), !!includeWhiteSpace, expectedText);
2831        }
2832
2833        private getOnlyRange() {
2834            const ranges = this.getRanges();
2835            if (ranges.length !== 1) {
2836                this.raiseError("Exactly one range should be specified in the testfile.");
2837            }
2838            return ts.first(ranges);
2839        }
2840
2841        private verifyTextMatches(actualText: string, includeWhitespace: boolean, expectedText: string) {
2842            const removeWhitespace = (s: string): string => includeWhitespace ? s : this.removeWhitespace(s);
2843            if (removeWhitespace(actualText) !== removeWhitespace(expectedText)) {
2844                this.raiseError(`Actual range text doesn't match expected text.\n${showTextDiff(expectedText, actualText)}`);
2845            }
2846        }
2847
2848        /**
2849         * Compares expected text to the text that would be in the sole range
2850         * (ie: [|...|]) in the file after applying the codefix sole codefix
2851         * in the source file.
2852         */
2853        public verifyRangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number) {
2854            this.getAndApplyCodeActions(errorCode, index);
2855            this.verifyRangeIs(expectedText, includeWhiteSpace);
2856        }
2857
2858        public verifyCodeFixAll({ fixId, fixAllDescription, newFileContent, commands: expectedCommands }: FourSlashInterface.VerifyCodeFixAllOptions): void {
2859            const fixWithId = ts.find(this.getCodeFixes(this.activeFile.fileName), a => a.fixId === fixId);
2860            ts.Debug.assert(fixWithId !== undefined, "No available code fix has the expected id. Fix All is not available if there is only one potentially fixable diagnostic present.", () =>
2861                `Expected '${fixId}'. Available actions:\n${ts.mapDefined(this.getCodeFixes(this.activeFile.fileName), a => `${a.fixName} (${a.fixId || "no fix id"})`).join("\n")}`);
2862            ts.Debug.assertEqual(fixWithId.fixAllDescription, fixAllDescription);
2863
2864            const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings, ts.emptyOptions);
2865            assert.deepEqual<readonly {}[] | undefined>(commands, expectedCommands);
2866            this.verifyNewContent({ newFileContent }, changes);
2867        }
2868
2869        public verifyCodeFix(options: FourSlashInterface.VerifyCodeFixOptions) {
2870            const fileName = this.activeFile.fileName;
2871            const actions = this.getCodeFixes(fileName, options.errorCode, options.preferences);
2872            let index = options.index;
2873            if (index === undefined) {
2874                if (!(actions && actions.length === 1)) {
2875                    this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found. ${actions ? actions.map(a => `${Harness.IO.newLine()} "${a.description}"`) : ""}`);
2876                }
2877                index = 0;
2878            }
2879            else {
2880                if (!(actions && actions.length >= index + 1)) {
2881                    this.raiseError(`Should find at least ${index + 1} codefix(es), but ${actions ? actions.length : "none"} found.`);
2882                }
2883            }
2884
2885            const action = actions[index];
2886
2887            if (typeof options.description === "string") {
2888                assert.equal(action.description, options.description);
2889            }
2890            else if (Array.isArray(options.description)) {
2891                const description = ts.formatStringFromArgs(options.description[0], options.description, 1);
2892                assert.equal(action.description, description);
2893            }
2894            else {
2895                assert.match(action.description, templateToRegExp(options.description.template));
2896            }
2897            assert.deepEqual(action.commands, options.commands);
2898
2899            if (options.applyChanges) {
2900                for (const change of action.changes) {
2901                    this.applyEdits(change.fileName, change.textChanges);
2902                }
2903                this.verifyNewContentAfterChange(options, action.changes.map(c => c.fileName));
2904            }
2905            else {
2906                this.verifyNewContent(options, action.changes);
2907            }
2908        }
2909
2910        private verifyNewContent({ newFileContent, newRangeContent }: FourSlashInterface.NewContentOptions, changes: readonly ts.FileTextChanges[]): void {
2911            if (newRangeContent !== undefined) {
2912                assert(newFileContent === undefined);
2913                assert(changes.length === 1, "Affected 0 or more than 1 file, must use 'newFileContent' instead of 'newRangeContent'");
2914                const change = ts.first(changes);
2915                assert(change.fileName = this.activeFile.fileName);
2916                const newText = ts.textChanges.applyChanges(this.getFileContent(this.activeFile.fileName), change.textChanges);
2917                const newRange = updateTextRangeForTextChanges(this.getOnlyRange(), change.textChanges);
2918                const actualText = newText.slice(newRange.pos, newRange.end);
2919                this.verifyTextMatches(actualText, /*includeWhitespace*/ true, newRangeContent);
2920            }
2921            else {
2922                if (newFileContent === undefined) throw ts.Debug.fail();
2923                if (typeof newFileContent !== "object") newFileContent = { [this.activeFile.fileName]: newFileContent };
2924                for (const change of changes) {
2925                    const expectedNewContent = newFileContent[change.fileName];
2926                    if (expectedNewContent === undefined) {
2927                        ts.Debug.fail(`Did not expect a change in ${change.fileName}`);
2928                    }
2929                    const oldText = this.tryGetFileContent(change.fileName);
2930                    ts.Debug.assert(!!change.isNewFile === (oldText === undefined));
2931                    const newContent = change.isNewFile ? ts.first(change.textChanges).newText : ts.textChanges.applyChanges(oldText!, change.textChanges);
2932                    this.verifyTextMatches(newContent, /*includeWhitespace*/ true, expectedNewContent);
2933                }
2934                for (const newFileName in newFileContent) {
2935                    ts.Debug.assert(changes.some(c => c.fileName === newFileName), "No change in file", () => newFileName);
2936                }
2937            }
2938        }
2939
2940        private verifyNewContentAfterChange({ newFileContent, newRangeContent }: FourSlashInterface.NewContentOptions, changedFiles: readonly string[]) {
2941            const assertedChangedFiles = !newFileContent || typeof newFileContent === "string"
2942                ? [this.activeFile.fileName]
2943                : ts.getOwnKeys(newFileContent);
2944            assert.deepEqual(assertedChangedFiles, changedFiles);
2945
2946            if (newFileContent !== undefined) {
2947                assert(!newRangeContent);
2948                if (typeof newFileContent === "string") {
2949                    this.verifyCurrentFileContent(newFileContent);
2950                }
2951                else {
2952                    for (const fileName in newFileContent) {
2953                        this.verifyFileContent(fileName, newFileContent[fileName]);
2954                    }
2955                }
2956            }
2957            else {
2958                this.verifyRangeIs(newRangeContent!, /*includeWhitespace*/ true);
2959            }
2960        }
2961
2962        /**
2963         * Rerieves a codefix satisfying the parameters, or undefined if no such codefix is found.
2964         * @param fileName Path to file where error should be retrieved from.
2965         */
2966        private getCodeFixes(fileName: string, errorCode?: number, preferences: ts.UserPreferences = ts.emptyOptions, position?: number): readonly ts.CodeFixAction[] {
2967            const diagnosticsForCodeFix = this.getDiagnostics(fileName, /*includeSuggestions*/ true).map(diagnostic => ({
2968                start: diagnostic.start,
2969                length: diagnostic.length,
2970                code: diagnostic.code
2971            }));
2972
2973            return ts.flatMap(ts.deduplicate(diagnosticsForCodeFix, ts.equalOwnProperties), diagnostic => {
2974                if (errorCode !== undefined && errorCode !== diagnostic.code) {
2975                    return;
2976                }
2977                if (position !== undefined && diagnostic.start !== undefined && diagnostic.length !== undefined) {
2978                    const span = ts.createTextRangeFromSpan({ start: diagnostic.start, length: diagnostic.length });
2979                    if (!ts.textRangeContainsPositionInclusive(span, position)) {
2980                        return;
2981                    }
2982                }
2983                return this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start!, diagnostic.start! + diagnostic.length!, [diagnostic.code], this.formatCodeSettings, preferences);
2984            });
2985        }
2986
2987        private applyChanges(changes: readonly ts.FileTextChanges[]): void {
2988            for (const change of changes) {
2989                this.applyEdits(change.fileName, change.textChanges);
2990            }
2991        }
2992
2993        public verifyImportFixAtPosition(expectedTextArray: string[], errorCode: number | undefined, preferences: ts.UserPreferences | undefined) {
2994            const { fileName } = this.activeFile;
2995            const ranges = this.getRanges().filter(r => r.fileName === fileName);
2996            if (ranges.length > 1) {
2997                this.raiseError("Exactly one range should be specified in the testfile.");
2998            }
2999            const range = ts.firstOrUndefined(ranges);
3000
3001            if (preferences) {
3002                this.configure(preferences);
3003            }
3004
3005            const codeFixes = this.getCodeFixes(fileName, errorCode, preferences).filter(f => f.fixName === ts.codefix.importFixName);
3006
3007            if (codeFixes.length === 0) {
3008                if (expectedTextArray.length !== 0) {
3009                    this.raiseError("No codefixes returned.");
3010                }
3011                return;
3012            }
3013
3014            const actualTextArray: string[] = [];
3015            const scriptInfo = this.languageServiceAdapterHost.getScriptInfo(fileName)!;
3016            const originalContent = scriptInfo.content;
3017            for (const codeFix of codeFixes) {
3018                ts.Debug.assert(codeFix.changes.length === 1);
3019                const change = ts.first(codeFix.changes);
3020                ts.Debug.assert(change.fileName === fileName);
3021                this.applyEdits(change.fileName, change.textChanges);
3022                const text = range ? this.rangeText(range) : this.getFileContent(fileName);
3023                actualTextArray.push(text);
3024
3025                // Undo changes to perform next fix
3026                const span = change.textChanges[0].span;
3027                 const deletedText = originalContent.substr(span.start, change.textChanges[0].span.length);
3028                 const insertedText = change.textChanges[0].newText;
3029                 this.editScriptAndUpdateMarkers(fileName, span.start, span.start + insertedText.length, deletedText);
3030            }
3031            if (expectedTextArray.length !== actualTextArray.length) {
3032                this.raiseError(`Expected ${expectedTextArray.length} import fixes, got ${actualTextArray.length}:\n\n${actualTextArray.join("\n\n" + "-".repeat(20) + "\n\n")}`);
3033            }
3034            ts.zipWith(expectedTextArray, actualTextArray, (expected, actual, index) => {
3035                if (expected !== actual) {
3036                    this.raiseError(`Import fix at index ${index} doesn't match.\n${showTextDiff(expected, actual)}`);
3037                }
3038            });
3039        }
3040
3041        public verifyImportFixModuleSpecifiers(markerName: string, moduleSpecifiers: string[]) {
3042            const marker = this.getMarkerByName(markerName);
3043            const codeFixes = this.getCodeFixes(marker.fileName, ts.Diagnostics.Cannot_find_name_0.code, {
3044                includeCompletionsForModuleExports: true,
3045                includeCompletionsWithInsertText: true
3046            }, marker.position).filter(f => f.fixName === ts.codefix.importFixName);
3047
3048            const actualModuleSpecifiers = ts.mapDefined(codeFixes, fix => {
3049                return ts.forEach(ts.flatMap(fix.changes, c => c.textChanges), c => {
3050                    const match = /(?:from |require\()(['"])((?:(?!\1).)*)\1/.exec(c.newText);
3051                    return match?.[2];
3052                });
3053            });
3054
3055            assert.deepEqual(actualModuleSpecifiers, moduleSpecifiers);
3056        }
3057
3058        public verifyDocCommentTemplate(expected: ts.TextInsertion | undefined, options?: ts.DocCommentTemplateOptions) {
3059            const name = "verifyDocCommentTemplate";
3060            const actual = this.languageService.getDocCommentTemplateAtPosition(this.activeFile.fileName, this.currentCaretPosition, options || { generateReturnInDocTemplate: true })!;
3061
3062            if (expected === undefined) {
3063                if (actual) {
3064                    this.raiseError(`${name} failed - expected no template but got {newText: "${actual.newText}", caretOffset: ${actual.caretOffset}}`);
3065                }
3066
3067                return;
3068            }
3069            else {
3070                if (actual === undefined) {
3071                    this.raiseError(`${name} failed - expected the template {newText: "${expected.newText}", caretOffset: "${expected.caretOffset}"} but got nothing instead`);
3072                }
3073
3074                if (actual.newText !== expected.newText) {
3075                    this.raiseError(`${name} failed for expected insertion.\n${showTextDiff(expected.newText, actual.newText)}`);
3076                }
3077
3078                if (actual.caretOffset !== expected.caretOffset) {
3079                    this.raiseError(`${name} failed - expected caretOffset: ${expected.caretOffset}\nactual caretOffset:${actual.caretOffset}`);
3080                }
3081            }
3082        }
3083
3084        public verifyBraceCompletionAtPosition(negative: boolean, openingBrace: string) {
3085
3086            const openBraceMap = new ts.Map(ts.getEntries<ts.CharacterCodes>({
3087                "(": ts.CharacterCodes.openParen,
3088                "{": ts.CharacterCodes.openBrace,
3089                "[": ts.CharacterCodes.openBracket,
3090                "'": ts.CharacterCodes.singleQuote,
3091                '"': ts.CharacterCodes.doubleQuote,
3092                "`": ts.CharacterCodes.backtick,
3093                "<": ts.CharacterCodes.lessThan
3094            }));
3095
3096            const charCode = openBraceMap.get(openingBrace);
3097
3098            if (!charCode) {
3099                throw this.raiseError(`Invalid openingBrace '${openingBrace}' specified.`);
3100            }
3101
3102            const position = this.currentCaretPosition;
3103
3104            const validBraceCompletion = this.languageService.isValidBraceCompletionAtPosition(this.activeFile.fileName, position, charCode);
3105
3106            if (!negative && !validBraceCompletion) {
3107                this.raiseError(`${position} is not a valid brace completion position for ${openingBrace}`);
3108            }
3109
3110            if (negative && validBraceCompletion) {
3111                this.raiseError(`${position} is a valid brace completion position for ${openingBrace}`);
3112            }
3113        }
3114
3115        public verifyJsxClosingTag(map: { [markerName: string]: ts.JsxClosingTagInfo | undefined }): void {
3116            for (const markerName in map) {
3117                this.goToMarker(markerName);
3118                const actual = this.languageService.getJsxClosingTagAtPosition(this.activeFile.fileName, this.currentCaretPosition);
3119                assert.deepEqual(actual, map[markerName]);
3120            }
3121        }
3122
3123        public verifyMatchingBracePosition(bracePosition: number, expectedMatchPosition: number) {
3124            const actual = this.languageService.getBraceMatchingAtPosition(this.activeFile.fileName, bracePosition);
3125
3126            if (actual.length !== 2) {
3127                this.raiseError(`verifyMatchingBracePosition failed - expected result to contain 2 spans, but it had ${actual.length}`);
3128            }
3129
3130            let actualMatchPosition = -1;
3131            if (bracePosition === actual[0].start) {
3132                actualMatchPosition = actual[1].start;
3133            }
3134            else if (bracePosition === actual[1].start) {
3135                actualMatchPosition = actual[0].start;
3136            }
3137            else {
3138                this.raiseError(`verifyMatchingBracePosition failed - could not find the brace position: ${bracePosition} in the returned list: (${actual[0].start},${ts.textSpanEnd(actual[0])}) and (${actual[1].start},${ts.textSpanEnd(actual[1])})`);
3139            }
3140
3141            if (actualMatchPosition !== expectedMatchPosition) {
3142                this.raiseError(`verifyMatchingBracePosition failed - expected: ${actualMatchPosition},  actual: ${expectedMatchPosition}`);
3143            }
3144        }
3145
3146        public verifyNoMatchingBracePosition(bracePosition: number) {
3147            const actual = this.languageService.getBraceMatchingAtPosition(this.activeFile.fileName, bracePosition);
3148
3149            if (actual.length !== 0) {
3150                this.raiseError("verifyNoMatchingBracePosition failed - expected: 0 spans, actual: " + actual.length);
3151            }
3152        }
3153
3154        public verifySpanOfEnclosingComment(negative: boolean, onlyMultiLineDiverges?: boolean) {
3155            const expected = !negative;
3156            const position = this.currentCaretPosition;
3157            const fileName = this.activeFile.fileName;
3158            const actual = !!this.languageService.getSpanOfEnclosingComment(fileName, position, /*onlyMultiLine*/ false);
3159            const actualOnlyMultiLine = !!this.languageService.getSpanOfEnclosingComment(fileName, position, /*onlyMultiLine*/ true);
3160            if (expected !== actual || onlyMultiLineDiverges === (actual === actualOnlyMultiLine)) {
3161                this.raiseError(`verifySpanOfEnclosingComment failed:
3162                position: '${position}'
3163                fileName: '${fileName}'
3164                onlyMultiLineDiverges: '${onlyMultiLineDiverges}'
3165                actual: '${actual}'
3166                actualOnlyMultiLine: '${actualOnlyMultiLine}'
3167                expected: '${expected}'.`);
3168            }
3169        }
3170
3171        public verifyNavigateTo(options: readonly FourSlashInterface.VerifyNavigateToOptions[]): void {
3172            for (const { pattern, expected, fileName } of options) {
3173                const items = this.languageService.getNavigateToItems(pattern, /*maxResultCount*/ undefined, fileName);
3174                this.assertObjectsEqual(items, expected.map((e): ts.NavigateToItem => ({
3175                    name: e.name,
3176                    kind: e.kind,
3177                    kindModifiers: e.kindModifiers || "",
3178                    matchKind: e.matchKind || "exact",
3179                    isCaseSensitive: e.isCaseSensitive === undefined ? true : e.isCaseSensitive,
3180                    fileName: e.range.fileName,
3181                    textSpan: ts.createTextSpanFromRange(e.range),
3182                    containerName: e.containerName || "",
3183                    containerKind: e.containerKind || ts.ScriptElementKind.unknown,
3184                })));
3185            }
3186        }
3187
3188        public verifyNavigationBar(json: any, options: { checkSpans?: boolean } | undefined) {
3189            this.verifyNavigationTreeOrBar(json, this.languageService.getNavigationBarItems(this.activeFile.fileName), "Bar", options);
3190        }
3191
3192        public verifyNavigationTree(json: any, options: { checkSpans?: boolean } | undefined) {
3193            this.verifyNavigationTreeOrBar(json, this.languageService.getNavigationTree(this.activeFile.fileName), "Tree", options);
3194        }
3195
3196        private verifyNavigationTreeOrBar(json: any, tree: any, name: "Tree" | "Bar", options: { checkSpans?: boolean } | undefined) {
3197            if (JSON.stringify(tree, replacer) !== JSON.stringify(json)) {
3198                this.raiseError(`verifyNavigation${name} failed - \n${showTextDiff(stringify(json), stringify(tree, replacer))}`);
3199            }
3200
3201            function replacer(key: string, value: any) {
3202                switch (key) {
3203                    case "spans":
3204                    case "nameSpan":
3205                        return options && options.checkSpans ? value : undefined;
3206                    case "start":
3207                    case "length":
3208                        // Never omit the values in a span, even if they are 0.
3209                        return value;
3210                    case "childItems":
3211                        return !value || value.length === 0 ? undefined : value;
3212                    default:
3213                        // Omit falsy values, those are presumed to be the default.
3214                        return value || undefined;
3215                }
3216            }
3217        }
3218
3219        public printNavigationItems(searchValue: string) {
3220            const items = this.languageService.getNavigateToItems(searchValue);
3221            Harness.IO.log(`NavigationItems list (${items.length} items)`);
3222            for (const item of items) {
3223                Harness.IO.log(`name: ${item.name}, kind: ${item.kind}, parentName: ${item.containerName}, fileName: ${item.fileName}`);
3224            }
3225        }
3226
3227        public printNavigationBar() {
3228            const items = this.languageService.getNavigationBarItems(this.activeFile.fileName);
3229            Harness.IO.log(`Navigation bar (${items.length} items)`);
3230            for (const item of items) {
3231                Harness.IO.log(`${ts.repeatString(" ", item.indent)}name: ${item.text}, kind: ${item.kind}, childItems: ${item.childItems.map(child => child.text)}`);
3232            }
3233        }
3234
3235        private getOccurrencesAtCurrentPosition() {
3236            return this.languageService.getOccurrencesAtPosition(this.activeFile.fileName, this.currentCaretPosition);
3237        }
3238
3239        public verifyOccurrencesAtPositionListContains(fileName: string, start: number, end: number, isWriteAccess?: boolean) {
3240            const occurrences = this.getOccurrencesAtCurrentPosition();
3241
3242            if (!occurrences || occurrences.length === 0) {
3243                return this.raiseError("verifyOccurrencesAtPositionListContains failed - found 0 references, expected at least one.");
3244            }
3245
3246            for (const occurrence of occurrences) {
3247                if (occurrence && occurrence.fileName === fileName && occurrence.textSpan.start === start && ts.textSpanEnd(occurrence.textSpan) === end) {
3248                    if (typeof isWriteAccess !== "undefined" && occurrence.isWriteAccess !== isWriteAccess) {
3249                        this.raiseError(`verifyOccurrencesAtPositionListContains failed - item isWriteAccess value does not match, actual: ${occurrence.isWriteAccess}, expected: ${isWriteAccess}.`);
3250                    }
3251                    return;
3252                }
3253            }
3254
3255            const missingItem = { fileName, start, end, isWriteAccess };
3256            this.raiseError(`verifyOccurrencesAtPositionListContains failed - could not find the item: ${stringify(missingItem)} in the returned list: (${stringify(occurrences)})`);
3257        }
3258
3259        public verifyOccurrencesAtPositionListCount(expectedCount: number) {
3260            const occurrences = this.getOccurrencesAtCurrentPosition();
3261            const actualCount = occurrences ? occurrences.length : 0;
3262            if (expectedCount !== actualCount) {
3263                this.raiseError(`verifyOccurrencesAtPositionListCount failed - actual: ${actualCount}, expected:${expectedCount}`);
3264            }
3265        }
3266
3267        private getDocumentHighlightsAtCurrentPosition(fileNamesToSearch: readonly string[]) {
3268            const filesToSearch = fileNamesToSearch.map(name => ts.combinePaths(this.basePath, name));
3269            return this.languageService.getDocumentHighlights(this.activeFile.fileName, this.currentCaretPosition, filesToSearch);
3270        }
3271
3272        public verifyRangesAreOccurrences(isWriteAccess?: boolean, ranges?: Range[]) {
3273            ranges = ranges || this.getRanges();
3274            assert(ranges.length);
3275            for (const r of ranges) {
3276                this.goToRangeStart(r);
3277                this.verifyOccurrencesAtPositionListCount(ranges.length);
3278                for (const range of ranges) {
3279                    this.verifyOccurrencesAtPositionListContains(range.fileName, range.pos, range.end, isWriteAccess);
3280                }
3281            }
3282        }
3283
3284        public verifyRangesWithSameTextAreRenameLocations(...texts: string[]) {
3285            if (texts.length) {
3286                texts.forEach(text => this.verifyRangesAreRenameLocations(this.rangesByText().get(text)));
3287            }
3288            else {
3289                this.rangesByText().forEach(ranges => this.verifyRangesAreRenameLocations(ranges));
3290            }
3291        }
3292
3293        public verifyRangesWithSameTextAreDocumentHighlights() {
3294            this.rangesByText().forEach(ranges => this.verifyRangesAreDocumentHighlights(ranges, /*options*/ undefined));
3295        }
3296
3297        public verifyDocumentHighlightsOf(startRange: Range, ranges: Range[], options: FourSlashInterface.VerifyDocumentHighlightsOptions | undefined) {
3298            const fileNames = options && options.filesToSearch || unique(ranges, range => range.fileName);
3299            this.goToRangeStart(startRange);
3300            this.verifyDocumentHighlights(ranges, fileNames);
3301        }
3302
3303        public verifyRangesAreDocumentHighlights(ranges: Range[] | undefined, options: FourSlashInterface.VerifyDocumentHighlightsOptions | undefined) {
3304            ranges = ranges || this.getRanges();
3305            assert(ranges.length);
3306            const fileNames = options && options.filesToSearch || unique(ranges, range => range.fileName);
3307            for (const range of ranges) {
3308                this.goToRangeStart(range);
3309                this.verifyDocumentHighlights(ranges, fileNames);
3310            }
3311        }
3312
3313        public verifyNoDocumentHighlights(startRange: Range) {
3314            this.goToRangeStart(startRange);
3315            const documentHighlights = this.getDocumentHighlightsAtCurrentPosition([this.activeFile.fileName]);
3316            const numHighlights = ts.length(documentHighlights);
3317            if (numHighlights > 0) {
3318                this.raiseError(`verifyNoDocumentHighlights failed - unexpectedly got ${numHighlights} highlights`);
3319            }
3320        }
3321
3322        private verifyDocumentHighlights(expectedRanges: Range[], fileNames: readonly string[] = [this.activeFile.fileName]) {
3323            fileNames = ts.map(fileNames, ts.normalizePath);
3324            const documentHighlights = this.getDocumentHighlightsAtCurrentPosition(fileNames) || [];
3325
3326            for (const dh of documentHighlights) {
3327                if (fileNames.indexOf(dh.fileName) === -1) {
3328                    this.raiseError(`verifyDocumentHighlights failed - got highlights in unexpected file name ${dh.fileName}`);
3329                }
3330            }
3331
3332            for (const fileName of fileNames) {
3333                const expectedRangesInFile = expectedRanges.filter(r => ts.normalizePath(r.fileName) === fileName);
3334                const highlights = ts.find(documentHighlights, dh => dh.fileName === fileName);
3335                const spansInFile = highlights ? highlights.highlightSpans.sort((s1, s2) => s1.textSpan.start - s2.textSpan.start) : [];
3336
3337                if (expectedRangesInFile.length !== spansInFile.length) {
3338                    this.raiseError(`verifyDocumentHighlights failed - In ${fileName}, expected ${expectedRangesInFile.length} highlights, got ${spansInFile.length}`);
3339                }
3340
3341                ts.zipWith(expectedRangesInFile, spansInFile, (expectedRange, span) => {
3342                    if (span.textSpan.start !== expectedRange.pos || ts.textSpanEnd(span.textSpan) !== expectedRange.end) {
3343                        this.raiseError(`verifyDocumentHighlights failed - span does not match, actual: ${stringify(span.textSpan)}, expected: ${expectedRange.pos}--${expectedRange.end}`);
3344                    }
3345                });
3346            }
3347        }
3348
3349        public verifyCodeFixAvailable(negative: boolean, expected: FourSlashInterface.VerifyCodeFixAvailableOptions[] | string | undefined): void {
3350            const codeFixes = this.getCodeFixes(this.activeFile.fileName);
3351            if (negative) {
3352                if (typeof expected === "undefined") {
3353                    this.assertObjectsEqual(codeFixes, ts.emptyArray);
3354                }
3355                else if (typeof expected === "string") {
3356                    if (codeFixes.some(fix => fix.fixName === expected)) {
3357                        this.raiseError(`Expected not to find a fix with the name '${expected}', but one exists.`);
3358                    }
3359                }
3360                else {
3361                    assert(typeof expected === "undefined" || typeof expected === "string", "With a negated assertion, 'expected' must be undefined or a string value of a codefix name.");
3362                }
3363            }
3364            else if (typeof expected === "string") {
3365                this.assertObjectsEqual(codeFixes.map(fix => fix.fixName), [expected]);
3366            }
3367            else {
3368                const actuals = codeFixes.map((fix): FourSlashInterface.VerifyCodeFixAvailableOptions => ({ description: fix.description, commands: fix.commands }));
3369                this.assertObjectsEqual(actuals, negative ? ts.emptyArray : expected);
3370            }
3371        }
3372
3373        public verifyCodeFixAllAvailable(negative: boolean, fixName: string) {
3374            const availableFixes = this.getCodeFixes(this.activeFile.fileName);
3375            const hasFix = availableFixes.some(fix => fix.fixName === fixName && fix.fixId);
3376            if (negative && hasFix) {
3377                this.raiseError(`Expected not to find a fix with the name '${fixName}', but one exists.`);
3378            }
3379            else if (!negative && !hasFix) {
3380                if (availableFixes.some(fix => fix.fixName === fixName)) {
3381                    this.raiseError(`Found a fix with the name '${fixName}', but fix-all is not available.`);
3382                }
3383
3384                this.raiseError(
3385                    `Expected to find a fix with the name '${fixName}', but none exists.` +
3386                        availableFixes.length
3387                        ? ` Available fixes: ${availableFixes.map(fix => `${fix.fixName} (${fix.fixId ? "with" : "without"} fix-all)`).join(", ")}`
3388                        : ""
3389                );
3390            }
3391        }
3392
3393        public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) {
3394            const isAvailable = this.getApplicableRefactors(this.getMarkerByName(markerName)).length > 0;
3395            if (negative && isAvailable) {
3396                this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected no refactor at marker ${markerName} but found some.`);
3397            }
3398            if (!negative && !isAvailable) {
3399                this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected a refactor at marker ${markerName} but found none.`);
3400            }
3401        }
3402
3403        private getSelection(): ts.TextRange {
3404            return {
3405                pos: this.currentCaretPosition,
3406                end: this.selectionEnd === -1 ? this.currentCaretPosition : this.selectionEnd
3407            };
3408        }
3409
3410        public verifyRefactorAvailable(negative: boolean, triggerReason: ts.RefactorTriggerReason, name: string, actionName?: string) {
3411            let refactors = this.getApplicableRefactorsAtSelection(triggerReason);
3412            refactors = refactors.filter(r => r.name === name && (actionName === undefined || r.actions.some(a => a.name === actionName)));
3413            const isAvailable = refactors.length > 0;
3414
3415            if (negative) {
3416                if (isAvailable) {
3417                    this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found: ${refactors.map(r => r.name).join(", ")}`);
3418                }
3419            }
3420            else {
3421                if (!isAvailable) {
3422                    this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected a refactor but found none.`);
3423                }
3424                if (refactors.length > 1) {
3425                    this.raiseError(`${refactors.length} available refactors both have name ${name} and action ${actionName}`);
3426                }
3427            }
3428        }
3429
3430        public verifyRefactorKindsAvailable(kind: string, expected: string[], preferences = ts.emptyOptions) {
3431            const refactors = this.getApplicableRefactorsAtSelection("invoked", kind, preferences);
3432            const availableKinds = ts.flatMap(refactors, refactor => refactor.actions).map(action => action.kind);
3433            assert.deepEqual(availableKinds.sort(), expected.sort(), `Expected kinds to be equal`);
3434        }
3435
3436        public verifyRefactorsAvailable(names: readonly string[]): void {
3437            assert.deepEqual(unique(this.getApplicableRefactorsAtSelection(), r => r.name), names);
3438        }
3439
3440        public verifyApplicableRefactorAvailableForRange(negative: boolean) {
3441            const ranges = this.getRanges();
3442            if (!(ranges && ranges.length === 1)) {
3443                throw new Error("Exactly one refactor range is allowed per test.");
3444            }
3445
3446            const isAvailable = this.getApplicableRefactors(ts.first(ranges)).length > 0;
3447            if (negative && isAvailable) {
3448                this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`);
3449            }
3450            if (!negative && !isAvailable) {
3451                this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected a refactor but found none.`);
3452            }
3453        }
3454
3455        public applyRefactor({ refactorName, actionName, actionDescription, newContent: newContentWithRenameMarker, triggerReason }: FourSlashInterface.ApplyRefactorOptions) {
3456            const range = this.getSelection();
3457            const refactors = this.getApplicableRefactorsAtSelection(triggerReason);
3458            const refactorsWithName = refactors.filter(r => r.name === refactorName);
3459            if (refactorsWithName.length === 0) {
3460                this.raiseError(`The expected refactor: ${refactorName} is not available at the marker location.\nAvailable refactors: ${refactors.map(r => r.name)}`);
3461            }
3462
3463            const action = ts.firstDefined(refactorsWithName, refactor => refactor.actions.find(a => a.name === actionName));
3464            if (!action) {
3465                throw this.raiseError(`The expected action: ${actionName} is not included in: ${ts.flatMap(refactorsWithName, r => r.actions.map(a => a.name))}`);
3466            }
3467            if (action.description !== actionDescription) {
3468                this.raiseError(`Expected action description to be ${JSON.stringify(actionDescription)}, got: ${JSON.stringify(action.description)}`);
3469            }
3470
3471            const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactorName, actionName, ts.emptyOptions)!;
3472            for (const edit of editInfo.edits) {
3473                this.applyEdits(edit.fileName, edit.textChanges);
3474            }
3475
3476            let renameFilename: string | undefined;
3477            let renamePosition: number | undefined;
3478
3479            const newFileContents = typeof newContentWithRenameMarker === "string" ? { [this.activeFile.fileName]: newContentWithRenameMarker } : newContentWithRenameMarker;
3480            for (const fileName in newFileContents) {
3481                const { renamePosition: rp, newContent } = TestState.parseNewContent(newFileContents[fileName]);
3482                if (renamePosition === undefined) {
3483                    renameFilename = fileName;
3484                    renamePosition = rp;
3485                }
3486                else {
3487                    ts.Debug.assert(rp === undefined);
3488                }
3489                this.verifyFileContent(fileName, newContent);
3490
3491            }
3492
3493            if (renamePosition === undefined) {
3494                if (editInfo.renameLocation !== undefined) {
3495                    this.raiseError(`Did not expect a rename location, got ${editInfo.renameLocation}`);
3496                }
3497            }
3498            else {
3499                this.assertObjectsEqual(editInfo.renameFilename, renameFilename);
3500                if (renamePosition !== editInfo.renameLocation) {
3501                    this.raiseError(`Expected rename position of ${renamePosition}, but got ${editInfo.renameLocation}`);
3502                }
3503            }
3504        }
3505
3506        private static parseNewContent(newContentWithRenameMarker: string): { readonly renamePosition: number | undefined, readonly newContent: string } {
3507            const renamePosition = newContentWithRenameMarker.indexOf("/*RENAME*/");
3508            if (renamePosition === -1) {
3509                return { renamePosition: undefined, newContent: newContentWithRenameMarker };
3510            }
3511            else {
3512                const newContent = newContentWithRenameMarker.slice(0, renamePosition) + newContentWithRenameMarker.slice(renamePosition + "/*RENAME*/".length);
3513                return { renamePosition, newContent };
3514            }
3515        }
3516
3517        public noMoveToNewFile() {
3518            const ranges = this.getRanges();
3519            assert(ranges.length);
3520            for (const range of ranges) {
3521                for (const refactor of this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true })) {
3522                    if (refactor.name === "Move to a new file") {
3523                        ts.Debug.fail("Did not expect to get 'move to a new file' refactor");
3524                    }
3525                }
3526            }
3527        }
3528
3529        public moveToNewFile(options: FourSlashInterface.MoveToNewFileOptions): void {
3530            assert(this.getRanges().length === 1, "Must have exactly one fourslash range (source enclosed between '[|' and '|]' delimiters) in the source file");
3531            const range = this.getRanges()[0];
3532            const refactor = ts.find(this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true }), r => r.name === "Move to a new file")!;
3533            assert(refactor.actions.length === 1);
3534            const action = ts.first(refactor.actions);
3535            assert(action.name === "Move to a new file" && action.description === "Move to a new file");
3536
3537            const editInfo = this.languageService.getEditsForRefactor(range.fileName, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.emptyOptions)!;
3538            this.verifyNewContent({ newFileContent: options.newFileContents }, editInfo.edits);
3539        }
3540
3541        private testNewFileContents(edits: readonly ts.FileTextChanges[], newFileContents: { [fileName: string]: string }, description: string): void {
3542            for (const { fileName, textChanges } of edits) {
3543                const newContent = newFileContents[fileName];
3544                if (newContent === undefined) {
3545                    this.raiseError(`${description} - There was an edit in ${fileName} but new content was not specified.`);
3546                }
3547
3548                const fileContent = this.tryGetFileContent(fileName);
3549                if (fileContent !== undefined) {
3550                    const actualNewContent = ts.textChanges.applyChanges(fileContent, textChanges);
3551                    assert.equal(actualNewContent, newContent, `new content for ${fileName}`);
3552                }
3553                else {
3554                    // Creates a new file.
3555                    assert(textChanges.length === 1);
3556                    const change = ts.first(textChanges);
3557                    assert.deepEqual(change.span, ts.createTextSpan(0, 0));
3558                    assert.equal(change.newText, newContent, `${description} - Content for ${fileName}`);
3559                }
3560            }
3561
3562            for (const fileName in newFileContents) {
3563                if (!edits.some(e => e.fileName === fileName)) {
3564                    ts.Debug.fail(`${description} - Asserted new contents of ${fileName} but there were no edits`);
3565                }
3566            }
3567        }
3568
3569        public verifyFileAfterApplyingRefactorAtMarker(
3570            markerName: string,
3571            expectedContent: string,
3572            refactorNameToApply: string,
3573            actionName: string,
3574            formattingOptions?: ts.FormatCodeSettings) {
3575
3576            formattingOptions = formattingOptions || this.formatCodeSettings;
3577            const marker = this.getMarkerByName(markerName);
3578
3579            const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, marker.position, ts.emptyOptions);
3580            const applicableRefactorToApply = ts.find(applicableRefactors, refactor => refactor.name === refactorNameToApply);
3581
3582            if (!applicableRefactorToApply) {
3583                this.raiseError(`The expected refactor: ${refactorNameToApply} is not available at the marker location.`);
3584            }
3585
3586            const editInfo = this.languageService.getEditsForRefactor(marker.fileName, formattingOptions, marker.position, refactorNameToApply, actionName, ts.emptyOptions)!;
3587
3588            for (const edit of editInfo.edits) {
3589                this.applyEdits(edit.fileName, edit.textChanges);
3590            }
3591            const actualContent = this.getFileContent(marker.fileName);
3592
3593            if (actualContent !== expectedContent) {
3594                this.raiseError(`verifyFileAfterApplyingRefactors failed:\n${showTextDiff(expectedContent, actualContent)}`);
3595            }
3596        }
3597
3598        public printAvailableCodeFixes() {
3599            const codeFixes = this.getCodeFixes(this.activeFile.fileName);
3600            Harness.IO.log(stringify(codeFixes));
3601        }
3602
3603        private formatCallHierarchyItemSpan(file: FourSlashFile, span: ts.TextSpan, prefix: string, trailingPrefix = prefix) {
3604            const startLc = this.languageServiceAdapterHost.positionToLineAndCharacter(file.fileName, span.start);
3605            const endLc = this.languageServiceAdapterHost.positionToLineAndCharacter(file.fileName, ts.textSpanEnd(span));
3606            const lines = this.spanLines(file, span, { fullLines: true, lineNumbers: true, selection: true });
3607            let text = "";
3608            text += `${prefix}╭ ${file.fileName}:${startLc.line + 1}:${startLc.character + 1}-${endLc.line + 1}:${endLc.character + 1}\n`;
3609            for (const line of lines) {
3610                text += `${prefix}│ ${line.trimRight()}\n`;
3611            }
3612            text += `${trailingPrefix}╰\n`;
3613            return text;
3614        }
3615
3616        private formatCallHierarchyItemSpans(file: FourSlashFile, spans: ts.TextSpan[], prefix: string, trailingPrefix = prefix) {
3617            let text = "";
3618            for (let i = 0; i < spans.length; i++) {
3619                text += this.formatCallHierarchyItemSpan(file, spans[i], prefix, i < spans.length - 1 ? prefix : trailingPrefix);
3620            }
3621            return text;
3622        }
3623
3624        private formatCallHierarchyItem(file: FourSlashFile, callHierarchyItem: ts.CallHierarchyItem, direction: CallHierarchyItemDirection, seen: ts.ESMap<string, boolean>, prefix: string, trailingPrefix: string = prefix) {
3625            const key = `${callHierarchyItem.file}|${JSON.stringify(callHierarchyItem.span)}|${direction}`;
3626            const alreadySeen = seen.has(key);
3627            seen.set(key, true);
3628
3629            const incomingCalls =
3630                direction === CallHierarchyItemDirection.Outgoing ? { result: "skip" } as const :
3631                    alreadySeen ? { result: "seen" } as const :
3632                        { result: "show", values: this.languageService.provideCallHierarchyIncomingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start) } as const;
3633
3634            const outgoingCalls =
3635                direction === CallHierarchyItemDirection.Incoming ? { result: "skip" } as const :
3636                    alreadySeen ? { result: "seen" } as const :
3637                        { result: "show", values: this.languageService.provideCallHierarchyOutgoingCalls(callHierarchyItem.file, callHierarchyItem.selectionSpan.start) } as const;
3638
3639            let text = "";
3640            text += `${prefix}╭ name: ${callHierarchyItem.name}\n`;
3641            text += `${prefix}├ kind: ${callHierarchyItem.kind}\n`;
3642            if (callHierarchyItem.containerName) {
3643                text += `${prefix}├ containerName: ${callHierarchyItem.containerName}\n`;
3644            }
3645            text += `${prefix}├ file: ${callHierarchyItem.file}\n`;
3646            text += `${prefix}├ span:\n`;
3647            text += this.formatCallHierarchyItemSpan(file, callHierarchyItem.span, `${prefix}│ `);
3648            text += `${prefix}├ selectionSpan:\n`;
3649            text += this.formatCallHierarchyItemSpan(file, callHierarchyItem.selectionSpan, `${prefix}│ `,
3650                incomingCalls.result !== "skip" || outgoingCalls.result !== "skip" ? `${prefix}│ ` :
3651                    `${trailingPrefix}╰ `);
3652
3653            if (incomingCalls.result === "seen") {
3654                if (outgoingCalls.result === "skip") {
3655                    text += `${trailingPrefix}╰ incoming: ...\n`;
3656                }
3657                else {
3658                    text += `${prefix}├ incoming: ...\n`;
3659                }
3660            }
3661            else if (incomingCalls.result === "show") {
3662                if (!ts.some(incomingCalls.values)) {
3663                    if (outgoingCalls.result === "skip") {
3664                        text += `${trailingPrefix}╰ incoming: none\n`;
3665                    }
3666                    else {
3667                        text += `${prefix}├ incoming: none\n`;
3668                    }
3669                }
3670                else {
3671                    text += `${prefix}├ incoming:\n`;
3672                    for (let i = 0; i < incomingCalls.values.length; i++) {
3673                        const incomingCall = incomingCalls.values[i];
3674                        const file = this.findFile(incomingCall.from.file);
3675                        text += `${prefix}│ ╭ from:\n`;
3676                        text += this.formatCallHierarchyItem(file, incomingCall.from, CallHierarchyItemDirection.Incoming, seen, `${prefix}│ │ `);
3677                        text += `${prefix}│ ├ fromSpans:\n`;
3678                        text += this.formatCallHierarchyItemSpans(file, incomingCall.fromSpans, `${prefix}│ │ `,
3679                            i < incomingCalls.values.length - 1 ? `${prefix}│ ╰ ` :
3680                                outgoingCalls.result !== "skip" ? `${prefix}│ ╰ ` :
3681                                    `${trailingPrefix}╰ ╰ `);
3682                    }
3683                }
3684            }
3685
3686            if (outgoingCalls.result === "seen") {
3687                text += `${trailingPrefix}╰ outgoing: ...\n`;
3688            }
3689            else if (outgoingCalls.result === "show") {
3690                if (!ts.some(outgoingCalls.values)) {
3691                    text += `${trailingPrefix}╰ outgoing: none\n`;
3692                }
3693                else {
3694                    text += `${prefix}├ outgoing:\n`;
3695                    for (let i = 0; i < outgoingCalls.values.length; i++) {
3696                        const outgoingCall = outgoingCalls.values[i];
3697                        text += `${prefix}│ ╭ to:\n`;
3698                        text += this.formatCallHierarchyItem(this.findFile(outgoingCall.to.file), outgoingCall.to, CallHierarchyItemDirection.Outgoing, seen, `${prefix}│ │ `);
3699                        text += `${prefix}│ ├ fromSpans:\n`;
3700                        text += this.formatCallHierarchyItemSpans(file, outgoingCall.fromSpans, `${prefix}│ │ `,
3701                            i < outgoingCalls.values.length - 1 ? `${prefix}│ ╰ ` :
3702                                `${trailingPrefix}╰ ╰ `);
3703                    }
3704                }
3705            }
3706            return text;
3707        }
3708
3709        private formatCallHierarchy(callHierarchyItem: ts.CallHierarchyItem | undefined) {
3710            let text = "";
3711            if (callHierarchyItem) {
3712                const file = this.findFile(callHierarchyItem.file);
3713                text += this.formatCallHierarchyItem(file, callHierarchyItem, CallHierarchyItemDirection.Root, new ts.Map(), "");
3714            }
3715            return text;
3716        }
3717
3718        public baselineCallHierarchy() {
3719            const baselineFile = this.getBaselineFileNameForContainingTestFile(".callHierarchy.txt");
3720            const callHierarchyItem = this.languageService.prepareCallHierarchy(this.activeFile.fileName, this.currentCaretPosition);
3721            const text = callHierarchyItem ? ts.mapOneOrMany(callHierarchyItem, item => this.formatCallHierarchy(item), result => result.join("")) : "none";
3722            Harness.Baseline.runBaseline(baselineFile, text);
3723        }
3724
3725        private assertTextSpanEqualsRange(span: ts.TextSpan, range: Range, message?: string) {
3726            if (!textSpanEqualsRange(span, range)) {
3727                this.raiseError(`${prefixMessage(message)}Expected to find TextSpan ${JSON.stringify({ start: range.pos, length: range.end - range.pos })} but got ${JSON.stringify(span)} instead.`);
3728            }
3729        }
3730
3731        private getLineContent(index: number) {
3732            const text = this.getFileContent(this.activeFile.fileName);
3733            const pos = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: index, character: 0 });
3734            let startPos = pos, endPos = pos;
3735
3736            while (startPos > 0) {
3737                const ch = text.charCodeAt(startPos - 1);
3738                if (ch === ts.CharacterCodes.carriageReturn || ch === ts.CharacterCodes.lineFeed) {
3739                    break;
3740                }
3741
3742                startPos--;
3743            }
3744
3745            while (endPos < text.length) {
3746                const ch = text.charCodeAt(endPos);
3747
3748                if (ch === ts.CharacterCodes.carriageReturn || ch === ts.CharacterCodes.lineFeed) {
3749                    break;
3750                }
3751
3752                endPos++;
3753            }
3754
3755            return text.substring(startPos, endPos);
3756        }
3757
3758        // Get the text of the entire line the caret is currently at
3759        private getCurrentLineContent() {
3760            return this.getLineContent(this.languageServiceAdapterHost.positionToLineAndCharacter(
3761                this.activeFile.fileName,
3762                this.currentCaretPosition,
3763            ).line);
3764        }
3765
3766        private findFile(indexOrName: string | number): FourSlashFile {
3767            if (typeof indexOrName === "number") {
3768                const index = indexOrName;
3769                if (index >= this.testData.files.length) {
3770                    throw new Error(`File index (${index}) in openFile was out of range. There are only ${this.testData.files.length} files in this test.`);
3771                }
3772                else {
3773                    return this.testData.files[index];
3774                }
3775            }
3776            else if (ts.isString(indexOrName)) {
3777                const { file, availableNames } = this.tryFindFileWorker(indexOrName);
3778                if (!file) {
3779                    throw new Error(`No test file named "${indexOrName}" exists. Available file names are: ${availableNames.join(", ")}`);
3780                }
3781                return file;
3782            }
3783            else {
3784                return ts.Debug.assertNever(indexOrName);
3785            }
3786        }
3787
3788        private tryFindFileWorker(name: string): { readonly file: FourSlashFile | undefined; readonly availableNames: readonly string[]; } {
3789            name = ts.normalizePath(name);
3790            // names are stored in the compiler with this relative path, this allows people to use goTo.file on just the fileName
3791            name = name.indexOf("/") === -1 ? (this.basePath + "/" + name) : name;
3792
3793            const availableNames: string[] = [];
3794            const file = ts.forEach(this.testData.files, file => {
3795                const fn = ts.normalizePath(file.fileName);
3796                if (fn) {
3797                    if (fn === name) {
3798                        return file;
3799                    }
3800                    availableNames.push(fn);
3801                }
3802            });
3803            return { file, availableNames };
3804        }
3805
3806        private hasFile(name: string): boolean {
3807            return this.tryFindFileWorker(name).file !== undefined;
3808        }
3809
3810        private getLineColStringAtPosition(position: number, file: FourSlashFile = this.activeFile) {
3811            const pos = this.languageServiceAdapterHost.positionToLineAndCharacter(file.fileName, position);
3812            return `line ${(pos.line + 1)}, col ${pos.character}`;
3813        }
3814
3815        public getMarkerByName(markerName: string) {
3816            const markerPos = this.testData.markerPositions.get(markerName);
3817            if (markerPos === undefined) {
3818                throw new Error(`Unknown marker "${markerName}" Available markers: ${this.getMarkerNames().map(m => "\"" + m + "\"").join(", ")}`);
3819            }
3820            else {
3821                return markerPos;
3822            }
3823        }
3824
3825        public setCancelled(numberOfCalls: number): void {
3826            this.cancellationToken.setCancelled(numberOfCalls);
3827        }
3828
3829        public resetCancelled(): void {
3830            this.cancellationToken.resetCancelled();
3831        }
3832
3833        public getEditsForFileRename({ oldPath, newPath, newFileContents, preferences }: FourSlashInterface.GetEditsForFileRenameOptions): void {
3834            const test = (fileContents: { readonly [fileName: string]: string }, description: string): void => {
3835                const changes = this.languageService.getEditsForFileRename(oldPath, newPath, this.formatCodeSettings, preferences);
3836                this.testNewFileContents(changes, fileContents, description);
3837            };
3838
3839            ts.Debug.assert(!this.hasFile(newPath), "initially, newPath should not exist");
3840
3841            test(newFileContents, "with file not yet moved");
3842
3843            this.languageServiceAdapterHost.renameFileOrDirectory(oldPath, newPath);
3844            this.languageService.cleanupSemanticCache();
3845            const pathUpdater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ false), /*sourceMapper*/ undefined);
3846            test(renameKeys(newFileContents, key => pathUpdater(key) || key), "with file moved");
3847        }
3848
3849        private getApplicableRefactorsAtSelection(triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string, preferences = ts.emptyOptions) {
3850            return this.getApplicableRefactorsWorker(this.getSelection(), this.activeFile.fileName, preferences, triggerReason, kind);
3851        }
3852        private getApplicableRefactors(rangeOrMarker: Range | Marker, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason = "implicit", kind?: string): readonly ts.ApplicableRefactorInfo[] {
3853            return this.getApplicableRefactorsWorker("position" in rangeOrMarker ? rangeOrMarker.position : rangeOrMarker, rangeOrMarker.fileName, preferences, triggerReason, kind); // eslint-disable-line no-in-operator
3854        }
3855        private getApplicableRefactorsWorker(positionOrRange: number | ts.TextRange, fileName: string, preferences = ts.emptyOptions, triggerReason: ts.RefactorTriggerReason, kind?: string): readonly ts.ApplicableRefactorInfo[] {
3856            return this.languageService.getApplicableRefactors(fileName, positionOrRange, preferences, triggerReason, kind) || ts.emptyArray;
3857        }
3858
3859        public configurePlugin(pluginName: string, configuration: any): void {
3860            (<ts.server.SessionClient>this.languageService).configurePlugin(pluginName, configuration);
3861        }
3862
3863        public toggleLineComment(newFileContent: string): void {
3864            const changes: ts.TextChange[] = [];
3865            for (const range of this.getRanges()) {
3866                changes.push.apply(changes, this.languageService.toggleLineComment(this.activeFile.fileName, range));
3867            }
3868
3869            this.applyEdits(this.activeFile.fileName, changes);
3870
3871            this.verifyCurrentFileContent(newFileContent);
3872        }
3873
3874        public toggleMultilineComment(newFileContent: string): void {
3875            const changes: ts.TextChange[] = [];
3876            for (const range of this.getRanges()) {
3877                changes.push.apply(changes, this.languageService.toggleMultilineComment(this.activeFile.fileName, range));
3878            }
3879
3880            this.applyEdits(this.activeFile.fileName, changes);
3881
3882            this.verifyCurrentFileContent(newFileContent);
3883        }
3884
3885        public commentSelection(newFileContent: string): void {
3886            const changes: ts.TextChange[] = [];
3887            for (const range of this.getRanges()) {
3888                changes.push.apply(changes, this.languageService.commentSelection(this.activeFile.fileName, range));
3889            }
3890
3891            this.applyEdits(this.activeFile.fileName, changes);
3892
3893            this.verifyCurrentFileContent(newFileContent);
3894        }
3895
3896        public uncommentSelection(newFileContent: string): void {
3897            const changes: ts.TextChange[] = [];
3898            for (const range of this.getRanges()) {
3899                changes.push.apply(changes, this.languageService.uncommentSelection(this.activeFile.fileName, range));
3900            }
3901
3902            this.applyEdits(this.activeFile.fileName, changes);
3903
3904            this.verifyCurrentFileContent(newFileContent);
3905        }
3906    }
3907
3908    function prefixMessage(message: string | undefined) {
3909        return message ? `${message} - ` : "";
3910    }
3911
3912    function textSpanEqualsRange(span: ts.TextSpan, range: Range) {
3913        return span.start === range.pos && span.length === range.end - range.pos;
3914    }
3915
3916    function updateTextRangeForTextChanges({ pos, end }: ts.TextRange, textChanges: readonly ts.TextChange[]): ts.TextRange {
3917        forEachTextChange(textChanges, change => {
3918            const update = (p: number): number => updatePosition(p, change.span.start, ts.textSpanEnd(change.span), change.newText);
3919            pos = update(pos);
3920            end = update(end);
3921        });
3922        return { pos, end };
3923    }
3924
3925    /** Apply each textChange in order, updating future changes to account for the text offset of previous changes. */
3926    function forEachTextChange(changes: readonly ts.TextChange[], cb: (change: ts.TextChange) => void): void {
3927        // Copy this so we don't ruin someone else's copy
3928        changes = JSON.parse(JSON.stringify(changes));
3929        for (let i = 0; i < changes.length; i++) {
3930            const change = changes[i];
3931            cb(change);
3932            const changeDelta = change.newText.length - change.span.length;
3933            for (let j = i + 1; j < changes.length; j++) {
3934                if (changes[j].span.start >= change.span.start) {
3935                    changes[j].span.start += changeDelta;
3936                }
3937            }
3938        }
3939    }
3940
3941    function updatePosition(position: number, editStart: number, editEnd: number, { length }: string): number {
3942        // If inside the edit, return -1 to mark as invalid
3943        return position <= editStart ? position : position < editEnd ? -1 : position + length - + (editEnd - editStart);
3944    }
3945
3946    function renameKeys<T>(obj: { readonly [key: string]: T }, renameKey: (key: string) => string): { readonly [key: string]: T } {
3947        const res: { [key: string]: T } = {};
3948        for (const key in obj) {
3949            res[renameKey(key)] = obj[key];
3950        }
3951        return res;
3952    }
3953
3954    export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) {
3955        const content = Harness.IO.readFile(fileName)!;
3956        runFourSlashTestContent(basePath, testType, content, fileName);
3957    }
3958
3959    export function runFourSlashTestContent(basePath: string, testType: FourSlashTestType, content: string, fileName: string): void {
3960        // Give file paths an absolute path for the virtual file system
3961        const absoluteBasePath = ts.combinePaths(Harness.virtualFileSystemRoot, basePath);
3962        const absoluteFileName = ts.combinePaths(Harness.virtualFileSystemRoot, fileName);
3963
3964        // Parse out the files and their metadata
3965        const testData = parseTestData(absoluteBasePath, content, absoluteFileName);
3966        const state = new TestState(absoluteFileName, absoluteBasePath, testType, testData);
3967        const actualFileName = Harness.IO.resolvePath(fileName) || absoluteFileName;
3968        const output = ts.transpileModule(content, { reportDiagnostics: true, fileName: actualFileName, compilerOptions: { target: ts.ScriptTarget.ES2015, inlineSourceMap: true, inlineSources: true } });
3969        if (output.diagnostics!.length > 0) {
3970            throw new Error(`Syntax error in ${absoluteBasePath}: ${output.diagnostics![0].messageText}`);
3971        }
3972        runCode(output.outputText, state, actualFileName);
3973    }
3974
3975    function runCode(code: string, state: TestState, fileName: string): void {
3976        // Compile and execute the test
3977        const generatedFile = ts.changeExtension(fileName, ".js");
3978        const wrappedCode = `(function(test, goTo, plugins, verify, edit, debug, format, cancellation, classification, completion, verifyOperationIsCancelled) {${code}\n//# sourceURL=${ts.getBaseFileName(generatedFile)}\n})`;
3979
3980        type SourceMapSupportModule = typeof import("source-map-support") & {
3981            // TODO(rbuckton): This is missing from the DT definitions and needs to be added.
3982            resetRetrieveHandlers(): void
3983        };
3984
3985        // Provide the content of the current test to 'source-map-support' so that it can give us the correct source positions
3986        // for test failures.
3987        let sourceMapSupportModule: SourceMapSupportModule | undefined;
3988        try {
3989            sourceMapSupportModule = require("source-map-support");
3990        }
3991        catch {
3992            // do nothing
3993        }
3994
3995        sourceMapSupportModule?.install({
3996            retrieveFile: path => {
3997                return path === generatedFile ? wrappedCode :
3998                    undefined!;
3999            }
4000        });
4001
4002        try {
4003            const test = new FourSlashInterface.Test(state);
4004            const goTo = new FourSlashInterface.GoTo(state);
4005            const plugins = new FourSlashInterface.Plugins(state);
4006            const verify = new FourSlashInterface.Verify(state);
4007            const edit = new FourSlashInterface.Edit(state);
4008            const debug = new FourSlashInterface.Debug(state);
4009            const format = new FourSlashInterface.Format(state);
4010            const cancellation = new FourSlashInterface.Cancellation(state);
4011            // eslint-disable-next-line no-eval
4012            const f = eval(wrappedCode);
4013            f(test, goTo, plugins, verify, edit, debug, format, cancellation, FourSlashInterface.classification, FourSlashInterface.Completion, verifyOperationIsCancelled);
4014        }
4015        catch (err) {
4016            // ensure 'source-map-support' is triggered while we still have the handler attached by accessing `error.stack`.
4017            err.stack?.toString();
4018            throw err;
4019        }
4020        finally {
4021            sourceMapSupportModule?.resetRetrieveHandlers();
4022        }
4023    }
4024
4025    function chompLeadingSpace(content: string) {
4026        const lines = content.split("\n");
4027        for (const line of lines) {
4028            if ((line.length !== 0) && (line.charAt(0) !== " ")) {
4029                return content;
4030            }
4031        }
4032
4033        return lines.map(s => s.substr(1)).join("\n");
4034    }
4035
4036    function parseTestData(basePath: string, contents: string, fileName: string): FourSlashData {
4037        // Regex for parsing options in the format "@Alpha: Value of any sort"
4038        const optionRegex = /^\s*@(\w+):\s*(.*)\s*/;
4039
4040        // List of all the subfiles we've parsed out
4041        const files: FourSlashFile[] = [];
4042        // Global options
4043        const globalOptions: { [s: string]: string; } = {};
4044        let symlinks: vfs.FileSet | undefined;
4045        // Marker positions
4046
4047        // Split up the input file by line
4048        // Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so
4049        // we have to string-based splitting instead and try to figure out the delimiting chars
4050        const lines = contents.split("\n");
4051        let i = 0;
4052
4053        const markerPositions = new ts.Map<string, Marker>();
4054        const markers: Marker[] = [];
4055        const ranges: Range[] = [];
4056
4057        // Stuff related to the subfile we're parsing
4058        let currentFileContent: string | undefined;
4059        let currentFileName = fileName;
4060        let currentFileSymlinks: string[] | undefined;
4061        let currentFileOptions: { [s: string]: string } = {};
4062
4063        function nextFile() {
4064            if (currentFileContent === undefined) return;
4065
4066            const file = parseFileContent(currentFileContent, currentFileName, markerPositions, markers, ranges);
4067            file.fileOptions = currentFileOptions;
4068            file.symlinks = currentFileSymlinks;
4069
4070            // Store result file
4071            files.push(file);
4072
4073            currentFileContent = undefined;
4074            currentFileOptions = {};
4075            currentFileName = fileName;
4076            currentFileSymlinks = undefined;
4077        }
4078
4079        for (let line of lines) {
4080            i++;
4081            if (line.length > 0 && line.charAt(line.length - 1) === "\r") {
4082                line = line.substr(0, line.length - 1);
4083            }
4084
4085            if (line.substr(0, 4) === "////") {
4086                const text = line.substr(4);
4087                currentFileContent = currentFileContent === undefined ? text : currentFileContent + "\n" + text;
4088            }
4089            else if (line.substr(0, 3) === "///" && currentFileContent !== undefined) {
4090                throw new Error("Three-slash line in the middle of four-slash region at line " + i);
4091            }
4092            else if (line.substr(0, 2) === "//") {
4093                const possiblySymlinks = Harness.TestCaseParser.parseSymlinkFromTest(line, symlinks);
4094                if (possiblySymlinks) {
4095                    symlinks = possiblySymlinks;
4096                }
4097                else {
4098                    // Comment line, check for global/file @options and record them
4099                    const match = optionRegex.exec(line.substr(2));
4100                    if (match) {
4101                        const key = match[1].toLowerCase();
4102                        const value = match[2];
4103                        if (!ts.contains(fileMetadataNames, key)) {
4104                            // Check if the match is already existed in the global options
4105                            if (globalOptions[key] !== undefined) {
4106                                throw new Error(`Global option '${key}' already exists`);
4107                            }
4108                            globalOptions[key] = value;
4109                        }
4110                        else {
4111                            switch (key) {
4112                                case MetadataOptionNames.fileName:
4113                                    // Found an @FileName directive, if this is not the first then create a new subfile
4114                                    nextFile();
4115                                    currentFileName = ts.isRootedDiskPath(value) ? value : basePath + "/" + value;
4116                                    currentFileOptions[key] = value;
4117                                    break;
4118                                case MetadataOptionNames.symlink:
4119                                    currentFileSymlinks = ts.append(currentFileSymlinks, value);
4120                                    break;
4121                                default:
4122                                    // Add other fileMetadata flag
4123                                    currentFileOptions[key] = value;
4124                            }
4125                        }
4126                    }
4127                }
4128            }
4129            // Previously blank lines between fourslash content caused it to be considered as 2 files,
4130            // Remove this behavior since it just causes errors now
4131            else if (line !== "") {
4132                // Code line, terminate current subfile if there is one
4133                nextFile();
4134            }
4135        }
4136
4137        // @Filename is the only directive that can be used in a test that contains tsconfig.json file.
4138        const config = ts.find(files, isConfig);
4139        if (config) {
4140            let directive = getNonFileNameOptionInFileList(files);
4141            if (!directive) {
4142                directive = getNonFileNameOptionInObject(globalOptions);
4143            }
4144            if (directive) {
4145                throw Error(`It is not allowed to use ${config.fileName} along with directive '${directive}'`);
4146            }
4147        }
4148
4149        return {
4150            markerPositions,
4151            markers,
4152            globalOptions,
4153            files,
4154            symlinks,
4155            ranges
4156        };
4157    }
4158
4159    function isConfig(file: FourSlashFile): boolean {
4160        return Harness.getConfigNameFromFileName(file.fileName) !== undefined;
4161    }
4162
4163    function getNonFileNameOptionInFileList(files: FourSlashFile[]): string | undefined {
4164        return ts.forEach(files, f => getNonFileNameOptionInObject(f.fileOptions));
4165    }
4166
4167    function getNonFileNameOptionInObject(optionObject: { [s: string]: string }): string | undefined {
4168        for (const option in optionObject) {
4169            switch (option) {
4170                case MetadataOptionNames.fileName:
4171                case MetadataOptionNames.baselineFile:
4172                case MetadataOptionNames.emitThisFile:
4173                    break;
4174                default:
4175                    return option;
4176            }
4177        }
4178        return undefined;
4179    }
4180
4181    const enum State {
4182        none,
4183        inSlashStarMarker,
4184        inObjectMarker
4185    }
4186
4187    function reportError(fileName: string, line: number, col: number, message: string) {
4188        const errorMessage = fileName + "(" + line + "," + col + "): " + message;
4189        throw new Error(errorMessage);
4190    }
4191
4192    function recordObjectMarker(fileName: string, location: LocationInformation, text: string, markerMap: ts.ESMap<string, Marker>, markers: Marker[]): Marker | undefined {
4193        let markerValue: any;
4194        try {
4195            // Attempt to parse the marker value as JSON
4196            markerValue = JSON.parse("{ " + text + " }");
4197        }
4198        catch (e) {
4199            reportError(fileName, location.sourceLine, location.sourceColumn, "Unable to parse marker text " + e.message);
4200        }
4201
4202        if (markerValue === undefined) {
4203            reportError(fileName, location.sourceLine, location.sourceColumn, "Object markers can not be empty");
4204            return undefined;
4205        }
4206
4207        const marker: Marker = {
4208            fileName,
4209            position: location.position,
4210            data: markerValue
4211        };
4212
4213        // Object markers can be anonymous
4214        if (markerValue.name) {
4215            markerMap.set(markerValue.name, marker);
4216        }
4217
4218        markers.push(marker);
4219
4220        return marker;
4221    }
4222
4223    function recordMarker(fileName: string, location: LocationInformation, name: string, markerMap: ts.ESMap<string, Marker>, markers: Marker[]): Marker | undefined {
4224        const marker: Marker = {
4225            fileName,
4226            position: location.position
4227        };
4228
4229        // Verify markers for uniqueness
4230        if (markerMap.has(name)) {
4231            const message = "Marker '" + name + "' is duplicated in the source file contents.";
4232            reportError(marker.fileName, location.sourceLine, location.sourceColumn, message);
4233            return undefined;
4234        }
4235        else {
4236            markerMap.set(name, marker);
4237            markers.push(marker);
4238            return marker;
4239        }
4240    }
4241
4242    function parseFileContent(content: string, fileName: string, markerMap: ts.ESMap<string, Marker>, markers: Marker[], ranges: Range[]): FourSlashFile {
4243        content = chompLeadingSpace(content);
4244
4245        // Any slash-star comment with a character not in this string is not a marker.
4246        const validMarkerChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$1234567890_";
4247
4248        /// The file content (minus metacharacters) so far
4249        let output = "";
4250
4251        /// The current marker (or maybe multi-line comment?) we're parsing, possibly
4252        let openMarker: LocationInformation | undefined;
4253
4254        /// A stack of the open range markers that are still unclosed
4255        const openRanges: RangeLocationInformation[] = [];
4256
4257        /// A list of ranges we've collected so far */
4258        let localRanges: Range[] = [];
4259
4260        /// The latest position of the start of an unflushed plain text area
4261        let lastNormalCharPosition = 0;
4262
4263        /// The total number of metacharacters removed from the file (so far)
4264        let difference = 0;
4265
4266        /// The fourslash file state object we are generating
4267        let state: State = State.none;
4268
4269        /// Current position data
4270        let line = 1;
4271        let column = 1;
4272
4273        const flush = (lastSafeCharIndex: number | undefined) => {
4274            output = output + content.substr(lastNormalCharPosition, lastSafeCharIndex === undefined ? undefined : lastSafeCharIndex - lastNormalCharPosition);
4275        };
4276
4277        if (content.length > 0) {
4278            let previousChar = content.charAt(0);
4279            for (let i = 1; i < content.length; i++) {
4280                const currentChar = content.charAt(i);
4281                switch (state) {
4282                    case State.none:
4283                        if (previousChar === "[" && currentChar === "|") {
4284                            // found a range start
4285                            openRanges.push({
4286                                position: (i - 1) - difference,
4287                                sourcePosition: i - 1,
4288                                sourceLine: line,
4289                                sourceColumn: column,
4290                            });
4291                            // copy all text up to marker position
4292                            flush(i - 1);
4293                            lastNormalCharPosition = i + 1;
4294                            difference += 2;
4295                        }
4296                        else if (previousChar === "|" && currentChar === "]") {
4297                            // found a range end
4298                            const rangeStart = openRanges.pop();
4299                            if (!rangeStart) {
4300                                throw reportError(fileName, line, column, "Found range end with no matching start.");
4301                            }
4302
4303                            const range: Range = {
4304                                fileName,
4305                                pos: rangeStart.position,
4306                                end: (i - 1) - difference,
4307                                marker: rangeStart.marker
4308                            };
4309                            localRanges.push(range);
4310
4311                            // copy all text up to range marker position
4312                            flush(i - 1);
4313                            lastNormalCharPosition = i + 1;
4314                            difference += 2;
4315                        }
4316                        else if (previousChar === "/" && currentChar === "*") {
4317                            // found a possible marker start
4318                            state = State.inSlashStarMarker;
4319                            openMarker = {
4320                                position: (i - 1) - difference,
4321                                sourcePosition: i - 1,
4322                                sourceLine: line,
4323                                sourceColumn: column,
4324                            };
4325                        }
4326                        else if (previousChar === "{" && currentChar === "|") {
4327                            // found an object marker start
4328                            state = State.inObjectMarker;
4329                            openMarker = {
4330                                position: (i - 1) - difference,
4331                                sourcePosition: i - 1,
4332                                sourceLine: line,
4333                                sourceColumn: column,
4334                            };
4335                            flush(i - 1);
4336                        }
4337                        break;
4338
4339                    case State.inObjectMarker:
4340                        // Object markers are only ever terminated by |} and have no content restrictions
4341                        if (previousChar === "|" && currentChar === "}") {
4342                            // Record the marker
4343                            const objectMarkerNameText = content.substring(openMarker!.sourcePosition + 2, i - 1).trim();
4344                            const marker = recordObjectMarker(fileName, openMarker!, objectMarkerNameText, markerMap, markers);
4345
4346                            if (openRanges.length > 0) {
4347                                openRanges[openRanges.length - 1].marker = marker;
4348                            }
4349
4350                            // Set the current start to point to the end of the current marker to ignore its text
4351                            lastNormalCharPosition = i + 1;
4352                            difference += i + 1 - openMarker!.sourcePosition;
4353
4354                            // Reset the state
4355                            openMarker = undefined;
4356                            state = State.none;
4357                        }
4358                        break;
4359
4360                    case State.inSlashStarMarker:
4361                        if (previousChar === "*" && currentChar === "/") {
4362                            // Record the marker
4363                            // start + 2 to ignore the */, -1 on the end to ignore the * (/ is next)
4364                            const markerNameText = content.substring(openMarker!.sourcePosition + 2, i - 1).trim();
4365                            const marker = recordMarker(fileName, openMarker!, markerNameText, markerMap, markers);
4366
4367                            if (openRanges.length > 0) {
4368                                openRanges[openRanges.length - 1].marker = marker;
4369                            }
4370
4371                            // Set the current start to point to the end of the current marker to ignore its text
4372                            flush(openMarker!.sourcePosition);
4373                            lastNormalCharPosition = i + 1;
4374                            difference += i + 1 - openMarker!.sourcePosition;
4375
4376                            // Reset the state
4377                            openMarker = undefined;
4378                            state = State.none;
4379                        }
4380                        else if (validMarkerChars.indexOf(currentChar) < 0) {
4381                            if (currentChar === "*" && i < content.length - 1 && content.charAt(i + 1) === "/") {
4382                                // The marker is about to be closed, ignore the 'invalid' char
4383                            }
4384                            else {
4385                                // We've hit a non-valid marker character, so we were actually in a block comment
4386                                // Bail out the text we've gathered so far back into the output
4387                                flush(i);
4388                                lastNormalCharPosition = i;
4389                                openMarker = undefined;
4390
4391                                state = State.none;
4392                            }
4393                        }
4394                        break;
4395                }
4396
4397                if (currentChar === "\n" && previousChar === "\r") {
4398                    // Ignore trailing \n after a \r
4399                    continue;
4400                }
4401                else if (currentChar === "\n" || currentChar === "\r") {
4402                    line++;
4403                    column = 1;
4404                    continue;
4405                }
4406
4407                column++;
4408                previousChar = currentChar;
4409            }
4410        }
4411
4412        // Add the remaining text
4413        flush(/*lastSafeCharIndex*/ undefined);
4414
4415        if (openRanges.length > 0) {
4416            const openRange = openRanges[0];
4417            reportError(fileName, openRange.sourceLine, openRange.sourceColumn, "Unterminated range.");
4418        }
4419
4420        if (openMarker) {
4421            reportError(fileName, openMarker.sourceLine, openMarker.sourceColumn, "Unterminated marker.");
4422        }
4423
4424        // put ranges in the correct order
4425        localRanges = localRanges.sort((a, b) => a.pos < b.pos ? -1 : a.pos === b.pos && a.end > b.end ? -1 : 1);
4426        localRanges.forEach((r) => { ranges.push(r); });
4427
4428        return {
4429            content: output,
4430            fileOptions: {},
4431            version: 0,
4432            fileName,
4433        };
4434    }
4435
4436    function stringify(data: any, replacer?: (key: string, value: any) => any): string {
4437        return JSON.stringify(data, replacer, 2);
4438    }
4439
4440    /** Collects an array of unique outputs. */
4441    function unique<T>(inputs: readonly T[], getOutput: (t: T) => string): string[] {
4442        const set = new ts.Map<string, true>();
4443        for (const input of inputs) {
4444            const out = getOutput(input);
4445            set.set(out, true);
4446        }
4447        return ts.arrayFrom(set.keys());
4448    }
4449
4450    function toArray<T>(x: ArrayOrSingle<T>): readonly T[] {
4451        return ts.isArray(x) ? x : [x];
4452    }
4453
4454    function makeWhitespaceVisible(text: string) {
4455        return text.replace(/ /g, "\u00B7").replace(/\r/g, "\u00B6").replace(/\n/g, "\u2193\n").replace(/\t/g, "\u2192\   ");
4456    }
4457
4458    function showTextDiff(expected: string, actual: string): string {
4459        // Only show whitespace if the difference is whitespace-only.
4460        if (differOnlyByWhitespace(expected, actual)) {
4461            expected = makeWhitespaceVisible(expected);
4462            actual = makeWhitespaceVisible(actual);
4463        }
4464        return displayExpectedAndActualString(expected, actual);
4465    }
4466
4467    function differOnlyByWhitespace(a: string, b: string) {
4468        return stripWhitespace(a) === stripWhitespace(b);
4469    }
4470
4471    function stripWhitespace(s: string): string {
4472        return s.replace(/\s/g, "");
4473    }
4474
4475    function findDuplicatedElement<T>(a: readonly T[], equal: (a: T, b: T) => boolean): T | undefined {
4476        for (let i = 0; i < a.length; i++) {
4477            for (let j = i + 1; j < a.length; j++) {
4478                if (equal(a[i], a[j])) {
4479                    return a[i];
4480                }
4481            }
4482        }
4483    }
4484
4485    function displayExpectedAndActualString(expected: string, actual: string, quoted = false) {
4486        const expectMsg = "\x1b[1mExpected\x1b[0m\x1b[31m";
4487        const actualMsg = "\x1b[1mActual\x1b[0m\x1b[31m";
4488        const expectedString = quoted ? "\"" + expected + "\"" : expected;
4489        const actualString = quoted ? "\"" + actual + "\"" : actual;
4490        return `\n${expectMsg}:\n${expectedString}\n\n${actualMsg}:\n${highlightDifferenceBetweenStrings(expected, actualString)}`;
4491    }
4492
4493    function templateToRegExp(template: string) {
4494        return new RegExp(`^${ts.regExpEscape(template).replace(/\\\{\d+\\\}/g, ".*?")}$`);
4495    }
4496
4497    function rangesOfDiffBetweenTwoStrings(source: string, target: string) {
4498        const ranges = [] as { start: number; length: number }[];
4499
4500        const addToIndex = (index: number) => {
4501            const closestIndex = ranges[ranges.length - 1];
4502            if (closestIndex) {
4503                const doesAddToIndex = closestIndex.start + closestIndex.length === index - 1;
4504                if (doesAddToIndex) {
4505                    closestIndex.length = closestIndex.length + 1;
4506                }
4507            else {
4508                ranges.push({ start: index - 1, length: 1 });
4509            }
4510          }
4511          else {
4512                ranges.push({ start: index - 1, length: 1 });
4513          }
4514        };
4515
4516        for (let index = 0; index < Math.max(source.length, target.length); index++) {
4517            const srcChar = source[index];
4518            const targetChar = target[index];
4519            if (srcChar !== targetChar) addToIndex(index);
4520        }
4521
4522        return ranges;
4523      }
4524
4525      // Adds an _ when the source string and the target string have a whitespace difference
4526      function highlightDifferenceBetweenStrings(source: string, target: string) {
4527        const ranges = rangesOfDiffBetweenTwoStrings(source, target);
4528        let emTarget = target;
4529        ranges.forEach((range, index) => {
4530            const lhs = `\x1b[4m`;
4531            const rhs = `\x1b[0m\x1b[31m`;
4532            const additionalOffset = index * lhs.length + index * rhs.length;
4533            const before = emTarget.slice(0, range.start + 1 + additionalOffset);
4534            const between = emTarget.slice(
4535                range.start + 1 + additionalOffset,
4536                range.start + range.length + 1 + additionalOffset
4537            );
4538             const after = emTarget.slice(range.start + range.length + 1 + additionalOffset, emTarget.length);
4539            emTarget = before + lhs + between + rhs + after;
4540        });
4541        return emTarget;
4542      }
4543}
4544