Skip to content

Commit 886377f

Browse files
author
Andy
authored
Add autoCloseTag language service (microsoft#24543)
* Add autoCloseTag language service * Change name to getJsxClosingTagAtPosition and return an object
1 parent 7b8426d commit 886377f

File tree

13 files changed

+132
-21
lines changed

13 files changed

+132
-21
lines changed

src/compiler/parser.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4147,27 +4147,6 @@ namespace ts {
41474147
return finishNode(node);
41484148
}
41494149

4150-
function tagNamesAreEquivalent(lhs: JsxTagNameExpression, rhs: JsxTagNameExpression): boolean {
4151-
if (lhs.kind !== rhs.kind) {
4152-
return false;
4153-
}
4154-
4155-
if (lhs.kind === SyntaxKind.Identifier) {
4156-
return (<Identifier>lhs).escapedText === (<Identifier>rhs).escapedText;
4157-
}
4158-
4159-
if (lhs.kind === SyntaxKind.ThisKeyword) {
4160-
return true;
4161-
}
4162-
4163-
// If we are at this statement then we must have PropertyAccessExpression and because tag name in Jsx element can only
4164-
// take forms of JsxTagNameExpression which includes an identifier, "this" expression, or another propertyAccessExpression
4165-
// it is safe to case the expression property as such. See parseJsxElementName for how we parse tag name in Jsx element
4166-
return (<PropertyAccessExpression>lhs).name.escapedText === (<PropertyAccessExpression>rhs).name.escapedText &&
4167-
tagNamesAreEquivalent((<PropertyAccessExpression>lhs).expression as JsxTagNameExpression, (<PropertyAccessExpression>rhs).expression as JsxTagNameExpression);
4168-
}
4169-
4170-
41714150
function parseJsxElementOrSelfClosingElementOrFragment(inExpressionContext: boolean): JsxElement | JsxSelfClosingElement | JsxFragment {
41724151
const opening = parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext);
41734152
let result: JsxElement | JsxSelfClosingElement | JsxFragment;
@@ -7906,4 +7885,25 @@ namespace ts {
79067885
}
79077886
return argMap;
79087887
}
7888+
7889+
/** @internal */
7890+
export function tagNamesAreEquivalent(lhs: JsxTagNameExpression, rhs: JsxTagNameExpression): boolean {
7891+
if (lhs.kind !== rhs.kind) {
7892+
return false;
7893+
}
7894+
7895+
if (lhs.kind === SyntaxKind.Identifier) {
7896+
return (<Identifier>lhs).escapedText === (<Identifier>rhs).escapedText;
7897+
}
7898+
7899+
if (lhs.kind === SyntaxKind.ThisKeyword) {
7900+
return true;
7901+
}
7902+
7903+
// If we are at this statement then we must have PropertyAccessExpression and because tag name in Jsx element can only
7904+
// take forms of JsxTagNameExpression which includes an identifier, "this" expression, or another propertyAccessExpression
7905+
// it is safe to case the expression property as such. See parseJsxElementName for how we parse tag name in Jsx element
7906+
return (<PropertyAccessExpression>lhs).name.escapedText === (<PropertyAccessExpression>rhs).name.escapedText &&
7907+
tagNamesAreEquivalent((<PropertyAccessExpression>lhs).expression as JsxTagNameExpression, (<PropertyAccessExpression>rhs).expression as JsxTagNameExpression);
7908+
}
79097909
}

src/harness/fourslash.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2743,6 +2743,14 @@ Actual: ${stringify(fullActual)}`);
27432743
}
27442744
}
27452745

2746+
public verifyJsxClosingTag(map: { [markerName: string]: ts.JsxClosingTagInfo | undefined }): void {
2747+
for (const markerName in map) {
2748+
this.goToMarker(markerName);
2749+
const actual = this.languageService.getJsxClosingTagAtPosition(this.activeFile.fileName, this.currentCaretPosition);
2750+
assert.deepEqual(actual, map[markerName]);
2751+
}
2752+
}
2753+
27462754
public verifyMatchingBracePosition(bracePosition: number, expectedMatchPosition: number) {
27472755
const actual = this.languageService.getBraceMatchingAtPosition(this.activeFile.fileName, bracePosition);
27482756

@@ -4079,6 +4087,10 @@ namespace FourSlashInterface {
40794087
this.state.verifyBraceCompletionAtPosition(this.negative, openingBrace);
40804088
}
40814089

4090+
public jsxClosingTag(map: { [markerName: string]: ts.JsxClosingTagInfo | undefined }): void {
4091+
this.state.verifyJsxClosingTag(map);
4092+
}
4093+
40824094
public isInCommentAtPosition(onlyMultiLineDiverges?: boolean) {
40834095
this.state.verifySpanOfEnclosingComment(this.negative, onlyMultiLineDiverges);
40844096
}

src/harness/harnessLanguageService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,9 @@ namespace Harness.LanguageService {
509509
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean {
510510
return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace));
511511
}
512+
getJsxClosingTagAtPosition(): never {
513+
throw new Error("Not supported on the shim.");
514+
}
512515
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan {
513516
return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine));
514517
}

src/harness/unittests/session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ namespace ts.server {
224224
CommandNames.Occurrences,
225225
CommandNames.DocumentHighlights,
226226
CommandNames.DocumentHighlightsFull,
227+
CommandNames.JsxClosingTag,
227228
CommandNames.Open,
228229
CommandNames.Quickinfo,
229230
CommandNames.QuickinfoFull,

src/server/client.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,10 @@ namespace ts.server {
552552
return notImplemented();
553553
}
554554

555+
getJsxClosingTagAtPosition(_fileName: string, _position: number): never {
556+
return notImplemented();
557+
}
558+
555559
getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan {
556560
return notImplemented();
557561
}

src/server/protocol.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
namespace ts.server.protocol {
77
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
88
export const enum CommandTypes {
9+
JsxClosingTag = "jsxClosingTag",
910
Brace = "brace",
1011
/* @internal */
1112
BraceFull = "brace-full",
@@ -890,6 +891,17 @@ namespace ts.server.protocol {
890891
openingBrace: string;
891892
}
892893

894+
export interface JsxClosingTagRequest extends FileLocationRequest {
895+
readonly command: CommandTypes.JsxClosingTag;
896+
readonly arguments: JsxClosingTagRequestArgs;
897+
}
898+
899+
export interface JsxClosingTagRequestArgs extends FileLocationRequestArgs {}
900+
901+
export interface JsxClosingTagResponse extends Response {
902+
readonly body: TextInsertion;
903+
}
904+
893905
/**
894906
* @deprecated
895907
* Get occurrences request; value of command field is

src/server/session.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,13 @@ namespace ts.server {
818818
return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSuggestionDiagnostics(file), !!args.includeLinePosition);
819819
}
820820

821+
private getJsxClosingTag(args: protocol.JsxClosingTagRequestArgs): TextInsertion | undefined {
822+
const { file, project } = this.getFileAndProject(args);
823+
const position = this.getPositionInFile(args, file);
824+
const tag = project.getLanguageService().getJsxClosingTagAtPosition(file, position);
825+
return tag === undefined ? undefined : { newText: tag.newText, caretOffset: 0 };
826+
}
827+
821828
private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.DocumentHighlightsItem> | ReadonlyArray<DocumentHighlights> {
822829
const { file, project } = this.getFileAndProject(args);
823830
const position = this.getPositionInFile(args, file);
@@ -2130,6 +2137,9 @@ namespace ts.server {
21302137
this.projectService.reloadProjects();
21312138
return this.notRequired();
21322139
},
2140+
[CommandNames.JsxClosingTag]: (request: protocol.JsxClosingTagRequest) => {
2141+
return this.requiredResponse(this.getJsxClosingTag(request.arguments));
2142+
},
21332143
[CommandNames.GetCodeFixes]: (request: protocol.CodeFixRequest) => {
21342144
return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ true));
21352145
},

src/services/services.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,6 +2047,17 @@ namespace ts {
20472047
return true;
20482048
}
20492049

2050+
function getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined {
2051+
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
2052+
const token = findPrecedingToken(position, sourceFile);
2053+
if (!token) return undefined;
2054+
const element = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningElement(token.parent) ? token.parent.parent
2055+
: isJsxText(token) ? token.parent : undefined;
2056+
if (element && !tagNamesAreEquivalent(element.openingElement.tagName, element.closingElement.tagName)) {
2057+
return { newText: `</${element.openingElement.tagName.getText(sourceFile)}>` };
2058+
}
2059+
}
2060+
20502061
function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined {
20512062
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
20522063
const range = formatting.getRangeOfEnclosingComment(sourceFile, position, onlyMultiLine);
@@ -2279,6 +2290,7 @@ namespace ts {
22792290
getFormattingEditsAfterKeystroke,
22802291
getDocCommentTemplateAtPosition,
22812292
isValidBraceCompletionAtPosition,
2293+
getJsxClosingTagAtPosition,
22822294
getSpanOfEnclosingComment,
22832295
getCodeFixesAtPosition,
22842296
getCombinedCodeFix,

src/services/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ namespace ts {
323323
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined;
324324

325325
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
326+
/**
327+
* This will return a defined result if the position is after the `>` of the opening tag, or somewhere in the text, of a JSXElement with no closing tag.
328+
* Editors should call this after `>` is typed.
329+
*/
330+
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
326331

327332
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
328333

@@ -353,6 +358,10 @@ namespace ts {
353358
dispose(): void;
354359
}
355360

361+
export interface JsxClosingTagInfo {
362+
readonly newText: string;
363+
}
364+
356365
export interface CombinedCodeFixScope { type: "file"; fileName: string; }
357366

358367
export type OrganizeImportsScope = CombinedCodeFixScope;

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4566,6 +4566,11 @@ declare namespace ts {
45664566
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[];
45674567
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined;
45684568
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
4569+
/**
4570+
* This will return a defined result if the position is after the `>` of the opening tag, or somewhere in the text, of a JSXElement with no closing tag.
4571+
* Editors should call this after `>` is typed.
4572+
*/
4573+
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
45694574
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
45704575
toLineColumnOffset?(fileName: string, position: number): LineAndCharacter;
45714576
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray<number>, formatOptions: FormatCodeSettings, preferences: UserPreferences): ReadonlyArray<CodeFixAction>;
@@ -4587,6 +4592,9 @@ declare namespace ts {
45874592
getProgram(): Program | undefined;
45884593
dispose(): void;
45894594
}
4595+
interface JsxClosingTagInfo {
4596+
readonly newText: string;
4597+
}
45904598
interface CombinedCodeFixScope {
45914599
type: "file";
45924600
fileName: string;
@@ -5516,6 +5524,7 @@ declare namespace ts.server {
55165524
*/
55175525
declare namespace ts.server.protocol {
55185526
enum CommandTypes {
5527+
JsxClosingTag = "jsxClosingTag",
55195528
Brace = "brace",
55205529
BraceCompletion = "braceCompletion",
55215530
GetSpanOfEnclosingComment = "getSpanOfEnclosingComment",
@@ -6185,6 +6194,15 @@ declare namespace ts.server.protocol {
61856194
*/
61866195
openingBrace: string;
61876196
}
6197+
interface JsxClosingTagRequest extends FileLocationRequest {
6198+
readonly command: CommandTypes.JsxClosingTag;
6199+
readonly arguments: JsxClosingTagRequestArgs;
6200+
}
6201+
interface JsxClosingTagRequestArgs extends FileLocationRequestArgs {
6202+
}
6203+
interface JsxClosingTagResponse extends Response {
6204+
readonly body: TextInsertion;
6205+
}
61886206
/**
61896207
* @deprecated
61906208
* Get occurrences request; value of command field is
@@ -8473,6 +8491,7 @@ declare namespace ts.server {
84738491
private getSyntacticDiagnosticsSync;
84748492
private getSemanticDiagnosticsSync;
84758493
private getSuggestionDiagnosticsSync;
8494+
private getJsxClosingTag;
84768495
private getDocumentHighlights;
84778496
private setCompilerOptionsForInferredProjects;
84788497
private getProjectInfo;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4566,6 +4566,11 @@ declare namespace ts {
45664566
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[];
45674567
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined;
45684568
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
4569+
/**
4570+
* This will return a defined result if the position is after the `>` of the opening tag, or somewhere in the text, of a JSXElement with no closing tag.
4571+
* Editors should call this after `>` is typed.
4572+
*/
4573+
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
45694574
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
45704575
toLineColumnOffset?(fileName: string, position: number): LineAndCharacter;
45714576
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray<number>, formatOptions: FormatCodeSettings, preferences: UserPreferences): ReadonlyArray<CodeFixAction>;
@@ -4587,6 +4592,9 @@ declare namespace ts {
45874592
getProgram(): Program | undefined;
45884593
dispose(): void;
45894594
}
4595+
interface JsxClosingTagInfo {
4596+
readonly newText: string;
4597+
}
45904598
interface CombinedCodeFixScope {
45914599
type: "file";
45924600
fileName: string;

tests/cases/fourslash/autoCloseTag.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @Filename: /a.tsx
4+
////const x = <div>/*0*/;
5+
////const x = <div> foo/*1*/ </div>;
6+
////const x = <div></div>/*2*/;
7+
////const x = <div/>/*3*/;
8+
////const x = <div>
9+
//// <p>/*4*/
10+
//// </div>
11+
////</p>;
12+
////const x = <div> text /*5*/;
13+
14+
verify.jsxClosingTag({
15+
0: { newText: "</div>" },
16+
1: undefined,
17+
2: undefined,
18+
3: undefined,
19+
4: { newText: "</p>" },
20+
});

tests/cases/fourslash/fourslash.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ declare namespace FourSlashInterface {
174174
typeDefinitionCountIs(expectedCount: number): void;
175175
implementationListIsEmpty(): void;
176176
isValidBraceCompletionAtPosition(openingBrace?: string): void;
177+
jsxClosingTag(map: { [markerName: string]: { readonly newText: string } | undefined }): void;
177178
isInCommentAtPosition(onlyMultiLineDiverges?: boolean): void;
178179
codeFix(options: {
179180
description: string,

0 commit comments

Comments
 (0)