diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 56026bb70d..8851c8f282 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 LargeFileReferencedEvent = "largeFileReferenced"; export const ConfigFileDiagEvent = "configFileDiag"; export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; export const ProjectInfoTelemetryEvent = "projectInfo"; @@ -16,6 +17,11 @@ namespace ts.server { data: { openFiles: string[]; }; } + export interface LargeFileReferencedEvent { + eventName: typeof LargeFileReferencedEvent; + data: { file: string; fileSize: number; maxFileSize: number; }; + } + export interface ConfigFileDiagEvent { eventName: typeof ConfigFileDiagEvent; data: { triggerFile: string, configFileName: string, diagnostics: ReadonlyArray }; @@ -92,7 +98,7 @@ namespace ts.server { readonly checkJs: boolean; } - export type ProjectServiceEvent = ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; + export type ProjectServiceEvent = LargeFileReferencedEvent | ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; export type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void; @@ -645,6 +651,19 @@ namespace ts.server { this.eventHandler(event); } + /* @internal */ + sendLargeFileReferencedEvent(file: string, fileSize: number) { + if (!this.eventHandler) { + return; + } + + const event: LargeFileReferencedEvent = { + eventName: LargeFileReferencedEvent, + data: { file, fileSize, maxFileSize } + }; + this.eventHandler(event); + } + /* @internal */ delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project: Project) { this.delayUpdateProjectGraph(project); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index a04b7fe5da..f8e94085c4 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2436,6 +2436,27 @@ namespace ts.server.protocol { openFiles: string[]; } + export type LargeFileReferencedEventName = "largeFileReferenced"; + export interface LargeFileReferencedEvent extends Event { + event: LargeFileReferencedEventName; + body: LargeFileReferencedEventBody; + } + + export interface LargeFileReferencedEventBody { + /** + * name of the large file being loaded + */ + file: string; + /** + * size of the file + */ + fileSize: number; + /** + * max file size allowed on the server + */ + maxFileSize: number; + } + /** * Arguments for reload request. */ diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 202b4c02c1..7d18b6ef5e 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -39,7 +39,7 @@ namespace ts.server { */ private pendingReloadFromDisk: boolean; - constructor(private readonly host: ServerHost, private readonly fileName: NormalizedPath, initialVersion?: ScriptInfoVersion) { + constructor(private readonly host: ServerHost, private readonly fileName: NormalizedPath, initialVersion: ScriptInfoVersion | undefined, private readonly info: ScriptInfo) { this.version = initialVersion || { svc: 0, text: 0 }; } @@ -164,9 +164,17 @@ namespace ts.server { private getFileText(tempFileName?: string) { let text: string; - const getText = () => text === undefined ? (text = this.host.readFile(tempFileName || this.fileName) || "") : text; - const size = this.host.getFileSize ? this.host.getFileSize(tempFileName || this.fileName) : getText().length; - return size > maxFileSize ? "" : getText(); + const fileName = tempFileName || this.fileName; + const getText = () => text === undefined ? (text = this.host.readFile(fileName) || "") : text; + const fileSize = this.host.getFileSize ? this.host.getFileSize(fileName) : getText().length; + if (fileSize > maxFileSize) { + Debug.assert(!!this.info.containingProjects.length); + const service = this.info.containingProjects[0].projectService; + service.logger.info(`Skipped loading contents of large file ${fileName} for info ${this.info.fileName}: fileSize: ${fileSize}`); + this.info.containingProjects[0].projectService.sendLargeFileReferencedEvent(fileName, fileSize); + return ""; + } + return getText(); } private switchToScriptVersionCache(): ScriptVersionCache { @@ -248,7 +256,7 @@ namespace ts.server { initialVersion?: ScriptInfoVersion) { this.isDynamic = isDynamicFileName(fileName); - this.textStorage = new TextStorage(host, fileName, initialVersion); + this.textStorage = new TextStorage(host, fileName, initialVersion, this); if (hasMixedContent || this.isDynamic) { this.textStorage.reload(""); this.realpath = this.path; diff --git a/src/server/session.ts b/src/server/session.ts index 2bb87ee25d..8d2fbe9e19 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -559,6 +559,10 @@ namespace ts.server { const { openFiles } = event.data; this.projectsUpdatedInBackgroundEvent(openFiles); break; + case LargeFileReferencedEvent: + const { file, fileSize, maxFileSize } = event.data; + this.event({ file, fileSize, maxFileSize }, "largeFileReferenced"); + break; case ConfigFileDiagEvent: const { triggerFile, configFileName: configFile, diagnostics } = event.data; const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true)); diff --git a/src/testRunner/unittests/textStorage.ts b/src/testRunner/unittests/textStorage.ts index 9938ed106e..5486cf5774 100644 --- a/src/testRunner/unittests/textStorage.ts +++ b/src/testRunner/unittests/textStorage.ts @@ -13,9 +13,9 @@ namespace ts.textStorage { it("text based storage should be have exactly the same as script version cache", () => { const host = projectSystem.createServerHost([f]); - - const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path)); - const ts2 = new server.TextStorage(host, server.asNormalizedPath(f.path)); + // Since script info is not used in these tests, just cheat by passing undefined + const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, /*info*/undefined!); + const ts2 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, /*info*/undefined!); ts1.useScriptVersionCache_TestOnly(); ts2.useText(); @@ -48,7 +48,8 @@ namespace ts.textStorage { it("should switch to script version cache if necessary", () => { const host = projectSystem.createServerHost([f]); - const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path)); + // Since script info is not used in these tests, just cheat by passing undefined + const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path), /*initialVersion*/ undefined, /*info*/undefined!); ts1.getSnapshot(); assert.isTrue(!ts1.hasScriptVersionCache_TestOnly(), "should not have script version cache - 1"); diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 395f4bbc1e..81c62ed808 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -2843,7 +2843,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) { + if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectsUpdatedInBackgroundEvent || e.eventName === server.ProjectInfoTelemetryEvent || e.eventName === server.OpenFileInfoTelemetryEvent || e.eventName === server.LargeFileReferencedEvent) { return; } assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); @@ -9028,6 +9028,27 @@ export const x = 10;` fileSize: server.maxFileSize + 1 }; + function createSessionWithEventHandler(host: TestServerHost) { + const largeFileReferencedEvents: server.LargeFileReferencedEvent[] = []; + const session = createSession(host, { + eventHandler: e => { + if (e.eventName === server.LargeFileReferencedEvent) { + largeFileReferencedEvents.push(e); + } + } + }); + + return { session, verifyLargeFileReferencedEvent }; + + function verifyLargeFileReferencedEvent() { + assert.equal(largeFileReferencedEvents.length, 1); + assert.deepEqual(largeFileReferencedEvents, [{ + eventName: server.LargeFileReferencedEvent, + data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: server.maxFileSize } + }]); + } + } + it("when large file is included by tsconfig", () => { const file: File = { path: `${projectRoot}/src/file.ts`, @@ -9039,13 +9060,15 @@ export const x = 10;` }; const files = [file, largeFile, libFile, tsconfig]; const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(file.path); - service.checkNumberOfProjects({ configuredProjects: 1 }); + const { session, verifyLargeFileReferencedEvent } = createSessionWithEventHandler(host); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { configuredProjects: 1 }); const project = service.configuredProjects.get(tsconfig.path)!; checkProjectActualFiles(project, [file.path, libFile.path, largeFile.path, tsconfig.path]); const info = service.getScriptInfo(largeFile.path)!; assert.equal(info.cacheSourceFile.sourceFile.text, ""); + verifyLargeFileReferencedEvent(); }); it("when large file is included by module resolution", () => { @@ -9055,13 +9078,15 @@ export const x = 10;` }; const files = [file, largeFile, libFile]; const host = createServerHost(files); - const service = createProjectService(host); - service.openClientFile(file.path); - service.checkNumberOfProjects({ inferredProjects: 1 }); + const { session, verifyLargeFileReferencedEvent } = createSessionWithEventHandler(host); + const service = session.getProjectService(); + openFilesForSession([file], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); const project = service.inferredProjects[0]; checkProjectActualFiles(project, [file.path, libFile.path, largeFile.path]); const info = service.getScriptInfo(largeFile.path)!; assert.equal(info.cacheSourceFile.sourceFile.text, ""); + verifyLargeFileReferencedEvent(); }); }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 9409b7ebe8..a16288c6c2 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7660,6 +7660,25 @@ declare namespace ts.server.protocol { */ openFiles: string[]; } + type LargeFileReferencedEventName = "largeFileReferenced"; + interface LargeFileReferencedEvent extends Event { + event: LargeFileReferencedEventName; + body: LargeFileReferencedEventBody; + } + interface LargeFileReferencedEventBody { + /** + * name of the large file being loaded + */ + file: string; + /** + * size of the file + */ + fileSize: number; + /** + * max file size allowed on the server + */ + maxFileSize: number; + } /** * Arguments for reload request. */ @@ -8380,6 +8399,7 @@ declare namespace ts.server { declare namespace ts.server { const maxProgramSizeForNonTsFiles: number; const ProjectsUpdatedInBackgroundEvent = "projectsUpdatedInBackground"; + const LargeFileReferencedEvent = "largeFileReferenced"; const ConfigFileDiagEvent = "configFileDiag"; const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; const ProjectInfoTelemetryEvent = "projectInfo"; @@ -8390,6 +8410,14 @@ declare namespace ts.server { openFiles: string[]; }; } + interface LargeFileReferencedEvent { + eventName: typeof LargeFileReferencedEvent; + data: { + file: string; + fileSize: number; + maxFileSize: number; + }; + } interface ConfigFileDiagEvent { eventName: typeof ConfigFileDiagEvent; data: { @@ -8460,7 +8488,7 @@ declare namespace ts.server { interface OpenFileInfo { readonly checkJs: boolean; } - type ProjectServiceEvent = ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; + type ProjectServiceEvent = LargeFileReferencedEvent | ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent | OpenFileInfoTelemetryEvent; type ProjectServiceEventHandler = (event: ProjectServiceEvent) => void; interface SafeList { [name: string]: {