diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 92041c885b2..103590dd381 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -3124,19 +3124,24 @@ namespace ts.server { return result; } - private collectChanges(lastKnownProjectVersions: protocol.ProjectVersionInfo[], currentProjects: Project[], result: ProjectFilesWithTSDiagnostics[]): void { + private collectChanges( + lastKnownProjectVersions: protocol.ProjectVersionInfo[], + currentProjects: Project[], + includeProjectReferenceRedirectInfo: boolean | undefined, + result: ProjectFilesWithTSDiagnostics[] + ): void { for (const proj of currentProjects) { const knownProject = find(lastKnownProjectVersions, p => p.projectName === proj.getProjectName()); - result.push(proj.getChangesSinceVersion(knownProject && knownProject.version)); + result.push(proj.getChangesSinceVersion(knownProject && knownProject.version, includeProjectReferenceRedirectInfo)); } } /* @internal */ - synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): ProjectFilesWithTSDiagnostics[] { + synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[], includeProjectReferenceRedirectInfo?: boolean): ProjectFilesWithTSDiagnostics[] { const files: ProjectFilesWithTSDiagnostics[] = []; - this.collectChanges(knownProjects, this.externalProjects, files); - this.collectChanges(knownProjects, arrayFrom(this.configuredProjects.values()), files); - this.collectChanges(knownProjects, this.inferredProjects, files); + this.collectChanges(knownProjects, this.externalProjects, includeProjectReferenceRedirectInfo, files); + this.collectChanges(knownProjects, arrayFrom(this.configuredProjects.values()), includeProjectReferenceRedirectInfo, files); + this.collectChanges(knownProjects, this.inferredProjects, includeProjectReferenceRedirectInfo, files); return files; } diff --git a/src/server/project.ts b/src/server/project.ts index 08c3b7203a3..60f23a7abe0 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -167,7 +167,7 @@ namespace ts.server { /** * Set of files that was returned from the last call to getChangesSinceVersion. */ - private lastReportedFileNames: Map | undefined; + private lastReportedFileNames: Map | undefined; /** * Last version that was reported. */ @@ -803,6 +803,14 @@ namespace ts.server { return result; } + /* @internal */ + getFileNamesWithRedirectInfo(includeProjectReferenceRedirectInfo: boolean) { + return this.getFileNames().map((fileName): protocol.FileWithProjectReferenceRedirectInfo => ({ + fileName, + isSourceOfProjectReferenceRedirect: includeProjectReferenceRedirectInfo && this.isSourceOfProjectReferenceRedirect(fileName) + })); + } + hasConfigFile(configFilePath: NormalizedPath) { if (this.program && this.languageServiceEnabled) { const configFile = this.program.getCompilerOptions().configFile; @@ -1300,7 +1308,15 @@ namespace ts.server { } /* @internal */ - getChangesSinceVersion(lastKnownVersion?: number): ProjectFilesWithTSDiagnostics { + getChangesSinceVersion(lastKnownVersion?: number, includeProjectReferenceRedirectInfo?: boolean): ProjectFilesWithTSDiagnostics { + const includeProjectReferenceRedirectInfoIfRequested = + includeProjectReferenceRedirectInfo + ? (files: Map) => arrayFrom(files.entries(), ([fileName, isSourceOfProjectReferenceRedirect]): protocol.FileWithProjectReferenceRedirectInfo => ({ + fileName, + isSourceOfProjectReferenceRedirect + })) + : (files: Map) => arrayFrom(files.keys()); + // Update the graph only if initial configured project load is not pending if (!this.isInitialLoadPending()) { updateProjectIfDirty(this); @@ -1324,35 +1340,75 @@ namespace ts.server { } // compute and return the difference const lastReportedFileNames = this.lastReportedFileNames; - const externalFiles = this.getExternalFiles().map(f => toNormalizedPath(f)); - const currentFiles = arrayToSet(this.getFileNames().concat(externalFiles)); + const externalFiles = this.getExternalFiles().map((f): protocol.FileWithProjectReferenceRedirectInfo => ({ + fileName: toNormalizedPath(f), + isSourceOfProjectReferenceRedirect: false + })); + const currentFiles = arrayToMap( + this.getFileNamesWithRedirectInfo(!!includeProjectReferenceRedirectInfo).concat(externalFiles), + info => info.fileName, + info => info.isSourceOfProjectReferenceRedirect + ); + + const added: Map = new Map(); + const removed: Map = new Map(); - const added: string[] = []; - const removed: string[] = []; const updated: string[] = updatedFileNames ? arrayFrom(updatedFileNames.keys()) : []; + const updatedRedirects: protocol.FileWithProjectReferenceRedirectInfo[] = []; - forEachKey(currentFiles, id => { - if (!lastReportedFileNames.has(id)) { - added.push(id); + forEachEntry(currentFiles, (isSourceOfProjectReferenceRedirect, fileName) => { + if (!lastReportedFileNames.has(fileName)) { + added.set(fileName, isSourceOfProjectReferenceRedirect); + } + else if (includeProjectReferenceRedirectInfo && isSourceOfProjectReferenceRedirect !== lastReportedFileNames.get(fileName)){ + updatedRedirects.push({ + fileName, + isSourceOfProjectReferenceRedirect + }); } }); - forEachKey(lastReportedFileNames, id => { - if (!currentFiles.has(id)) { - removed.push(id); + forEachEntry(lastReportedFileNames, (isSourceOfProjectReferenceRedirect, fileName) => { + if (!currentFiles.has(fileName)) { + removed.set(fileName, isSourceOfProjectReferenceRedirect); } }); this.lastReportedFileNames = currentFiles; this.lastReportedVersion = this.projectProgramVersion; - return { info, changes: { added, removed, updated }, projectErrors: this.getGlobalProjectErrors() }; + return { + info, + changes: { + added: includeProjectReferenceRedirectInfoIfRequested(added), + removed: includeProjectReferenceRedirectInfoIfRequested(removed), + updated: includeProjectReferenceRedirectInfoIfRequested + ? updated.map((fileName): protocol.FileWithProjectReferenceRedirectInfo => ({ + fileName, + isSourceOfProjectReferenceRedirect: this.isSourceOfProjectReferenceRedirect(fileName) + })) + : updated, + updatedRedirects: includeProjectReferenceRedirectInfo ? updatedRedirects : undefined + }, + projectErrors: this.getGlobalProjectErrors() + }; } else { // unknown version - return everything - const projectFileNames = this.getFileNames(); - const externalFiles = this.getExternalFiles().map(f => toNormalizedPath(f)); + const projectFileNames = this.getFileNamesWithRedirectInfo(!!includeProjectReferenceRedirectInfo); + const externalFiles = this.getExternalFiles().map((f): protocol.FileWithProjectReferenceRedirectInfo => ({ + fileName: toNormalizedPath(f), + isSourceOfProjectReferenceRedirect: false + })); const allFiles = projectFileNames.concat(externalFiles); - this.lastReportedFileNames = arrayToSet(allFiles); + this.lastReportedFileNames = arrayToMap( + allFiles, + info => info.fileName, + info => info.isSourceOfProjectReferenceRedirect + ); this.lastReportedVersion = this.projectProgramVersion; - return { info, files: allFiles, projectErrors: this.getGlobalProjectErrors() }; + return { + info, + files: includeProjectReferenceRedirectInfo ? allFiles : allFiles.map(f => f.fileName), + projectErrors: this.getGlobalProjectErrors() + }; } } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index e44472ed3f0..7f4399842a4 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1324,6 +1324,18 @@ namespace ts.server.protocol { lastFileExceededProgramSize?: string; } + export interface FileWithProjectReferenceRedirectInfo { + /** + * Name of file + */ + fileName: string; + + /** + * True if the file is primarily included in a referenced project + */ + isSourceOfProjectReferenceRedirect: boolean; + } + /** * Represents a set of changes that happen in project */ @@ -1331,15 +1343,20 @@ namespace ts.server.protocol { /** * List of added files */ - added: string[]; + added: string[] | FileWithProjectReferenceRedirectInfo[]; /** * List of removed files */ - removed: string[]; + removed: string[] | FileWithProjectReferenceRedirectInfo[]; /** * List of updated files */ - updated: string[]; + updated: string[] | FileWithProjectReferenceRedirectInfo[]; + /** + * List of files that have had their project reference redirect status updated + * Only provided when the synchronizeProjectList request has includeProjectReferenceRedirectInfo set to true + */ + updatedRedirects?: FileWithProjectReferenceRedirectInfo[]; } /** @@ -1357,8 +1374,10 @@ namespace ts.server.protocol { info?: ProjectVersionInfo; /** * List of files in project (might be omitted if current state of project can be computed using only information from 'changes') + * This property will have type FileWithProjectReferenceRedirectInfo[] if includeProjectReferenceRedirectInfo is set to true in + * the corresponding SynchronizeProjectList request; otherwise, it will have type string[]. */ - files?: string[]; + files?: string[] | FileWithProjectReferenceRedirectInfo[]; /** * Set of changes in project (omitted if the entire set of files in project should be replaced) */ @@ -1621,6 +1640,11 @@ namespace ts.server.protocol { * List of last known projects */ knownProjects: ProjectVersionInfo[]; + /** + * If true, response specifies whether or not each file in each project + * is a source from a project reference redirect + */ + includeProjectReferenceRedirectInfo?: boolean; } /** diff --git a/src/server/session.ts b/src/server/session.ts index 2733ca51412..89231ede007 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2265,7 +2265,7 @@ namespace ts.server { return this.requiredResponse(/*response*/ true); }, [CommandNames.SynchronizeProjectList]: (request: protocol.SynchronizeProjectListRequest) => { - const result = this.projectService.synchronizeProjectList(request.arguments.knownProjects); + const result = this.projectService.synchronizeProjectList(request.arguments.knownProjects, request.arguments.includeProjectReferenceRedirectInfo); if (!result.some(p => p.projectErrors && p.projectErrors.length !== 0)) { return this.requiredResponse(result); } diff --git a/src/testRunner/unittests/tsserver/projects.ts b/src/testRunner/unittests/tsserver/projects.ts index 9fb3da6490e..ae257385268 100644 --- a/src/testRunner/unittests/tsserver/projects.ts +++ b/src/testRunner/unittests/tsserver/projects.ts @@ -1431,6 +1431,176 @@ var x = 10;` host.checkTimeoutQueueLength(0); }); + it("synchronizeProjectList provides redirect info when requested", () => { + const projectRootPath = "/users/username/projects/project"; + const fileA: File = { + path: `${projectRootPath}/A/a.ts`, + content: "export const foo: string = 5;" + }; + const configA: File = { + path: `${projectRootPath}/A/tsconfig.json`, + content: `{ + "compilerOptions": { + "composite": true, + "declaration": true + } +}` + }; + const fileB: File = { + path: `${projectRootPath}/B/b.ts`, + content: "import { foo } from \"../A/a\"; console.log(foo);" + }; + const configB: File = { + path: `${projectRootPath}/B/tsconfig.json`, + content: `{ + "compilerOptions": { + "composite": true, + "declaration": true + }, + "references": [ + { "path": "../A" } + ] +}` + }; + const files = [fileA, fileB, configA, configB, libFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(fileA.path); + projectService.openClientFile(fileB.path); + const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true); + assert.deepEqual(knownProjects[0].files, [ + { + fileName: libFile.path, + isSourceOfProjectReferenceRedirect: false + }, + { + fileName: fileA.path, + isSourceOfProjectReferenceRedirect: false + }, + { + fileName: configA.path, + isSourceOfProjectReferenceRedirect: false + } + ]); + assert.deepEqual(knownProjects[1].files, [ + { + fileName: libFile.path, + isSourceOfProjectReferenceRedirect: false + }, + { + fileName: fileA.path, + isSourceOfProjectReferenceRedirect: true, + }, + { + fileName: fileB.path, + isSourceOfProjectReferenceRedirect: false, + }, + { + fileName: configB.path, + isSourceOfProjectReferenceRedirect: false + } + ]); + }); + + it("synchronizeProjectList provides updates to redirect info when requested", () => { + const projectRootPath = "/users/username/projects/project"; + const fileA: File = { + path: `${projectRootPath}/A/a.ts`, + content: "export const foo: string = 5;" + }; + const configA: File = { + path: `${projectRootPath}/A/tsconfig.json`, + content: `{ + "compilerOptions": { + "composite": true, + "declaration": true + } +}` + }; + const fileB: File = { + path: `${projectRootPath}/B/b.ts`, + content: "import { foo } from \"../B/b2\"; console.log(foo);" + }; + const fileB2: File = { + path: `${projectRootPath}/B/b2.ts`, + content: "export const foo: string = 5;" + }; + const configB: File = { + path: `${projectRootPath}/B/tsconfig.json`, + content: `{ + "compilerOptions": { + "composite": true, + "declaration": true + }, + "references": [ + { "path": "../A" } + ] +}` + }; + const files = [fileA, fileB, fileB2, configA, configB, libFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(fileA.path); + projectService.openClientFile(fileB.path); + const knownProjects = projectService.synchronizeProjectList([], /*includeProjectReferenceRedirectInfo*/ true); + assert.deepEqual(knownProjects[0].files, [ + { + fileName: libFile.path, + isSourceOfProjectReferenceRedirect: false + }, + { + fileName: fileA.path, + isSourceOfProjectReferenceRedirect: false + }, + { + fileName: configA.path, + isSourceOfProjectReferenceRedirect: false + } + ]); + assert.deepEqual(knownProjects[1].files, [ + { + fileName: libFile.path, + isSourceOfProjectReferenceRedirect: false + }, + { + fileName: fileB2.path, + isSourceOfProjectReferenceRedirect: false, + }, + { + fileName: fileB.path, + isSourceOfProjectReferenceRedirect: false, + }, + { + fileName: configB.path, + isSourceOfProjectReferenceRedirect: false + } + ]); + + host.modifyFile(configA.path, `{ + "compilerOptions": { + "composite": true, + "declaration": true + }, + "include": [ + "**/*", + "../B/b2.ts" + ] +}`); + const newKnownProjects = projectService.synchronizeProjectList(knownProjects.map(proj => proj.info!), /*includeProjectReferenceRedirectInfo*/ true); + assert.deepEqual(newKnownProjects[0].changes?.added, [ + { + fileName: fileB2.path, + isSourceOfProjectReferenceRedirect: false + } + ]); + assert.deepEqual(newKnownProjects[1].changes?.updatedRedirects, [ + { + fileName: fileB2.path, + isSourceOfProjectReferenceRedirect: true + } + ]); + }); + it("handles delayed directory watch invoke on file creation", () => { const projectRootPath = "/users/username/projects/project"; const fileB: File = { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 9a542d064b0..89cc8429f05 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7066,6 +7066,16 @@ declare namespace ts.server.protocol { * compiler settings. */ type ExternalProjectCompilerOptions = CompilerOptions & CompileOnSaveMixin & WatchOptions; + interface FileWithProjectReferenceRedirectInfo { + /** + * Name of file + */ + fileName: string; + /** + * True if the file is primarily included in a referenced project + */ + isSourceOfProjectReferenceRedirect: boolean; + } /** * Represents a set of changes that happen in project */ @@ -7073,15 +7083,20 @@ declare namespace ts.server.protocol { /** * List of added files */ - added: string[]; + added: string[] | FileWithProjectReferenceRedirectInfo[]; /** * List of removed files */ - removed: string[]; + removed: string[] | FileWithProjectReferenceRedirectInfo[]; /** * List of updated files */ - updated: string[]; + updated: string[] | FileWithProjectReferenceRedirectInfo[]; + /** + * List of files that have had their project reference redirect status updated + * Only provided when the synchronizeProjectList request has includeProjectReferenceRedirectInfo set to true + */ + updatedRedirects?: FileWithProjectReferenceRedirectInfo[]; } /** * Information found in a configure request.