From 27b29cda6a0544e19ce9e42bbd44701468b34baa Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 12 Jan 2022 13:24:12 -0800 Subject: [PATCH 1/3] Revert "Cherry-pick PR #47096 into release-4.5 (#47105)" This reverts commit 1d4ec40130bae10f4b955bd7672054e34cba8caa. --- src/services/completions.ts | 19 +----- .../jsxAttributeAsTagNameNoSnippet.ts | 61 ------------------- 2 files changed, 1 insertion(+), 79 deletions(-) delete mode 100644 tests/cases/fourslash/jsxAttributeAsTagNameNoSnippet.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index b0b7075191462..9961a699945f7 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -713,25 +713,8 @@ 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 (kind === ScriptElementKind.jsxAttribute && preferences.includeCompletionsWithSnippetText && preferences.jsxAttributeCompletionStyle && preferences.jsxAttributeCompletionStyle !== "none") { let useBraces = preferences.jsxAttributeCompletionStyle === "braces"; const type = typeChecker.getTypeOfSymbolAtLocation(symbol, location); 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, - } - }, -); From 133e191c9bf5187a5a8306483af85846c97702de 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 2/3] 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 9961a699945f7..2c6acde8ac589 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -713,8 +713,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); @@ -759,7 +797,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 +//// +//// +////} +////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 +//// +////} +////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 f1dd5d698e497b421309bee5221589139d87a705 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 3/3] 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 2c6acde8ac589..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,46 +720,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); @@ -1163,6 +1131,8 @@ namespace ts.Completions { recommendedCompletion?: Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, symbolToSortTextIdMap?: SymbolSortTextIdMap, + isJsxIdentifierExpected?: boolean, + isRightOfOpenTag?: boolean, ): UniqueNameSet { const start = timestamp(); const variableDeclaration = getVariableDeclaration(location); @@ -1204,6 +1174,8 @@ namespace ts.Completions { compilerOptions, preferences, kind, + isJsxIdentifierExpected, + isRightOfOpenTag, ); if (!entry) { continue; @@ -1555,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; } @@ -1962,6 +1935,7 @@ namespace ts.Completions { symbolToSortTextIdMap, isTypeOnlyLocation, isJsxIdentifierExpected, + isRightOfOpenTag, importCompletionNode, hasUnresolvedAutoImports, };