8000 Added Jsx Snippet Completion feature (#45903) · microsoft/TypeScript@24e3b6b · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 40 additions & 5 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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";
Lines changed: 89 additions & 0 deletions
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+
});
Lines changed: 95 additions & 0 deletions
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+
});
Lines changed: 75 additions & 0 deletions
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)
0