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