From d14d8ff36ceb75e2473d7696d513717540184ded Mon Sep 17 00:00:00 2001
From: Jake Bailey <5341706+jakebailey@users.noreply.github.com>
Date: Wed, 12 Jan 2022 13:05:07 -0800
Subject: [PATCH 1/2] Rewrite logic for JSX attribute completion detection
---
src/services/completions.ts | 44 ++++++++++-
.../jsxAttributeSnippetCompletionClosed.ts | 74 +++++++++++++++++++
.../jsxAttributeSnippetCompletionUnclosed.ts | 74 +++++++++++++++++++
.../fourslash/jsxTagNameCompletionClosed.ts | 54 ++++++++++++++
.../fourslash/jsxTagNameCompletionUnclosed.ts | 54 ++++++++++++++
.../jsxTagNameCompletionUnderElementClosed.ts | 35 +++++++++
...sxTagNameCompletionUnderElementUnclosed.ts | 35 +++++++++
7 files changed, 367 insertions(+), 3 deletions(-)
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 bb19bf2798087..fce0f15cc3171 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -744,8 +744,46 @@ namespace ts.Completions {
}
}
- const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
- if (kind === ScriptElementKind.jsxAttribute && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") {
+ const isJSXAttributeCompletion = contextToken && forEachAncestor(contextToken, (n) => {
+ if (isJsxAttributeLike(n)) {
+ return true;
+ }
+
+ if (isJsxFragment(n) || isJsxOpeningFragment(n) || isJsxClosingFragment(n)) {
+ return false;
+ }
+
+ if (isJsxOpeningElement(n) || isJsxSelfClosingElement(n) || isJsxClosingElement(n)) {
+ if (contextToken.getEnd() <= n.tagName.getFullStart()) {
+ // Definitely completing part of the tag name.
+ return false;
+ }
+
+ if (rangeContainsRange(n.tagName, contextToken)) {
+ // We are to the right of the tag name, as the context is there.
+ // figure out where we are based on where the location is.
+
+ if (contextToken.kind === SyntaxKind.DotToken || contextToken.kind === SyntaxKind.QuestionDotToken) {
+ // Unfinished dotted tag name.
+ return false;
+ }
+
+ if (!rangeContainsRange(n, location)) {
+ // Unclosed JSX element; location is entirely outside the element.
+ return true;
+ }
+
+ if (n.tagName.getEnd() <= location.getFullStart()) {
+ // After existing attributes, so is another attribute.
+ return true;
+ }
+ }
+
+ return false;
+ }
+ });
+
+ if (isJSXAttributeCompletion && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") {
let useBraces = preferences.jsxAttributeCompletionStyle === "braces";
const type = typeChecker.getTypeOfSymbolAtLocation(symbol, location);
@@ -790,7 +828,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,
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" } },
+)
From 2983d5293252a74f668a1c9e179d72b2f9e88e66 Mon Sep 17 00:00:00 2001
From: Jake Bailey <5341706+jakebailey@users.noreply.github.com>
Date: Thu, 13 Jan 2022 22:54:42 -0800
Subject: [PATCH 2/2] Use the flags that are already set up for this, grr
---
src/services/completions.ts | 58 ++++++++++---------------------------
1 file changed, 16 insertions(+), 42 deletions(-)
diff --git a/src/services/completions.ts b/src/services/completions.ts
index fce0f15cc3171..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,46 +751,7 @@ namespace ts.Completions {
}
}
- const isJSXAttributeCompletion = contextToken && forEachAncestor(contextToken, (n) => {
- if (isJsxAttributeLike(n)) {
- return true;
- }
-
- if (isJsxFragment(n) || isJsxOpeningFragment(n) || isJsxClosingFragment(n)) {
- return false;
- }
-
- if (isJsxOpeningElement(n) || isJsxSelfClosingElement(n) || isJsxClosingElement(n)) {
- if (contextToken.getEnd() <= n.tagName.getFullStart()) {
- // Definitely completing part of the tag name.
- return false;
- }
-
- if (rangeContainsRange(n.tagName, contextToken)) {
- // We are to the right of the tag name, as the context is there.
- // figure out where we are based on where the location is.
-
- if (contextToken.kind === SyntaxKind.DotToken || contextToken.kind === SyntaxKind.QuestionDotToken) {
- // Unfinished dotted tag name.
- return false;
- }
-
- if (!rangeContainsRange(n, location)) {
- // Unclosed JSX element; location is entirely outside the element.
- return true;
- }
-
- if (n.tagName.getEnd() <= location.getFullStart()) {
- // After existing attributes, so is another attribute.
- return true;
- }
- }
-
- return false;
- }
- });
-
- if (isJSXAttributeCompletion && 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);
@@ -1224,6 +1192,8 @@ namespace ts.Completions {
recommendedCompletion?: Symbol,
symbolToOriginInfoMap?: SymbolOriginInfoMap,
symbolToSortTextIdMap?: SymbolSortTextIdMap,
+ isJsxIdentifierExpected?: boolean,
+ isRightOfOpenTag?: boolean,
): UniqueNameSet {
const start = timestamp();
const variableDeclaration = getVariableDeclaration(location);
@@ -1266,6 +1236,8 @@ namespace ts.Completions {
preferences,
kind,
formatContext,
+ isJsxIdentifierExpected,
+ isRightOfOpenTag,
);
if (!entry) {
continue;
@@ -1618,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;
}
@@ -2025,6 +1998,7 @@ namespace ts.Completions {
symbolToSortTextIdMap,
isTypeOnlyLocation,
isJsxIdentifierExpected,
+ isRightOfOpenTag,
importCompletionNode,
hasUnresolvedAutoImports,
};