Skip to content

Commit 20ce292

Browse files
authored
Provide completion for partial expression on closing jsx tags (microsoft#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
1 parent f561856 commit 20ce292

File tree

2 files changed

+99
-16
lines changed

2 files changed

+99
-16
lines changed

src/services/completions.ts

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -232,22 +232,12 @@ namespace ts.Completions {
232232
symbolToSortTextMap,
233233
} = completionData;
234234

235-
if (location && location.parent && isJsxClosingElement(location.parent)) {
236-
// In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag,
237-
// instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element.
238-
// For example:
239-
// var x = <div> </ /*1*/
240-
// The completion list at "1" will contain "div>" with type any
241-
// And at `<div> </ /*1*/ >` (with a closing `>`), the completion list will contain "div".
242-
const tagName = location.parent.parent.openingElement.tagName;
243-
const hasClosingAngleBracket = !!findChildOfKind(location.parent, SyntaxKind.GreaterThanToken, sourceFile);
244-
const entry: CompletionEntry = {
245-
name: tagName.getFullText(sourceFile) + (hasClosingAngleBracket ? "" : ">"),
246-
kind: ScriptElementKind.classElement,
247-
kindModifiers: undefined,
248-
sortText: SortText.LocationPriority,
249-
};
250-
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, optionalReplacementSpan: getOptionalReplacementSpan(location), entries: [entry] };
235+
// Verify if the file is JSX language variant
236+
if (getLanguageVariant(sourceFile.scriptKind) === LanguageVariant.JSX) {
237+
const completionInfo = getJsxClosingTagCompletion(location, sourceFile);
238+
if (completionInfo) {
239+
return completionInfo;
240+
}
251241
}
252242

253243
const entries: CompletionEntry[] = [];
@@ -335,6 +325,52 @@ namespace ts.Completions {
335325
}
336326
}
337327

328+
function getJsxClosingTagCompletion(location: Node | undefined, sourceFile: SourceFile): CompletionInfo | undefined {
329+
// We wanna walk up the tree till we find a JSX closing element
330+
const jsxClosingElement = findAncestor(location, node => {
331+
switch (node.kind) {
332+
case SyntaxKind.JsxClosingElement:
333+
return true;
334+
case SyntaxKind.SlashToken:
335+
case SyntaxKind.GreaterThanToken:
336+
case SyntaxKind.Identifier:
337+
case SyntaxKind.PropertyAccessExpression:
338+
return false;
339+
default:
340+
return "quit";
341+
}
342+
}) as JsxClosingElement | undefined;
343+
344+
if (jsxClosingElement) {
345+
// In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag,
346+
// instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element.
347+
// For example:
348+
// var x = <div> </ /*1*/
349+
// The completion list at "1" will contain "div>" with type any
350+
// And at `<div> </ /*1*/ >` (with a closing `>`), the completion list will contain "div".
351+
// And at property access expressions `<MainComponent.Child> </MainComponent. /*1*/ >` the completion will
352+
// return full closing tag with an optional replacement span
353+
// For example:
354+
// var x = <MainComponent.Child> </ MainComponent /*1*/ >
355+
// var y = <MainComponent.Child> </ /*2*/ MainComponent >
356+
// the completion list at "1" and "2" will contain "MainComponent.Child" with a replacement span of closing tag name
357+
const hasClosingAngleBracket = !!findChildOfKind(jsxClosingElement, SyntaxKind.GreaterThanToken, sourceFile);
358+
const tagName = jsxClosingElement.parent.openingElement.tagName;
359+
const closingTag = tagName.getText(sourceFile);
360+
const fullClosingTag = closingTag + (hasClosingAngleBracket ? "" : ">");
361+
const replacementSpan = createTextSpanFromNode(jsxClosingElement.tagName);
362+
363+
const entry: CompletionEntry = {
364+
name: fullClosingTag,
365+
kind: ScriptElementKind.classElement,
366+
kindModifiers: undefined,
367+
sortText: SortText.LocationPriority,
368+
};
369+
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, optionalReplacementSpan: replacementSpan, entries: [entry] };
370+
}
371+
return;
372+
}
373+
338374
function getJSCompletionEntries(
339375
sourceFile: SourceFile,
340376
position: number,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/// <reference path='fourslash.ts' />
2+
//@module: commonjs
3+
//@jsx: preserve
4+
5+
//// declare module JSX {
6+
//// interface Element { }
7+
//// interface IntrinsicElements {
8+
//// }
9+
//// interface ElementAttributesProperty { props; }
10+
//// }
11+
12+
//@Filename: exporter.tsx
13+
//// export module M {
14+
//// export declare function SFCComp(props: { Three: number; Four: string }): JSX.Element;
15+
//// }
16+
17+
//@Filename: file.tsx
18+
//// import * as Exp from './exporter';
19+
//// var x1 = <Exp.M.SFCComp></[|/*1*/|]>;
20+
//// var x2 = <Exp.M.SFCComp></[|Exp./*2*/|]>;
21+
//// var x3 = <Exp.M.SFCComp></[|Exp.M./*3*/|]>;
22+
//// var x4 = <Exp.M.SFCComp></[|Exp.M.SFCComp/*4*/|]
23+
//// var x5 = <Exp.M.SFCComp></[|Exp.M.SFCComp/*5*/|]>;
24+
//// var x6 = <Exp.M.SFCComp></ [|Exp./*6*/|]>;
25+
//// var x7 = <Exp.M.SFCComp></[|/*7*/Exp.M.SFCComp|]>;
26+
//// var x8 = <Exp.M.SFCComp></[|Exp/*8*/|]>;
27+
//// var x9 = <Exp.M.SFCComp></[|Exp.M./*9*/|]>;
28+
//// var x10 = <Exp.M.SFCComp></ [|/*10*/Exp.M.Foo.Bar.Baz.Wut|]>;
29+
//// var x11 = <Exp.M.SFCComp></[|Exp./*11*/M.SFCComp|]>;
30+
//// var x12 = <Exp.M.SFCComp><div><span /></div></[|Exp.M./*12*/SFCComp|]>;
31+
32+
const ranges = test.ranges();
33+
34+
verify.completions(
35+
{ marker: '1', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[0] },
36+
{ marker: '2', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[1] },
37+
{ marker: '3', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[2] },
38+
{ marker: '4', exact: 'Exp.M.SFCComp>', optionalReplacementSpan: ranges[3] },
39+
{ marker: '5', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[4] },
40+
{ marker: '6', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[5] },
41+
{ marker: '7', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[6] },
42+
{ marker: '8', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[7] },
43+
{ marker: '9', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[8] },
44+
{ marker: '10', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[9] },
45+
{ marker: '11', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[10] },
46+
{ marker: '12', exact: 'Exp.M.SFCComp', optionalReplacementSpan: ranges[11] },
47+
);

0 commit comments

Comments
 (0)