diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 14bb7712ce7..b9f76db4c9e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -224,6 +224,14 @@ namespace ts.server { this.roots.push(info); } } + + removeRoot(info: ScriptInfo) { + var scriptInfo = ts.lookUp(this.filenameToScript, info.fileName); + if (scriptInfo) { + this.filenameToScript[info.fileName] = undefined; + this.roots = copyListRemovingItem(info, this.roots); + } + } saveTo(filename: string, tmpfilename: string) { var script = this.getScriptInfo(filename); @@ -345,6 +353,7 @@ namespace ts.server { export class Project { compilerService: CompilerService; projectFilename: string; + projectFileWatcher: FileWatcher; program: ts.Program; filenameToSourceFile: ts.Map = {}; updateGraphSeq = 0; @@ -352,7 +361,7 @@ namespace ts.server { openRefCount = 0; constructor(public projectService: ProjectService, public projectOptions?: ProjectOptions) { - this.compilerService = new CompilerService(this,projectOptions && projectOptions.compilerOptions); + this.compilerService = new CompilerService(this, projectOptions && projectOptions.compilerOptions); } addOpenRef() { @@ -423,6 +432,12 @@ namespace ts.server { info.defaultProject = this; this.compilerService.host.addRoot(info); } + + // remove a root file from project + removeRoot(info: ScriptInfo) { + info.defaultProject = undefined; + this.compilerService.host.removeRoot(info); + } filesToString() { var strBuilder = ""; @@ -517,6 +532,12 @@ namespace ts.server { } } + watchedProjectConfigFileChanged(project: Project) { + this.log("Config File Changed: " + project.projectFilename); + this.updateConfiguredProject(project); + this.updateProjectStructure(); + } + log(msg: string, type = "Err") { this.psLogger.msg(msg, type); } @@ -593,6 +614,19 @@ namespace ts.server { } this.configuredProjects = configuredProjects; } + + removeConfiguredProject(project: Project) { + project.projectFileWatcher.close(); + this.configuredProjects = copyListRemovingItem(project, this.configuredProjects); + + let fileNames = project.getFileNames(); + for (let fileName of fileNames) { + let info = this.getScriptInfo(fileName); + if (info.defaultProject == project){ + info.defaultProject = undefined; + } + } + } setConfiguredProjectRoot(info: ScriptInfo) { for (var i = 0, len = this.configuredProjects.length; i < len; i++) { @@ -647,7 +681,6 @@ 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 - * @param openedByConfig True if info has become a root of a configured project */ closeOpenFile(info: ScriptInfo) { var openFileRoots: ScriptInfo[] = []; @@ -736,18 +769,42 @@ namespace ts.server { return referencingProjects; } + reloadProjects() { + // First check if there is new tsconfig file added for inferred project roots + for (let info of this.openFileRoots) { + this.openOrUpdateConfiguredProjectForFile(info.fileName); + } + this.updateProjectStructure(); + } + + /** + * This function is to update the project structure for every projects. + * It is called on the premise that all the configured projects are + * up to date. + */ updateProjectStructure() { this.log("updating project structure from ...", "Info"); this.printProjects(); + + let unattachedOpenFiles: ScriptInfo[] = []; + let openFileRootsConfigured: ScriptInfo[] = []; + for (let info of this.openFileRootsConfigured) { + let project = info.defaultProject; + if (!project || !(project.getSourceFile(info))) { + info.defaultProject = undefined; + unattachedOpenFiles.push(info); + } + else { + openFileRootsConfigured.push(info); + } + } + this.openFileRootsConfigured = openFileRootsConfigured; // First loop through all open files that are referenced by projects but are not // project roots. For each referenced file, see if the default project still // references that file. If so, then just keep the file in the referenced list. // If not, add the file to an unattached list, to be rechecked later. - var openFilesReferenced: ScriptInfo[] = []; - var unattachedOpenFiles: ScriptInfo[] = []; - for (var i = 0, len = this.openFilesReferenced.length; i < len; i++) { var referencedFile = this.openFilesReferenced[i]; referencedFile.defaultProject.updateGraph(); @@ -857,31 +914,39 @@ namespace ts.server { * Open file whose contents is managed by the client * @param filename is absolute pathname */ - openClientFile(fileName: string) { - var searchPath = ts.normalizePath(getDirectoryPath(fileName)); - this.log("Search path: " + searchPath,"Info"); - var configFileName = this.findConfigFile(searchPath); - if (configFileName) { - this.log("Config file name: " + configFileName, "Info"); - } else { - this.log("no config file"); - } - if (configFileName && (!this.configProjectIsActive(configFileName))) { - var configResult = this.openConfigFile(configFileName, fileName); - if (!configResult.success) { - this.log("Error opening config file " + configFileName + " " + configResult.errorMsg); - } - else { - this.log("Opened configuration file " + configFileName,"Info"); - this.configuredProjects.push(configResult.project); - } - } + this.openOrUpdateConfiguredProjectForFile(fileName); var info = this.openFile(fileName, true); this.addOpenFile(info); this.printProjects(); return info; } + + openOrUpdateConfiguredProjectForFile(fileName: string) { + let searchPath = ts.normalizePath(getDirectoryPath(fileName)); + this.log("Search path: " + searchPath,"Info"); + let configFileName = this.findConfigFile(searchPath); + if (configFileName) { + this.log("Config file name: " + configFileName, "Info"); + let project = this.findConfiguredProjectByConfigFile(configFileName); + if (!project) { + var configResult = this.openConfigFile(configFileName, fileName); + if (!configResult.success) { + this.log("Error opening config file " + configFileName + " " + configResult.errorMsg); + } + else { + this.log("Opened configuration file " + configFileName,"Info"); + this.configuredProjects.push(configResult.project); + } + } + else { + this.updateConfiguredProject(project); + } + } + else { + this.log("No config files found."); + } + } /** * Close file whose contents is managed by the client @@ -959,49 +1024,111 @@ namespace ts.server { } configProjectIsActive(fileName: string) { - for (var i = 0, len = this.configuredProjects.length; i < len; i++) { - if (this.configuredProjects[i].projectFilename == fileName) { - return true; - } - } - return false; + return this.findConfiguredProjectByConfigFile(fileName) === undefined; } - openConfigFile(configFilename: string, clientFileName?: string): ProjectOpenResult { + findConfiguredProjectByConfigFile(configFileName: string) { + for (var i = 0, len = this.configuredProjects.length; i < len; i++) { + if (this.configuredProjects[i].projectFilename == configFileName) { + return this.configuredProjects[i]; + } + } + return undefined; + } + + configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, error?: ProjectOpenResult } { configFilename = ts.normalizePath(configFilename); // file references will be relative to dirPath (or absolute) var dirPath = ts.getDirectoryPath(configFilename); var contents = this.host.readFile(configFilename) var rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileText(configFilename, contents); if (rawConfig.error) { - return rawConfig.error; + return { succeeded: false, error: rawConfig.error }; } else { var parsedCommandLine = ts.parseConfigFile(rawConfig.config, this.host, dirPath); if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { - return { errorMsg: "tsconfig option errors" }; + return { succeeded: false, error: { errorMsg: "tsconfig option errors" }}; } - else if (parsedCommandLine.fileNames) { + else if (parsedCommandLine.fileNames == null) { + return { succeeded: false, error: { errorMsg: "no files found" }} + } + else { var projectOptions: ProjectOptions = { files: parsedCommandLine.fileNames, compilerOptions: parsedCommandLine.options }; - var proj = this.createProject(configFilename, projectOptions); - for (var i = 0, len = parsedCommandLine.fileNames.length; i < len; i++) { - var rootFilename = parsedCommandLine.fileNames[i]; - if (this.host.fileExists(rootFilename)) { - var info = this.openFile(rootFilename, clientFileName == rootFilename); - proj.addRoot(info); - } - else { - return { errorMsg: "specified file " + rootFilename + " not found" }; - } + return { succeeded: true, projectOptions }; + } + } + + } + + openConfigFile(configFilename: string, clientFileName?: string): ProjectOpenResult { + let { succeeded, projectOptions, error } = this.configFileToProjectOptions(configFilename); + if (!succeeded) { + return error; + } + else { + let proj = this.createProject(configFilename, projectOptions); + for (let i = 0, len = projectOptions.files.length; i < len; i++) { + let rootFilename = projectOptions.files[i]; + if (this.host.fileExists(rootFilename)) { + let info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename); + proj.addRoot(info); } - proj.finishGraph(); - return { success: true, project: proj }; + else { + return { errorMsg: "specified file " + rootFilename + " not found" }; + } + } + proj.finishGraph(); + proj.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(proj)); + return { success: true, project: proj }; + } + } + + updateConfiguredProject(project: Project) { + if (!this.host.fileExists(project.projectFilename)) { + this.log("Config file deleted"); + this.removeConfiguredProject(project); + } + else { + let { succeeded, projectOptions, error } = this.configFileToProjectOptions(project.projectFilename); + if (!succeeded) { + return error; } else { - return { errorMsg: "no files found" }; + let oldFileNames = project.compilerService.host.roots.map(info => info.fileName); + let newFileNames = projectOptions.files; + let fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0); + let fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0); + + for (let fileName of fileNamesToRemove) { + let info = this.getScriptInfo(fileName); + project.removeRoot(info); + } + + for (let fileName of fileNamesToAdd) { + let info = this.getScriptInfo(fileName); + if (!info) { + info = this.openFile(fileName, false); + } + else { + // if the root file was opened by client, it would belong to either + // openFileRoots or openFileReferenced. + if (this.openFileRoots.indexOf(info) >= 0) { + this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); + } + if (this.openFilesReferenced.indexOf(info) >= 0) { + this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); + } + this.openFileRootsConfigured.push(info); + } + project.addRoot(info); + } + + project.setProjectOptions(projectOptions); + project.finishGraph(); } } } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 2d694025c84..ce918abda3c 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -31,6 +31,12 @@ declare namespace ts.server.protocol { */ arguments?: any; } + + /** + * Request to reload the project structure for all the opened files + */ + export interface ReloadProjectsRequest extends Message { + } /** * Server-initiated event message diff --git a/src/server/session.ts b/src/server/session.ts index 108cf4726dc..da044e7b4c6 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -100,6 +100,7 @@ namespace ts.server { export const SignatureHelp = "signatureHelp"; export const TypeDefinition = "typeDefinition"; export const ProjectInfo = "projectInfo"; + export const ReloadProjects = "reloadProjects"; export const Unknown = "unknown"; } @@ -226,6 +227,10 @@ namespace ts.server { this.syntacticCheck(file, project); this.semanticCheck(file, project); } + + private reloadProjects() { + this.projectService.reloadProjects(); + } private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) { setTimeout(() => { @@ -1033,6 +1038,10 @@ namespace ts.server { var { file, needFileNameList } = request.arguments; return {response: this.getProjectInfo(file, needFileNameList), responseRequired: true}; }, + [CommandNames.ReloadProjects]: (request: protocol.ReloadProjectsRequest) => { + this.reloadProjects(); + return {responseRequired: false}; + } }; public addProtocolHandler(command: string, handler: (request: protocol.Request) => {response?: any, responseRequired: boolean}) { if (this.handlers[command]) {