From 7eb96f6bf8637dae14902bccf8ca9cf325a91f22 Mon Sep 17 00:00:00 2001 From: Arthur Adams Date: Sat, 9 Aug 2025 20:28:08 +0300 Subject: [PATCH 1/4] Fix JSX attribute completion for union types containing string-like types --- src/services/completions.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index dc01ea8ede4b9..a1e404ef8aecb 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1860,7 +1860,38 @@ 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; From 79ecf5f60b4078e38dcba1f594285fb6fb3e0914 Mon Sep 17 00:00:00 2001 From: Arthur Adams Date: Sat, 9 Aug 2025 20:45:09 +0300 Subject: [PATCH 2/4] Fix CI/Formatting issues --- src/services/completions.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index a1e404ef8aecb..c0f3f4d1b7abe 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1862,27 +1862,25 @@ function createCompletionEntry( ) { // 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) { + } + 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)) - ); - + const allTypesAreStringLikeOrUndefined = every(unionType.types, type => !!(type.flags & (TypeFlags.StringLike | TypeFlags.Undefined) || isStringAndEmptyAnonymousObjectIntersection(type))); + if (allTypesAreStringLikeOrUndefined) { shouldUseQuotes = true; - } else { + } + 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)) - ); - + 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) { @@ -1890,7 +1888,7 @@ function createCompletionEntry( } } } - + if (shouldUseQuotes) { // If is string like or undefined use quotes insertText = `${escapeSnippetText(name)}=${quote(sourceFile, preferences, "$1")}`; From 9c099c2b7cb3bcb790189eee3d93356007b751d4 Mon Sep 17 00:00:00 2001 From: Arthur Adams Date: Sat, 9 Aug 2025 20:59:40 +0300 Subject: [PATCH 3/4] Add test case --- ...sxAttributeCompletionStyleAutoSignalish.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/cases/fourslash/jsxAttributeCompletionStyleAutoSignalish.ts 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, + } +}); From 9aee3c31e9aa4e731af7e387bdcb7404868b3c30 Mon Sep 17 00:00:00 2001 From: Arthur Adams Date: Tue, 12 Aug 2025 21:38:02 +0300 Subject: [PATCH 4/4] Rename variable --- src/services/completions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index c0f3f4d1b7abe..71524b5467364 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1879,11 +1879,11 @@ function createCompletionEntry( // 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))); + const hasPrimitiveTypes = 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) { + // If the union has string-like types and contains primitive types (string, undefined, null), + // prefer quotes since users commonly want to provide string values rather than complex objects + if (hasStringLikeTypes && hasPrimitiveTypes) { shouldUseQuotes = true; } }