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