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/README.MD b/README.MD
index 9e54d85..03275bd 100644
--- a/README.MD
+++ b/README.MD
@@ -1,5 +1,12 @@
# TypeScript Essential Plugins
+**Use next-Gen TypeScript features in VSCode today!**
+
+No-AI smart predictable completions that efficiently reuses the world of types!
+
+- Stays fast
+- No internet connection required
+
Feature-complete TypeScript plugin that improves every single builtin feature such as completions, definitions, references and so on, and also adds even new TypeScript killer features, so you can work with large codebases faster!
We make completions more informative. Definitions, references (and sometimes even completions) less noisy. And finally our main goal is to provide most customizable TypeScript experience for IDE features.
@@ -16,7 +23,7 @@ We make completions more informative. Definitions, references (and sometimes eve
- [Rename Features](#rename-features)
- [Even Even More](#even-even-more)
-> *Note*: You can disable all optional features with `> Disable All Optional Features` command right after install.
+> *Note*: You can disable all optional features with `> Disable All Optional Features` command just after installing.
>
> *Note*: Visit website for list of recommended settings:
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..eb1b98b 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,24 @@ export type Configuration = {
typeAlias: string
interface: string
}
+ /**
+ * @default {}
+ */
+ 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/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 bb0764d..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,
@@ -92,9 +96,7 @@ export const decorateLanguageService = (
prevCompletionsAdditionalData = result.prevCompletionsAdditionalData
return result.completions
} catch (err) {
- setTimeout(() => {
- throw err as Error
- })
+ console.error(err)
return {
entries: [
{
@@ -139,6 +141,57 @@ 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)
+
+ 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
+ }
+ }
+
languageService[thisPluginMarker] = true
if (!__WEB__ && c('enableHooksFile')) {
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
}