diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ae1e7c16d00d2..f86978f689021 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 jsxSnippetCompletion?: "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..f181f890105a2 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 jsxSnippetCompletion?: "auto" | "braces" | "none"; readonly displayPartsForJSDoc?: boolean; readonly generateReturnInDocTemplate?: boolean; diff --git a/src/services/completions.ts b/src/services/completions.ts index ad5e24ea3ebfb..29dbe1f61b01e 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); // TODO: GH#18217 + if (kind === ScriptElementKind.jsxAttribute && preferences.jsxSnippetCompletion && preferences.jsxSnippetCompletion !== "none") { + let useBraces = preferences.jsxSnippetCompletion === "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.jsxSnippetCompletion === "auto" + && !(type.flags & TypeFlags.BooleanLike) + && !(type.flags & TypeFlags.Union && every((type as UnionType).types, type => !!(type.flags & (TypeFlags.BooleanLike | TypeFlags.Undefined)))) + ) { + 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 = `${name}=${quote(sourceFile, preferences, "$1")}`; + isSnippet = true; + } + else { + // Use braces for everything else + useBraces = true; + } + } + + if (useBraces) { + insertText = `${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), diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index f23a441f98c01..4349dbb3ffedd 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 jsxSnippetCompletion?: "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 jsxSnippetCompletion?: "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..18145d5ca78ee 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 jsxSnippetCompletion?: "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..5187b77918aa4 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 jsxSnippetCompletion?: "auto" | "braces" | "none"; } interface InlayHintsOptions extends UserPreferences { readonly includeInlayParameterNameHints?: "none" | "literals" | "all"; diff --git a/tests/cases/fourslash/jsxAttributeSnippetCompletionAuto.ts b/tests/cases/fourslash/jsxAttributeSnippetCompletionAuto.ts new file mode 100644 index 0000000000000..e42b747e533fb --- /dev/null +++ b/tests/cases/fourslash/jsxAttributeSnippetCompletionAuto.ts @@ -0,0 +1,88 @@ +/// + +// @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; +//// 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: { + jsxSnippetCompletion: "auto" + } +}); \ No newline at end of file diff --git a/tests/cases/fourslash/jsxAttributeSnippetCompletionBraces.ts b/tests/cases/fourslash/jsxAttributeSnippetCompletionBraces.ts new file mode 100644 index 0000000000000..1ff939fa70233 --- /dev/null +++ b/tests/cases/fourslash/jsxAttributeSnippetCompletionBraces.ts @@ -0,0 +1,94 @@ +/// + +// @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; +//// 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: { + jsxSnippetCompletion: "braces" + } +}); \ No newline at end of file diff --git a/tests/cases/fourslash/jsxAttributeSnippetCompletionDefault.ts b/tests/cases/fourslash/jsxAttributeSnippetCompletionDefault.ts new file mode 100644 index 0000000000000..40527a1916982 --- /dev/null +++ b/tests/cases/fourslash/jsxAttributeSnippetCompletionDefault.ts @@ -0,0 +1,74 @@ +/// + +// @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; +//// 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: { + jsxSnippetCompletion: undefined + } +}); \ No newline at end of file diff --git a/tests/cases/fourslash/jsxAttributeSnippetCompletionNone.ts b/tests/cases/fourslash/jsxAttributeSnippetCompletionNone.ts new file mode 100644 index 0000000000000..46e5e0339a500 --- /dev/null +++ b/tests/cases/fourslash/jsxAttributeSnippetCompletionNone.ts @@ -0,0 +1,74 @@ +/// + +// @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; +//// 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: { + jsxSnippetCompletion: "none" + } +}); \ No newline at end of file