From 8a8048fb625dfbf008d0110f2e8ecff8bb9d7de6 Mon Sep 17 00:00:00 2001
From: Jake Bailey <5341706+jakebailey@users.noreply.github.com>
Date: Fri, 14 Jan 2022 10:15:58 -0800
Subject: [PATCH] Rewrite logic for JSX attribute completion detection [4.5
cherry pick] (#47413)
---
src/services/completions.ts | 39 +++++-----
.../jsxAttributeAsTagNameNoSnippet.ts | 61 ---------------
.../jsxAttributeSnippetCompletionClosed.ts | 74 +++++++++++++++++++
.../jsxAttributeSnippetCompletionUnclosed.ts | 74 +++++++++++++++++++
.../fourslash/jsxTagNameCompletionClosed.ts | 54 ++++++++++++++
.../fourslash/jsxTagNameCompletionUnclosed.ts | 54 ++++++++++++++
.../jsxTagNameCompletionUnderElementClosed.ts | 35 +++++++++
...sxTagNameCompletionUnderElementUnclosed.ts | 35 +++++++++
8 files changed, 343 insertions(+), 83 deletions(-)
delete mode 100644 tests/cases/fourslash/jsxAttributeAsTagNameNoSnippet.ts
create mode 100644 tests/cases/fourslash/jsxAttributeSnippetCompletionClosed.ts
create mode 100644 tests/cases/fourslash/jsxAttributeSnippetCompletionUnclosed.ts
create mode 100644 tests/cases/fourslash/jsxTagNameCompletionClosed.ts
create mode 100644 tests/cases/fourslash/jsxTagNameCompletionUnclosed.ts
create mode 100644 tests/cases/fourslash/jsxTagNameCompletionUnderElementClosed.ts
create mode 100644 tests/cases/fourslash/jsxTagNameCompletionUnderElementUnclosed.ts
diff --git a/src/services/completions.ts b/src/services/completions.ts
index b0b7075191462..3f351f932d352 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -428,6 +428,7 @@ namespace ts.Completions {
isJsxInitializer,
isTypeOnlyLocation,
isJsxIdentifierExpected,
+ isRightOfOpenTag,
importCompletionNode,
insideJsDocTagTypeExpression,
symbolToSortTextIdMap,
@@ -466,7 +467,9 @@ namespace ts.Completions {
importCompletionNode,
recommendedCompletion,
symbolToOriginInfoMap,
- symbolToSortTextIdMap
+ symbolToSortTextIdMap,
+ isJsxIdentifierExpected,
+ isRightOfOpenTag,
);
getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries); // TODO: GH#18217
}
@@ -496,7 +499,9 @@ namespace ts.Completions {
importCompletionNode,
recommendedCompletion,
symbolToOriginInfoMap,
- symbolToSortTextIdMap
+ symbolToSortTextIdMap,
+ isJsxIdentifierExpected,
+ isRightOfOpenTag,
);
}
@@ -638,6 +643,8 @@ namespace ts.Completions {
options: CompilerOptions,
preferences: UserPreferences,
completionKind: CompletionKind,
+ isJsxIdentifierExpected: boolean | undefined,
+ isRightOfOpenTag: boolean | undefined,
): CompletionEntry | undefined {
let insertText: string | undefined;
let replacementSpan = getReplacementSpanForContextToken(replacementToken);
@@ -713,25 +720,7 @@ namespace ts.Completions {
}
}
- // Before offering up a JSX attribute snippet, ensure that we aren't potentially completing
- // a tag name; this may appear as an attribute after the "<" when the tag has not yet been
- // closed, as in:
- //
- // return <>
- // foo
- //
- // We can detect this case by checking if both:
- //
- // 1. The location is "<", so we are completing immediately after it.
- // 2. The "<" has the same position as its parent, so is not a binary expression.
- const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
- if (
- kind === ScriptElementKind.jsxAttribute
- && (location.kind !== SyntaxKind.LessThanToken || location.pos !== location.parent.pos)
- && 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);
@@ -776,7 +765,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,
@@ -1142,6 +1131,8 @@ namespace ts.Completions {
recommendedCompletion?: Symbol,
symbolToOriginInfoMap?: SymbolOriginInfoMap,
symbolToSortTextIdMap?: SymbolSortTextIdMap,
+ isJsxIdentifierExpected?: boolean,
+ isRightOfOpenTag?: boolean,
): UniqueNameSet {
const start = timestamp();
const variableDeclaration = getVariableDeclaration(location);
@@ -1183,6 +1174,8 @@ namespace ts.Completions {
compilerOptions,
preferences,
kind,
+ isJsxIdentifierExpected,
+ isRightOfOpenTag,
);
if (!entry) {
continue;
@@ -1534,6 +1527,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;
}
@@ -1941,6 +1935,7 @@ namespace ts.Completions {
symbolToSortTextIdMap,
isTypeOnlyLocation,
isJsxIdentifierExpected,
+ isRightOfOpenTag,
importCompletionNode,
hasUnresolvedAutoImports,
};
diff --git a/tests/cases/fourslash/jsxAttributeAsTagNameNoSnippet.ts b/tests/cases/fourslash/jsxAttributeAsTagNameNoSnippet.ts
deleted file mode 100644
index 40766cf294619..0000000000000
--- a/tests/cases/fourslash/jsxAttributeAsTagNameNoSnippet.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-///
-//@Filename: file.tsx
-////declare namespace JSX {
-//// interface IntrinsicElements {
-//// button: any;
-//// div: any;
-//// }
-////}
-////function fn() {
-//// return <>
-//// ;
-////}
-////function fn2() {
-//// return <>
-//// preceding junk ;
-////}
-////function fn3() {
-//// return <>
-//// ;
-////}
-
-
-
-verify.completions(
- {
- marker: "1",
- includes: [
- { name: "button", insertText: undefined, isSnippet: undefined }
- ],
- preferences: {
- jsxAttributeCompletionStyle: "braces",
- includeCompletionsWithSnippetText: true,
- includeCompletionsWithInsertText: true,
- }
- },
- {
- marker: "2",
- includes: [
- { name: "button", insertText: undefined, isSnippet: undefined }
- ],
- preferences: {
- jsxAttributeCompletionStyle: "braces",
- includeCompletionsWithSnippetText: true,
- includeCompletionsWithInsertText: true,
- }
- },
- {
- marker: "3",
- includes: [
- { name: "button", insertText: undefined, isSnippet: undefined }
- ],
- preferences: {
- jsxAttributeCompletionStyle: "braces",
- includeCompletionsWithSnippetText: true,
- includeCompletionsWithInsertText: true,
- }
- },
-);
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" } },
+)