Skip to content

Commit 16bbddb

Browse files
authored
Rewrite logic for JSX attribute completion detection (#47412)
1 parent b7fee7f commit 16bbddb

7 files changed

+343
-5
lines changed

src/services/completions.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ namespace ts.Completions {
456456
isJsxInitializer,
457457
isTypeOnlyLocation,
458458
isJsxIdentifierExpected,
459+
isRightOfOpenTag,
459460
importCompletionNode,
460461
insideJsDocTagTypeExpression,
461462
symbolToSortTextIdMap,
@@ -495,7 +496,9 @@ namespace ts.Completions {
495496
importCompletionNode,
496497
recommendedCompletion,
497498
symbolToOriginInfoMap,
498-
symbolToSortTextIdMap
499+
symbolToSortTextIdMap,
500+
isJsxIdentifierExpected,
501+
isRightOfOpenTag,
499502
);
500503
getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries); // TODO: GH#18217
501504
}
@@ -526,7 +529,9 @@ namespace ts.Completions {
526529
importCompletionNode,
527530
recommendedCompletion,
528531
symbolToOriginInfoMap,
529-
symbolToSortTextIdMap
532+
symbolToSortTextIdMap,
533+
isJsxIdentifierExpected,
534+
isRightOfOpenTag,
530535
);
531536
}
532537

@@ -669,6 +674,8 @@ namespace ts.Completions {
669674
preferences: UserPreferences,
670675
completionKind: CompletionKind,
671676
formatContext: formatting.FormatContext | undefined,
677+
isJsxIdentifierExpected: boolean | undefined,
678+
isRightOfOpenTag: boolean | undefined,
672679
): CompletionEntry | undefined {
673680
let insertText: string | undefined;
674681
let replacementSpan = getReplacementSpanForContextToken(replacementToken);
@@ -744,8 +751,7 @@ namespace ts.Completions {
744751
}
745752
}
746753

747-
const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
748-
if (kind === ScriptElementKind.jsxAttribute && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") {
754+
if (isJsxIdentifierExpected && !isRightOfOpenTag && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") {
749755
let useBraces = preferences.jsxAttributeCompletionStyle === "braces";
750756
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, location);
751757

@@ -790,7 +796,7 @@ namespace ts.Completions {
790796
// entries (like JavaScript identifier entries).
791797
return {
792798
name,
793-
kind,
799+
kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location),
794800
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
795801
sortText,
796802
source,
@@ -1186,6 +1192,8 @@ namespace ts.Completions {
11861192
recommendedCompletion?: Symbol,
11871193
symbolToOriginInfoMap?: SymbolOriginInfoMap,
11881194
symbolToSortTextIdMap?: SymbolSortTextIdMap,
1195+
isJsxIdentifierExpected?: boolean,
1196+
isRightOfOpenTag?: boolean,
11891197
): UniqueNameSet {
11901198
const start = timestamp();
11911199
const variableDeclaration = getVariableDeclaration(location);
@@ -1228,6 +1236,8 @@ namespace ts.Completions {
12281236
preferences,
12291237
kind,
12301238
formatContext,
1239+
isJsxIdentifierExpected,
1240+
isRightOfOpenTag,
12311241
);
12321242
if (!entry) {
12331243
continue;
@@ -1580,6 +1590,7 @@ namespace ts.Completions {
15801590
readonly isTypeOnlyLocation: boolean;
15811591
/** In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. */
15821592
readonly isJsxIdentifierExpected: boolean;
1593+
readonly isRightOfOpenTag: boolean;
15831594
readonly importCompletionNode?: Node;
15841595
readonly hasUnresolvedAutoImports?: boolean;
15851596
}
@@ -1987,6 +1998,7 @@ namespace ts.Completions {
19871998
symbolToSortTextIdMap,
19881999
isTypeOnlyLocation,
19892000
isJsxIdentifierExpected,
2001+
isRightOfOpenTag,
19902002
importCompletionNode,
19912003
hasUnresolvedAutoImports,
19922004
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/// <reference path="fourslash.ts" />
2+
//@Filename: file.tsx
3+
////interface NestedInterface {
4+
//// Foo: NestedInterface;
5+
//// (props: {className?: string}): any;
6+
////}
7+
////
8+
////declare const Foo: NestedInterface;
9+
////
10+
////function fn1() {
11+
//// return <Foo>
12+
//// <Foo /*1*/ />
13+
//// </Foo>
14+
////}
15+
////function fn2() {
16+
//// return <Foo>
17+
//// <Foo.Foo /*2*/ />
18+
//// </Foo>
19+
////}
20+
////function fn3() {
21+
//// return <Foo>
22+
//// <Foo.Foo cla/*3*/ />
23+
//// </Foo>
24+
////}
25+
////function fn4() {
26+
//// return <Foo>
27+
//// <Foo.Foo cla/*4*/ something />
28+
//// </Foo>
29+
////}
30+
////function fn5() {
31+
//// return <Foo>
32+
//// <Foo.Foo something /*5*/ />
33+
//// </Foo>
34+
////}
35+
////function fn6() {
36+
//// return <Foo>
37+
//// <Foo.Foo something cla/*6*/ />
38+
//// </Foo>
39+
////}
40+
////function fn7() {
41+
//// return <Foo /*7*/ />
42+
////}
43+
////function fn8() {
44+
//// return <Foo cla/*8*/ />
45+
////}
46+
////function fn9() {
47+
//// return <Foo cla/*9*/ something />
48+
////}
49+
////function fn10() {
50+
//// return <Foo something /*10*/ />
51+
////}
52+
////function fn11() {
53+
//// return <Foo something cla/*11*/ />
54+
////}
55+
56+
var preferences: FourSlashInterface.UserPreferences = {
57+
jsxAttributeCompletionStyle: "braces",
58+
includeCompletionsWithSnippetText: true,
59+
includeCompletionsWithInsertText: true,
60+
};
61+
62+
verify.completions(
63+
{ marker: "1", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
64+
{ marker: "2", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
65+
{ marker: "3", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
66+
{ marker: "4", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
67+
{ marker: "5", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
68+
{ marker: "6", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
69+
{ marker: "7", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
70+
{ marker: "8", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
71+
{ marker: "9", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
72+
{ marker: "10", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
73+
{ marker: "11", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
74+
)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/// <reference path="fourslash.ts" />
2+
//@Filename: file.tsx
3+
////interface NestedInterface {
4+
//// Foo: NestedInterface;
5+
//// (props: {className?: string}): any;
6+
////}
7+
////
8+
////declare const Foo: NestedInterface;
9+
////
10+
////function fn1() {
11+
//// return <Foo>
12+
//// <Foo /*1*/
13+
//// </Foo>
14+
////}
15+
////function fn2() {
16+
//// return <Foo>
17+
//// <Foo.Foo /*2*/
18+
//// </Foo>
19+
////}
20+
////function fn3() {
21+
//// return <Foo>
22+
//// <Foo.Foo cla/*3*/
23+
//// </Foo>
24+
////}
25+
////function fn4() {
26+
//// return <Foo>
27+
//// <Foo.Foo cla/*4*/ something
28+
//// </Foo>
29+
////}
30+
////function fn5() {
31+
//// return <Foo>
32+
//// <Foo.Foo something /*5*/
33+
//// </Foo>
34+
////}
35+
////function fn6() {
36+
//// return <Foo>
37+
//// <Foo.Foo something cla/*6*/
38+
//// </Foo>
39+
////}
40+
////function fn7() {
41+
//// return <Foo /*7*/
42+
////}
43+
////function fn8() {
44+
//// return <Foo cla/*8*/
45+
////}
46+
////function fn9() {
47+
//// return <Foo cla/*9*/ something
48+
////}
49+
////function fn10() {
50+
//// return <Foo something /*10*/
51+
////}
52+
////function fn11() {
53+
//// return <Foo something cla/*11*/
54+
////}
55+
56+
var preferences: FourSlashInterface.UserPreferences = {
57+
jsxAttributeCompletionStyle: "braces",
58+
includeCompletionsWithSnippetText: true,
59+
includeCompletionsWithInsertText: true,
60+
};
61+
62+
verify.completions(
63+
{ marker: "1", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
64+
{ marker: "2", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
65+
{ marker: "3", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
66+
{ marker: "4", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
67+
{ marker: "5", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
68+
{ marker: "6", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
69+
{ marker: "7", preferences, includes: { name: "className", insertText: "className={$1}", text: "(property) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
70+
{ marker: "8", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
71+
{ marker: "9", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
72+
{ marker: "10", preferences, includes: { name: "className", insertText: "className={$1}", text: "(property) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
73+
{ marker: "11", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
74+
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/// <reference path="fourslash.ts" />
2+
//@Filename: file.tsx
3+
////interface NestedInterface {
4+
//// Foo: NestedInterface;
5+
//// (props: {}): any;
6+
////}
7+
////
8+
////declare const Foo: NestedInterface;
9+
////
10+
////function fn1() {
11+
//// return <Foo>
12+
//// </*1*/ />
13+
//// </Foo>
14+
////}
15+
////function fn2() {
16+
//// return <Foo>
17+
//// <Fo/*2*/ />
18+
//// </Foo>
19+
////}
20+
////function fn3() {
21+
//// return <Foo>
22+
//// <Foo./*3*/ />
23+
//// </Foo>
24+
////}
25+
////function fn4() {
26+
//// return <Foo>
27+
//// <Foo.F/*4*/ />
28+
//// </Foo>
29+
////}
30+
////function fn5() {
31+
//// return <Foo>
32+
//// <Foo.Foo./*5*/ />
33+
//// </Foo>
34+
////}
35+
////function fn6() {
36+
//// return <Foo>
37+
//// <Foo.Foo.F/*6*/ />
38+
//// </Foo>
39+
////}
40+
41+
var preferences: FourSlashInterface.UserPreferences = {
42+
jsxAttributeCompletionStyle: "braces",
43+
includeCompletionsWithSnippetText: true,
44+
includeCompletionsWithInsertText: true,
45+
};
46+
47+
verify.completions(
48+
{ marker: "1", preferences, includes: { name: "Foo", text: "const Foo: NestedInterface" } },
49+
{ marker: "2", preferences, includes: { name: "Foo", text: "const Foo: NestedInterface" } },
50+
{ marker: "3", preferences, includes: { name: "Foo", text: "(JSX attribute) NestedInterface.Foo: NestedInterface" } },
51+
{ marker: "4", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } },
52+
{ marker: "5", preferences, includes: { name: "Foo", text: "(JSX attribute) NestedInterface.Foo: NestedInterface" } },
53+
{ marker: "6", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } },
54+
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/// <reference path="fourslash.ts" />
2+
//@Filename: file.tsx
3+
////interface NestedInterface {
4+
//// Foo: NestedInterface;
5+
//// (props: {}): any;
6+
////}
7+
////
8+
////declare const Foo: NestedInterface;
9+
////
10+
////function fn1() {
11+
//// return <Foo>
12+
//// </*1*/
13+
//// </Foo>
14+
////}
15+
////function fn2() {
16+
//// return <Foo>
17+
//// <Fo/*2*/
18+
//// </Foo>
19+
////}
20+
////function fn3() {
21+
//// return <Foo>
22+
//// <Foo./*3*/
23+
//// </Foo>
24+
////}
25+
////function fn4() {
26+
//// return <Foo>
27+
//// <Foo.F/*4*/
28+
//// </Foo>
29+
////}
30+
////function fn5() {
31+
//// return <Foo>
32+
//// <Foo.Foo./*5*/
33+
//// </Foo>
34+
////}
35+
////function fn6() {
36+
//// return <Foo>
37+
//// <Foo.Foo.F/*6*/
38+
//// </Foo>
39+
////}
40+
41+
var preferences: FourSlashInterface.UserPreferences = {
42+
jsxAttributeCompletionStyle: "braces",
43+
includeCompletionsWithSnippetText: true,
44+
includeCompletionsWithInsertText: true,
45+
};
46+
47+
verify.completions(
48+
{ marker: "1", preferences, includes: { name: "Foo", text: "const Foo: NestedInterface" } },
49+
{ marker: "2", preferences, includes: { name: "Foo", text: "const Foo: NestedInterface" } },
50+
{ marker: "3", preferences, includes: { name: "Foo", text: "(JSX attribute) NestedInterface.Foo: NestedInterface" } },
51+
{ marker: "4", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } },
52+
{ marker: "5", preferences, includes: { name: "Foo", text: "(JSX attribute) NestedInterface.Foo: NestedInterface" } },
53+
{ marker: "6", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } },
54+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/// <reference path="fourslash.ts" />
2+
//@Filename: file.tsx
3+
////declare namespace JSX {
4+
//// interface IntrinsicElements {
5+
//// button: any;
6+
//// div: any;
7+
//// }
8+
////}
9+
////function fn() {
10+
//// return <>
11+
//// <butto/*1*/ />
12+
//// </>;
13+
////}
14+
////function fn2() {
15+
//// return <>
16+
//// preceding junk <butto/*2*/ />
17+
//// </>;
18+
////}
19+
////function fn3() {
20+
//// return <>
21+
//// <butto/*3*/ style="" />
22+
//// </>;
23+
////}
24+
25+
var preferences: FourSlashInterface.UserPreferences = {
26+
jsxAttributeCompletionStyle: "braces",
27+
includeCompletionsWithSnippetText: true,
28+
includeCompletionsWithInsertText: true,
29+
};
30+
31+
verify.completions(
32+
{ marker: "1", preferences, includes: { name: "button", text: "(JSX attribute) JSX.IntrinsicElements.button: any" } },
33+
{ marker: "2", preferences, includes: { name: "button", text: "(JSX attribute) JSX.IntrinsicElements.button: any" } },
34+
{ marker: "3", preferences, includes: { name: "button", text: "(JSX attribute) JSX.IntrinsicElements.button: any" } },
35+
)

0 commit comments

Comments
 (0)