Skip to content

Commit 65a27ae

Browse files
committed
feat: string template type completions!
e.g. `const a: `foo_${string}_bar` = '/*now completes!*/'`
1 parent b8fa504 commit 65a27ae

File tree

4 files changed

+89
-3
lines changed

4 files changed

+89
-3
lines changed

typescript/src/completions/filterJsxComponents.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export default (entries: ts.CompletionEntry[], node: ts.Node, position: number,
108108
const entryType = typeChecker.getTypeOfSymbolAtLocation(symbol, node)
109109
typeAtLocLog[entry.name] = nowGetter.now() - mark
110110
addMark('getTypeAtLocation')
111-
// todo setting to allow any!
111+
// todo setting to allow any?
112112
if (entryType.flags & ts.TypeFlags.Any) return false
113113
// startMark()
114114
const signatures = typeChecker.getSignaturesOfType(entryType, ts.SignatureKind.Call)
@@ -123,10 +123,10 @@ export default (entries: ts.CompletionEntry[], node: ts.Node, position: number,
123123
const newEntries = entries.filter(entry => {
124124
// if (!entry.name[0] || entry.name[0].toLowerCase() === entry.name[0]) return false
125125
if (entry.kind === ts.ScriptElementKind.keyword) return false
126-
// todo
126+
// todo?
127127
if (c('jsxImproveElementsSuggestions.filterNamespaces') && entry.kind === ts.ScriptElementKind.moduleElement) return false
128128
if (!c('experiments.excludeNonJsxCompletions')) return true
129-
// todo!!!
129+
// I'm not inrested personally
130130
if (entry.kind === ts.ScriptElementKind.classElement) return false
131131
if (entry.kind === ts.ScriptElementKind.localClassElement) return false
132132
if (!interestedKinds.includes(entry.kind)) return true
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { compact } from '@zardoy/utils'
2+
import { sharedCompletionContext } from './sharedContext'
3+
4+
export default (): ts.CompletionEntry[] | void => {
5+
const { program } = sharedCompletionContext
6+
let { node } = sharedCompletionContext
7+
if (!node || !ts.isStringLiteralLike(node)) return
8+
const checker = program.getTypeChecker()!
9+
let type: ts.Type
10+
let objType: ts.Type | undefined
11+
if (ts.isElementAccessExpression(node.parent)) {
12+
objType = checker.getTypeAtLocation(node.parent.expression)
13+
} else if (ts.isPropertyAssignment(node.parent) && ts.isObjectLiteralExpression(node.parent.parent)) {
14+
objType = checker.getContextualType(node.parent.parent) ?? checker.getTypeAtLocation(node.parent.parent)
15+
}
16+
if (objType) {
17+
const [indexInfo] = checker.getIndexInfosOfType(objType)
18+
if (indexInfo) {
19+
type = indexInfo.keyType
20+
}
21+
}
22+
type ??= checker.getContextualType(node) ?? checker.getTypeAtLocation(node)
23+
const types = type.isUnion() ? type.types : [type]
24+
if (types.some(type => type.flags & ts.TypeFlags.TemplateLiteral)) {
25+
return compact(
26+
types.map(type => {
27+
if (!(type.flags & ts.TypeFlags.TemplateLiteral)) return
28+
29+
const {
30+
texts: [head, ...spans],
31+
} = type as ts.TemplateLiteralType
32+
const texts = [head!, ...spans.flatMap(span => (span === '' ? [''] : ['', span]))]
33+
let tabStop = 1
34+
return {
35+
kind: ts.ScriptElementKind.string,
36+
name: texts.map(text => (text === '' ? '|' : text)).join(''),
37+
sortText: '07',
38+
insertText: texts.map(text => (text === '' ? `$${tabStop++}` : text.replace(/\$/g, '\\$'))).join(''),
39+
isSnippet: true,
40+
}
41+
}),
42+
)
43+
}
44+
45+
return
46+
}

typescript/src/completionsAtPosition.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import displayImportedInfo from './completions/displayImportedInfo'
2929
import changeKindToFunction from './completions/changeKindToFunction'
3030
import functionPropsAndMethods from './completions/functionPropsAndMethods'
3131
import { getTupleSignature } from './tupleSignature'
32+
import stringTemplateTypeCompletions from './completions/stringTemplateType'
3233

3334
export type PrevCompletionMap = Record<
3435
string,
@@ -147,6 +148,13 @@ export const getCompletionsAtPosition = (
147148
if (newEntries?.length && ensurePrior() && prior) prior.entries = newEntries
148149
}
149150

151+
if (!prior?.entries.length) {
152+
const addStringTemplateTypeCompletions = stringTemplateTypeCompletions()
153+
if (addStringTemplateTypeCompletions && ensurePrior() && prior) {
154+
prior.entries = [...prior.entries, ...addStringTemplateTypeCompletions]
155+
}
156+
}
157+
150158
if (!prior) return
151159

152160
if (c('tupleHelpSignature') && node) {

typescript/test/completions.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,38 @@ test('Array Method Snippets', () => {
258258
}
259259
})
260260

261+
test('String template type completions', () => {
262+
const tester = fourslashLikeTester(/* ts */ `
263+
const a: \`v\${'b' | 'c'}.\${number}.\${number}\` = '/*1*/';
264+
265+
const b: {
266+
[a: \`foo_\${string}\`]: string
267+
} = {
268+
'foo_': '/*2*/'
269+
}
270+
271+
const c = (p: typeof b) => { }
272+
273+
c({
274+
'/*3*/'
275+
})
276+
277+
b['/*4*/']
278+
`)
279+
280+
tester.completion(1, {
281+
exact: {
282+
names: ['vb.|.|', 'vc.|.|'],
283+
},
284+
})
285+
286+
tester.completion([2, 3, 4], {
287+
exact: {
288+
names: ['foo_|'],
289+
},
290+
})
291+
})
292+
261293
test('Switch Case Exclude Covered', () => {
262294
const [, _, numPositions] = fileContentsSpecialPositions(/*ts*/ `
263295
let test: 'foo' | 'bar'

0 commit comments

Comments
 (0)