From fa7f3e85fe21bd17136a1cc77fe67b1a2fede2fb Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Thu, 3 Aug 2017 16:03:24 -0700 Subject: [PATCH 1/4] Adds support for inferred project isolation by projectRootPath --- src/harness/harnessLanguageService.ts | 1 + .../unittests/cachingInServerLSHost.ts | 1 + src/harness/unittests/compileOnSave.ts | 3 +- src/harness/unittests/session.ts | 3 + .../unittests/tsserverProjectSystem.ts | 182 +++++++++++++++--- src/server/editorServices.ts | 136 +++++++++---- src/server/project.ts | 4 +- src/server/protocol.ts | 7 + src/server/server.ts | 4 + src/server/session.ts | 4 +- 10 files changed, 278 insertions(+), 67 deletions(-) diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index c604b22465..9f4649e4d4 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -828,6 +828,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 eb2907e89d..ea5b8f021e 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -69,6 +69,7 @@ namespace ts { logger, 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 79e1f921dc..9321577c72 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, @@ -550,7 +551,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 862ebee4b0..4b79f71a7d 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -55,6 +55,7 @@ namespace ts.server { host: mockHost, cancellationToken: nullCancellationToken, useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, typingsInstaller: undefined, byteLength: Utils.byteLength, hrtime: process.hrtime, @@ -405,6 +406,7 @@ namespace ts.server { host: mockHost, cancellationToken: nullCancellationToken, useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, typingsInstaller: undefined, byteLength: Utils.byteLength, hrtime: process.hrtime, @@ -472,6 +474,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 4a7cafa245..2d6b370e23 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -186,23 +186,25 @@ 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; + } + return new TestSession({ host, - cancellationToken: cancellationToken || server.nullCancellationToken, + cancellationToken: server.nullCancellationToken, useSingleInferredProject: false, - typingsInstaller, + useInferredProjectPerProjectRoot: false, + typingsInstaller: opts.typingsInstaller, byteLength: Utils.byteLength, hrtime: process.hrtime, logger: nullLogger, - canUseEvents: projectServiceEventHandler !== undefined, - eventHandler: projectServiceEventHandler, - throttleWaitMilliseconds - }; - return new TestSession(opts); + canUseEvents: false, + ...opts + }); } export interface CreateProjectServiceParameters { @@ -216,9 +218,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 }); } @@ -632,7 +641,7 @@ namespace ts.projectSystem { } } - describe("tsserver-project-system", () => { + describe("tsserverProjectSystem", () => { const commonFile1: FileOrFolder = { path: "/a/b/commonFile1.ts", content: "let x = 1" @@ -2231,13 +2240,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, @@ -2281,12 +2293,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, @@ -3070,7 +3085,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); @@ -3097,7 +3115,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); }); @@ -3116,7 +3137,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); @@ -3505,6 +3529,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", () => { @@ -3698,7 +3809,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({ @@ -3738,7 +3849,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", @@ -3871,7 +3986,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 4ffdf5d6c5..e7e041711a 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; @@ -463,17 +466,33 @@ namespace ts.server { project.updateGraph(); } - setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions): void { - this.compilerOptionsForInferredProjects = convertCompilerOptions(projectCompilerOptions); + setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void { + // ignore this settings if we are not creating inferred projects per project root. + if (projectRootPath && !this.useInferredProjectPerProjectRoot) return; + + const compilerOptionsForInferredProjects = 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; + compilerOptionsForInferredProjects.allowNonTsExtensions = true; + + if (projectRootPath) { + this.compilerOptionsForInferredProjectsPerProjectRoot.set(projectRootPath, compilerOptionsForInferredProjects); } - this.updateProjectGraphs(this.inferredProjects); + else { + this.compilerOptionsForInferredProjects = compilerOptionsForInferredProjects; + } + + const updatedProjects: Project[] = []; + for (const project of this.inferredProjects) { + if (project.projectRootPath === projectRootPath || (project.projectRootPath && !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath))) { + project.setCompilerOptions(compilerOptionsForInferredProjects); + project.compileOnSaveEnabled = compilerOptionsForInferredProjects.compileOnSave; + updatedProjects.push(project); + } + } + + this.updateProjectGraphs(updatedProjects); } stopWatchingDirectory(directory: string) { @@ -713,7 +732,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 @@ -741,30 +760,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); } } @@ -1285,11 +1304,65 @@ 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 best matches 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 (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); @@ -1300,9 +1373,6 @@ namespace ts.server { project.updateGraph(); - if (!useExistingProject) { - this.inferredProjects.push(project); - } return project; } @@ -1476,7 +1546,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 524b6c4d28..7f02b0ec84 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -836,6 +836,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; @@ -875,7 +876,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, @@ -884,6 +885,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 b6756a7d4f..c0760dbfd3 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 commpiler options. + * This message is ignored if this property has been specified and the server is not + * configured to create an inferred project per project root. + */ + projectRootPath?: string; } /** diff --git a/src/server/server.ts b/src/server/server.ts index 6600b63dcb..9026a624bb 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; @@ -410,6 +411,7 @@ namespace ts.server { host, cancellationToken, useSingleInferredProject, + useInferredProjectPerProjectRoot, typingsInstaller: typingsInstaller || nullTypingsInstaller, byteLength: Buffer.byteLength, hrtime: process.hrtime, @@ -765,6 +767,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); @@ -774,6 +777,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 074ba4d6ca..cf27bbc613 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -251,6 +251,7 @@ namespace ts.server { host: ServerHost; cancellationToken: ServerCancellationToken; useSingleInferredProject: boolean; + useInferredProjectPerProjectRoot: boolean; typingsInstaller: ITypingsInstaller; byteLength: (buf: string, encoding?: string) => number; hrtime: (start?: number[]) => number[]; @@ -311,6 +312,7 @@ namespace ts.server { logger: this.logger, cancellationToken: this.cancellationToken, useSingleInferredProject: opts.useSingleInferredProject, + useInferredProjectPerProjectRoot: opts.useInferredProjectPerProjectRoot, typingsInstaller: this.typingsInstaller, throttleWaitMilliseconds, eventHandler: this.eventHandler, @@ -744,7 +746,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 { From 37b9b7089c3c31adf37b9abdbc0467db2020a7c2 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 8 Aug 2017 17:23:50 -0700 Subject: [PATCH 2/4] PR Feedback --- .../unittests/tsserverProjectSystem.ts | 15 ++++++---- src/server/editorServices.ts | 30 ++++++++++++------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 2d6b370e23..362bd847d6 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -188,23 +188,26 @@ namespace ts.projectSystem { export function createSession(host: server.ServerHost, opts: Partial = {}) { if (opts.typingsInstaller === undefined) { - opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); + opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); } + if (opts.eventHandler !== undefined) { opts.canUseEvents = true; } - return new TestSession({ + + const sessionOptions: server.SessionOptions = { host, cancellationToken: server.nullCancellationToken, useSingleInferredProject: false, useInferredProjectPerProjectRoot: false, - typingsInstaller: opts.typingsInstaller, + typingsInstaller: undefined, byteLength: Utils.byteLength, hrtime: process.hrtime, logger: nullLogger, - canUseEvents: false, - ...opts - }); + canUseEvents: false + }; + + return new TestSession({ ...sessionOptions, ...opts }); } export interface CreateProjectServiceParameters { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e7e041711a..6084b38bf8 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -467,27 +467,28 @@ namespace ts.server { } setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void { - // ignore this settings if we are not creating inferred projects per project root. - if (projectRootPath && !this.useInferredProjectPerProjectRoot) return; + Debug.assert(projectRootPath === undefined || this.useInferredProjectPerProjectRoot, "Setting compiler options per project root path is only supported when useInferredProjectPerProjectRoot is enabled"); - const compilerOptionsForInferredProjects = convertCompilerOptions(projectCompilerOptions); + 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 - compilerOptionsForInferredProjects.allowNonTsExtensions = true; + compilerOptions.allowNonTsExtensions = true; if (projectRootPath) { - this.compilerOptionsForInferredProjectsPerProjectRoot.set(projectRootPath, compilerOptionsForInferredProjects); + this.compilerOptionsForInferredProjectsPerProjectRoot.set(projectRootPath, compilerOptions); } else { - this.compilerOptionsForInferredProjects = compilerOptionsForInferredProjects; + this.compilerOptionsForInferredProjects = compilerOptions; } const updatedProjects: Project[] = []; for (const project of this.inferredProjects) { - if (project.projectRootPath === projectRootPath || (project.projectRootPath && !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath))) { - project.setCompilerOptions(compilerOptionsForInferredProjects); - project.compileOnSaveEnabled = compilerOptionsForInferredProjects.compileOnSave; + if (projectRootPath ? + project.projectRootPath === projectRootPath : + !project.projectRootPath || !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath)) { + project.setCompilerOptions(compilerOptions); + project.compileOnSaveEnabled = compilerOptions.compileOnSave; updatedProjects.push(project); } } @@ -1319,7 +1320,8 @@ namespace ts.server { return this.createInferredProject(/*isSingleInferredProject*/ false, projectRootPath); } - // we don't have an explicit root path, so we should try to find an inferred project that best matches the file. + // 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) @@ -1340,6 +1342,14 @@ namespace ts.server { 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]; } From b0317775667b440c31b040459639b55a0d1afd7b Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Thu, 10 Aug 2017 17:43:22 -0700 Subject: [PATCH 3/4] PR Feedback --- .../unittests/tsserverProjectSystem.ts | 2 +- src/server/editorServices.ts | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 362bd847d6..2b1d0dd172 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -190,7 +190,7 @@ namespace ts.projectSystem { if (opts.typingsInstaller === undefined) { opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); } - + if (opts.eventHandler !== undefined) { opts.canUseEvents = true; } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 6084b38bf8..e6da7afc3f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -484,8 +484,16 @@ namespace ts.server { const updatedProjects: Project[] = []; for (const project of this.inferredProjects) { - if (projectRootPath ? - project.projectRootPath === projectRootPath : + // 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; @@ -1320,7 +1328,7 @@ namespace ts.server { return this.createInferredProject(/*isSingleInferredProject*/ false, projectRootPath); } - // we don't have an explicit root path, so we should try to find an inferred project + // 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) { @@ -1342,11 +1350,11 @@ namespace ts.server { return undefined; } - // If `useInferredProjectPerProjectRoot` is not enabled, then there will only be one - // inferred project for all files. If `useInferredProjectPerProjectRoot` is enabled + // 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. From c272c3c5cd153499ca7d80f5ef114f47f9d938ee Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Fri, 11 Aug 2017 16:06:24 -0700 Subject: [PATCH 4/4] Comment update --- src/server/protocol.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/protocol.ts b/src/server/protocol.ts index c0760dbfd3..746d979196 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1306,9 +1306,9 @@ namespace ts.server.protocol { options: ExternalProjectCompilerOptions; /** - * Specifies the project root path used to scope commpiler options. - * This message is ignored if this property has been specified and the server is not - * configured to create an inferred project per project root. + * 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; }