Skip to content

Commit 2e31ec2

Browse files
committed
feat: signature for tuple types! (disabled by default, but recommended)
1 parent 2cd6a12 commit 2e31ec2

File tree

6 files changed

+142
-7
lines changed

6 files changed

+142
-7
lines changed

src/configurationType.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,17 @@ export type Configuration = {
224224
* @default false
225225
*/
226226
'jsxEmmet.dotOverride': string | false
227+
/**
228+
* Wether to provide signature help when destructuring tuples e.g.:
229+
*
230+
* ```ts
231+
* declare const foo: [x: number, y: number]
232+
* const [] = foo
233+
* ```
234+
* @recommended
235+
* @default false
236+
*/
237+
tupleHelpSignature: boolean
227238
/**
228239
* We already change sorting of suggestions, but enabling this option will also make:
229240
* - removing `id` from input suggestions

typescript/src/completionEntryDetails.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function completionEntryDetails(
2323
displayParts: typeof documentationOverride === 'string' ? [{ kind: 'text', text: documentationOverride }] : documentationOverride,
2424
}
2525
}
26-
const prior = languageService.getCompletionEntryDetails(
26+
let prior = languageService.getCompletionEntryDetails(
2727
fileName,
2828
position,
2929
prevCompletionsMap[entryName]?.originalName || entryName,
@@ -32,6 +32,15 @@ export default function completionEntryDetails(
3232
preferences,
3333
data,
3434
)
35+
if (detailPrepend) {
36+
prior ??= {
37+
name: entryName,
38+
kind: ts.ScriptElementKind.alias,
39+
kindModifiers: '',
40+
displayParts: [],
41+
}
42+
prior.displayParts = [{ kind: 'text', text: detailPrepend }, ...prior.displayParts]
43+
}
3544
if (!prior) return
3645
if (source) {
3746
const namespaceImport = namespaceAutoImports(
@@ -62,9 +71,6 @@ export default function completionEntryDetails(
6271
]
6372
}
6473
}
65-
if (detailPrepend) {
66-
prior.displayParts = [{ kind: 'text', text: detailPrepend }, ...prior.displayParts]
67-
}
6874
if (documentationAppend) {
6975
prior.documentation = [...(prior.documentation ?? []), { kind: 'text', text: documentationAppend }]
7076
}

typescript/src/completionsAtPosition.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { sharedCompletionContext } from './completions/sharedContext'
2828
import displayImportedInfo from './completions/displayImportedInfo'
2929
import changeKindToFunction from './completions/changeKindToFunction'
3030
import functionPropsAndMethods from './completions/functionPropsAndMethods'
31+
import { getTupleSignature } from './tupleSignature'
3132

3233
export type PrevCompletionMap = Record<
3334
string,
@@ -106,6 +107,7 @@ export const getCompletionsAtPosition = (
106107
}
107108
const hasSuggestions = prior && prior.entries.filter(({ kind }) => kind !== ts.ScriptElementKind.warning).length !== 0
108109
const node = findChildContainingPosition(ts, sourceFile, position)
110+
109111
/** node that is one character behind
110112
* useful as in most cases we work with node that is behind the cursor */
111113
const leftNode = findChildContainingPosition(ts, sourceFile, position - 1)
@@ -147,6 +149,24 @@ export const getCompletionsAtPosition = (
147149

148150
if (!prior) return
149151

152+
if (c('tupleHelpSignature') && node) {
153+
const tupleSignature = getTupleSignature(node, program.getTypeChecker()!)
154+
if (tupleSignature) {
155+
const { currentHasLabel, currentMember, tupleMembers } = tupleSignature
156+
const tupleCurrentItem = tupleMembers[currentMember]
157+
if (currentHasLabel && tupleCurrentItem) {
158+
const name = tupleCurrentItem.split(':', 1)[0]!
159+
prior.entries.push({
160+
name,
161+
kind: ts.ScriptElementKind.warning,
162+
sortText: '07',
163+
})
164+
prevCompletionsMap[name] ??= {}
165+
prevCompletionsMap[name]!.detailPrepend = `[${currentMember}]: ${tupleCurrentItem.slice(tupleCurrentItem.indexOf(':') + 2)}`
166+
}
167+
}
168+
}
169+
150170
if (c('caseSensitiveCompletions')) {
151171
const fullText = sourceFile.getFullText()
152172
const currentWord = fullText.slice(0, position).match(/[\w\d]+$/)

typescript/src/decorateProxy.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { GetConfig } from './types'
1313
import lodashGet from 'lodash.get'
1414
import decorateWorkspaceSymbolSearch from './workspaceSymbolSearch'
1515
import decorateFormatFeatures from './decorateFormatFeatures'
16-
import namespaceAutoImports from './namespaceAutoImports'
1716
import libDomPatching from './libDomPatching'
1817
import decorateSignatureHelp from './decorateSignatureHelp'
1918

typescript/src/decorateSignatureHelp.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,49 @@
1+
import { getTupleSignature } from './tupleSignature'
12
import { GetConfig } from './types'
23
import { findChildContainingExactPosition } from './utils'
34

45
export default (proxy: ts.LanguageService, languageService: ts.LanguageService, languageServiceHost: ts.LanguageServiceHost, c: GetConfig) => {
56
proxy.getSignatureHelpItems = (fileName, position, options) => {
7+
const program = languageService.getProgram()!
8+
const sourceFile = program.getSourceFile(fileName)!
9+
let node: ts.Node | undefined
10+
11+
if (c('tupleHelpSignature')) {
12+
node ??= findChildContainingExactPosition(sourceFile, position)
13+
const tupleSignature = node && getTupleSignature(node, program.getTypeChecker()!)
14+
if (tupleSignature && node) {
15+
const { tupleMembers, currentMember } = tupleSignature
16+
const nodeStart = node.getLeadingTriviaWidth() + node.pos
17+
return {
18+
argumentCount: tupleMembers.length,
19+
argumentIndex: currentMember,
20+
selectedItemIndex: 0,
21+
applicableSpan: ts.createTextSpanFromBounds(nodeStart, node.end),
22+
items: [
23+
{
24+
isVariadic: false,
25+
tags: [],
26+
prefixDisplayParts: [{ kind: 'text', text: '[' }],
27+
suffixDisplayParts: [{ kind: 'text', text: ']' }],
28+
documentation: [],
29+
separatorDisplayParts: [{ kind: 'text', text: ', ' }],
30+
parameters: tupleMembers.map(tupleMember => ({
31+
name: '',
32+
displayParts: [{ kind: 'text', text: tupleMember }],
33+
documentation: [],
34+
isOptional: false,
35+
})),
36+
},
37+
] as ts.SignatureHelpItem[],
38+
} as ts.SignatureHelpItems
39+
}
40+
}
41+
642
if (!c('signatureHelp.excludeBlockScope') || options?.triggerReason?.kind !== 'invoked') {
743
return languageService.getSignatureHelpItems(fileName, position, options)
844
}
945

10-
const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!
11-
const node = findChildContainingExactPosition(sourceFile, position)
46+
node ??= findChildContainingExactPosition(sourceFile, position)
1247
const returnStatement =
1348
ts.findAncestor(node, node => {
1449
return ts.isBlock(node) ? 'quit' : ts.isReturnStatement(node)

typescript/src/tupleSignature.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { compact } from '@zardoy/utils'
2+
3+
export const getTupleSignature = (node: ts.Node, typeChecker: ts.TypeChecker) => {
4+
const originalNode = node
5+
6+
let i = -1
7+
let targetNode: ts.Node | undefined
8+
9+
const setIndex = (elements: ts.NodeArray<any>) => {
10+
i = elements.indexOf(originalNode)
11+
if (i === -1) i = elements.length
12+
}
13+
14+
if (ts.isIdentifier(node)) node = node.parent
15+
if (ts.isOmittedExpression(node)) node = node.parent
16+
if (ts.isArrayBindingPattern(node)) {
17+
if (node.parent.name === node) {
18+
targetNode = node
19+
setIndex(node.parent.name.elements)
20+
}
21+
// if (ts.isVariableDeclaration(node.parent) && ts.isVariableDeclarationList(node.parent.parent) && ts.isForOfStatement(node.parent.parent.parent)) {
22+
// const expr = node.parent.parent.parent.expression
23+
// targetNode = expr
24+
// setIndex((node.parent.name as ts.ArrayBindingPattern).elements)
25+
// }
26+
// if (ts.isVariableDeclaration(node.parent) && node.parent.initializer) {
27+
// targetNode = node.parent.initializer
28+
// setIndex((node.parent.name as ts.ArrayBindingPattern).elements)
29+
// }
30+
// if (ts.isParameter(node.parent)) {
31+
// targetNode = node.parent
32+
// setIndex((node.parent.name as ts.ArrayBindingPattern).elements)
33+
// }
34+
} else if (ts.isArrayLiteralExpression(node)) {
35+
if (ts.isBinaryExpression(node.parent) && node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
36+
targetNode = node.parent.left
37+
setIndex(node.elements)
38+
}
39+
}
40+
41+
if (!targetNode) return
42+
43+
const type = typeChecker.getTypeAtLocation(targetNode)
44+
const properties = type.getProperties()
45+
// simple detect that not a tuple
46+
if (!properties.some(property => property.name === '0')) return
47+
const currentMember = properties.findIndex(property => property.name === i.toString())
48+
let currentHasLabel = false
49+
const tupleMembers = compact(
50+
properties.map((property, i) => {
51+
if (!property.name.match(/^\d+$/)) return
52+
const type = typeChecker.getTypeOfSymbolAtLocation(property, targetNode!)
53+
let displayString = typeChecker.typeToString(type)
54+
const tupleLabelDeclaration: ts.NamedTupleMember | undefined = property['target']?.['tupleLabelDeclaration']
55+
const tupleLabel = tupleLabelDeclaration?.name.text
56+
if (tupleLabel) {
57+
displayString = `${tupleLabel}: ${displayString}`
58+
if (i === currentMember) currentHasLabel = true
59+
}
60+
return displayString
61+
}),
62+
)
63+
return { tupleMembers, currentMember, currentHasLabel }
64+
}

0 commit comments

Comments
 (0)