diff --git a/src/services/completions.ts b/src/services/completions.ts index ca265f268f035..e2d56f77dd810 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -739,8 +739,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); @@ -785,7 +823,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/src/services/symbolDisplay.ts b/src/services/symbolDisplay.ts index f1447b30cd9b5..c7ee58ff28e63 100644 --- a/src/services/symbolDisplay.ts +++ b/src/services/symbolDisplay.ts @@ -83,18 +83,8 @@ namespace ts.SymbolDisplay { } return unionPropertyKind; } - // If we requested completions after `x.` at the top-level, we may be at a source file location. - switch (location.parent && location.parent.kind) { - // If we've typed a character of the attribute name, will be 'JsxAttribute', else will be 'JsxOpeningElement'. - case SyntaxKind.JsxOpeningElement: - case SyntaxKind.JsxElement: - case SyntaxKind.JsxSelfClosingElement: - return location.kind === SyntaxKind.Identifier ? ScriptElementKind.memberVariableElement : ScriptElementKind.jsxAttribute; - case SyntaxKind.JsxAttribute: - return ScriptElementKind.jsxAttribute; - default: - return ScriptElementKind.memberVariableElement; - } + + return ScriptElementKind.memberVariableElement; } return ScriptElementKind.unknown; diff --git a/src/services/types.ts b/src/services/types.ts index b9bd9f2177d5b..b30583bb0f4e8 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1456,6 +1456,7 @@ namespace ts { /** * + * @deprecated */ jsxAttribute = "JSX attribute", diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 4df9e6328ecbd..cb1d033b1578c 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6593,6 +6593,7 @@ declare namespace ts { externalModuleName = "external module name", /** * + * @deprecated */ jsxAttribute = "JSX attribute", /** String literal */ diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index fdd55dab1f2d9..5985a23e8b361 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6593,6 +6593,7 @@ declare namespace ts { externalModuleName = "external module name", /** * + * @deprecated */ jsxAttribute = "JSX attribute", /** String literal */ diff --git a/tests/cases/fourslash/completionsInJsxTag.ts b/tests/cases/fourslash/completionsInJsxTag.ts index 4f5862e820885..8fd2fcfe4cf5a 100644 --- a/tests/cases/fourslash/completionsInJsxTag.ts +++ b/tests/cases/fourslash/completionsInJsxTag.ts @@ -26,16 +26,16 @@ verify.completions({ exact: [ { name: "aria-label", - text: "(JSX attribute) \"aria-label\": string", + text: "(property) \"aria-label\": string", documentation: "Label docs", - kind: "JSX attribute", + kind: "property", kindModifiers: "declare", }, { name: "foo", - text: "(JSX attribute) foo: string", + text: "(property) foo: string", documentation: "Doc", - kind: "JSX attribute", + kind: "property", kindModifiers: "declare", }, ], diff --git a/tests/cases/fourslash/completionsJsxAttribute.ts b/tests/cases/fourslash/completionsJsxAttribute.ts index 592c2c8ed3c72..3dcf49d487108 100644 --- a/tests/cases/fourslash/completionsJsxAttribute.ts +++ b/tests/cases/fourslash/completionsJsxAttribute.ts @@ -17,8 +17,8 @@ ////
; const exact: ReadonlyArray = [ - { name: "bar", kind: "JSX attribute", kindModifiers: "declare", text: "(JSX attribute) bar: string" }, - { name: "foo", kind: "JSX attribute", kindModifiers: "declare", text: "(JSX attribute) foo: boolean", documentation: "Doc" }, + { name: "bar", kind: "property", kindModifiers: "declare", text: "(property) bar: string" }, + { name: "foo", kind: "property", kindModifiers: "declare", text: "(property) foo: boolean", documentation: "Doc" }, ]; verify.completions({ marker: "", exact }); edit.insert("f"); diff --git a/tests/cases/fourslash/completionsJsxAttributeGeneric.ts b/tests/cases/fourslash/completionsJsxAttributeGeneric.ts index 9180c691f09f4..2e50a8a895e8c 100644 --- a/tests/cases/fourslash/completionsJsxAttributeGeneric.ts +++ b/tests/cases/fourslash/completionsJsxAttributeGeneric.ts @@ -11,7 +11,7 @@ marker, exact: [{ name: 'name', - kind: 'JSX attribute', + kind: 'property', kindModifiers: 'declare' }] }) diff --git a/tests/cases/fourslash/completionsJsxAttributeInitializer.ts b/tests/cases/fourslash/completionsJsxAttributeInitializer.ts index 2076c6588f4d7..f383d52375912 100644 --- a/tests/cases/fourslash/completionsJsxAttributeInitializer.ts +++ b/tests/cases/fourslash/completionsJsxAttributeInitializer.ts @@ -9,8 +9,8 @@ verify.completions({ marker: "", includes: [ { name: "x", text: "(parameter) x: number", kind: "parameter", insertText: "{x}" }, - { name: "p", text: "(JSX attribute) p: number", kind: "JSX attribute", insertText: "{this.p}", sortText: completion.SortText.SuggestedClassMembers, source: completion.CompletionSource.ThisProperty }, - { name: "a b", text: '(JSX attribute) "a b": number', kind: "JSX attribute", insertText: '{this["a b"]}', sortText: completion.SortText.SuggestedClassMembers, source: completion.CompletionSource.ThisProperty }, + { name: "p", text: "(property) p: number", kind: "property", insertText: "{this.p}", sortText: completion.SortText.SuggestedClassMembers, source: completion.CompletionSource.ThisProperty }, + { name: "a b", text: '(property) "a b": number', kind: "property", insertText: '{this["a b"]}', sortText: completion.SortText.SuggestedClassMembers, source: completion.CompletionSource.ThisProperty }, ], preferences: { includeInsertTextCompletions: true, diff --git a/tests/cases/fourslash/jsxGenericQuickInfo.tsx b/tests/cases/fourslash/jsxGenericQuickInfo.tsx index 7566193737651..9e1ff3b84b083 100644 --- a/tests/cases/fourslash/jsxGenericQuickInfo.tsx +++ b/tests/cases/fourslash/jsxGenericQuickInfo.tsx @@ -29,6 +29,6 @@ verify.quickInfoAt("0", "(property) PropsA.renderItem: (item: number) => string"); verify.quickInfoAt("1", "(parameter) item: number"); verify.quickInfoAt("2", `(property) PropsA.name: "A"`, 'comments for A'); -verify.quickInfoAt("3", "(JSX attribute) PropsA.renderItem: (item: number) => string"); +verify.quickInfoAt("3", "(property) PropsA.renderItem: (item: number) => string"); verify.quickInfoAt("4", "(parameter) item: number"); -verify.quickInfoAt("5", `(JSX attribute) PropsA.name: "A"`, 'comments for A'); +verify.quickInfoAt("5", `(property) PropsA.name: "A"`, 'comments for A'); diff --git a/tests/cases/fourslash/jsxTagNameDottedAttributeSnippet.ts b/tests/cases/fourslash/jsxTagNameDottedAttributeSnippet.ts new file mode 100644 index 0000000000000..98d4c92e20493 --- /dev/null +++ b/tests/cases/fourslash/jsxTagNameDottedAttributeSnippet.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: {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 +////} + +verify.completions( + { + marker: test.markers(), + includes: [ + { + name: "className", + insertText: "className={$1}", + isSnippet: true, + sortText: completion.SortText.OptionalMember, + text: "(property) className?: string" + } + ], + preferences: { + jsxAttributeCompletionStyle: "braces", + includeCompletionsWithSnippetText: true, + includeCompletionsWithInsertText: true, + }, + } +) diff --git a/tests/cases/fourslash/jsxTagNameDottedNoSnippet.ts b/tests/cases/fourslash/jsxTagNameDottedNoSnippet.ts new file mode 100644 index 0000000000000..515c631c75ec0 --- /dev/null +++ b/tests/cases/fourslash/jsxTagNameDottedNoSnippet.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: "(property) NestedInterface.Foo: NestedInterface" } }, + { marker: "4", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } }, + { marker: "5", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } }, + { marker: "6", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } }, +) diff --git a/tests/cases/fourslash/jsxTagNameDottedNoSnippetClosed.ts b/tests/cases/fourslash/jsxTagNameDottedNoSnippetClosed.ts new file mode 100644 index 0000000000000..144c53ba60654 --- /dev/null +++ b/tests/cases/fourslash/jsxTagNameDottedNoSnippetClosed.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: "(property) NestedInterface.Foo: NestedInterface" } }, + { marker: "4", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } }, + { marker: "5", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } }, + { marker: "6", preferences, includes: { name: "Foo", text: "(property) NestedInterface.Foo: NestedInterface" } }, +) diff --git a/tests/cases/fourslash/jsxTagNameNoSnippet.ts b/tests/cases/fourslash/jsxTagNameNoSnippet.ts new file mode 100644 index 0000000000000..db243b1102558 --- /dev/null +++ b/tests/cases/fourslash/jsxTagNameNoSnippet.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: "(property) JSX.IntrinsicElements.button: any" } }, + { marker: "2", preferences, includes: { name: "button", text: "(property) JSX.IntrinsicElements.button: any" } }, + { marker: "3", preferences, includes: { name: "button", text: "(property) JSX.IntrinsicElements.button: any" } }, +) diff --git a/tests/cases/fourslash/tsxCompletion12.ts b/tests/cases/fourslash/tsxCompletion12.ts index 4b0b01c2241d0..c4feb1ae51ae7 100644 --- a/tests/cases/fourslash/tsxCompletion12.ts +++ b/tests/cases/fourslash/tsxCompletion12.ts @@ -28,14 +28,14 @@ verify.completions( exact: [ "propString", "propx", - { name: "optional", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + { name: "optional", kind: "property", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, ] }, { marker: "3", exact: [ "propString", - { name: "optional", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + { name: "optional", kind: "property", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, ] }, { marker: "4", exact: "propString" }, diff --git a/tests/cases/fourslash/tsxCompletion13.ts b/tests/cases/fourslash/tsxCompletion13.ts index b3dc913783a40..5b8a8d7e18b41 100644 --- a/tests/cases/fourslash/tsxCompletion13.ts +++ b/tests/cases/fourslash/tsxCompletion13.ts @@ -36,8 +36,8 @@ verify.completions( exact: [ "goTo", "onClick", - { name: "children", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, - { name: "className", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + { name: "children", kind: "property", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + { name: "className", kind: "property", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, ] }, { @@ -45,14 +45,14 @@ verify.completions( exact: [ "goTo", "onClick", - { name: "className", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + { name: "className", kind: "property", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, ] }, { marker: ["3", "4", "5"], exact: [ - { name: "children", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, - { name: "className", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + { name: "children", kind: "property", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + { name: "className", kind: "property", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, ] }, ); diff --git a/tests/cases/fourslash/tsxCompletion7.ts b/tests/cases/fourslash/tsxCompletion7.ts index 90a98c4f5a0d0..1d31441ca1b94 100644 --- a/tests/cases/fourslash/tsxCompletion7.ts +++ b/tests/cases/fourslash/tsxCompletion7.ts @@ -13,7 +13,7 @@ verify.completions({ marker: "", exact: [ - { name: "TWO", kind: "JSX attribute", kindModifiers: "declare", sortText: completion.SortText.LocationPriority }, - { name: "ONE", kind: "JSX attribute", kindModifiers: "declare", sortText: completion.SortText.MemberDeclaredBySpreadAssignment }, + { name: "TWO", kind: "property", kindModifiers: "declare", sortText: completion.SortText.LocationPriority }, + { name: "ONE", kind: "property", kindModifiers: "declare", sortText: completion.SortText.MemberDeclaredBySpreadAssignment }, ] }); diff --git a/tests/cases/fourslash/tsxQuickInfo3.ts b/tests/cases/fourslash/tsxQuickInfo3.ts index 614ffa878ef09..068fcab19f51f 100644 --- a/tests/cases/fourslash/tsxQuickInfo3.ts +++ b/tests/cases/fourslash/tsxQuickInfo3.ts @@ -24,7 +24,7 @@ verify.quickInfos({ 1: "class Opt", - 2: "(JSX attribute) propx: number", + 2: "(property) propx: number", 3: "const obj1: OptionProp", - 4: "(JSX attribute) propx: true" + 4: "(property) propx: true" }); diff --git a/tests/cases/fourslash/tsxQuickInfo4.ts b/tests/cases/fourslash/tsxQuickInfo4.ts index 7e1f8e2bc7ba0..f66d4dc4afd9a 100644 --- a/tests/cases/fourslash/tsxQuickInfo4.ts +++ b/tests/cases/fourslash/tsxQuickInfo4.ts @@ -48,8 +48,8 @@ verify.quickInfos({ 1: "function MainButton(linkProps: LinkProps): JSX.Element (+1 overload)", - 2: "(JSX attribute) LinkProps.to: string", + 2: "(property) LinkProps.to: string", 3: "function MainButton(buttonProps: ButtonProps): JSX.Element (+1 overload)", 4: "(method) ButtonProps.onClick(event?: React.MouseEvent): void", - 5: "(JSX attribute) extra-prop: true" + 5: "(property) extra-prop: true" }); diff --git a/tests/cases/fourslash/tsxQuickInfo5.ts b/tests/cases/fourslash/tsxQuickInfo5.ts index 3ebf1bd0bdd04..35f821fe6b28d 100644 --- a/tests/cases/fourslash/tsxQuickInfo5.ts +++ b/tests/cases/fourslash/tsxQuickInfo5.ts @@ -13,6 +13,6 @@ verify.quickInfos({ 1: "function ComponentWithTwoAttributes(l: {\n key1: T;\n value: U;\n}): JSX.Element", - 2: "(JSX attribute) key1: T", - 3: "(JSX attribute) value: U", + 2: "(property) key1: T", + 3: "(property) value: U", });