Skip to content

Commit 9d4a010

Browse files
authored
Merge pull request #193 from zardoy/develop
Important release
2 parents 88f69aa + adee249 commit 9d4a010

File tree

8 files changed

+156
-52
lines changed

8 files changed

+156
-52
lines changed

.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"@typescript-eslint/consistent-type-imports": "off",
3535
"@typescript-eslint/ban-types": "off",
3636
"sonarjs/prefer-single-boolean-return": "off",
37-
"unicorn/no-typeof-undefined": "off" // todo disable globally
37+
"unicorn/no-typeof-undefined": "off", // todo disable globally
38+
"@typescript-eslint/consistent-type-definitions": "off"
3839
},
3940
"overrides": [
4041
{

README.MD

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# TypeScript Essential Plugins
22

3+
**Use next-Gen TypeScript features in VSCode today!**
4+
5+
No-AI smart predictable completions that efficiently reuses the world of types!
6+
7+
- Stays fast
8+
- No internet connection required
9+
310
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!
411
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.
512

@@ -16,7 +23,7 @@ We make completions more informative. Definitions, references (and sometimes eve
1623
- [Rename Features](#rename-features)
1724
- [Even Even More](#even-even-more)
1825

19-
> *Note*: You can disable all optional features with `> Disable All Optional Features` command right after install.
26+
> *Note*: You can disable all optional features with `> Disable All Optional Features` command just after installing.
2027
>
2128
> *Note*: Visit website for list of recommended settings: <https://ts-plugin.zardoy.com/>
2229

buildTsPlugin.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const result = await buildTsPlugin('typescript', undefined, undefined, {
3030
js: 'let ts, tsFull;',
3131
// js: 'const log = (...args) => console.log(...args.map(a => JSON.stringify(a)))',
3232
},
33+
external: ['perf_hooks'],
3334
plugins: [
3435
{
3536
name: 'watch-notifier',

src/configurationType.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ScriptElementKind, ScriptKind } from 'typescript/lib/tsserverlibrary'
1+
import { ScriptElementKind, ScriptKind, LanguageService } from 'typescript/lib/tsserverlibrary'
22

33
type ReplaceRule = {
44
/**
@@ -660,6 +660,24 @@ export type Configuration = {
660660
typeAlias: string
661661
interface: string
662662
}
663+
/**
664+
* @default {}
665+
*/
666+
customizeEnabledFeatures: {
667+
[path: string]:
668+
| 'disable-auto-invoked'
669+
// | 'disable-heavy-features'
670+
| {
671+
/** @default true */
672+
[feature in keyof LanguageService]: boolean
673+
}
674+
}
675+
// bigFilesLimitFeatures: 'do-not-limit' | 'limit-auto-invoking' | 'force-limit-all-features'
676+
/**
677+
* in kb default is 1.5mb
678+
* @default 100000
679+
*/
680+
// bigFilesThreshold: number
663681
/** @default false */
664682
enableHooksFile: boolean
665683
}

typescript/src/completions/boostNameSuggestions.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cachedResponse } from '../decorateProxy'
12
import { boostExistingSuggestions, boostOrAddSuggestions, findChildContainingPosition } from '../utils'
23
import { getCannotFindCodes } from '../utils/cannotFindCodes'
34

@@ -46,7 +47,7 @@ export default (
4647
}
4748

4849
if (filterBlock === undefined) return entries
49-
const semanticDiagnostics = languageService.getSemanticDiagnostics(sourceFile.fileName)
50+
const semanticDiagnostics = cachedResponse.getSemanticDiagnostics?.[sourceFile.fileName] ?? []
5051

5152
const notFoundIdentifiers = semanticDiagnostics
5253
.filter(({ code }) => cannotFindCodes.includes(code))

typescript/src/decorateProxy.ts

+56-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export const getInitialProxy = (languageService: ts.LanguageService, proxy = Obj
3333
return proxy
3434
}
3535

36+
export const cachedResponse = {
37+
getSemanticDiagnostics: {} as Record<string, ts.Diagnostic[]>,
38+
}
39+
3640
export const decorateLanguageService = (
3741
{ languageService, languageServiceHost }: PluginCreateArg,
3842
existingProxy: ts.LanguageService | undefined,
@@ -92,9 +96,7 @@ export const decorateLanguageService = (
9296
prevCompletionsAdditionalData = result.prevCompletionsAdditionalData
9397
return result.completions
9498
} catch (err) {
95-
setTimeout(() => {
96-
throw err as Error
97-
})
99+
console.error(err)
98100
return {
99101
entries: [
100102
{
@@ -139,6 +141,57 @@ export const decorateLanguageService = (
139141
}
140142
}
141143

144+
const readonlyModeDisableFeatures: Array<keyof ts.LanguageService> = [
145+
'getOutliningSpans',
146+
'getSyntacticDiagnostics',
147+
'getSemanticDiagnostics',
148+
'getSuggestionDiagnostics',
149+
'provideInlayHints',
150+
'getLinkedEditingRangeAtPosition',
151+
'getApplicableRefactors',
152+
'getCompletionsAtPosition',
153+
'getDefinitionAndBoundSpan',
154+
'getFormattingEditsAfterKeystroke',
155+
'getDocumentHighlights',
156+
]
157+
for (const feature of readonlyModeDisableFeatures) {
158+
const orig = proxy[feature]
159+
proxy[feature] = (...args) => {
160+
const enabledFeaturesSetting = c('customizeEnabledFeatures') ?? {}
161+
const toDisableRaw =
162+
Object.entries(enabledFeaturesSetting).find(([path]) => {
163+
if (typeof args[0] !== 'string') return false
164+
return args[0].includes(path)
165+
})?.[1] ??
166+
enabledFeaturesSetting['*'] ??
167+
{}
168+
const toDisable: string[] =
169+
toDisableRaw === 'disable-auto-invoked'
170+
? // todo
171+
readonlyModeDisableFeatures
172+
: Object.entries(toDisableRaw)
173+
.filter(([, v]) => v === false)
174+
.map(([k]) => k)
175+
if (toDisable.includes(feature)) return undefined
176+
177+
// eslint-disable-next-line @typescript-eslint/no-require-imports
178+
const performance = globalThis.performance ?? require('perf_hooks').performance
179+
const start = performance.now()
180+
181+
//@ts-expect-error
182+
const result = orig(...args)
183+
184+
if (feature in cachedResponse) {
185+
// todo use weakmap with sourcefiles to ensure it doesn't grow up
186+
cachedResponse[feature][args[0]] = result
187+
}
188+
189+
const time = performance.now() - start
190+
if (time > 100) console.log(`[typescript-vscode-plugin perf warning] ${feature} took ${time}ms: ${args[0]} ${args[1]}`)
191+
return result
192+
}
193+
}
194+
142195
languageService[thisPluginMarker] = true
143196

144197
if (!__WEB__ && c('enableHooksFile')) {

typescript/test/completions.spec.ts

+13
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ test('Banned positions', () => {
4040
expect(getCompletionsAtPosition(cursorPositions[2]!)?.entries).toHaveLength(1)
4141
})
4242

43+
test.todo('Const name suggestions (boostNameSuggestions)', () => {
44+
const tester = fourslashLikeTester(/* ts */ `
45+
const /*0*/ = 5
46+
testVariable
47+
`)
48+
languageService.getSemanticDiagnostics(entrypoint)
49+
tester.completion(0, {
50+
includes: {
51+
names: ['testVariable'],
52+
},
53+
})
54+
})
55+
4356
test('Banned positions for all method snippets', () => {
4457
const cursorPositions = newFileContents(/* tsx */ `
4558
import {/*|*/} from 'test'

typescript/test/testing.ts

+55-45
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface CodeActionMatcher {
2727

2828
const { languageService, languageServiceHost, updateProject, getCurrentFile } = sharedLanguageService
2929

30-
const fakeProxy = {} as Pick<typeof languageService, 'getApplicableRefactors' | 'getEditsForRefactor'>
30+
export const fakeProxy = {} as Pick<typeof languageService, 'getApplicableRefactors' | 'getEditsForRefactor'>
3131

3232
codeActionsDecorateProxy(fakeProxy as typeof languageService, languageService, languageServiceHost, defaultConfigFunc)
3333

@@ -82,7 +82,7 @@ export const fourslashLikeTester = (contents: string, fileName = entrypoint, { d
8282

8383
const ranges = positive.reduce<number[][]>(
8484
(prevRanges, pos) => {
85-
const lastPrev = prevRanges[prevRanges.length - 1]!
85+
const lastPrev = prevRanges.at(-1)!
8686
if (lastPrev.length < 2) {
8787
lastPrev.push(pos)
8888
return prevRanges
@@ -92,58 +92,68 @@ export const fourslashLikeTester = (contents: string, fileName = entrypoint, { d
9292
[[]],
9393
)
9494
return {
95-
completion: (marker: number | number[], matcher: CompletionMatcher, meta?) => {
96-
for (const mark of Array.isArray(marker) ? marker : [marker]) {
97-
if (numberedPositions[mark] === undefined) throw new Error(`No marker ${mark} found`)
98-
const result = getCompletionsAtPosition(numberedPositions[mark]!, { shouldHave: true })!
99-
const message = ` at marker ${mark}`
100-
const { exact, includes, excludes } = matcher
101-
if (exact) {
102-
const { names, all, insertTexts } = exact
103-
if (names) {
104-
expect(result?.entryNames, message).toEqual(names)
105-
}
106-
if (insertTexts) {
107-
expect(
108-
result.entries.map(entry => entry.insertText),
109-
message,
110-
).toEqual(insertTexts)
111-
}
112-
if (all) {
113-
for (const entry of result.entries) {
114-
expect(entry, entry.name + message).toContain(all)
115-
}
116-
}
117-
}
118-
if (includes) {
119-
const { names, all, insertTexts } = includes
120-
if (names) {
121-
for (const name of names) {
122-
expect(result?.entryNames, message).toContain(name)
95+
completion(marker: number | number[], matcher: CompletionMatcher, meta?) {
96+
const oldGetSemanticDiagnostics = languageService.getSemanticDiagnostics
97+
languageService.getSemanticDiagnostics = () => {
98+
throw new Error('getSemanticDiagnostics should not be called because of performance reasons')
99+
// return []
100+
}
101+
102+
try {
103+
for (const mark of Array.isArray(marker) ? marker : [marker]) {
104+
if (numberedPositions[mark] === undefined) throw new Error(`No marker ${mark} found`)
105+
const result = getCompletionsAtPosition(numberedPositions[mark]!, { shouldHave: true })!
106+
const message = ` at marker ${mark}`
107+
const { exact, includes, excludes } = matcher
108+
if (exact) {
109+
const { names, all, insertTexts } = exact
110+
if (names) {
111+
expect(result?.entryNames, message).toEqual(names)
123112
}
124-
}
125-
if (insertTexts) {
126-
for (const insertText of insertTexts) {
113+
if (insertTexts) {
127114
expect(
128115
result.entries.map(entry => entry.insertText),
129116
message,
130-
).toContain(insertText)
117+
).toEqual(insertTexts)
118+
}
119+
if (all) {
120+
for (const entry of result.entries) {
121+
expect(entry, entry.name + message).toContain(all)
122+
}
131123
}
132124
}
133-
if (all) {
134-
for (const entry of result.entries.filter(e => names?.includes(e.name))) {
135-
expect(entry, entry.name + message).toContain(all)
125+
if (includes) {
126+
const { names, all, insertTexts } = includes
127+
if (names) {
128+
for (const name of names) {
129+
expect(result?.entryNames, message).toContain(name)
130+
}
131+
}
132+
if (insertTexts) {
133+
for (const insertText of insertTexts) {
134+
expect(
135+
result.entries.map(entry => entry.insertText),
136+
message,
137+
).toContain(insertText)
138+
}
139+
}
140+
if (all) {
141+
for (const entry of result.entries.filter(e => names?.includes(e.name))) {
142+
expect(entry, entry.name + message).toContain(all)
143+
}
136144
}
137145
}
138-
}
139-
if (excludes) {
140-
for (const exclude of excludes) {
141-
expect(result?.entryNames, message).not.toContain(exclude)
146+
if (excludes) {
147+
for (const exclude of excludes) {
148+
expect(result?.entryNames, message).not.toContain(exclude)
149+
}
142150
}
143151
}
152+
} finally {
153+
languageService.getSemanticDiagnostics = oldGetSemanticDiagnostics
144154
}
145155
},
146-
codeAction: (marker: number | number[], matcher: CodeActionMatcher, meta?, { compareContent = false } = {}) => {
156+
codeAction(marker: number | number[], matcher: CodeActionMatcher, meta?, { compareContent = false } = {}) {
147157
for (const mark of Array.isArray(marker) ? marker : [marker]) {
148158
if (!ranges[mark]) throw new Error(`No range with index ${mark} found, highest index is ${ranges.length - 1}`)
149159
const start = ranges[mark]![0]!
@@ -192,10 +202,10 @@ export const fileContentsSpecialPositions = (contents: string, fileName = entryp
192202
let mainMatch = currentMatch[1]!
193203
if (addOnly) mainMatch = mainMatch.slice(0, -1)
194204
const possiblyNum = +mainMatch
195-
if (!Number.isNaN(possiblyNum)) {
196-
addArr[2][possiblyNum] = offset
197-
} else {
205+
if (Number.isNaN(possiblyNum)) {
198206
addArr[mainMatch === 't' ? '0' : '1'].push(offset)
207+
} else {
208+
addArr[2][possiblyNum] = offset
199209
}
200210
replacement.lastIndex -= matchLength
201211
}

0 commit comments

Comments
 (0)