Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overhaul detection of JSX attributes and tag names #47096

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 41 additions & 3 deletions src/services/completions.ts
Expand Up @@ -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);

Expand Down Expand Up @@ -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,
Expand Down
14 changes: 2 additions & 12 deletions src/services/symbolDisplay.ts
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/services/types.ts
Expand Up @@ -1456,6 +1456,7 @@ namespace ts {

/**
* <JsxTagName attribute1 attribute2={0} />
* @deprecated
*/
jsxAttribute = "JSX attribute",

Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Expand Up @@ -6593,6 +6593,7 @@ declare namespace ts {
externalModuleName = "external module name",
/**
* <JsxTagName attribute1 attribute2={0} />
* @deprecated
*/
jsxAttribute = "JSX attribute",
/** String literal */
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/typescript.d.ts
Expand Up @@ -6593,6 +6593,7 @@ declare namespace ts {
externalModuleName = "external module name",
/**
* <JsxTagName attribute1 attribute2={0} />
* @deprecated
*/
jsxAttribute = "JSX attribute",
/** String literal */
Expand Down
8 changes: 4 additions & 4 deletions tests/cases/fourslash/completionsInJsxTag.ts
Expand Up @@ -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",
},
],
Expand Down
4 changes: 2 additions & 2 deletions tests/cases/fourslash/completionsJsxAttribute.ts
Expand Up @@ -17,8 +17,8 @@
////<div /**/></div>;

const exact: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntry> = [
{ 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");
Expand Down
2 changes: 1 addition & 1 deletion tests/cases/fourslash/completionsJsxAttributeGeneric.ts
Expand Up @@ -11,7 +11,7 @@
marker,
exact: [{
name: 'name',
kind: 'JSX attribute',
kind: 'property',
kindModifiers: 'declare'
}]
})
Expand Down
4 changes: 2 additions & 2 deletions tests/cases/fourslash/completionsJsxAttributeInitializer.ts
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions tests/cases/fourslash/jsxGenericQuickInfo.tsx
Expand Up @@ -29,6 +29,6 @@
verify.quickInfoAt("0", "(property) PropsA<number>.renderItem: (item: number) => string");
verify.quickInfoAt("1", "(parameter) item: number");
verify.quickInfoAt("2", `(property) PropsA<T>.name: "A"`, 'comments for A');
verify.quickInfoAt("3", "(JSX attribute) PropsA<number>.renderItem: (item: number) => string");
verify.quickInfoAt("3", "(property) PropsA<number>.renderItem: (item: number) => string");
verify.quickInfoAt("4", "(parameter) item: number");
verify.quickInfoAt("5", `(JSX attribute) PropsA<T>.name: "A"`, 'comments for A');
verify.quickInfoAt("5", `(property) PropsA<T>.name: "A"`, 'comments for A');
74 changes: 74 additions & 0 deletions tests/cases/fourslash/jsxTagNameDottedAttributeSnippet.ts
@@ -0,0 +1,74 @@
/// <reference path="fourslash.ts" />
//@Filename: file.tsx
////interface NestedInterface {
//// Foo: NestedInterface;
//// (props: {className?: string}): any;
////}
////
////declare const Foo: NestedInterface;
////
////function fn1() {
//// return <Foo>
//// <Foo /*1*/
//// </Foo>
////}
////function fn2() {
//// return <Foo>
//// <Foo.Foo /*2*/
//// </Foo>
////}
////function fn3() {
//// return <Foo>
//// <Foo.Foo cla/*3*/
//// </Foo>
////}
////function fn4() {
//// return <Foo>
//// <Foo.Foo cla/*4*/ something
//// </Foo>
////}
////function fn5() {
//// return <Foo>
//// <Foo.Foo something /*5*/
//// </Foo>
////}
////function fn6() {
//// return <Foo>
//// <Foo.Foo something cla/*6*/
//// </Foo>
////}
////function fn7() {
//// return <Foo /*7*/
////}
////function fn8() {
//// return <Foo cla/*8*/
////}
////function fn9() {
//// return <Foo cla/*9*/ something
////}
////function fn10() {
//// return <Foo something /*10*/
////}
////function fn11() {
//// return <Foo something cla/*11*/
////}

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,
},
}
)
74 changes: 74 additions & 0 deletions tests/cases/fourslash/jsxTagNameDottedAttributeSnippetClosed.ts
@@ -0,0 +1,74 @@
/// <reference path="fourslash.ts" />
//@Filename: file.tsx
////interface NestedInterface {
//// Foo: NestedInterface;
//// (props: {className?: string}): any;
////}
////
////declare const Foo: NestedInterface;
////
////function fn1() {
//// return <Foo>
//// <Foo /*1*/ />
//// </Foo>
////}
////function fn2() {
//// return <Foo>
//// <Foo.Foo /*2*/ />
//// </Foo>
////}
////function fn3() {
//// return <Foo>
//// <Foo.Foo cla/*3*/ />
//// </Foo>
////}
////function fn4() {
//// return <Foo>
//// <Foo.Foo cla/*4*/ something />
//// </Foo>
////}
////function fn5() {
//// return <Foo>
//// <Foo.Foo something /*5*/ />
//// </Foo>
////}
////function fn6() {
//// return <Foo>
//// <Foo.Foo something cla/*6*/ />
//// </Foo>
////}
////function fn7() {
//// return <Foo /*7*/ />
////}
////function fn8() {
//// return <Foo cla/*8*/ />
////}
////function fn9() {
//// return <Foo cla/*9*/ something />
////}
////function fn10() {
//// return <Foo something /*10*/ />
////}
////function fn11() {
//// return <Foo something cla/*11*/ />
////}

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,
},
}
)
54 changes: 54 additions & 0 deletions tests/cases/fourslash/jsxTagNameDottedNoSnippet.ts
@@ -0,0 +1,54 @@
/// <reference path="fourslash.ts" />
//@Filename: file.tsx
////interface NestedInterface {
//// Foo: NestedInterface;
//// (props: {}): any;
////}
////
////declare const Foo: NestedInterface;
////
////function fn1() {
//// return <Foo>
//// </*1*/
//// </Foo>
////}
////function fn2() {
//// return <Foo>
//// <Fo/*2*/
//// </Foo>
////}
////function fn3() {
//// return <Foo>
//// <Foo./*3*/
//// </Foo>
////}
////function fn4() {
//// return <Foo>
//// <Foo.F/*4*/
//// </Foo>
////}
////function fn5() {
//// return <Foo>
//// <Foo.Foo./*5*/
//// </Foo>
////}
////function fn6() {
//// return <Foo>
//// <Foo.Foo.F/*6*/
//// </Foo>
////}

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