From 8798235c68dc6a05af6c13135db6724bb9dbc554 Mon Sep 17 00:00:00 2001 From: steveluc Date: Fri, 20 Mar 2015 21:56:20 -0700 Subject: [PATCH] Added support in the server for tsconfig.json. When a host opens a file F, the server will check whether F is configured by a tsconfig.json file. If so, the project specified by the tsconfig.json file becomes the LS context for F. --- src/server/editorServices.ts | 277 ++++++++++++++++++++++++----------- src/server/session.ts | 2 +- src/server/tsconfig.json | 17 +++ 3 files changed, 211 insertions(+), 85 deletions(-) create mode 100644 src/server/tsconfig.json diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 1f9df9b46f0..bcb9fb24f3f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -272,14 +272,29 @@ module ts.server { export class Project { compilerService: CompilerService; - projectOptions: ProjectOptions; projectFilename: string; program: ts.Program; filenameToSourceFile: ts.Map = {}; updateGraphSeq = 0; + /** Used for configured projects which may have multiple open roots */ + openRefCount = 0; - constructor(public projectService: ProjectService) { - this.compilerService = new CompilerService(this); + constructor(public projectService: ProjectService, public projectOptions?: ProjectOptions) { + if (projectOptions && projectOptions.compilerOptions) { + this.compilerService = new CompilerService(this,projectOptions.compilerOptions); + } + else { + this.compilerService = new CompilerService(this); + } + } + + addOpenRef() { + this.openRefCount++; + } + + deleteOpenRef() { + this.openRefCount--; + return this.openRefCount; } openReferencedFile(filename: string) { @@ -299,6 +314,10 @@ module ts.server { } } + isRoot(info: ScriptInfo) { + return this.compilerService.host.roots.some(root => root === info); + } + removeReferencedFile(info: ScriptInfo) { this.compilerService.host.removeReferencedFile(info); this.updateGraph(); @@ -375,11 +394,16 @@ module ts.server { export class ProjectService { filenameToScriptInfo: ts.Map = {}; - // open, non-configured files in two lists + // open, non-configured root files openFileRoots: ScriptInfo[] = []; - openFilesReferenced: ScriptInfo[] = []; - // projects covering open files + // projects built from openFileRoots inferredProjects: Project[] = []; + // projects specified by a tsconfig.json file + configuredProjects: Project[] = []; + // open files referenced by a project + openFilesReferenced: ScriptInfo[] = []; + // open files that are roots of a configured project + openFileRootsConfigured: ScriptInfo[] = []; hostConfiguration: HostConfiguration; constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) { @@ -484,44 +508,66 @@ module ts.server { this.printProjects(); } + setConfiguredProjectRoot(info: ScriptInfo) { + for (var i = 0, len = this.configuredProjects.length; i < len; i++) { + let configuredProject = this.configuredProjects[i]; + if (configuredProject.isRoot(info)) { + info.defaultProject = configuredProject; + configuredProject.addOpenRef(); + return true; + } + } + return false; + } + addOpenFile(info: ScriptInfo) { - this.findReferencingProjects(info); - if (info.defaultProject) { - this.openFilesReferenced.push(info); + if (this.setConfiguredProjectRoot(info)) { + this.openFileRootsConfigured.push(info); } else { - // create new inferred project p with the newly opened file as root - info.defaultProject = this.createInferredProject(info); - var openFileRoots: ScriptInfo[] = []; - // for each inferred project root r - for (var i = 0, len = this.openFileRoots.length; i < len; i++) { - var r = this.openFileRoots[i]; - // if r referenced by the new project - if (info.defaultProject.getSourceFile(r)) { - // remove project rooted at r - this.inferredProjects = - copyListRemovingItem(r.defaultProject, this.inferredProjects); - // put r in referenced open file list - this.openFilesReferenced.push(r); - // set default project of r to the new project - r.defaultProject = info.defaultProject; - } - else { - // otherwise, keep r as root of inferred project - openFileRoots.push(r); - } + this.findReferencingProjects(info); + if (info.defaultProject) { + this.openFilesReferenced.push(info); + } + else { + // create new inferred project p with the newly opened file as root + info.defaultProject = this.createInferredProject(info); + var openFileRoots: ScriptInfo[] = []; + // for each inferred project root r + for (var i = 0, len = this.openFileRoots.length; i < len; i++) { + var r = this.openFileRoots[i]; + // if r referenced by the new project + if (info.defaultProject.getSourceFile(r)) { + // remove project rooted at r + this.inferredProjects = + copyListRemovingItem(r.defaultProject, this.inferredProjects); + // put r in referenced open file list + this.openFilesReferenced.push(r); + // set default project of r to the new project + r.defaultProject = info.defaultProject; + } + else { + // otherwise, keep r as root of inferred project + openFileRoots.push(r); + } + } + this.openFileRoots = openFileRoots; + this.openFileRoots.push(info); } - this.openFileRoots = openFileRoots; - this.openFileRoots.push(info); } } + /** + * 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[] = []; var removedProject: Project; for (var i = 0, len = this.openFileRoots.length; i < len; i++) { // if closed file is root of project - if (info == this.openFileRoots[i]) { + if (info === this.openFileRoots[i]) { // remove that project and remember it removedProject = info.defaultProject; } @@ -530,9 +576,29 @@ module ts.server { } } this.openFileRoots = openFileRoots; + if (!removedProject) { + var openFileRootsConfigured: ScriptInfo[] = []; + + for (var i = 0, len = this.openFileRootsConfigured.length; i < len; i++) { + if (info === this.openFileRootsConfigured[i]) { + if (info.defaultProject.deleteOpenRef() === 0) { + removedProject = info.defaultProject; + } + } + else { + openFileRootsConfigured.push(this.openFileRootsConfigured[i]); + } + } + + this.openFileRootsConfigured = openFileRootsConfigured; + } if (removedProject) { - // remove project from inferred projects list - this.inferredProjects = copyListRemovingItem(removedProject, this.inferredProjects); + if (removedProject.isConfiguredProject()) { + this.configuredProjects = copyListRemovingItem(removedProject, this.configuredProjects); + } + else { + this.inferredProjects = copyListRemovingItem(removedProject, this.inferredProjects); + } var openFilesReferenced: ScriptInfo[] = []; var orphanFiles: ScriptInfo[] = []; // for all open, referenced files f @@ -564,14 +630,22 @@ module ts.server { var referencingProjects: Project[] = []; info.defaultProject = undefined; for (var i = 0, len = this.inferredProjects.length; i < len; i++) { - this.inferredProjects[i].updateGraph(); - if (this.inferredProjects[i] != excludedProject) { - if (this.inferredProjects[i].getSourceFile(info)) { - info.defaultProject = this.inferredProjects[i]; - referencingProjects.push(this.inferredProjects[i]); + var inferredProject = this.inferredProjects[i]; + inferredProject.updateGraph(); + if (inferredProject != excludedProject) { + if (inferredProject.getSourceFile(info)) { + info.defaultProject = inferredProject; + referencingProjects.push(inferredProject); } } } + for (var i = 0, len = this.configuredProjects.length; i < len; i++) { + var configuredProject = this.configuredProjects[i]; + configuredProject.updateGraph(); + if (configuredProject.getSourceFile(info)) { + info.defaultProject = configuredProject; + } + } return referencingProjects; } @@ -676,9 +750,18 @@ module ts.server { * @param filename is absolute pathname */ - openClientFile(filename: string) { - // TODO: tsconfig check - var info = this.openFile(filename, true); + openClientFile(fileName: string) { + var configFileName = this.findConfigFile(fileName); + 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.configuredProjects.push(configResult.project); + } + } + var info = this.openFile(fileName, true); this.addOpenFile(info); this.printProjects(); return info; @@ -699,19 +782,6 @@ module ts.server { this.printProjects(); } - getProjectsReferencingFile(filename: string) { - var scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); - if (scriptInfo) { - var projects: Project[] = []; - for (var i = 0, len = this.inferredProjects.length; i < len; i++) { - if (this.inferredProjects[i].getSourceFile(scriptInfo)) { - projects.push(this.inferredProjects[i]); - } - } - return projects; - } - } - getProjectForFile(filename: string) { var scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); if (scriptInfo) { @@ -724,9 +794,9 @@ module ts.server { if (scriptInfo) { this.psLogger.startGroup(); this.psLogger.info("Projects for " + filename) - var projects = this.getProjectsReferencingFile(filename); + var projects = this.findReferencingProjects(scriptInfo); for (var i = 0, len = projects.length; i < len; i++) { - this.psLogger.info("Inferred Project " + i.toString()); + this.psLogger.info("Project " + i.toString()); } this.psLogger.endGroup(); } @@ -744,18 +814,58 @@ module ts.server { this.psLogger.info(project.filesToString()); this.psLogger.info("-----------------------------------------------"); } - this.psLogger.info("Open file roots: ") + for (var i = 0, len = this.configuredProjects.length; i < len; i++) { + var 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 (var i = 0, len = this.openFileRoots.length; i < len; i++) { this.psLogger.info(this.openFileRoots[i].fileName); } - this.psLogger.info("Open files referenced: ") + this.psLogger.info("Open files referenced by inferred or configured projects: ") for (var i = 0, len = this.openFilesReferenced.length; i < len; i++) { - this.psLogger.info(this.openFilesReferenced[i].fileName); + var fileInfo = this.openFilesReferenced[i].fileName; + if (this.openFilesReferenced[i].defaultProject.isConfiguredProject()) { + fileInfo += " (configured)"; + } + this.psLogger.info(fileInfo); + } + this.psLogger.info("Open file roots of configured projects: ") + for (var i = 0, len = this.openFileRootsConfigured.length; i < len; i++) { + this.psLogger.info(this.openFileRootsConfigured[i].fileName); } this.psLogger.endGroup(); } - openConfigFile(configFilename: string): ProjectOpenResult { + configProjectIsActive(fileName: string) { + for (var i = 0, len = this.configuredProjects.length; i < len; i++) { + if (this.configuredProjects[i].projectFilename == fileName) { + return true; + } + } + return false; + } + + findConfigFile(openedFileName: string): string { + var searchPath = getDirectoryPath(openedFileName); + while (true) { + var fileName = searchPath + ts.directorySeparator + "tsconfig.json"; + if (sys.fileExists(fileName)) { + return fileName; + } + var parentPath = getDirectoryPath(searchPath); + if (parentPath === searchPath) { + break; + } + searchPath = parentPath; + } + return undefined; + } + + openConfigFile(configFilename: string, clientFileName?: string): ProjectOpenResult { configFilename = ts.normalizePath(configFilename); // file references will be relative to dirPath (or absolute) var dirPath = ts.getDirectoryPath(configFilename); @@ -764,33 +874,27 @@ module ts.server { return { errorMsg: "tsconfig syntax error" }; } else { - // REVIEW: specify no base path so can get absolute path below - var parsedCommandLine = ts.parseConfigFile(rawConfig); - - if (parsedCommandLine.errors) { - // TODO: gather diagnostics and transmit + var parsedCommandLine = ts.parseConfigFile(rawConfig, dirPath); + if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { return { errorMsg: "tsconfig option errors" }; } else if (parsedCommandLine.fileNames) { - var proj = this.createProject(configFilename); + 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]; - var normRootFilename = ts.normalizePath(rootFilename); - normRootFilename = getAbsolutePath(normRootFilename, dirPath); - if (this.host.fileExists(normRootFilename)) { - // TODO: pass true for file exiplicitly opened - var info = this.openFile(normRootFilename, false); + if (ts.sys.fileExists(rootFilename)) { + var info = this.openFile(rootFilename, clientFileName == rootFilename); proj.addRoot(info); } else { return { errorMsg: "specified file " + rootFilename + " not found" }; } } - var projectOptions: ProjectOptions = { - files: parsedCommandLine.fileNames, - compilerOptions: parsedCommandLine.options - }; - proj.setProjectOptions(projectOptions); + proj.finishGraph(); return { success: true, project: proj }; } else { @@ -799,8 +903,8 @@ module ts.server { } } - createProject(projectFilename: string) { - var eproj = new Project(this); + createProject(projectFilename: string, projectOptions?: ProjectOptions) { + var eproj = new Project(this, projectOptions); eproj.projectFilename = projectFilename; return eproj; } @@ -811,20 +915,25 @@ module ts.server { host: LSHost; languageService: ts.LanguageService; classifier: ts.Classifier; - settings = ts.getDefaultCompilerOptions(); + settings: ts.CompilerOptions; documentRegistry = ts.createDocumentRegistry(); - constructor(public project: Project) { + constructor(public project: Project, opt?: ts.CompilerOptions) { this.host = new LSHost(project.projectService.host, project); - // override default ES6 (remove when compiler default back at ES5) - this.settings.target = ts.ScriptTarget.ES5; - this.host.setCompilationSettings(this.settings); + if (opt) { + this.setCompilerOptions(opt); + } + else { + this.setCompilerOptions(ts.getDefaultCompilerOptions()); + } this.languageService = ts.createLanguageService(this.host, this.documentRegistry); this.classifier = ts.createClassifier(); } setCompilerOptions(opt: ts.CompilerOptions) { this.settings = opt; + // override default ES6 (remove when compiler default back at ES5) + this.settings.target = ts.ScriptTarget.ES5; this.host.setCompilationSettings(opt); } diff --git a/src/server/session.ts b/src/server/session.ts index 79eefb879da..91c77a4a769 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -554,7 +554,7 @@ module ts.server { result.push(entry); } return result; - }, []); + }, []).sort((a, b) => a.name.localeCompare(b.name)); } getCompletionEntryDetails(line: number, offset: number, diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json new file mode 100644 index 00000000000..2c8538c61e3 --- /dev/null +++ b/src/server/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + "out": "../../built/local/tsserver.js", + "sourceMap": true + }, + "files": [ + "node.d.ts", + "editorServices.ts", + "protocol.d.ts", + "server.ts", + "session.ts" + ] +}