diff --git a/src/compiler/types.ts b/src/compiler/types.ts
index ae1e7c16d00d2..00983e6745275 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -8547,6 +8547,7 @@ namespace ts {
readonly providePrefixAndSuffixTextForRename?: boolean;
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
readonly provideRefactorNotApplicableReason?: boolean;
+ readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
}
/** Represents a bigint literal value without requiring bigint support */
diff --git a/src/server/protocol.ts b/src/server/protocol.ts
index 6147077051b52..67d6fda2a3b1b 100644
--- a/src/server/protocol.ts
+++ b/src/server/protocol.ts
@@ -3391,6 +3391,7 @@ namespace ts.server.protocol {
readonly provideRefactorNotApplicableReason?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
+ readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
readonly displayPartsForJSDoc?: boolean;
readonly generateReturnInDocTemplate?: boolean;
diff --git a/src/services/completions.ts b/src/services/completions.ts
index ad5e24ea3ebfb..c5ddaeca0bfdf 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -675,6 +675,37 @@ namespace ts.Completions {
hasAction = !importCompletionNode;
}
+ const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
+ if (kind === ScriptElementKind.jsxAttribute && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") {
+ let useBraces = preferences.jsxAttributeCompletionStyle === "braces";
+ const type = typeChecker.getTypeOfSymbolAtLocation(symbol, location);
+
+ // If is boolean like or undefined, don't return a snippet we want just to return the completion.
+ if (preferences.jsxAttributeCompletionStyle === "auto"
+ && !(type.flags & TypeFlags.BooleanLike)
+ && !(type.flags & TypeFlags.Union && find((type as UnionType).types, type => !!(type.flags & TypeFlags.BooleanLike)))
+ ) {
+ if (type.flags & TypeFlags.StringLike || (type.flags & TypeFlags.Union && every((type as UnionType).types, type => !!(type.flags & (TypeFlags.StringLike | TypeFlags.Undefined))))) {
+ // If is string like or undefined use quotes
+ insertText = `${escapeSnippetText(name)}=${quote(sourceFile, preferences, "$1")}`;
+ isSnippet = true;
+ }
+ else {
+ // Use braces for everything else
+ useBraces = true;
+ }
+ }
+
+ if (useBraces) {
+ insertText = `${escapeSnippetText(name)}={$1}`;
+ isSnippet = true;
+ }
+
+ if (isSnippet) {
+ replacementSpan = createTextSpanFromNode(location, sourceFile);
+ }
+ }
+
// TODO(drosen): Right now we just permit *all* semantic meanings when calling
// 'getSymbolKind' which is permissible given that it is backwards compatible; but
// really we should consider passing the meaning for the node so that we don't report
@@ -685,7 +716,7 @@ namespace ts.Completions {
// entries (like JavaScript identifier entries).
return {
name,
- kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location), // TODO: GH#18217
+ kind,
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
sortText,
source: getSourceFromOrigin(origin),
@@ -701,6 +732,10 @@ namespace ts.Completions {
};
}
+ function escapeSnippetText(text: string): string {
+ return text.replace(/\$/gm, "\\$");
+ }
+
function originToCompletionEntryData(origin: SymbolOriginInfoExport): CompletionEntryData | undefined {
return {
exportName: origin.exportName,
@@ -723,10 +758,10 @@ namespace ts.Completions {
const importKind = codefix.getImportKind(sourceFile, exportKind, options, /*forceImportKeyword*/ true);
const suffix = useSemicolons ? ";" : "";
switch (importKind) {
- case ImportKind.CommonJS: return { replacementSpan, insertText: `import ${name}${tabStop} = require(${quotedModuleSpecifier})${suffix}` };
- case ImportKind.Default: return { replacementSpan, insertText: `import ${name}${tabStop} from ${quotedModuleSpecifier}${suffix}` };
- case ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${name} from ${quotedModuleSpecifier}${suffix}` };
- case ImportKind.Named: return { replacementSpan, insertText: `import { ${name}${tabStop} } from ${quotedModuleSpecifier}${suffix}` };
+ case ImportKind.CommonJS: return { replacementSpan, insertText: `import ${escapeSnippetText(name)}${tabStop} = require(${quotedModuleSpecifier})${suffix}` };
+ case ImportKind.Default: return { replacementSpan, insertText: `import ${escapeSnippetText(name)}${tabStop} from ${quotedModuleSpecifier}${suffix}` };
+ case ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${escapeSnippetText(name)} from ${quotedModuleSpecifier}${suffix}` };
+ case ImportKind.Named: return { replacementSpan, insertText: `import { ${escapeSnippetText(name)}${tabStop} } from ${quotedModuleSpecifier}${suffix}` };
}
}
diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts
index f23a441f98c01..3387aa55c1768 100644
--- a/tests/baselines/reference/api/tsserverlibrary.d.ts
+++ b/tests/baselines/reference/api/tsserverlibrary.d.ts
@@ -3995,6 +3995,7 @@ declare namespace ts {
readonly providePrefixAndSuffixTextForRename?: boolean;
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
readonly provideRefactorNotApplicableReason?: boolean;
+ readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
}
/** Represents a bigint literal value without requiring bigint support */
export interface PseudoBigInt {
@@ -9458,6 +9459,7 @@ declare namespace ts.server.protocol {
readonly provideRefactorNotApplicableReason?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
+ readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
readonly displayPartsForJSDoc?: boolean;
readonly generateReturnInDocTemplate?: boolean;
}
diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts
index 8a9603b7b7cae..40950e13fd321 100644
--- a/tests/baselines/reference/api/typescript.d.ts
+++ b/tests/baselines/reference/api/typescript.d.ts
@@ -3995,6 +3995,7 @@ declare namespace ts {
readonly providePrefixAndSuffixTextForRename?: boolean;
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
readonly provideRefactorNotApplicableReason?: boolean;
+ readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
}
/** Represents a bigint literal value without requiring bigint support */
export interface PseudoBigInt {
diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts
index 273adfb9707fe..01a1a4a4c0a47 100644
--- a/tests/cases/fourslash/fourslash.ts
+++ b/tests/cases/fourslash/fourslash.ts
@@ -658,6 +658,7 @@ declare namespace FourSlashInterface {
readonly includeAutomaticOptionalChainCompletions?: boolean;
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
readonly importModuleSpecifierEnding?: "minimal" | "index" | "js";
+ readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
}
interface InlayHintsOptions extends UserPreferences {
readonly includeInlayParameterNameHints?: "none" | "literals" | "all";
diff --git a/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts b/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts
new file mode 100644
index 0000000000000..dc7463df4b407
--- /dev/null
+++ b/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts
@@ -0,0 +1,89 @@
+///
+
+// @Filename: foo.tsx
+//// declare namespace JSX {
+//// interface Element { }
+//// interface IntrinsicElements {
+//// foo: {
+//// prop_a: boolean;
+//// prop_b: string;
+//// prop_c: any;
+//// prop_d: { p1: string; }
+//// prop_e: string | undefined;
+//// prop_f: boolean | undefined | { p1: string; };
+//// prop_g: { p1: string; } | undefined;
+//// prop_h?: string;
+//// prop_i?: boolean;
+//// prop_j?: { p1: string; };
+//// }
+//// }
+//// }
+////
+////
+
+verify.completions({
+ marker: "",
+ exact: [
+ {
+ name: "prop_a",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_b",
+ insertText: "prop_b=\"$1\"",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_c",
+ insertText: "prop_c={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_d",
+ insertText: "prop_d={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_e",
+ insertText: "prop_e=\"$1\"",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_f",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_g",
+ insertText: "prop_g={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_h",
+ insertText: "prop_h=\"$1\"",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_i",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_j",
+ insertText: "prop_j={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ sortText: completion.SortText.OptionalMember,
+ }
+ ],
+ preferences: {
+ jsxAttributeCompletionStyle: "auto",
+ includeCompletionsWithSnippetText: true
+ }
+});
\ No newline at end of file
diff --git a/tests/cases/fourslash/jsxAttributeCompletionStyleBraces.ts b/tests/cases/fourslash/jsxAttributeCompletionStyleBraces.ts
new file mode 100644
index 0000000000000..051e1bf410d18
--- /dev/null
+++ b/tests/cases/fourslash/jsxAttributeCompletionStyleBraces.ts
@@ -0,0 +1,95 @@
+///
+
+// @Filename: foo.tsx
+//// declare namespace JSX {
+//// interface Element { }
+//// interface IntrinsicElements {
+//// foo: {
+//// prop_a: boolean;
+//// prop_b: string;
+//// prop_c: any;
+//// prop_d: { p1: string; }
+//// prop_e: string | undefined;
+//// prop_f: boolean | undefined | { p1: string; };
+//// prop_g: { p1: string; } | undefined;
+//// prop_h?: string;
+//// prop_i?: boolean;
+//// prop_j?: { p1: string; };
+//// }
+//// }
+//// }
+////
+////
+
+verify.completions({
+ marker: "",
+ exact: [
+ {
+ name: "prop_a",
+ insertText: "prop_a={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_b",
+ insertText: "prop_b={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_c",
+ insertText: "prop_c={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_d",
+ insertText: "prop_d={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_e",
+ insertText: "prop_e={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_f",
+ insertText: "prop_f={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_g",
+ insertText: "prop_g={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ },
+ {
+ name: "prop_h",
+ insertText: "prop_h={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_i",
+ insertText: "prop_i={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_j",
+ insertText: "prop_j={$1}",
+ replacementSpan: test.ranges()[0],
+ isSnippet: true,
+ sortText: completion.SortText.OptionalMember,
+ }
+ ],
+ preferences: {
+ jsxAttributeCompletionStyle: "braces",
+ includeCompletionsWithSnippetText: true
+ }
+});
\ No newline at end of file
diff --git a/tests/cases/fourslash/jsxAttributeCompletionStyleDefault.ts b/tests/cases/fourslash/jsxAttributeCompletionStyleDefault.ts
new file mode 100644
index 0000000000000..134721cc67e09
--- /dev/null
+++ b/tests/cases/fourslash/jsxAttributeCompletionStyleDefault.ts
@@ -0,0 +1,75 @@
+///
+
+// @Filename: foo.tsx
+//// declare namespace JSX {
+//// interface Element { }
+//// interface IntrinsicElements {
+//// foo: {
+//// prop_a: boolean;
+//// prop_b: string;
+//// prop_c: any;
+//// prop_d: { p1: string; }
+//// prop_e: string | undefined;
+//// prop_f: boolean | undefined | { p1: string; };
+//// prop_g: { p1: string; } | undefined;
+//// prop_h?: string;
+//// prop_i?: boolean;
+//// prop_j?: { p1: string; };
+//// }
+//// }
+//// }
+////
+////
+
+verify.completions({
+ marker: "",
+ exact: [
+ {
+ name: "prop_a",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_b",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_c",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_d",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_e",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_f",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_g",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_h",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_i",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_j",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ }
+ ],
+ preferences: {
+ jsxAttributeCompletionStyle: undefined,
+ includeCompletionsWithSnippetText: true
+ }
+});
\ No newline at end of file
diff --git a/tests/cases/fourslash/jsxAttributeCompletionStyleNoSnippet.ts b/tests/cases/fourslash/jsxAttributeCompletionStyleNoSnippet.ts
new file mode 100644
index 0000000000000..8c555f720340c
--- /dev/null
+++ b/tests/cases/fourslash/jsxAttributeCompletionStyleNoSnippet.ts
@@ -0,0 +1,75 @@
+///
+
+// @Filename: foo.tsx
+//// declare namespace JSX {
+//// interface Element { }
+//// interface IntrinsicElements {
+//// foo: {
+//// prop_a: boolean;
+//// prop_b: string;
+//// prop_c: any;
+//// prop_d: { p1: string; }
+//// prop_e: string | undefined;
+//// prop_f: boolean | undefined | { p1: string; };
+//// prop_g: { p1: string; } | undefined;
+//// prop_h?: string;
+//// prop_i?: boolean;
+//// prop_j?: { p1: string; };
+//// }
+//// }
+//// }
+////
+////
+
+verify.completions({
+ marker: "",
+ exact: [
+ {
+ name: "prop_a",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_b",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_c",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_d",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_e",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_f",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_g",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_h",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_i",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_j",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ }
+ ],
+ preferences: {
+ jsxAttributeCompletionStyle: "auto",
+ includeCompletionsWithSnippetText: false
+ }
+});
\ No newline at end of file
diff --git a/tests/cases/fourslash/jsxAttributeCompletionStyleNone.ts b/tests/cases/fourslash/jsxAttributeCompletionStyleNone.ts
new file mode 100644
index 0000000000000..99d0a7ab31cf0
--- /dev/null
+++ b/tests/cases/fourslash/jsxAttributeCompletionStyleNone.ts
@@ -0,0 +1,75 @@
+///
+
+// @Filename: foo.tsx
+//// declare namespace JSX {
+//// interface Element { }
+//// interface IntrinsicElements {
+//// foo: {
+//// prop_a: boolean;
+//// prop_b: string;
+//// prop_c: any;
+//// prop_d: { p1: string; }
+//// prop_e: string | undefined;
+//// prop_f: boolean | undefined | { p1: string; };
+//// prop_g: { p1: string; } | undefined;
+//// prop_h?: string;
+//// prop_i?: boolean;
+//// prop_j?: { p1: string; };
+//// }
+//// }
+//// }
+////
+////
+
+verify.completions({
+ marker: "",
+ exact: [
+ {
+ name: "prop_a",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_b",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_c",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_d",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_e",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_f",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_g",
+ isSnippet: undefined,
+ },
+ {
+ name: "prop_h",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_i",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ },
+ {
+ name: "prop_j",
+ isSnippet: undefined,
+ sortText: completion.SortText.OptionalMember,
+ }
+ ],
+ preferences: {
+ jsxAttributeCompletionStyle: "none",
+ includeCompletionsWithSnippetText: true
+ }
+});
\ No newline at end of file