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