diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 117887d7611ac..4bbd1b7d6b88a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -5,6 +5,7 @@ namespace ts.server { // tslint:disable variable-name export const ProjectsUpdatedInBackgroundEvent = "projectsUpdatedInBackground"; + export const SurveyReady = "surveyReady"; export const LargeFileReferencedEvent = "largeFileReferenced"; export const ConfigFileDiagEvent = "configFileDiag"; export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; @@ -17,6 +18,11 @@ namespace ts.server { data: { openFiles: string[]; }; } + export interface SurveyReady { + eventName: typeof SurveyReady; + data: { surveyId: string; }; + } + export interface LargeFileReferencedEvent { eventName: typeof LargeFileReferencedEvent; data: { file: string; fileSize: number; maxFileSize: number; }; @@ -98,7 +104,7 @@ namespace ts.server { readonly checkJs: boolean; } - export type ProjectServiceEvent = LargeFileReferencedEvent | ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; + export type ProjectServiceEvent = LargeFileReferencedEvent | SurveyReady | ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; export type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void; @@ -456,6 +462,9 @@ namespace ts.server { /** Tracks projects that we have already sent telemetry for. */ private readonly seenProjects = createMap(); + /** Tracks projects that we have already sent survey events for. */ + private readonly seenSurveyProjects = createMap(); + /*@internal*/ readonly watchFactory: WatchFactory; @@ -657,6 +666,14 @@ namespace ts.server { this.eventHandler(event); } + /* @internal */ + sendSurveyReadyEvent(surveyId: string) { + if (!this.eventHandler) { + return; + } + this.eventHandler({ eventName: SurveyReady, data: { surveyId } }); + } + /* @internal */ sendLargeFileReferencedEvent(file: string, fileSize: number) { if (!this.eventHandler) { @@ -1471,6 +1488,20 @@ namespace ts.server { return project; } + /*@internal*/ + sendSurveyReady(project: ExternalProject | ConfiguredProject): void { + if (this.seenSurveyProjects.has(project.projectName)) { + return; + } + + if (project.getCompilerOptions().checkJs !== undefined) { + const name = "checkJs"; + this.logger.info(`Survey ${name} is ready`); + this.sendSurveyReadyEvent(name); + this.seenSurveyProjects.set(project.projectName, true); + } + } + /*@internal*/ sendProjectTelemetry(project: ExternalProject | ConfiguredProject): void { if (this.seenProjects.has(project.projectName)) { diff --git a/src/server/project.ts b/src/server/project.ts index ff2c504a54a0a..59c1e082039c8 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1366,6 +1366,7 @@ namespace ts.server { result = super.updateGraph(); } this.projectService.sendProjectTelemetry(this); + this.projectService.sendSurveyReady(this); return result; } @@ -1565,6 +1566,7 @@ namespace ts.server { updateGraph() { const result = super.updateGraph(); this.projectService.sendProjectTelemetry(this); + this.projectService.sendSurveyReady(this); return result; } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index f8e94085c479e..89417082b4a72 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2436,6 +2436,18 @@ namespace ts.server.protocol { openFiles: string[]; } + export type SurveyReadyEventName = "surveyReady"; + + export interface SurveyReadyEvent extends Event { + event: SurveyReadyEventName; + body: SurveyReadyEventBody; + } + + export interface SurveyReadyEventBody { + /** Name of the survey. This is an internal machine- and programmer-friendly name */ + surveyId: string; + } + export type LargeFileReferencedEventName = "largeFileReferenced"; export interface LargeFileReferencedEvent extends Event { event: LargeFileReferencedEventName; diff --git a/src/server/session.ts b/src/server/session.ts index 8d2fbe9e190a1..97541d1995552 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -572,6 +572,10 @@ namespace ts.server { diagnostics: bakedDiags }, "configFileDiag"); break; + case SurveyReady: + const { surveyId } = event.data; + this.event({ surveyId }, "surveyReady"); + break; case ProjectLanguageServiceStateEvent: { const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState"; this.event({ diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index b99a2d9fe999e..6c98db8f0a0cf 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -2846,7 +2846,7 @@ namespace ts.projectSystem { const session = createSession(host, { canUseEvents: true, eventHandler: e => { - if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectsUpdatedInBackgroundEvent || e.eventName === server.ProjectInfoTelemetryEvent || e.eventName === server.OpenFileInfoTelemetryEvent || e.eventName === server.LargeFileReferencedEvent) { + if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectsUpdatedInBackgroundEvent || e.eventName === server.ProjectInfoTelemetryEvent || e.eventName === server.OpenFileInfoTelemetryEvent || e.eventName === server.LargeFileReferencedEvent || e.eventName === server.SurveyReady) { return; } assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); @@ -3496,6 +3496,121 @@ namespace ts.projectSystem { } }); + function createSessionWithEventHandler(host: TestServerHost) { + const surveyEvents: server.SurveyReady[] = []; + const session = createSession(host, { + eventHandler: e => { + if (e.eventName === server.SurveyReady) { + surveyEvents.push(e); + } + } + }); + + return { session, verifySurveyReadyEvent }; + + function verifySurveyReadyEvent(numberOfEvents: number) { + assert.equal(surveyEvents.length, numberOfEvents); + const expectedEvents = numberOfEvents === 0 ? [] : [{ + eventName: server.SurveyReady, + data: { surveyId: "checkJs" } + }]; + assert.deepEqual(surveyEvents, expectedEvents); + } + } + + it("doesn't log an event when checkJs isn't set", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { } }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects: 1 }); + const project = service.configuredProjects.get(tsconfig.path)!; + checkProjectActualFiles(project, [file.path, tsconfig.path]); + + verifySurveyReadyEvent(0); + }); + + it("logs an event when checkJs is set", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { checkJs: true } }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + }); + + it("logs an event when checkJs is set, only the first time", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const rando: File = { + path: `/rando/calrissian.ts`, + content: "export function f() { }" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { checkJs: true } }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + + closeFilesForSession([file], session); + openFilesForSession([rando], session); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + }); + + it("logs an event when checkJs is set after closing and reopening", () => { + const projectRoot = "/user/username/projects/project"; + const file: File = { + path: `${projectRoot}/src/file.ts`, + content: "export var y = 10;" + }; + const rando: File = { + path: `/rando/calrissian.ts`, + content: "export function f() { }" + }; + const tsconfig: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ }), + }; + const host = createServerHost([file, tsconfig]); + const { session, verifySurveyReadyEvent } = createSessionWithEventHandler(host); + openFilesForSession([file], session); + + verifySurveyReadyEvent(0); + + closeFilesForSession([file], session); + openFilesForSession([rando], session); + host.writeFile(tsconfig.path, JSON.stringify({ compilerOptions: { checkJs: true } })); + openFilesForSession([file], session); + + verifySurveyReadyEvent(1); + }); + describe("CompileOnSaveAffectedFileListRequest with and without projectFileName in request", () => { const projectRoot = "/user/username/projects/myproject"; const core: File = { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 5e992f3847a8b..ccf65bb705e85 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7460,6 +7460,15 @@ declare namespace ts.server.protocol { */ openFiles: string[]; } + type SurveyReadyEventName = "surveyReady"; + interface SurveyReadyEvent extends Event { + event: SurveyReadyEventName; + body: SurveyReadyEventBody; + } + interface SurveyReadyEventBody { + /** Name of the survey. This is an internal machine- and programmer-friendly name */ + surveyId: string; + } type LargeFileReferencedEventName = "largeFileReferenced"; interface LargeFileReferencedEvent extends Event { event: LargeFileReferencedEventName; @@ -8201,6 +8210,7 @@ declare namespace ts.server { declare namespace ts.server { const maxProgramSizeForNonTsFiles: number; const ProjectsUpdatedInBackgroundEvent = "projectsUpdatedInBackground"; + const SurveyReady = "surveyReady"; const LargeFileReferencedEvent = "largeFileReferenced"; const ConfigFileDiagEvent = "configFileDiag"; const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; @@ -8212,6 +8222,12 @@ declare namespace ts.server { openFiles: string[]; }; } + interface SurveyReady { + eventName: typeof SurveyReady; + data: { + surveyId: string; + }; + } interface LargeFileReferencedEvent { eventName: typeof LargeFileReferencedEvent; data: { @@ -8290,7 +8306,7 @@ declare namespace ts.server { interface OpenFileInfo { readonly checkJs: boolean; } - type ProjectServiceEvent = LargeFileReferencedEvent | ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; + type ProjectServiceEvent = LargeFileReferencedEvent | SurveyReady | ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void; interface SafeList { [name: string]: { @@ -8409,6 +8425,8 @@ declare namespace ts.server { readonly syntaxOnly?: boolean; /** Tracks projects that we have already sent telemetry for. */ private readonly seenProjects; + /** Tracks projects that we have already sent survey events for. */ + private readonly seenSurveyProjects; constructor(opts: ProjectServiceOptions); toPath(fileName: string): Path; private loadTypesMap;