diff --git a/src/services/completions.ts b/src/services/completions.ts index dc01ea8ede4b9..c0f3f4d1b7abe 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1860,7 +1860,36 @@ function createCompletionEntry( && !(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) || isStringAndEmptyAnonymousObjectIntersection(type))))) { + // Check if we should use quotes for string-like types + let shouldUseQuotes = false; + + if (type.flags & TypeFlags.StringLike) { + // Direct string-like type + shouldUseQuotes = true; + } + else if (type.flags & TypeFlags.Union) { + const unionType = type as UnionType; + // Check if all types are string-like or undefined (original logic) + const allTypesAreStringLikeOrUndefined = every(unionType.types, type => !!(type.flags & (TypeFlags.StringLike | TypeFlags.Undefined) || isStringAndEmptyAnonymousObjectIntersection(type))); + + if (allTypesAreStringLikeOrUndefined) { + shouldUseQuotes = true; + } + else { + // Check if the union contains string-like types that users would typically provide as strings + // This handles cases like Preact's Signalish = string | undefined | SignalLike + const hasStringLikeTypes = some(unionType.types, type => !!(type.flags & TypeFlags.StringLike)); + const hasNonObjectTypes = some(unionType.types, type => !!(type.flags & (TypeFlags.StringLike | TypeFlags.Undefined | TypeFlags.Null))); + + // If the union has string-like types and at least some primitive types (not just objects), + // prefer quotes since users commonly want to provide string values + if (hasStringLikeTypes && hasNonObjectTypes) { + shouldUseQuotes = true; + } + } + } + + if (shouldUseQuotes) { // If is string like or undefined use quotes insertText = `${escapeSnippetText(name)}=${quote(sourceFile, preferences, "$1")}`; isSnippet = true; diff --git a/tests/cases/fourslash/jsxAttributeCompletionStyleAutoSignalish.ts b/tests/cases/fourslash/jsxAttributeCompletionStyleAutoSignalish.ts new file mode 100644 index 0000000000000..17854407a8354 --- /dev/null +++ b/tests/cases/fourslash/jsxAttributeCompletionStyleAutoSignalish.ts @@ -0,0 +1,52 @@ +/// + +// @Filename: foo.tsx +//// declare namespace JSX { +//// interface Element { } +//// interface SignalLike { +//// value: T; +//// peek(): T; +//// subscribe(fn: (value: T) => void): () => void; +//// } +//// type Signalish = T | SignalLike; +//// interface IntrinsicElements { +//// div: { +//// class?: Signalish; +//// id?: Signalish; +//// title?: Signalish; +//// disabled?: Signalish; +//// 'data-testid'?: Signalish; +//// role?: Signalish; +//// // For comparison - pure string type should still work +//// pureString?: string; +//// // Boolean-like should not get quotes +//// booleanProp?: boolean; +//// } +//// } +//// } +//// +////
+ +// Test that string-like Signalish types prefer quotes over braces +verify.completions({ + marker: "", + includes: [ + { + name: "class", + insertText: "class=\"$1\"", + isSnippet: true, + sortText: completion.SortText.OptionalMember, + }, + { + name: "id", + insertText: "id=\"$1\"", + isSnippet: true, + sortText: completion.SortText.OptionalMember, + }, + ], + preferences: { + jsxAttributeCompletionStyle: "auto", + includeCompletionsWithSnippetText: true, + includeCompletionsWithInsertText: true, + } +});