Skip to content

Commit

Permalink
Rewrite logic for JSX attribute completion detection [4.5 cherry pick] (
Browse files Browse the repository at this point in the history
  • Loading branch information
jakebailey committed Jan 14, 2022
1 parent 14016a7 commit 8a8048f
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 83 deletions.
39 changes: 17 additions & 22 deletions src/services/completions.ts
Expand Up @@ -428,6 +428,7 @@ namespace ts.Completions {
isJsxInitializer,
isTypeOnlyLocation,
isJsxIdentifierExpected,
isRightOfOpenTag,
importCompletionNode,
insideJsDocTagTypeExpression,
symbolToSortTextIdMap,
Expand Down Expand Up @@ -466,7 +467,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 @@ -496,7 +499,9 @@ namespace ts.Completions {
importCompletionNode,
recommendedCompletion,
symbolToOriginInfoMap,
symbolToSortTextIdMap
symbolToSortTextIdMap,
isJsxIdentifierExpected,
isRightOfOpenTag,
);
}

Expand Down Expand Up @@ -638,6 +643,8 @@ namespace ts.Completions {
options: CompilerOptions,
preferences: UserPreferences,
completionKind: CompletionKind,
isJsxIdentifierExpected: boolean | undefined,
isRightOfOpenTag: boolean | undefined,
): CompletionEntry | undefined {
let insertText: string | undefined;
let replacementSpan = getReplacementSpanForContextToken(replacementToken);
Expand Down Expand Up @@ -713,25 +720,7 @@ namespace ts.Completions {
}
}

// Before offering up a JSX attribute snippet, ensure that we aren't potentially completing
// a tag name; this may appear as an attribute after the "<" when the tag has not yet been
// closed, as in:
//
// return <>
// foo <butto|
// </>
//
// We can detect this case by checking if both:
//
// 1. The location is "<", so we are completing immediately after it.
// 2. The "<" has the same position as its parent, so is not a binary expression.
const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, location);
if (
kind === ScriptElementKind.jsxAttribute
&& (location.kind !== SyntaxKind.LessThanToken || location.pos !== location.parent.pos)
&& 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 @@ -776,7 +765,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 @@ -1142,6 +1131,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 @@ -1183,6 +1174,8 @@ namespace ts.Completions {
compilerOptions,
preferences,
kind,
isJsxIdentifierExpected,
isRightOfOpenTag,
);
if (!entry) {
continue;
Expand Down Expand Up @@ -1534,6 +1527,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 @@ -1941,6 +1935,7 @@ namespace ts.Completions {
symbolToSortTextIdMap,
isTypeOnlyLocation,
isJsxIdentifierExpected,
isRightOfOpenTag,
importCompletionNode,
hasUnresolvedAutoImports,
};
Expand Down
61 changes: 0 additions & 61 deletions tests/cases/fourslash/jsxAttributeAsTagNameNoSnippet.ts

This file was deleted.

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

0 comments on commit 8a8048f

Please sign in to comment.