From cefaa171eb462efc4674dcbc4595b046fe7deb30 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 24 Jun 2016 14:30:45 -0700 Subject: [PATCH] [in progress] project system work - major code reorg --- src/server/editorServices.ts | 557 +++++++++++++++++-------------- src/server/lshost.ts | 47 ++- src/server/project.ts | 71 ++-- src/server/scriptInfo.ts | 20 +- src/server/scriptVersionCache.ts | 4 +- src/server/session.ts | 9 +- src/server/utilities.ts | 8 +- 7 files changed, 386 insertions(+), 330 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f95899925e5..bc645cb9f8e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -8,7 +8,6 @@ /// namespace ts.server { - export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; /** @@ -24,10 +23,29 @@ namespace ts.server { } export interface HostConfiguration { - formatCodeOptions: ts.FormatCodeSettings; + formatCodeOptions: FormatCodeSettings; hostInfo: string; } + interface ConfigFileConversionResult { + success: boolean; + errors?: Diagnostic[]; + + projectOptions?: ProjectOptions; + } + + interface OpenConfigFileResult { + success: boolean, + errors?: Diagnostic[] + + project?: ConfiguredProject, + } + + interface OpenConfiguredProjectResult { + configFileName?: string; + configFileErrors?: Diagnostic[]; + } + function findProjectByName(projectName: string, projects: T[]): T { for (const proj of projects) { if (proj.getProjectName() === projectName) { @@ -36,22 +54,16 @@ namespace ts.server { } } - export interface ProjectOpenResult { - success?: boolean; - errorMsg?: string; - project?: Project; - } - class DirectoryWatchers { /** * a path to directory watcher map that detects added tsconfig files **/ - private directoryWatchersForTsconfig: ts.Map = {}; + private directoryWatchersForTsconfig: Map = {}; /** * count of how many projects are using the directory watcher. * If the number becomes 0 for a watcher, then we should close it. **/ - private directoryWatchersRefCount: ts.Map = {}; + private directoryWatchersRefCount: Map = {}; constructor(private readonly projectService: ProjectService) { } @@ -60,18 +72,18 @@ namespace ts.server { // if the ref count for this directory watcher drops to 0, it's time to close it this.directoryWatchersRefCount[directory]--; if (this.directoryWatchersRefCount[directory] === 0) { - this.projectService.log("Close directory watcher for: " + directory); + this.projectService.log(`Close directory watcher for: ${directory}`); this.directoryWatchersForTsconfig[directory].close(); delete this.directoryWatchersForTsconfig[directory]; } } startWatchingContainingDirectoriesForFile(fileName: string, project: InferredProject, callback: (fileName: string) => void) { - let currentPath = ts.getDirectoryPath(fileName); - let parentPath = ts.getDirectoryPath(currentPath); + let currentPath = getDirectoryPath(fileName); + let parentPath = getDirectoryPath(currentPath); while (currentPath != parentPath) { if (!this.directoryWatchersForTsconfig[currentPath]) { - this.projectService.log("Add watcher for: " + currentPath); + this.projectService.log(`Add watcher for: ${currentPath}`); this.directoryWatchersForTsconfig[currentPath] = this.projectService.host.watchDirectory(currentPath, callback); this.directoryWatchersRefCount[currentPath] = 1; } @@ -80,30 +92,32 @@ namespace ts.server { } project.directoriesWatchedForTsconfig.push(currentPath); currentPath = parentPath; - parentPath = ts.getDirectoryPath(parentPath); + parentPath = getDirectoryPath(parentPath); } } } export class ProjectService { + private readonly documentRegistry: DocumentRegistry; + /** * Container of all known scripts */ - private filenameToScriptInfo = createNormalizedPathMap(); + private readonly filenameToScriptInfo = createNormalizedPathMap(); /** * maps external project file name to list of config files that were the part of this project */ - externalProjectToConfiguredProjectMap: Map; + private readonly externalProjectToConfiguredProjectMap: Map; /** * external projects (configuration and list of root files is not controlled by tsserver) */ - externalProjects: ExternalProject[] = []; + readonly externalProjects: ExternalProject[] = []; /** * projects built from openFileRoots **/ - inferredProjects: InferredProject[] = []; + readonly inferredProjects: InferredProject[] = []; /** * projects specified by a tsconfig.json file **/ @@ -121,22 +135,21 @@ namespace ts.server { **/ openFileRootsConfigured: ScriptInfo[] = []; - private directoryWatchers: DirectoryWatchers; + private readonly directoryWatchers: DirectoryWatchers; private hostConfiguration: HostConfiguration; + private timerForDetectingProjectFileListChanges: Map = {}; - private documentRegistry: ts.DocumentRegistry; - constructor(public readonly host: ServerHost, - public readonly psLogger: Logger, + public readonly logger: Logger, public readonly cancellationToken: HostCancellationToken, private readonly eventHandler?: ProjectServiceEventHandler) { this.directoryWatchers = new DirectoryWatchers(this); // ts.disableIncrementalParsing = true; this.setDefaultHostConfiguration(); - this.documentRegistry = ts.createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); + this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); } private setDefaultHostConfiguration() { @@ -162,7 +175,7 @@ namespace ts.server { getFormatCodeOptions(file?: NormalizedPath) { if (file) { - const info = this.filenameToScriptInfo.get(file); + const info = this.getScriptInfoForNormalizedPath(file); if (info) { return info.formatCodeSettings; } @@ -171,9 +184,9 @@ namespace ts.server { } private onSourceFileChanged(fileName: NormalizedPath) { - const info = this.filenameToScriptInfo.get(fileName); + const info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { - this.psLogger.info("Error: got watch notification for unknown file: " + fileName); + this.logger.info(`Error: got watch notification for unknown file: ${fileName}`); } if (!this.host.fileExists(fileName)) { @@ -182,13 +195,13 @@ namespace ts.server { } else { if (info && (!info.isOpen)) { - info.reloadFromFile(info.fileName); + info.reloadFromFile(); } } } private handleDeletedFile(info: ScriptInfo) { - this.psLogger.info(info.fileName + " deleted"); + this.logger.info(`${info.fileName} deleted`); info.stopWatcher(); @@ -212,6 +225,7 @@ namespace ts.server { this.printProjects(); } + /** * This is the callback function when a watched directory has added or removed source code files. * @param project the project that associates with this directory watcher @@ -225,7 +239,7 @@ namespace ts.server { return; } - this.log("Detected source file changes: " + fileName); + this.log(`Detected source file changes: ${fileName}`); const timeoutId = this.timerForDetectingProjectFileListChanges[project.configFileName]; if (timeoutId) { this.host.clearTimeout(timeoutId); @@ -237,13 +251,13 @@ namespace ts.server { } private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) { - const { projectOptions } = this.configFileToProjectOptions(project.configFileName); + const { projectOptions } = this.convertConfigFileContentToProjectOptions(project.configFileName); const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f))); const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f))); // We check if the project file list has changed. If so, we update the project. - if (!arrayIsEqualTo(currentRootFiles && currentRootFiles.sort(), newRootFiles && newRootFiles.sort())) { + if (!arrayIsEqualTo(currentRootFiles.sort(), newRootFiles.sort())) { // For configured projects, the change is made outside the tsconfig file, and // it is not likely to affect the project for other files opened by the client. We can // just update the current project. @@ -256,7 +270,7 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject) { - this.log("Config file changed: " + project.configFileName); + this.log(`Config file changed: ${project.configFileName}`); this.updateConfiguredProject(project); this.updateProjectStructure(); } @@ -264,39 +278,38 @@ namespace ts.server { /** * This is the callback function when a watched directory has an added tsconfig file. */ - private onConfigChangeForInferredProject(fileName: string) { - if (ts.getBaseFileName(fileName) != "tsconfig.json") { - this.log(fileName + " is not tsconfig.json"); + private onConfigFileAddedForInferredProject(fileName: string) { + // TODO: check directory separators + if (getBaseFileName(fileName) != "tsconfig.json") { + this.log(`${fileName} is not tsconfig.json`); return; } - this.log("Detected newly added tsconfig file: " + fileName); - - const { projectOptions } = this.configFileToProjectOptions(fileName); - + this.log(`Detected newly added tsconfig file: ${fileName}`); + const { projectOptions } = this.convertConfigFileContentToProjectOptions(fileName); const rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f)); // We should only care about the new tsconfig file if it contains any // opened root files of existing inferred projects for (const rootFile of this.openFileRoots) { if (contains(rootFilesInTsconfig, this.getCanonicalFileName(rootFile.fileName))) { this.reloadProjects(); - return; + break; } } } private getCanonicalFileName(fileName: string) { const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); - return ts.normalizePath(name); + return normalizePath(name); } + // TODO: delete if unused private releaseNonReferencedConfiguredProjects() { if (this.configuredProjects.every(p => p.openRefCount > 0)) { return; } const configuredProjects: ConfiguredProject[] = []; - for (const proj of this.configuredProjects) { if (proj.openRefCount > 0) { configuredProjects.push(proj); @@ -310,7 +323,7 @@ namespace ts.server { } private removeProject(project: Project) { - this.log("remove project: " + project.getRootFiles().toString()); + this.log(`remove project: ${project.getRootFiles().toString()}`); project.close(); @@ -345,14 +358,15 @@ namespace ts.server { return undefined; } - private addOpenFile(info: ScriptInfo) { + private addOpenFile(info: ScriptInfo): void { const externalProject = this.findContainingExternalProject(info.fileName); if (externalProject) { + // file is already included in some external project - do nothing return; } const configuredProject = this.findContainingConfiguredProject(info); if (configuredProject) { - // info.defaultProject = configuredProject; + // file is the part of configured project configuredProject.addOpenRef(); if (configuredProject.isRoot(info)) { this.openFileRootsConfigured.push(info); @@ -362,6 +376,7 @@ namespace ts.server { } return; } + // create new inferred project p with the newly opened file as root const inferredProject = this.createAndAddInferredProject(info); const openFileRoots: ScriptInfo[] = []; @@ -369,8 +384,11 @@ namespace ts.server { for (const rootFile of this.openFileRoots) { // if r referenced by the new project if (inferredProject.containsScriptInfo(rootFile)) { - // remove project rooted at r - this.removeProject(rootFile.getDefaultProject()); + // remove inferred project that was initially created for rootFile + const defaultProject = rootFile.getDefaultProject(); + Debug.assert(defaultProject.projectKind === ProjectKind.Inferred); + + this.removeProject(defaultProject); // put r in referenced open file list this.openFilesReferenced.push(rootFile); // set default project of r to the new project @@ -389,14 +407,14 @@ namespace ts.server { * Remove this file from the set of open, non-configured files. * @param info The file that has been closed or newly configured */ - private closeOpenFile(info: ScriptInfo) { + private closeOpenFile(info: ScriptInfo): void { // Closing file should trigger re-reading the file content from disk. This is // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. - info.reloadFromFile(info.fileName); + info.reloadFromFile(); - this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); - this.openFileRootsConfigured = copyListRemovingItem(info, this.openFileRootsConfigured); + removeItemFromSet(this.openFileRoots, info); + removeItemFromSet(this.openFileRootsConfigured, info); // collect all projects that should be removed let projectsToRemove: Project[]; @@ -421,6 +439,10 @@ namespace ts.server { const orphanFiles: ScriptInfo[] = []; // for all open, referenced files f for (const f of this.openFilesReferenced) { + if (f === info) { + // skip closed file + continue; + } // collect orphanted files and try to re-add them as newly opened if (f.containingProjects.length === 0) { orphanFiles.push(f); @@ -430,17 +452,20 @@ namespace ts.server { openFilesReferenced.push(f); } } + this.openFilesReferenced = openFilesReferenced; // treat orphaned files as newly opened - for (let i = 0, len = orphanFiles.length; i < len; i++) { - this.addOpenFile(orphanFiles[i]); + for (const f of orphanFiles) { + this.addOpenFile(f); } } else { - this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); + // just close file + removeItemFromSet(this.openFilesReferenced, info); } - this.releaseNonReferencedConfiguredProjects(); + // projectsToRemove should already cover it + // this.releaseNonReferencedConfiguredProjects(); info.isOpen = false; } @@ -450,36 +475,38 @@ namespace ts.server { * we first detect if there is already a configured project created for it: if so, we re-read * the tsconfig file content and update the project; otherwise we create a new one. */ - private openOrUpdateConfiguredProjectForFile(fileName: string): { configFileName?: string, configFileErrors?: Diagnostic[] } { - const searchPath = asNormalizedPath(getDirectoryPath(toNormalizedPath(fileName))); - this.log("Search path: " + searchPath, "Info"); + private openOrUpdateConfiguredProjectForFile(fileName: NormalizedPath): OpenConfiguredProjectResult { + const searchPath = getDirectoryPath(fileName); + this.log(`Search path: ${searchPath}`, "Info"); + // check if this file is already included in one of external projects - const configFileName = this.findConfigFile(searchPath); - if (configFileName) { - this.log("Config file name: " + configFileName, "Info"); - const project = this.findConfiguredProjectByProjectName(configFileName); - if (!project) { - const { success, errors } = this.openConfigFile(configFileName, fileName); - if (!success) { - return { configFileName, configFileErrors: errors }; - } - else { - // even if opening config file was successful, it could still - // contain errors that were tolerated. - this.log("Opened configuration file " + configFileName, "Info"); - if (errors && errors.length > 0) { - return { configFileName, configFileErrors: errors }; - } - } + const configFileName = this.findConfigFile(asNormalizedPath(searchPath)); + if (!configFileName) { + this.log("No config files found."); + return {}; + } + + this.log(`Config file name: ${configFileName}`, "Info"); + + const project = this.findConfiguredProjectByProjectName(configFileName); + if (!project) { + const { success, errors } = this.openConfigFile(configFileName, fileName); + if (!success) { + return { configFileName, configFileErrors: errors }; } - else { - this.updateConfiguredProject(project); + + // even if opening config file was successful, it could still + // contain errors that were tolerated. + this.log(`Opened configuration file ${configFileName}`, "Info"); + if (errors && errors.length > 0) { + return { configFileName, configFileErrors: errors }; } } else { - this.log("No config files found."); + this.updateConfiguredProject(project); } - return configFileName ? { configFileName } : {}; + + return { configFileName }; } // This is different from the method the compiler uses because @@ -509,38 +536,42 @@ namespace ts.server { } private printProjects() { - if (!this.psLogger.isVerbose()) { + if (!this.logger.isVerbose()) { return; } - this.psLogger.startGroup(); - for (let i = 0, len = this.inferredProjects.length; i < len; i++) { - const project = this.inferredProjects[i]; - project.updateGraph(); - this.psLogger.info("Project " + i.toString()); - this.psLogger.info(project.filesToString()); - this.psLogger.info("-----------------------------------------------"); + this.logger.startGroup(); + + let counter = 0; + counter = printProjects(this.externalProjects, counter); + counter = printProjects(this.configuredProjects, counter); + counter = printProjects(this.inferredProjects, counter); + + this.logger.info("Open file roots of inferred projects: "); + for (const rootFile of this.openFileRoots) { + this.logger.info(rootFile.fileName); } - for (let i = 0, len = this.configuredProjects.length; i < len; i++) { - const project = this.configuredProjects[i]; - project.updateGraph(); - this.psLogger.info("Project (configured) " + (i + this.inferredProjects.length).toString()); - this.psLogger.info(project.filesToString()); - this.psLogger.info("-----------------------------------------------"); - } - this.psLogger.info("Open file roots of inferred projects: "); - for (let i = 0, len = this.openFileRoots.length; i < len; i++) { - this.psLogger.info(this.openFileRoots[i].fileName); - } - this.psLogger.info("Open files referenced by inferred or configured projects: "); + this.logger.info("Open files referenced by inferred or configured projects: "); for (const referencedFile of this.openFilesReferenced) { const fileInfo = `${referencedFile.fileName} ${ProjectKind[referencedFile.getDefaultProject().projectKind]}`; - this.psLogger.info(fileInfo); + this.logger.info(fileInfo); } - this.psLogger.info("Open file roots of configured projects: "); - for (let i = 0, len = this.openFileRootsConfigured.length; i < len; i++) { - this.psLogger.info(this.openFileRootsConfigured[i].fileName); + this.logger.info("Open file roots of configured projects: "); + for (const configuredRoot of this.openFileRootsConfigured) { + this.logger.info(configuredRoot.fileName); + } + + this.logger.endGroup(); + + function printProjects(projects: Project[], counter: number) { + for (const project of projects) { + project.updateGraph(); + this.psLogger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`); + this.psLogger.info(project.filesToString()); + this.psLogger.info("-----------------------------------------------"); + counter++; + } + return counter; } - this.psLogger.endGroup(); } private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { @@ -551,48 +582,46 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } { - configFilename = ts.normalizePath(configFilename); - // file references will be relative to dirPath (or absolute) - const dirPath = ts.getDirectoryPath(configFilename); - const contents = this.host.readFile(configFilename); - const rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileTextToJson(configFilename, contents); - if (rawConfig.error) { - return { succeeded: false, errors: [rawConfig.error] }; - } - else { - const configHasFilesProperty = rawConfig.config["files"] !== undefined; - const parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath, /*existingOptions*/ {}, configFilename); - Debug.assert(!!parsedCommandLine.fileNames); + private convertConfigFileContentToProjectOptions(configFilename: string): ConfigFileConversionResult { + configFilename = normalizePath(configFilename); - if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { - return { succeeded: false, errors: parsedCommandLine.errors }; - } - else if (parsedCommandLine.fileNames.length === 0) { - const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename); - return { succeeded: false, errors: [error] }; - } - else { - const projectOptions: ProjectOptions = { - files: parsedCommandLine.fileNames, - compilerOptions: parsedCommandLine.options, - configHasFilesProperty, - wildcardDirectories: parsedCommandLine.wildcardDirectories, - }; - return { succeeded: true, projectOptions }; - } + const configObj = parseConfigFileTextToJson(configFilename, this.host.readFile(configFilename)); + if (configObj.error) { + return { success: false, errors: [configObj.error] }; } + + const parsedCommandLine = parseJsonConfigFileContent( + configObj.config, + this.host, + getDirectoryPath(configFilename), + /*existingOptions*/ {}, + configFilename); + + Debug.assert(!!parsedCommandLine.fileNames); + + if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { + return { success: false, errors: parsedCommandLine.errors }; + } + + if (parsedCommandLine.fileNames.length === 0) { + const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename); + return { success: false, errors: [error] }; + } + + const projectOptions: ProjectOptions = { + files: parsedCommandLine.fileNames, + compilerOptions: parsedCommandLine.options, + configHasFilesProperty: configObj.config["files"] !== undefined, + wildcardDirectories: parsedCommandLine.wildcardDirectories, + }; + return { success: true, projectOptions }; } - private exceedTotalNonTsFileSizeLimit(options: CompilerOptions, fileNames: string[]) { - if (options && options.disableSizeLimit) { + private exceededTotalSizeLimitForNonTsFiles(options: CompilerOptions, fileNames: string[]) { + if (options && options.disableSizeLimit || !this.host.getFileSize) { return false; } let totalNonTsFileSize = 0; - if (!this.host.getFileSize) { - return false; - } - for (const fileName of fileNames) { if (hasTypeScriptFileExtension(fileName)) { continue; @@ -605,16 +634,21 @@ namespace ts.server { return false; } - private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions, clientFileName?: string) { - const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(compilerOptions, files); - const project = new ExternalProject(projectFileName, this, this.documentRegistry, compilerOptions, !sizeLimitExceeded); - const errors = this.addFilesToProject(project, files, clientFileName); + private createAndAddExternalProject(projectFileName: string, files: string[], compilerOptions: CompilerOptions) { + const project = new ExternalProject( + projectFileName, + this, + this.documentRegistry, + compilerOptions, + /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, files)); + + const errors = this.addFilesToProjectAndUpdateGraph(project, files, /*clientFileName*/ undefined); this.externalProjects.push(project); return { project, errors }; } private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, clientFileName?: string) { - const sizeLimitExceeded = this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files); + const sizeLimitExceeded = !this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files); const project = new ConfiguredProject( configFileName, this, @@ -622,25 +656,27 @@ namespace ts.server { projectOptions.configHasFilesProperty, projectOptions.compilerOptions, projectOptions.wildcardDirectories, - !sizeLimitExceeded); + /*languageServiceEnabled*/ !sizeLimitExceeded); + + const errors = this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, clientFileName); - const errors = this.addFilesToProject(project, projectOptions.files, clientFileName); project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project)); if (!sizeLimitExceeded) { this.watchConfigDirectoryForProject(project, projectOptions); } project.watchWildcards((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); + this.configuredProjects.push(project); return { project, errors }; } - private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions) { + private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions): void { if (!options.configHasFilesProperty) { project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); } } - private addFilesToProject(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] { + private addFilesToProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: string[], clientFileName: string): Diagnostic[] { let errors: Diagnostic[]; for (const rootFilename of files) { if (this.host.fileExists(rootFilename)) { @@ -655,22 +691,23 @@ namespace ts.server { return errors; } - private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { - const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFileName); - if (!succeeded) { - return { success: false, errors }; - } - else { - const { project, errors } = this.createAndAddConfiguredProject(configFileName, projectOptions, clientFileName); - return { success: true, project, errors }; + private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): OpenConfigFileResult { + const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName); + if (!conversionResult.success) { + return { success: false, errors: conversionResult.errors }; } + const { project, errors } = this.createAndAddConfiguredProject(configFileName, conversionResult.projectOptions, clientFileName); + return { success: true, project, errors }; } private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newRootFiles: string[], newOptions: CompilerOptions) { const oldRootFiles = project.getRootFiles(); + + // TODO: verify that newRootFiles are always normalized + // TODO: avoid N^2 const newFileNames = asNormalizedPathArray(filter(newRootFiles, f => this.host.fileExists(f))); - const fileNamesToRemove = oldRootFiles.filter(f => !contains(newFileNames, f)); - const fileNamesToAdd = newFileNames.filter(f => !contains(oldRootFiles, f)); + const fileNamesToRemove = asNormalizedPathArray(oldRootFiles.filter(f => !contains(newFileNames, f))); + const fileNamesToAdd = asNormalizedPathArray(newFileNames.filter(f => !contains(oldRootFiles, f))); for (const fileName of fileNamesToRemove) { const info = this.getScriptInfoForNormalizedPath(fileName); @@ -680,7 +717,7 @@ namespace ts.server { } for (const fileName of fileNamesToAdd) { - let info = this.getScriptInfo(fileName); + let info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); } @@ -689,7 +726,8 @@ namespace ts.server { // openFileRoots or openFileReferenced. if (info.isOpen) { if (contains(this.openFileRoots, info)) { - this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); + removeItemFromSet(this.openFileRoots, info); + // delete inferred project let toRemove: Project[]; for (const p of info.containingProjects) { @@ -722,30 +760,29 @@ namespace ts.server { if (!this.host.fileExists(project.configFileName)) { this.log("Config file deleted"); this.removeProject(project); + return; + } + + const { success, projectOptions, errors } = this.convertConfigFileContentToProjectOptions(project.configFileName); + if (!success) { + return errors; + } + + if (this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files)) { + project.setCompilerOptions(projectOptions.compilerOptions); + if (!project.languageServiceEnabled) { + // language service is already disabled + return; + } + project.disableLanguageService(); + project.stopWatchingDirectory(); } else { - const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(project.configFileName); - if (!succeeded) { - return errors; - } - else { - if (this.exceedTotalNonTsFileSizeLimit(projectOptions.compilerOptions, projectOptions.files)) { - project.setCompilerOptions(projectOptions.compilerOptions); - if (!project.languageServiceEnabled) { - // language service is already disabled - return; - } - project.disableLanguageService(); - project.stopWatchingDirectory(); - } - else { - if (!project.languageServiceEnabled) { - project.enableLanguageService(); - } - this.watchConfigDirectoryForProject(project, projectOptions); - this.updateNonInferredProject(project, projectOptions.files, projectOptions.compilerOptions); - } + if (!project.languageServiceEnabled) { + project.enableLanguageService(); } + this.watchConfigDirectoryForProject(project, projectOptions); + this.updateNonInferredProject(project, projectOptions.files, projectOptions.compilerOptions); } } @@ -756,7 +793,7 @@ namespace ts.server { this.directoryWatchers.startWatchingContainingDirectoriesForFile( root.fileName, project, - fileName => this.onConfigChangeForInferredProject(fileName)); + fileName => this.onConfigFileAddedForInferredProject(fileName)); project.updateGraph(); this.inferredProjects.push(project); @@ -764,15 +801,20 @@ namespace ts.server { } /** - * @param filename is absolute pathname + * @param uncheckedFileName is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - getOrCreateScriptInfo(fileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { - return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(fileName), openedByClient, fileContent, scriptKind); + getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { + return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName), openedByClient, fileContent, scriptKind); } + + getScriptInfo(uncheckedFileName: string) { + return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); + } + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { - let info = this.filenameToScriptInfo.get(fileName); + let info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { let content: string; if (this.host.fileExists(fileName)) { @@ -803,22 +845,26 @@ namespace ts.server { return info; } - log(msg: string, type = "Err") { - this.psLogger.msg(msg, type); + getScriptInfoForNormalizedPath(fileName: NormalizedPath) { + return this.filenameToScriptInfo.get(fileName); } - setHostConfiguration(args: ts.server.protocol.ConfigureRequestArguments) { + log(msg: string, type = "Err") { + this.logger.msg(msg, type); + } + + setHostConfiguration(args: protocol.ConfigureRequestArguments) { if (args.file) { - const info = this.filenameToScriptInfo.get(toNormalizedPath(args.file)); + const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); if (info) { info.setFormatOptions(args.formatOptions); - this.log("Host configuration update for file " + args.file, "Info"); + this.log(`Host configuration update for file ${args.file}`, "Info"); } } else { if (args.hostInfo !== undefined) { this.hostConfiguration.hostInfo = args.hostInfo; - this.log("Host information " + args.hostInfo, "Info"); + this.log(`Host information ${args.hostInfo}`, "Info"); } if (args.formatOptions) { mergeMaps(this.hostConfiguration.formatCodeOptions, args.formatOptions); @@ -828,7 +874,7 @@ namespace ts.server { } closeLog() { - this.psLogger.close(); + this.logger.close(); } /** @@ -921,27 +967,33 @@ namespace ts.server { } } else { - // - openFileRoots.push(rootFile); - } - if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { - // file was included in non-inferred project - drop old inferred project - - } - else { - openFileRoots.push(rootFile); - } - let inferredProjectsToRemove: Project[]; - for (const p of rootFile.containingProjects) { - if (p.projectKind !== ProjectKind.Inferred) { - // file was included in non-inferred project - drop old inferred project + if (rootFile.containingProjects.length === 1) { + // file contained only in one project + openFileRoots.push(rootFile); + } + else { + // TODO: fixme + // file is contained in more than one inferred project - keep only ones where it is used as reference + const roots = rootFile.containingProjects.filter(p => p.isRoot(rootFile)); + for (const root of roots) { + this.removeProject(root); + } + Debug.assert(rootFile.containingProjects.length > 0); + this.openFilesReferenced.push(rootFile); } } - // if (inInferredProjectOnly) { - // openFileRoots.push(rootFile); + // if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { + // // file was included in non-inferred project - drop old inferred project + // } // else { - + // openFileRoots.push(rootFile); + // } + // let inferredProjectsToRemove: Project[]; + // for (const p of rootFile.containingProjects) { + // if (p.projectKind !== ProjectKind.Inferred) { + // // file was included in non-inferred project - drop old inferred project + // } // } // const rootedProject = rootFile.defaultProject; @@ -979,29 +1031,19 @@ namespace ts.server { this.printProjects(); } - getScriptInfo(uncheckedFileName: string) { - return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); - } - - getScriptInfoForNormalizedPath(fileName: NormalizedPath) { - return this.filenameToScriptInfo.get(fileName); - } /** * Open file whose contents is managed by the client * @param filename is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { + openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): OpenConfiguredProjectResult { return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind); } - openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind): { configFileName?: string, configFileErrors?: Diagnostic[] } { - let configFileName: string; - let configFileErrors: Diagnostic[]; - - if (!this.findContainingExternalProject(fileName)) { - ({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName)); - } + openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind): OpenConfiguredProjectResult { + const { configFileName = undefined, configFileErrors = undefined }: OpenConfiguredProjectResult = this.findContainingExternalProject(fileName) + ? {} + : this.openOrUpdateConfiguredProjectForFile(fileName); // at this point if file is the part of some configured/external project then this project should be created const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind); @@ -1015,7 +1057,7 @@ namespace ts.server { * @param filename is absolute pathname */ closeClientFile(uncheckedFileName: string) { - const info = this.filenameToScriptInfo.get(toNormalizedPath(uncheckedFileName)); + const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); if (info) { this.closeOpenFile(info); info.isOpen = false; @@ -1024,22 +1066,22 @@ namespace ts.server { } getDefaultProjectForFile(fileName: NormalizedPath) { - const scriptInfo = this.filenameToScriptInfo.get(fileName); + const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); return scriptInfo && scriptInfo.getDefaultProject(); } - private syncExternalFilesList(knownProjects: protocol.ProjectVersionInfo[], projects: Project[], result: protocol.ProjectFiles[]): void { - for (const proj of projects) { - const knownProject = ts.forEach(knownProjects, p => p.projectName === proj.getProjectName() && p); + private collectChanges(lastKnownProjectVersions: protocol.ProjectVersionInfo[], currentProjects: Project[], result: protocol.ProjectFiles[]): void { + for (const proj of currentProjects) { + const knownProject = forEach(lastKnownProjectVersions, p => p.projectName === proj.getProjectName() && p); result.push(proj.getChangesSinceVersion(knownProject && knownProject.version)); } } synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): protocol.ProjectFiles[] { const files: protocol.ProjectFiles[] = []; - this.syncExternalFilesList(knownProjects, this.externalProjects, files); - this.syncExternalFilesList(knownProjects, this.configuredProjects, files); - this.syncExternalFilesList(knownProjects, this.inferredProjects, files); + this.collectChanges(knownProjects, this.externalProjects, files); + this.collectChanges(knownProjects, this.configuredProjects, files); + this.collectChanges(knownProjects, this.inferredProjects, files); return files; } @@ -1053,6 +1095,7 @@ namespace ts.server { for (const file of changedFiles) { const scriptInfo = this.getScriptInfo(file.fileName); Debug.assert(!!scriptInfo); + // apply changes in reverse order for (let i = file.changes.length - 1; i >= 0; i--) { const change = file.changes[i]; scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); @@ -1074,9 +1117,9 @@ namespace ts.server { const configuredProject = this.findConfiguredProjectByProjectName(configFile); if (configuredProject) { this.removeProject(configuredProject); - this.updateProjectStructure(); } } + this.updateProjectStructure(); } else { // close external project @@ -1092,33 +1135,33 @@ namespace ts.server { const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); if (externalProject) { this.updateNonInferredProject(externalProject, proj.rootFiles, proj.options); + return; } - else { - let tsConfigFiles: NormalizedPath[]; - const rootFiles: string[] = []; - for (const file of proj.rootFiles) { - if (getBaseFileName(file) === "tsconfig.json") { - (tsConfigFiles || (tsConfigFiles = [])).push(toNormalizedPath(file)); - } - else { - rootFiles.push(file); - } - } - if (tsConfigFiles) { - // store the list of tsconfig files that belong to the external project - this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles; - for (const tsconfigFile of tsConfigFiles) { - const { success, project, errors } = this.openConfigFile(tsconfigFile); - if (success) { - // keep project alive - its lifetime is bound to the lifetime of containing external project - project.addOpenRef(); - } - } + + let tsConfigFiles: NormalizedPath[]; + const rootFiles: string[] = []; + for (const file of proj.rootFiles) { + if (getBaseFileName(file) === "tsconfig.json") { + (tsConfigFiles || (tsConfigFiles = [])).push(toNormalizedPath(file)); } else { - this.createAndAddExternalProject(proj.projectFileName, proj.rootFiles, proj.options); + rootFiles.push(file); } } + if (tsConfigFiles) { + // store the list of tsconfig files that belong to the external project + this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles; + for (const tsconfigFile of tsConfigFiles) { + const { success, project, errors } = this.openConfigFile(tsconfigFile); + if (success) { + // keep project alive - its lifetime is bound to the lifetime of containing external project + project.addOpenRef(); + } + } + } + else { + this.createAndAddExternalProject(proj.projectFileName, proj.rootFiles, proj.options); + } } } } diff --git a/src/server/lshost.ts b/src/server/lshost.ts index c5b44834e2f..b65c71d5e28 100644 --- a/src/server/lshost.ts +++ b/src/server/lshost.ts @@ -72,6 +72,10 @@ namespace ts.server { return this.project.getProjectVersion(); } + getCompilationSettings() { + return this.compilationSettings; + } + getCancellationToken() { return this.cancellationToken; } @@ -90,53 +94,30 @@ namespace ts.server { } getScriptSnapshot(filename: string): ts.IScriptSnapshot { - const scriptInfo = this.project.getScriptInfo(filename); + const scriptInfo = this.project.getScriptInfoLSHost(filename); if (scriptInfo) { return scriptInfo.snap(); } } - setCompilationSettings(opt: ts.CompilerOptions) { - this.compilationSettings = opt; - // conservatively assume that changing compiler options might affect module resolution strategy - this.resolvedModuleNames.clear(); - this.resolvedTypeReferenceDirectives.clear(); - } - - getCompilationSettings() { - // change this to return active project settings for file - return this.compilationSettings; - } - getScriptFileNames() { return this.project.getRootFiles(); } getScriptKind(fileName: string) { - const info = this.project.getScriptInfo(fileName); + const info = this.project.getScriptInfoLSHost(fileName); return info && info.scriptKind; } getScriptVersion(filename: string) { - return this.project.getScriptInfo(filename).getLatestVersion(); + const info = this.project.getScriptInfoLSHost(filename); + return info && info.getLatestVersion(); } getCurrentDirectory(): string { return ""; } - removeReferencedFile(info: ScriptInfo) { - if (!info.isOpen) { - this.resolvedModuleNames.remove(info.path); - this.resolvedTypeReferenceDirectives.remove(info.path); - } - } - - removeRoot(info: ScriptInfo) { - this.resolvedModuleNames.remove(info.path); - this.resolvedTypeReferenceDirectives.remove(info.path); - } - resolvePath(path: string): string { return this.host.resolvePath(path); } @@ -156,5 +137,17 @@ namespace ts.server { getDirectories(path: string): string[] { return this.host.getDirectories(path); } + + notifyFileRemoved(info: ScriptInfo) { + this.resolvedModuleNames.remove(info.path); + this.resolvedTypeReferenceDirectives.remove(info.path); + } + + setCompilationSettings(opt: ts.CompilerOptions) { + this.compilationSettings = opt; + // conservatively assume that changing compiler options might affect module resolution strategy + this.resolvedModuleNames.clear(); + this.resolvedTypeReferenceDirectives.clear(); + } } } \ No newline at end of file diff --git a/src/server/project.ts b/src/server/project.ts index 01c7939ca82..a3be7358b30 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -4,14 +4,22 @@ /// namespace ts.server { + export enum ProjectKind { Inferred, Configured, External } + function remove(items: T[], item: T) { + const index = items.indexOf(item); + if (index >= 0) { + items.splice(index, 1); + } + } + export abstract class Project { - private rootFiles: ScriptInfo[] = []; + private readonly rootFiles: ScriptInfo[] = []; private readonly rootFilesMap: FileMap = createFileMap(); private lsHost: ServerLanguageServiceHost; private program: ts.Program; @@ -130,10 +138,8 @@ namespace ts.server { containsFile(filename: NormalizedPath, requireOpen?: boolean) { const info = this.projectService.getScriptInfoForNormalizedPath(filename); - if (info) { - if ((!requireOpen) || info.isOpen) { - return this.containsScriptInfo(info); - } + if (info && (info.isOpen || !requireOpen)) { + return this.containsScriptInfo(info); } } @@ -153,12 +159,13 @@ namespace ts.server { } removeFile(info: ScriptInfo, detachFromProject: boolean = true) { - if (!this.removeRoot(info)) { - this.removeReferencedFile(info) - } + this.removeRootFileIfNecessary(info); + this.lsHost.notifyFileRemoved(info); + if (detachFromProject) { info.detachFromProject(this); } + this.markAsDirty(); } @@ -179,14 +186,31 @@ namespace ts.server { // - newProgram is different from the old program and structure of the old program was not reused. if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) { this.projectStructureVersion++; + if (oldProgram) { + for (const f of oldProgram.getSourceFiles()) { + if (!this.program.getSourceFileByPath(f.path)) { + // new program does not contain this file - detach it from the project + const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName); + if (scriptInfoToDetach) { + scriptInfoToDetach.detachFromProject(this); + } + } + } + } } } + getScriptInfoLSHost(fileName: string) { + const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + if (scriptInfo) { + scriptInfo.attachToProject(this); + } + return scriptInfo; + } + getScriptInfoForNormalizedPath(fileName: NormalizedPath) { const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); - if (scriptInfo && scriptInfo.attachToProject(this)) { - this.markAsDirty(); - } + Debug.assert(!scriptInfo || scriptInfo.isAttached(this)); return scriptInfo; } @@ -215,19 +239,23 @@ namespace ts.server { } } - saveTo(filename: string, tmpfilename: string) { - const script = this.getScriptInfo(filename); + saveTo(filename: NormalizedPath, tmpfilename: NormalizedPath) { + const script = this.projectService.getScriptInfoForNormalizedPath(filename); if (script) { + Debug.assert(script.isAttached(this)); const snap = script.snap(); this.projectService.host.writeFile(tmpfilename, snap.getText(0, snap.getLength())); } } - reloadScript(filename: NormalizedPath, cb: () => void) { - const script = this.getScriptInfoForNormalizedPath(filename); + reloadScript(filename: NormalizedPath): boolean { + const script = this.projectService.getScriptInfoForNormalizedPath(filename); if (script) { - script.reloadFromFile(filename, cb); + Debug.assert(script.isAttached(this)); + script.reloadFromFile(); + return true; } + return false; } getChangesSinceVersion(lastKnownVersion?: number): protocol.ProjectFiles { @@ -274,18 +302,11 @@ namespace ts.server { } // remove a root file from project - private removeRoot(info: ScriptInfo): boolean { + private removeRootFileIfNecessary(info: ScriptInfo): void { if (this.isRoot(info)) { - this.rootFiles = copyListRemovingItem(info, this.rootFiles); + remove(this.rootFiles, info); this.rootFilesMap.remove(info.path); - this.lsHost.removeRoot(info); - return true; } - return false; - } - - private removeReferencedFile(info: ScriptInfo) { - this.lsHost.removeReferencedFile(info) } } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 52e1baf36b4..95023a93b74 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -3,15 +3,15 @@ namespace ts.server { export class ScriptInfo { - private svc: ScriptVersionCache; /** * All projects that include this file */ readonly containingProjects: Project[] = []; + readonly formatCodeSettings: ts.FormatCodeSettings; + readonly path: Path; private fileWatcher: FileWatcher; - formatCodeSettings: ts.FormatCodeSettings; - readonly path: Path; + private svc: ScriptVersionCache; constructor( private readonly host: ServerHost, @@ -29,11 +29,15 @@ namespace ts.server { } attachToProject(project: Project): boolean { - if (!contains(this.containingProjects, project)) { + const isNew = !this.isAttached(project); + if (isNew) { this.containingProjects.push(project); - return true; } - return false; + return isNew; + } + + isAttached(project: Project) { + return contains(this.containingProjects, project); } detachFromProject(project: Project) { @@ -80,8 +84,8 @@ namespace ts.server { this.markContainingProjectsAsDirty(); } - reloadFromFile(fileName: string, cb?: () => void) { - this.svc.reloadFromFile(fileName, cb) + reloadFromFile() { + this.svc.reloadFromFile(this.fileName); this.markContainingProjectsAsDirty(); } diff --git a/src/server/scriptVersionCache.ts b/src/server/scriptVersionCache.ts index 8269896059f..09f5905bff6 100644 --- a/src/server/scriptVersionCache.ts +++ b/src/server/scriptVersionCache.ts @@ -297,7 +297,7 @@ namespace ts.server { return this.currentVersion; } - reloadFromFile(filename: string, cb?: () => void) { + reloadFromFile(filename: string) { let content = this.host.readFile(filename); // If the file doesn't exist or cannot be read, we should // wipe out its cached content on the server to avoid side effects. @@ -305,8 +305,6 @@ namespace ts.server { content = ""; } this.reload(content); - if (cb) - cb(); } // reload whole script, leaving no change history behind reload diff --git a/src/server/session.ts b/src/server/session.ts index 29c8dcd4abd..c866ec178cc 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1018,7 +1018,7 @@ namespace ts.server { scriptInfo.editContent(start, end, args.insertString); this.changeSeq++; } - this.updateProjectStructure(this.changeSeq, (n) => n === this.changeSeq); + this.updateProjectStructure(this.changeSeq, n => n === this.changeSeq); } } @@ -1028,15 +1028,15 @@ namespace ts.server { if (project) { this.changeSeq++; // make sure no changes happen before this one is finished - project.reloadScript(file, () => { + if (project.reloadScript(file)) { this.output(undefined, CommandNames.Reload, reqSeq); - }); + } } } private saveToTmp(fileName: string, tempFileName: string) { const file = toNormalizedPath(fileName); - const tmpfile = ts.normalizePath(tempFileName); + const tmpfile = toNormalizedPath(tempFileName); const project = this.projectService.getDefaultProjectForFile(file); if (project) { @@ -1267,6 +1267,7 @@ namespace ts.server { }, [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles, request.arguments.closedFiles); + this.changeSeq++; // TODO: report errors return this.requiredResponse(true); }, diff --git a/src/server/utilities.ts b/src/server/utilities.ts index c6da27505ac..ef877362281 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -140,17 +140,13 @@ namespace ts.server { }; export interface ServerLanguageServiceHost { - getCompilationSettings(): CompilerOptions; setCompilationSettings(options: CompilerOptions): void; - removeRoot(info: ScriptInfo): void; - removeReferencedFile(info: ScriptInfo): void; + notifyFileRemoved(info: ScriptInfo): void; } export const nullLanguageServiceHost: ServerLanguageServiceHost = { - getCompilationSettings: () => undefined, setCompilationSettings: () => undefined, - removeRoot: () => undefined, - removeReferencedFile: () => undefined + notifyFileRemoved: () => undefined }; export interface ProjectOptions {