Skip to content

Commit c92d568

Browse files
committed
feat: object literal completions for just true or false types
fix(object-literal-completions): use symbol completion knowledge. In other words starting from TS 5 now object literal completions should work in absolutely any location for any supported completion
1 parent 2716279 commit c92d568

File tree

3 files changed

+74
-37
lines changed

3 files changed

+74
-37
lines changed

typescript/src/completions/objectLiteralCompletions.ts

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GetConfig } from '../types'
2-
import { getFullTypeChecker } from '../utils'
2+
import { getFullTypeChecker, isTs5 } from '../utils'
33
import { sharedCompletionContext } from './sharedContext'
44

55
export default (
@@ -24,12 +24,20 @@ export default (
2424
entries = [...entries]
2525
const typeChecker = languageService.getProgram()!.getTypeChecker()!
2626
const objType = typeChecker.getContextualType(node)
27-
if (!objType) return
28-
const properties = getAllPropertiesOfType(objType, typeChecker)
29-
for (const property of properties) {
30-
const entry = entries.find(({ name }) => name === property.name)
31-
if (!entry) continue
32-
const type = typeChecker.getTypeOfSymbolAtLocation(property, node)
27+
let oldProperties: ts.Symbol[] | undefined
28+
if (!isTs5()) {
29+
if (!objType) return
30+
oldProperties = getAllPropertiesOfType(objType, typeChecker)
31+
}
32+
for (const entry of entries) {
33+
let type: ts.Type | undefined
34+
if (!isTs5()) {
35+
const property = oldProperties!.find(property => property.name === entry.name)
36+
if (!property) continue
37+
type = typeChecker.getTypeOfSymbolAtLocation(property, node)
38+
} else if (entry.symbol) {
39+
type = typeChecker.getTypeOfSymbol(entry.symbol)
40+
}
3341
if (!type) continue
3442
if (isFunctionType(type, typeChecker)) {
3543
if (['above', 'remove'].includes(keepOriginal) && preferences.includeCompletionsWithObjectLiteralMethodSnippets) {
@@ -52,14 +60,16 @@ export default (
5260
return [`: ${quote}$1${quote},$0`, `: ${quote}${quote},`]
5361
}
5462
const insertObjectArrayInnerText = c('objectLiteralCompletions.insertNewLine') ? '\n\t$1\n' : '$1'
63+
const booleanCompletion = getBooleanCompletion(type, typeChecker)
5564
const completingStyleMap = [
5665
[getQuotedSnippet, isStringCompletion],
57-
[[': ${1|true,false|},$0', `: true/false,`], isBooleanCompletion],
66+
[[`: ${booleanCompletion?.[0] ?? ''},`, `: ${booleanCompletion?.[0] ?? ''}`], () => booleanCompletion?.length === 1],
67+
[[': ${1|true,false|},$0', `: true/false,`], () => booleanCompletion?.length === 2],
5868
[[`: [${insertObjectArrayInnerText}],$0`, `: [],`], isArrayCompletion],
5969
[[`: {${insertObjectArrayInnerText}},$0`, `: {},`], isObjectCompletion],
6070
] as const
6171
const fallbackSnippet = c('objectLiteralCompletions.fallbackVariant') ? ([': $0,', ': ,'] as const) : undefined
62-
const insertSnippetVariant = completingStyleMap.find(([, detector]) => detector(type, typeChecker))?.[0] ?? fallbackSnippet
72+
const insertSnippetVariant = completingStyleMap.find(([, detector]) => detector(type!, typeChecker))?.[0] ?? fallbackSnippet
6373
if (!insertSnippetVariant) continue
6474
const [insertSnippetText, insertSnippetPreview] = typeof insertSnippetVariant === 'function' ? insertSnippetVariant() : insertSnippetVariant
6575
const insertText = entry.name + insertSnippetText
@@ -104,31 +114,36 @@ const isStringCompletion = (type: ts.Type) => {
104114
return false
105115
}
106116

107-
const isBooleanCompletion = (type: ts.Type, checker: ts.TypeChecker) => {
108-
if (type.flags & ts.TypeFlags.Undefined) return false
117+
const getBooleanCompletion = (type: ts.Type, checker: ts.TypeChecker) => {
118+
if (type.flags & ts.TypeFlags.Undefined) return
109119
// todo support boolean literals (boolean like)
110-
if (type.flags & ts.TypeFlags.Boolean) return true
111120
const trueType = getFullTypeChecker(checker).getTrueType() as any
112121
const falseType = getFullTypeChecker(checker).getFalseType() as any
113-
let seenTrueType = false
114-
let seenFalseType = false
115-
if (type.isUnion()) {
116-
const match = isEverySubtype(type, type => {
117-
if (type.flags & ts.TypeFlags.Boolean) return true
118-
if (type === trueType) {
119-
seenTrueType = true
120-
return true
121-
}
122-
if (type === falseType) {
123-
seenFalseType = true
124-
return true
125-
}
126-
return false
127-
})
128-
if (seenFalseType !== seenTrueType) return false
129-
return match
122+
const seenTypes = new Set<string>()
123+
if (type.flags & ts.TypeFlags.Boolean) {
124+
seenTypes.add('true')
125+
seenTypes.add('false')
130126
}
131-
return false
127+
const match = isEverySubtype({ types: type.isUnion() ? type.types : [type] } as any, type => {
128+
if (type.flags & ts.TypeFlags.Boolean) {
129+
seenTypes.add('true')
130+
seenTypes.add('false')
131+
return true
132+
}
133+
if (type === trueType) {
134+
seenTypes.add('true')
135+
return true
136+
}
137+
if (type === falseType) {
138+
seenTypes.add('false')
139+
return true
140+
}
141+
return false
142+
})
143+
if (!match) return
144+
145+
if (seenTypes.size === 0) return
146+
return [...seenTypes.keys()]
132147
}
133148

134149
const isArrayCompletion = (type: ts.Type, checker: ts.TypeChecker) => {

typescript/src/constructMethodSnippet.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default (
88
languageService: ts.LanguageService,
99
sourceFile: ts.SourceFile,
1010
position: number,
11-
symbol: ts.Symbol,
11+
symbol: ts.Symbol | /*for easier testing*/ undefined,
1212
c: GetConfig,
1313
// acceptAmbiguous: boolean,
1414
resolveData: {
@@ -19,7 +19,7 @@ export default (
1919
if (!containerNode || isTypeNode(containerNode)) return
2020

2121
const checker = languageService.getProgram()!.getTypeChecker()!
22-
const type = checker.getTypeOfSymbol(symbol)
22+
const type = symbol ? checker.getTypeOfSymbol(symbol) : checker.getTypeAtLocation(containerNode)
2323

2424
if (ts.isIdentifier(containerNode)) containerNode = containerNode.parent
2525
if (ts.isPropertyAccessExpression(containerNode)) containerNode = containerNode.parent

typescript/test/completions.spec.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,12 @@ test('Function props: cleans & highlights', () => {
109109
const compareMethodSnippetAgainstMarker = (inputMarkers: number[], marker: number, expected: string | null | string[]) => {
110110
const obj = Object.fromEntries(inputMarkers.entries())
111111
const markerPos = obj[marker]!
112-
const methodSnippet = constructMethodSnippet(languageService, getSourceFile(), markerPos, defaultConfigFunc, false)
113-
if (methodSnippet === 'ambiguous') {
114-
expect(methodSnippet).toEqual(expected)
112+
const resolvedData = {
113+
isAmbiguous: false,
114+
}
115+
const methodSnippet = constructMethodSnippet(languageService, getSourceFile(), markerPos, undefined, defaultConfigFunc, resolvedData)
116+
if (resolvedData.isAmbiguous) {
117+
expect('ambiguous').toEqual(expected)
115118
return
116119
}
117120
const snippetToInsert = methodSnippet ? `(${methodSnippet.join(', ')})` : null
@@ -531,13 +534,15 @@ test('Additional types suggestions', () => {
531534
})
532535
})
533536

534-
test('Object Literal Completions', () => {
537+
test.only('Object Literal Completions', () => {
535538
const [_positivePositions, _negativePositions, numPositions] = fileContentsSpecialPositions(/* ts */ `
536539
interface Options {
537540
usedOption
538541
mood?: 'happy' | 'sad'
539542
callback?()
540543
additionalOptions?: {
544+
bar: boolean
545+
bar2: false
541546
foo?: boolean
542547
}
543548
plugins: Array<{ name: string, setup(build) }>
@@ -551,11 +556,18 @@ test('Object Literal Completions', () => {
551556
})
552557
553558
const somethingWithUntions: { a: string } | { a: any[], b: string } = {/*2*/}
559+
560+
makeDay({
561+
additionalOptions: {
562+
/*3*/
563+
}
564+
})
554565
`)
555566
const { entriesSorted: pos1 } = getCompletionsAtPosition(numPositions[1]!)!
556567
const { entriesSorted: pos2 } = getCompletionsAtPosition(numPositions[2]!)!
568+
const { entriesSorted: pos3 } = getCompletionsAtPosition(numPositions[3]!)!
557569
// todo resolve sorting problem + add tests with other keepOriginal (it was tested manually)
558-
for (const entry of [...pos1, ...pos2]) {
570+
for (const entry of [...pos1, ...pos2, ...pos3]) {
559571
entry.insertText = entry.insertText?.replaceAll('\n', '\\n')
560572
}
561573
expect(pos1).toMatchInlineSnapshot(/* json */ `
@@ -634,6 +646,16 @@ test('Object Literal Completions', () => {
634646
"b: \\"$1\\",$0",
635647
]
636648
`)
649+
expect(pos3.map(x => x.insertText)).toMatchInlineSnapshot(`
650+
[
651+
"bar",
652+
"bar: \${1|true,false|},$0",
653+
"bar2",
654+
"bar2: false,",
655+
"foo",
656+
"foo: \${1|true,false|},$0",
657+
]
658+
`)
637659
})
638660

639661
test('Extract to type / interface name inference', () => {

0 commit comments

Comments
 (0)