diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 0a619da1cd39a..b32bd8d411399 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -168,11 +168,12 @@ namespace ts.projectSystem { readonly session: TestSession; readonly service: server.ProjectService; readonly host: TestServerHost; - constructor(files: FileOrFolder[]) { + constructor(files: FileOrFolder[], suppressDiagnosticEvents?: boolean) { this.host = createServerHost(files); this.session = createSession(this.host, { canUseEvents: true, eventHandler: event => this.events.push(event), + suppressDiagnosticEvents, }); this.service = this.session.getProjectService(); } @@ -485,6 +486,12 @@ namespace ts.projectSystem { checkNthEvent(session, server.toEvent("projectsUpdatedInBackground", { openFiles }), 0, /*isMostRecent*/ true); } + function checkNoDiagnosticEvents(session: TestSession) { + for (const event of session.events) { + assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); + } + } + function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { const events = session.events; assert.deepEqual(events[index], expectedEvent); @@ -4074,6 +4081,63 @@ namespace ts.projectSystem { session.clearMessages(); }); + it("suppressed diagnostic events", () => { + const file: FileOrFolder = { + path: "/a.ts", + content: "1 = 2;", + }; + + const host = createServerHost([file]); + const session = createSession(host, { canUseEvents: true, suppressDiagnosticEvents: true }); + const service = session.getProjectService(); + + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { file: file.path, fileContent: file.content }, + }); + + checkNumberOfProjects(service, { inferredProjects: 1 }); + + host.checkTimeoutQueueLength(0); + checkNoDiagnosticEvents(session); + + session.clearMessages(); + + let expectedSequenceId = session.getNextSeq(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [file.path], + } + }); + + host.checkTimeoutQueueLength(0); + checkNoDiagnosticEvents(session); + + checkCompleteEvent(session, 1, expectedSequenceId); + + session.clearMessages(); + + expectedSequenceId = session.getNextSeq(); + + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + file: file.path, + } + }); + + host.checkTimeoutQueueLength(0); + checkNoDiagnosticEvents(session); + + checkCompleteEvent(session, 1, expectedSequenceId); + + session.clearMessages(); + }); + function createDiagnostic(start: protocol.Location, end: protocol.Location, message: DiagnosticMessage, args: ReadonlyArray = []): protocol.Diagnostic { return { start, end, text: formatStringFromArgs(message.message, args), code: message.code, category: diagnosticCategoryName(message), source: undefined }; } @@ -4149,7 +4213,7 @@ namespace ts.projectSystem { serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, configFile.path); }); - it("are not generated when the config file doesnot include file opened and config file has errors", () => { + it("are not generated when the config file does not include file opened and config file has errors", () => { const file = { path: "/a/b/app.ts", content: "let x = 10" @@ -4173,7 +4237,26 @@ namespace ts.projectSystem { serverEventManager.hasZeroEvent("configFileDiag"); }); - it("are not generated when the config file doesnot include file opened and doesnt contain any errors", () => { + it("are not generated when the config file has errors but suppressDiagnosticEvents is true", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + } + }` + }; + const serverEventManager = new TestServerEventManager([file, configFile], /*suppressDiagnosticEvents*/ true); + openFilesForSession([file], serverEventManager.session); + serverEventManager.hasZeroEvent("configFileDiag"); + }); + + it("are not generated when the config file does not include file opened and doesnt contain any errors", () => { const file = { path: "/a/b/app.ts", content: "let x = 10" diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ba0a80890fe8e..386aa2629646e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -305,6 +305,7 @@ namespace ts.server { useInferredProjectPerProjectRoot: boolean; typingsInstaller: ITypingsInstaller; eventHandler?: ProjectServiceEventHandler; + suppressDiagnosticEvents?: boolean; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; pluginProbeLocations?: ReadonlyArray; @@ -392,6 +393,7 @@ namespace ts.server { public readonly typingsInstaller: ITypingsInstaller; public readonly throttleWaitMilliseconds?: number; private readonly eventHandler?: ProjectServiceEventHandler; + private readonly suppressDiagnosticEvents?: boolean; public readonly globalPlugins: ReadonlyArray; public readonly pluginProbeLocations: ReadonlyArray; @@ -413,6 +415,7 @@ namespace ts.server { this.typingsInstaller = opts.typingsInstaller || nullTypingsInstaller; this.throttleWaitMilliseconds = opts.throttleWaitMilliseconds; this.eventHandler = opts.eventHandler; + this.suppressDiagnosticEvents = opts.suppressDiagnosticEvents; this.globalPlugins = opts.globalPlugins || emptyArray; this.pluginProbeLocations = opts.pluginProbeLocations || emptyArray; this.allowLocalPluginLoads = !!opts.allowLocalPluginLoads; @@ -1598,7 +1601,7 @@ namespace ts.server { } private sendConfigFileDiagEvent(project: ConfiguredProject, triggerFile: NormalizedPath) { - if (!this.eventHandler) { + if (!this.eventHandler || this.suppressDiagnosticEvents) { return; } diff --git a/src/server/server.ts b/src/server/server.ts index 8fb9bfb64bc7b..4141a36161f81 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -512,6 +512,7 @@ namespace ts.server { hrtime: process.hrtime, logger, canUseEvents: true, + suppressDiagnosticEvents, globalPlugins, pluginProbeLocations, allowLocalPluginLoads, @@ -927,6 +928,7 @@ namespace ts.server { const useSingleInferredProject = hasArgument("--useSingleInferredProject"); const useInferredProjectPerProjectRoot = hasArgument("--useInferredProjectPerProjectRoot"); const disableAutomaticTypingAcquisition = hasArgument("--disableAutomaticTypingAcquisition"); + const suppressDiagnosticEvents = hasArgument("--suppressDiagnosticEvents"); const telemetryEnabled = hasArgument(Arguments.EnableTelemetry); logger.info(`Starting TS Server`); diff --git a/src/server/session.ts b/src/server/session.ts index 1999360e34b06..4d9c296f6a96f 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -295,6 +295,8 @@ namespace ts.server { */ canUseEvents: boolean; eventHandler?: ProjectServiceEventHandler; + /** Has no effect if eventHandler is also specified. */ + suppressDiagnosticEvents?: boolean; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; @@ -318,6 +320,7 @@ namespace ts.server { protected logger: Logger; protected canUseEvents: boolean; + private suppressDiagnosticEvents?: boolean; private eventHandler: ProjectServiceEventHandler; constructor(opts: SessionOptions) { @@ -328,6 +331,7 @@ namespace ts.server { this.hrtime = opts.hrtime; this.logger = opts.logger; this.canUseEvents = opts.canUseEvents; + this.suppressDiagnosticEvents = opts.suppressDiagnosticEvents; const { throttleWaitMilliseconds } = opts; @@ -352,6 +356,7 @@ namespace ts.server { typingsInstaller: this.typingsInstaller, throttleWaitMilliseconds, eventHandler: this.eventHandler, + suppressDiagnosticEvents: this.suppressDiagnosticEvents, globalPlugins: opts.globalPlugins, pluginProbeLocations: opts.pluginProbeLocations, allowLocalPluginLoads: opts.allowLocalPluginLoads @@ -401,11 +406,12 @@ namespace ts.server { private projectsUpdatedInBackgroundEvent(openFiles: string[]): void { this.projectService.logger.info(`got projects updated in background, updating diagnostics for ${openFiles}`); if (openFiles.length) { - const checkList = this.createCheckList(openFiles); - - // For now only queue error checking for open files. We can change this to include non open files as well - this.errorCheck.startNew(next => this.updateErrorCheck(next, checkList, 100, /*requireOpen*/ true)); + if (!this.suppressDiagnosticEvents) { + const checkList = this.createCheckList(openFiles); + // For now only queue error checking for open files. We can change this to include non open files as well + this.errorCheck.startNew(next => this.updateErrorCheck(next, checkList, 100, /*requireOpen*/ true)); + } // Send project changed event this.event({ @@ -489,7 +495,10 @@ namespace ts.server { } } + /** It is the caller's responsibility to verify that `!this.suppressDiagnosticEvents`. */ private updateErrorCheck(next: NextStep, checkList: PendingErrorCheck[], ms: number, requireOpen = true) { + Debug.assert(!this.suppressDiagnosticEvents); // Caller's responsibility + const seq = this.changeSeq; const followMs = Math.min(ms, 200); @@ -1379,6 +1388,10 @@ namespace ts.server { } private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void { + if (this.suppressDiagnosticEvents) { + return; + } + const checkList = this.createCheckList(fileNames); if (checkList.length > 0) { this.updateErrorCheck(next, checkList, delay); @@ -1748,6 +1761,10 @@ namespace ts.server { } private getDiagnosticsForProject(next: NextStep, delay: number, fileName: string): void { + if (this.suppressDiagnosticEvents) { + return; + } + const { fileNames, languageServiceDisabled } = this.getProjectInfoWorker(fileName, /*projectFileName*/ undefined, /*needFileNameList*/ true, /*excludeConfigFiles*/ true); if (languageServiceDisabled) { return; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index ed9ac13c3ebba..4045f2212a1c9 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7256,6 +7256,8 @@ declare namespace ts.server { */ canUseEvents: boolean; eventHandler?: ProjectServiceEventHandler; + /** Has no effect if eventHandler is also specified. */ + suppressDiagnosticEvents?: boolean; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; pluginProbeLocations?: ReadonlyArray; @@ -7274,6 +7276,7 @@ declare namespace ts.server { private hrtime; protected logger: Logger; protected canUseEvents: boolean; + private suppressDiagnosticEvents?; private eventHandler; constructor(opts: SessionOptions); private sendRequestCompletedEvent; @@ -7289,6 +7292,7 @@ declare namespace ts.server { private syntacticCheck; private infoCheck; private sendDiagnosticsEvent; + /** It is the caller's responsibility to verify that `!this.suppressDiagnosticEvents`. */ private updateErrorCheck; private cleanProjects; private cleanup; @@ -7817,6 +7821,7 @@ declare namespace ts.server { useInferredProjectPerProjectRoot: boolean; typingsInstaller: ITypingsInstaller; eventHandler?: ProjectServiceEventHandler; + suppressDiagnosticEvents?: boolean; throttleWaitMilliseconds?: number; globalPlugins?: ReadonlyArray; pluginProbeLocations?: ReadonlyArray; @@ -7883,6 +7888,7 @@ declare namespace ts.server { readonly typingsInstaller: ITypingsInstaller; readonly throttleWaitMilliseconds?: number; private readonly eventHandler?; + private readonly suppressDiagnosticEvents?; readonly globalPlugins: ReadonlyArray; readonly pluginProbeLocations: ReadonlyArray; readonly allowLocalPluginLoads: boolean;