diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index fd893e9a544..b84654f38e1 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -275,6 +275,7 @@ namespace ts.server { export interface ProjectOptions { // these fields can be present in the project file + configHasFilesProperty?: boolean; files?: string[]; compilerOptions?: ts.CompilerOptions; } @@ -284,33 +285,34 @@ namespace ts.server { private readonly lsHost: LSHost; readonly languageService: LanguageService; - private program: ts.Program; constructor( readonly projectFilename: string, public readonly projectService: ProjectService, documentRegistry: ts.DocumentRegistry, - public projectOptions?: ProjectOptions) { + files: string[], + compilerOptions: CompilerOptions) { - if (projectOptions && projectOptions.files) { + if (!compilerOptions) { + compilerOptions = ts.getDefaultCompilerOptions(); + compilerOptions.allowNonTsExtensions = true; + compilerOptions.allowJs = true; + } + else if (files) { // If files are listed explicitly, allow all extensions - projectOptions.compilerOptions.allowNonTsExtensions = true; + compilerOptions.allowNonTsExtensions = true; } this.lsHost = new LSHost(this.projectService.host, this); - if (projectOptions && projectOptions.compilerOptions) { - this.lsHost.setCompilationSettings(projectOptions.compilerOptions); - } - else { - const defaultOpts = ts.getDefaultCompilerOptions(); - defaultOpts.allowNonTsExtensions = true; - defaultOpts.allowJs = true; - this.lsHost.setCompilationSettings(defaultOpts); - } + this.lsHost.setCompilationSettings(compilerOptions); this.languageService = ts.createLanguageService(this.lsHost, documentRegistry); } + getCompilerOptions() { + return this.lsHost.getCompilationSettings(); + } + getRootFiles() { return this.rootFiles.map(info => info.fileName); } @@ -387,11 +389,10 @@ namespace ts.server { return strBuilder; } - setProjectOptions(projectOptions: ProjectOptions) { - this.projectOptions = projectOptions; - if (projectOptions.compilerOptions) { - projectOptions.compilerOptions.allowNonTsExtensions = true; - this.lsHost.setCompilationSettings(projectOptions.compilerOptions); + setCompilerOptions(compilerOptions: CompilerOptions) { + if (compilerOptions) { + compilerOptions.allowNonTsExtensions = true; + this.lsHost.setCompilationSettings(compilerOptions); } } @@ -425,18 +426,40 @@ namespace ts.server { // Used to keep track of what directories are watched for this project directoriesWatchedForTsconfig: string[] = []; constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry) { - super(/*projectFilename*/ undefined, projectService, documentRegistry); + super(/*projectFilename*/ undefined, projectService, documentRegistry, /*files*/ undefined, /*compilerOptions*/ undefined); } } class ConfiguredProject extends Project { - projectFileWatcher: FileWatcher; - directoryWatcher: FileWatcher; + private projectFileWatcher: FileWatcher; + private directoryWatcher: FileWatcher; /** Used for configured projects which may have multiple open roots */ openRefCount = 0; - constructor(projectFilename: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, projectOptions: ProjectOptions) { - super(projectFilename, projectService, documentRegistry, projectOptions); + constructor(projectFilename: string, projectService: ProjectService, documentRegistry: ts.DocumentRegistry, files: string[], compilerOptions: CompilerOptions) { + super(projectFilename, projectService, documentRegistry, files, compilerOptions); + } + + watchConfigFile(callback: (project: Project) => void) { + this.projectFileWatcher = this.projectService.host.watchFile(this.projectFilename, _ => callback(this)); + } + + watchConfigDirectory(callback: (project: Project, path: string) => void) { + this.projectService.log("Add recursive watcher for: " + ts.getDirectoryPath(this.projectFilename)); + this.directoryWatcher = this.projectService.host.watchDirectory( + ts.getDirectoryPath(this.projectFilename), + path => callback(this, path), + /*recursive*/ true + ); + } + + close() { + if (this.projectFileWatcher) { + this.projectFileWatcher.close(); + } + if (this.directoryWatcher) { + this.directoryWatcher.close(); + } } addOpenRef() { @@ -554,7 +577,7 @@ namespace ts.server { // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". // We don't respond to the second one. - if (fileName && !ts.isSupportedSourceFileName(fileName, project.projectOptions ? project.projectOptions.compilerOptions : undefined)) { + if (fileName && !ts.isSupportedSourceFileName(fileName, project.isConfiguredProject() && project.getCompilerOptions())) { return; } @@ -729,8 +752,7 @@ namespace ts.server { removeProject(project: Project) { this.log("remove project: " + project.getRootFiles().toString()); if (project.isConfiguredProject()) { - (project).projectFileWatcher.close(); - (project).directoryWatcher.close(); + (project).close(); this.configuredProjects = copyListRemovingItem((project), this.configuredProjects); } else { @@ -1082,6 +1104,7 @@ namespace ts.server { openOrUpdateConfiguredProjectForFile(fileName: string): { configFileName?: string, configFileErrors?: Diagnostic[] } { const searchPath = ts.normalizePath(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"); @@ -1185,8 +1208,32 @@ namespace ts.server { this.psLogger.endGroup(); } - configProjectIsActive(fileName: string) { - return this.findConfiguredProjectByConfigFile(fileName) === undefined; + loadExternalProject(externalProject: protocol.ExternalProject): Project { + let project = this.findConfiguredProjectByConfigFile(externalProject.projectFileName); + if (project) { + this.updateConfiguredProjectWorker(project, externalProject.rootFiles, externalProject.options); + } + else { + // TODO: get and handle errors + ({ project } = this.createConfiguredProject( + externalProject.projectFileName, + externalProject.rootFiles, + externalProject.options, + /*watchConfigFile*/ false, + /*watchConfigDirectory*/ false)); + this.configuredProjects.push(project); + } + return project; + } + + loadExternalProjects(externalProjects: protocol.ExternalProject[], openFiles: protocol.OpenFile[]): void { + for (const project of externalProjects) { + this.loadExternalProject(project); + } + for (const openFile of openFiles) { + this.openFile(openFile.fileName, /*openedByClient*/ true, openFile.content); + } + // TODO: return diff } findConfiguredProjectByConfigFile(configFileName: string) { @@ -1208,6 +1255,7 @@ namespace ts.server { 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); @@ -1221,7 +1269,8 @@ namespace ts.server { else { const projectOptions: ProjectOptions = { files: parsedCommandLine.fileNames, - compilerOptions: parsedCommandLine.options + compilerOptions: parsedCommandLine.options, + configHasFilesProperty }; return { succeeded: true, projectOptions }; } @@ -1229,33 +1278,86 @@ namespace ts.server { } - openConfigFile(configFilename: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { - const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFilename); + private createConfiguredProject(configFileName: string, files: string[], compilerOptions: CompilerOptions, watchConfigFile: boolean, watchConfigDirectory: boolean, clientFileName?: string) { + let errors: Diagnostic[]; + const project = new ConfiguredProject(configFileName, this, this.documentRegistry, files, compilerOptions); + for (const rootFilename of files) { + if (this.host.fileExists(rootFilename)) { + const info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename); + project.addRoot(info); + } + else { + (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename)); + } + } + project.finishGraph(); + if (watchConfigFile) { + project.watchConfigFile(project => this.watchedProjectConfigFileChanged(project)); + } + if (watchConfigDirectory) { + project.watchConfigDirectory((project, path) => this.directoryWatchedForSourceFilesChanged(project, path)); + } + return { project, errors }; + } + + openConfigFile(configFileName: string, clientFileName?: string): { success: boolean, project?: ConfiguredProject, errors?: Diagnostic[] } { + const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFileName); if (!succeeded) { return { success: false, errors }; } else { - const project = new ConfiguredProject(configFilename, this, this.documentRegistry, projectOptions); - let errors: Diagnostic[]; - for (const rootFilename of projectOptions.files) { - if (this.host.fileExists(rootFilename)) { - const info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename); - project.addRoot(info); - } - else { - (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename)); + const { project, errors } = + this.createConfiguredProject(configFileName, + projectOptions.files, + projectOptions.compilerOptions, + /*watchConfigFile*/ true, + /*watchConfigDirectory*/ !projectOptions.configHasFilesProperty, + clientFileName); + + return { success: true, project, errors }; + } + } + + updateConfiguredProjectWorker(project: Project, newFiles: string[], newOptions: CompilerOptions) { + const oldFileNames = project.getRootFiles(); + const newFileNames = ts.filter(newFiles, f => this.host.fileExists(f)); + const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0); + const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0); + + for (const fileName of fileNamesToRemove) { + const info = this.getScriptInfo(fileName); + if (info) { + project.removeRoot(info); + } + } + + for (const fileName of fileNamesToAdd) { + let info = this.getScriptInfo(fileName); + if (!info) { + info = this.openFile(fileName, /*openedByClient*/ false); + } + else { + // if the root file was opened by client, it would belong to either + // openFileRoots or openFileReferenced. + if (info.isOpen) { + if (this.openFileRoots.indexOf(info) >= 0) { + this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); + if (info.defaultProject && !info.defaultProject.isConfiguredProject()) { + this.removeProject(info.defaultProject); + } + } + if (this.openFilesReferenced.indexOf(info) >= 0) { + this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); + } + this.openFileRootsConfigured.push(info); + info.defaultProject = project; } } - project.finishGraph(); - project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project)); - this.log("Add recursive watcher for: " + ts.getDirectoryPath(configFilename)); - project.directoryWatcher = this.host.watchDirectory( - ts.getDirectoryPath(configFilename), - path => this.directoryWatchedForSourceFilesChanged(project, path), - /*recursive*/ true - ); - return { success: true, project: project, errors }; + project.addRoot(info); } + + project.setCompilerOptions(newOptions); + project.finishGraph(); } updateConfiguredProject(project: Project) { @@ -1269,45 +1371,7 @@ namespace ts.server { return errors; } else { - const oldFileNames = project.getRootFiles(); - const newFileNames = ts.filter(projectOptions.files, f => this.host.fileExists(f)); - const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0); - const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0); - - for (const fileName of fileNamesToRemove) { - const info = this.getScriptInfo(fileName); - if (info) { - project.removeRoot(info); - } - } - - for (const fileName of fileNamesToAdd) { - let info = this.getScriptInfo(fileName); - if (!info) { - info = this.openFile(fileName, /*openedByClient*/ false); - } - else { - // if the root file was opened by client, it would belong to either - // openFileRoots or openFileReferenced. - if (info.isOpen) { - if (this.openFileRoots.indexOf(info) >= 0) { - this.openFileRoots = copyListRemovingItem(info, this.openFileRoots); - if (info.defaultProject && !info.defaultProject.isConfiguredProject()) { - this.removeProject(info.defaultProject); - } - } - if (this.openFilesReferenced.indexOf(info) >= 0) { - this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); - } - this.openFileRootsConfigured.push(info); - info.defaultProject = project; - } - } - project.addRoot(info); - } - - project.setProjectOptions(projectOptions); - project.finishGraph(); + this.updateConfiguredProjectWorker(project, projectOptions.files, projectOptions.compilerOptions); } } } diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index f6a418fb578..ad2b7cd07ee 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -418,6 +418,17 @@ declare namespace ts.server.protocol { body?: RenameResponseBody; } + export interface ExternalProject { + projectFileName: string; + rootFiles: string[]; + options: CompilerOptions; + } + + export interface OpenFile { + fileName: string; + content?: string; + } + /** * Editor options */ @@ -537,6 +548,16 @@ declare namespace ts.server.protocol { arguments: OpenRequestArgs; } + type LoadExternalProjectArgs = ExternalProject; + + export interface LoadExternalProject extends Request { + arguments: LoadExternalProjectArgs; + } + + interface LoadExternalProjectResponse extends Response { + files: string[]; + } + /** * Exit request; value of command field is "exit". Ask the server process * to exit. diff --git a/src/server/session.ts b/src/server/session.ts index e8675613707..a3a2917664a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -127,6 +127,7 @@ namespace ts.server { export const ProjectInfo = "projectInfo"; export const ReloadProjects = "reloadProjects"; export const Unknown = "unknown"; + export const LoadExternalProject = "loadExternalProject"; } namespace Errors { @@ -1027,6 +1028,10 @@ namespace ts.server { } private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { + [CommandNames.LoadExternalProject]: (request: protocol.Request) => { + const project = this.projectService.loadExternalProject(request.arguments); + return { responseRequired: true, response: { files: project.getFileNames() } }; + }, [CommandNames.Exit]: () => { this.exit(); return { responseRequired: false }; diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index f0574bbf793..40c1deb7476 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -82,7 +82,7 @@ namespace ts { const projectService = new server.ProjectService(serverHost, logger); const rootScriptInfo = projectService.openFile(rootFile, /* openedByClient */true); const project = projectService.createInferredProject(rootScriptInfo); - project.setProjectOptions({ files: [rootScriptInfo.fileName], compilerOptions: { module: ts.ModuleKind.AMD } }); + project.setCompilerOptions({ module: ts.ModuleKind.AMD } ); return { project, rootScriptInfo @@ -166,10 +166,9 @@ namespace ts { // setting compiler options discards module resolution cache fileExistsCalled = false; - const opts = ts.clone(project.projectOptions); - opts.compilerOptions = ts.clone(opts.compilerOptions); - opts.compilerOptions.target = ts.ScriptTarget.ES5; - project.setProjectOptions(opts); + const compilerOptions = ts.clone(project.getCompilerOptions()); + compilerOptions.target = ts.ScriptTarget.ES5; + project.setCompilerOptions(compilerOptions); project.languageService.getSemanticDiagnostics(imported.name); assert.isTrue(fileExistsCalled);