Skip to content

Commit

Permalink
Rewrite logic for JSX attribute completion detection
Browse files Browse the repository at this point in the history
  • Loading branch information
jakebailey committed Jan 12, 2022
1 parent 14f33d5 commit d14d8ff
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 3 deletions.
44 changes: 41 additions & 3 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,8 +744,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 @@ -790,7 +828,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
74 changes: 74 additions & 0 deletions tests/cases/fourslash/jsxAttributeSnippetCompletionClosed.ts
Original file line number Diff line number Diff line change
@@ -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*/ />
////}

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 } },
)
74 changes: 74 additions & 0 deletions tests/cases/fourslash/jsxAttributeSnippetCompletionUnclosed.ts
Original file line number Diff line number Diff line change
@@ -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*/
////}

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: "(property) 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: "(property) 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 } },
)
54 changes: 54 additions & 0 deletions tests/cases/fourslash/jsxTagNameCompletionClosed.ts
Original file line number Diff line number Diff line change
@@ -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: "(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" } },
)
54 changes: 54 additions & 0 deletions tests/cases/fourslash/jsxTagNameCompletionUnclosed.ts
Original file line number Diff line number Diff line change
@@ -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: "(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" } },
)
35 changes: 35 additions & 0 deletions tests/cases/fourslash/jsxTagNameCompletionUnderElementClosed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/// <reference path="fourslash.ts" />
//@Filename: file.tsx
////declare namespace JSX {
//// interface IntrinsicElements {
//// button: any;
//// div: any;
//// }
////}
////function fn() {
//// return <>
//// <butto/*1*/ />
//// </>;
////}
////function fn2() {
//// return <>
//// preceding junk <butto/*2*/ />
//// </>;
////}
////function fn3() {
//// return <>
//// <butto/*3*/ style="" />
//// </>;
////}

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" } },
)
35 changes: 35 additions & 0 deletions tests/cases/fourslash/jsxTagNameCompletionUnderElementUnclosed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/// <reference path="fourslash.ts" />
//@Filename: file.tsx
////declare namespace JSX {
//// interface IntrinsicElements {
//// button: any;
//// div: any;
//// }
////}
////function fn() {
//// return <>
//// <butto/*1*/
//// </>;
////}
////function fn2() {
//// return <>
//// preceding junk <butto/*2*/
//// </>;
////}
////function fn3() {
//// return <>
//// <butto/*3*/ style=""
//// </>;
////}

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

0 comments on commit d14d8ff

Please sign in to comment.