Skip to content

Commit 9312b04

Browse files
committed
feat: restore properties sorting for JSX component props
microsoft/TypeScript#52080
1 parent e953448 commit 9312b04

File tree

2 files changed

+40
-19
lines changed

2 files changed

+40
-19
lines changed

typescript/src/completions/fixPropertiesSorting.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,38 @@ export default (entries: ts.CompletionEntry[]) => {
77
const { node, program, c } = sharedCompletionContext
88
if (!c('fixSuggestionsSorting')) return
99
if (!node) return
10-
// if (ts.isObjectLiteralExpression(node) && ts.isCallExpression(node.parent)) {
11-
// const typeChecker = program.getTypeChecker()
12-
// const type = typeChecker.getTypeAtLocation(node.parent)
13-
// const callSignatures = type.getCallSignatures()
14-
// }
15-
let rightNode: ts.Node | undefined
16-
const upperNode = ts.isIdentifier(node) ? node.parent : node
17-
if (ts.isObjectLiteralExpression(node)) rightNode = node
18-
if (ts.isPropertyAccessExpression(upperNode)) rightNode = upperNode.expression
10+
let targetNode: ts.Node | undefined
11+
let upperNode = ts.isIdentifier(node) ? node.parent : node
12+
if (ts.isJsxAttributes(upperNode)) upperNode = upperNode.parent
13+
if (ts.isObjectLiteralExpression(node)) targetNode = node
14+
const isJsxElem = ts.isJsxOpeningElement(upperNode) || ts.isJsxSelfClosingElement(upperNode)
15+
if (isJsxElem) {
16+
targetNode = upperNode
17+
} else if (ts.isPropertyAccessExpression(upperNode)) targetNode = upperNode.expression
1918
else if (ts.isObjectBindingPattern(node)) {
2019
if (ts.isVariableDeclaration(node.parent)) {
2120
const { initializer } = node.parent
2221
if (initializer) {
23-
if (ts.isIdentifier(initializer)) rightNode = initializer
24-
if (ts.isPropertyAccessExpression(initializer)) rightNode = initializer.name
22+
if (ts.isIdentifier(initializer)) targetNode = initializer
23+
if (ts.isPropertyAccessExpression(initializer)) targetNode = initializer.name
2524
}
2625
}
27-
if (ts.isParameter(node.parent)) rightNode = node.parent.type
26+
if (ts.isParameter(node.parent)) targetNode = node.parent.type
2827
} else if (ts.isObjectLiteralExpression(node) && ts.isReturnStatement(node.parent) && ts.isArrowFunction(node.parent.parent.parent)) {
29-
rightNode = node.parent.parent.parent.type
28+
targetNode = node.parent.parent.parent.type
3029
}
31-
if (!rightNode) return
30+
if (!targetNode) return
3231
const typeChecker = program.getTypeChecker()
33-
const type = typeChecker.getContextualType(rightNode as ts.Expression) ?? typeChecker.getTypeAtLocation(rightNode)
34-
const sourceProps = getAllPropertiesOfType(type, typeChecker)?.map(({ name }) => name)
32+
let sourceProps: string[]
33+
if (isJsxElem) {
34+
const type = typeChecker.getContextualType((node as ts.JsxOpeningElement).attributes)
35+
if (!type) return
36+
// usually component own props defined first like interface Props extends ... {} or type A = Props & ..., but this is not a case with mui...
37+
sourceProps = (type.isIntersection() ? type.types.flatMap(type => type.getProperties()) : type.getProperties()).map(symbol => symbol.name)
38+
} else {
39+
const type = typeChecker.getContextualType(targetNode as ts.Expression) ?? typeChecker.getTypeAtLocation(targetNode)
40+
sourceProps = getAllPropertiesOfType(type, typeChecker)?.map(({ name }) => name)
41+
}
3542
// languageService.getSignatureHelpItems(fileName, position, {}))
3643
if (!sourceProps) return
3744
// const entriesBySortText = groupBy(({ sortText }) => sortText, entries)
@@ -42,11 +49,17 @@ export default (entries: ts.CompletionEntry[]) => {
4249
// if sortText first symbol is not a number, than most probably it was highlighted by IntelliCode, keep them high
4350
const [sortableEntries, notSortableEntries] = partition(entry => !isNaN(parseInt(entry.sortText)), interestedEntries)
4451
const lowestSortText = Math.min(...sortableEntries.map(({ sortText }) => parseInt(sortText)))
52+
const getScore = (completion: ts.CompletionEntry) => {
53+
return (
54+
sourceProps.indexOf(completion.name) +
55+
(isJsxElem && (completion['symbol'] as ts.Symbol | undefined)?.declarations?.[0]?.getSourceFile().fileName.includes('@types/react') ? 10_000 : 0)
56+
)
57+
}
4558
// make sorted
4659
const sortedEntries = sortableEntries
4760
.sort((a, b) => {
48-
return sourceProps.indexOf(a.name) - sourceProps.indexOf(b.name)
61+
return getScore(a) - getScore(b)
4962
})
50-
.map((entry, i) => ({ ...entry, sortText: String(lowestSortText + i) }))
63+
.map((entry, i) => ({ ...entry /* sortText: String(lowestSortText + i) */ }))
5164
return [...notSortableEntries, ...sortedEntries, ...notInterestedEntries]
5265
}

typescript/test/completions.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ test('Case-sensetive completions', () => {
313313

314314
test('Fix properties sorting', () => {
315315
settingsOverride.fixSuggestionsSorting = true
316-
const tester = fourslashLikeTester(/* ts */ `
316+
const tester = fourslashLikeTester(/* tsx */ `
317317
let a: {
318318
d
319319
b(a: {c, a}): {c, a}
@@ -325,6 +325,9 @@ test('Fix properties sorting', () => {
325325
a./*1*/;
326326
a.b({/*2*/})./*3*/
327327
}
328+
329+
declare function MyComponent(props: { b?; c? } & { a? }): JSX.Element
330+
<MyComponent /*4*/ />;
328331
`)
329332
tester.completion(1, {
330333
exact: {
@@ -336,6 +339,11 @@ test('Fix properties sorting', () => {
336339
names: ['c', 'b'],
337340
},
338341
})
342+
tester.completion(4, {
343+
exact: {
344+
names: ['b', 'c', 'a'],
345+
},
346+
})
339347
settingsOverride.fixSuggestionsSorting = false
340348
})
341349

0 commit comments

Comments
 (0)