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 +//// +//// +////} +////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" } }, +)