diff --git a/src/services/completions.ts b/src/services/completions.ts
index bb19bf2798087..b91e49e757aab 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -456,6 +456,7 @@ namespace ts.Completions {
isJsxInitializer,
isTypeOnlyLocation,
isJsxIdentifierExpected,
+ isRightOfOpenTag,
importCompletionNode,
insideJsDocTagTypeExpression,
symbolToSortTextIdMap,
@@ -495,7 +496,9 @@ namespace ts.Completions {
importCompletionNode,
recommendedCompletion,
symbolToOriginInfoMap,
- symbolToSortTextIdMap
+ symbolToSortTextIdMap,
+ isJsxIdentifierExpected,
+ isRightOfOpenTag,
);
getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries); // TODO: GH#18217
}
@@ -526,7 +529,9 @@ namespace ts.Completions {
importCompletionNode,
recommendedCompletion,
symbolToOriginInfoMap,
- symbolToSortTextIdMap
+ symbolToSortTextIdMap,
+ isJsxIdentifierExpected,
+ isRightOfOpenTag,
);
}
@@ -669,6 +674,8 @@ namespace ts.Completions {
preferences: UserPreferences,
completionKind: CompletionKind,
formatContext: formatting.FormatContext | undefined,
+ isJsxIdentifierExpected: boolean | undefined,
+ isRightOfOpenTag: boolean | undefined,
): CompletionEntry | undefined {
let insertText: string | undefined;
let replacementSpan = getReplacementSpanForContextToken(replacementToken);
@@ -744,8 +751,7 @@ namespace ts.Completions {
}
}
- const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
- if (kind === ScriptElementKind.jsxAttribute && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") {
+ if (isJsxIdentifierExpected && !isRightOfOpenTag && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") {
let useBraces = preferences.jsxAttributeCompletionStyle === "braces";
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, location);
@@ -790,7 +796,7 @@ namespace ts.Completions {
// entries (like JavaScript identifier entries).
return {
name,
- kind,
+ kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location),
kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol),
sortText,
source,
@@ -1186,6 +1192,8 @@ namespace ts.Completions {
recommendedCompletion?: Symbol,
symbolToOriginInfoMap?: SymbolOriginInfoMap,
symbolToSortTextIdMap?: SymbolSortTextIdMap,
+ isJsxIdentifierExpected?: boolean,
+ isRightOfOpenTag?: boolean,
): UniqueNameSet {
const start = timestamp();
const variableDeclaration = getVariableDeclaration(location);
@@ -1228,6 +1236,8 @@ namespace ts.Completions {
preferences,
kind,
formatContext,
+ isJsxIdentifierExpected,
+ isRightOfOpenTag,
);
if (!entry) {
continue;
@@ -1580,6 +1590,7 @@ namespace ts.Completions {
readonly isTypeOnlyLocation: boolean;
/** In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. */
readonly isJsxIdentifierExpected: boolean;
+ readonly isRightOfOpenTag: boolean;
readonly importCompletionNode?: Node;
readonly hasUnresolvedAutoImports?: boolean;
}
@@ -1987,6 +1998,7 @@ namespace ts.Completions {
symbolToSortTextIdMap,
isTypeOnlyLocation,
isJsxIdentifierExpected,
+ isRightOfOpenTag,
importCompletionNode,
hasUnresolvedAutoImports,
};
diff --git a/tests/cases/fourslash/jsxAttributeSnippetCompletionClosed.ts b/tests/cases/fourslash/jsxAttributeSnippetCompletionClosed.ts
new file mode 100644
index 0000000000000..ddb12244867c5
--- /dev/null
+++ b/tests/cases/fourslash/jsxAttributeSnippetCompletionClosed.ts
@@ -0,0 +1,74 @@
+///
+//@Filename: file.tsx
+////interface NestedInterface {
+//// Foo: NestedInterface;
+//// (props: {className?: string}): any;
+////}
+////
+////declare const Foo: NestedInterface;
+////
+////function fn1() {
+//// return
+////
+////
+////}
+////function fn2() {
+//// return
+////
+////
+////}
+////function fn3() {
+//// return
+////
+////
+////}
+////function fn4() {
+//// return
+////
+////
+////}
+////function fn5() {
+//// return
+////
+////
+////}
+////function fn6() {
+//// return
+////
+////
+////}
+////function fn7() {
+//// return
+////}
+////function fn8() {
+//// return
+////}
+////function fn9() {
+//// return
+////}
+////function fn10() {
+//// return
+////}
+////function fn11() {
+//// return
+////}
+
+var preferences: FourSlashInterface.UserPreferences = {
+ jsxAttributeCompletionStyle: "braces",
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+};
+
+verify.completions(
+ { marker: "1", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "2", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "3", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "4", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "5", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "6", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "7", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "8", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "9", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "10", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+ { marker: "11", preferences, includes: { name: "className", insertText: "className={$1}", text: "(JSX attribute) className?: string", isSnippet: true, sortText: completion.SortText.OptionalMember } },
+)
diff --git a/tests/cases/fourslash/jsxAttributeSnippetCompletionUnclosed.ts b/tests/cases/fourslash/jsxAttributeSnippetCompletionUnclosed.ts
new file mode 100644
index 0000000000000..7099e13c2057f
--- /dev/null
+++ b/tests/cases/fourslash/jsxAttributeSnippetCompletionUnclosed.ts
@@ -0,0 +1,74 @@
+///
+//@Filename: file.tsx
+////interface NestedInterface {
+//// Foo: NestedInterface;
+//// (props: {className?: string}): any;
+////}
+////
+////declare const Foo: NestedInterface;
+////
+////function fn1() {
+//// return
+////
+////}
+////function fn2() {
+//// return
+////
+////}
+////function fn3() {
+//// return
+////
+////}
+////function fn4() {
+//// return
+////
+////}
+////function fn5() {
+//// return
+////
+////}
+////function fn6() {
+//// return
+////
+////}
+////function fn7() {
+//// return
+//@Filename: file.tsx
+////interface NestedInterface {
+//// Foo: NestedInterface;
+//// (props: {}): any;
+////}
+////
+////declare const Foo: NestedInterface;
+////
+////function fn1() {
+//// return
+//// *1*/ />
+////
+////}
+////function fn2() {
+//// return
+////
+////
+////}
+////function fn3() {
+//// return
+////
+////
+////}
+////function fn4() {
+//// return
+////
+////
+////}
+////function fn5() {
+//// return
+////
+////
+////}
+////function fn6() {
+//// return
+////
+////
+////}
+
+var preferences: FourSlashInterface.UserPreferences = {
+ jsxAttributeCompletionStyle: "braces",
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+};
+
+verify.completions(
+ { marker: "1", preferences, includes: { name: "Foo", text: "const Foo: NestedInterface" } },
+ { marker: "2", preferences, includes: { name: "Foo", text: "const Foo: NestedInterface" } },
+ { marker: "3", preferences, includes: { name: "Foo", text: "(JSX attribute) NestedInterface.Foo: NestedInterface" } },
+ { marker: "4", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } },
+ { marker: "5", preferences, includes: { name: "Foo", text: "(JSX attribute) NestedInterface.Foo: NestedInterface" } },
+ { marker: "6", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } },
+)
diff --git a/tests/cases/fourslash/jsxTagNameCompletionUnclosed.ts b/tests/cases/fourslash/jsxTagNameCompletionUnclosed.ts
new file mode 100644
index 0000000000000..fe62c44247a1e
--- /dev/null
+++ b/tests/cases/fourslash/jsxTagNameCompletionUnclosed.ts
@@ -0,0 +1,54 @@
+///
+//@Filename: file.tsx
+////interface NestedInterface {
+//// Foo: NestedInterface;
+//// (props: {}): any;
+////}
+////
+////declare const Foo: NestedInterface;
+////
+////function fn1() {
+//// return
+//// *1*/
+////
+////}
+////function fn2() {
+//// return
+////
+////}
+////function fn3() {
+//// return
+////
+////}
+////function fn4() {
+//// return
+////
+////}
+////function fn5() {
+//// return
+////
+////}
+////function fn6() {
+//// return
+////
+////}
+
+var preferences: FourSlashInterface.UserPreferences = {
+ jsxAttributeCompletionStyle: "braces",
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+};
+
+verify.completions(
+ { marker: "1", preferences, includes: { name: "Foo", text: "const Foo: NestedInterface" } },
+ { marker: "2", preferences, includes: { name: "Foo", text: "const Foo: NestedInterface" } },
+ { marker: "3", preferences, includes: { name: "Foo", text: "(JSX attribute) NestedInterface.Foo: NestedInterface" } },
+ { marker: "4", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } },
+ { marker: "5", preferences, includes: { name: "Foo", text: "(JSX attribute) NestedInterface.Foo: NestedInterface" } },
+ { marker: "6", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } },
+)
diff --git a/tests/cases/fourslash/jsxTagNameCompletionUnderElementClosed.ts b/tests/cases/fourslash/jsxTagNameCompletionUnderElementClosed.ts
new file mode 100644
index 0000000000000..0ebb48011fc62
--- /dev/null
+++ b/tests/cases/fourslash/jsxTagNameCompletionUnderElementClosed.ts
@@ -0,0 +1,35 @@
+///
+//@Filename: file.tsx
+////declare namespace JSX {
+//// interface IntrinsicElements {
+//// button: any;
+//// div: any;
+//// }
+////}
+////function fn() {
+//// return <>
+////
+//// >;
+////}
+////function fn2() {
+//// return <>
+//// preceding junk
+//// >;
+////}
+////function fn3() {
+//// return <>
+////
+//// >;
+////}
+
+var preferences: FourSlashInterface.UserPreferences = {
+ jsxAttributeCompletionStyle: "braces",
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+};
+
+verify.completions(
+ { marker: "1", preferences, includes: { name: "button", text: "(JSX attribute) JSX.IntrinsicElements.button: any" } },
+ { marker: "2", preferences, includes: { name: "button", text: "(JSX attribute) JSX.IntrinsicElements.button: any" } },
+ { marker: "3", preferences, includes: { name: "button", text: "(JSX attribute) JSX.IntrinsicElements.button: any" } },
+)
diff --git a/tests/cases/fourslash/jsxTagNameCompletionUnderElementUnclosed.ts b/tests/cases/fourslash/jsxTagNameCompletionUnderElementUnclosed.ts
new file mode 100644
index 0000000000000..e037477a7ab82
--- /dev/null
+++ b/tests/cases/fourslash/jsxTagNameCompletionUnderElementUnclosed.ts
@@ -0,0 +1,35 @@
+///
+//@Filename: file.tsx
+////declare namespace JSX {
+//// interface IntrinsicElements {
+//// button: any;
+//// div: any;
+//// }
+////}
+////function fn() {
+//// return <>
+//// ;
+////}
+////function fn2() {
+//// return <>
+//// preceding junk ;
+////}
+////function fn3() {
+//// return <>
+//// ;
+////}
+
+var preferences: FourSlashInterface.UserPreferences = {
+ jsxAttributeCompletionStyle: "braces",
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+};
+
+verify.completions(
+ { marker: "1", preferences, includes: { name: "button", text: "(JSX attribute) JSX.IntrinsicElements.button: any" } },
+ { marker: "2", preferences, includes: { name: "button", text: "(JSX attribute) JSX.IntrinsicElements.button: any" } },
+ { marker: "3", preferences, includes: { name: "button", text: "(JSX attribute) JSX.IntrinsicElements.button: any" } },
+)