Skip to content

Commit

Permalink
Rewrite logic for JSX attribute completion detection (#47412)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakebailey committed Jan 14, 2022
1 parent b7fee7f commit 16bbddb
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 5 deletions.
22 changes: 17 additions & 5 deletions src/services/completions.ts
Expand Up @@ -456,6 +456,7 @@ namespace ts.Completions {
isJsxInitializer,
isTypeOnlyLocation,
isJsxIdentifierExpected,
isRightOfOpenTag,
importCompletionNode,
insideJsDocTagTypeExpression,
symbolToSortTextIdMap,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -526,7 +529,9 @@ namespace ts.Completions {
importCompletionNode,
recommendedCompletion,
symbolToOriginInfoMap,
symbolToSortTextIdMap
symbolToSortTextIdMap,
isJsxIdentifierExpected,
isRightOfOpenTag,
);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1228,6 +1236,8 @@ namespace ts.Completions {
preferences,
kind,
formatContext,
isJsxIdentifierExpected,
isRightOfOpenTag,
);
if (!entry) {
continue;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -1987,6 +1998,7 @@ namespace ts.Completions {
symbolToSortTextIdMap,
isTypeOnlyLocation,
isJsxIdentifierExpected,
isRightOfOpenTag,
importCompletionNode,
hasUnresolvedAutoImports,
};
Expand Down
74 changes: 74 additions & 0 deletions tests/cases/fourslash/jsxAttributeSnippetCompletionClosed.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*/ />
////}

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
@@ -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
@@ -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
@@ -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
@@ -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 16bbddb

Please sign in to comment.