• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.server {
2    export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
3    /*@internal*/
4    export const maxFileSize = 4 * 1024 * 1024;
5
6    export const ProjectsUpdatedInBackgroundEvent = "projectsUpdatedInBackground";
7    export const ProjectLoadingStartEvent = "projectLoadingStart";
8    export const ProjectLoadingFinishEvent = "projectLoadingFinish";
9    export const LargeFileReferencedEvent = "largeFileReferenced";
10    export const ConfigFileDiagEvent = "configFileDiag";
11    export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState";
12    export const ProjectInfoTelemetryEvent = "projectInfo";
13    export const OpenFileInfoTelemetryEvent = "openFileInfo";
14    const ensureProjectForOpenFileSchedule = "*ensureProjectForOpenFiles*";
15
16    export interface ProjectsUpdatedInBackgroundEvent {
17        eventName: typeof ProjectsUpdatedInBackgroundEvent;
18        data: { openFiles: string[]; };
19    }
20
21    export interface ProjectLoadingStartEvent {
22        eventName: typeof ProjectLoadingStartEvent;
23        data: { project: Project; reason: string; };
24    }
25
26    export interface ProjectLoadingFinishEvent {
27        eventName: typeof ProjectLoadingFinishEvent;
28        data: { project: Project; };
29    }
30
31    export interface LargeFileReferencedEvent {
32        eventName: typeof LargeFileReferencedEvent;
33        data: { file: string; fileSize: number; maxFileSize: number; };
34    }
35
36    export interface ConfigFileDiagEvent {
37        eventName: typeof ConfigFileDiagEvent;
38        data: { triggerFile: string, configFileName: string, diagnostics: readonly Diagnostic[] };
39    }
40
41    export interface ProjectLanguageServiceStateEvent {
42        eventName: typeof ProjectLanguageServiceStateEvent;
43        data: { project: Project, languageServiceEnabled: boolean };
44    }
45
46    /** This will be converted to the payload of a protocol.TelemetryEvent in session.defaultEventHandler. */
47    export interface ProjectInfoTelemetryEvent {
48        readonly eventName: typeof ProjectInfoTelemetryEvent;
49        readonly data: ProjectInfoTelemetryEventData;
50    }
51
52    /*
53     * __GDPR__
54     * "projectInfo" : {
55     *      "${include}": ["${TypeScriptCommonProperties}"],
56     *      "projectId": { "classification": "EndUserPseudonymizedInformation", "purpose": "FeatureInsight", "endpoint": "ProjectId" },
57     *      "fileStats": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
58     *      "compilerOptions": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
59     *      "extends": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
60     *      "files": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
61     *      "include": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
62     *      "exclude": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
63     *      "compileOnSave": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
64     *      "typeAcquisition": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
65     *      "configFileName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
66     *      "projectType": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
67     *      "languageServiceEnabled": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
68     *      "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
69     * }
70     */
71    export interface ProjectInfoTelemetryEventData {
72        /** Cryptographically secure hash of project file location. */
73        readonly projectId: string;
74        /** Count of file extensions seen in the project. */
75        readonly fileStats: FileStats;
76        /**
77         * Any compiler options that might contain paths will be taken out.
78         * Enum compiler options will be converted to strings.
79         */
80        readonly compilerOptions: CompilerOptions;
81        // "extends", "files", "include", or "exclude" will be undefined if an external config is used.
82        // Otherwise, we will use "true" if the property is present and "false" if it is missing.
83        readonly extends: boolean | undefined;
84        readonly files: boolean | undefined;
85        readonly include: boolean | undefined;
86        readonly exclude: boolean | undefined;
87        readonly compileOnSave: boolean;
88        readonly typeAcquisition: ProjectInfoTypeAcquisitionData;
89
90        readonly configFileName: "tsconfig.json" | "jsconfig.json" | "other";
91        readonly projectType: "external" | "configured";
92        readonly languageServiceEnabled: boolean;
93        /** TypeScript version used by the server. */
94        readonly version: string;
95    }
96
97    /**
98     * Info that we may send about a file that was just opened.
99     * Info about a file will only be sent once per session, even if the file changes in ways that might affect the info.
100     * Currently this is only sent for '.js' files.
101     */
102    export interface OpenFileInfoTelemetryEvent {
103        readonly eventName: typeof OpenFileInfoTelemetryEvent;
104        readonly data: OpenFileInfoTelemetryEventData;
105    }
106
107    export interface OpenFileInfoTelemetryEventData {
108        readonly info: OpenFileInfo;
109    }
110
111    export interface ProjectInfoTypeAcquisitionData {
112        readonly enable: boolean | undefined;
113        // Actual values of include/exclude entries are scrubbed.
114        readonly include: boolean;
115        readonly exclude: boolean;
116    }
117
118    export interface FileStats {
119        readonly js: number;
120        readonly jsSize?: number;
121
122        readonly jsx: number;
123        readonly jsxSize?: number;
124
125        readonly ts: number;
126        readonly tsSize?: number;
127
128        readonly tsx: number;
129        readonly tsxSize?: number;
130
131        readonly dts: number;
132        readonly dtsSize?: number;
133
134        readonly deferred: number;
135        readonly deferredSize?: number;
136
137        readonly ets: number;
138        readonly etsSize?: number;
139
140        readonly dets: number;
141        readonly detsSize?: number;
142    }
143
144    export interface OpenFileInfo {
145        readonly checkJs: boolean;
146    }
147
148    export type ProjectServiceEvent =
149        LargeFileReferencedEvent
150        | ProjectsUpdatedInBackgroundEvent
151        | ProjectLoadingStartEvent
152        | ProjectLoadingFinishEvent
153        | ConfigFileDiagEvent
154        | ProjectLanguageServiceStateEvent
155        | ProjectInfoTelemetryEvent
156        | OpenFileInfoTelemetryEvent;
157
158    export type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void;
159
160    /*@internal*/
161    export type PerformanceEventHandler = (event: PerformanceEvent) => void;
162
163    export interface SafeList {
164        [name: string]: { match: RegExp, exclude?: (string | number)[][], types?: string[] };
165    }
166
167    function prepareConvertersForEnumLikeCompilerOptions(commandLineOptions: CommandLineOption[]): ESMap<string, ESMap<string, number>> {
168        const map = new Map<string, ESMap<string, number>>();
169        for (const option of commandLineOptions) {
170            if (typeof option.type === "object") {
171                const optionMap = <ESMap<string, number>>option.type;
172                // verify that map contains only numbers
173                optionMap.forEach(value => {
174                    Debug.assert(typeof value === "number");
175                });
176                map.set(option.name, optionMap);
177            }
178        }
179        return map;
180    }
181
182    const compilerOptionConverters = prepareConvertersForEnumLikeCompilerOptions(optionDeclarations);
183    const watchOptionsConverters = prepareConvertersForEnumLikeCompilerOptions(optionsForWatch);
184    const indentStyle = new Map(getEntries({
185        none: IndentStyle.None,
186        block: IndentStyle.Block,
187        smart: IndentStyle.Smart
188    }));
189
190    export interface TypesMapFile {
191        typesMap: SafeList;
192        simpleMap: { [libName: string]: string };
193    }
194
195    /**
196     * How to understand this block:
197     *  * The 'match' property is a regexp that matches a filename.
198     *  * If 'match' is successful, then:
199     *     * All files from 'exclude' are removed from the project. See below.
200     *     * All 'types' are included in ATA
201     *  * What the heck is 'exclude' ?
202     *     * An array of an array of strings and numbers
203     *     * Each array is:
204     *       * An array of strings and numbers
205     *       * The strings are literals
206     *       * The numbers refer to capture group indices from the 'match' regexp
207     *          * Remember that '1' is the first group
208     *       * These are concatenated together to form a new regexp
209     *       * Filenames matching these regexps are excluded from the project
210     * This default value is tested in tsserverProjectSystem.ts; add tests there
211     *   if you are changing this so that you can be sure your regexp works!
212     */
213    const defaultTypeSafeList: SafeList = {
214        "jquery": {
215            // jquery files can have names like "jquery-1.10.2.min.js" (or "jquery.intellisense.js")
216            match: /jquery(-[\d\.]+)?(\.intellisense)?(\.min)?\.js$/i,
217            types: ["jquery"]
218        },
219        "WinJS": {
220            // e.g. c:/temp/UWApp1/lib/winjs-4.0.1/js/base.js
221            match: /^(.*\/winjs-[.\d]+)\/js\/base\.js$/i,        // If the winjs/base.js file is found..
222            exclude: [["^", 1, "/.*"]],                // ..then exclude all files under the winjs folder
223            types: ["winjs"]                           // And fetch the @types package for WinJS
224        },
225        "Kendo": {
226            // e.g. /Kendo3/wwwroot/lib/kendo/kendo.all.min.js
227            match: /^(.*\/kendo(-ui)?)\/kendo\.all(\.min)?\.js$/i,
228            exclude: [["^", 1, "/.*"]],
229            types: ["kendo-ui"]
230        },
231        "Office Nuget": {
232            // e.g. /scripts/Office/1/excel-15.debug.js
233            match: /^(.*\/office\/1)\/excel-\d+\.debug\.js$/i, // Office NuGet package is installed under a "1/office" folder
234            exclude: [["^", 1, "/.*"]],                     // Exclude that whole folder if the file indicated above is found in it
235            types: ["office"]                               // @types package to fetch instead
236        },
237        "References": {
238            match: /^(.*\/_references\.js)$/i,
239            exclude: [["^", 1, "$"]]
240        }
241    };
242
243    export function convertFormatOptions(protocolOptions: protocol.FormatCodeSettings): FormatCodeSettings {
244        if (isString(protocolOptions.indentStyle)) {
245            protocolOptions.indentStyle = indentStyle.get(protocolOptions.indentStyle.toLowerCase());
246            Debug.assert(protocolOptions.indentStyle !== undefined);
247        }
248        return <any>protocolOptions;
249    }
250
251    export function convertCompilerOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): CompilerOptions & protocol.CompileOnSaveMixin {
252        compilerOptionConverters.forEach((mappedValues, id) => {
253            const propertyValue = protocolOptions[id];
254            if (isString(propertyValue)) {
255                protocolOptions[id] = mappedValues.get(propertyValue.toLowerCase());
256            }
257        });
258        return <any>protocolOptions;
259    }
260
261    export function convertWatchOptions(protocolOptions: protocol.ExternalProjectCompilerOptions, currentDirectory?: string): WatchOptionsAndErrors | undefined {
262        let watchOptions: WatchOptions | undefined;
263        let errors: Diagnostic[] | undefined;
264        optionsForWatch.forEach(option => {
265            const propertyValue = protocolOptions[option.name];
266            if (propertyValue === undefined) return;
267            const mappedValues = watchOptionsConverters.get(option.name);
268            (watchOptions || (watchOptions = {}))[option.name] = mappedValues ?
269                isString(propertyValue) ? mappedValues.get(propertyValue.toLowerCase()) : propertyValue :
270                convertJsonOption(option, propertyValue, currentDirectory || "", errors || (errors = []));
271        });
272        return watchOptions && { watchOptions, errors };
273    }
274
275    export function convertTypeAcquisition(protocolOptions: protocol.InferredProjectCompilerOptions): TypeAcquisition | undefined {
276        let result: TypeAcquisition | undefined;
277        typeAcquisitionDeclarations.forEach((option) => {
278            const propertyValue = protocolOptions[option.name];
279            if (propertyValue === undefined) return;
280            (result || (result = {}))[option.name] = propertyValue;
281        });
282        return result;
283    }
284
285    export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind {
286        return isString(scriptKindName) ? convertScriptKindName(scriptKindName) : scriptKindName;
287    }
288
289    export function convertScriptKindName(scriptKindName: protocol.ScriptKindName) {
290        switch (scriptKindName) {
291            case "JS":
292                return ScriptKind.JS;
293            case "JSX":
294                return ScriptKind.JSX;
295            case "TS":
296                return ScriptKind.TS;
297            case "TSX":
298                return ScriptKind.TSX;
299            case "ETS":
300                return ScriptKind.ETS;
301            default:
302                return ScriptKind.Unknown;
303        }
304    }
305
306    /*@internal*/
307    export function convertUserPreferences(preferences: protocol.UserPreferences): UserPreferences {
308        const { lazyConfiguredProjectsFromExternalProject, ...userPreferences } = preferences;
309        return userPreferences;
310    }
311
312    export interface HostConfiguration {
313        formatCodeOptions: FormatCodeSettings;
314        preferences: protocol.UserPreferences;
315        hostInfo: string;
316        extraFileExtensions?: FileExtensionInfo[];
317        watchOptions?: WatchOptions;
318    }
319
320    export interface OpenConfiguredProjectResult {
321        configFileName?: NormalizedPath;
322        configFileErrors?: readonly Diagnostic[];
323    }
324
325    interface AssignProjectResult extends OpenConfiguredProjectResult {
326        retainProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined;
327    }
328
329    interface FilePropertyReader<T> {
330        getFileName(f: T): string;
331        getScriptKind(f: T, extraFileExtensions?: FileExtensionInfo[]): ScriptKind;
332        hasMixedContent(f: T, extraFileExtensions: FileExtensionInfo[] | undefined): boolean;
333    }
334
335    const fileNamePropertyReader: FilePropertyReader<string> = {
336        getFileName: x => x,
337        getScriptKind: (fileName, extraFileExtensions) => {
338            let result: ScriptKind | undefined;
339            if (extraFileExtensions) {
340                const fileExtension = getAnyExtensionFromPath(fileName);
341                if (fileExtension) {
342                    some(extraFileExtensions, info => {
343                        if (info.extension === fileExtension) {
344                            result = info.scriptKind;
345                            return true;
346                        }
347                        return false;
348                    });
349                }
350            }
351            return result!; // TODO: GH#18217
352        },
353        hasMixedContent: (fileName, extraFileExtensions) => some(extraFileExtensions, ext => ext.isMixedContent && fileExtensionIs(fileName, ext.extension)),
354    };
355
356    const externalFilePropertyReader: FilePropertyReader<protocol.ExternalFile> = {
357        getFileName: x => x.fileName,
358        getScriptKind: x => tryConvertScriptKindName(x.scriptKind!), // TODO: GH#18217
359        hasMixedContent: x => !!x.hasMixedContent,
360    };
361
362    function findProjectByName<T extends Project>(projectName: string, projects: T[]): T | undefined {
363        for (const proj of projects) {
364            if (proj.getProjectName() === projectName) {
365                return proj;
366            }
367        }
368    }
369
370    const enum ConfigFileWatcherStatus {
371        ReloadingFiles = "Reloading configured projects for files",
372        ReloadingInferredRootFiles = "Reloading configured projects for only inferred root files",
373        UpdatedCallback = "Updated the callback",
374        OpenFilesImpactedByConfigFileAdd = "File added to open files impacted by this config file",
375        OpenFilesImpactedByConfigFileRemove = "File removed from open files impacted by this config file",
376        RootOfInferredProjectTrue = "Open file was set as Inferred root",
377        RootOfInferredProjectFalse = "Open file was set as not inferred root",
378    }
379
380    /*@internal*/
381    interface ConfigFileExistenceInfo {
382        /**
383         * Cached value of existence of config file
384         * It is true if there is configured project open for this file.
385         * It can be either true or false if this is the config file that is being watched by inferred project
386         *   to decide when to update the structure so that it knows about updating the project for its files
387         *   (config file may include the inferred project files after the change and hence may be wont need to be in inferred project)
388         */
389        exists: boolean;
390        /**
391         * openFilesImpactedByConfigFiles is a map of open files that would be impacted by this config file
392         *   because these are the paths being looked up for their default configured project location
393         * The value in the map is true if the open file is root of the inferred project
394         * It is false when the open file that would still be impacted by existence of
395         *   this config file but it is not the root of inferred project
396         */
397        openFilesImpactedByConfigFile: ESMap<Path, boolean>;
398        /**
399         * The file watcher watching the config file because there is open script info that is root of
400         * inferred project and will be impacted by change in the status of the config file
401         * The watcher is present only when there is no open configured project for the config file
402         */
403        configFileWatcherForRootOfInferredProject?: FileWatcher;
404    }
405
406    export interface ProjectServiceOptions {
407        host: ServerHost;
408        logger: Logger;
409        cancellationToken: HostCancellationToken;
410        useSingleInferredProject: boolean;
411        useInferredProjectPerProjectRoot: boolean;
412        typingsInstaller: ITypingsInstaller;
413        eventHandler?: ProjectServiceEventHandler;
414        suppressDiagnosticEvents?: boolean;
415        throttleWaitMilliseconds?: number;
416        globalPlugins?: readonly string[];
417        pluginProbeLocations?: readonly string[];
418        allowLocalPluginLoads?: boolean;
419        typesMapLocation?: string;
420        /** @deprecated use serverMode instead */
421        syntaxOnly?: boolean;
422        serverMode?: LanguageServiceMode;
423    }
424
425    interface OriginalFileInfo { fileName: NormalizedPath; path: Path; }
426    interface AncestorConfigFileInfo {
427        /** config file name */
428        fileName: string;
429        /** path of open file so we can look at correct root */
430        path: Path;
431        configFileInfo: true;
432    }
433    type OpenScriptInfoOrClosedFileInfo = ScriptInfo | OriginalFileInfo;
434    type OpenScriptInfoOrClosedOrConfigFileInfo = OpenScriptInfoOrClosedFileInfo | AncestorConfigFileInfo;
435
436    function isOpenScriptInfo(infoOrFileNameOrConfig: OpenScriptInfoOrClosedOrConfigFileInfo): infoOrFileNameOrConfig is ScriptInfo {
437        return !!(infoOrFileNameOrConfig as ScriptInfo).containingProjects;
438    }
439
440    function isAncestorConfigFileInfo(infoOrFileNameOrConfig: OpenScriptInfoOrClosedOrConfigFileInfo): infoOrFileNameOrConfig is AncestorConfigFileInfo {
441        return !!(infoOrFileNameOrConfig as AncestorConfigFileInfo).configFileInfo;
442    }
443
444    /*@internal*/
445    /** Kind of operation to perform to get project reference project */
446    export enum ProjectReferenceProjectLoadKind {
447        /** Find existing project for project reference */
448        Find,
449        /** Find existing project or create one for the project reference */
450        FindCreate,
451        /** Find existing project or create and load it for the project reference */
452        FindCreateLoad
453    }
454
455    /*@internal*/
456    export function forEachResolvedProjectReferenceProject<T>(
457        project: ConfiguredProject,
458        fileName: string | undefined,
459        cb: (child: ConfiguredProject) => T | undefined,
460        projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind.Find | ProjectReferenceProjectLoadKind.FindCreate,
461    ): T | undefined;
462    /*@internal*/
463    export function forEachResolvedProjectReferenceProject<T>(
464        project: ConfiguredProject,
465        fileName: string | undefined,
466        cb: (child: ConfiguredProject) => T | undefined,
467        projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind,
468        reason: string
469    ): T | undefined;
470    export function forEachResolvedProjectReferenceProject<T>(
471        project: ConfiguredProject,
472        fileName: string | undefined,
473        cb: (child: ConfiguredProject) => T | undefined,
474        projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind,
475        reason?: string
476    ): T | undefined {
477        const resolvedRefs = project.getCurrentProgram()?.getResolvedProjectReferences();
478        if (!resolvedRefs) return undefined;
479        let seenResolvedRefs: ESMap<string, ProjectReferenceProjectLoadKind> | undefined;
480        const possibleDefaultRef = fileName ? project.getResolvedProjectReferenceToRedirect(fileName) : undefined;
481        if (possibleDefaultRef) {
482            // Try to find the name of the file directly through resolved project references
483            const configFileName = toNormalizedPath(possibleDefaultRef.sourceFile.fileName);
484            const child = project.projectService.findConfiguredProjectByProjectName(configFileName);
485            if (child) {
486                const result = cb(child);
487                if (result) return result;
488            }
489            else if (projectReferenceProjectLoadKind !== ProjectReferenceProjectLoadKind.Find) {
490                seenResolvedRefs = new Map();
491                // Try to see if this project can be loaded
492                const result = forEachResolvedProjectReferenceProjectWorker(
493                    resolvedRefs,
494                    project.getCompilerOptions(),
495                    (ref, loadKind) => possibleDefaultRef === ref ? callback(ref, loadKind) : undefined,
496                    projectReferenceProjectLoadKind,
497                    project.projectService,
498                    seenResolvedRefs
499                );
500                if (result) return result;
501                // Cleanup seenResolvedRefs
502                seenResolvedRefs.clear();
503            }
504        }
505
506        return forEachResolvedProjectReferenceProjectWorker(
507            resolvedRefs,
508            project.getCompilerOptions(),
509            (ref, loadKind) => possibleDefaultRef !== ref ? callback(ref, loadKind) : undefined,
510            projectReferenceProjectLoadKind,
511            project.projectService,
512            seenResolvedRefs
513        );
514
515        function callback(ref: ResolvedProjectReference, loadKind: ProjectReferenceProjectLoadKind) {
516            const configFileName = toNormalizedPath(ref.sourceFile.fileName);
517            const child = project.projectService.findConfiguredProjectByProjectName(configFileName) || (
518                loadKind === ProjectReferenceProjectLoadKind.Find ?
519                    undefined :
520                    loadKind === ProjectReferenceProjectLoadKind.FindCreate ?
521                        project.projectService.createConfiguredProject(configFileName) :
522                        loadKind === ProjectReferenceProjectLoadKind.FindCreateLoad ?
523                            project.projectService.createAndLoadConfiguredProject(configFileName, reason!) :
524                            Debug.assertNever(loadKind)
525            );
526
527            return child && cb(child);
528        }
529    }
530
531    function forEachResolvedProjectReferenceProjectWorker<T>(
532        resolvedProjectReferences: readonly (ResolvedProjectReference | undefined)[],
533        parentOptions: CompilerOptions,
534        cb: (resolvedRef: ResolvedProjectReference, loadKind: ProjectReferenceProjectLoadKind) => T | undefined,
535        projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind,
536        projectService: ProjectService,
537        seenResolvedRefs: ESMap<string, ProjectReferenceProjectLoadKind> | undefined,
538    ): T | undefined {
539        const loadKind = parentOptions.disableReferencedProjectLoad ? ProjectReferenceProjectLoadKind.Find : projectReferenceProjectLoadKind;
540        return forEach(resolvedProjectReferences, ref => {
541            if (!ref) return undefined;
542
543            const configFileName = toNormalizedPath(ref.sourceFile.fileName);
544            const canonicalPath = projectService.toCanonicalFileName(configFileName);
545            const seenValue = seenResolvedRefs?.get(canonicalPath);
546            if (seenValue !== undefined && seenValue >= loadKind) {
547                return undefined;
548            }
549            const result = cb(ref, loadKind);
550            if (result) {
551                return result;
552            }
553
554            (seenResolvedRefs || (seenResolvedRefs = new Map())).set(canonicalPath, loadKind);
555            return ref.references && forEachResolvedProjectReferenceProjectWorker(ref.references, ref.commandLine.options, cb, loadKind, projectService, seenResolvedRefs);
556        });
557    }
558
559    function forEachPotentialProjectReference<T>(
560        project: ConfiguredProject,
561        cb: (potentialProjectReference: Path) => T | undefined
562    ): T | undefined {
563        return project.potentialProjectReferences &&
564            forEachKey(project.potentialProjectReferences, cb);
565    }
566
567    function forEachAnyProjectReferenceKind<T>(
568        project: ConfiguredProject,
569        cb: (resolvedProjectReference: ResolvedProjectReference) => T | undefined,
570        cbProjectRef: (projectReference: ProjectReference) => T | undefined,
571        cbPotentialProjectRef: (potentialProjectReference: Path) => T | undefined
572    ): T | undefined {
573        return project.getCurrentProgram() ?
574            project.forEachResolvedProjectReference(cb) :
575            project.isInitialLoadPending() ?
576                forEachPotentialProjectReference(project, cbPotentialProjectRef) :
577                forEach(project.getProjectReferences(), cbProjectRef);
578    }
579
580    function callbackRefProject<T>(
581        project: ConfiguredProject,
582        cb: (refProj: ConfiguredProject) => T | undefined,
583        refPath: Path | undefined
584    ) {
585        const refProject = refPath && project.projectService.configuredProjects.get(refPath);
586        return refProject && cb(refProject);
587    }
588
589    function forEachReferencedProject<T>(
590        project: ConfiguredProject,
591        cb: (refProj: ConfiguredProject) => T | undefined
592    ): T | undefined {
593        return forEachAnyProjectReferenceKind(
594            project,
595            resolvedRef => callbackRefProject(project, cb, resolvedRef.sourceFile.path),
596            projectRef => callbackRefProject(project, cb, project.toPath(resolveProjectReferencePath(projectRef))),
597            potentialProjectRef => callbackRefProject(project, cb, potentialProjectRef)
598        );
599    }
600
601    interface ScriptInfoInNodeModulesWatcher extends FileWatcher {
602        refCount: number;
603    }
604
605    function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) {
606        return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`;
607    }
608
609    function isScriptInfoWatchedFromNodeModules(info: ScriptInfo) {
610        return !info.isScriptOpen() && info.mTime !== undefined;
611    }
612
613    /*@internal*/
614    /** true if script info is part of project and is not in project because it is referenced from project reference source */
615    export function projectContainsInfoDirectly(project: Project, info: ScriptInfo) {
616        return project.containsScriptInfo(info) &&
617            !project.isSourceOfProjectReferenceRedirect(info.path);
618    }
619
620    /*@internal*/
621    export function updateProjectIfDirty(project: Project) {
622        project.invalidateResolutionsOfFailedLookupLocations();
623        return project.dirty && project.updateGraph();
624    }
625
626    function setProjectOptionsUsed(project: ConfiguredProject | ExternalProject) {
627        if (isConfiguredProject(project)) {
628            project.projectOptions = true;
629        }
630    }
631
632    /*@internal*/
633    export interface OpenFileArguments {
634        fileName: string;
635        content?: string;
636        scriptKind?: protocol.ScriptKindName | ScriptKind;
637        hasMixedContent?: boolean;
638        projectRootPath?: string;
639    }
640
641    /*@internal*/
642    export interface ChangeFileArguments {
643        fileName: string;
644        changes: Iterator<TextChange>;
645    }
646
647    export interface WatchOptionsAndErrors {
648        watchOptions: WatchOptions;
649        errors: Diagnostic[] | undefined;
650    }
651
652    export class ProjectService {
653
654        /*@internal*/
655        readonly typingsCache: TypingsCache;
656
657        /*@internal*/
658        readonly documentRegistry: DocumentRegistry;
659
660        /**
661         * Container of all known scripts
662         */
663        /*@internal*/
664        readonly filenameToScriptInfo = new Map<string, ScriptInfo>();
665        private readonly scriptInfoInNodeModulesWatchers = new Map<string, ScriptInfoInNodeModulesWatcher>();
666        /**
667         * Contains all the deleted script info's version information so that
668         * it does not reset when creating script info again
669         * (and could have potentially collided with version where contents mismatch)
670         */
671        private readonly filenameToScriptInfoVersion = new Map<string, ScriptInfoVersion>();
672        // Set of all '.js' files ever opened.
673        private readonly allJsFilesForOpenFileTelemetry = new Map<string, true>();
674
675        /**
676         * Map to the real path of the infos
677         */
678        /* @internal */
679        readonly realpathToScriptInfos: MultiMap<Path, ScriptInfo> | undefined;
680        /**
681         * maps external project file name to list of config files that were the part of this project
682         */
683        private readonly externalProjectToConfiguredProjectMap = new Map<string, NormalizedPath[]>();
684
685        /**
686         * external projects (configuration and list of root files is not controlled by tsserver)
687         */
688        readonly externalProjects: ExternalProject[] = [];
689        /**
690         * projects built from openFileRoots
691         */
692        readonly inferredProjects: InferredProject[] = [];
693        /**
694         * projects specified by a tsconfig.json file
695         */
696        readonly configuredProjects: Map<ConfiguredProject> = new Map<string, ConfiguredProject>();
697        /**
698         * Open files: with value being project root path, and key being Path of the file that is open
699         */
700        readonly openFiles: Map<NormalizedPath | undefined> = new Map<Path, NormalizedPath | undefined>();
701        /* @internal */
702        readonly configFileForOpenFiles: ESMap<Path, NormalizedPath | false> = new Map();
703        /**
704         * Map of open files that are opened without complete path but have projectRoot as current directory
705         */
706        private readonly openFilesWithNonRootedDiskPath = new Map<string, ScriptInfo>();
707
708        private compilerOptionsForInferredProjects: CompilerOptions | undefined;
709        private compilerOptionsForInferredProjectsPerProjectRoot = new Map<string, CompilerOptions>();
710        private watchOptionsForInferredProjects: WatchOptionsAndErrors | undefined;
711        private watchOptionsForInferredProjectsPerProjectRoot = new Map<string, WatchOptionsAndErrors | false>();
712        private typeAcquisitionForInferredProjects: TypeAcquisition | undefined;
713        private typeAcquisitionForInferredProjectsPerProjectRoot = new Map<string, TypeAcquisition | undefined>();
714        /**
715         * Project size for configured or external projects
716         */
717        private readonly projectToSizeMap = new Map<string, number>();
718        /**
719         * This is a map of config file paths existence that doesnt need query to disk
720         * - The entry can be present because there is inferred project that needs to watch addition of config file to directory
721         *   In this case the exists could be true/false based on config file is present or not
722         * - Or it is present if we have configured project open with config file at that location
723         *   In this case the exists property is always true
724         */
725        private readonly configFileExistenceInfoCache = new Map<string, ConfigFileExistenceInfo>();
726        /*@internal*/ readonly throttledOperations: ThrottledOperations;
727
728        private readonly hostConfiguration: HostConfiguration;
729        private safelist: SafeList = defaultTypeSafeList;
730        private readonly legacySafelist = new Map<string, string>();
731
732        private pendingProjectUpdates = new Map<string, Project>();
733        /* @internal */
734        pendingEnsureProjectForOpenFiles = false;
735
736        readonly currentDirectory: NormalizedPath;
737        readonly toCanonicalFileName: (f: string) => string;
738
739        public readonly host: ServerHost;
740        public readonly logger: Logger;
741        public readonly cancellationToken: HostCancellationToken;
742        public readonly useSingleInferredProject: boolean;
743        public readonly useInferredProjectPerProjectRoot: boolean;
744        public readonly typingsInstaller: ITypingsInstaller;
745        private readonly globalCacheLocationDirectoryPath: Path | undefined;
746        public readonly throttleWaitMilliseconds?: number;
747        private readonly eventHandler?: ProjectServiceEventHandler;
748        private readonly suppressDiagnosticEvents?: boolean;
749
750        public readonly globalPlugins: readonly string[];
751        public readonly pluginProbeLocations: readonly string[];
752        public readonly allowLocalPluginLoads: boolean;
753        private currentPluginConfigOverrides: ESMap<string, any> | undefined;
754
755        public readonly typesMapLocation: string | undefined;
756
757        /** @deprecated use serverMode instead */
758        public readonly syntaxOnly: boolean;
759        public readonly serverMode: LanguageServiceMode;
760
761        /** Tracks projects that we have already sent telemetry for. */
762        private readonly seenProjects = new Map<string, true>();
763
764        /*@internal*/
765        readonly watchFactory: WatchFactory<WatchType, Project>;
766
767        /*@internal*/
768        private readonly sharedExtendedConfigFileWatchers = new Map<Path, SharedExtendedConfigFileWatcher<NormalizedPath>>();
769
770        /*@internal*/
771        readonly packageJsonCache: PackageJsonCache;
772        /*@internal*/
773        private packageJsonFilesMap: ESMap<Path, FileWatcher> | undefined;
774
775
776        private performanceEventHandler?: PerformanceEventHandler;
777
778        constructor(opts: ProjectServiceOptions) {
779            this.host = opts.host;
780            this.logger = opts.logger;
781            this.cancellationToken = opts.cancellationToken;
782            this.useSingleInferredProject = opts.useSingleInferredProject;
783            this.useInferredProjectPerProjectRoot = opts.useInferredProjectPerProjectRoot;
784            this.typingsInstaller = opts.typingsInstaller || nullTypingsInstaller;
785            this.throttleWaitMilliseconds = opts.throttleWaitMilliseconds;
786            this.eventHandler = opts.eventHandler;
787            this.suppressDiagnosticEvents = opts.suppressDiagnosticEvents;
788            this.globalPlugins = opts.globalPlugins || emptyArray;
789            this.pluginProbeLocations = opts.pluginProbeLocations || emptyArray;
790            this.allowLocalPluginLoads = !!opts.allowLocalPluginLoads;
791            this.typesMapLocation = (opts.typesMapLocation === undefined) ? combinePaths(getDirectoryPath(this.getExecutingFilePath()), "typesMap.json") : opts.typesMapLocation;
792            if (opts.serverMode !== undefined) {
793                this.serverMode = opts.serverMode;
794                this.syntaxOnly = this.serverMode === LanguageServiceMode.Syntactic;
795            }
796            else if (opts.syntaxOnly) {
797                this.serverMode = LanguageServiceMode.Syntactic;
798                this.syntaxOnly = true;
799            }
800            else {
801                this.serverMode = LanguageServiceMode.Semantic;
802                this.syntaxOnly = false;
803            }
804
805            if (this.host.realpath) {
806                this.realpathToScriptInfos = createMultiMap();
807            }
808            this.currentDirectory = toNormalizedPath(this.host.getCurrentDirectory());
809            this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames);
810            this.globalCacheLocationDirectoryPath = this.typingsInstaller.globalTypingsCacheLocation
811                ? ensureTrailingDirectorySeparator(this.toPath(this.typingsInstaller.globalTypingsCacheLocation))
812                : undefined;
813            this.throttledOperations = new ThrottledOperations(this.host, this.logger);
814
815            if (this.typesMapLocation) {
816                this.loadTypesMap();
817            }
818            else {
819                this.logger.info("No types map provided; using the default");
820            }
821
822            this.typingsInstaller.attach(this);
823
824            this.typingsCache = new TypingsCache(this.typingsInstaller);
825
826            this.hostConfiguration = {
827                formatCodeOptions: getDefaultFormatCodeSettings(this.host.newLine),
828                preferences: emptyOptions,
829                hostInfo: "Unknown host",
830                extraFileExtensions: [],
831            };
832
833            this.documentRegistry = createDocumentRegistryInternal(this.host.useCaseSensitiveFileNames, this.currentDirectory, this);
834            const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose :
835                this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None;
836            const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop;
837            this.packageJsonCache = createPackageJsonCache(this);
838            this.watchFactory = this.serverMode !== LanguageServiceMode.Semantic ?
839                {
840                    watchFile: returnNoopFileWatcher,
841                    watchDirectory: returnNoopFileWatcher,
842                } :
843                getWatchFactory(this.host, watchLogLevel, log, getDetailWatchInfo);
844        }
845
846        toPath(fileName: string) {
847            return toPath(fileName, this.currentDirectory, this.toCanonicalFileName);
848        }
849
850        /*@internal*/
851        getExecutingFilePath() {
852            return this.getNormalizedAbsolutePath(this.host.getExecutingFilePath());
853        }
854
855        /*@internal*/
856        getNormalizedAbsolutePath(fileName: string) {
857            return getNormalizedAbsolutePath(fileName, this.host.getCurrentDirectory());
858        }
859
860        /*@internal*/
861        setDocument(key: DocumentRegistryBucketKey, path: Path, sourceFile: SourceFile) {
862            const info = Debug.checkDefined(this.getScriptInfoForPath(path));
863            info.cacheSourceFile = { key, sourceFile };
864        }
865
866        /*@internal*/
867        getDocument(key: DocumentRegistryBucketKey, path: Path): SourceFile | undefined {
868            const info = this.getScriptInfoForPath(path);
869            return info && info.cacheSourceFile && info.cacheSourceFile.key === key ? info.cacheSourceFile.sourceFile : undefined;
870        }
871
872        /* @internal */
873        ensureInferredProjectsUpToDate_TestOnly() {
874            this.ensureProjectStructuresUptoDate();
875        }
876
877        /* @internal */
878        getCompilerOptionsForInferredProjects() {
879            return this.compilerOptionsForInferredProjects;
880        }
881
882        /* @internal */
883        onUpdateLanguageServiceStateForProject(project: Project, languageServiceEnabled: boolean) {
884            if (!this.eventHandler) {
885                return;
886            }
887            const event: ProjectLanguageServiceStateEvent = {
888                eventName: ProjectLanguageServiceStateEvent,
889                data: { project, languageServiceEnabled }
890            };
891            this.eventHandler(event);
892        }
893
894        private loadTypesMap() {
895            try {
896                const fileContent = this.host.readFile(this.typesMapLocation!); // TODO: GH#18217
897                if (fileContent === undefined) {
898                    this.logger.info(`Provided types map file "${this.typesMapLocation}" doesn't exist`);
899                    return;
900                }
901                const raw: TypesMapFile = JSON.parse(fileContent);
902                // Parse the regexps
903                for (const k of Object.keys(raw.typesMap)) {
904                    raw.typesMap[k].match = new RegExp(raw.typesMap[k].match as {} as string, "i");
905                }
906                // raw is now fixed and ready
907                this.safelist = raw.typesMap;
908                for (const key in raw.simpleMap) {
909                    if (raw.simpleMap.hasOwnProperty(key)) {
910                        this.legacySafelist.set(key, raw.simpleMap[key].toLowerCase());
911                    }
912                }
913            }
914            catch (e) {
915                this.logger.info(`Error loading types map: ${e}`);
916                this.safelist = defaultTypeSafeList;
917                this.legacySafelist.clear();
918            }
919        }
920
921        updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse): void;
922        /** @internal */
923        updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse | BeginInstallTypes | EndInstallTypes): void; // eslint-disable-line @typescript-eslint/unified-signatures
924        updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse | BeginInstallTypes | EndInstallTypes): void {
925            const project = this.findProject(response.projectName);
926            if (!project) {
927                return;
928            }
929            switch (response.kind) {
930                case ActionSet:
931                    // Update the typing files and update the project
932                    project.updateTypingFiles(this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typeAcquisition, response.unresolvedImports, response.typings));
933                    return;
934                case ActionInvalidate:
935                    // Do not clear resolution cache, there was changes detected in typings, so enque typing request and let it get us correct results
936                    this.typingsCache.enqueueInstallTypingsForProject(project, project.lastCachedUnresolvedImportsList, /*forceRefresh*/ true);
937                    return;
938            }
939        }
940
941        /*@internal*/
942        delayEnsureProjectForOpenFiles() {
943            if (!this.openFiles.size) return;
944            this.pendingEnsureProjectForOpenFiles = true;
945            this.throttledOperations.schedule(ensureProjectForOpenFileSchedule, /*delay*/ 2500, () => {
946                if (this.pendingProjectUpdates.size !== 0) {
947                    this.delayEnsureProjectForOpenFiles();
948                }
949                else {
950                    if (this.pendingEnsureProjectForOpenFiles) {
951                        this.ensureProjectForOpenFiles();
952
953                        // Send the event to notify that there were background project updates
954                        // send current list of open files
955                        this.sendProjectsUpdatedInBackgroundEvent();
956                    }
957                }
958            });
959        }
960
961        private delayUpdateProjectGraph(project: Project) {
962            project.markAsDirty();
963            const projectName = project.getProjectName();
964            this.pendingProjectUpdates.set(projectName, project);
965            this.throttledOperations.schedule(projectName, /*delay*/ 250, () => {
966                if (this.pendingProjectUpdates.delete(projectName)) {
967                    updateProjectIfDirty(project);
968                }
969            });
970        }
971
972        /*@internal*/
973        hasPendingProjectUpdate(project: Project) {
974            return this.pendingProjectUpdates.has(project.getProjectName());
975        }
976
977        /* @internal */
978        sendProjectsUpdatedInBackgroundEvent() {
979            if (!this.eventHandler) {
980                return;
981            }
982
983            const event: ProjectsUpdatedInBackgroundEvent = {
984                eventName: ProjectsUpdatedInBackgroundEvent,
985                data: {
986                    openFiles: arrayFrom(this.openFiles.keys(), path => this.getScriptInfoForPath(path as Path)!.fileName)
987                }
988            };
989            this.eventHandler(event);
990        }
991
992        /* @internal */
993        sendLargeFileReferencedEvent(file: string, fileSize: number) {
994            if (!this.eventHandler) {
995                return;
996            }
997
998            const event: LargeFileReferencedEvent = {
999                eventName: LargeFileReferencedEvent,
1000                data: { file, fileSize, maxFileSize }
1001            };
1002            this.eventHandler(event);
1003        }
1004
1005        /* @internal */
1006        sendProjectLoadingStartEvent(project: ConfiguredProject, reason: string) {
1007            if (!this.eventHandler) {
1008                return;
1009            }
1010            project.sendLoadingProjectFinish = true;
1011            const event: ProjectLoadingStartEvent = {
1012                eventName: ProjectLoadingStartEvent,
1013                data: { project, reason }
1014            };
1015            this.eventHandler(event);
1016        }
1017
1018        /* @internal */
1019        sendProjectLoadingFinishEvent(project: ConfiguredProject) {
1020            if (!this.eventHandler || !project.sendLoadingProjectFinish) {
1021                return;
1022            }
1023
1024            project.sendLoadingProjectFinish = false;
1025            const event: ProjectLoadingFinishEvent = {
1026                eventName: ProjectLoadingFinishEvent,
1027                data: { project }
1028            };
1029            this.eventHandler(event);
1030        }
1031
1032        /* @internal */
1033        sendPerformanceEvent(kind: PerformanceEvent["kind"], durationMs: number) {
1034            if (this.performanceEventHandler) {
1035                this.performanceEventHandler({ kind, durationMs });
1036            }
1037        }
1038
1039        /* @internal */
1040        delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project: Project) {
1041            this.delayUpdateProjectGraph(project);
1042            this.delayEnsureProjectForOpenFiles();
1043        }
1044
1045        private delayUpdateProjectGraphs(projects: readonly Project[], clearSourceMapperCache: boolean) {
1046            if (projects.length) {
1047                for (const project of projects) {
1048                    // Even if program doesnt change, clear the source mapper cache
1049                    if (clearSourceMapperCache) project.clearSourceMapperCache();
1050                    this.delayUpdateProjectGraph(project);
1051                }
1052                this.delayEnsureProjectForOpenFiles();
1053            }
1054        }
1055
1056        setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.InferredProjectCompilerOptions, projectRootPath?: string): void {
1057            Debug.assert(projectRootPath === undefined || this.useInferredProjectPerProjectRoot, "Setting compiler options per project root path is only supported when useInferredProjectPerProjectRoot is enabled");
1058
1059            const compilerOptions = convertCompilerOptions(projectCompilerOptions);
1060            const watchOptions = convertWatchOptions(projectCompilerOptions, projectRootPath);
1061            const typeAcquisition = convertTypeAcquisition(projectCompilerOptions);
1062
1063            // always set 'allowNonTsExtensions' for inferred projects since user cannot configure it from the outside
1064            // previously we did not expose a way for user to change these settings and this option was enabled by default
1065            compilerOptions.allowNonTsExtensions = true;
1066            const canonicalProjectRootPath = projectRootPath && this.toCanonicalFileName(projectRootPath);
1067            if (canonicalProjectRootPath) {
1068                this.compilerOptionsForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, compilerOptions);
1069                this.watchOptionsForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, watchOptions || false);
1070                this.typeAcquisitionForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, typeAcquisition);
1071            }
1072            else {
1073                this.compilerOptionsForInferredProjects = compilerOptions;
1074                this.watchOptionsForInferredProjects = watchOptions;
1075                this.typeAcquisitionForInferredProjects = typeAcquisition;
1076            }
1077
1078            for (const project of this.inferredProjects) {
1079                // Only update compiler options in the following cases:
1080                // - Inferred projects without a projectRootPath, if the new options do not apply to
1081                //   a workspace root
1082                // - Inferred projects with a projectRootPath, if the new options do not apply to a
1083                //   workspace root and there is no more specific set of options for that project's
1084                //   root path
1085                // - Inferred projects with a projectRootPath, if the new options apply to that
1086                //   project root path.
1087                if (canonicalProjectRootPath ?
1088                    project.projectRootPath === canonicalProjectRootPath :
1089                    !project.projectRootPath || !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath)) {
1090                    project.setCompilerOptions(compilerOptions);
1091                    project.setTypeAcquisition(typeAcquisition);
1092                    project.setWatchOptions(watchOptions?.watchOptions);
1093                    project.setProjectErrors(watchOptions?.errors);
1094                    project.compileOnSaveEnabled = compilerOptions.compileOnSave!;
1095                    project.markAsDirty();
1096                    this.delayUpdateProjectGraph(project);
1097                }
1098            }
1099
1100            this.delayEnsureProjectForOpenFiles();
1101        }
1102
1103        findProject(projectName: string): Project | undefined {
1104            if (projectName === undefined) {
1105                return undefined;
1106            }
1107            if (isInferredProjectName(projectName)) {
1108                return findProjectByName(projectName, this.inferredProjects);
1109            }
1110            return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName));
1111        }
1112
1113        /* @internal */
1114        private forEachProject(cb: (project: Project) => void) {
1115            this.externalProjects.forEach(cb);
1116            this.configuredProjects.forEach(cb);
1117            this.inferredProjects.forEach(cb);
1118        }
1119
1120        /* @internal */
1121        forEachEnabledProject(cb: (project: Project) => void) {
1122            this.forEachProject(project => {
1123                if (!project.isOrphan() && project.languageServiceEnabled) {
1124                    cb(project);
1125                }
1126            });
1127        }
1128
1129        getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean): Project | undefined {
1130            return ensureProject ? this.ensureDefaultProjectForFile(fileName) : this.tryGetDefaultProjectForFile(fileName);
1131        }
1132
1133        /* @internal */
1134        tryGetDefaultProjectForFile(fileName: NormalizedPath): Project | undefined {
1135            const scriptInfo = this.getScriptInfoForNormalizedPath(fileName);
1136            return scriptInfo && !scriptInfo.isOrphan() ? scriptInfo.getDefaultProject() : undefined;
1137        }
1138
1139        /* @internal */
1140        ensureDefaultProjectForFile(fileName: NormalizedPath): Project {
1141            return this.tryGetDefaultProjectForFile(fileName) || this.doEnsureDefaultProjectForFile(fileName);
1142        }
1143
1144        private doEnsureDefaultProjectForFile(fileName: NormalizedPath): Project {
1145            this.ensureProjectStructuresUptoDate();
1146            const scriptInfo = this.getScriptInfoForNormalizedPath(fileName);
1147            return scriptInfo ? scriptInfo.getDefaultProject() : (this.logErrorForScriptInfoNotFound(fileName), Errors.ThrowNoProject());
1148        }
1149
1150        getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) {
1151            this.ensureProjectStructuresUptoDate();
1152            return this.getScriptInfo(uncheckedFileName);
1153        }
1154
1155        /**
1156         * Ensures the project structures are upto date
1157         * This means,
1158         * - we go through all the projects and update them if they are dirty
1159         * - if updates reflect some change in structure or there was pending request to ensure projects for open files
1160         *   ensure that each open script info has project
1161         */
1162        private ensureProjectStructuresUptoDate() {
1163            let hasChanges = this.pendingEnsureProjectForOpenFiles;
1164            this.pendingProjectUpdates.clear();
1165            const updateGraph = (project: Project) => {
1166                hasChanges = updateProjectIfDirty(project) || hasChanges;
1167            };
1168
1169            this.externalProjects.forEach(updateGraph);
1170            this.configuredProjects.forEach(updateGraph);
1171            this.inferredProjects.forEach(updateGraph);
1172            if (hasChanges) {
1173                this.ensureProjectForOpenFiles();
1174            }
1175        }
1176
1177        getFormatCodeOptions(file: NormalizedPath) {
1178            const info = this.getScriptInfoForNormalizedPath(file);
1179            return info && info.getFormatCodeSettings() || this.hostConfiguration.formatCodeOptions;
1180        }
1181
1182        getPreferences(file: NormalizedPath): protocol.UserPreferences {
1183            const info = this.getScriptInfoForNormalizedPath(file);
1184            return { ...this.hostConfiguration.preferences, ...info && info.getPreferences() };
1185        }
1186
1187        getHostFormatCodeOptions(): FormatCodeSettings {
1188            return this.hostConfiguration.formatCodeOptions;
1189        }
1190
1191        getHostPreferences(): protocol.UserPreferences {
1192            return this.hostConfiguration.preferences;
1193        }
1194
1195        private onSourceFileChanged(info: ScriptInfo, eventKind: FileWatcherEventKind) {
1196            if (info.containingProjects) {
1197                info.containingProjects.forEach(project => project.resolutionCache.removeResolutionsFromProjectReferenceRedirects(info.path));
1198            }
1199            if (eventKind === FileWatcherEventKind.Deleted) {
1200                // File was deleted
1201                this.handleDeletedFile(info);
1202            }
1203            else if (!info.isScriptOpen()) {
1204                // file has been changed which might affect the set of referenced files in projects that include
1205                // this file and set of inferred projects
1206                info.delayReloadNonMixedContentFile();
1207                this.delayUpdateProjectGraphs(info.containingProjects, /*clearSourceMapperCache*/ false);
1208                this.handleSourceMapProjects(info);
1209            }
1210        }
1211
1212        private handleSourceMapProjects(info: ScriptInfo) {
1213            // Change in d.ts, update source projects as well
1214            if (info.sourceMapFilePath) {
1215                if (isString(info.sourceMapFilePath)) {
1216                    const sourceMapFileInfo = this.getScriptInfoForPath(info.sourceMapFilePath);
1217                    this.delayUpdateSourceInfoProjects(sourceMapFileInfo && sourceMapFileInfo.sourceInfos);
1218                }
1219                else {
1220                    this.delayUpdateSourceInfoProjects(info.sourceMapFilePath.sourceInfos);
1221                }
1222            }
1223            // Change in mapInfo, update declarationProjects and source projects
1224            this.delayUpdateSourceInfoProjects(info.sourceInfos);
1225            if (info.declarationInfoPath) {
1226                this.delayUpdateProjectsOfScriptInfoPath(info.declarationInfoPath);
1227            }
1228        }
1229
1230        private delayUpdateSourceInfoProjects(sourceInfos: Set<Path> | undefined) {
1231            if (sourceInfos) {
1232                sourceInfos.forEach((_value, path) => this.delayUpdateProjectsOfScriptInfoPath(path));
1233            }
1234        }
1235
1236        private delayUpdateProjectsOfScriptInfoPath(path: Path) {
1237            const info = this.getScriptInfoForPath(path);
1238            if (info) {
1239                this.delayUpdateProjectGraphs(info.containingProjects, /*clearSourceMapperCache*/ true);
1240            }
1241        }
1242
1243        private handleDeletedFile(info: ScriptInfo) {
1244            this.stopWatchingScriptInfo(info);
1245
1246            if (!info.isScriptOpen()) {
1247                this.deleteScriptInfo(info);
1248
1249                // capture list of projects since detachAllProjects will wipe out original list
1250                const containingProjects = info.containingProjects.slice();
1251
1252                info.detachAllProjects();
1253
1254                // update projects to make sure that set of referenced files is correct
1255                this.delayUpdateProjectGraphs(containingProjects, /*clearSourceMapperCache*/ false);
1256                this.handleSourceMapProjects(info);
1257                info.closeSourceMapFileWatcher();
1258                // need to recalculate source map from declaration file
1259                if (info.declarationInfoPath) {
1260                    const declarationInfo = this.getScriptInfoForPath(info.declarationInfoPath);
1261                    if (declarationInfo) {
1262                        declarationInfo.sourceMapFilePath = undefined;
1263                    }
1264                }
1265            }
1266        }
1267
1268        /**
1269         * This is to watch whenever files are added or removed to the wildcard directories
1270         */
1271        /*@internal*/
1272        watchWildcardDirectory(directory: Path, flags: WatchDirectoryFlags, project: ConfiguredProject) {
1273            const watchOptions = this.getWatchOptions(project);
1274            return this.watchFactory.watchDirectory(
1275                directory,
1276                fileOrDirectory => {
1277                    const fileOrDirectoryPath = this.toPath(fileOrDirectory);
1278                    const fsResult = project.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
1279                    const configFileName = project.getConfigFilePath();
1280                    if (getBaseFileName(fileOrDirectoryPath) === "package.json" && !isInsideNodeModules(fileOrDirectoryPath) &&
1281                        (fsResult && fsResult.fileExists || !fsResult && this.host.fileExists(fileOrDirectoryPath))
1282                    ) {
1283                        this.logger.info(`Project: ${configFileName} Detected new package.json: ${fileOrDirectory}`);
1284                        this.onAddPackageJson(fileOrDirectoryPath);
1285                    }
1286
1287                    if (isIgnoredFileFromWildCardWatching({
1288                        watchedDirPath: directory,
1289                        fileOrDirectory,
1290                        fileOrDirectoryPath,
1291                        configFileName,
1292                        extraFileExtensions: this.hostConfiguration.extraFileExtensions,
1293                        currentDirectory: this.currentDirectory,
1294                        options: project.getCompilationSettings(),
1295                        program: project.getCurrentProgram(),
1296                        useCaseSensitiveFileNames: this.host.useCaseSensitiveFileNames,
1297                        writeLog: s => this.logger.info(s)
1298                    })) return;
1299
1300                    // don't trigger callback on open, existing files
1301                    if (project.fileIsOpen(fileOrDirectoryPath)) {
1302                        if (project.pendingReload !== ConfigFileProgramReloadLevel.Full) {
1303                            const info = Debug.checkDefined(this.getScriptInfoForPath(fileOrDirectoryPath));
1304                            if (info.isAttached(project)) {
1305                                project.openFileWatchTriggered.set(fileOrDirectoryPath, true);
1306                            }
1307                            else {
1308                                project.pendingReload = ConfigFileProgramReloadLevel.Partial;
1309                                this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
1310                            }
1311                        }
1312                        return;
1313                    }
1314
1315                    // Reload is pending, do the reload
1316                    if (project.pendingReload !== ConfigFileProgramReloadLevel.Full) {
1317                        project.pendingReload = ConfigFileProgramReloadLevel.Partial;
1318                        this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
1319                    }
1320                },
1321                flags,
1322                watchOptions,
1323                WatchType.WildcardDirectory,
1324                project
1325            );
1326        }
1327
1328        /** Gets the config file existence info for the configured project */
1329        /*@internal*/
1330        getConfigFileExistenceInfo(project: ConfiguredProject) {
1331            return this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath)!;
1332        }
1333
1334        /*@internal*/
1335        onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) {
1336            const configFileExistenceInfo = this.getConfigFileExistenceInfo(project);
1337            if (eventKind === FileWatcherEventKind.Deleted) {
1338                // Update the cached status
1339                // We arent updating or removing the cached config file presence info as that will be taken care of by
1340                // setConfigFilePresenceByClosedConfigFile when the project is closed (depending on tracking open files)
1341                configFileExistenceInfo.exists = false;
1342                this.removeProject(project);
1343
1344                // Reload the configured projects for the open files in the map as they are affected by this config file
1345                // Since the configured project was deleted, we want to reload projects for all the open files including files
1346                // that are not root of the inferred project
1347                this.logConfigFileWatchUpdate(project.getConfigFilePath(), project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingFiles);
1348                this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ false);
1349            }
1350            else {
1351                this.logConfigFileWatchUpdate(project.getConfigFilePath(), project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingInferredRootFiles);
1352                // Skip refresh if project is not yet loaded
1353                if (project.isInitialLoadPending()) return;
1354                project.pendingReload = ConfigFileProgramReloadLevel.Full;
1355                project.pendingReloadReason = "Change in config file detected";
1356                this.delayUpdateProjectGraph(project);
1357                // As we scheduled the update on configured project graph,
1358                // we would need to schedule the project reload for only the root of inferred projects
1359                this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ true);
1360            }
1361        }
1362
1363        /*@internal*/
1364        updateSharedExtendedConfigFileMap({ canonicalConfigFilePath }: ConfiguredProject, parsedCommandLine: ParsedCommandLine) {
1365            updateSharedExtendedConfigFileWatcher(
1366                canonicalConfigFilePath,
1367                parsedCommandLine,
1368                this.sharedExtendedConfigFileWatchers,
1369                (extendedConfigFileName, extendedConfigFilePath) => this.watchFactory.watchFile(
1370                    extendedConfigFileName,
1371                    () => {
1372                        let ensureProjectsForOpenFiles = false;
1373                        this.sharedExtendedConfigFileWatchers.get(extendedConfigFilePath)?.projects.forEach(canonicalPath => {
1374                            const project = this.configuredProjects.get(canonicalPath);
1375                            // Skip refresh if project is not yet loaded
1376                            if (!project || project.isInitialLoadPending()) return;
1377                            project.pendingReload = ConfigFileProgramReloadLevel.Full;
1378                            project.pendingReloadReason = `Change in extended config file ${extendedConfigFileName} detected`;
1379                            this.delayUpdateProjectGraph(project);
1380                            ensureProjectsForOpenFiles = true;
1381                        });
1382                        if (ensureProjectsForOpenFiles) this.delayEnsureProjectForOpenFiles();
1383                    },
1384                    PollingInterval.High,
1385                    this.hostConfiguration.watchOptions,
1386                    WatchType.ExtendedConfigFile
1387                ),
1388                fileName => this.toPath(fileName),
1389            );
1390        }
1391
1392        /*@internal*/
1393        removeProjectFromSharedExtendedConfigFileMap(project: ConfiguredProject) {
1394            this.sharedExtendedConfigFileWatchers.forEach(watcher => {
1395                watcher.projects.delete(project.canonicalConfigFilePath);
1396                watcher.close();
1397            });
1398        }
1399
1400        /**
1401         * This is the callback function for the config file add/remove/change at any location
1402         * that matters to open script info but doesnt have configured project open
1403         * for the config file
1404         */
1405        private onConfigFileChangeForOpenScriptInfo(configFileName: NormalizedPath, eventKind: FileWatcherEventKind) {
1406            // This callback is called only if we dont have config file project for this config file
1407            const canonicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName);
1408            const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigPath)!;
1409            configFileExistenceInfo.exists = (eventKind !== FileWatcherEventKind.Deleted);
1410            this.logConfigFileWatchUpdate(configFileName, canonicalConfigPath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingFiles);
1411
1412            // Because there is no configured project open for the config file, the tracking open files map
1413            // will only have open files that need the re-detection of the project and hence
1414            // reload projects for all the tracking open files in the map
1415            this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ false);
1416        }
1417
1418        private removeProject(project: Project) {
1419            this.logger.info("`remove Project::");
1420            project.print(/*writeProjectFileNames*/ true);
1421
1422            project.close();
1423            if (Debug.shouldAssert(AssertionLevel.Normal)) {
1424                this.filenameToScriptInfo.forEach(info => Debug.assert(
1425                    !info.isAttached(project),
1426                    "Found script Info still attached to project",
1427                    () => `${project.projectName}: ScriptInfos still attached: ${JSON.stringify(
1428                        arrayFrom(
1429                            mapDefinedIterator(
1430                                this.filenameToScriptInfo.values(),
1431                                info => info.isAttached(project) ?
1432                                    {
1433                                        fileName: info.fileName,
1434                                        projects: info.containingProjects.map(p => p.projectName),
1435                                        hasMixedContent: info.hasMixedContent
1436                                    } : undefined
1437                            )
1438                        ),
1439                        /*replacer*/ undefined,
1440                        " "
1441                    )}`));
1442            }
1443            // Remove the project from pending project updates
1444            this.pendingProjectUpdates.delete(project.getProjectName());
1445
1446            switch (project.projectKind) {
1447                case ProjectKind.External:
1448                    unorderedRemoveItem(this.externalProjects, <ExternalProject>project);
1449                    this.projectToSizeMap.delete(project.getProjectName());
1450                    break;
1451                case ProjectKind.Configured:
1452                    this.configuredProjects.delete((<ConfiguredProject>project).canonicalConfigFilePath);
1453                    this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath);
1454                    this.setConfigFileExistenceInfoByClosedConfiguredProject(<ConfiguredProject>project);
1455                    break;
1456                case ProjectKind.Inferred:
1457                    unorderedRemoveItem(this.inferredProjects, <InferredProject>project);
1458                    break;
1459            }
1460        }
1461
1462        /*@internal*/
1463        assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath: NormalizedPath | undefined) {
1464            Debug.assert(info.isOrphan());
1465
1466            const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) ||
1467                this.getOrCreateSingleInferredProjectIfEnabled() ||
1468                this.getOrCreateSingleInferredWithoutProjectRoot(
1469                    info.isDynamic ?
1470                        projectRootPath || this.currentDirectory :
1471                        getDirectoryPath(
1472                            isRootedDiskPath(info.fileName) ?
1473                                info.fileName :
1474                                getNormalizedAbsolutePath(
1475                                    info.fileName,
1476                                    projectRootPath ?
1477                                        this.getNormalizedAbsolutePath(projectRootPath) :
1478                                        this.currentDirectory
1479                                )
1480                        )
1481                );
1482
1483            project.addRoot(info);
1484            if (info.containingProjects[0] !== project) {
1485                // Ensure this is first project, we could be in this scenario because info could be part of orphan project
1486                info.detachFromProject(project);
1487                info.containingProjects.unshift(project);
1488            }
1489            project.updateGraph();
1490
1491            if (!this.useSingleInferredProject && !project.projectRootPath) {
1492                // Note that we need to create a copy of the array since the list of project can change
1493                for (const inferredProject of this.inferredProjects) {
1494                    if (inferredProject === project || inferredProject.isOrphan()) {
1495                        continue;
1496                    }
1497
1498                    // Remove the inferred project if the root of it is now part of newly created inferred project
1499                    // e.g through references
1500                    // Which means if any root of inferred project is part of more than 1 project can be removed
1501                    // This logic is same as iterating over all open files and calling
1502                    // this.removeRootOfInferredProjectIfNowPartOfOtherProject(f);
1503                    // Since this is also called from refreshInferredProject and closeOpen file
1504                    // to update inferred projects of the open file, this iteration might be faster
1505                    // instead of scanning all open files
1506                    const roots = inferredProject.getRootScriptInfos();
1507                    Debug.assert(roots.length === 1 || !!inferredProject.projectRootPath);
1508                    if (roots.length === 1 && forEach(roots[0].containingProjects, p => p !== roots[0].containingProjects[0] && !p.isOrphan())) {
1509                        inferredProject.removeFile(roots[0], /*fileExists*/ true, /*detachFromProject*/ true);
1510                    }
1511                }
1512            }
1513
1514            return project;
1515        }
1516
1517        private assignOrphanScriptInfosToInferredProject() {
1518            // collect orphaned files and assign them to inferred project just like we treat open of a file
1519            this.openFiles.forEach((projectRootPath, path) => {
1520                const info = this.getScriptInfoForPath(path as Path)!;
1521                // collect all orphaned script infos from open files
1522                if (info.isOrphan()) {
1523                    this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
1524                }
1525            });
1526        }
1527
1528        /**
1529         * Remove this file from the set of open, non-configured files.
1530         * @param info The file that has been closed or newly configured
1531         */
1532        private closeOpenFile(info: ScriptInfo, skipAssignOrphanScriptInfosToInferredProject?: true) {
1533            // Closing file should trigger re-reading the file content from disk. This is
1534            // because the user may chose to discard the buffer content before saving
1535            // to the disk, and the server's version of the file can be out of sync.
1536            const fileExists = info.isDynamic ? false : this.host.fileExists(info.fileName);
1537            info.close(fileExists);
1538            this.stopWatchingConfigFilesForClosedScriptInfo(info);
1539
1540            const canonicalFileName = this.toCanonicalFileName(info.fileName);
1541            if (this.openFilesWithNonRootedDiskPath.get(canonicalFileName) === info) {
1542                this.openFilesWithNonRootedDiskPath.delete(canonicalFileName);
1543            }
1544
1545            // collect all projects that should be removed
1546            let ensureProjectsForOpenFiles = false;
1547            for (const p of info.containingProjects) {
1548                if (isConfiguredProject(p)) {
1549                    if (info.hasMixedContent) {
1550                        info.registerFileUpdate();
1551                    }
1552                    // Do not remove the project so that we can reuse this project
1553                    // if it would need to be re-created with next file open
1554
1555                    // If project had open file affecting
1556                    // Reload the root Files from config if its not already scheduled
1557                    if (p.openFileWatchTriggered.has(info.path)) {
1558                        p.openFileWatchTriggered.delete(info.path);
1559                        if (!p.pendingReload) {
1560                            p.pendingReload = ConfigFileProgramReloadLevel.Partial;
1561                            p.markFileAsDirty(info.path);
1562                        }
1563                    }
1564                }
1565                else if (isInferredProject(p) && p.isRoot(info)) {
1566                    // If this was the last open root file of inferred project
1567                    if (p.isProjectWithSingleRoot()) {
1568                        ensureProjectsForOpenFiles = true;
1569                    }
1570
1571                    p.removeFile(info, fileExists, /*detachFromProject*/ true);
1572                    // Do not remove the project even if this was last root of the inferred project
1573                    // so that we can reuse this project, if it would need to be re-created with next file open
1574                }
1575
1576                if (!p.languageServiceEnabled) {
1577                    // if project language service is disabled then we create a program only for open files.
1578                    // this means that project should be marked as dirty to force rebuilding of the program
1579                    // on the next request
1580                    p.markAsDirty();
1581                }
1582            }
1583
1584            this.openFiles.delete(info.path);
1585            this.configFileForOpenFiles.delete(info.path);
1586
1587            if (!skipAssignOrphanScriptInfosToInferredProject && ensureProjectsForOpenFiles) {
1588                this.assignOrphanScriptInfosToInferredProject();
1589            }
1590
1591            // Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project)
1592            // is postponed to next file open so that if file from same project is opened,
1593            // we wont end up creating same script infos
1594
1595            // If the current info is being just closed - add the watcher file to track changes
1596            // But if file was deleted, handle that part
1597            if (fileExists) {
1598                this.watchClosedScriptInfo(info);
1599            }
1600            else {
1601                this.handleDeletedFile(info);
1602            }
1603
1604            return ensureProjectsForOpenFiles;
1605        }
1606
1607        private deleteScriptInfo(info: ScriptInfo) {
1608            this.filenameToScriptInfo.delete(info.path);
1609            this.filenameToScriptInfoVersion.set(info.path, info.getVersion());
1610            const realpath = info.getRealpathIfDifferent();
1611            if (realpath) {
1612                this.realpathToScriptInfos!.remove(realpath, info); // TODO: GH#18217
1613            }
1614        }
1615
1616        private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: OpenScriptInfoOrClosedOrConfigFileInfo) {
1617            let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
1618            if (configFileExistenceInfo) {
1619                // By default the info would get impacted by presence of config file since its in the detection path
1620                // Only adding the info as a root to inferred project will need the existence to be watched by file watcher
1621                if (isOpenScriptInfo(info) && !configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) {
1622                    configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false);
1623                    this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd);
1624                }
1625                return configFileExistenceInfo.exists;
1626            }
1627
1628            // Theoretically we should be adding watch for the directory here itself.
1629            // In practice there will be very few scenarios where the config file gets added
1630            // somewhere inside the another config file directory.
1631            // And technically we could handle that case in configFile's directory watcher in some cases
1632            // But given that its a rare scenario it seems like too much overhead. (we werent watching those directories earlier either)
1633
1634            // So what we are now watching is: configFile if the configured project corresponding to it is open
1635            // Or the whole chain of config files for the roots of the inferred projects
1636
1637            // Cache the host value of file exists and add the info to map of open files impacted by this config file
1638            const exists = this.host.fileExists(configFileName);
1639            const openFilesImpactedByConfigFile = new Map<Path, boolean>();
1640            if (isOpenScriptInfo(info)) {
1641                openFilesImpactedByConfigFile.set(info.path, false);
1642            }
1643            configFileExistenceInfo = { exists, openFilesImpactedByConfigFile };
1644            this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo);
1645            this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd);
1646            return exists;
1647        }
1648
1649        private setConfigFileExistenceByNewConfiguredProject(project: ConfiguredProject) {
1650            const configFileExistenceInfo = this.getConfigFileExistenceInfo(project);
1651            if (configFileExistenceInfo) {
1652                // The existence might not be set if the file watcher is not invoked by the time config project is created by external project
1653                configFileExistenceInfo.exists = true;
1654                // close existing watcher
1655                if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) {
1656                    const configFileName = project.getConfigFilePath();
1657                    configFileExistenceInfo.configFileWatcherForRootOfInferredProject.close();
1658                    configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined;
1659                    this.logConfigFileWatchUpdate(configFileName, project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback);
1660                }
1661            }
1662            else {
1663                // We could be in this scenario if project is the configured project tracked by external project
1664                // Since that route doesnt check if the config file is present or not
1665                this.configFileExistenceInfoCache.set(project.canonicalConfigFilePath, {
1666                    exists: true,
1667                    openFilesImpactedByConfigFile: new Map<Path, boolean>()
1668                });
1669            }
1670        }
1671
1672        /**
1673         * Returns true if the configFileExistenceInfo is needed/impacted by open files that are root of inferred project
1674         */
1675        private configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo: ConfigFileExistenceInfo) {
1676            return forEachEntry(configFileExistenceInfo.openFilesImpactedByConfigFile, (isRootOfInferredProject) => isRootOfInferredProject);
1677        }
1678
1679        private setConfigFileExistenceInfoByClosedConfiguredProject(closedProject: ConfiguredProject) {
1680            const configFileExistenceInfo = this.getConfigFileExistenceInfo(closedProject);
1681            Debug.assert(!!configFileExistenceInfo);
1682            if (configFileExistenceInfo.openFilesImpactedByConfigFile.size) {
1683                const configFileName = closedProject.getConfigFilePath();
1684                // If there are open files that are impacted by this config file existence
1685                // but none of them are root of inferred project, the config file watcher will be
1686                // created when any of the script infos are added as root of inferred project
1687                if (this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) {
1688                    Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject);
1689                    this.createConfigFileWatcherOfConfigFileExistence(configFileName, closedProject.canonicalConfigFilePath, configFileExistenceInfo);
1690                }
1691            }
1692            else {
1693                // There is not a single file open thats tracking the status of this config file. Remove from cache
1694                this.configFileExistenceInfoCache.delete(closedProject.canonicalConfigFilePath);
1695            }
1696        }
1697
1698        private logConfigFileWatchUpdate(configFileName: NormalizedPath, canonicalConfigFilePath: string, configFileExistenceInfo: ConfigFileExistenceInfo, status: ConfigFileWatcherStatus) {
1699            if (!this.logger.hasLevel(LogLevel.verbose)) {
1700                return;
1701            }
1702            const inferredRoots: string[] = [];
1703            const otherFiles: string[] = [];
1704            configFileExistenceInfo.openFilesImpactedByConfigFile.forEach((isRootOfInferredProject, key) => {
1705                const info = this.getScriptInfoForPath(key)!;
1706                (isRootOfInferredProject ? inferredRoots : otherFiles).push(info.fileName);
1707            });
1708
1709            const watches: WatchType[] = [];
1710            if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) {
1711                watches.push(
1712                    configFileExistenceInfo.configFileWatcherForRootOfInferredProject === noopFileWatcher ?
1713                        WatchType.NoopConfigFileForInferredRoot :
1714                        WatchType.ConfigFileForInferredRoot
1715                );
1716            }
1717            if (this.configuredProjects.has(canonicalConfigFilePath)) {
1718                watches.push(WatchType.ConfigFile);
1719            }
1720            this.logger.info(`ConfigFilePresence:: Current Watches: ${watches}:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`);
1721        }
1722
1723        /**
1724         * Create the watcher for the configFileExistenceInfo
1725         */
1726        private createConfigFileWatcherOfConfigFileExistence(
1727            configFileName: NormalizedPath,
1728            canonicalConfigFilePath: string,
1729            configFileExistenceInfo: ConfigFileExistenceInfo
1730        ) {
1731            configFileExistenceInfo.configFileWatcherForRootOfInferredProject =
1732                canWatchDirectory(getDirectoryPath(canonicalConfigFilePath) as Path) ?
1733                    this.watchFactory.watchFile(
1734                        configFileName,
1735                        (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind),
1736                        PollingInterval.High,
1737                        this.hostConfiguration.watchOptions,
1738                        WatchType.ConfigFileForInferredRoot
1739                    ) :
1740                    noopFileWatcher;
1741            this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback);
1742        }
1743
1744        /**
1745         * Close the config file watcher in the cached ConfigFileExistenceInfo
1746         *   if there arent any open files that are root of inferred project
1747         */
1748        private closeConfigFileWatcherOfConfigFileExistenceInfo(configFileExistenceInfo: ConfigFileExistenceInfo) {
1749            // Close the config file watcher if there are no more open files that are root of inferred project
1750            if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject &&
1751                !this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) {
1752                configFileExistenceInfo.configFileWatcherForRootOfInferredProject.close();
1753                configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined;
1754            }
1755        }
1756
1757        /**
1758         * This is called on file close, so that we stop watching the config file for this script info
1759         */
1760        private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) {
1761            Debug.assert(!info.isScriptOpen());
1762            this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
1763                const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
1764                if (configFileExistenceInfo) {
1765                    const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile.get(info.path);
1766
1767                    // Delete the info from map, since this file is no more open
1768                    configFileExistenceInfo.openFilesImpactedByConfigFile.delete(info.path);
1769                    this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileRemove);
1770
1771                    // If the script info was not root of inferred project,
1772                    // there wont be config file watch open because of this script info
1773                    if (infoIsRootOfInferredProject) {
1774                        // But if it is a root, it could be the last script info that is root of inferred project
1775                        // and hence we would need to close the config file watcher
1776                        this.closeConfigFileWatcherOfConfigFileExistenceInfo(configFileExistenceInfo);
1777                    }
1778
1779                    // If there are no open files that are impacted by configFileExistenceInfo after closing this script info
1780                    // there is no configured project present, remove the cached existence info
1781                    if (!configFileExistenceInfo.openFilesImpactedByConfigFile.size &&
1782                        !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) {
1783                        Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject);
1784                        this.configFileExistenceInfoCache.delete(canonicalConfigFilePath);
1785                    }
1786                }
1787            });
1788        }
1789
1790        /**
1791         * This is called by inferred project whenever script info is added as a root
1792         */
1793        /* @internal */
1794        startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) {
1795            Debug.assert(info.isScriptOpen());
1796            this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
1797                let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
1798                if (!configFileExistenceInfo) {
1799                    // Create the cache
1800                    configFileExistenceInfo = {
1801                        exists: this.host.fileExists(configFileName),
1802                        openFilesImpactedByConfigFile: new Map<Path, boolean>()
1803                    };
1804                    this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo);
1805                }
1806
1807                // Set this file as the root of inferred project
1808                configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, true);
1809                this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectTrue);
1810
1811                // If there is no configured project for this config file, add the file watcher
1812                if (!configFileExistenceInfo.configFileWatcherForRootOfInferredProject &&
1813                    !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) {
1814                    this.createConfigFileWatcherOfConfigFileExistence(configFileName, canonicalConfigFilePath, configFileExistenceInfo);
1815                }
1816            });
1817        }
1818
1819        /**
1820         * This is called by inferred project whenever root script info is removed from it
1821         */
1822        /* @internal */
1823        stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) {
1824            this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
1825                const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
1826                if (configFileExistenceInfo && configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) {
1827                    Debug.assert(info.isScriptOpen());
1828
1829                    // Info is not root of inferred project any more
1830                    configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false);
1831                    this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectFalse);
1832
1833                    // Close the config file watcher
1834                    this.closeConfigFileWatcherOfConfigFileExistenceInfo(configFileExistenceInfo);
1835                }
1836            });
1837        }
1838
1839        /**
1840         * This function tries to search for a tsconfig.json for the given file.
1841         * This is different from the method the compiler uses because
1842         * the compiler can assume it will always start searching in the
1843         * current directory (the directory in which tsc was invoked).
1844         * The server must start searching from the directory containing
1845         * the newly opened file.
1846         */
1847        private forEachConfigFileLocation(info: OpenScriptInfoOrClosedOrConfigFileInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) {
1848            if (this.serverMode !== LanguageServiceMode.Semantic) {
1849                return undefined;
1850            }
1851
1852            Debug.assert(!isOpenScriptInfo(info) || this.openFiles.has(info.path));
1853            const projectRootPath = this.openFiles.get(info.path);
1854            const scriptInfo = Debug.checkDefined(this.getScriptInfo(info.path));
1855            if (scriptInfo.isDynamic) return undefined;
1856
1857            let searchPath = asNormalizedPath(getDirectoryPath(info.fileName));
1858            const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames);
1859
1860            // If projectRootPath doesn't contain info.path, then do normal search for config file
1861            const anySearchPathOk = !projectRootPath || !isSearchPathInProjectRoot();
1862            // For ancestor of config file always ignore its own directory since its going to result in itself
1863            let searchInDirectory = !isAncestorConfigFileInfo(info);
1864            do {
1865                if (searchInDirectory) {
1866                    const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName);
1867                    const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json"));
1868                    let result = action(tsconfigFileName, combinePaths(canonicalSearchPath, "tsconfig.json"));
1869                    if (result) return tsconfigFileName;
1870
1871                    const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json"));
1872                    result = action(jsconfigFileName, combinePaths(canonicalSearchPath, "jsconfig.json"));
1873                    if (result) return jsconfigFileName;
1874
1875                    // If we started within node_modules, don't look outside node_modules.
1876                    // Otherwise, we might pick up a very large project and pull in the world,
1877                    // causing an editor delay.
1878                    if (isNodeModulesDirectory(canonicalSearchPath)) {
1879                        break;
1880                    }
1881                }
1882
1883                const parentPath = asNormalizedPath(getDirectoryPath(searchPath));
1884                if (parentPath === searchPath) break;
1885                searchPath = parentPath;
1886                searchInDirectory = true;
1887            } while (anySearchPathOk || isSearchPathInProjectRoot());
1888
1889            return undefined;
1890        }
1891
1892        /*@internal*/
1893        findDefaultConfiguredProject(info: ScriptInfo) {
1894            if (!info.isScriptOpen()) return undefined;
1895            const configFileName = this.getConfigFileNameForFile(info);
1896            const project = configFileName &&
1897                this.findConfiguredProjectByProjectName(configFileName);
1898
1899            return project && projectContainsInfoDirectly(project, info) ?
1900                project :
1901                project?.getDefaultChildProjectFromProjectWithReferences(info);
1902        }
1903
1904        /**
1905         * This function tries to search for a tsconfig.json for the given file.
1906         * This is different from the method the compiler uses because
1907         * the compiler can assume it will always start searching in the
1908         * current directory (the directory in which tsc was invoked).
1909         * The server must start searching from the directory containing
1910         * the newly opened file.
1911         * If script info is passed in, it is asserted to be open script info
1912         * otherwise just file name
1913         */
1914        private getConfigFileNameForFile(info: OpenScriptInfoOrClosedOrConfigFileInfo) {
1915            if (isOpenScriptInfo(info)) {
1916                Debug.assert(info.isScriptOpen());
1917                const result = this.configFileForOpenFiles.get(info.path);
1918                if (result !== undefined) return result || undefined;
1919            }
1920            this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`);
1921            const configFileName = this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) =>
1922                this.configFileExists(configFileName, canonicalConfigFilePath, info));
1923            if (configFileName) {
1924                this.logger.info(`For info: ${info.fileName} :: Config file name: ${configFileName}`);
1925            }
1926            else {
1927                this.logger.info(`For info: ${info.fileName} :: No config files found.`);
1928            }
1929            if (isOpenScriptInfo(info)) {
1930                this.configFileForOpenFiles.set(info.path, configFileName || false);
1931            }
1932            return configFileName;
1933        }
1934
1935        private printProjects() {
1936            if (!this.logger.hasLevel(LogLevel.normal)) {
1937                return;
1938            }
1939
1940            this.logger.startGroup();
1941
1942            this.externalProjects.forEach(printProjectWithoutFileNames);
1943            this.configuredProjects.forEach(printProjectWithoutFileNames);
1944            this.inferredProjects.forEach(printProjectWithoutFileNames);
1945
1946            this.logger.info("Open files: ");
1947            this.openFiles.forEach((projectRootPath, path) => {
1948                const info = this.getScriptInfoForPath(path as Path)!;
1949                this.logger.info(`\tFileName: ${info.fileName} ProjectRootPath: ${projectRootPath}`);
1950                this.logger.info(`\t\tProjects: ${info.containingProjects.map(p => p.getProjectName())}`);
1951            });
1952
1953            this.logger.endGroup();
1954        }
1955
1956        /*@internal*/
1957        findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined {
1958            // make sure that casing of config file name is consistent
1959            const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName));
1960            return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath);
1961        }
1962
1963        private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string): ConfiguredProject | undefined {
1964            return this.configuredProjects.get(canonicalConfigFilePath);
1965        }
1966
1967        private findExternalProjectByProjectName(projectFileName: string) {
1968            return findProjectByName(projectFileName, this.externalProjects);
1969        }
1970
1971        /** Get a filename if the language service exceeds the maximum allowed program size; otherwise returns undefined. */
1972        private getFilenameForExceededTotalSizeLimitForNonTsFiles<T>(name: string, options: CompilerOptions | undefined, fileNames: T[], propertyReader: FilePropertyReader<T>): string | undefined {
1973            if (options && options.disableSizeLimit || !this.host.getFileSize) {
1974                return;
1975            }
1976
1977            let availableSpace = maxProgramSizeForNonTsFiles;
1978            this.projectToSizeMap.set(name, 0);
1979            this.projectToSizeMap.forEach(val => (availableSpace -= (val || 0)));
1980
1981            let totalNonTsFileSize = 0;
1982
1983            for (const f of fileNames) {
1984                const fileName = propertyReader.getFileName(f);
1985                if (hasTSFileExtension(fileName)) {
1986                    continue;
1987                }
1988
1989                totalNonTsFileSize += this.host.getFileSize(fileName);
1990
1991                if (totalNonTsFileSize > maxProgramSizeForNonTsFiles || totalNonTsFileSize > availableSpace) {
1992                    const top5LargestFiles = fileNames.map(f => propertyReader.getFileName(f))
1993                        .filter(name => !hasTSFileExtension(name))
1994                        .map(name => ({ name, size: this.host.getFileSize!(name) }))
1995                        .sort((a, b) => b.size - a.size)
1996                        .slice(0, 5);
1997                    this.logger.info(`Non TS file size exceeded limit (${totalNonTsFileSize}). Largest files: ${top5LargestFiles.map(file => `${file.name}:${file.size}`).join(", ")}`);
1998                    // Keep the size as zero since it's disabled
1999                    return fileName;
2000                }
2001            }
2002            this.projectToSizeMap.set(name, totalNonTsFileSize);
2003        }
2004
2005        private createExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition, excludedFiles: NormalizedPath[]) {
2006            const compilerOptions = convertCompilerOptions(options);
2007            const watchOptionsAndErrors = convertWatchOptions(options, getDirectoryPath(normalizeSlashes(projectFileName)));
2008            const project = new ExternalProject(
2009                projectFileName,
2010                this,
2011                this.documentRegistry,
2012                compilerOptions,
2013                /*lastFileExceededProgramSize*/ this.getFilenameForExceededTotalSizeLimitForNonTsFiles(projectFileName, compilerOptions, files, externalFilePropertyReader),
2014                options.compileOnSave === undefined ? true : options.compileOnSave,
2015                /*projectFilePath*/ undefined,
2016                this.currentPluginConfigOverrides,
2017                watchOptionsAndErrors?.watchOptions
2018            );
2019            project.setProjectErrors(watchOptionsAndErrors?.errors);
2020            project.excludedFiles = excludedFiles;
2021
2022            this.addFilesToNonInferredProject(project, files, externalFilePropertyReader, typeAcquisition);
2023            this.externalProjects.push(project);
2024            return project;
2025        }
2026
2027        /*@internal*/
2028        sendProjectTelemetry(project: ExternalProject | ConfiguredProject): void {
2029            if (this.seenProjects.has(project.projectName)) {
2030                setProjectOptionsUsed(project);
2031                return;
2032            }
2033            this.seenProjects.set(project.projectName, true);
2034
2035            if (!this.eventHandler || !this.host.createSHA256Hash) {
2036                setProjectOptionsUsed(project);
2037                return;
2038            }
2039
2040            const projectOptions = isConfiguredProject(project) ? project.projectOptions as ProjectOptions : undefined;
2041            setProjectOptionsUsed(project);
2042            const data: ProjectInfoTelemetryEventData = {
2043                projectId: this.host.createSHA256Hash(project.projectName),
2044                fileStats: countEachFileTypes(project.getScriptInfos(), /*includeSizes*/ true),
2045                compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilationSettings()),
2046                typeAcquisition: convertTypeAcquisition(project.getTypeAcquisition()),
2047                extends: projectOptions && projectOptions.configHasExtendsProperty,
2048                files: projectOptions && projectOptions.configHasFilesProperty,
2049                include: projectOptions && projectOptions.configHasIncludeProperty,
2050                exclude: projectOptions && projectOptions.configHasExcludeProperty,
2051                compileOnSave: project.compileOnSaveEnabled,
2052                configFileName: configFileName(),
2053                projectType: project instanceof ExternalProject ? "external" : "configured",
2054                languageServiceEnabled: project.languageServiceEnabled,
2055                version: ts.version, // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier
2056            };
2057            this.eventHandler({ eventName: ProjectInfoTelemetryEvent, data });
2058
2059            function configFileName(): ProjectInfoTelemetryEventData["configFileName"] {
2060                if (!isConfiguredProject(project)) {
2061                    return "other";
2062                }
2063
2064                return getBaseConfigFileName(project.getConfigFilePath()) || "other";
2065            }
2066
2067            function convertTypeAcquisition({ enable, include, exclude }: TypeAcquisition): ProjectInfoTypeAcquisitionData {
2068                return {
2069                    enable,
2070                    include: include !== undefined && include.length !== 0,
2071                    exclude: exclude !== undefined && exclude.length !== 0,
2072                };
2073            }
2074        }
2075
2076        private addFilesToNonInferredProject<T>(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader<T>, typeAcquisition: TypeAcquisition): void {
2077            this.updateNonInferredProjectFiles(project, files, propertyReader);
2078            project.setTypeAcquisition(typeAcquisition);
2079        }
2080
2081        /* @internal */
2082        createConfiguredProject(configFileName: NormalizedPath) {
2083            const cachedDirectoryStructureHost = createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames)!; // TODO: GH#18217
2084            this.logger.info(`Opened configuration file ${configFileName}`);
2085            const project = new ConfiguredProject(
2086                configFileName,
2087                this,
2088                this.documentRegistry,
2089                cachedDirectoryStructureHost);
2090            project.createConfigFileWatcher();
2091            this.configuredProjects.set(project.canonicalConfigFilePath, project);
2092            this.setConfigFileExistenceByNewConfiguredProject(project);
2093            return project;
2094        }
2095
2096        /* @internal */
2097        private createConfiguredProjectWithDelayLoad(configFileName: NormalizedPath, reason: string) {
2098            const project = this.createConfiguredProject(configFileName);
2099            project.pendingReload = ConfigFileProgramReloadLevel.Full;
2100            project.pendingReloadReason = reason;
2101            return project;
2102        }
2103
2104        /* @internal */
2105        createAndLoadConfiguredProject(configFileName: NormalizedPath, reason: string) {
2106            const project = this.createConfiguredProject(configFileName);
2107            this.loadConfiguredProject(project, reason);
2108            return project;
2109        }
2110
2111        /* @internal */
2112        private createLoadAndUpdateConfiguredProject(configFileName: NormalizedPath, reason: string) {
2113            const project = this.createAndLoadConfiguredProject(configFileName, reason);
2114            project.updateGraph();
2115            return project;
2116        }
2117
2118        /**
2119         * Read the config file of the project, and update the project root file names.
2120         */
2121        /* @internal */
2122        private loadConfiguredProject(project: ConfiguredProject, reason: string) {
2123            this.sendProjectLoadingStartEvent(project, reason);
2124
2125            // Read updated contents from disk
2126            const configFilename = normalizePath(project.getConfigFilePath());
2127
2128            const configFileContent = tryReadFile(configFilename, fileName => this.host.readFile(fileName));
2129            const result = parseJsonText(configFilename, isString(configFileContent) ? configFileContent : "");
2130            const configFileErrors = result.parseDiagnostics as Diagnostic[];
2131            if (!isString(configFileContent)) configFileErrors.push(configFileContent);
2132            const parsedCommandLine = parseJsonSourceFileConfigFileContent(
2133                result,
2134                project.getCachedDirectoryStructureHost(),
2135                getDirectoryPath(configFilename),
2136                /*existingOptions*/ {},
2137                configFilename,
2138                /*resolutionStack*/[],
2139                this.hostConfiguration.extraFileExtensions,
2140                /*extendedConfigCache*/ undefined,
2141            );
2142
2143            if (parsedCommandLine.errors.length) {
2144                configFileErrors.push(...parsedCommandLine.errors);
2145            }
2146
2147            this.logger.info(`Config: ${configFilename} : ${JSON.stringify({
2148                rootNames: parsedCommandLine.fileNames,
2149                options: parsedCommandLine.options,
2150                projectReferences: parsedCommandLine.projectReferences
2151            }, /*replacer*/ undefined, " ")}`);
2152
2153            Debug.assert(!!parsedCommandLine.fileNames);
2154            const compilerOptions = parsedCommandLine.options;
2155
2156            // Update the project
2157            if (!project.projectOptions) {
2158                project.projectOptions = {
2159                    configHasExtendsProperty: parsedCommandLine.raw.extends !== undefined,
2160                    configHasFilesProperty: parsedCommandLine.raw.files !== undefined,
2161                    configHasIncludeProperty: parsedCommandLine.raw.include !== undefined,
2162                    configHasExcludeProperty: parsedCommandLine.raw.exclude !== undefined
2163                };
2164            }
2165            project.canConfigFileJsonReportNoInputFiles = canJsonReportNoInputFiles(parsedCommandLine.raw);
2166            project.setProjectErrors(configFileErrors);
2167            project.updateReferences(parsedCommandLine.projectReferences);
2168            const lastFileExceededProgramSize = this.getFilenameForExceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, compilerOptions, parsedCommandLine.fileNames, fileNamePropertyReader);
2169            if (lastFileExceededProgramSize) {
2170                project.disableLanguageService(lastFileExceededProgramSize);
2171                project.stopWatchingWildCards();
2172                this.removeProjectFromSharedExtendedConfigFileMap(project);
2173            }
2174            else {
2175                project.setCompilerOptions(compilerOptions);
2176                project.setWatchOptions(parsedCommandLine.watchOptions);
2177                project.enableLanguageService();
2178                project.watchWildcards(new Map(getEntries(parsedCommandLine.wildcardDirectories!))); // TODO: GH#18217
2179                this.updateSharedExtendedConfigFileMap(project, parsedCommandLine);
2180            }
2181            project.enablePluginsWithOptions(compilerOptions, this.currentPluginConfigOverrides);
2182            const filesToAdd = parsedCommandLine.fileNames.concat(project.getExternalFiles());
2183            this.updateRootAndOptionsOfNonInferredProject(project, filesToAdd, fileNamePropertyReader, compilerOptions, parsedCommandLine.typeAcquisition!, parsedCommandLine.compileOnSave, parsedCommandLine.watchOptions);
2184        }
2185
2186        private updateNonInferredProjectFiles<T>(project: ExternalProject | ConfiguredProject | AutoImportProviderProject, files: T[], propertyReader: FilePropertyReader<T>) {
2187            const projectRootFilesMap = project.getRootFilesMap();
2188            const newRootScriptInfoMap = new Map<string, true>();
2189
2190            for (const f of files) {
2191                const newRootFile = propertyReader.getFileName(f);
2192                const fileName = toNormalizedPath(newRootFile);
2193                const isDynamic = isDynamicFileName(fileName);
2194                let path: Path;
2195                // Use the project's fileExists so that it can use caching instead of reaching to disk for the query
2196                if (!isDynamic && !project.fileExists(newRootFile)) {
2197                    path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName);
2198                    const existingValue = projectRootFilesMap.get(path);
2199                    if (existingValue) {
2200                        if (existingValue.info) {
2201                            project.removeFile(existingValue.info, /*fileExists*/ false, /*detachFromProject*/ true);
2202                            existingValue.info = undefined;
2203                        }
2204                        existingValue.fileName = fileName;
2205                    }
2206                    else {
2207                        projectRootFilesMap.set(path, { fileName });
2208                    }
2209                }
2210                else {
2211                    const scriptKind = propertyReader.getScriptKind(f, this.hostConfiguration.extraFileExtensions);
2212                    const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions);
2213                    const scriptInfo = Debug.checkDefined(this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(
2214                        fileName,
2215                        project.currentDirectory,
2216                        scriptKind,
2217                        hasMixedContent,
2218                        project.directoryStructureHost
2219                    ));
2220                    path = scriptInfo.path;
2221                    const existingValue = projectRootFilesMap.get(path);
2222                    // If this script info is not already a root add it
2223                    if (!existingValue || existingValue.info !== scriptInfo) {
2224                        project.addRoot(scriptInfo, fileName);
2225                        if (scriptInfo.isScriptOpen()) {
2226                            // if file is already root in some inferred project
2227                            // - remove the file from that project and delete the project if necessary
2228                            this.removeRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo);
2229                        }
2230                    }
2231                    else {
2232                        // Already root update the fileName
2233                        existingValue.fileName = fileName;
2234                    }
2235                }
2236                newRootScriptInfoMap.set(path, true);
2237            }
2238
2239            // project's root file map size is always going to be same or larger than new roots map
2240            // as we have already all the new files to the project
2241            if (projectRootFilesMap.size > newRootScriptInfoMap.size) {
2242                projectRootFilesMap.forEach((value, path) => {
2243                    if (!newRootScriptInfoMap.has(path)) {
2244                        if (value.info) {
2245                            project.removeFile(value.info, project.fileExists(path), /*detachFromProject*/ true);
2246                        }
2247                        else {
2248                            projectRootFilesMap.delete(path);
2249                        }
2250                    }
2251                });
2252            }
2253
2254            // Just to ensure that even if root files dont change, the changes to the non root file are picked up,
2255            // mark the project as dirty unconditionally
2256            project.markAsDirty();
2257        }
2258
2259        private updateRootAndOptionsOfNonInferredProject<T>(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader<T>, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean | undefined, watchOptions: WatchOptions | undefined) {
2260            project.setCompilerOptions(newOptions);
2261            project.setWatchOptions(watchOptions);
2262            // VS only set the CompileOnSaveEnabled option in the request if the option was changed recently
2263            // therefore if it is undefined, it should not be updated.
2264            if (compileOnSave !== undefined) {
2265                project.compileOnSaveEnabled = compileOnSave;
2266            }
2267            this.addFilesToNonInferredProject(project, newUncheckedFiles, propertyReader, newTypeAcquisition);
2268        }
2269
2270        /**
2271         * Reload the file names from config file specs and update the project graph
2272         */
2273        /*@internal*/
2274        reloadFileNamesOfConfiguredProject(project: ConfiguredProject) {
2275            const configFileSpecs = project.getCompilerOptions().configFile!.configFileSpecs!;
2276            const configFileName = project.getConfigFilePath();
2277            const fileNames = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), project.getCompilationSettings(), project.getCachedDirectoryStructureHost(), this.hostConfiguration.extraFileExtensions);
2278            project.updateErrorOnNoInputFiles(fileNames);
2279            this.updateNonInferredProjectFiles(project, fileNames.concat(project.getExternalFiles()), fileNamePropertyReader);
2280            return project.updateGraph();
2281        }
2282
2283        /*@internal*/
2284        setFileNamesOfAutoImportProviderProject(project: AutoImportProviderProject, fileNames: string[]) {
2285            this.updateNonInferredProjectFiles(project, fileNames, fileNamePropertyReader);
2286        }
2287
2288        /**
2289         * Read the config file of the project again by clearing the cache and update the project graph
2290         */
2291        /* @internal */
2292        reloadConfiguredProject(project: ConfiguredProject, reason: string, isInitialLoad: boolean, clearSemanticCache: boolean) {
2293            // At this point, there is no reason to not have configFile in the host
2294            const host = project.getCachedDirectoryStructureHost();
2295            if (clearSemanticCache) this.clearSemanticCache(project);
2296
2297            // Clear the cache since we are reloading the project from disk
2298            host.clearCache();
2299            const configFileName = project.getConfigFilePath();
2300            this.logger.info(`${isInitialLoad ? "Loading" : "Reloading"} configured project ${configFileName}`);
2301
2302            // Load project from the disk
2303            this.loadConfiguredProject(project, reason);
2304            project.updateGraph();
2305
2306            this.sendConfigFileDiagEvent(project, configFileName);
2307        }
2308
2309        /* @internal */
2310        private clearSemanticCache(project: Project) {
2311            project.resolutionCache.clear();
2312            project.getLanguageService(/*ensureSynchronized*/ false).cleanupSemanticCache();
2313            project.markAsDirty();
2314        }
2315
2316        private sendConfigFileDiagEvent(project: ConfiguredProject, triggerFile: NormalizedPath) {
2317            if (!this.eventHandler || this.suppressDiagnosticEvents) {
2318                return;
2319            }
2320            const diagnostics = project.getLanguageService().getCompilerOptionsDiagnostics();
2321            diagnostics.push(...project.getAllProjectErrors());
2322
2323            this.eventHandler(<ConfigFileDiagEvent>{
2324                eventName: ConfigFileDiagEvent,
2325                data: { configFileName: project.getConfigFilePath(), diagnostics, triggerFile }
2326            });
2327        }
2328
2329        private getOrCreateInferredProjectForProjectRootPathIfEnabled(info: ScriptInfo, projectRootPath: NormalizedPath | undefined): InferredProject | undefined {
2330            if (!this.useInferredProjectPerProjectRoot ||
2331                // Its a dynamic info opened without project root
2332                (info.isDynamic && projectRootPath === undefined)) {
2333                return undefined;
2334            }
2335
2336            if (projectRootPath) {
2337                const canonicalProjectRootPath = this.toCanonicalFileName(projectRootPath);
2338                // if we have an explicit project root path, find (or create) the matching inferred project.
2339                for (const project of this.inferredProjects) {
2340                    if (project.projectRootPath === canonicalProjectRootPath) {
2341                        return project;
2342                    }
2343                }
2344                return this.createInferredProject(projectRootPath, /*isSingleInferredProject*/ false, projectRootPath);
2345            }
2346
2347            // we don't have an explicit root path, so we should try to find an inferred project
2348            // that more closely contains the file.
2349            let bestMatch: InferredProject | undefined;
2350            for (const project of this.inferredProjects) {
2351                // ignore single inferred projects (handled elsewhere)
2352                if (!project.projectRootPath) continue;
2353                // ignore inferred projects that don't contain the root's path
2354                if (!containsPath(project.projectRootPath, info.path, this.host.getCurrentDirectory(), !this.host.useCaseSensitiveFileNames)) continue;
2355                // ignore inferred projects that are higher up in the project root.
2356                // TODO(rbuckton): Should we add the file as a root to these as well?
2357                if (bestMatch && bestMatch.projectRootPath!.length > project.projectRootPath.length) continue;
2358                bestMatch = project;
2359            }
2360
2361            return bestMatch;
2362        }
2363
2364        private getOrCreateSingleInferredProjectIfEnabled(): InferredProject | undefined {
2365            if (!this.useSingleInferredProject) {
2366                return undefined;
2367            }
2368
2369            // If `useInferredProjectPerProjectRoot` is not enabled, then there will only be one
2370            // inferred project for all files. If `useInferredProjectPerProjectRoot` is enabled
2371            // then we want to put all files that are not opened with a `projectRootPath` into
2372            // the same inferred project.
2373            //
2374            // To avoid the cost of searching through the array and to optimize for the case where
2375            // `useInferredProjectPerProjectRoot` is not enabled, we will always put the inferred
2376            // project for non-rooted files at the front of the array.
2377            if (this.inferredProjects.length > 0 && this.inferredProjects[0].projectRootPath === undefined) {
2378                return this.inferredProjects[0];
2379            }
2380
2381            // Single inferred project does not have a project root and hence no current directory
2382            return this.createInferredProject(/*currentDirectory*/ undefined, /*isSingleInferredProject*/ true);
2383        }
2384
2385        private getOrCreateSingleInferredWithoutProjectRoot(currentDirectory: string | undefined): InferredProject {
2386            Debug.assert(!this.useSingleInferredProject);
2387            const expectedCurrentDirectory = this.toCanonicalFileName(this.getNormalizedAbsolutePath(currentDirectory || ""));
2388            // Reuse the project with same current directory but no roots
2389            for (const inferredProject of this.inferredProjects) {
2390                if (!inferredProject.projectRootPath &&
2391                    inferredProject.isOrphan() &&
2392                    inferredProject.canonicalCurrentDirectory === expectedCurrentDirectory) {
2393                    return inferredProject;
2394                }
2395            }
2396
2397            return this.createInferredProject(currentDirectory);
2398        }
2399
2400        private createInferredProject(currentDirectory: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: NormalizedPath): InferredProject {
2401            const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects!; // TODO: GH#18217
2402            let watchOptionsAndErrors: WatchOptionsAndErrors | false | undefined;
2403            let typeAcquisition: TypeAcquisition | undefined;
2404            if (projectRootPath) {
2405                watchOptionsAndErrors = this.watchOptionsForInferredProjectsPerProjectRoot.get(projectRootPath);
2406                typeAcquisition = this.typeAcquisitionForInferredProjectsPerProjectRoot.get(projectRootPath);
2407            }
2408            if (watchOptionsAndErrors === undefined) {
2409                watchOptionsAndErrors = this.watchOptionsForInferredProjects;
2410            }
2411            if (typeAcquisition === undefined) {
2412                typeAcquisition = this.typeAcquisitionForInferredProjects;
2413            }
2414            watchOptionsAndErrors = watchOptionsAndErrors || undefined;
2415            const project = new InferredProject(this, this.documentRegistry, compilerOptions, watchOptionsAndErrors?.watchOptions, projectRootPath, currentDirectory, this.currentPluginConfigOverrides, typeAcquisition);
2416            project.setProjectErrors(watchOptionsAndErrors?.errors);
2417            if (isSingleInferredProject) {
2418                this.inferredProjects.unshift(project);
2419            }
2420            else {
2421                this.inferredProjects.push(project);
2422            }
2423            return project;
2424        }
2425
2426        /*@internal*/
2427        getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, currentDirectory: string, hostToQueryFileExistsOn: DirectoryStructureHost) {
2428            return this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(
2429                toNormalizedPath(uncheckedFileName), currentDirectory, /*scriptKind*/ undefined,
2430                /*hasMixedContent*/ undefined, hostToQueryFileExistsOn
2431            );
2432        }
2433
2434        getScriptInfo(uncheckedFileName: string) {
2435            return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
2436        }
2437
2438        /* @internal */
2439        getScriptInfoOrConfig(uncheckedFileName: string): ScriptInfoOrConfig | undefined {
2440            const path = toNormalizedPath(uncheckedFileName);
2441            const info = this.getScriptInfoForNormalizedPath(path);
2442            if (info) return info;
2443            const configProject = this.configuredProjects.get(this.toPath(uncheckedFileName));
2444            return configProject && configProject.getCompilerOptions().configFile;
2445        }
2446
2447        /* @internal */
2448        logErrorForScriptInfoNotFound(fileName: string): void {
2449            const names = arrayFrom(this.filenameToScriptInfo.entries()).map(([path, scriptInfo]) => ({ path, fileName: scriptInfo.fileName }));
2450            this.logger.msg(`Could not find file ${JSON.stringify(fileName)}.\nAll files are: ${JSON.stringify(names)}`, Msg.Err);
2451        }
2452
2453        /**
2454         * Returns the projects that contain script info through SymLink
2455         * Note that this does not return projects in info.containingProjects
2456         */
2457        /*@internal*/
2458        getSymlinkedProjects(info: ScriptInfo): MultiMap<Path, Project> | undefined {
2459            let projects: MultiMap<Path, Project> | undefined;
2460            if (this.realpathToScriptInfos) {
2461                const realpath = info.getRealpathIfDifferent();
2462                if (realpath) {
2463                    forEach(this.realpathToScriptInfos.get(realpath), combineProjects);
2464                }
2465                forEach(this.realpathToScriptInfos.get(info.path), combineProjects);
2466            }
2467
2468            return projects;
2469
2470            function combineProjects(toAddInfo: ScriptInfo) {
2471                if (toAddInfo !== info) {
2472                    for (const project of toAddInfo.containingProjects) {
2473                        // Add the projects only if they can use symLink targets and not already in the list
2474                        if (project.languageServiceEnabled &&
2475                            !project.isOrphan() &&
2476                            !project.getCompilerOptions().preserveSymlinks &&
2477                            !info.isAttached(project)) {
2478                            if (!projects) {
2479                                projects = createMultiMap();
2480                                projects.add(toAddInfo.path, project);
2481                            }
2482                            else if (!forEachEntry(projects, (projs, path) => path === toAddInfo.path ? false : contains(projs, project))) {
2483                                projects.add(toAddInfo.path, project);
2484                            }
2485                        }
2486                    }
2487                }
2488            }
2489        }
2490
2491        private watchClosedScriptInfo(info: ScriptInfo) {
2492            Debug.assert(!info.fileWatcher);
2493            // do not watch files with mixed content - server doesn't know how to interpret it
2494            // do not watch files in the global cache location
2495            if (!info.isDynamicOrHasMixedContent() &&
2496                (!this.globalCacheLocationDirectoryPath ||
2497                    !startsWith(info.path, this.globalCacheLocationDirectoryPath))) {
2498                const indexOfNodeModules = info.path.indexOf("/node_modules/");
2499                if (!this.host.getModifiedTime || indexOfNodeModules === -1) {
2500                    info.fileWatcher = this.watchFactory.watchFile(
2501                        info.fileName,
2502                        (_fileName, eventKind) => this.onSourceFileChanged(info, eventKind),
2503                        PollingInterval.Medium,
2504                        this.hostConfiguration.watchOptions,
2505                        WatchType.ClosedScriptInfo
2506                    );
2507                }
2508                else {
2509                    info.mTime = this.getModifiedTime(info);
2510                    info.fileWatcher = this.watchClosedScriptInfoInNodeModules(info.path.substr(0, indexOfNodeModules) as Path);
2511                }
2512            }
2513        }
2514
2515        private watchClosedScriptInfoInNodeModules(dir: Path): ScriptInfoInNodeModulesWatcher {
2516            // Watch only directory
2517            const existing = this.scriptInfoInNodeModulesWatchers.get(dir);
2518            if (existing) {
2519                existing.refCount++;
2520                return existing;
2521            }
2522
2523            const watchDir = dir + "/node_modules" as Path;
2524            const watcher = this.watchFactory.watchDirectory(
2525                watchDir,
2526                fileOrDirectory => {
2527                    const fileOrDirectoryPath = removeIgnoredPath(this.toPath(fileOrDirectory));
2528                    if (!fileOrDirectoryPath) return;
2529
2530                    // Has extension
2531                    Debug.assert(result.refCount > 0);
2532                    if (watchDir === fileOrDirectoryPath) {
2533                        this.refreshScriptInfosInDirectory(watchDir);
2534                    }
2535                    else {
2536                        const info = this.getScriptInfoForPath(fileOrDirectoryPath);
2537                        if (info) {
2538                            if (isScriptInfoWatchedFromNodeModules(info)) {
2539                                this.refreshScriptInfo(info);
2540                            }
2541                        }
2542                        // Folder
2543                        else if (!hasExtension(fileOrDirectoryPath)) {
2544                            this.refreshScriptInfosInDirectory(fileOrDirectoryPath);
2545                        }
2546                    }
2547                },
2548                WatchDirectoryFlags.Recursive,
2549                this.hostConfiguration.watchOptions,
2550                WatchType.NodeModulesForClosedScriptInfo
2551            );
2552            const result: ScriptInfoInNodeModulesWatcher = {
2553                close: () => {
2554                    if (result.refCount === 1) {
2555                        watcher.close();
2556                        this.scriptInfoInNodeModulesWatchers.delete(dir);
2557                    }
2558                    else {
2559                        result.refCount--;
2560                    }
2561                },
2562                refCount: 1
2563            };
2564            this.scriptInfoInNodeModulesWatchers.set(dir, result);
2565            return result;
2566        }
2567
2568        private getModifiedTime(info: ScriptInfo) {
2569            return (this.host.getModifiedTime!(info.path) || missingFileModifiedTime).getTime();
2570        }
2571
2572        private refreshScriptInfo(info: ScriptInfo) {
2573            const mTime = this.getModifiedTime(info);
2574            if (mTime !== info.mTime) {
2575                const eventKind = getFileWatcherEventKind(info.mTime!, mTime);
2576                info.mTime = mTime;
2577                this.onSourceFileChanged(info, eventKind);
2578            }
2579        }
2580
2581        private refreshScriptInfosInDirectory(dir: Path) {
2582            dir = dir + directorySeparator as Path;
2583            this.filenameToScriptInfo.forEach(info => {
2584                if (isScriptInfoWatchedFromNodeModules(info) && startsWith(info.path, dir)) {
2585                    this.refreshScriptInfo(info);
2586                }
2587            });
2588        }
2589
2590        private stopWatchingScriptInfo(info: ScriptInfo) {
2591            if (info.fileWatcher) {
2592                info.fileWatcher.close();
2593                info.fileWatcher = undefined;
2594            }
2595        }
2596
2597        private getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, hostToQueryFileExistsOn: DirectoryStructureHost | undefined) {
2598            if (isRootedDiskPath(fileName) || isDynamicFileName(fileName)) {
2599                return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
2600            }
2601
2602            // This is non rooted path with different current directory than project service current directory
2603            // Only paths recognized are open relative file paths
2604            const info = this.openFilesWithNonRootedDiskPath.get(this.toCanonicalFileName(fileName));
2605            if (info) {
2606                return info;
2607            }
2608
2609            // This means triple slash references wont be resolved in dynamic and unsaved files
2610            // which is intentional since we dont know what it means to be relative to non disk files
2611            return undefined;
2612        }
2613
2614        private getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined) {
2615            return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
2616        }
2617
2618        getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) {
2619            return this.getOrCreateScriptInfoWorker(fileName, this.currentDirectory, openedByClient, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
2620        }
2621
2622        private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) {
2623            Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content");
2624            const path = normalizedPathToPath(fileName, currentDirectory, this.toCanonicalFileName);
2625            let info = this.getScriptInfoForPath(path);
2626            if (!info) {
2627                const isDynamic = isDynamicFileName(fileName);
2628                Debug.assert(isRootedDiskPath(fileName) || isDynamic || openedByClient, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nScript info with non-dynamic relative file name can only be open script info or in context of host currentDirectory`);
2629                Debug.assert(!isRootedDiskPath(fileName) || this.currentDirectory === currentDirectory || !this.openFilesWithNonRootedDiskPath.has(this.toCanonicalFileName(fileName)), "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nOpen script files with non rooted disk path opened with current directory context cannot have same canonical names`);
2630                Debug.assert(!isDynamic || this.currentDirectory === currentDirectory || this.useInferredProjectPerProjectRoot, "", () => `${JSON.stringify({ fileName, currentDirectory, hostCurrentDirectory: this.currentDirectory, openKeys: arrayFrom(this.openFilesWithNonRootedDiskPath.keys()) })}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.`);
2631                // If the file is not opened by client and the file doesnot exist on the disk, return
2632                if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) {
2633                    return;
2634                }
2635                info = new ScriptInfo(this.host, fileName, scriptKind!, !!hasMixedContent, path, this.filenameToScriptInfoVersion.get(path)); // TODO: GH#18217
2636                this.filenameToScriptInfo.set(info.path, info);
2637                this.filenameToScriptInfoVersion.delete(info.path);
2638                if (!openedByClient) {
2639                    this.watchClosedScriptInfo(info);
2640                }
2641                else if (!isRootedDiskPath(fileName) && (!isDynamic || this.currentDirectory !== currentDirectory)) {
2642                    // File that is opened by user but isn't rooted disk path
2643                    this.openFilesWithNonRootedDiskPath.set(this.toCanonicalFileName(fileName), info);
2644                }
2645            }
2646            if (openedByClient) {
2647                // Opening closed script info
2648                // either it was created just now, or was part of projects but was closed
2649                this.stopWatchingScriptInfo(info);
2650                info.open(fileContent!);
2651                if (hasMixedContent) {
2652                    info.registerFileUpdate();
2653                }
2654            }
2655            return info;
2656        }
2657
2658        /**
2659         * This gets the script info for the normalized path. If the path is not rooted disk path then the open script info with project root context is preferred
2660         */
2661        getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
2662            return !isRootedDiskPath(fileName) && this.openFilesWithNonRootedDiskPath.get(this.toCanonicalFileName(fileName)) ||
2663                this.getScriptInfoForPath(normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName));
2664        }
2665
2666        getScriptInfoForPath(fileName: Path) {
2667            return this.filenameToScriptInfo.get(fileName);
2668        }
2669
2670        /*@internal*/
2671        getDocumentPositionMapper(project: Project, generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined {
2672            // Since declaration info and map file watches arent updating project's directory structure host (which can cache file structure) use host
2673            const declarationInfo = this.getOrCreateScriptInfoNotOpenedByClient(generatedFileName, project.currentDirectory, this.host);
2674            if (!declarationInfo) {
2675                if (sourceFileName) {
2676                    // Project contains source file and it generates the generated file name
2677                    project.addGeneratedFileWatch(generatedFileName, sourceFileName);
2678                }
2679                return undefined;
2680            }
2681
2682            // Try to get from cache
2683            declarationInfo.getSnapshot(); // Ensure synchronized
2684            if (isString(declarationInfo.sourceMapFilePath)) {
2685                // Ensure mapper is synchronized
2686                const sourceMapFileInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath);
2687                if (sourceMapFileInfo) {
2688                    sourceMapFileInfo.getSnapshot();
2689                    if (sourceMapFileInfo.documentPositionMapper !== undefined) {
2690                        sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos);
2691                        return sourceMapFileInfo.documentPositionMapper ? sourceMapFileInfo.documentPositionMapper : undefined;
2692                    }
2693                }
2694                declarationInfo.sourceMapFilePath = undefined;
2695            }
2696            else if (declarationInfo.sourceMapFilePath) {
2697                declarationInfo.sourceMapFilePath.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, declarationInfo.sourceMapFilePath.sourceInfos);
2698                return undefined;
2699            }
2700            else if (declarationInfo.sourceMapFilePath !== undefined) {
2701                // Doesnt have sourceMap
2702                return undefined;
2703            }
2704
2705            // Create the mapper
2706            let sourceMapFileInfo: ScriptInfo | undefined;
2707            let mapFileNameFromDeclarationInfo: string | undefined;
2708
2709            let readMapFile: ReadMapFile | undefined = (mapFileName, mapFileNameFromDts) => {
2710                const mapInfo = this.getOrCreateScriptInfoNotOpenedByClient(mapFileName, project.currentDirectory, this.host);
2711                if (!mapInfo) {
2712                    mapFileNameFromDeclarationInfo = mapFileNameFromDts;
2713                    return undefined;
2714                }
2715                sourceMapFileInfo = mapInfo;
2716                const snap = mapInfo.getSnapshot();
2717                if (mapInfo.documentPositionMapper !== undefined) return mapInfo.documentPositionMapper;
2718                return snap.getText(0, snap.getLength());
2719            };
2720            const projectName = project.projectName;
2721            const documentPositionMapper = getDocumentPositionMapper(
2722                { getCanonicalFileName: this.toCanonicalFileName, log: s => this.logger.info(s), getSourceFileLike: f => this.getSourceFileLike(f, projectName, declarationInfo) },
2723                declarationInfo.fileName,
2724                declarationInfo.getLineInfo(),
2725                readMapFile
2726            );
2727            readMapFile = undefined; // Remove ref to project
2728            if (sourceMapFileInfo) {
2729                declarationInfo.sourceMapFilePath = sourceMapFileInfo.path;
2730                sourceMapFileInfo.declarationInfoPath = declarationInfo.path;
2731                sourceMapFileInfo.documentPositionMapper = documentPositionMapper || false;
2732                sourceMapFileInfo.sourceInfos = this.addSourceInfoToSourceMap(sourceFileName, project, sourceMapFileInfo.sourceInfos);
2733            }
2734            else if (mapFileNameFromDeclarationInfo) {
2735                declarationInfo.sourceMapFilePath = {
2736                    watcher: this.addMissingSourceMapFile(
2737                        project.currentDirectory === this.currentDirectory ?
2738                            mapFileNameFromDeclarationInfo :
2739                            getNormalizedAbsolutePath(mapFileNameFromDeclarationInfo, project.currentDirectory),
2740                        declarationInfo.path
2741                    ),
2742                    sourceInfos: this.addSourceInfoToSourceMap(sourceFileName, project)
2743                };
2744            }
2745            else {
2746                declarationInfo.sourceMapFilePath = false;
2747            }
2748            return documentPositionMapper;
2749        }
2750
2751        private addSourceInfoToSourceMap(sourceFileName: string | undefined, project: Project, sourceInfos?: Set<Path>) {
2752            if (sourceFileName) {
2753                // Attach as source
2754                const sourceInfo = this.getOrCreateScriptInfoNotOpenedByClient(sourceFileName, project.currentDirectory, project.directoryStructureHost)!;
2755                (sourceInfos || (sourceInfos = new Set())).add(sourceInfo.path);
2756            }
2757            return sourceInfos;
2758        }
2759
2760        private addMissingSourceMapFile(mapFileName: string, declarationInfoPath: Path) {
2761            const fileWatcher = this.watchFactory.watchFile(
2762                mapFileName,
2763                () => {
2764                    const declarationInfo = this.getScriptInfoForPath(declarationInfoPath);
2765                    if (declarationInfo && declarationInfo.sourceMapFilePath && !isString(declarationInfo.sourceMapFilePath)) {
2766                        // Update declaration and source projects
2767                        this.delayUpdateProjectGraphs(declarationInfo.containingProjects, /*clearSourceMapperCache*/ true);
2768                        this.delayUpdateSourceInfoProjects(declarationInfo.sourceMapFilePath.sourceInfos);
2769                        declarationInfo.closeSourceMapFileWatcher();
2770                    }
2771                },
2772                PollingInterval.High,
2773                this.hostConfiguration.watchOptions,
2774                WatchType.MissingSourceMapFile,
2775            );
2776            return fileWatcher;
2777        }
2778
2779        /*@internal*/
2780        getSourceFileLike(fileName: string, projectNameOrProject: string | Project, declarationInfo?: ScriptInfo) {
2781            const project = (projectNameOrProject as Project).projectName ? projectNameOrProject as Project : this.findProject(projectNameOrProject as string);
2782            if (project) {
2783                const path = project.toPath(fileName);
2784                const sourceFile = project.getSourceFile(path);
2785                if (sourceFile && sourceFile.resolvedPath === path) return sourceFile;
2786            }
2787
2788            // Need to look for other files.
2789            const info = this.getOrCreateScriptInfoNotOpenedByClient(fileName, (project || this).currentDirectory, project ? project.directoryStructureHost : this.host);
2790            if (!info) return undefined;
2791
2792            // Attach as source
2793            if (declarationInfo && isString(declarationInfo.sourceMapFilePath) && info !== declarationInfo) {
2794                const sourceMapInfo = this.getScriptInfoForPath(declarationInfo.sourceMapFilePath);
2795                if (sourceMapInfo) {
2796                    (sourceMapInfo.sourceInfos || (sourceMapInfo.sourceInfos = new Set())).add(info.path);
2797                }
2798            }
2799
2800            // Key doesnt matter since its only for text and lines
2801            if (info.cacheSourceFile) return info.cacheSourceFile.sourceFile;
2802
2803            // Create sourceFileLike
2804            if (!info.sourceFileLike) {
2805                info.sourceFileLike = {
2806                    get text() {
2807                        Debug.fail("shouldnt need text");
2808                        return "";
2809                    },
2810                    getLineAndCharacterOfPosition: pos => {
2811                        const lineOffset = info.positionToLineOffset(pos);
2812                        return { line: lineOffset.line - 1, character: lineOffset.offset - 1 };
2813                    },
2814                    getPositionOfLineAndCharacter: (line, character, allowEdits) => info.lineOffsetToPosition(line + 1, character + 1, allowEdits)
2815                };
2816            }
2817            return info.sourceFileLike;
2818        }
2819
2820        /*@internal*/
2821        setPerformanceEventHandler(performanceEventHandler: PerformanceEventHandler) {
2822            this.performanceEventHandler = performanceEventHandler;
2823        }
2824
2825        setHostConfiguration(args: protocol.ConfigureRequestArguments) {
2826            if (args.file) {
2827                const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file));
2828                if (info) {
2829                    info.setOptions(convertFormatOptions(args.formatOptions!), args.preferences);
2830                    this.logger.info(`Host configuration update for file ${args.file}`);
2831                }
2832            }
2833            else {
2834                if (args.hostInfo !== undefined) {
2835                    this.hostConfiguration.hostInfo = args.hostInfo;
2836                    this.logger.info(`Host information ${args.hostInfo}`);
2837                }
2838                if (args.formatOptions) {
2839                    this.hostConfiguration.formatCodeOptions = { ...this.hostConfiguration.formatCodeOptions, ...convertFormatOptions(args.formatOptions) };
2840                    this.logger.info("Format host information updated");
2841                }
2842                if (args.preferences) {
2843                    const { lazyConfiguredProjectsFromExternalProject, includePackageJsonAutoImports } = this.hostConfiguration.preferences;
2844                    this.hostConfiguration.preferences = { ...this.hostConfiguration.preferences, ...args.preferences };
2845                    if (lazyConfiguredProjectsFromExternalProject && !this.hostConfiguration.preferences.lazyConfiguredProjectsFromExternalProject) {
2846                        // Load configured projects for external projects that are pending reload
2847                        this.configuredProjects.forEach(project => {
2848                            if (project.hasExternalProjectRef() &&
2849                                project.pendingReload === ConfigFileProgramReloadLevel.Full &&
2850                                !this.pendingProjectUpdates.has(project.getProjectName())) {
2851                                project.updateGraph();
2852                            }
2853                        });
2854                    }
2855                    if (includePackageJsonAutoImports !== args.preferences.includePackageJsonAutoImports) {
2856                        this.invalidateProjectAutoImports(/*packageJsonPath*/ undefined);
2857                    }
2858                }
2859                if (args.extraFileExtensions) {
2860                    this.hostConfiguration.extraFileExtensions = args.extraFileExtensions;
2861                    // We need to update the project structures again as it is possible that existing
2862                    // project structure could have more or less files depending on extensions permitted
2863                    this.reloadProjects();
2864                    this.logger.info("Host file extension mappings updated");
2865                }
2866
2867                if (args.watchOptions) {
2868                    this.hostConfiguration.watchOptions = convertWatchOptions(args.watchOptions)?.watchOptions;
2869                    this.logger.info(`Host watch options changed to ${JSON.stringify(this.hostConfiguration.watchOptions)}, it will be take effect for next watches.`);
2870                }
2871            }
2872        }
2873
2874        /*@internal*/
2875        getWatchOptions(project: Project) {
2876            const projectOptions = project.getWatchOptions();
2877            return projectOptions && this.hostConfiguration.watchOptions ?
2878                { ...this.hostConfiguration.watchOptions, ...projectOptions } :
2879                projectOptions || this.hostConfiguration.watchOptions;
2880        }
2881
2882        closeLog() {
2883            this.logger.close();
2884        }
2885
2886        /**
2887         * This function rebuilds the project for every file opened by the client
2888         * This does not reload contents of open files from disk. But we could do that if needed
2889         */
2890        reloadProjects() {
2891            this.logger.info("reload projects.");
2892            // If we want this to also reload open files from disk, we could do that,
2893            // but then we need to make sure we arent calling this function
2894            // (and would separate out below reloading of projects to be called when immediate reload is needed)
2895            // as there is no need to load contents of the files from the disk
2896
2897            // Reload script infos
2898            this.filenameToScriptInfo.forEach(info => {
2899                if (this.openFiles.has(info.path)) return; // Skip open files
2900                if (!info.fileWatcher) return; // not watched file
2901                // Handle as if file is changed or deleted
2902                this.onSourceFileChanged(info, this.host.fileExists(info.fileName) ? FileWatcherEventKind.Changed : FileWatcherEventKind.Deleted);
2903            });
2904            // Cancel all project updates since we will be updating them now
2905            this.pendingProjectUpdates.forEach((_project, projectName) => {
2906                this.throttledOperations.cancel(projectName);
2907                this.pendingProjectUpdates.delete(projectName);
2908            });
2909            this.throttledOperations.cancel(ensureProjectForOpenFileSchedule);
2910            this.pendingEnsureProjectForOpenFiles = false;
2911
2912            // Reload Projects
2913            this.reloadConfiguredProjectForFiles(this.openFiles as ESMap<Path, NormalizedPath | undefined>, /*clearSemanticCache*/ true, /*delayReload*/ false, returnTrue, "User requested reload projects");
2914            this.externalProjects.forEach(project => {
2915                this.clearSemanticCache(project);
2916                project.updateGraph();
2917            });
2918            this.inferredProjects.forEach(project => this.clearSemanticCache(project));
2919            this.ensureProjectForOpenFiles();
2920        }
2921
2922        private delayReloadConfiguredProjectForFiles(configFileExistenceInfo: ConfigFileExistenceInfo, ignoreIfNotRootOfInferredProject: boolean) {
2923            // Get open files to reload projects for
2924            this.reloadConfiguredProjectForFiles(
2925                configFileExistenceInfo.openFilesImpactedByConfigFile,
2926                /*clearSemanticCache*/ false,
2927                /*delayReload*/ true,
2928                ignoreIfNotRootOfInferredProject ?
2929                    isRootOfInferredProject => isRootOfInferredProject : // Reload open files if they are root of inferred project
2930                    returnTrue, // Reload all the open files impacted by config file
2931                "Change in config file detected"
2932            );
2933            this.delayEnsureProjectForOpenFiles();
2934        }
2935
2936        /**
2937         * This function goes through all the openFiles and tries to file the config file for them.
2938         * If the config file is found and it refers to existing project, it reloads it either immediately
2939         * or schedules it for reload depending on delayReload option
2940         * If the there is no existing project it just opens the configured project for the config file
2941         * reloadForInfo provides a way to filter out files to reload configured project for
2942         */
2943        private reloadConfiguredProjectForFiles<T>(openFiles: ESMap<Path, T>, clearSemanticCache: boolean, delayReload: boolean, shouldReloadProjectFor: (openFileValue: T) => boolean, reason: string) {
2944            const updatedProjects = new Map<string, true>();
2945            const reloadChildProject = (child: ConfiguredProject) => {
2946                if (!updatedProjects.has(child.canonicalConfigFilePath)) {
2947                    updatedProjects.set(child.canonicalConfigFilePath, true);
2948                    this.reloadConfiguredProject(child, reason, /*isInitialLoad*/ false, clearSemanticCache);
2949                }
2950            };
2951            // try to reload config file for all open files
2952            openFiles.forEach((openFileValue, path) => {
2953                // Invalidate default config file name for open file
2954                this.configFileForOpenFiles.delete(path);
2955                // Filter out the files that need to be ignored
2956                if (!shouldReloadProjectFor(openFileValue)) {
2957                    return;
2958                }
2959
2960                const info = this.getScriptInfoForPath(path)!; // TODO: GH#18217
2961                Debug.assert(info.isScriptOpen());
2962                // This tries to search for a tsconfig.json for the given file. If we found it,
2963                // we first detect if there is already a configured project created for it: if so,
2964                // we re- read the tsconfig file content and update the project only if we havent already done so
2965                // otherwise we create a new one.
2966                const configFileName = this.getConfigFileNameForFile(info);
2967                if (configFileName) {
2968                    const project = this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName);
2969                    if (!updatedProjects.has(project.canonicalConfigFilePath)) {
2970                        updatedProjects.set(project.canonicalConfigFilePath, true);
2971                        if (delayReload) {
2972                            project.pendingReload = ConfigFileProgramReloadLevel.Full;
2973                            project.pendingReloadReason = reason;
2974                            if (clearSemanticCache) this.clearSemanticCache(project);
2975                            this.delayUpdateProjectGraph(project);
2976                        }
2977                        else {
2978                            // reload from the disk
2979                            this.reloadConfiguredProject(project, reason, /*isInitialLoad*/ false, clearSemanticCache);
2980                            // If this project does not contain this file directly, reload the project till the reloaded project contains the script info directly
2981                            if (!projectContainsInfoDirectly(project, info)) {
2982                                const referencedProject = forEachResolvedProjectReferenceProject(
2983                                    project,
2984                                    info.path,
2985                                    child => {
2986                                        reloadChildProject(child);
2987                                        return projectContainsInfoDirectly(child, info);
2988                                    },
2989                                    ProjectReferenceProjectLoadKind.FindCreate
2990                                );
2991                                if (referencedProject) {
2992                                    // Reload the project's tree that is already present
2993                                    forEachResolvedProjectReferenceProject(
2994                                        project,
2995                                        /*fileName*/ undefined,
2996                                        reloadChildProject,
2997                                        ProjectReferenceProjectLoadKind.Find
2998                                    );
2999                                }
3000                            }
3001                        }
3002                    }
3003                }
3004            });
3005        }
3006
3007        /**
3008         * Remove the root of inferred project if script info is part of another project
3009         */
3010        private removeRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) {
3011            // If the script info is root of inferred project, it could only be first containing project
3012            // since info is added as root to the inferred project only when there are no other projects containing it
3013            // So when it is root of the inferred project and after project structure updates its now part
3014            // of multiple project it needs to be removed from that inferred project because:
3015            // - references in inferred project supersede the root part
3016            // - root / reference in non - inferred project beats root in inferred project
3017
3018            // eg. say this is structure /a/b/a.ts /a/b/c.ts where c.ts references a.ts
3019            // When a.ts is opened, since there is no configured project/external project a.ts can be part of
3020            // a.ts is added as root to inferred project.
3021            // Now at time of opening c.ts, c.ts is also not aprt of any existing project,
3022            // so it will be added to inferred project as a root. (for sake of this example assume single inferred project is false)
3023            // So at this poing a.ts is part of first inferred project and second inferred project (of which c.ts is root)
3024            // And hence it needs to be removed from the first inferred project.
3025            Debug.assert(info.containingProjects.length > 0);
3026            const firstProject = info.containingProjects[0];
3027
3028            if (!firstProject.isOrphan() &&
3029                isInferredProject(firstProject) &&
3030                firstProject.isRoot(info) &&
3031                forEach(info.containingProjects, p => p !== firstProject && !p.isOrphan())) {
3032                firstProject.removeFile(info, /*fileExists*/ true, /*detachFromProject*/ true);
3033            }
3034        }
3035
3036        /**
3037         * This function is to update the project structure for every inferred project.
3038         * It is called on the premise that all the configured projects are
3039         * up to date.
3040         * This will go through open files and assign them to inferred project if open file is not part of any other project
3041         * After that all the inferred project graphs are updated
3042         */
3043        private ensureProjectForOpenFiles() {
3044            this.logger.info("Before ensureProjectForOpenFiles:");
3045            this.printProjects();
3046
3047            this.openFiles.forEach((projectRootPath, path) => {
3048                const info = this.getScriptInfoForPath(path as Path)!;
3049                // collect all orphaned script infos from open files
3050                if (info.isOrphan()) {
3051                    this.assignOrphanScriptInfoToInferredProject(info, projectRootPath);
3052                }
3053                else {
3054                    // Or remove the root of inferred project if is referenced in more than one projects
3055                    this.removeRootOfInferredProjectIfNowPartOfOtherProject(info);
3056                }
3057            });
3058            this.pendingEnsureProjectForOpenFiles = false;
3059            this.inferredProjects.forEach(updateProjectIfDirty);
3060
3061            this.logger.info("After ensureProjectForOpenFiles:");
3062            this.printProjects();
3063        }
3064
3065        /**
3066         * Open file whose contents is managed by the client
3067         * @param filename is absolute pathname
3068         * @param fileContent is a known version of the file content that is more up to date than the one on disk
3069         */
3070        openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind, projectRootPath?: string): OpenConfiguredProjectResult {
3071            return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath ? toNormalizedPath(projectRootPath) : undefined);
3072        }
3073
3074        /*@internal*/
3075        getOriginalLocationEnsuringConfiguredProject(project: Project, location: DocumentPosition): DocumentPosition | undefined {
3076            const originalLocation = project.isSourceOfProjectReferenceRedirect(location.fileName) ?
3077                location :
3078                project.getSourceMapper().tryGetSourcePosition(location);
3079            if (!originalLocation) return undefined;
3080
3081            const { fileName } = originalLocation;
3082            if (!this.getScriptInfo(fileName) && !this.host.fileExists(fileName)) return undefined;
3083
3084            const originalFileInfo: OriginalFileInfo = { fileName: toNormalizedPath(fileName), path: this.toPath(fileName) };
3085            const configFileName = this.getConfigFileNameForFile(originalFileInfo);
3086            if (!configFileName) return undefined;
3087
3088            let configuredProject: ConfiguredProject | undefined = this.findConfiguredProjectByProjectName(configFileName) ||
3089                this.createAndLoadConfiguredProject(configFileName, `Creating project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}`);
3090            updateProjectIfDirty(configuredProject);
3091
3092            const projectContainsOriginalInfo = (project: ConfiguredProject) => {
3093                const info = this.getScriptInfo(fileName);
3094                return info && projectContainsInfoDirectly(project, info);
3095            };
3096
3097            if (configuredProject.isSolution() || !projectContainsOriginalInfo(configuredProject)) {
3098                // Find the project that is referenced from this solution that contains the script info directly
3099                configuredProject = forEachResolvedProjectReferenceProject(
3100                    configuredProject,
3101                    fileName,
3102                    child => {
3103                        updateProjectIfDirty(child);
3104                        return projectContainsOriginalInfo(child) ? child : undefined;
3105                    },
3106                    ProjectReferenceProjectLoadKind.FindCreateLoad,
3107                    `Creating project referenced in solution ${configuredProject.projectName} to find possible configured project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}`
3108                );
3109                if (!configuredProject) return undefined;
3110                if (configuredProject === project) return originalLocation;
3111            }
3112
3113            // Keep this configured project as referenced from project
3114            addOriginalConfiguredProject(configuredProject);
3115
3116            const originalScriptInfo = this.getScriptInfo(fileName);
3117            if (!originalScriptInfo || !originalScriptInfo.containingProjects.length) return undefined;
3118
3119            // Add configured projects as referenced
3120            originalScriptInfo.containingProjects.forEach(project => {
3121                if (isConfiguredProject(project)) {
3122                    addOriginalConfiguredProject(project);
3123                }
3124            });
3125            return originalLocation;
3126
3127            function addOriginalConfiguredProject(originalProject: ConfiguredProject) {
3128                if (!project.originalConfiguredProjects) {
3129                    project.originalConfiguredProjects = new Set();
3130                }
3131                project.originalConfiguredProjects.add(originalProject.canonicalConfigFilePath);
3132            }
3133        }
3134
3135        /** @internal */
3136        fileExists(fileName: NormalizedPath): boolean {
3137            return !!this.getScriptInfoForNormalizedPath(fileName) || this.host.fileExists(fileName);
3138        }
3139
3140        private findExternalProjectContainingOpenScriptInfo(info: ScriptInfo): ExternalProject | undefined {
3141            return find(this.externalProjects, proj => {
3142                // Ensure project structure is up-to-date to check if info is present in external project
3143                updateProjectIfDirty(proj);
3144                return proj.containsScriptInfo(info);
3145            });
3146        }
3147
3148        private getOrCreateOpenScriptInfo(fileName: NormalizedPath, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, projectRootPath: NormalizedPath | undefined) {
3149            const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory, fileContent, scriptKind, hasMixedContent)!; // TODO: GH#18217
3150            this.openFiles.set(info.path, projectRootPath);
3151            return info;
3152        }
3153
3154        private assignProjectToOpenedScriptInfo(info: ScriptInfo): AssignProjectResult {
3155            let configFileName: NormalizedPath | undefined;
3156            let configFileErrors: readonly Diagnostic[] | undefined;
3157            let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info);
3158            let retainProjects: ConfiguredProject[] | ConfiguredProject | undefined;
3159            let projectForConfigFileDiag: ConfiguredProject | undefined;
3160            let defaultConfigProjectIsCreated = false;
3161            if (!project && this.serverMode === LanguageServiceMode.Semantic) { // Checking semantic mode is an optimization
3162                configFileName = this.getConfigFileNameForFile(info);
3163                if (configFileName) {
3164                    project = this.findConfiguredProjectByProjectName(configFileName);
3165                    if (!project) {
3166                        project = this.createLoadAndUpdateConfiguredProject(configFileName, `Creating possible configured project for ${info.fileName} to open`);
3167                        defaultConfigProjectIsCreated = true;
3168                    }
3169                    else {
3170                        // Ensure project is ready to check if it contains opened script info
3171                        updateProjectIfDirty(project);
3172                    }
3173
3174                    projectForConfigFileDiag = project.containsScriptInfo(info) ? project : undefined;
3175                    retainProjects = project;
3176
3177                    // If this configured project doesnt contain script info but
3178                    // it is solution with project references, try those project references
3179                    if (!projectContainsInfoDirectly(project, info)) {
3180                        forEachResolvedProjectReferenceProject(
3181                            project,
3182                            info.path,
3183                            child => {
3184                                updateProjectIfDirty(child);
3185                                // Retain these projects
3186                                if (!isArray(retainProjects)) {
3187                                    retainProjects = [project as ConfiguredProject, child];
3188                                }
3189                                else {
3190                                    retainProjects.push(child);
3191                                }
3192
3193                                // If script info belongs to this child project, use this as default config project
3194                                if (projectContainsInfoDirectly(child, info)) {
3195                                    projectForConfigFileDiag = child;
3196                                    return child;
3197                                }
3198
3199                                // If this project uses the script info (even through project reference), if default project is not found, use this for configFileDiag
3200                                if (!projectForConfigFileDiag && child.containsScriptInfo(info)) {
3201                                    projectForConfigFileDiag = child;
3202                                }
3203                            },
3204                            ProjectReferenceProjectLoadKind.FindCreateLoad,
3205                            `Creating project referenced in solution ${project.projectName} to find possible configured project for ${info.fileName} to open`
3206                        );
3207                    }
3208
3209                    // Send the event only if the project got created as part of this open request and info is part of the project
3210                    if (projectForConfigFileDiag) {
3211                        configFileName = projectForConfigFileDiag.getConfigFilePath();
3212                        if (projectForConfigFileDiag !== project || defaultConfigProjectIsCreated) {
3213                            configFileErrors = projectForConfigFileDiag.getAllProjectErrors();
3214                            this.sendConfigFileDiagEvent(projectForConfigFileDiag, info.fileName);
3215                        }
3216                    }
3217                    else {
3218                        // Since the file isnt part of configured project, do not send config file info
3219                        configFileName = undefined;
3220                    }
3221
3222                    // Create ancestor configured project
3223                    this.createAncestorProjects(info, project);
3224                }
3225            }
3226
3227            // Project we have at this point is going to be updated since its either found through
3228            // - external project search, which updates the project before checking if info is present in it
3229            // - configured project - either created or updated to ensure we know correct status of info
3230
3231            // At this point we need to ensure that containing projects of the info are uptodate
3232            // This will ensure that later question of info.isOrphan() will return correct answer
3233            // and we correctly create inferred project for the info
3234            info.containingProjects.forEach(updateProjectIfDirty);
3235
3236            // At this point if file is part of any any configured or external project, then it would be present in the containing projects
3237            // So if it still doesnt have any containing projects, it needs to be part of inferred project
3238            if (info.isOrphan()) {
3239                // Even though this info did not belong to any of the configured projects, send the config file diag
3240                if (isArray(retainProjects)) {
3241                    retainProjects.forEach(project => this.sendConfigFileDiagEvent(project, info.fileName));
3242                }
3243                else if (retainProjects) {
3244                    this.sendConfigFileDiagEvent(retainProjects, info.fileName);
3245                }
3246                Debug.assert(this.openFiles.has(info.path));
3247                this.assignOrphanScriptInfoToInferredProject(info, this.openFiles.get(info.path));
3248            }
3249            Debug.assert(!info.isOrphan());
3250            return { configFileName, configFileErrors, retainProjects };
3251        }
3252
3253        private createAncestorProjects(info: ScriptInfo, project: ConfiguredProject) {
3254            // Skip if info is not part of default configured project
3255            if (!info.isAttached(project)) return;
3256
3257            // Create configured project till project root
3258            while (true) {
3259                // Skip if project is not composite
3260                if (!project.isInitialLoadPending() &&
3261                    (
3262                        !project.getCompilerOptions().composite ||
3263                        project.getCompilerOptions().disableSolutionSearching
3264                    )
3265                ) return;
3266
3267                // Get config file name
3268                const configFileName = this.getConfigFileNameForFile({
3269                    fileName: project.getConfigFilePath(),
3270                    path: info.path,
3271                    configFileInfo: true
3272                });
3273                if (!configFileName) return;
3274
3275                // find or delay load the project
3276                const ancestor = this.findConfiguredProjectByProjectName(configFileName) ||
3277                    this.createConfiguredProjectWithDelayLoad(configFileName, `Creating project possibly referencing default composite project ${project.getProjectName()} of open file ${info.fileName}`);
3278                if (ancestor.isInitialLoadPending()) {
3279                    // Set a potential project reference
3280                    ancestor.setPotentialProjectReference(project.canonicalConfigFilePath);
3281                }
3282                project = ancestor;
3283            }
3284        }
3285
3286        /*@internal*/
3287        loadAncestorProjectTree(forProjects?: ReadonlyCollection<string>) {
3288            forProjects = forProjects || mapDefinedEntries(
3289                this.configuredProjects,
3290                (key, project) => !project.isInitialLoadPending() ? [key, true] : undefined
3291            );
3292
3293            const seenProjects = new Set<NormalizedPath>();
3294            // Work on array copy as we could add more projects as part of callback
3295            for (const project of arrayFrom(this.configuredProjects.values())) {
3296                // If this project has potential project reference for any of the project we are loading ancestor tree for
3297                // load this project first
3298                if (forEachPotentialProjectReference(project, potentialRefPath => forProjects!.has(potentialRefPath))) {
3299                    updateProjectIfDirty(project);
3300                }
3301                this.ensureProjectChildren(project, forProjects, seenProjects);
3302            }
3303        }
3304
3305        private ensureProjectChildren(project: ConfiguredProject, forProjects: ReadonlyCollection<string>, seenProjects: Set<NormalizedPath>) {
3306            if (!tryAddToSet(seenProjects, project.canonicalConfigFilePath)) return;
3307
3308            // If this project disables child load ignore it
3309            if (project.getCompilerOptions().disableReferencedProjectLoad) return;
3310
3311            const children = project.getCurrentProgram()?.getResolvedProjectReferences();
3312            if (!children) return;
3313
3314            for (const child of children) {
3315                if (!child) continue;
3316                const referencedProject = forEachResolvedProjectReference(child.references, ref => forProjects.has(ref.sourceFile.path) ? ref : undefined);
3317                if (!referencedProject) continue;
3318
3319                // Load this project,
3320                const configFileName = toNormalizedPath(child.sourceFile.fileName);
3321                const childProject = project.projectService.findConfiguredProjectByProjectName(configFileName) ||
3322                    project.projectService.createAndLoadConfiguredProject(configFileName, `Creating project referenced by : ${project.projectName} as it references project ${referencedProject.sourceFile.fileName}`);
3323                updateProjectIfDirty(childProject);
3324
3325                // Ensure children for this project
3326                this.ensureProjectChildren(childProject, forProjects, seenProjects);
3327            }
3328        }
3329
3330        private cleanupAfterOpeningFile(toRetainConfigProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) {
3331            // This was postponed from closeOpenFile to after opening next file,
3332            // so that we can reuse the project if we need to right away
3333            this.removeOrphanConfiguredProjects(toRetainConfigProjects);
3334
3335            // Remove orphan inferred projects now that we have reused projects
3336            // We need to create a duplicate because we cant guarantee order after removal
3337            for (const inferredProject of this.inferredProjects.slice()) {
3338                if (inferredProject.isOrphan()) {
3339                    this.removeProject(inferredProject);
3340                }
3341            }
3342
3343            // Delete the orphan files here because there might be orphan script infos (which are not part of project)
3344            // when some file/s were closed which resulted in project removal.
3345            // It was then postponed to cleanup these script infos so that they can be reused if
3346            // the file from that old project is reopened because of opening file from here.
3347            this.removeOrphanScriptInfos();
3348        }
3349
3350        openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult {
3351            const info = this.getOrCreateOpenScriptInfo(fileName, fileContent, scriptKind, hasMixedContent, projectRootPath);
3352            const { retainProjects, ...result } = this.assignProjectToOpenedScriptInfo(info);
3353            this.cleanupAfterOpeningFile(retainProjects);
3354            this.telemetryOnOpenFile(info);
3355            this.printProjects();
3356            return result;
3357        }
3358
3359        private removeOrphanConfiguredProjects(toRetainConfiguredProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) {
3360            const toRemoveConfiguredProjects = new Map(this.configuredProjects);
3361            const markOriginalProjectsAsUsed = (project: Project) => {
3362                if (!project.isOrphan() && project.originalConfiguredProjects) {
3363                    project.originalConfiguredProjects.forEach(
3364                        (_value, configuredProjectPath) => {
3365                            const project = this.getConfiguredProjectByCanonicalConfigFilePath(configuredProjectPath);
3366                            return project && retainConfiguredProject(project);
3367                        }
3368                    );
3369                }
3370            };
3371            if (toRetainConfiguredProjects) {
3372                if (isArray(toRetainConfiguredProjects)) {
3373                    toRetainConfiguredProjects.forEach(retainConfiguredProject);
3374                }
3375                else {
3376                    retainConfiguredProject(toRetainConfiguredProjects);
3377                }
3378            }
3379
3380            // Do not remove configured projects that are used as original projects of other
3381            this.inferredProjects.forEach(markOriginalProjectsAsUsed);
3382            this.externalProjects.forEach(markOriginalProjectsAsUsed);
3383            this.configuredProjects.forEach(project => {
3384                // If project has open ref (there are more than zero references from external project/open file), keep it alive as well as any project it references
3385                if (project.hasOpenRef()) {
3386                    retainConfiguredProject(project);
3387                }
3388                else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) {
3389                    // If the configured project for project reference has more than zero references, keep it alive
3390                    forEachReferencedProject(
3391                        project,
3392                        ref => isRetained(ref) && retainConfiguredProject(project)
3393                    );
3394                }
3395            });
3396
3397            // Remove all the non marked projects
3398            toRemoveConfiguredProjects.forEach(project => this.removeProject(project));
3399
3400            function isRetained(project: ConfiguredProject) {
3401                return project.hasOpenRef() || !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath);
3402            }
3403
3404            function retainConfiguredProject(project: ConfiguredProject) {
3405                if (toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath)) {
3406                    // Keep original projects used
3407                    markOriginalProjectsAsUsed(project);
3408                    // Keep all the references alive
3409                    forEachReferencedProject(project, retainConfiguredProject);
3410                }
3411            }
3412        }
3413
3414        private removeOrphanScriptInfos() {
3415            const toRemoveScriptInfos = new Map(this.filenameToScriptInfo);
3416            this.filenameToScriptInfo.forEach(info => {
3417                // If script info is open or orphan, retain it and its dependencies
3418                if (!info.isScriptOpen() && info.isOrphan() && !info.isContainedByAutoImportProvider()) {
3419                    // Otherwise if there is any source info that is alive, this alive too
3420                    if (!info.sourceMapFilePath) return;
3421                    let sourceInfos: Set<Path> | undefined;
3422                    if (isString(info.sourceMapFilePath)) {
3423                        const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath);
3424                        sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos;
3425                    }
3426                    else {
3427                        sourceInfos = info.sourceMapFilePath.sourceInfos;
3428                    }
3429                    if (!sourceInfos) return;
3430                    if (!forEachKey(sourceInfos, path => {
3431                        const info = this.getScriptInfoForPath(path);
3432                        return !!info && (info.isScriptOpen() || !info.isOrphan());
3433                    })) {
3434                        return;
3435                    }
3436                }
3437
3438                // Retain this script info
3439                toRemoveScriptInfos.delete(info.path);
3440                if (info.sourceMapFilePath) {
3441                    let sourceInfos: Set<Path> | undefined;
3442                    if (isString(info.sourceMapFilePath)) {
3443                        // And map file info and source infos
3444                        toRemoveScriptInfos.delete(info.sourceMapFilePath);
3445                        const sourceMapInfo = this.getScriptInfoForPath(info.sourceMapFilePath);
3446                        sourceInfos = sourceMapInfo && sourceMapInfo.sourceInfos;
3447                    }
3448                    else {
3449                        sourceInfos = info.sourceMapFilePath.sourceInfos;
3450                    }
3451                    if (sourceInfos) {
3452                        sourceInfos.forEach((_value, path) => toRemoveScriptInfos.delete(path));
3453                    }
3454                }
3455            });
3456
3457            toRemoveScriptInfos.forEach(info => {
3458                // if there are not projects that include this script info - delete it
3459                this.stopWatchingScriptInfo(info);
3460                this.deleteScriptInfo(info);
3461                info.closeSourceMapFileWatcher();
3462            });
3463        }
3464
3465        private telemetryOnOpenFile(scriptInfo: ScriptInfo): void {
3466            if (this.serverMode !== LanguageServiceMode.Semantic || !this.eventHandler || !scriptInfo.isJavaScript() || !addToSeen(this.allJsFilesForOpenFileTelemetry, scriptInfo.path)) {
3467                return;
3468            }
3469
3470            const project = scriptInfo.getDefaultProject();
3471            if (!project.languageServiceEnabled) {
3472                return;
3473            }
3474
3475            const sourceFile = project.getSourceFile(scriptInfo.path);
3476            const checkJs = !!sourceFile && !!sourceFile.checkJsDirective;
3477            this.eventHandler({ eventName: OpenFileInfoTelemetryEvent, data: { info: { checkJs } } });
3478        }
3479
3480        /**
3481         * Close file whose contents is managed by the client
3482         * @param filename is absolute pathname
3483         */
3484        closeClientFile(uncheckedFileName: string): void;
3485        /*@internal*/
3486        closeClientFile(uncheckedFileName: string, skipAssignOrphanScriptInfosToInferredProject: true): boolean;
3487        closeClientFile(uncheckedFileName: string, skipAssignOrphanScriptInfosToInferredProject?: true) {
3488            const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
3489            const result = info ? this.closeOpenFile(info, skipAssignOrphanScriptInfosToInferredProject) : false;
3490            if (!skipAssignOrphanScriptInfosToInferredProject) {
3491                this.printProjects();
3492            }
3493            return result;
3494        }
3495
3496        private collectChanges(
3497            lastKnownProjectVersions: protocol.ProjectVersionInfo[],
3498            currentProjects: Project[],
3499            includeProjectReferenceRedirectInfo: boolean | undefined,
3500            result: ProjectFilesWithTSDiagnostics[]
3501            ): void {
3502            for (const proj of currentProjects) {
3503                const knownProject = find(lastKnownProjectVersions, p => p.projectName === proj.getProjectName());
3504                result.push(proj.getChangesSinceVersion(knownProject && knownProject.version, includeProjectReferenceRedirectInfo));
3505            }
3506        }
3507
3508        /* @internal */
3509        synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[], includeProjectReferenceRedirectInfo?: boolean): ProjectFilesWithTSDiagnostics[] {
3510            const files: ProjectFilesWithTSDiagnostics[] = [];
3511            this.collectChanges(knownProjects, this.externalProjects, includeProjectReferenceRedirectInfo, files);
3512            this.collectChanges(knownProjects, arrayFrom(this.configuredProjects.values()), includeProjectReferenceRedirectInfo, files);
3513            this.collectChanges(knownProjects, this.inferredProjects, includeProjectReferenceRedirectInfo, files);
3514            return files;
3515        }
3516
3517        /* @internal */
3518        applyChangesInOpenFiles(openFiles: Iterator<OpenFileArguments> | undefined, changedFiles?: Iterator<ChangeFileArguments>, closedFiles?: string[]): void {
3519            let openScriptInfos: ScriptInfo[] | undefined;
3520            let assignOrphanScriptInfosToInferredProject = false;
3521            if (openFiles) {
3522                while (true) {
3523                    const iterResult = openFiles.next();
3524                    if (iterResult.done) break;
3525                    const file = iterResult.value;
3526                    // Create script infos so we have the new content for all the open files before we do any updates to projects
3527                    const info = this.getOrCreateOpenScriptInfo(
3528                        toNormalizedPath(file.fileName),
3529                        file.content,
3530                        tryConvertScriptKindName(file.scriptKind!),
3531                        file.hasMixedContent,
3532                        file.projectRootPath ? toNormalizedPath(file.projectRootPath) : undefined
3533                    );
3534                    (openScriptInfos || (openScriptInfos = [])).push(info);
3535                }
3536            }
3537
3538            if (changedFiles) {
3539                while (true) {
3540                    const iterResult = changedFiles.next();
3541                    if (iterResult.done) break;
3542                    const file = iterResult.value;
3543                    const scriptInfo = this.getScriptInfo(file.fileName)!;
3544                    Debug.assert(!!scriptInfo);
3545                    // Make edits to script infos and marks containing project as dirty
3546                    this.applyChangesToFile(scriptInfo, file.changes);
3547                }
3548            }
3549
3550            if (closedFiles) {
3551                for (const file of closedFiles) {
3552                    // Close files, but dont assign projects to orphan open script infos, that part comes later
3553                    assignOrphanScriptInfosToInferredProject = this.closeClientFile(file, /*skipAssignOrphanScriptInfosToInferredProject*/ true) || assignOrphanScriptInfosToInferredProject;
3554                }
3555            }
3556
3557            // All the script infos now exist, so ok to go update projects for open files
3558            let retainProjects: readonly ConfiguredProject[] | undefined;
3559            if (openScriptInfos) {
3560                retainProjects = flatMap(openScriptInfos, info => this.assignProjectToOpenedScriptInfo(info).retainProjects);
3561            }
3562
3563            // While closing files there could be open files that needed assigning new inferred projects, do it now
3564            if (assignOrphanScriptInfosToInferredProject) {
3565                this.assignOrphanScriptInfosToInferredProject();
3566            }
3567
3568            if (openScriptInfos) {
3569                // Cleanup projects
3570                this.cleanupAfterOpeningFile(retainProjects);
3571                // Telemetry
3572                openScriptInfos.forEach(info => this.telemetryOnOpenFile(info));
3573                this.printProjects();
3574            }
3575            else if (length(closedFiles)) {
3576                this.printProjects();
3577            }
3578        }
3579
3580        /* @internal */
3581        applyChangesToFile(scriptInfo: ScriptInfo, changes: Iterator<TextChange>) {
3582            while (true) {
3583                const iterResult = changes.next();
3584                if (iterResult.done) break;
3585                const change = iterResult.value;
3586                scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText);
3587            }
3588        }
3589
3590        private closeConfiguredProjectReferencedFromExternalProject(configFile: NormalizedPath) {
3591            const configuredProject = this.findConfiguredProjectByProjectName(configFile);
3592            if (configuredProject) {
3593                configuredProject.deleteExternalProjectReference();
3594                if (!configuredProject.hasOpenRef()) {
3595                    this.removeProject(configuredProject);
3596                    return;
3597                }
3598            }
3599        }
3600
3601        closeExternalProject(uncheckedFileName: string): void {
3602            const fileName = toNormalizedPath(uncheckedFileName);
3603            const configFiles = this.externalProjectToConfiguredProjectMap.get(fileName);
3604            if (configFiles) {
3605                for (const configFile of configFiles) {
3606                    this.closeConfiguredProjectReferencedFromExternalProject(configFile);
3607                }
3608                this.externalProjectToConfiguredProjectMap.delete(fileName);
3609            }
3610            else {
3611                // close external project
3612                const externalProject = this.findExternalProjectByProjectName(uncheckedFileName);
3613                if (externalProject) {
3614                    this.removeProject(externalProject);
3615                }
3616            }
3617        }
3618
3619        openExternalProjects(projects: protocol.ExternalProject[]): void {
3620            // record project list before the update
3621            const projectsToClose = arrayToMap(this.externalProjects, p => p.getProjectName(), _ => true);
3622            forEachKey(this.externalProjectToConfiguredProjectMap, externalProjectName => {
3623                projectsToClose.set(externalProjectName, true);
3624            });
3625
3626            for (const externalProject of projects) {
3627                this.openExternalProject(externalProject);
3628                // delete project that is present in input list
3629                projectsToClose.delete(externalProject.projectFileName);
3630            }
3631
3632            // close projects that were missing in the input list
3633            forEachKey(projectsToClose, externalProjectName => {
3634                this.closeExternalProject(externalProjectName);
3635            });
3636        }
3637
3638        /** Makes a filename safe to insert in a RegExp */
3639        private static readonly filenameEscapeRegexp = /[-\/\\^$*+?.()|[\]{}]/g;
3640        private static escapeFilenameForRegex(filename: string) {
3641            return filename.replace(this.filenameEscapeRegexp, "\\$&");
3642        }
3643
3644        resetSafeList(): void {
3645            this.safelist = defaultTypeSafeList;
3646        }
3647
3648        applySafeList(proj: protocol.ExternalProject): NormalizedPath[] {
3649            const { rootFiles } = proj;
3650            const typeAcquisition = proj.typeAcquisition!;
3651            Debug.assert(!!typeAcquisition, "proj.typeAcquisition should be set by now");
3652
3653            if (typeAcquisition.enable === false || typeAcquisition.disableFilenameBasedTypeAcquisition) {
3654                return [];
3655            }
3656
3657            const typeAcqInclude = typeAcquisition.include || (typeAcquisition.include = []);
3658            const excludeRules: string[] = [];
3659
3660            const normalizedNames = rootFiles.map(f => normalizeSlashes(f.fileName)) as NormalizedPath[];
3661            const excludedFiles: NormalizedPath[] = [];
3662
3663            for (const name of Object.keys(this.safelist)) {
3664                const rule = this.safelist[name];
3665                for (const root of normalizedNames) {
3666                    if (rule.match.test(root)) {
3667                        this.logger.info(`Excluding files based on rule ${name} matching file '${root}'`);
3668
3669                        // If the file matches, collect its types packages and exclude rules
3670                        if (rule.types) {
3671                            for (const type of rule.types) {
3672                                // Best-effort de-duping here - doesn't need to be unduplicated but
3673                                // we don't want the list to become a 400-element array of just 'kendo'
3674                                if (typeAcqInclude.indexOf(type) < 0) {
3675                                    typeAcqInclude.push(type);
3676                                }
3677                            }
3678                        }
3679
3680                        if (rule.exclude) {
3681                            for (const exclude of rule.exclude) {
3682                                const processedRule = root.replace(rule.match, (...groups: string[]) => {
3683                                    return exclude.map(groupNumberOrString => {
3684                                        // RegExp group numbers are 1-based, but the first element in groups
3685                                        // is actually the original string, so it all works out in the end.
3686                                        if (typeof groupNumberOrString === "number") {
3687                                            if (!isString(groups[groupNumberOrString])) {
3688                                                // Specification was wrong - exclude nothing!
3689                                                this.logger.info(`Incorrect RegExp specification in safelist rule ${name} - not enough groups`);
3690                                                // * can't appear in a filename; escape it because it's feeding into a RegExp
3691                                                return "\\*";
3692                                            }
3693                                            return ProjectService.escapeFilenameForRegex(groups[groupNumberOrString]);
3694                                        }
3695                                        return groupNumberOrString;
3696                                    }).join("");
3697                                });
3698
3699                                if (excludeRules.indexOf(processedRule) === -1) {
3700                                    excludeRules.push(processedRule);
3701                                }
3702                            }
3703                        }
3704                        else {
3705                            // If not rules listed, add the default rule to exclude the matched file
3706                            const escaped = ProjectService.escapeFilenameForRegex(root);
3707                            if (excludeRules.indexOf(escaped) < 0) {
3708                                excludeRules.push(escaped);
3709                            }
3710                        }
3711                    }
3712                }
3713            }
3714
3715            const excludeRegexes = excludeRules.map(e => new RegExp(e, "i"));
3716            const filesToKeep: protocol.ExternalFile[] = [];
3717            for (let i = 0; i < proj.rootFiles.length; i++) {
3718                if (excludeRegexes.some(re => re.test(normalizedNames[i]))) {
3719                    excludedFiles.push(normalizedNames[i]);
3720                }
3721                else {
3722                    let exclude = false;
3723                    if (typeAcquisition.enable || typeAcquisition.enableAutoDiscovery) {
3724                        const baseName = getBaseFileName(toFileNameLowerCase(normalizedNames[i]));
3725                        if (fileExtensionIs(baseName, "js")) {
3726                            const inferredTypingName = removeFileExtension(baseName);
3727                            const cleanedTypingName = removeMinAndVersionNumbers(inferredTypingName);
3728                            const typeName = this.legacySafelist.get(cleanedTypingName);
3729                            if (typeName !== undefined) {
3730                                this.logger.info(`Excluded '${normalizedNames[i]}' because it matched ${cleanedTypingName} from the legacy safelist`);
3731                                excludedFiles.push(normalizedNames[i]);
3732                                // *exclude* it from the project...
3733                                exclude = true;
3734                                // ... but *include* it in the list of types to acquire
3735                                // Same best-effort dedupe as above
3736                                if (typeAcqInclude.indexOf(typeName) < 0) {
3737                                    typeAcqInclude.push(typeName);
3738                                }
3739                            }
3740                        }
3741                    }
3742                    if (!exclude) {
3743                        // Exclude any minified files that get this far
3744                        if (/^.+[\.-]min\.js$/.test(normalizedNames[i])) {
3745                            excludedFiles.push(normalizedNames[i]);
3746                        }
3747                        else {
3748                            filesToKeep.push(proj.rootFiles[i]);
3749                        }
3750                    }
3751                }
3752            }
3753            proj.rootFiles = filesToKeep;
3754            return excludedFiles;
3755        }
3756
3757        openExternalProject(proj: protocol.ExternalProject): void {
3758            // typingOptions has been deprecated and is only supported for backward compatibility
3759            // purposes. It should be removed in future releases - use typeAcquisition instead.
3760            if (proj.typingOptions && !proj.typeAcquisition) {
3761                const typeAcquisition = convertEnableAutoDiscoveryToEnable(proj.typingOptions);
3762                proj.typeAcquisition = typeAcquisition;
3763            }
3764            proj.typeAcquisition = proj.typeAcquisition || {};
3765            proj.typeAcquisition.include = proj.typeAcquisition.include || [];
3766            proj.typeAcquisition.exclude = proj.typeAcquisition.exclude || [];
3767            if (proj.typeAcquisition.enable === undefined) {
3768                proj.typeAcquisition.enable = hasNoTypeScriptSource(proj.rootFiles.map(f => f.fileName));
3769            }
3770
3771            const excludedFiles = this.applySafeList(proj);
3772
3773            let tsConfigFiles: NormalizedPath[] | undefined;
3774            const rootFiles: protocol.ExternalFile[] = [];
3775            for (const file of proj.rootFiles) {
3776                const normalized = toNormalizedPath(file.fileName);
3777                if (getBaseConfigFileName(normalized)) {
3778                    if (this.serverMode === LanguageServiceMode.Semantic && this.host.fileExists(normalized)) {
3779                        (tsConfigFiles || (tsConfigFiles = [])).push(normalized);
3780                    }
3781                }
3782                else {
3783                    rootFiles.push(file);
3784                }
3785            }
3786
3787            // sort config files to simplify comparison later
3788            if (tsConfigFiles) {
3789                tsConfigFiles.sort();
3790            }
3791
3792            const externalProject = this.findExternalProjectByProjectName(proj.projectFileName);
3793            let exisingConfigFiles: string[] | undefined;
3794            if (externalProject) {
3795                externalProject.excludedFiles = excludedFiles;
3796                if (!tsConfigFiles) {
3797                    const compilerOptions = convertCompilerOptions(proj.options);
3798                    const watchOptionsAndErrors = convertWatchOptions(proj.options, externalProject.getCurrentDirectory());
3799                    const lastFileExceededProgramSize = this.getFilenameForExceededTotalSizeLimitForNonTsFiles(proj.projectFileName, compilerOptions, proj.rootFiles, externalFilePropertyReader);
3800                    if (lastFileExceededProgramSize) {
3801                        externalProject.disableLanguageService(lastFileExceededProgramSize);
3802                    }
3803                    else {
3804                        externalProject.enableLanguageService();
3805                    }
3806                    externalProject.setProjectErrors(watchOptionsAndErrors?.errors);
3807                    // external project already exists and not config files were added - update the project and return;
3808                    // The graph update here isnt postponed since any file open operation needs all updated external projects
3809                    this.updateRootAndOptionsOfNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave, watchOptionsAndErrors?.watchOptions);
3810                    externalProject.updateGraph();
3811                    return;
3812                }
3813                // some config files were added to external project (that previously were not there)
3814                // close existing project and later we'll open a set of configured projects for these files
3815                this.closeExternalProject(proj.projectFileName);
3816            }
3817            else if (this.externalProjectToConfiguredProjectMap.get(proj.projectFileName)) {
3818                // this project used to include config files
3819                if (!tsConfigFiles) {
3820                    // config files were removed from the project - close existing external project which in turn will close configured projects
3821                    this.closeExternalProject(proj.projectFileName);
3822                }
3823                else {
3824                    // project previously had some config files - compare them with new set of files and close all configured projects that correspond to unused files
3825                    const oldConfigFiles = this.externalProjectToConfiguredProjectMap.get(proj.projectFileName)!;
3826                    let iNew = 0;
3827                    let iOld = 0;
3828                    while (iNew < tsConfigFiles.length && iOld < oldConfigFiles.length) {
3829                        const newConfig = tsConfigFiles[iNew];
3830                        const oldConfig = oldConfigFiles[iOld];
3831                        if (oldConfig < newConfig) {
3832                            this.closeConfiguredProjectReferencedFromExternalProject(oldConfig);
3833                            iOld++;
3834                        }
3835                        else if (oldConfig > newConfig) {
3836                            iNew++;
3837                        }
3838                        else {
3839                            // record existing config files so avoid extra add-refs
3840                            (exisingConfigFiles || (exisingConfigFiles = [])).push(oldConfig);
3841                            iOld++;
3842                            iNew++;
3843                        }
3844                    }
3845                    for (let i = iOld; i < oldConfigFiles.length; i++) {
3846                        // projects for all remaining old config files should be closed
3847                        this.closeConfiguredProjectReferencedFromExternalProject(oldConfigFiles[i]);
3848                    }
3849                }
3850            }
3851            if (tsConfigFiles) {
3852                // store the list of tsconfig files that belong to the external project
3853                this.externalProjectToConfiguredProjectMap.set(proj.projectFileName, tsConfigFiles);
3854                for (const tsconfigFile of tsConfigFiles) {
3855                    let project = this.findConfiguredProjectByProjectName(tsconfigFile);
3856                    if (!project) {
3857                        // errors are stored in the project, do not need to update the graph
3858                        project = this.getHostPreferences().lazyConfiguredProjectsFromExternalProject ?
3859                            this.createConfiguredProjectWithDelayLoad(tsconfigFile, `Creating configured project in external project: ${proj.projectFileName}`) :
3860                            this.createLoadAndUpdateConfiguredProject(tsconfigFile, `Creating configured project in external project: ${proj.projectFileName}`);
3861                    }
3862                    if (project && !contains(exisingConfigFiles, tsconfigFile)) {
3863                        // keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project
3864                        project.addExternalProjectReference();
3865                    }
3866                }
3867            }
3868            else {
3869                // no config files - remove the item from the collection
3870                // Create external project and update its graph, do not delay update since
3871                // any file open operation needs all updated external projects
3872                this.externalProjectToConfiguredProjectMap.delete(proj.projectFileName);
3873                const project = this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition, excludedFiles);
3874                project.updateGraph();
3875            }
3876        }
3877
3878        hasDeferredExtension() {
3879            for (const extension of this.hostConfiguration.extraFileExtensions!) { // TODO: GH#18217
3880                if (extension.scriptKind === ScriptKind.Deferred) {
3881                    return true;
3882                }
3883            }
3884
3885            return false;
3886        }
3887
3888        configurePlugin(args: protocol.ConfigurePluginRequestArguments) {
3889            // For any projects that already have the plugin loaded, configure the plugin
3890            this.forEachEnabledProject(project => project.onPluginConfigurationChanged(args.pluginName, args.configuration));
3891
3892            // Also save the current configuration to pass on to any projects that are yet to be loaded.
3893            // If a plugin is configured twice, only the latest configuration will be remembered.
3894            this.currentPluginConfigOverrides = this.currentPluginConfigOverrides || new Map();
3895            this.currentPluginConfigOverrides.set(args.pluginName, args.configuration);
3896        }
3897
3898        /*@internal*/
3899        getPackageJsonsVisibleToFile(fileName: string, rootDir?: string): readonly PackageJsonInfo[] {
3900            const packageJsonCache = this.packageJsonCache;
3901            const rootPath = rootDir && this.toPath(rootDir);
3902            const filePath = this.toPath(fileName);
3903            const result: PackageJsonInfo[] = [];
3904            const processDirectory = (directory: Path): boolean | undefined => {
3905                switch (packageJsonCache.directoryHasPackageJson(directory)) {
3906                    // Sync and check same directory again
3907                    case Ternary.Maybe:
3908                        packageJsonCache.searchDirectoryAndAncestors(directory);
3909                        return processDirectory(directory);
3910                    // Check package.json
3911                    case Ternary.True:
3912                        const packageJsonFileName = combinePaths(directory, "package.json");
3913                        this.watchPackageJsonFile(packageJsonFileName as Path);
3914                        const info = packageJsonCache.getInDirectory(directory);
3915                        if (info) result.push(info);
3916                }
3917                if (rootPath && rootPath === directory) {
3918                    return true;
3919                }
3920            };
3921
3922            forEachAncestorDirectory(getDirectoryPath(filePath), processDirectory);
3923            return result;
3924        }
3925
3926        /*@internal*/
3927        getNearestAncestorDirectoryWithPackageJson(fileName: string): string | undefined {
3928            return forEachAncestorDirectory(fileName, directory => {
3929                switch (this.packageJsonCache.directoryHasPackageJson(this.toPath(directory))) {
3930                    case Ternary.True: return directory;
3931                    case Ternary.False: return undefined;
3932                    case Ternary.Maybe:
3933                        return this.host.fileExists(combinePaths(directory, "package.json"))
3934                            ? directory
3935                            : undefined;
3936                }
3937            });
3938        }
3939
3940        /*@internal*/
3941        private watchPackageJsonFile(path: Path) {
3942            const watchers = this.packageJsonFilesMap || (this.packageJsonFilesMap = new Map());
3943            if (!watchers.has(path)) {
3944                this.invalidateProjectAutoImports(path);
3945                watchers.set(path, this.watchFactory.watchFile(
3946                    path,
3947                    (fileName, eventKind) => {
3948                        const path = this.toPath(fileName);
3949                        switch (eventKind) {
3950                            case FileWatcherEventKind.Created:
3951                                return Debug.fail();
3952                            case FileWatcherEventKind.Changed:
3953                                this.packageJsonCache.addOrUpdate(path);
3954                                this.invalidateProjectAutoImports(path);
3955                                break;
3956                            case FileWatcherEventKind.Deleted:
3957                                this.packageJsonCache.delete(path);
3958                                this.invalidateProjectAutoImports(path);
3959                                watchers.get(path)!.close();
3960                                watchers.delete(path);
3961                        }
3962                    },
3963                    PollingInterval.Low,
3964                    this.hostConfiguration.watchOptions,
3965                    WatchType.PackageJsonFile,
3966                ));
3967            }
3968        }
3969
3970        /*@internal*/
3971        private onAddPackageJson(path: Path) {
3972            this.packageJsonCache.addOrUpdate(path);
3973            this.watchPackageJsonFile(path);
3974        }
3975
3976        /*@internal*/
3977        includePackageJsonAutoImports(): PackageJsonAutoImportPreference {
3978            switch (this.hostConfiguration.preferences.includePackageJsonAutoImports) {
3979                case "on": return PackageJsonAutoImportPreference.On;
3980                case "off": return PackageJsonAutoImportPreference.Off;
3981                default: return PackageJsonAutoImportPreference.Auto;
3982            }
3983        }
3984
3985        /*@internal*/
3986        private invalidateProjectAutoImports(packageJsonPath: Path | undefined) {
3987            if (this.includePackageJsonAutoImports()) {
3988                this.configuredProjects.forEach(invalidate);
3989                this.inferredProjects.forEach(invalidate);
3990                this.externalProjects.forEach(invalidate);
3991            }
3992            function invalidate(project: Project) {
3993                if (!packageJsonPath || project.packageJsonsForAutoImport?.has(packageJsonPath)) {
3994                    project.markAutoImportProviderAsDirty();
3995                }
3996            }
3997        }
3998    }
3999
4000    /* @internal */
4001    export type ScriptInfoOrConfig = ScriptInfo | TsConfigSourceFile;
4002    /* @internal */
4003    export function isConfigFile(config: ScriptInfoOrConfig): config is TsConfigSourceFile {
4004        return (config as TsConfigSourceFile).kind !== undefined;
4005    }
4006
4007    function printProjectWithoutFileNames(project: Project) {
4008        project.print(/*writeProjectFileNames*/ false);
4009    }
4010}
4011