diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index b67468206f..5e8d6e7671 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -825,6 +825,7 @@ namespace Harness.LanguageService { host: serverHost, cancellationToken: ts.server.nullCancellationToken, useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, typingsInstaller: undefined, byteLength: Utils.byteLength, hrtime: process.hrtime, diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index 92c10c1ff6..7c832b210b 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -57,6 +57,7 @@ namespace ts { logger: projectSystem.nullLogger, cancellationToken: { isCancellationRequested: () => false }, useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, typingsInstaller: undefined }; const projectService = new server.ProjectService(svcOpts); diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index 31a882b19d..e4b84e848c 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -36,6 +36,7 @@ namespace ts.projectSystem { host, cancellationToken: nullCancellationToken, useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, typingsInstaller: typingsInstaller || server.nullTypingsInstaller, byteLength: Utils.byteLength, hrtime: process.hrtime, @@ -552,7 +553,7 @@ namespace ts.projectSystem { }; const host = createServerHost([file1, file2, configFile, libFile], { newLine: "\r\n" }); const typingsInstaller = createTestTypingsInstaller(host); - const session = createSession(host, typingsInstaller); + const session = createSession(host, { typingsInstaller }); openFilesForSession([file1, file2], session); const compileFileRequest = makeSessionRequest(CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: configFile.path }); diff --git a/src/harness/unittests/session.ts b/src/harness/unittests/session.ts index 1f79213bee..18109cfa9d 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -43,6 +43,7 @@ namespace ts.server { host: mockHost, cancellationToken: nullCancellationToken, useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, typingsInstaller: undefined, byteLength: Utils.byteLength, hrtime: process.hrtime, @@ -394,6 +395,7 @@ namespace ts.server { host: mockHost, cancellationToken: nullCancellationToken, useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, typingsInstaller: undefined, byteLength: Utils.byteLength, hrtime: process.hrtime, @@ -461,6 +463,7 @@ namespace ts.server { host: mockHost, cancellationToken: nullCancellationToken, useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, typingsInstaller: undefined, byteLength: Utils.byteLength, hrtime: process.hrtime, diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 6c60160740..596e080430 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -185,23 +185,28 @@ namespace ts.projectSystem { } } - export function createSession(host: server.ServerHost, typingsInstaller?: server.ITypingsInstaller, projectServiceEventHandler?: server.ProjectServiceEventHandler, cancellationToken?: server.ServerCancellationToken, throttleWaitMilliseconds?: number) { - if (typingsInstaller === undefined) { - typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); + export function createSession(host: server.ServerHost, opts: Partial = {}) { + if (opts.typingsInstaller === undefined) { + opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); } - const opts: server.SessionOptions = { + + if (opts.eventHandler !== undefined) { + opts.canUseEvents = true; + } + + const sessionOptions: server.SessionOptions = { host, - cancellationToken: cancellationToken || server.nullCancellationToken, + cancellationToken: server.nullCancellationToken, useSingleInferredProject: false, - typingsInstaller, + useInferredProjectPerProjectRoot: false, + typingsInstaller: undefined, byteLength: Utils.byteLength, hrtime: process.hrtime, logger: nullLogger, - canUseEvents: projectServiceEventHandler !== undefined, - eventHandler: projectServiceEventHandler, - throttleWaitMilliseconds + canUseEvents: false }; - return new TestSession(opts); + + return new TestSession({ ...sessionOptions, ...opts }); } interface CreateProjectServiceParameters { @@ -215,9 +220,16 @@ namespace ts.projectSystem { export class TestProjectService extends server.ProjectService { constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, - typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler) { + typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler, opts: Partial = {}) { super({ - host, logger, cancellationToken, useSingleInferredProject, typingsInstaller, eventHandler + host, + logger, + cancellationToken, + useSingleInferredProject, + useInferredProjectPerProjectRoot: false, + typingsInstaller, + eventHandler, + ...opts }); } @@ -631,7 +643,7 @@ namespace ts.projectSystem { } } - describe("tsserver-project-system", () => { + describe("tsserverProjectSystem", () => { const commonFile1: FileOrFolder = { path: "/a/b/commonFile1.ts", content: "let x = 1" @@ -2230,13 +2242,16 @@ namespace ts.projectSystem { filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); let lastEvent: server.ProjectLanguageServiceStateEvent; - const session = createSession(host, /*typingsInstaller*/ undefined, e => { - if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent || e.eventName === server.ProjectInfoTelemetryEvent) { - return; + const session = createSession(host, { + canUseEvents: true, + eventHandler: e => { + if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent || e.eventName === server.ProjectInfoTelemetryEvent) { + return; + } + assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); + assert.equal(e.data.project.getProjectName(), config.path, "project name"); + lastEvent = e; } - assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); - assert.equal(e.data.project.getProjectName(), config.path, "project name"); - lastEvent = e; }); session.executeCommand({ seq: 0, @@ -2280,12 +2295,15 @@ namespace ts.projectSystem { host.getFileSize = (filePath: string) => filePath === f2.path ? server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath); let lastEvent: server.ProjectLanguageServiceStateEvent; - const session = createSession(host, /*typingsInstaller*/ undefined, e => { - if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectInfoTelemetryEvent) { - return; + const session = createSession(host, { + canUseEvents: true, + eventHandler: e => { + if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectInfoTelemetryEvent) { + return; + } + assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); + lastEvent = e; } - assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); - lastEvent = e; }); session.executeCommand({ seq: 0, @@ -3069,7 +3087,10 @@ namespace ts.projectSystem { }; const host = createServerHost([file, configFile]); - const session = createSession(host, /*typingsInstaller*/ undefined, serverEventManager.handler); + const session = createSession(host, { + canUseEvents: true, + eventHandler: serverEventManager.handler + }); openFilesForSession([file], session); serverEventManager.checkEventCountOfType("configFileDiag", 1); @@ -3096,7 +3117,10 @@ namespace ts.projectSystem { }; const host = createServerHost([file, configFile]); - const session = createSession(host, /*typingsInstaller*/ undefined, serverEventManager.handler); + const session = createSession(host, { + canUseEvents: true, + eventHandler: serverEventManager.handler + }); openFilesForSession([file], session); serverEventManager.checkEventCountOfType("configFileDiag", 1); }); @@ -3115,7 +3139,10 @@ namespace ts.projectSystem { }; const host = createServerHost([file, configFile]); - const session = createSession(host, /*typingsInstaller*/ undefined, serverEventManager.handler); + const session = createSession(host, { + canUseEvents: true, + eventHandler: serverEventManager.handler + }); openFilesForSession([file], session); serverEventManager.checkEventCountOfType("configFileDiag", 1); @@ -3504,6 +3531,93 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { inferredProjects: 1 }); checkProjectActualFiles(projectService.inferredProjects[0], [f.path]); }); + + it("inferred projects per project root", () => { + const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" }; + const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" }; + const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" }; + const file4 = { path: "/c/file3.ts", content: "let z = 4;" }; + const host = createServerHost([file1, file2, file3, file4]); + const session = createSession(host, { + useSingleInferredProject: true, + useInferredProjectPerProjectRoot: true + }); + session.executeCommand({ + seq: 1, + type: "request", + command: CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ScriptTarget.ESNext + } + } + }); + session.executeCommand({ + seq: 2, + type: "request", + command: CommandNames.CompilerOptionsForInferredProjects, + arguments: { + options: { + allowJs: true, + target: ScriptTarget.ES2015 + }, + projectRootPath: "/b" + } + }); + session.executeCommand({ + seq: 3, + type: "request", + command: CommandNames.Open, + arguments: { + file: file1.path, + fileContent: file1.content, + scriptKindName: "JS", + projectRootPath: file1.projectRootPath + } + }); + session.executeCommand({ + seq: 4, + type: "request", + command: CommandNames.Open, + arguments: { + file: file2.path, + fileContent: file2.content, + scriptKindName: "JS", + projectRootPath: file2.projectRootPath + } + }); + session.executeCommand({ + seq: 5, + type: "request", + command: CommandNames.Open, + arguments: { + file: file3.path, + fileContent: file3.content, + scriptKindName: "JS", + projectRootPath: file3.projectRootPath + } + }); + session.executeCommand({ + seq: 6, + type: "request", + command: CommandNames.Open, + arguments: { + file: file4.path, + fileContent: file4.content, + scriptKindName: "JS" + } + }); + + const projectService = session.getProjectService(); + checkNumberOfProjects(projectService, { inferredProjects: 3 }); + checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); + checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); + checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); + assert.equal(projectService.inferredProjects[0].getCompilerOptions().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[1].getCompilerOptions().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[2].getCompilerOptions().target, ScriptTarget.ES2015); + }); }); describe("No overwrite emit error", () => { @@ -3697,7 +3811,7 @@ namespace ts.projectSystem { resetRequest: noop }; - const session = createSession(host, /*typingsInstaller*/ undefined, /*projectServiceEventHandler*/ undefined, cancellationToken); + const session = createSession(host, { cancellationToken }); expectedRequestId = session.getNextSeq(); session.executeCommandSeq({ @@ -3737,7 +3851,11 @@ namespace ts.projectSystem { const cancellationToken = new TestServerCancellationToken(); const host = createServerHost([f1, config]); - const session = createSession(host, /*typingsInstaller*/ undefined, () => { }, cancellationToken); + const session = createSession(host, { + canUseEvents: true, + eventHandler: () => { }, + cancellationToken + }); { session.executeCommandSeq({ command: "open", @@ -3870,7 +3988,12 @@ namespace ts.projectSystem { }; const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3); const host = createServerHost([f1, config]); - const session = createSession(host, /*typingsInstaller*/ undefined, () => { }, cancellationToken, /*throttleWaitMilliseconds*/ 0); + const session = createSession(host, { + canUseEvents: true, + eventHandler: () => { }, + cancellationToken, + throttleWaitMilliseconds: 0 + }); { session.executeCommandSeq({ command: "open", diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index b03959d12a..fe7946fc0f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -323,6 +323,7 @@ namespace ts.server { logger: Logger; cancellationToken: HostCancellationToken; useSingleInferredProject: boolean; + useInferredProjectPerProjectRoot: boolean; typingsInstaller: ITypingsInstaller; eventHandler?: ProjectServiceEventHandler; throttleWaitMilliseconds?: number; @@ -364,7 +365,7 @@ namespace ts.server { readonly openFiles: ScriptInfo[] = []; private compilerOptionsForInferredProjects: CompilerOptions; - private compileOnSaveForInferredProjects: boolean; + private compilerOptionsForInferredProjectsPerProjectRoot = createMap(); private readonly projectToSizeMap: Map = createMap(); private readonly directoryWatchers: DirectoryWatchers; private readonly throttledOperations: ThrottledOperations; @@ -382,6 +383,7 @@ namespace ts.server { public readonly logger: Logger; public readonly cancellationToken: HostCancellationToken; public readonly useSingleInferredProject: boolean; + public readonly useInferredProjectPerProjectRoot: boolean; public readonly typingsInstaller: ITypingsInstaller; public readonly throttleWaitMilliseconds?: number; private readonly eventHandler?: ProjectServiceEventHandler; @@ -398,6 +400,7 @@ namespace ts.server { this.logger = opts.logger; this.cancellationToken = opts.cancellationToken; this.useSingleInferredProject = opts.useSingleInferredProject; + this.useInferredProjectPerProjectRoot = opts.useInferredProjectPerProjectRoot; this.typingsInstaller = opts.typingsInstaller || nullTypingsInstaller; this.throttleWaitMilliseconds = opts.throttleWaitMilliseconds; this.eventHandler = opts.eventHandler; @@ -464,17 +467,42 @@ namespace ts.server { project.updateGraph(); } - setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions): void { - this.compilerOptionsForInferredProjects = convertCompilerOptions(projectCompilerOptions); + setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void { + Debug.assert(projectRootPath === undefined || this.useInferredProjectPerProjectRoot, "Setting compiler options per project root path is only supported when useInferredProjectPerProjectRoot is enabled"); + + const compilerOptions = convertCompilerOptions(projectCompilerOptions); + // always set 'allowNonTsExtensions' for inferred projects since user cannot configure it from the outside // previously we did not expose a way for user to change these settings and this option was enabled by default - this.compilerOptionsForInferredProjects.allowNonTsExtensions = true; - this.compileOnSaveForInferredProjects = projectCompilerOptions.compileOnSave; - for (const proj of this.inferredProjects) { - proj.setCompilerOptions(this.compilerOptionsForInferredProjects); - proj.compileOnSaveEnabled = projectCompilerOptions.compileOnSave; + compilerOptions.allowNonTsExtensions = true; + + if (projectRootPath) { + this.compilerOptionsForInferredProjectsPerProjectRoot.set(projectRootPath, compilerOptions); } - this.updateProjectGraphs(this.inferredProjects); + else { + this.compilerOptionsForInferredProjects = compilerOptions; + } + + const updatedProjects: Project[] = []; + for (const project of this.inferredProjects) { + // Only update compiler options in the following cases: + // - Inferred projects without a projectRootPath, if the new options do not apply to + // a workspace root + // - Inferred projects with a projectRootPath, if the new options do not apply to a + // workspace root and there is no more specific set of options for that project's + // root path + // - Inferred projects with a projectRootPath, if the new options apply to that + // project root path. + if (projectRootPath ? + project.projectRootPath === projectRootPath : + !project.projectRootPath || !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath)) { + project.setCompilerOptions(compilerOptions); + project.compileOnSaveEnabled = compilerOptions.compileOnSave; + updatedProjects.push(project); + } + } + + this.updateProjectGraphs(updatedProjects); } stopWatchingDirectory(directory: string) { @@ -715,7 +743,7 @@ namespace ts.server { } } - private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean): void { + private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean, projectRootPath?: string): void { const externalProject = this.findContainingExternalProject(info.fileName); if (externalProject) { // file is already included in some external project - do nothing @@ -743,30 +771,30 @@ namespace ts.server { } if (info.containingProjects.length === 0) { - // create new inferred project p with the newly opened file as root - // or add root to existing inferred project if 'useOneInferredProject' is true - const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info); - if (!this.useSingleInferredProject) { - // if useOneInferredProject is not set then try to fixup ownership of open files - // check 'defaultProject !== inferredProject' is necessary to handle cases - // when creation inferred project for some file has added other open files into this project (i.e. as referenced files) - // we definitely don't want to delete the project that was just created + // get (or create) an inferred project using the newly opened file as a root. + const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info, projectRootPath); + if (!this.useSingleInferredProject && !inferredProject.projectRootPath) { + // if useSingleInferredProject is false and the inferred project is not associated + // with a project root, then try to repair the ownership of open files. for (const f of this.openFiles) { if (f.containingProjects.length === 0 || !inferredProject.containsScriptInfo(f)) { // this is orphaned file that we have not processed yet - skip it continue; } - for (const fContainingProject of f.containingProjects) { - if (fContainingProject.projectKind === ProjectKind.Inferred && - fContainingProject.isRoot(f) && - fContainingProject !== inferredProject) { - + for (const containingProject of f.containingProjects) { + // We verify 'containingProject !== inferredProject' to handle cases + // where the inferred project for some file has added other open files + // into this project (i.e. as referenced files) as we don't want to + // delete the project that was just created + if (containingProject.projectKind === ProjectKind.Inferred && + containingProject !== inferredProject && + containingProject.isRoot(f)) { // open file used to be root in inferred project, // this inferred project is different from the one we've just created for current file // and new inferred project references this open file. // We should delete old inferred project and attach open file to the new one - this.removeProject(fContainingProject); + this.removeProject(containingProject); f.attachToProject(inferredProject); } } @@ -1286,11 +1314,74 @@ namespace ts.server { return configFileErrors; } - createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) { - const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length; - const project = useExistingProject - ? this.inferredProjects[0] - : new InferredProject(this, this.documentRegistry, this.compilerOptionsForInferredProjects); + private getOrCreateInferredProjectForProjectRootPathIfEnabled(root: ScriptInfo, projectRootPath: string | undefined): InferredProject | undefined { + if (!this.useInferredProjectPerProjectRoot) { + return undefined; + } + + if (projectRootPath) { + // if we have an explicit project root path, find (or create) the matching inferred project. + for (const project of this.inferredProjects) { + if (project.projectRootPath === projectRootPath) { + return project; + } + } + return this.createInferredProject(/*isSingleInferredProject*/ false, projectRootPath); + } + + // we don't have an explicit root path, so we should try to find an inferred project + // that more closely contains the file. + let bestMatch: InferredProject; + for (const project of this.inferredProjects) { + // ignore single inferred projects (handled elsewhere) + if (!project.projectRootPath) continue; + // ignore inferred projects that don't contain the root's path + if (!containsPath(project.projectRootPath, root.path, this.host.getCurrentDirectory(), !this.host.useCaseSensitiveFileNames)) continue; + // ignore inferred projects that are higher up in the project root. + // TODO(rbuckton): Should we add the file as a root to these as well? + if (bestMatch && bestMatch.projectRootPath.length > project.projectRootPath.length) continue; + bestMatch = project; + } + + return bestMatch; + } + + private getOrCreateSingleInferredProjectIfEnabled(): InferredProject | undefined { + if (!this.useSingleInferredProject) { + return undefined; + } + + // If `useInferredProjectPerProjectRoot` is not enabled, then there will only be one + // inferred project for all files. If `useInferredProjectPerProjectRoot` is enabled + // then we want to put all files that are not opened with a `projectRootPath` into + // the same inferred project. + // + // To avoid the cost of searching through the array and to optimize for the case where + // `useInferredProjectPerProjectRoot` is not enabled, we will always put the inferred + // project for non-rooted files at the front of the array. + if (this.inferredProjects.length > 0 && this.inferredProjects[0].projectRootPath === undefined) { + return this.inferredProjects[0]; + } + + return this.createInferredProject(/*isSingleInferredProject*/ true); + } + + private createInferredProject(isSingleInferredProject?: boolean, projectRootPath?: string): InferredProject { + const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects; + const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath); + if (isSingleInferredProject) { + this.inferredProjects.unshift(project); + } + else { + this.inferredProjects.push(project); + } + return project; + } + + createInferredProjectWithRootFileIfNecessary(root: ScriptInfo, projectRootPath?: string) { + const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(root, projectRootPath) || + this.getOrCreateSingleInferredProjectIfEnabled() || + this.createInferredProject(); project.addRoot(root); @@ -1301,9 +1392,6 @@ namespace ts.server { project.updateGraph(); - if (!useExistingProject) { - this.inferredProjects.push(project); - } return project; } @@ -1477,7 +1565,7 @@ namespace ts.server { // at this point if file is the part of some configured/external project then this project should be created const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); - this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true); + this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true, projectRootPath); // Delete the orphan files here because there might be orphan script infos (which are not part of project) // when some file/s were closed which resulted in project removal. // It was then postponed to cleanup these script infos so that they can be reused if diff --git a/src/server/project.ts b/src/server/project.ts index 50020682ab..c7cd503953 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -837,6 +837,7 @@ namespace ts.server { * the file and its imports/references are put into an InferredProject. */ export class InferredProject extends Project { + public readonly projectRootPath: string | undefined; private static readonly newName = (() => { let nextId = 1; @@ -876,7 +877,7 @@ namespace ts.server { // Used to keep track of what directories are watched for this project directoriesWatchedForTsconfig: string[] = []; - constructor(projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions) { + constructor(projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions, projectRootPath?: string) { super(InferredProject.newName(), ProjectKind.Inferred, projectService, @@ -885,6 +886,7 @@ namespace ts.server { /*languageServiceEnabled*/ true, compilerOptions, /*compileOnSaveEnabled*/ false); + this.projectRootPath = projectRootPath; } addRoot(info: ScriptInfo) { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index e8ce8611a5..0b7405c7b6 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1304,6 +1304,13 @@ namespace ts.server.protocol { * Compiler options to be used with inferred projects. */ options: ExternalProjectCompilerOptions; + + /** + * Specifies the project root path used to scope compiler options. + * It is an error to provide this property if the server has not been started with + * `useInferredProjectPerProjectRoot` enabled. + */ + projectRootPath?: string; } /** diff --git a/src/server/server.ts b/src/server/server.ts index 47a858c0ef..66467fd46a 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -9,6 +9,7 @@ namespace ts.server { canUseEvents: boolean; installerEventPort: number; useSingleInferredProject: boolean; + useInferredProjectPerProjectRoot: boolean; disableAutomaticTypingAcquisition: boolean; globalTypingsCacheLocation: string; logger: Logger; @@ -414,6 +415,7 @@ namespace ts.server { host, cancellationToken, useSingleInferredProject, + useInferredProjectPerProjectRoot, typingsInstaller: typingsInstaller || nullTypingsInstaller, byteLength: Buffer.byteLength, hrtime: process.hrtime, @@ -779,6 +781,7 @@ namespace ts.server { const allowLocalPluginLoads = hasArgument("--allowLocalPluginLoads"); const useSingleInferredProject = hasArgument("--useSingleInferredProject"); + const useInferredProjectPerProjectRoot = hasArgument("--useInferredProjectPerProjectRoot"); const disableAutomaticTypingAcquisition = hasArgument("--disableAutomaticTypingAcquisition"); const telemetryEnabled = hasArgument(Arguments.EnableTelemetry); @@ -788,6 +791,7 @@ namespace ts.server { installerEventPort: eventPort, canUseEvents: eventPort === undefined, useSingleInferredProject, + useInferredProjectPerProjectRoot, disableAutomaticTypingAcquisition, globalTypingsCacheLocation: getGlobalTypingsCacheLocation(), typingSafeListLocation, diff --git a/src/server/session.ts b/src/server/session.ts index a797ffc36d..8090936298 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -247,6 +247,7 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; useSingleInferredProject: boolean; + useInferredProjectPerProjectRoot: boolean; typingsInstaller: ITypingsInstaller; byteLength: (buf: string, encoding?: string) => number; hrtime: (start?: number[]) => number[]; @@ -307,6 +308,7 @@ namespace ts.server { logger: this.logger, cancellationToken: this.cancellationToken, useSingleInferredProject: opts.useSingleInferredProject, + useInferredProjectPerProjectRoot: opts.useInferredProjectPerProjectRoot, typingsInstaller: this.typingsInstaller, throttleWaitMilliseconds, eventHandler: this.eventHandler, @@ -743,7 +745,7 @@ namespace ts.server { } private setCompilerOptionsForInferredProjects(args: protocol.SetCompilerOptionsForInferredProjectsArgs): void { - this.projectService.setCompilerOptionsForInferredProjects(args.options); + this.projectService.setCompilerOptionsForInferredProjects(args.options, args.projectRootPath); } private getProjectInfo(args: protocol.ProjectInfoRequestArgs): protocol.ProjectInfo {