Skip to content

Commit

Permalink
Adds linked editing for JSX tags (#53284)
Browse files Browse the repository at this point in the history
  • Loading branch information
iisaduan authored Apr 7, 2023
1 parent c89f87f commit d4c48e1
Show file tree
Hide file tree
Showing 21 changed files with 694 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,10 @@ export class SessionClient implements LanguageService {
return notImplemented();
}

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

getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan {
return notImplemented();
}
Expand Down
8 changes: 8 additions & 0 deletions src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3530,6 +3530,14 @@ export class TestState {
}
}

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

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

Expand Down
4 changes: 4 additions & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ export class VerifyNegatable {
this.state.verifyJsxClosingTag(map);
}

public linkedEditing(map: { [markerName: string]: ts.LinkedEditingInfo | undefined }): void {
this.state.verifyLinkedEditingRange(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 @@ -593,6 +593,9 @@ class LanguageServiceShimProxy implements ts.LanguageService {
getJsxClosingTagAtPosition(): never {
throw new Error("Not supported on the shim.");
}
getLinkedEditingRangeAtPosition(): 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
13 changes: 13 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {

export const enum CommandTypes {
JsxClosingTag = "jsxClosingTag",
LinkedEditingRange = "linkedEditingRange",
Brace = "brace",
/** @internal */
BraceFull = "brace-full",
Expand Down Expand Up @@ -1101,6 +1102,18 @@ export interface JsxClosingTagResponse extends Response {
readonly body: TextInsertion;
}

export interface LinkedEditingRangeRequest extends FileLocationRequest {
readonly command: CommandTypes.LinkedEditingRange;
}

export interface LinkedEditingRangesBody {
ranges: TextSpan[];
wordPattern?: string;
}

export interface LinkedEditingRangeResponse extends Response {
readonly body: LinkedEditingRangesBody;
}

/**
* Get document highlights request; value of command field is
Expand Down
26 changes: 26 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
JSDocTagInfo,
LanguageServiceMode,
LineAndCharacter,
LinkedEditingInfo,
map,
mapDefined,
mapDefinedIterator,
Expand Down Expand Up @@ -1805,6 +1806,15 @@ export class Session<TMessage = string> implements EventSender {
return tag === undefined ? undefined : { newText: tag.newText, caretOffset: 0 };
}

private getLinkedEditingRange(args: protocol.FileLocationRequestArgs): protocol.LinkedEditingRangesBody | undefined {
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
const position = this.getPositionInFile(args, file);
const linkedEditInfo = languageService.getLinkedEditingRangeAtPosition(file, position);
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file);
if (scriptInfo === undefined || linkedEditInfo === undefined) return undefined;
return convertLinkedEditInfoToRanges(linkedEditInfo, scriptInfo);
}

private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): readonly protocol.DocumentHighlightsItem[] | readonly DocumentHighlights[] {
const { file, project } = this.getFileAndProject(args);
const position = this.getPositionInFile(args, file);
Expand Down Expand Up @@ -3392,6 +3402,9 @@ export class Session<TMessage = string> implements EventSender {
[protocol.CommandTypes.JsxClosingTag]: (request: protocol.JsxClosingTagRequest) => {
return this.requiredResponse(this.getJsxClosingTag(request.arguments));
},
[protocol.CommandTypes.LinkedEditingRange]: (request: protocol.LinkedEditingRangeRequest) => {
return this.requiredResponse(this.getLinkedEditingRange(request.arguments));
},
[protocol.CommandTypes.GetCodeFixes]: (request: protocol.CodeFixRequest) => {
return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ true));
},
Expand Down Expand Up @@ -3647,6 +3660,19 @@ function positionToLineOffset(info: ScriptInfoOrConfig, position: number): proto
return isConfigFile(info) ? locationFromLineAndCharacter(info.getLineAndCharacterOfPosition(position)) : info.positionToLineOffset(position);
}

function convertLinkedEditInfoToRanges(linkedEdit: LinkedEditingInfo, scriptInfo: ScriptInfo): protocol.LinkedEditingRangesBody {
const ranges = linkedEdit.ranges.map(
r => {
return {
start: scriptInfo.positionToLineOffset(r.start),
end: scriptInfo.positionToLineOffset(r.start + r.length),
};
}
);
if (!linkedEdit.wordPattern) return { ranges };
return { ranges, wordPattern: linkedEdit.wordPattern };
}

function locationFromLineAndCharacter(lc: LineAndCharacter): protocol.Location {
return { line: lc.line + 1, offset: lc.character + 1 };
}
Expand Down
55 changes: 55 additions & 0 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
Completions,
computePositionOfLineAndCharacter,
computeSuggestionDiagnostics,
containsParseError,
createDocumentRegistry,
createGetCanonicalFileName,
createMultiMap,
Expand Down Expand Up @@ -69,6 +70,7 @@ import {
filter,
find,
FindAllReferences,
findAncestor,
findChildOfKind,
findPrecedingToken,
first,
Expand Down Expand Up @@ -196,6 +198,7 @@ import {
length,
LineAndCharacter,
lineBreakPart,
LinkedEditingInfo,
LiteralType,
map,
mapDefined,
Expand Down Expand Up @@ -2480,6 +2483,57 @@ export function createLanguageService(
}
}

function getLinkedEditingRangeAtPosition(fileName: string, position: number): LinkedEditingInfo | undefined {
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
const token = findPrecedingToken(position, sourceFile);
if (!token || token.parent.kind === SyntaxKind.SourceFile) return undefined;

if (isJsxFragment(token.parent.parent)) {
const openFragment = token.parent.parent.openingFragment;
const closeFragment = token.parent.parent.closingFragment;
if (containsParseError(openFragment) || containsParseError(closeFragment)) return undefined;

const openPos = openFragment.getStart(sourceFile) + 1; // "<".length
const closePos = closeFragment.getStart(sourceFile) + 2; // "</".length

// only allows linked editing right after opening bracket: <| ></| >
if ((position !== openPos) && (position !== closePos)) return undefined;

return { ranges: [{ start: openPos, length: 0 }, { start: closePos, length: 0 }] };
}
else {
// determines if the cursor is in an element tag
const tag = findAncestor(token.parent,
n => {
if (isJsxOpeningElement(n) || isJsxClosingElement(n)) {
return true;
}
return false;
});
if (!tag) return undefined;
Debug.assert(isJsxOpeningElement(tag) || isJsxClosingElement(tag), "tag should be opening or closing element");

const openTag = tag.parent.openingElement;
const closeTag = tag.parent.closingElement;

const openTagStart = openTag.tagName.getStart(sourceFile);
const openTagEnd = openTag.tagName.end;
const closeTagStart = closeTag.tagName.getStart(sourceFile);
const closeTagEnd = closeTag.tagName.end;

// only return linked cursors if the cursor is within a tag name
if (!(openTagStart <= position && position <= openTagEnd || closeTagStart <= position && position <= closeTagEnd)) return undefined;

// only return linked cursors if text in both tags is identical
const openingTagText = openTag.tagName.getText(sourceFile);
if (openingTagText !== closeTag.tagName.getText(sourceFile)) return undefined;

return {
ranges: [{ start: openTagStart, length: openTagEnd - openTagStart }, { start: closeTagStart, length: closeTagEnd - closeTagStart }],
};
}
}

function getLinesForRange(sourceFile: SourceFile, textRange: TextRange) {
return {
lineStarts: sourceFile.getLineStarts(),
Expand Down Expand Up @@ -3011,6 +3065,7 @@ export function createLanguageService(
getDocCommentTemplateAtPosition,
isValidBraceCompletionAtPosition,
getJsxClosingTagAtPosition,
getLinkedEditingRangeAtPosition,
getSpanOfEnclosingComment,
getCodeFixesAtPosition,
getCombinedCodeFix,
Expand Down
6 changes: 6 additions & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ export interface LanguageService {
* Editors should call this after `>` is typed.
*/
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
getLinkedEditingRangeAtPosition(fileName: string, position: number): LinkedEditingInfo | undefined;

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

Expand Down Expand Up @@ -661,6 +662,11 @@ export interface JsxClosingTagInfo {
readonly newText: string;
}

export interface LinkedEditingInfo {
readonly ranges: TextSpan[];
wordPattern?: string;
}

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

export const enum OrganizeImportsMode {
Expand Down
17 changes: 17 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ declare namespace ts {
namespace protocol {
enum CommandTypes {
JsxClosingTag = "jsxClosingTag",
LinkedEditingRange = "linkedEditingRange",
Brace = "brace",
BraceCompletion = "braceCompletion",
GetSpanOfEnclosingComment = "getSpanOfEnclosingComment",
Expand Down Expand Up @@ -885,6 +886,16 @@ declare namespace ts {
interface JsxClosingTagResponse extends Response {
readonly body: TextInsertion;
}
interface LinkedEditingRangeRequest extends FileLocationRequest {
readonly command: CommandTypes.LinkedEditingRange;
}
interface LinkedEditingRangesBody {
ranges: TextSpan[];
wordPattern?: string;
}
interface LinkedEditingRangeResponse extends Response {
readonly body: LinkedEditingRangesBody;
}
/**
* Get document highlights request; value of command field is
* "documentHighlights". Return response giving spans that are relevant
Expand Down Expand Up @@ -3903,6 +3914,7 @@ declare namespace ts {
private getSemanticDiagnosticsSync;
private getSuggestionDiagnosticsSync;
private getJsxClosingTag;
private getLinkedEditingRange;
private getDocumentHighlights;
private provideInlayHints;
private setCompilerOptionsForInferredProjects;
Expand Down Expand Up @@ -10090,6 +10102,7 @@ declare namespace ts {
* Editors should call this after `>` is typed.
*/
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
getLinkedEditingRangeAtPosition(fileName: string, position: number): LinkedEditingInfo | undefined;
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
toLineColumnOffset?(fileName: string, position: number): LineAndCharacter;
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: UserPreferences): readonly CodeFixAction[];
Expand Down Expand Up @@ -10119,6 +10132,10 @@ declare namespace ts {
interface JsxClosingTagInfo {
readonly newText: string;
}
interface LinkedEditingInfo {
readonly ranges: TextSpan[];
wordPattern?: string;
}
interface CombinedCodeFixScope {
type: "file";
fileName: string;
Expand Down
5 changes: 5 additions & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6172,6 +6172,7 @@ declare namespace ts {
* Editors should call this after `>` is typed.
*/
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
getLinkedEditingRangeAtPosition(fileName: string, position: number): LinkedEditingInfo | undefined;
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined;
toLineColumnOffset?(fileName: string, position: number): LineAndCharacter;
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: UserPreferences): readonly CodeFixAction[];
Expand Down Expand Up @@ -6201,6 +6202,10 @@ declare namespace ts {
interface JsxClosingTagInfo {
readonly newText: string;
}
interface LinkedEditingInfo {
readonly ranges: TextSpan[];
wordPattern?: string;
}
interface CombinedCodeFixScope {
type: "file";
fileName: string;
Expand Down
6 changes: 6 additions & 0 deletions tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ declare namespace FourSlashInterface {
quickInfoExists(): void;
isValidBraceCompletionAtPosition(openingBrace?: string): void;
jsxClosingTag(map: { [markerName: string]: { readonly newText: string } | undefined }): void;
linkedEditing(map: { [markerName: string]: LinkedEditingInfo | undefined }): void;
isInCommentAtPosition(onlyMultiLineDiverges?: boolean): void;
codeFix(options: {
description: string | [string, ...(string | number)[]] | DiagnosticIgnoredInterpolations,
Expand Down Expand Up @@ -737,6 +738,11 @@ declare namespace FourSlashInterface {
generateReturnInDocTemplate?: boolean;
}

type LinkedEditingInfo = {
readonly ranges: { start: number, length: number }[];
wordPattern?: string;
}

export type SignatureHelpTriggerReason =
| SignatureHelpInvokedReason
| SignatureHelpCharacterTypedReason
Expand Down
57 changes: 57 additions & 0 deletions tests/cases/fourslash/linkedEditingJsxTag1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/// <reference path='fourslash.ts' />

// the content of basic.tsx
//const jsx = (
// <div>
// </div>
//);

// @Filename: /basic.tsx
/////*a*/const j/*b*/sx = (
//// /*c*/</*0*/d/*1*/iv/*2*/>/*3*/
//// </*4*///*5*/di/*6*/v/*7*/>/*8*/
////);
////const jsx2 = (
//// <d/*9*/iv>
//// <d/*10*/iv>
//// <p/*11*/>
//// <//*12*/p>
//// </d/*13*/iv>
//// </d/*14*/iv>
////);/*d*/

const linkedCursors1 = {
ranges: [{ start: test.markerByName("0").position, length: 3 }, { start: test.markerByName("5").position, length: 3 }],
};
const linkedCursors2 = {
ranges: [{ start: test.markerByName("9").position - 1, length: 3 }, { start: test.markerByName("14").position - 1, length: 3 }],
};
const linkedCursors3 = {
ranges: [{ start: test.markerByName("10").position - 1, length: 3 }, { start: test.markerByName("13").position - 1, length: 3 }],
};
const linkedCursors4 = {
ranges: [{ start: test.markerByName("11").position - 1, length: 1 }, { start: test.markerByName("12").position, length: 1 }],
};

verify.linkedEditing( {
"0": linkedCursors1,
"1": linkedCursors1,
"2": linkedCursors1,
"3": undefined,
"4": undefined,
"5": linkedCursors1,
"6": linkedCursors1,
"7": linkedCursors1,
"8": undefined,
"9": linkedCursors2,
"10": linkedCursors3,
"11": linkedCursors4,
"12": linkedCursors4,
"13": linkedCursors3,
"14": linkedCursors2,
"a": undefined,
"b": undefined,
"c": undefined,
"d": undefined,
});

Loading

0 comments on commit d4c48e1

Please sign in to comment.