diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index df406d10a70..51f681dd441 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -27,7 +27,7 @@ namespace ts { content: libFileContent }; - abstract class TestTypingsInstaller extends server.typingsInstaller.TypingsInstaller implements server.ITypingsInstaller { + class TestTypingsInstaller extends server.typingsInstaller.TypingsInstaller implements server.ITypingsInstaller { protected projectService: server.ProjectService; constructor(readonly cachePath: string, readonly installTypingHost: server.ServerHost) { super(cachePath, ""); @@ -44,6 +44,10 @@ namespace ts { this.postInstallActions = []; } + onProjectClosed(p: server.Project) { + + } + attach(projectService: server.ProjectService) { this.projectService = projectService; } @@ -66,7 +70,7 @@ namespace ts { }); } - sendResponse(response: server.InstallTypingsResponse) { + sendResponse(response: server.SetTypings | server.InvalidateCachedTypings) { this.projectService.updateTypingsForProject(response); } @@ -1515,12 +1519,7 @@ namespace ts { }; const host = createServerHost([file1, tsconfig, packageJson]); - class TypingInstaller extends TestTypingsInstaller { - constructor(host: server.ServerHost) { - super("/a/data/", host); - } - }; - const installer = new TypingInstaller(host); + const installer = new TestTypingsInstaller("/a/data/", host); const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ true, installer); projectService.openClientFile(file1.path); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 1c177437e55..00f839faad6 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -206,14 +206,21 @@ namespace ts.server { this.ensureInferredProjectsUpToDate(); } - updateTypingsForProject(response: InstallTypingsResponse): void { + updateTypingsForProject(response: SetTypings | InvalidateCachedTypings): void { const project = this.findProject(response.projectName); if (!project) { return; } - this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typingOptions, response.typings); - project.setTypings(response.typings); - project.updateGraph(); + switch (response.kind) { + case "set": + this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typingOptions, response.typings); + project.setTypings(response.typings); + project.updateGraph(); + break; + case "invalidate": + this.typingsCache.invalidateCachedTypingsForProject(project); + break; + } } setCompilerOptionsForInferredProjects(compilerOptions: CompilerOptions): void { diff --git a/src/server/server.ts b/src/server/server.ts index b69ecc50aa7..afa0b134736 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -177,6 +177,10 @@ namespace ts.server { this.installer.on("message", m => this.handleMessage(m)); } + onProjectClosed(p: Project): void { + this.installer.send({ projectName: p.getProjectName(), kind: "closeProject" }); + } + enqueueInstallTypingsRequest(project: Project, typingOptions: TypingOptions): void { const request = createInstallTypingsRequest(project, typingOptions); if (this.logger.hasLevel(LogLevel.verbose)) { @@ -185,7 +189,7 @@ namespace ts.server { this.installer.send(request); } - private handleMessage(response: InstallTypingsResponse) { + private handleMessage(response: SetTypings | InvalidateCachedTypings) { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Received response: ${JSON.stringify(response)}`); } diff --git a/src/server/types.d.ts b/src/server/types.d.ts index 05a86399588..b4694cc4017 100644 --- a/src/server/types.d.ts +++ b/src/server/types.d.ts @@ -3,15 +3,6 @@ /// declare namespace ts.server { - export interface InstallTypingsRequest { - readonly projectName: string; - readonly fileNames: string[]; - readonly projectRootPath: ts.Path; - readonly typingOptions: ts.TypingOptions; - readonly compilerOptions: ts.CompilerOptions; - readonly cachePath?: string; - } - export interface CompressedData { length: number; compressionKind: string; @@ -27,15 +18,43 @@ declare namespace ts.server { gc?(): void; } - export interface InstallTypingsResponse { + export interface TypingInstallerRequest { readonly projectName: string; + readonly kind: "discover" | "closeProject"; + } + + export interface DiscoverTypings extends TypingInstallerRequest { + readonly fileNames: string[]; + readonly projectRootPath: ts.Path; + readonly typingOptions: ts.TypingOptions; + readonly compilerOptions: ts.CompilerOptions; + readonly cachePath?: string; + readonly kind: "discover"; + } + + export interface CloseProject extends TypingInstallerRequest { + readonly kind: "closeProject"; + } + + export interface TypingInstallerResponse { + readonly projectName: string; + readonly kind: "set" | "invalidate"; + } + + export interface SetTypings extends TypingInstallerResponse { readonly typingOptions: ts.TypingOptions; readonly compilerOptions: ts.CompilerOptions; readonly typings: string[]; + readonly kind: "set"; + } + + export interface InvalidateCachedTypings extends TypingInstallerResponse { + readonly kind: "invalidate"; } export interface InstallTypingHost extends JsTyping.TypingResolutionHost { writeFile(path: string, content: string): void; createDirectory(path: string): void; + watchFile?(path: string, callback: FileWatcherCallback): FileWatcher; } } \ No newline at end of file diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index 8f5ccd5476d..4b5b2ceb9d4 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -4,11 +4,13 @@ namespace ts.server { export interface ITypingsInstaller { enqueueInstallTypingsRequest(p: Project, typingOptions: TypingOptions): void; attach(projectService: ProjectService): void; + onProjectClosed(p: Project): void; } export const nullTypingsInstaller: ITypingsInstaller = { enqueueInstallTypingsRequest: () => {}, - attach: (projectService: ProjectService) => {} + attach: (projectService: ProjectService) => {}, + onProjectClosed: (p: Project) => {} }; class TypingsCacheEntry { @@ -95,6 +97,14 @@ namespace ts.server { return entry ? entry.typings : emptyArray; } + invalidateCachedTypingsForProject(project: Project) { + const typingOptions = getTypingOptionsForProjects(project); + if (!typingOptions.enableAutoDiscovery) { + return; + } + this.installer.enqueueInstallTypingsRequest(project, typingOptions); + } + updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typingOptions: TypingOptions, newTypings: string[]) { this.perProjectCache[projectName] = { compilerOptions, @@ -103,8 +113,9 @@ namespace ts.server { }; } - deleteTypingsForProject(project: Project) { + onProjectClosed(project: Project) { delete this.perProjectCache[project.getProjectName()]; + this.installer.onProjectClosed(project); } } } \ No newline at end of file diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index 44efda6f0a6..f422b55e32d 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -71,8 +71,14 @@ namespace ts.server.typingsInstaller { this.log.writeLine(`Error when getting npm bin path: ${e}. Set bin path to ""`); } } - process.on("message", (req: InstallTypingsRequest) => { - this.install(req); + process.on("message", (req: DiscoverTypings | CloseProject) => { + switch (req.kind) { + case "discover": + this.install(req); + break; + case "closeProject": + this.closeProject(req); + } }) } @@ -96,7 +102,7 @@ namespace ts.server.typingsInstaller { } } - protected sendResponse(response: InstallTypingsResponse) { + protected sendResponse(response: SetTypings | InvalidateCachedTypings) { if (this.log.isEnabled()) { this.log.writeLine(`Sending response: ${JSON.stringify(response)}`) } diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index 911b5d965b0..87e5c8fb7dc 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -40,6 +40,8 @@ namespace ts.server.typingsInstaller { private missingTypingsSet: Map = createMap(); private knownCachesSet: Map = createMap(); + private projectWatchers: Map = createMap(); + abstract readonly installTypingHost: InstallTypingHost; constructor(readonly globalCachePath: string, readonly safeListPath: Path, protected readonly log = nullLog) { @@ -66,7 +68,36 @@ namespace ts.server.typingsInstaller { this.processCacheLocation(this.globalCachePath); } - install(req: InstallTypingsRequest) { + closeProject(req: CloseProject) { + this.closeWatchers(req.projectName); + } + + private closeWatchers(projectName: string): boolean { + if (this.log.isEnabled()) { + this.log.writeLine(`Closing file watchers for project '${projectName}'`); + } + const watchers = this.projectWatchers[projectName]; + if (!watchers) { + if (this.log.isEnabled()) { + this.log.writeLine(`No watchers are registered for project '${projectName}'`); + } + + return false; + } + for (const w of watchers) { + w.close(); + } + + delete this.projectWatchers[projectName] + + if (this.log.isEnabled()) { + this.log.writeLine(`Closing file watchers for project '${projectName}' - done.`); + } + + return true; + } + + install(req: DiscoverTypings) { if (!this.isTsdInstalled) { if (this.log.isEnabled()) { this.log.writeLine(`tsd is not installed, ignoring request...`); @@ -100,10 +131,10 @@ namespace ts.server.typingsInstaller { } // respond with whatever cached typings we have now - this.sendResponse(this.createResponse(req, discoverTypingsResult.cachedTypingPaths)); + this.sendResponse(this.createSetTypings(req, discoverTypingsResult.cachedTypingPaths)); // start watching files - this.watchFiles(discoverTypingsResult.filesToWatch); + this.watchFiles(req.projectRootPath, discoverTypingsResult.filesToWatch); // install typings this.installTypings(req, req.cachePath || this.globalCachePath, discoverTypingsResult.cachedTypingPaths, discoverTypingsResult.newTypingNames); @@ -158,7 +189,7 @@ namespace ts.server.typingsInstaller { this.knownCachesSet[cacheLocation] = true; } - private installTypings(req: InstallTypingsRequest, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) { + private installTypings(req: DiscoverTypings, cachePath: string, currentlyCachedTypings: string[], typingsToInstall: string[]) { if (this.log.isEnabled()) { this.log.writeLine(`Installing typings ${JSON.stringify(typingsToInstall)}`); } @@ -210,7 +241,7 @@ namespace ts.server.typingsInstaller { } } - this.sendResponse(this.createResponse(req, currentlyCachedTypings.concat(installedTypingFiles))); + this.sendResponse(this.createSetTypings(req, currentlyCachedTypings.concat(installedTypingFiles))); }); } @@ -224,22 +255,38 @@ namespace ts.server.typingsInstaller { } } - private watchFiles(files: string[]) { - // TODO: start watching files + private watchFiles(projectRootPath: string, files: string[]) { + if (!files.length) { + return; + } + const watchers: FileWatcher[] = []; + for (const file of files) { + const w = this.installTypingHost.watchFile(file, f => { + if (this.log.isEnabled()) { + this.log.writeLine(`FS notification for '${f}', sending 'clean' message for project '${projectRootPath}'`); + } + if (!this.closeWatchers(projectRootPath)) { + return; + } + this.sendResponse({ projectName: projectRootPath, kind: "invalidate" }) + }); + watchers.push(w); + } } - private createResponse(request: InstallTypingsRequest, typings: string[]) { + private createSetTypings(request: DiscoverTypings, typings: string[]): SetTypings { return { projectName: request.projectName, typingOptions: request.typingOptions, compilerOptions: request.compilerOptions, - typings + typings, + kind: "set" }; } protected abstract isPackageInstalled(packageName: string): boolean; protected abstract installPackage(packageName: string): boolean; - protected abstract sendResponse(response: InstallTypingsResponse): void; + protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings): void; protected abstract runTsd(cachePath: string, typingsToInstall: string[], postInstallAction: (installedTypings: string[]) => void): void; } } \ No newline at end of file diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 9fa526ea995..d702893c8f3 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -29,14 +29,15 @@ namespace ts.server { export type Types = Err | Info | Perf; } - export function createInstallTypingsRequest(project: Project, typingOptions: TypingOptions, cachePath?: string): InstallTypingsRequest { + export function createInstallTypingsRequest(project: Project, typingOptions: TypingOptions, cachePath?: string): DiscoverTypings { return { projectName: project.getProjectName(), fileNames: project.getFileNames(), compilerOptions: project.getCompilerOptions(), typingOptions, projectRootPath: (project.projectKind === ProjectKind.Inferred ? "" : getDirectoryPath(project.getProjectName())), // TODO: fixme - cachePath + cachePath, + kind: "discover" }; }