Skip to content

Add autoCloseTag language service #24543

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
2 commits merged into from
Jun 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4147,27 +4147,6 @@ namespace ts {
return finishNode(node);
}

function tagNamesAreEquivalent(lhs: JsxTagNameExpression, rhs: JsxTagNameExpression): boolean {
if (lhs.kind !== rhs.kind) {
return false;
}

if (lhs.kind === SyntaxKind.Identifier) {
return (<Identifier>lhs).escapedText === (<Identifier>rhs).escapedText;
}

if (lhs.kind === SyntaxKind.ThisKeyword) {
return true;
}

// If we are at this statement then we must have PropertyAccessExpression and because tag name in Jsx element can only
// take forms of JsxTagNameExpression which includes an identifier, "this" expression, or another propertyAccessExpression
// it is safe to case the expression property as such. See parseJsxElementName for how we parse tag name in Jsx element
return (<PropertyAccessExpression>lhs).name.escapedText === (<PropertyAccessExpression>rhs).name.escapedText &&
tagNamesAreEquivalent((<PropertyAccessExpression>lhs).expression as JsxTagNameExpression, (<PropertyAccessExpression>rhs).expression as JsxTagNameExpression);
}


function parseJsxElementOrSelfClosingElementOrFragment(inExpressionContext: boolean): JsxElement | JsxSelfClosingElement | JsxFragment {
const opening = parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext);
let result: JsxElement | JsxSelfClosingElement | JsxFragment;
Expand Down Expand Up @@ -7906,4 +7885,25 @@ namespace ts {
}
return argMap;
}

/** @internal */
export function tagNamesAreEquivalent(lhs: JsxTagNameExpression, rhs: JsxTagNameExpression): boolean {
if (lhs.kind !== rhs.kind) {
return false;
}

if (lhs.kind === SyntaxKind.Identifier) {
return (<Identifier>lhs).escapedText === (<Identifier>rhs).escapedText;
}

if (lhs.kind === SyntaxKind.ThisKeyword) {
return true;
}

// If we are at this statement then we must have PropertyAccessExpression and because tag name in Jsx element can only
// take forms of JsxTagNameExpression which includes an identifier, "this" expression, or another propertyAccessExpression
// it is safe to case the expression property as such. See parseJsxElementName for how we parse tag name in Jsx element
return (<PropertyAccessExpression>lhs).name.escapedText === (<PropertyAccessExpression>rhs).name.escapedText &&
tagNamesAreEquivalent((<PropertyAccessExpression>lhs).expression as JsxTagNameExpression, (<PropertyAccessExpression>rhs).expression as JsxTagNameExpression);
}
}
12 changes: 12 additions & 0 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2743,6 +2743,14 @@ Actual: ${stringify(fullActual)}`);
}
}

public verifyJsxClosingTag(map: { [markerName: string]: ts.JsxClosingTagInfo | undefined }): void {
for (const markerName in map) {
this.goToMarker(markerName);
const actual = this.languageService.getJsxClosingTagAtPosition(this.activeFile.fileName, this.currentCaretPosition);
assert.deepEqual(actual, map[markerName]);
}
}

public verifyMatchingBracePosition(bracePosition: number, expectedMatchPosition: number) {
const actual = this.languageService.getBraceMatchingAtPosition(this.activeFile.fileName, bracePosition);

Expand Down Expand Up @@ -4079,6 +4087,10 @@ namespace FourSlashInterface {
this.state.verifyBraceCompletionAtPosition(this.negative, openingBrace);
}

public jsxClosingTag(map: { [markerName: string]: ts.JsxClosingTagInfo | undefined }): void {
this.state.verifyJsxClosingTag(map);
}

public isInCommentAtPosition(onlyMultiLineDiverges?: boolean) {
this.state.verifySpanOfEnclosingComment(this.negative, onlyMultiLineDiverges);
}
Expand Down
3 changes: 3 additions & 0 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,9 @@ namespace Harness.LanguageService {
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean {
return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace));
}
getJsxClosingTagAtPosition(): never {
throw new Error("Not supported on the shim.");
}
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan {
return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine));
}
Expand Down
1 change: 1 addition & 0 deletions src/harness/unittests/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ namespace ts.server {
CommandNames.Occurrences,
CommandNames.DocumentHighlights,
CommandNames.DocumentHighlightsFull,
CommandNames.JsxClosingTag,
CommandNames.Open,
CommandNames.Quickinfo,
CommandNames.QuickinfoFull,
Expand Down
4 changes: 4 additions & 0 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,10 @@ namespace ts.server {
return notImplemented();
}

getJsxClosingTagAtPosition(_fileName: string, _position: number): never {
return notImplemented();
}

getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan {
return notImplemented();
}
Expand Down
12 changes: 12 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace ts.server.protocol {
// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
export const enum CommandTypes {
JsxClosingTag = "jsxClosingTag",
Brace = "brace",
/* @internal */
BraceFull = "brace-full",
Expand Down Expand Up @@ -890,6 +891,17 @@ namespace ts.server.protocol {
openingBrace: string;
}

export interface JsxClosingTagRequest extends FileLocationRequest {
readonly command: CommandTypes.JsxClosingTag;
readonly arguments: JsxClosingTagRequestArgs;
}

export interface JsxClosingTagRequestArgs extends FileLocationRequestArgs {}

export interface JsxClosingTagResponse extends Response {
readonly body: TextInsertion;
}

/**
* @deprecated
* Get occurrences request; value of command field is
Expand Down
10 changes: 10 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,13 @@ namespace ts.server {
return this.getDiagnosticsWorker(args, /*isSemantic*/ true, (project, file) => project.getLanguageService().getSuggestionDiagnostics(file), !!args.includeLinePosition);
}

private getJsxClosingTag(args: protocol.JsxClosingTagRequestArgs): TextInsertion | undefined {
const { file, project } = this.getFileAndProject(args);
const position = this.getPositionInFile(args, file);
const tag = project.getLanguageService().getJsxClosingTagAtPosition(file, position);
return tag === undefined ? undefined : { newText: tag.newText, caretOffset: 0 };
}

private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.DocumentHighlightsItem> | ReadonlyArray<DocumentHighlights> {
const { file, project } = this.getFileAndProject(args);
const position = this.getPositionInFile(args, file);
Expand Down Expand Up @@ -2130,6 +2137,9 @@ namespace ts.server {
this.projectService.reloadProjects();
return this.notRequired();
},
[CommandNames.JsxClosingTag]: (request: protocol.JsxClosingTagRequest) => {
return this.requiredResponse(this.getJsxClosingTag(request.arguments));
},
[CommandNames.GetCodeFixes]: (request: protocol.CodeFixRequest) => {
return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ true));
},
Expand Down
12 changes: 12 additions & 0 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2051,6 +2051,17 @@ namespace ts {
return true;
}

function getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined {
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
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;
if (element && !tagNamesAreEquivalent(element.openingElement.tagName, element.closingElement.tagName)) {
return { newText: `</${element.openingElement.tagName.getText(sourceFile)}>` };
}
}

function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined {
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
const range = formatting.getRangeOfEnclosingComment(sourceFile, position, onlyMultiLine);
Expand Down Expand Up @@ -2283,6 +2294,7 @@ namespace ts {
getFormattingEditsAfterKeystroke,
getDocCommentTemplateAtPosition,
isValidBraceCompletionAtPosition,
getJsxClosingTagAtPosition,
getSpanOfEnclosingComment,
getCodeFixesAtPosition,
getCombinedCodeFix,
Expand Down
9 changes: 9 additions & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ namespace ts {
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined;

isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
/**
* 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.
* Editors should call this after `>` is typed.
*/
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;

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

Expand Down Expand Up @@ -359,6 +364,10 @@ namespace ts {
dispose(): void;
}

export interface JsxClosingTagInfo {
readonly newText: string;
}

export interface CombinedCodeFixScope { type: "file"; fileName: string; }

export type OrganizeImportsScope = CombinedCodeFixScope;
Expand Down
19 changes: 19 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4556,6 +4556,11 @@ declare namespace ts {
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[];
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined;
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
/**
* 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.
* Editors should call this after `>` is typed.
*/
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
toLineColumnOffset?(fileName: string, position: number): LineAndCharacter;
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray<number>, formatOptions: FormatCodeSettings, preferences: UserPreferences): ReadonlyArray<CodeFixAction>;
Expand All @@ -4577,6 +4582,9 @@ declare namespace ts {
getProgram(): Program | undefined;
dispose(): void;
}
interface JsxClosingTagInfo {
readonly newText: string;
}
interface CombinedCodeFixScope {
type: "file";
fileName: string;
Expand Down Expand Up @@ -5506,6 +5514,7 @@ declare namespace ts.server {
*/
declare namespace ts.server.protocol {
enum CommandTypes {
JsxClosingTag = "jsxClosingTag",
Brace = "brace",
BraceCompletion = "braceCompletion",
GetSpanOfEnclosingComment = "getSpanOfEnclosingComment",
Expand Down Expand Up @@ -6175,6 +6184,15 @@ declare namespace ts.server.protocol {
*/
openingBrace: string;
}
interface JsxClosingTagRequest extends FileLocationRequest {
readonly command: CommandTypes.JsxClosingTag;
readonly arguments: JsxClosingTagRequestArgs;
}
interface JsxClosingTagRequestArgs extends FileLocationRequestArgs {
}
interface JsxClosingTagResponse extends Response {
readonly body: TextInsertion;
}
/**
* @deprecated
* Get occurrences request; value of command field is
Expand Down Expand Up @@ -8463,6 +8481,7 @@ declare namespace ts.server {
private getSyntacticDiagnosticsSync;
private getSemanticDiagnosticsSync;
private getSuggestionDiagnosticsSync;
private getJsxClosingTag;
private getDocumentHighlights;
private setCompilerOptionsForInferredProjects;
private getProjectInfo;
Expand Down
8 changes: 8 additions & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4556,6 +4556,11 @@ declare namespace ts {
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[];
getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined;
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
/**
* 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.
* Editors should call this after `>` is typed.
*/
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
toLineColumnOffset?(fileName: string, position: number): LineAndCharacter;
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray<number>, formatOptions: FormatCodeSettings, preferences: UserPreferences): ReadonlyArray<CodeFixAction>;
Expand All @@ -4577,6 +4582,9 @@ declare namespace ts {
getProgram(): Program | undefined;
dispose(): void;
}
interface JsxClosingTagInfo {
readonly newText: string;
}
interface CombinedCodeFixScope {
type: "file";
fileName: string;
Expand Down
20 changes: 20 additions & 0 deletions tests/cases/fourslash/autoCloseTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />

// @Filename: /a.tsx
////const x = <div>/*0*/;
////const x = <div> foo/*1*/ </div>;
////const x = <div></div>/*2*/;
////const x = <div/>/*3*/;
////const x = <div>
//// <p>/*4*/
//// </div>
////</p>;
////const x = <div> text /*5*/;

verify.jsxClosingTag({
0: { newText: "</div>" },
1: undefined,
2: undefined,
3: undefined,
4: { newText: "</p>" },
});
1 change: 1 addition & 0 deletions tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ declare namespace FourSlashInterface {
typeDefinitionCountIs(expectedCount: number): void;
implementationListIsEmpty(): void;
isValidBraceCompletionAtPosition(openingBrace?: string): void;
jsxClosingTag(map: { [markerName: string]: { readonly newText: string } | undefined }): void;
isInCommentAtPosition(onlyMultiLineDiverges?: boolean): void;
codeFix(options: {
description: string,
Expand Down