diff --git a/src/server/session.ts b/src/server/session.ts index aae9ab8980f..7dccda3c82c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -258,10 +258,6 @@ namespace ts.server { readonly symLinkedProjects: MultiMap; }; - function isProjectsArray(projects: Projects): projects is ReadonlyArray { - return !!(>projects).length; - } - /** * This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project. */ @@ -273,8 +269,8 @@ namespace ts.server { comparer?: (a: U, b: U) => number, areEqual?: (a: U, b: U) => boolean, ): U[] { - const outputs = flatMap(isProjectsArray(projects) ? projects : projects.projects, project => action(project, defaultValue)); - if (!isProjectsArray(projects) && projects.symLinkedProjects) { + const outputs = flatMap(isArray(projects) ? projects : projects.projects, project => action(project, defaultValue)); + if (!isArray(projects) && projects.symLinkedProjects) { projects.symLinkedProjects.forEach((projects, path) => { const value = getValue(path as Path); outputs.push(...flatMap(projects, project => action(project, value))); @@ -370,7 +366,7 @@ namespace ts.server { } function forEachProjectInProjects(projects: Projects, path: string | undefined, cb: (project: Project, path: string | undefined) => void): void { - for (const project of isProjectsArray(projects) ? projects : projects.projects) { + for (const project of isArray(projects) ? projects : projects.projects) { cb(project, path); } if (!isArray(projects) && projects.symLinkedProjects) { @@ -1106,7 +1102,7 @@ namespace ts.server { return project.getLanguageService().getRenameInfo(file, position); } - private getProjects(args: protocol.FileRequestArgs): Projects { + private getProjects(args: protocol.FileRequestArgs, getScriptInfoEnsuringProjectsUptoDate?: boolean, ignoreNoProjectError?: boolean): Projects { let projects: ReadonlyArray | undefined; let symLinkedProjects: MultiMap | undefined; if (args.projectFileName) { @@ -1116,13 +1112,18 @@ namespace ts.server { } } else { - const scriptInfo = this.projectService.getScriptInfo(args.file)!; + const scriptInfo = getScriptInfoEnsuringProjectsUptoDate ? + this.projectService.getScriptInfoEnsuringProjectsUptoDate(args.file) : + this.projectService.getScriptInfo(args.file); + if (!scriptInfo) { + return ignoreNoProjectError ? emptyArray : Errors.ThrowNoProject(); + } projects = scriptInfo.containingProjects; symLinkedProjects = this.projectService.getSymlinkedProjects(scriptInfo); } // filter handles case when 'projects' is undefined projects = filter(projects, p => p.languageServiceEnabled && !p.isOrphan()); - if ((!projects || !projects.length) && !symLinkedProjects) { + if (!ignoreNoProjectError && (!projects || !projects.length) && !symLinkedProjects) { return Errors.ThrowNoProject(); } return symLinkedProjects ? { projects: projects!, symLinkedProjects } : projects!; // TODO: GH#18217 @@ -1465,18 +1466,16 @@ namespace ts.server { } private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): ReadonlyArray { - const info = this.projectService.getScriptInfoEnsuringProjectsUptoDate(args.file); + const projects = this.getProjects(args, /*getScriptInfoEnsuringProjectsUptoDate*/ true, /*ignoreNoProjectError*/ true); + const info = this.projectService.getScriptInfo(args.file); if (!info) { return emptyArray; } - // if specified a project, we only return affected file list in this project - const projects = args.projectFileName ? [this.projectService.findProject(args.projectFileName)!] : info.containingProjects; - const symLinkedProjects = !args.projectFileName && this.projectService.getSymlinkedProjects(info); return combineProjectOutput( info, path => this.projectService.getScriptInfoForPath(path)!, - symLinkedProjects ? { projects, symLinkedProjects } : projects, + projects, (project, info) => { let result: protocol.CompileOnSaveAffectedFileListSingleProject | undefined; if (project.compileOnSaveEnabled && project.languageServiceEnabled && !project.isOrphan() && !project.getCompilationSettings().noEmit) { diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index fe6cfd25712..6d39ecb76d1 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -3495,6 +3495,102 @@ namespace ts.projectSystem { openFilesForSession([{ file, projectRootPath }], session); } }); + + describe("CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => { + const projectRoot = "/user/username/projects/myproject"; + const core: File = { + path: `${projectRoot}/core/core.ts`, + content: "let z = 10;" + }; + const app1: File = { + path: `${projectRoot}/app1/app.ts`, + content: "let x = 10;" + }; + const app2: File = { + path: `${projectRoot}/app2/app.ts`, + content: "let y = 10;" + }; + const app1Config: File = { + path: `${projectRoot}/app1/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile : "build/output.js" }, + compileOnSave: true + }) + }; + const app2Config: File = { + path: `${projectRoot}/app2/tsconfig.json`, + content: JSON.stringify({ + files: ["app.ts", "../core/core.ts"], + compilerOptions: { outFile: "build/output.js" }, + compileOnSave: true + }) + }; + const files = [libFile, core, app1, app2, app1Config, app2Config]; + + function insertString(session: TestSession, file: File) { + session.executeCommandSeq({ + command: protocol.CommandTypes.Change, + arguments: { + file: file.path, + line: 1, + offset: 1, + endLine: 1, + endOffset: 1, + insertString: "let k = 1" + } + }); + } + + function getSession() { + const host = createServerHost(files); + const session = createSession(host); + openFilesForSession([app1, app2, core], session); + const service = session.getProjectService(); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + const project1 = service.configuredProjects.get(app1Config.path)!; + const project2 = service.configuredProjects.get(app2Config.path)!; + checkProjectActualFiles(project1, [libFile.path, app1.path, core.path, app1Config.path]); + checkProjectActualFiles(project2, [libFile.path, app2.path, core.path, app2Config.path]); + insertString(session, app1); + insertString(session, app2); + assert.equal(project1.dirty, true); + assert.equal(project2.dirty, true); + return session; + } + + it("when projectFile is specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { + file: core.path, + projectFileName: app1Config.path + } + }).response; + assert.deepEqual(response, [ + { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true } + ]); + assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); + assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, true); + }); + + it("when projectFile is not specified", () => { + const session = getSession(); + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.CompileOnSaveAffectedFileList, + arguments: { + file: core.path + } + }).response; + assert.deepEqual(response, [ + { projectFileName: app1Config.path, fileNames: [core.path, app1.path], projectUsesOutFile: true }, + { projectFileName: app2Config.path, fileNames: [core.path, app2.path], projectUsesOutFile: true } + ]); + assert.equal(session.getProjectService().configuredProjects.get(app1Config.path)!.dirty, false); + assert.equal(session.getProjectService().configuredProjects.get(app2Config.path)!.dirty, false); + }); + }); }); describe("tsserverProjectSystem Proper errors", () => {