Skip to content

Commit f63c372

Browse files
committed
feat: add in keyword completions
Don't see a reason too add a setting to disable them. There are awesome!
1 parent 4c27a0f commit f63c372

File tree

5 files changed

+175
-2
lines changed

5 files changed

+175
-2
lines changed

typescript/src/completionsAtPosition.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import _ from 'lodash'
22
import type tslib from 'typescript/lib/tsserverlibrary'
3+
import inKeywordCompletions from './inKeywordCompletions'
34
import * as emmet from '@vscode/emmet-helper'
45
import isInBannedPosition from './isInBannedPosition'
56
import { GetConfig } from './types'
@@ -165,6 +166,17 @@ export const getCompletionsAtPosition = (
165166
return !banAutoImportPackages.includes(text)
166167
})
167168

169+
const result = inKeywordCompletions(position, fileName, node, sourceFile, program, languageService, ts)
170+
if (result) {
171+
prior.entries.push(...result.completions)
172+
Object.assign(
173+
prevCompletionsMap,
174+
_.mapValues(result.docPerCompletion, value => ({
175+
documentationOverride: value,
176+
})),
177+
)
178+
}
179+
168180
if (c('suggestions.keywordsInsertText') === 'space') {
169181
const charAhead = scriptSnapshot.getText(position, position + 1)
170182
const bannedKeywords = [
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type tslib from 'typescript/lib/tsserverlibrary'
2+
3+
export default (
4+
position: number,
5+
fileName: string,
6+
node: tslib.Node | undefined,
7+
sourceFile: tslib.SourceFile,
8+
program: tslib.Program,
9+
languageService: tslib.LanguageService,
10+
ts: typeof tslib,
11+
) => {
12+
if (!node) return
13+
function isBinaryExpression(node: ts.Node): node is ts.BinaryExpression {
14+
return node.kind === ts.SyntaxKind.BinaryExpression
15+
}
16+
const typeChecker = program.getTypeChecker()
17+
// TODO info diagnostic if used that doesn't exist
18+
if (
19+
ts.isStringLiteralLike(node) &&
20+
isBinaryExpression(node.parent) &&
21+
node.parent.left === node &&
22+
node.parent.operatorToken.kind === ts.SyntaxKind.InKeyword
23+
) {
24+
const quote = node.getText()[0]!
25+
const type = typeChecker.getTypeAtLocation(node.parent.right)
26+
const suggestions: Record<
27+
string,
28+
{
29+
insertText: string
30+
usingDisplayIndexes: number[]
31+
documentations: string[]
32+
}
33+
> = {}
34+
const unitedTypes = type.isUnion() ? type.types : [type]
35+
for (let [typeIndex, localType] of unitedTypes.entries()) {
36+
// TODO set deprecated tag
37+
for (let prop of localType.getProperties()) {
38+
const { name } = prop
39+
if (!suggestions[name])
40+
suggestions[name] = {
41+
insertText: prop.name.replace(quote, `\\${quote}`),
42+
usingDisplayIndexes: [],
43+
documentations: [],
44+
}
45+
suggestions[name]!.usingDisplayIndexes.push(typeIndex + 1)
46+
// const doc = prop.getDocumentationComment(typeChecker)
47+
const declaration = prop.getDeclarations()?.[0]
48+
suggestions[name]!.documentations.push(
49+
`${typeIndex + 1}: ${declaration && typeChecker.typeToString(typeChecker.getTypeAtLocation(declaration))}`,
50+
)
51+
}
52+
}
53+
const docPerCompletion: Record<string, string> = {}
54+
const completions: ts.CompletionEntry[] = Object.entries(suggestions)
55+
.map(([originaName, { insertText, usingDisplayIndexes, documentations }], i) => {
56+
const name = unitedTypes.length > 1 && usingDisplayIndexes.length === 1 ? `☆${originaName}` : originaName
57+
docPerCompletion[name] = documentations.join('\n\n')
58+
return {
59+
// ⚀ ⚁ ⚂ ⚃ ⚄ ⚅
60+
name: name,
61+
kind: ts.ScriptElementKind.string,
62+
insertText,
63+
sourceDisplay: [{ kind: 'text', text: usingDisplayIndexes.join(', ') }],
64+
sortText: `${usingDisplayIndexes.length}_${i}`,
65+
}
66+
})
67+
.sort((a, b) => a.sortText.localeCompare(b.sortText))
68+
return {
69+
completions,
70+
// make lazy?
71+
docPerCompletion,
72+
}
73+
}
74+
return
75+
}

typescript/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t
6666
name: entryName,
6767
kind: ts.ScriptElementKind.alias,
6868
kindModifiers: '',
69-
displayParts: typeof documentationOverride === 'string' ? [{ kind: 'text', text: documentationOverride }] : documentationOverride,
69+
displayParts: [],
70+
documentation: typeof documentationOverride === 'string' ? [{ kind: 'text', text: documentationOverride }] : documentationOverride,
7071
}
7172
}
7273
const prior = info.languageService.getCompletionEntryDetails(

typescript/src/typeCompletions.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type tslib from 'typescript/lib/tsserverlibrary'
2+
3+
export default (
4+
position: number,
5+
fileName: string,
6+
node: tslib.Node | undefined,
7+
sourceFile: tslib.SourceFile,
8+
program: tslib.Program,
9+
languageService: tslib.LanguageService,
10+
ts: typeof tslib,
11+
) => {
12+
if (!node) return
13+
}
14+
15+
// spec isnt strict as well
16+
export const notStrictStringCompletion = (entry: tslib.CompletionEntry): tslib.CompletionEntry => ({
17+
...entry,
18+
name: `◯${entry.name}`,
19+
insertText: entry.insertText ?? entry.name,
20+
})

typescript/test/completions.spec.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { pickObj } from '@zardoy/utils'
12
import { createLanguageService } from '../src/dummyLanguageService'
23
import { getCompletionsAtPosition as getCompletionsAtPositionRaw } from '../src/completionsAtPosition'
34
import type {} from 'vitest/globals'
@@ -26,9 +27,10 @@ const newFileContents = (contents: string, fileName = entrypoint) => {
2627
//@ts-ignore
2728
const defaultConfigFunc = await getDefaultConfigFunc()
2829

29-
const getCompletionsAtPosition = (pos: number, fileName = entrypoint) => {
30+
const getCompletionsAtPosition = (pos: number, { fileName = entrypoint, shouldHave }: { fileName?: string; shouldHave?: boolean } = {}) => {
3031
if (pos === undefined) throw new Error('getCompletionsAtPosition: pos is undefined')
3132
const result = getCompletionsAtPositionRaw(fileName, pos, {}, defaultConfigFunc, languageService, ts.ScriptSnapshot.fromString(files[entrypoint]), ts)
33+
if (shouldHave) expect(result).not.toBeUndefined()
3234
if (!result) return
3335
return {
3436
...result,
@@ -59,3 +61,66 @@ test.skip('Remove Useless Function Props', () => {
5961
console.log(entryNames)
6062
// expect(entryNames).not.includes('bind')
6163
})
64+
65+
test('In Keyword Completions', () => {
66+
const [pos] = newFileContents(/* ts */ `
67+
const a: { a: boolean, b: string } | { a: number, c: number } = {} as any
68+
if ('/*|*/' in a) {}
69+
`)
70+
const completion = pickObj(getCompletionsAtPosition(pos, { shouldHave: true })!, 'entries', 'prevCompletionsMap')
71+
expect(completion).toMatchInlineSnapshot(`
72+
{
73+
"entries": [
74+
{
75+
"insertText": "b",
76+
"kind": "string",
77+
"name": "☆b",
78+
"sortText": "1_10",
79+
"sourceDisplay": [
80+
{
81+
"kind": "text",
82+
"text": "1",
83+
},
84+
],
85+
},
86+
{
87+
"insertText": "c",
88+
"kind": "string",
89+
"name": "☆c",
90+
"sortText": "1_21",
91+
"sourceDisplay": [
92+
{
93+
"kind": "text",
94+
"text": "2",
95+
},
96+
],
97+
},
98+
{
99+
"insertText": "a",
100+
"kind": "string",
101+
"name": "a",
102+
"sortText": "2_02",
103+
"sourceDisplay": [
104+
{
105+
"kind": "text",
106+
"text": "1, 2",
107+
},
108+
],
109+
},
110+
],
111+
"prevCompletionsMap": {
112+
"a": {
113+
"documentationOverride": "1: boolean
114+
115+
2: number",
116+
},
117+
"☆b": {
118+
"documentationOverride": "1: string",
119+
},
120+
"☆c": {
121+
"documentationOverride": "2: number",
122+
},
123+
},
124+
}
125+
`)
126+
})

0 commit comments

Comments
 (0)