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