diff --git a/src/services/completions.ts b/src/services/completions.ts
index ca265f268f035..53f059b1e77eb 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -739,7 +739,7 @@ namespace ts.Completions {
}
}
- const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
+ const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location, contextToken);
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/src/services/symbolDisplay.ts b/src/services/symbolDisplay.ts
index f1447b30cd9b5..3151f4ce8eb59 100644
--- a/src/services/symbolDisplay.ts
+++ b/src/services/symbolDisplay.ts
@@ -3,8 +3,8 @@ namespace ts.SymbolDisplay {
const symbolDisplayNodeBuilderFlags = NodeBuilderFlags.OmitParameterModifiers | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope;
// TODO(drosen): use contextual SemanticMeaning.
- export function getSymbolKind(typeChecker: TypeChecker, symbol: Symbol, location: Node): ScriptElementKind {
- const result = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location);
+ export function getSymbolKind(typeChecker: TypeChecker, symbol: Symbol, location: Node, contextToken?: Node): ScriptElementKind {
+ const result = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, location, contextToken);
if (result !== ScriptElementKind.unknown) {
return result;
}
@@ -25,7 +25,7 @@ namespace ts.SymbolDisplay {
return result;
}
- function getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker: TypeChecker, symbol: Symbol, location: Node): ScriptElementKind {
+ function getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker: TypeChecker, symbol: Symbol, location: Node, contextToken?: Node): ScriptElementKind {
const roots = typeChecker.getRootSymbols(symbol);
// If this is a method from a mapped type, leave as a method so long as it still has a call signature.
if (roots.length === 1
@@ -83,6 +83,20 @@ namespace ts.SymbolDisplay {
}
return unionPropertyKind;
}
+
+ if (contextToken) {
+ const result = getSymbolKindOfJsxTagNameOrAttribute(location, contextToken);
+ if (result !== ScriptElementKind.unknown) {
+ return result;
+ }
+ }
+
+ if (isJsxAttribute(location) || location.parent && isJsxAttribute(location.parent) && location.parent.name === location) {
+ return ScriptElementKind.jsxAttribute;
+ }
+
+ // TODO(jakebailey): Delete the below code, once the edge cases it handles are handled above.
+
// 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'.
@@ -100,6 +114,50 @@ namespace ts.SymbolDisplay {
return ScriptElementKind.unknown;
}
+ function getSymbolKindOfJsxTagNameOrAttribute(location: Node, contextToken: Node): ScriptElementKind {
+ const symbolKindFromContext = forEachAncestor(contextToken, (n) => {
+ if (isJsxAttributeLike(n)) {
+ return ScriptElementKind.jsxAttribute;
+ }
+
+ if (isJsxFragment(n) || isJsxOpeningFragment(n) || isJsxClosingFragment(n)) {
+ return "quit";
+ }
+
+ if (isJsxOpeningElement(n) || isJsxSelfClosingElement(n) || isJsxClosingElement(n)) {
+ if (contextToken.getEnd() <= n.tagName.getFullStart()) {
+ // Definitely completing part of the tag name.
+ return ScriptElementKind.jsxTagName;
+ }
+
+ 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.
+
+ // TODO(jakebailey): This seems hacky.
+ if (contextToken.kind === SyntaxKind.DotToken || contextToken.kind === SyntaxKind.QuestionDotToken) {
+ // Unfinished dotted tag name.
+ return ScriptElementKind.jsxTagName;
+ }
+
+ if (!rangeContainsRange(n, location)) {
+ // Unclosed JSX element; location is entirely outside the element.
+ return ScriptElementKind.jsxAttribute;
+ }
+
+ if (n.tagName.getEnd() <= location.getFullStart()) {
+ // After existing attributes, so is another attribute.
+ return ScriptElementKind.jsxAttribute;
+ }
+ }
+
+ return "quit";
+ }
+ });
+
+ return symbolKindFromContext || ScriptElementKind.unknown;
+ }
+
function getNormalizedSymbolModifiers(symbol: Symbol) {
if (symbol.declarations && symbol.declarations.length) {
const [declaration, ...declarations] = symbol.declarations;
diff --git a/src/services/types.ts b/src/services/types.ts
index b9bd9f2177d5b..7df40b22c8349 100644
--- a/src/services/types.ts
+++ b/src/services/types.ts
@@ -1457,6 +1457,8 @@ namespace ts {
/**
*
*/
+ jsxTagName = "JSX tag name",
+
jsxAttribute = "JSX attribute",
/** String literal */
diff --git a/src/services/utilities.ts b/src/services/utilities.ts
index 593cbc73913f7..ec8b7f95df903 100644
--- a/src/services/utilities.ts
+++ b/src/services/utilities.ts
@@ -1106,16 +1106,16 @@ namespace ts {
* If position === end, returns the preceding token if includeItemAtEndPosition(previousToken) === true
*/
export function getTouchingToken(sourceFile: SourceFile, position: number, includePrecedingTokenAtEndPosition?: (n: Node) => boolean): Node {
- return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ false, includePrecedingTokenAtEndPosition, /*includeEndPosition*/ false);
+ return getTokenAtPositionWorker(sourceFile, position, () => false, includePrecedingTokenAtEndPosition, /*includeEndPosition*/ false);
}
/** Returns a token if position is in [start-of-leading-trivia, end) */
export function getTokenAtPosition(sourceFile: SourceFile, position: number): Node {
- return getTokenAtPositionWorker(sourceFile, position, /*allowPositionInLeadingTrivia*/ true, /*includePrecedingTokenAtEndPosition*/ undefined, /*includeEndPosition*/ false);
+ return getTokenAtPositionWorker(sourceFile, position, () => true, /*includePrecedingTokenAtEndPosition*/ undefined, /*includeEndPosition*/ false);
}
/** Get the token whose text contains the position */
- function getTokenAtPositionWorker(sourceFile: SourceFile, position: number, allowPositionInLeadingTrivia: boolean, includePrecedingTokenAtEndPosition: ((n: Node) => boolean) | undefined, includeEndPosition: boolean): Node {
+ function getTokenAtPositionWorker(sourceFile: SourceFile, position: number, allowPositionInLeadingTrivia: (n: Node) => boolean, includePrecedingTokenAtEndPosition: ((n: Node) => boolean) | undefined, includeEndPosition: boolean): Node {
let current: Node = sourceFile;
let foundToken: Node | undefined;
outer: while (true) {
@@ -1145,7 +1145,7 @@ namespace ts {
// position and whose end is greater than the position.
- const start = allowPositionInLeadingTrivia ? children[middle].getFullStart() : children[middle].getStart(sourceFile, /*includeJsDoc*/ true);
+ const start = allowPositionInLeadingTrivia(children[middle]) ? children[middle].getFullStart() : children[middle].getStart(sourceFile, /*includeJsDoc*/ true);
if (start > position) {
return Comparison.GreaterThan;
}
@@ -1180,7 +1180,7 @@ namespace ts {
}
function nodeContainsPosition(node: Node) {
- const start = allowPositionInLeadingTrivia ? node.getFullStart() : node.getStart(sourceFile, /*includeJsDoc*/ true);
+ const start = allowPositionInLeadingTrivia(node) ? node.getFullStart() : node.getStart(sourceFile, /*includeJsDoc*/ true);
if (start > position) {
// If this child begins after position, then all subsequent children will as well.
return false;
diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts
index 4df9e6328ecbd..b620c68b3cce1 100644
--- a/tests/baselines/reference/api/tsserverlibrary.d.ts
+++ b/tests/baselines/reference/api/tsserverlibrary.d.ts
@@ -6594,6 +6594,7 @@ declare namespace ts {
/**
*
*/
+ jsxTagName = "JSX tag name",
jsxAttribute = "JSX attribute",
/** String literal */
string = "string",
diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts
index fdd55dab1f2d9..11d25bd2ab52d 100644
--- a/tests/baselines/reference/api/typescript.d.ts
+++ b/tests/baselines/reference/api/typescript.d.ts
@@ -6594,6 +6594,7 @@ declare namespace ts {
/**
*
*/
+ jsxTagName = "JSX tag name",
jsxAttribute = "JSX attribute",
/** String literal */
string = "string",
diff --git a/tests/cases/fourslash/jsxTagNameDottedAttributeSnippet.ts b/tests/cases/fourslash/jsxTagNameDottedAttributeSnippet.ts
new file mode 100644
index 0000000000000..e94e05d1684a0
--- /dev/null
+++ b/tests/cases/fourslash/jsxTagNameDottedAttributeSnippet.ts
@@ -0,0 +1,69 @@
+///
+//@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 }
+ ],
+ 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..6b9b589a1bf41
--- /dev/null
+++ b/tests/cases/fourslash/jsxTagNameDottedNoSnippet.ts
@@ -0,0 +1,53 @@
+///
+//@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
+////
+////}
+
+verify.completions(
+ {
+ marker: test.markers(),
+ includes: [
+ { name: "Foo", insertText: undefined, isSnippet: undefined }
+ ],
+ preferences: {
+ jsxAttributeCompletionStyle: "braces",
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ },
+ }
+)
diff --git a/tests/cases/fourslash/jsxTagNameDottedNoSnippetClosed.ts b/tests/cases/fourslash/jsxTagNameDottedNoSnippetClosed.ts
new file mode 100644
index 0000000000000..6d1af353626b7
--- /dev/null
+++ b/tests/cases/fourslash/jsxTagNameDottedNoSnippetClosed.ts
@@ -0,0 +1,53 @@
+///
+//@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
+////
+////
+////}
+
+verify.completions(
+ {
+ marker: test.markers(),
+ includes: [
+ { name: "Foo", insertText: undefined, isSnippet: undefined }
+ ],
+ preferences: {
+ jsxAttributeCompletionStyle: "braces",
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ },
+ }
+)
diff --git a/tests/cases/fourslash/jsxTagNameNoSnippet.ts b/tests/cases/fourslash/jsxTagNameNoSnippet.ts
new file mode 100644
index 0000000000000..0f6c07b0d87a4
--- /dev/null
+++ b/tests/cases/fourslash/jsxTagNameNoSnippet.ts
@@ -0,0 +1,37 @@
+///
+//@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: test.markers(),
+ includes: [
+ { name: "button", insertText: undefined, isSnippet: undefined }
+ ],
+ preferences: {
+ jsxAttributeCompletionStyle: "braces",
+ includeCompletionsWithSnippetText: true,
+ includeCompletionsWithInsertText: true,
+ }
+ }
+);
diff --git a/tests/cases/fourslash/tsxFindAllReferences11.ts b/tests/cases/fourslash/tsxFindAllReferences11.ts
index 8c7e7a4690937..b965b87d8f831 100644
--- a/tests/cases/fourslash/tsxFindAllReferences11.ts
+++ b/tests/cases/fourslash/tsxFindAllReferences11.ts
@@ -25,4 +25,4 @@
//// declare function MainButton(props: ButtonProps | LinkProps): JSX.Element;
//// let opt = ;
-verify.singleReferenceGroup("(property) wrong: true");
+verify.singleReferenceGroup("(JSX attribute) wrong: true");
diff --git a/tests/cases/fourslash/tsxFindAllReferences6.ts b/tests/cases/fourslash/tsxFindAllReferences6.ts
index c0e119a4e456e..8d54ddd9537e1 100644
--- a/tests/cases/fourslash/tsxFindAllReferences6.ts
+++ b/tests/cases/fourslash/tsxFindAllReferences6.ts
@@ -19,4 +19,4 @@
//// declare function Opt(attributes: OptionPropBag): JSX.Element;
//// let opt = ;
-verify.singleReferenceGroup("(property) wrong: true");
+verify.singleReferenceGroup("(JSX attribute) wrong: true");