Skip to content

Commit f76f10b

Browse files
author
Andy Hanson
committed
Add autoCloseTag language service
1 parent 9d42ab9 commit f76f10b

File tree

13 files changed

+111
-21
lines changed

13 files changed

+111
-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 verifyAutoCloseTag(map: { [markerName: string]: string | undefined }): void {
2747+
for (const markerName in map) {
2748+
this.goToMarker(markerName);
2749+
const actual = this.languageService.getAutoCloseTagAtPosition(this.activeFile.fileName, this.currentCaretPosition);
2750+
assert.equal(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 autoCloseTag(map: { [markerName: string]: string | undefined }): void {
4091+
this.state.verifyAutoCloseTag(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+
getAutoCloseTagAtPosition(): string | undefined {
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
@@ -188,6 +188,7 @@ namespace ts.server {
188188

189189
describe("onMessage", () => {
190190
const allCommandNames: CommandNames[] = [
191+
CommandNames.AutoCloseTag,
191192
CommandNames.Brace,
192193
CommandNames.BraceFull,
193194
CommandNames.BraceCompletion,

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+
getAutoCloseTagAtPosition(_fileName: string, _position: number): string | undefined {
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+
AutoCloseTag = "autoCloseTag",
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 AutoCloseTagRequest extends FileLocationRequest {
895+
readonly command: CommandTypes.AutoCloseTag;
896+
readonly arguments: AutoCloseTagRequestArgs;
897+
}
898+
899+
export interface AutoCloseTagRequestArgs extends FileLocationRequestArgs {}
900+
901+
export interface AutoCloseTagResponse 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 getAutoCloseTag(args: protocol.AutoCloseTagRequestArgs): TextInsertion | undefined {
822+
const { file, project } = this.getFileAndProject(args);
823+
const position = this.getPositionInFile(args, file);
824+
const tag = project.getLanguageService().getAutoCloseTagAtPosition(file, position);
825+
return tag === undefined ? undefined : { newText: tag, 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.AutoCloseTag]: (request: protocol.AutoCloseTagRequest) => {
2141+
return this.requiredResponse(this.getAutoCloseTag(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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2051,6 +2051,18 @@ namespace ts {
20512051
return true;
20522052
}
20532053

2054+
function getAutoCloseTagAtPosition(fileName: string, position: number): string | undefined {
2055+
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
2056+
const token = findPrecedingToken(position, sourceFile);
2057+
if (!token) return undefined;
2058+
const element = token.kind === SyntaxKind.GreaterThanToken
2059+
? isJsxOpeningElement(token.parent) ? token.parent.parent : undefined
2060+
: isJsxText(token) ? token.parent : undefined;
2061+
if (element && !tagNamesAreEquivalent(element.openingElement.tagName, element.closingElement.tagName)) {
2062+
return `</${element.openingElement.tagName.getText(sourceFile)}>`;
2063+
}
2064+
}
2065+
20542066
function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined {
20552067
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
20562068
const range = formatting.getRangeOfEnclosingComment(sourceFile, position, onlyMultiLine);
@@ -2283,6 +2295,7 @@ namespace ts {
22832295
getFormattingEditsAfterKeystroke,
22842296
getDocCommentTemplateAtPosition,
22852297
isValidBraceCompletionAtPosition,
2298+
getAutoCloseTagAtPosition,
22862299
getSpanOfEnclosingComment,
22872300
getCodeFixesAtPosition,
22882301
getCombinedCodeFix,

src/services/types.ts

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

325325
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
326+
getAutoCloseTagAtPosition(fileName: string, position: number): string | undefined;
326327

327328
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
328329

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4556,6 +4556,7 @@ declare namespace ts {
45564556
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[];
45574557
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined;
45584558
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
4559+
getAutoCloseTagAtPosition(fileName: string, position: number): string | undefined;
45594560
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
45604561
toLineColumnOffset?(fileName: string, position: number): LineAndCharacter;
45614562
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray<number>, formatOptions: FormatCodeSettings, preferences: UserPreferences): ReadonlyArray<CodeFixAction>;
@@ -5506,6 +5507,7 @@ declare namespace ts.server {
55065507
*/
55075508
declare namespace ts.server.protocol {
55085509
enum CommandTypes {
5510+
AutoCloseTag = "autoCloseTag",
55095511
Brace = "brace",
55105512
BraceCompletion = "braceCompletion",
55115513
GetSpanOfEnclosingComment = "getSpanOfEnclosingComment",
@@ -6175,6 +6177,15 @@ declare namespace ts.server.protocol {
61756177
*/
61766178
openingBrace: string;
61776179
}
6180+
interface AutoCloseTagRequest extends FileLocationRequest {
6181+
readonly command: CommandTypes.AutoCloseTag;
6182+
readonly arguments: AutoCloseTagRequestArgs;
6183+
}
6184+
interface AutoCloseTagRequestArgs extends FileLocationRequestArgs {
6185+
}
6186+
interface AutoCloseTagResponse extends Response {
6187+
readonly body: TextInsertion;
6188+
}
61786189
/**
61796190
* @deprecated
61806191
* Get occurrences request; value of command field is
@@ -8463,6 +8474,7 @@ declare namespace ts.server {
84638474
private getSyntacticDiagnosticsSync;
84648475
private getSemanticDiagnosticsSync;
84658476
private getSuggestionDiagnosticsSync;
8477+
private getAutoCloseTag;
84668478
private getDocumentHighlights;
84678479
private setCompilerOptionsForInferredProjects;
84688480
private getProjectInfo;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4556,6 +4556,7 @@ declare namespace ts {
45564556
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[];
45574557
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined;
45584558
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
4559+
getAutoCloseTagAtPosition(fileName: string, position: number): string | undefined;
45594560
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
45604561
toLineColumnOffset?(fileName: string, position: number): LineAndCharacter;
45614562
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray<number>, formatOptions: FormatCodeSettings, preferences: UserPreferences): ReadonlyArray<CodeFixAction>;

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.autoCloseTag({
15+
0: "</div>",
16+
1: undefined,
17+
2: undefined,
18+
3: undefined,
19+
4: "</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+
autoCloseTag(map: { [markerName: string]: string | undefined }): void;
177178
isInCommentAtPosition(onlyMultiLineDiverges?: boolean): void;
178179
codeFix(options: {
179180
description: string,

0 commit comments

Comments
 (0)