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