From 20ce292484ea4f6556d149ac61c01672891b9e16 Mon Sep 17 00:00:00 2001 From: Amin Pakseresht <9244395+aminpaks@users.noreply.github.com> Date: Wed, 17 Feb 2021 20:06:13 -0500 Subject: [PATCH] Provide completion for partial expression on closing jsx tags (#42029) * Provide completion for partial expression on closing jsx tags * Add more cases and cover opening tag is parent parent of location * Fix code style indentation * Add some more notes * Guarding null pointer * Guarding null pointer 2 * PR reviews & adjustments 1 * Fix typos * Better namings * Remove failing test-case * PR reviews & adjustments 2 - new approach * More comments * More comments 2 * PR reviews & adjustments 3 * Revert previous test-case file changes * Write explicit completions from ranges * PR reviews & adjustments 4 - adding exact entry * Add another missing test-case * Find jsx closing element by findAncestor * Walk up till find jsx closing element * Add one more test-case * PR reviews & adjustments 4 - Pattern matching to get jsx closing element * Minor change * Linting fixes --- src/services/completions.ts | 68 ++++++++++++++++++------ tests/cases/fourslash/tsxCompletion15.ts | 47 ++++++++++++++++ 2 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 tests/cases/fourslash/tsxCompletion15.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index a3c957442cb..5cc1a12674c 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -232,22 +232,12 @@ namespace ts.Completions { symbolToSortTextMap, } = completionData; - if (location && location.parent && isJsxClosingElement(location.parent)) { - // In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag, - // instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element. - // For example: - // var x =
" with type any - // And at `
` (with a closing `>`), the completion list will contain "div". - const tagName = location.parent.parent.openingElement.tagName; - const hasClosingAngleBracket = !!findChildOfKind(location.parent, SyntaxKind.GreaterThanToken, sourceFile); - const entry: CompletionEntry = { - name: tagName.getFullText(sourceFile) + (hasClosingAngleBracket ? "" : ">"), - kind: ScriptElementKind.classElement, - kindModifiers: undefined, - sortText: SortText.LocationPriority, - }; - return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, optionalReplacementSpan: getOptionalReplacementSpan(location), entries: [entry] }; + // Verify if the file is JSX language variant + if (getLanguageVariant(sourceFile.scriptKind) === LanguageVariant.JSX) { + const completionInfo = getJsxClosingTagCompletion(location, sourceFile); + if (completionInfo) { + return completionInfo; + } } const entries: CompletionEntry[] = []; @@ -335,6 +325,52 @@ namespace ts.Completions { } } + function getJsxClosingTagCompletion(location: Node | undefined, sourceFile: SourceFile): CompletionInfo | undefined { + // We wanna walk up the tree till we find a JSX closing element + const jsxClosingElement = findAncestor(location, node => { + switch (node.kind) { + case SyntaxKind.JsxClosingElement: + return true; + case SyntaxKind.SlashToken: + case SyntaxKind.GreaterThanToken: + case SyntaxKind.Identifier: + case SyntaxKind.PropertyAccessExpression: + return false; + default: + return "quit"; + } + }) as JsxClosingElement | undefined; + + if (jsxClosingElement) { + // In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag, + // instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element. + // For example: + // var x =
" with type any + // And at `
` (with a closing `>`), the completion list will contain "div". + // And at property access expressions ` ` the completion will + // return full closing tag with an optional replacement span + // For example: + // var x = + // var y = + // the completion list at "1" and "2" will contain "MainComponent.Child" with a replacement span of closing tag name + const hasClosingAngleBracket = !!findChildOfKind(jsxClosingElement, SyntaxKind.GreaterThanToken, sourceFile); + const tagName = jsxClosingElement.parent.openingElement.tagName; + const closingTag = tagName.getText(sourceFile); + const fullClosingTag = closingTag + (hasClosingAngleBracket ? "" : ">"); + const replacementSpan = createTextSpanFromNode(jsxClosingElement.tagName); + + const entry: CompletionEntry = { + name: fullClosingTag, + kind: ScriptElementKind.classElement, + kindModifiers: undefined, + sortText: SortText.LocationPriority, + }; + return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, optionalReplacementSpan: replacementSpan, entries: [entry] }; + } + return; + } + function getJSCompletionEntries( sourceFile: SourceFile, position: number, diff --git a/tests/cases/fourslash/tsxCompletion15.ts b/tests/cases/fourslash/tsxCompletion15.ts new file mode 100644 index 00000000000..268f408ff96 --- /dev/null +++ b/tests/cases/fourslash/tsxCompletion15.ts @@ -0,0 +1,47 @@ +/// +//@module: commonjs +//@jsx: preserve + +//// declare module JSX { +//// interface Element { } +//// interface IntrinsicElements { +//// } +//// interface ElementAttributesProperty { props; } +//// } + +//@Filename: exporter.tsx +//// export module M { +//// export declare function SFCComp(props: { Three: number; Four: string }): JSX.Element; +//// } + +//@Filename: file.tsx +//// import * as Exp from './exporter'; +//// var x1 = ; +//// var x2 = ; +//// var x3 = ; +//// var x4 = ; +//// var x6 = ; +//// var x7 = ; +//// var x8 = ; +//// var x9 = ; +//// var x10 = ; +//// var x11 = ; +//// var x12 =
; + +const ranges = test.ranges(); + +verify.completions( + { marker: '1', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[0] }, + { marker: '2', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[1] }, + { marker: '3', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[2] }, + { marker: '4', exact: 'Exp.M.SFCComp>', optionalReplacementSpan: ranges[3] }, + { marker: '5', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[4] }, + { marker: '6', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[5] }, + { marker: '7', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[6] }, + { marker: '8', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[7] }, + { marker: '9', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[8] }, + { marker: '10', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[9] }, + { marker: '11', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[10] }, + { marker: '12', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[11] }, +);