Skip to content

Commit 24e3b6b

Browse files
authored
Added Jsx Snippet Completion feature (#45903)
* Added Jsx completion feature and tests * Renamed jsxSnippetCompletion to jsxAttributeCompletionStyle * Renamed tests files * Changed boolean filter * Escaped snippet
1 parent f0fe1b8 commit 24e3b6b

11 files changed

+455
-5
lines changed

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8575,6 +8575,7 @@ namespace ts {
85758575
readonly providePrefixAndSuffixTextForRename?: boolean;
85768576
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
85778577
readonly provideRefactorNotApplicableReason?: boolean;
8578+
readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
85788579
}
85798580

85808581
/** Represents a bigint literal value without requiring bigint support */

src/server/protocol.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3391,6 +3391,7 @@ namespace ts.server.protocol {
33913391
readonly provideRefactorNotApplicableReason?: boolean;
33923392
readonly allowRenameOfImportPath?: boolean;
33933393
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
3394+
readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
33943395

33953396
readonly displayPartsForJSDoc?: boolean;
33963397
readonly generateReturnInDocTemplate?: boolean;

src/services/completions.ts

+40-5
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,37 @@ namespace ts.Completions {
675675
hasAction = !importCompletionNode;
676676
}
677677

678+
const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
679+
if (kind === ScriptElementKind.jsxAttribute && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") {
680+
let useBraces = preferences.jsxAttributeCompletionStyle === "braces";
681+
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, location);
682+
683+
// If is boolean like or undefined, don't return a snippet we want just to return the completion.
684+
if (preferences.jsxAttributeCompletionStyle === "auto"
685+
&& !(type.flags & TypeFlags.BooleanLike)
686+
&& !(type.flags & TypeFlags.Union && find((type as UnionType).types, type => !!(type.flags & TypeFlags.BooleanLike)))
687+
) {
688+
if (type.flags & TypeFlags.StringLike || (type.flags & TypeFlags.Union && every((type as UnionType).types, type => !!(type.flags & (TypeFlags.StringLike | TypeFlags.Undefined))))) {
689+
// If is string like or undefined use quotes
690+
insertText = `${escapeSnippetText(name)}=${quote(sourceFile, preferences, "$1")}`;
691+
isSnippet = true;
692+
}
693+
else {
694+
// Use braces for everything else
695+
useBraces = true;
696+
}
697+
}
698+
699+
if (useBraces) {
700+
insertText = `${escapeSnippetText(name)}={$1}`;
701+
isSnippet = true;
702+
}
703+
704+
if (isSnippet) {
705+
replacementSpan = createTextSpanFromNode(location, sourceFile);
706+
}
707+
}
708+
678709
// TODO(drosen): Right now we just permit *all* semantic meanings when calling
679710
// 'getSymbolKind' which is permissible given that it is backwards compatible; but
680711
// really we should consider passing the meaning for the node so that we don't report
@@ -685,7 +716,7 @@ namespace ts.Completions {
685716
// entries (like JavaScript identifier entries).
686717
return {
687718
name,
688-
kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location), // TODO: GH#18217
719+
kind,
689720
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
690721
sortText,
691722
source: getSourceFromOrigin(origin),
@@ -701,6 +732,10 @@ namespace ts.Completions {
701732
};
702733
}
703734

735+
function escapeSnippetText(text: string): string {
736+
return text.replace(/\$/gm, "\\$");
737+
}
738+
704739
function originToCompletionEntryData(origin: SymbolOriginInfoExport): CompletionEntryData | undefined {
705740
return {
706741
exportName: origin.exportName,
@@ -723,10 +758,10 @@ namespace ts.Completions {
723758
const importKind = codefix.getImportKind(sourceFile, exportKind, options, /*forceImportKeyword*/ true);
724759
const suffix = useSemicolons ? ";" : "";
725760
switch (importKind) {
726-
case ImportKind.CommonJS: return { replacementSpan, insertText: `import ${name}${tabStop} = require(${quotedModuleSpecifier})${suffix}` };
727-
case ImportKind.Default: return { replacementSpan, insertText: `import ${name}${tabStop} from ${quotedModuleSpecifier}${suffix}` };
728-
case ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${name} from ${quotedModuleSpecifier}${suffix}` };
729-
case ImportKind.Named: return { replacementSpan, insertText: `import { ${name}${tabStop} } from ${quotedModuleSpecifier}${suffix}` };
761+
case ImportKind.CommonJS: return { replacementSpan, insertText: `import ${escapeSnippetText(name)}${tabStop} = require(${quotedModuleSpecifier})${suffix}` };
762+
case ImportKind.Default: return { replacementSpan, insertText: `import ${escapeSnippetText(name)}${tabStop} from ${quotedModuleSpecifier}${suffix}` };
763+
case ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${escapeSnippetText(name)} from ${quotedModuleSpecifier}${suffix}` };
764+
case ImportKind.Named: return { replacementSpan, insertText: `import { ${escapeSnippetText(name)}${tabStop} } from ${quotedModuleSpecifier}${suffix}` };
730765
}
731766
}
732767

tests/baselines/reference/api/tsserverlibrary.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4019,6 +4019,7 @@ declare namespace ts {
40194019
readonly providePrefixAndSuffixTextForRename?: boolean;
40204020
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
40214021
readonly provideRefactorNotApplicableReason?: boolean;
4022+
readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
40224023
}
40234024
/** Represents a bigint literal value without requiring bigint support */
40244025
export interface PseudoBigInt {
@@ -9485,6 +9486,7 @@ declare namespace ts.server.protocol {
94859486
readonly provideRefactorNotApplicableReason?: boolean;
94869487
readonly allowRenameOfImportPath?: boolean;
94879488
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
9489+
readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
94889490
readonly displayPartsForJSDoc?: boolean;
94899491
readonly generateReturnInDocTemplate?: boolean;
94909492
}

tests/baselines/reference/api/typescript.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4019,6 +4019,7 @@ declare namespace ts {
40194019
readonly providePrefixAndSuffixTextForRename?: boolean;
40204020
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
40214021
readonly provideRefactorNotApplicableReason?: boolean;
4022+
readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
40224023
}
40234024
/** Represents a bigint literal value without requiring bigint support */
40244025
export interface PseudoBigInt {

tests/cases/fourslash/fourslash.ts

+1
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ declare namespace FourSlashInterface {
652652
readonly includeAutomaticOptionalChainCompletions?: boolean;
653653
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
654654
readonly importModuleSpecifierEnding?: "minimal" | "index" | "js";
655+
readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
655656
}
656657
interface InlayHintsOptions extends UserPreferences {
657658
readonly includeInlayParameterNameHints?: "none" | "literals" | "all";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: foo.tsx
4+
//// declare namespace JSX {
5+
//// interface Element { }
6+
//// interface IntrinsicElements {
7+
//// foo: {
8+
//// prop_a: boolean;
9+
//// prop_b: string;
10+
//// prop_c: any;
11+
//// prop_d: { p1: string; }
12+
//// prop_e: string | undefined;
13+
//// prop_f: boolean | undefined | { p1: string; };
14+
//// prop_g: { p1: string; } | undefined;
15+
//// prop_h?: string;
16+
//// prop_i?: boolean;
17+
//// prop_j?: { p1: string; };
18+
//// }
19+
//// }
20+
//// }
21+
////
22+
//// <foo [|prop_/**/|] />
23+
24+
verify.completions({
25+
marker: "",
26+
exact: [
27+
{
28+
name: "prop_a",
29+
isSnippet: undefined,
30+
},
31+
{
32+
name: "prop_b",
33+
insertText: "prop_b=\"$1\"",
34+
replacementSpan: test.ranges()[0],
35+
isSnippet: true,
36+
},
37+
{
38+
name: "prop_c",
39+
insertText: "prop_c={$1}",
40+
replacementSpan: test.ranges()[0],
41+
isSnippet: true,
42+
},
43+
{
44+
name: "prop_d",
45+
insertText: "prop_d={$1}",
46+
replacementSpan: test.ranges()[0],
47+
isSnippet: true,
48+
},
49+
{
50+
name: "prop_e",
51+
insertText: "prop_e=\"$1\"",
52+
replacementSpan: test.ranges()[0],
53+
isSnippet: true,
54+
},
55+
{
56+
name: "prop_f",
57+
isSnippet: undefined,
58+
},
59+
{
60+
name: "prop_g",
61+
insertText: "prop_g={$1}",
62+
replacementSpan: test.ranges()[0],
63+
isSnippet: true,
64+
},
65+
{
66+
name: "prop_h",
67+
insertText: "prop_h=\"$1\"",
68+
replacementSpan: test.ranges()[0],
69+
isSnippet: true,
70+
sortText: completion.SortText.OptionalMember,
71+
},
72+
{
73+
name: "prop_i",
74+
isSnippet: undefined,
75+
sortText: completion.SortText.OptionalMember,
76+
},
77+
{
78+
name: "prop_j",
79+
insertText: "prop_j={$1}",
80+
replacementSpan: test.ranges()[0],
81+
isSnippet: true,
82+
sortText: completion.SortText.OptionalMember,
83+
}
84+
],
85+
preferences: {
86+
jsxAttributeCompletionStyle: "auto",
87+
includeCompletionsWithSnippetText: true
88+
}
89+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: foo.tsx
4+
//// declare namespace JSX {
5+
//// interface Element { }
6+
//// interface IntrinsicElements {
7+
//// foo: {
8+
//// prop_a: boolean;
9+
//// prop_b: string;
10+
//// prop_c: any;
11+
//// prop_d: { p1: string; }
12+
//// prop_e: string | undefined;
13+
//// prop_f: boolean | undefined | { p1: string; };
14+
//// prop_g: { p1: string; } | undefined;
15+
//// prop_h?: string;
16+
//// prop_i?: boolean;
17+
//// prop_j?: { p1: string; };
18+
//// }
19+
//// }
20+
//// }
21+
////
22+
//// <foo [|prop_/**/|] />
23+
24+
verify.completions({
25+
marker: "",
26+
exact: [
27+
{
28+
name: "prop_a",
29+
insertText: "prop_a={$1}",
30+
replacementSpan: test.ranges()[0],
31+
isSnippet: true,
32+
},
33+
{
34+
name: "prop_b",
35+
insertText: "prop_b={$1}",
36+
replacementSpan: test.ranges()[0],
37+
isSnippet: true,
38+
},
39+
{
40+
name: "prop_c",
41+
insertText: "prop_c={$1}",
42+
replacementSpan: test.ranges()[0],
43+
isSnippet: true,
44+
},
45+
{
46+
name: "prop_d",
47+
insertText: "prop_d={$1}",
48+
replacementSpan: test.ranges()[0],
49+
isSnippet: true,
50+
},
51+
{
52+
name: "prop_e",
53+
insertText: "prop_e={$1}",
54+
replacementSpan: test.ranges()[0],
55+
isSnippet: true,
56+
},
57+
{
58+
name: "prop_f",
59+
insertText: "prop_f={$1}",
60+
replacementSpan: test.ranges()[0],
61+
isSnippet: true,
62+
},
63+
{
64+
name: "prop_g",
65+
insertText: "prop_g={$1}",
66+
replacementSpan: test.ranges()[0],
67+
isSnippet: true,
68+
},
69+
{
70+
name: "prop_h",
71+
insertText: "prop_h={$1}",
72+
replacementSpan: test.ranges()[0],
73+
isSnippet: true,
74+
sortText: completion.SortText.OptionalMember,
75+
},
76+
{
77+
name: "prop_i",
78+
insertText: "prop_i={$1}",
79+
replacementSpan: test.ranges()[0],
80+
isSnippet: true,
81+
sortText: completion.SortText.OptionalMember,
82+
},
83+
{
84+
name: "prop_j",
85+
insertText: "prop_j={$1}",
86+
replacementSpan: test.ranges()[0],
87+
isSnippet: true,
88+
sortText: completion.SortText.OptionalMember,
89+
}
90+
],
91+
preferences: {
92+
jsxAttributeCompletionStyle: "braces",
93+
includeCompletionsWithSnippetText: true
94+
}
95+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: foo.tsx
4+
//// declare namespace JSX {
5+
//// interface Element { }
6+
//// interface IntrinsicElements {
7+
//// foo: {
8+
//// prop_a: boolean;
9+
//// prop_b: string;
10+
//// prop_c: any;
11+
//// prop_d: { p1: string; }
12+
//// prop_e: string | undefined;
13+
//// prop_f: boolean | undefined | { p1: string; };
14+
//// prop_g: { p1: string; } | undefined;
15+
//// prop_h?: string;
16+
//// prop_i?: boolean;
17+
//// prop_j?: { p1: string; };
18+
//// }
19+
//// }
20+
//// }
21+
////
22+
//// <foo [|prop_/**/|] />
23+
24+
verify.completions({
25+
marker: "",
26+
exact: [
27+
{
28+
name: "prop_a",
29+
isSnippet: undefined,
30+
},
31+
{
32+
name: "prop_b",
33+
isSnippet: undefined,
34+
},
35+
{
36+
name: "prop_c",
37+
isSnippet: undefined,
38+
},
39+
{
40+
name: "prop_d",
41+
isSnippet: undefined,
42+
},
43+
{
44+
name: "prop_e",
45+
isSnippet: undefined,
46+
},
47+
{
48+
name: "prop_f",
49+
isSnippet: undefined,
50+
},
51+
{
52+
name: "prop_g",
53+
isSnippet: undefined,
54+
},
55+
{
56+
name: "prop_h",
57+
isSnippet: undefined,
58+
sortText: completion.SortText.OptionalMember,
59+
},
60+
{
61+
name: "prop_i",
62+
isSnippet: undefined,
63+
sortText: completion.SortText.OptionalMember,
64+
},
65+
{
66+
name: "prop_j",
67+
isSnippet: undefined,
68+
sortText: completion.SortText.OptionalMember,
69+
}
70+
],
71+
preferences: {
72+
jsxAttributeCompletionStyle: undefined,
73+
includeCompletionsWithSnippetText: true
74+
}
75+
});

0 commit comments

Comments
 (0)