diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1af5eefc9c85c..c2512bea1b243 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2593,14 +2593,14 @@ namespace ts { export interface JsxExpression extends Expression { readonly kind: SyntaxKind.JsxExpression; - readonly parent: JsxElement | JsxAttributeLike; + readonly parent: JsxElement | JsxFragment | JsxAttributeLike; readonly dotDotDotToken?: Token; readonly expression?: Expression; } export interface JsxText extends LiteralLikeNode { readonly kind: SyntaxKind.JsxText; - readonly parent: JsxElement; + readonly parent: JsxElement | JsxFragment; readonly containsOnlyTriviaWhiteSpaces: boolean; } diff --git a/src/services/completions.ts b/src/services/completions.ts index 77dda768290bd..b1d76c531b329 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1180,7 +1180,7 @@ namespace ts.Completions { case SyntaxKind.CaseKeyword: return getSwitchedType(cast(parent, isCaseClause), checker); case SyntaxKind.OpenBraceToken: - return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; + return isJsxExpression(parent) && !isJsxElement(parent.parent) && !isJsxFragment(parent.parent) ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; default: const argInfo = SignatureHelp.getArgumentInfoForCompletions(previousToken, position, sourceFile); return argInfo ? diff --git a/src/services/services.ts b/src/services/services.ts index 3f4096093a0f9..426fe7a52690d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2088,10 +2088,15 @@ namespace ts { const token = findPrecedingToken(position, sourceFile); if (!token) return undefined; const element = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningElement(token.parent) ? token.parent.parent - : isJsxText(token) ? token.parent : undefined; + : isJsxText(token) && isJsxElement(token.parent) ? token.parent : undefined; if (element && isUnclosedTag(element)) { return { newText: `` }; } + const fragment = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningFragment(token.parent) ? token.parent.parent + : isJsxText(token) && isJsxFragment(token.parent) ? token.parent : undefined; + if (fragment && isUnclosedFragment(fragment)) { + return { newText: "" }; + } } function getLinesForRange(sourceFile: SourceFile, textRange: TextRange) { @@ -2334,6 +2339,10 @@ namespace ts { isJsxElement(parent) && tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent); } + function isUnclosedFragment({ closingFragment, parent }: JsxFragment): boolean { + return !!(closingFragment.flags & NodeFlags.ThisNodeHasError) || (isJsxFragment(parent) && isUnclosedFragment(parent)); + } + function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined { const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); const range = formatting.getRangeOfEnclosingComment(sourceFile, position); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 6ca4baa8cbb41..11f90819fc17d 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1381,13 +1381,13 @@ declare namespace ts { } export interface JsxExpression extends Expression { readonly kind: SyntaxKind.JsxExpression; - readonly parent: JsxElement | JsxAttributeLike; + readonly parent: JsxElement | JsxFragment | JsxAttributeLike; readonly dotDotDotToken?: Token; readonly expression?: Expression; } export interface JsxText extends LiteralLikeNode { readonly kind: SyntaxKind.JsxText; - readonly parent: JsxElement; + readonly parent: JsxElement | JsxFragment; readonly containsOnlyTriviaWhiteSpaces: boolean; } export type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement | JsxFragment; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index fea14c1fcb999..88f113eadbfa9 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1381,13 +1381,13 @@ declare namespace ts { } export interface JsxExpression extends Expression { readonly kind: SyntaxKind.JsxExpression; - readonly parent: JsxElement | JsxAttributeLike; + readonly parent: JsxElement | JsxFragment | JsxAttributeLike; readonly dotDotDotToken?: Token; readonly expression?: Expression; } export interface JsxText extends LiteralLikeNode { readonly kind: SyntaxKind.JsxText; - readonly parent: JsxElement; + readonly parent: JsxElement | JsxFragment; readonly containsOnlyTriviaWhiteSpaces: boolean; } export type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement | JsxFragment; diff --git a/tests/cases/fourslash/autoCloseFragment.ts b/tests/cases/fourslash/autoCloseFragment.ts new file mode 100644 index 0000000000000..baa80a8aa6261 --- /dev/null +++ b/tests/cases/fourslash/autoCloseFragment.ts @@ -0,0 +1,51 @@ +/// + +// Using separate files for each example to avoid unclosed JSX tags affecting other tests. + +// @Filename: /0.tsx +////const x = <>/*0*/; + +// @Filename: /1.tsx +////const x = <> foo/*1*/ ; + +// @Filename: /2.tsx +////const x = <>/*2*/; + +// @Filename: /3.tsx +////const x = /*3*/; + +// @Filename: /4.tsx +////const x =
+//// <>/*4*/ +////
+////; + +// @Filename: /5.tsx +////const x = <> text /*5*/; + +// @Filename: /6.tsx +////const x = <> +//// <>/*6*/ +////; + +// @Filename: /7.tsx +////const x =
+//// <>/*7*/ +////
; + +// @Filename: /8.tsx +////const x =
+//// <>/*8*/ +////
; + +verify.jsxClosingTag({ + 0: { newText: "" }, + 1: undefined, + 2: undefined, + 3: undefined, + 4: { newText: "" }, + 5: { newText: "" }, + 6: { newText: "" }, + 7: { newText: "" }, + 8: undefined, +});