From 5f9579f3bb21ba85f7acc03a592a2ecb45504f21 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sun, 8 Oct 2023 01:37:07 +0300 Subject: [PATCH 1/5] wip --- src/configurationType.ts | 17 ++++++++++++++++- typescript/src/decorateProxy.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/configurationType.ts b/src/configurationType.ts index 13686c3..36bab3c 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -1,4 +1,4 @@ -import { ScriptElementKind, ScriptKind } from 'typescript/lib/tsserverlibrary' +import { ScriptElementKind, ScriptKind, LanguageService } from 'typescript/lib/tsserverlibrary' type ReplaceRule = { /** @@ -647,6 +647,21 @@ export type Configuration = { typeAlias: string interface: string } + customizeEnabledFeatures: { + [path: string]: + | 'disable-auto-invoked' + | 'disable-heavy-features' + | { + /** @default true */ + [feature in keyof LanguageService]: boolean + } + } + // bigFilesLimitFeatures: 'do-not-limit' | 'limit-auto-invoking' | 'force-limit-all-features' + /** + * in kb default is 1.5mb + * @default 100000 + */ + // bigFilesThreshold: number } // scrapped using search editor. config: caseInsensitive, context lines: 0, regex: const fix\w+ = "[^ ]+" diff --git a/typescript/src/decorateProxy.ts b/typescript/src/decorateProxy.ts index 2d930bd..1754f72 100644 --- a/typescript/src/decorateProxy.ts +++ b/typescript/src/decorateProxy.ts @@ -1,3 +1,4 @@ +import { performance } from 'perf_hooks' import lodashGet from 'lodash.get' import { getCompletionsAtPosition, PrevCompletionMap, PrevCompletionsAdditionalData } from './completionsAtPosition' import { RequestInputTypes, TriggerCharacterCommand } from './ipcTypes' @@ -115,6 +116,31 @@ export const decorateLanguageService = ( } } + const readonlyModeDisableFeatures: Array = [ + 'getOutliningSpans', + 'getSyntacticDiagnostics', + 'getSemanticDiagnostics', + 'getSuggestionDiagnostics', + 'provideInlayHints', + 'getLinkedEditingRangeAtPosition', + 'getApplicableRefactors', + 'getCompletionsAtPosition', + 'getDefinitionAndBoundSpan', + 'getFormattingEditsAfterKeystroke', + 'getDocumentHighlights', + ] + for (const feature of readonlyModeDisableFeatures) { + const orig = proxy[feature] + proxy[feature] = (...args) => { + const start = performance.now() + //@ts-expect-error + const result = orig(...args) + const time = performance.now() - start + if (time > 100) console.log(`[typescript-vscode-plugin perf warning] ${feature} took ${time}ms: ${args[0]} ${args[1]}`) + return result + } + } + languageService[thisPluginMarker] = true return proxy } From 107ee06389bbcc717573cdfb03b7f2309679d08e Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 29 Jan 2024 12:04:38 +0530 Subject: [PATCH 2/5] feat: `customizeEnabledFeatures` to allow disable some TS language service features per file or globally (e.g. completions or diagnostics) feat: print warning in logs if some operation tooks too much time --- buildTsPlugin.mjs | 1 + src/configurationType.ts | 17 ++++++++++++- typescript/src/decorateProxy.ts | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/buildTsPlugin.mjs b/buildTsPlugin.mjs index ff45d7e..d34470b 100644 --- a/buildTsPlugin.mjs +++ b/buildTsPlugin.mjs @@ -30,6 +30,7 @@ const result = await buildTsPlugin('typescript', undefined, undefined, { js: 'let ts, tsFull;', // js: 'const log = (...args) => console.log(...args.map(a => JSON.stringify(a)))', }, + external: ['perf_hooks'], plugins: [ { name: 'watch-notifier', diff --git a/src/configurationType.ts b/src/configurationType.ts index 5101642..2835169 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -1,4 +1,4 @@ -import { ScriptElementKind, ScriptKind } from 'typescript/lib/tsserverlibrary' +import { ScriptElementKind, ScriptKind, LanguageService } from 'typescript/lib/tsserverlibrary' type ReplaceRule = { /** @@ -660,6 +660,21 @@ export type Configuration = { typeAlias: string interface: string } + customizeEnabledFeatures: { + [path: string]: + | 'disable-auto-invoked' + // | 'disable-heavy-features' + | { + /** @default true */ + [feature in keyof LanguageService]: boolean + } + } + // bigFilesLimitFeatures: 'do-not-limit' | 'limit-auto-invoking' | 'force-limit-all-features' + /** + * in kb default is 1.5mb + * @default 100000 + */ + // bigFilesThreshold: number /** @default false */ enableHooksFile: boolean } diff --git a/typescript/src/decorateProxy.ts b/typescript/src/decorateProxy.ts index bb0764d..f2d7900 100644 --- a/typescript/src/decorateProxy.ts +++ b/typescript/src/decorateProxy.ts @@ -139,6 +139,49 @@ export const decorateLanguageService = ( } } + const readonlyModeDisableFeatures: Array = [ + 'getOutliningSpans', + 'getSyntacticDiagnostics', + 'getSemanticDiagnostics', + 'getSuggestionDiagnostics', + 'provideInlayHints', + 'getLinkedEditingRangeAtPosition', + 'getApplicableRefactors', + 'getCompletionsAtPosition', + 'getDefinitionAndBoundSpan', + 'getFormattingEditsAfterKeystroke', + 'getDocumentHighlights', + ] + for (const feature of readonlyModeDisableFeatures) { + const orig = proxy[feature] + proxy[feature] = (...args) => { + const enabledFeaturesSetting = c('customizeEnabledFeatures') ?? {} + const toDisableRaw = + Object.entries(enabledFeaturesSetting).find(([path]) => { + if (typeof args[0] !== 'string') return false + return args[0].includes(path) + })?.[1] ?? + enabledFeaturesSetting['*'] ?? + {} + const toDisable: string[] = + toDisableRaw === 'disable-auto-invoked' + ? // todo + readonlyModeDisableFeatures + : Object.entries(toDisableRaw) + .filter(([, v]) => v === false) + .map(([k]) => k) + if (toDisable.includes(feature)) return undefined + // eslint-disable-next-line @typescript-eslint/no-require-imports + const performance = globalThis.performance ?? require('perf_hooks').performance + const start = performance.now() + //@ts-expect-error + const result = orig(...args) + const time = performance.now() - start + if (time > 100) console.log(`[typescript-vscode-plugin perf warning] ${feature} took ${time}ms: ${args[0]} ${args[1]}`) + return result + } + } + languageService[thisPluginMarker] = true if (!__WEB__ && c('enableHooksFile')) { From 9a2afd967bdc72d53af63554060910cdba20869d Mon Sep 17 00:00:00 2001 From: Ilya Golovin <74474615+Ilanaya@users.noreply.github.com> Date: Mon, 29 Jan 2024 06:53:46 -0800 Subject: [PATCH 3/5] fix: replace throw with console.error (#194) --- typescript/src/decorateProxy.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/typescript/src/decorateProxy.ts b/typescript/src/decorateProxy.ts index f2d7900..5e9a034 100644 --- a/typescript/src/decorateProxy.ts +++ b/typescript/src/decorateProxy.ts @@ -92,9 +92,7 @@ export const decorateLanguageService = ( prevCompletionsAdditionalData = result.prevCompletionsAdditionalData return result.completions } catch (err) { - setTimeout(() => { - throw err as Error - }) + console.error(err) return { entries: [ { From fe70eb301bb4d9a1711eaa58b536c0eb8de3069d Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 29 Jan 2024 20:28:19 +0530 Subject: [PATCH 4/5] should fix test --- src/configurationType.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/configurationType.ts b/src/configurationType.ts index 2835169..eb1b98b 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -660,6 +660,9 @@ export type Configuration = { typeAlias: string interface: string } + /** + * @default {} + */ customizeEnabledFeatures: { [path: string]: | 'disable-auto-invoked' From adee249801def5a4430e6af6edbaadff4ef6d2f9 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Wed, 31 Jan 2024 14:02:00 +0400 Subject: [PATCH 5/5] fix: significantly improve completions performance in heavy applications that use MUI (unoptimized Material-UI) by using cached diagnostics for not declared const variable names suggestions instead --- .eslintrc.json | 3 +- .../src/completions/boostNameSuggestions.ts | 3 +- typescript/src/decorateProxy.ts | 12 +++ typescript/test/completions.spec.ts | 13 +++ typescript/test/testing.ts | 100 ++++++++++-------- 5 files changed, 84 insertions(+), 47 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index de10df0..354f900 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -34,7 +34,8 @@ "@typescript-eslint/consistent-type-imports": "off", "@typescript-eslint/ban-types": "off", "sonarjs/prefer-single-boolean-return": "off", - "unicorn/no-typeof-undefined": "off" // todo disable globally + "unicorn/no-typeof-undefined": "off", // todo disable globally + "@typescript-eslint/consistent-type-definitions": "off" }, "overrides": [ { diff --git a/typescript/src/completions/boostNameSuggestions.ts b/typescript/src/completions/boostNameSuggestions.ts index 2ce3a56..7a919ef 100644 --- a/typescript/src/completions/boostNameSuggestions.ts +++ b/typescript/src/completions/boostNameSuggestions.ts @@ -1,3 +1,4 @@ +import { cachedResponse } from '../decorateProxy' import { boostExistingSuggestions, boostOrAddSuggestions, findChildContainingPosition } from '../utils' import { getCannotFindCodes } from '../utils/cannotFindCodes' @@ -46,7 +47,7 @@ export default ( } if (filterBlock === undefined) return entries - const semanticDiagnostics = languageService.getSemanticDiagnostics(sourceFile.fileName) + const semanticDiagnostics = cachedResponse.getSemanticDiagnostics?.[sourceFile.fileName] ?? [] const notFoundIdentifiers = semanticDiagnostics .filter(({ code }) => cannotFindCodes.includes(code)) diff --git a/typescript/src/decorateProxy.ts b/typescript/src/decorateProxy.ts index 5e9a034..64af965 100644 --- a/typescript/src/decorateProxy.ts +++ b/typescript/src/decorateProxy.ts @@ -33,6 +33,10 @@ export const getInitialProxy = (languageService: ts.LanguageService, proxy = Obj return proxy } +export const cachedResponse = { + getSemanticDiagnostics: {} as Record, +} + export const decorateLanguageService = ( { languageService, languageServiceHost }: PluginCreateArg, existingProxy: ts.LanguageService | undefined, @@ -169,11 +173,19 @@ export const decorateLanguageService = ( .filter(([, v]) => v === false) .map(([k]) => k) if (toDisable.includes(feature)) return undefined + // eslint-disable-next-line @typescript-eslint/no-require-imports const performance = globalThis.performance ?? require('perf_hooks').performance const start = performance.now() + //@ts-expect-error const result = orig(...args) + + if (feature in cachedResponse) { + // todo use weakmap with sourcefiles to ensure it doesn't grow up + cachedResponse[feature][args[0]] = result + } + const time = performance.now() - start if (time > 100) console.log(`[typescript-vscode-plugin perf warning] ${feature} took ${time}ms: ${args[0]} ${args[1]}`) return result diff --git a/typescript/test/completions.spec.ts b/typescript/test/completions.spec.ts index 6abcd2b..9e95a59 100644 --- a/typescript/test/completions.spec.ts +++ b/typescript/test/completions.spec.ts @@ -40,6 +40,19 @@ test('Banned positions', () => { expect(getCompletionsAtPosition(cursorPositions[2]!)?.entries).toHaveLength(1) }) +test.todo('Const name suggestions (boostNameSuggestions)', () => { + const tester = fourslashLikeTester(/* ts */ ` + const /*0*/ = 5 + testVariable + `) + languageService.getSemanticDiagnostics(entrypoint) + tester.completion(0, { + includes: { + names: ['testVariable'], + }, + }) +}) + test('Banned positions for all method snippets', () => { const cursorPositions = newFileContents(/* tsx */ ` import {/*|*/} from 'test' diff --git a/typescript/test/testing.ts b/typescript/test/testing.ts index 8faad6c..d7ecce8 100644 --- a/typescript/test/testing.ts +++ b/typescript/test/testing.ts @@ -27,7 +27,7 @@ interface CodeActionMatcher { const { languageService, languageServiceHost, updateProject, getCurrentFile } = sharedLanguageService -const fakeProxy = {} as Pick +export const fakeProxy = {} as Pick codeActionsDecorateProxy(fakeProxy as typeof languageService, languageService, languageServiceHost, defaultConfigFunc) @@ -82,7 +82,7 @@ export const fourslashLikeTester = (contents: string, fileName = entrypoint, { d const ranges = positive.reduce( (prevRanges, pos) => { - const lastPrev = prevRanges[prevRanges.length - 1]! + const lastPrev = prevRanges.at(-1)! if (lastPrev.length < 2) { lastPrev.push(pos) return prevRanges @@ -92,58 +92,68 @@ export const fourslashLikeTester = (contents: string, fileName = entrypoint, { d [[]], ) return { - completion: (marker: number | number[], matcher: CompletionMatcher, meta?) => { - for (const mark of Array.isArray(marker) ? marker : [marker]) { - if (numberedPositions[mark] === undefined) throw new Error(`No marker ${mark} found`) - const result = getCompletionsAtPosition(numberedPositions[mark]!, { shouldHave: true })! - const message = ` at marker ${mark}` - const { exact, includes, excludes } = matcher - if (exact) { - const { names, all, insertTexts } = exact - if (names) { - expect(result?.entryNames, message).toEqual(names) - } - if (insertTexts) { - expect( - result.entries.map(entry => entry.insertText), - message, - ).toEqual(insertTexts) - } - if (all) { - for (const entry of result.entries) { - expect(entry, entry.name + message).toContain(all) - } - } - } - if (includes) { - const { names, all, insertTexts } = includes - if (names) { - for (const name of names) { - expect(result?.entryNames, message).toContain(name) + completion(marker: number | number[], matcher: CompletionMatcher, meta?) { + const oldGetSemanticDiagnostics = languageService.getSemanticDiagnostics + languageService.getSemanticDiagnostics = () => { + throw new Error('getSemanticDiagnostics should not be called because of performance reasons') + // return [] + } + + try { + for (const mark of Array.isArray(marker) ? marker : [marker]) { + if (numberedPositions[mark] === undefined) throw new Error(`No marker ${mark} found`) + const result = getCompletionsAtPosition(numberedPositions[mark]!, { shouldHave: true })! + const message = ` at marker ${mark}` + const { exact, includes, excludes } = matcher + if (exact) { + const { names, all, insertTexts } = exact + if (names) { + expect(result?.entryNames, message).toEqual(names) } - } - if (insertTexts) { - for (const insertText of insertTexts) { + if (insertTexts) { expect( result.entries.map(entry => entry.insertText), message, - ).toContain(insertText) + ).toEqual(insertTexts) + } + if (all) { + for (const entry of result.entries) { + expect(entry, entry.name + message).toContain(all) + } } } - if (all) { - for (const entry of result.entries.filter(e => names?.includes(e.name))) { - expect(entry, entry.name + message).toContain(all) + if (includes) { + const { names, all, insertTexts } = includes + if (names) { + for (const name of names) { + expect(result?.entryNames, message).toContain(name) + } + } + if (insertTexts) { + for (const insertText of insertTexts) { + expect( + result.entries.map(entry => entry.insertText), + message, + ).toContain(insertText) + } + } + if (all) { + for (const entry of result.entries.filter(e => names?.includes(e.name))) { + expect(entry, entry.name + message).toContain(all) + } } } - } - if (excludes) { - for (const exclude of excludes) { - expect(result?.entryNames, message).not.toContain(exclude) + if (excludes) { + for (const exclude of excludes) { + expect(result?.entryNames, message).not.toContain(exclude) + } } } + } finally { + languageService.getSemanticDiagnostics = oldGetSemanticDiagnostics } }, - codeAction: (marker: number | number[], matcher: CodeActionMatcher, meta?, { compareContent = false } = {}) => { + codeAction(marker: number | number[], matcher: CodeActionMatcher, meta?, { compareContent = false } = {}) { for (const mark of Array.isArray(marker) ? marker : [marker]) { if (!ranges[mark]) throw new Error(`No range with index ${mark} found, highest index is ${ranges.length - 1}`) const start = ranges[mark]![0]! @@ -192,10 +202,10 @@ export const fileContentsSpecialPositions = (contents: string, fileName = entryp let mainMatch = currentMatch[1]! if (addOnly) mainMatch = mainMatch.slice(0, -1) const possiblyNum = +mainMatch - if (!Number.isNaN(possiblyNum)) { - addArr[2][possiblyNum] = offset - } else { + if (Number.isNaN(possiblyNum)) { addArr[mainMatch === 't' ? '0' : '1'].push(offset) + } else { + addArr[2][possiblyNum] = offset } replacement.lastIndex -= matchLength }