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