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