diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index c90f21ae5..80ae3b864 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -19,6 +19,7 @@ export class LSAndTSDocResolver { * @param docManager * @param workspaceUris * @param configManager + * @param notifyExceedSizeLimit * @param isSvelteCheck True, if used in the context of svelte-check * @param tsconfigPath This should only be set via svelte-check. Makes sure all documents are resolved to that tsconfig. Has to be absolute. */ @@ -26,6 +27,7 @@ export class LSAndTSDocResolver { private readonly docManager: DocumentManager, private readonly workspaceUris: string[], private readonly configManager: LSConfigManager, + private readonly notifyExceedSizeLimit?: () => void, private readonly isSvelteCheck = false, private readonly tsconfigPath?: string ) { @@ -69,7 +71,8 @@ export class LSAndTSDocResolver { ambientTypesSource: this.isSvelteCheck ? 'svelte-check' : 'svelte2tsx', createDocument: this.createDocument, transformOnTemplateError: !this.isSvelteCheck, - globalSnapshotsManager: this.globalSnapshotsManager + globalSnapshotsManager: this.globalSnapshotsManager, + notifyExceedSizeLimit: this.notifyExceedSizeLimit }; } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index bc1222782..dfa2f0ea7 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -13,7 +13,7 @@ import { ignoredBuildDirectories, SnapshotManager } from './SnapshotManager'; -import { ensureRealSvelteFilePath, findTsConfigPath } from './utils'; +import { ensureRealSvelteFilePath, findTsConfigPath, hasTsExtensions } from './utils'; export interface LanguageServiceContainer { readonly tsconfigPath: string; @@ -39,13 +39,16 @@ export interface LanguageServiceContainer { fileBelongsToProject(filePath: string): boolean; } +const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; // 20 MB const services = new Map>(); +const serviceSizeMap: Map = new Map(); export interface LanguageServiceDocumentContext { ambientTypesSource: string; transformOnTemplateError: boolean; createDocument: (fileName: string, content: string) => Document; globalSnapshotsManager: GlobalSnapshotsManager; + notifyExceedSizeLimit: (() => void) | undefined; } export async function getService( @@ -123,12 +126,14 @@ async function createLanguageService( './svelte-native-jsx.d.ts' ].map((f) => ts.sys.resolvePath(resolve(svelteTsPath, f))); + let languageServiceReducedMode = false; + const host: ts.LanguageServiceHost = { getCompilationSettings: () => compilerOptions, getScriptFileNames: () => Array.from( new Set([ - ...snapshotManager.getProjectFileNames(), + ...(languageServiceReducedMode ? [] : snapshotManager.getProjectFileNames()), ...snapshotManager.getFileNames(), ...svelteTsxFiles ]) @@ -150,6 +155,8 @@ async function createLanguageService( transformOnTemplateError: docContext.transformOnTemplateError }; + reduceLanguageServiceCapabilityIfFileSizeTooBig(); + return { tsconfigPath, compilerOptions, @@ -232,7 +239,15 @@ async function createLanguageService( } function updateProjectFiles(): void { + const projectFileCountBefore = snapshotManager.getProjectFileNames().length; snapshotManager.updateProjectFiles(); + const projectFileCountAfter = snapshotManager.getProjectFileNames().length; + + if (projectFileCountAfter <= projectFileCountBefore) { + return; + } + + reduceLanguageServiceCapabilityIfFileSizeTooBig(); } function hasFile(filePath: string): boolean { @@ -350,4 +365,68 @@ async function createLanguageService( function getDefaultExclude() { return ['node_modules', ...ignoredBuildDirectories]; } + + /** + * Disable usage of project files. + * running language service in a reduced mode for + * large projects with improperly excluded tsconfig. + */ + function reduceLanguageServiceCapabilityIfFileSizeTooBig() { + if (exceedsTotalSizeLimitForNonTsFiles(compilerOptions, tsconfigPath, snapshotManager)) { + languageService.cleanupSemanticCache(); + languageServiceReducedMode = true; + docContext.notifyExceedSizeLimit?.(); + } + } +} + +/** + * adopted from https://github.com/microsoft/TypeScript/blob/3c8e45b304b8572094c5d7fbb9cd768dbf6417c0/src/server/editorServices.ts#L1955 + */ +function exceedsTotalSizeLimitForNonTsFiles( + compilerOptions: ts.CompilerOptions, + tsconfigPath: string, + snapshotManager: SnapshotManager +): boolean { + if (compilerOptions.disableSizeLimit) { + return false; + } + + let availableSpace = maxProgramSizeForNonTsFiles; + serviceSizeMap.set(tsconfigPath, 0); + + serviceSizeMap.forEach((size) => { + availableSpace -= size; + }); + + let totalNonTsFileSize = 0; + + const fileNames = snapshotManager.getProjectFileNames(); + for (const fileName of fileNames) { + if (hasTsExtensions(fileName)) { + continue; + } + + totalNonTsFileSize += ts.sys.getFileSize?.(fileName) ?? 0; + + if (totalNonTsFileSize > availableSpace) { + const top5LargestFiles = fileNames + .filter((name) => !hasTsExtensions(name)) + .map((name) => ({ name, size: ts.sys.getFileSize?.(name) ?? 0 })) + .sort((a, b) => b.size - a.size) + .slice(0, 5); + + Logger.log( + `Non TS file size exceeded limit (${totalNonTsFileSize}). ` + + `Largest files: ${top5LargestFiles + .map((file) => `${file.name}:${file.size}`) + .join(', ')}` + ); + + return true; + } + } + + serviceSizeMap.set(tsconfigPath, totalNonTsFileSize); + return false; } diff --git a/packages/language-server/src/plugins/typescript/utils.ts b/packages/language-server/src/plugins/typescript/utils.ts index c6c120ef3..7a24fa9e8 100644 --- a/packages/language-server/src/plugins/typescript/utils.ts +++ b/packages/language-server/src/plugins/typescript/utils.ts @@ -313,3 +313,11 @@ export function getDiagnosticTag(diagnostic: ts.Diagnostic): DiagnosticTag[] { } return tags; } + +export function hasTsExtensions(fileName: string) { + return ( + fileName.endsWith(ts.Extension.Dts) || + fileName.endsWith(ts.Extension.Tsx) || + fileName.endsWith(ts.Extension.Ts) + ); +} diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 140d38e8c..b6aa6467d 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -145,7 +145,12 @@ export function startServer(options?: LSOptions) { pluginHost.register( new TypeScriptPlugin( configManager, - new LSAndTSDocResolver(docManager, workspaceUris.map(normalizeUri), configManager) + new LSAndTSDocResolver( + docManager, + workspaceUris.map(normalizeUri), + configManager, + notifyTsServiceExceedSizeLimit + ) ) ); @@ -241,6 +246,16 @@ export function startServer(options?: LSOptions) { }; }); + function notifyTsServiceExceedSizeLimit() { + connection?.sendNotification(ShowMessageNotification.type, { + message: + 'Svelte language server detected a large amount of JS/Svelte files. ' + + 'To enable project-wide JavaScript/TypeScript language features for Svelte files,' + + 'exclude large folders in the tsconfig.json or jsconfig.json with source files that you do not work on.', + type: MessageType.Warning + }); + } + connection.onExit(() => { watcher?.dispose(); }); diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts index 042418eb0..0671ed728 100644 --- a/packages/language-server/src/svelte-check.ts +++ b/packages/language-server/src/svelte-check.ts @@ -64,6 +64,7 @@ export class SvelteCheck { this.docManager, [pathToUrl(workspacePath)], this.configManager, + undefined, true, options.tsconfig );