Skip to content
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

Adds linked editing for JSX tags #53284

Merged
merged 34 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
409d949
add tests and fourslash framework
iisaduan Mar 9, 2023
94c4b65
cd
iisaduan Mar 11, 2023
a5192e8
first implementation, all tests except 3 (namespace) passes
iisaduan Mar 11, 2023
21aa9e7
forgot to include into last commit
iisaduan Mar 11, 2023
398f673
passes tests!
iisaduan Mar 13, 2023
2bafe8d
change testing structure
iisaduan Mar 15, 2023
da0ddf1
change testing structure
iisaduan Mar 16, 2023
9d7f32c
add correct content into tests (and some more test cases)
iisaduan Mar 16, 2023
1fff99d
changes to getLinkedEdit.., saving some extra code in comments as well
iisaduan Mar 16, 2023
0b0541d
remove comments, extra code, rename functions
iisaduan Mar 16, 2023
b70ee90
remove commented code and todos, also renames functions
iisaduan Mar 16, 2023
25393f1
revert changes to lib
iisaduan Mar 16, 2023
bb98df0
Merge branch 'mirror' of https:/iisaduan/TypeScript into …
iisaduan Mar 16, 2023
6ba6321
link everything together (fix session client)
iisaduan Mar 16, 2023
92875ce
missed renames
iisaduan Mar 16, 2023
57ef9b8
linted
iisaduan Mar 16, 2023
06afbbd
update baselines
iisaduan Mar 16, 2023
f4ec7bd
debugged whitespace issues
iisaduan Mar 17, 2023
803b292
fixed some whitespace in code
iisaduan Mar 20, 2023
34f5e40
adds better behavior for whitespace and trivia in fragments
iisaduan Mar 22, 2023
03fa5bb
refactor getJsxLinkedEditAtPosition
iisaduan Mar 22, 2023
4faee99
change type to TextSpan
iisaduan Mar 22, 2023
6a1d3dd
updated tests to have textspan
iisaduan Mar 23, 2023
c8ea559
fix names
iisaduan Mar 27, 2023
fef7ab6
fix names
iisaduan Mar 27, 2023
bb720e1
Merge branch 'mirror' of https:/iisaduan/TypeScript into …
iisaduan Mar 28, 2023
be34a1e
update protocol, refactor services
iisaduan Mar 31, 2023
7f2f885
cleaned up tests
iisaduan Mar 31, 2023
5164495
rename to match lsp
iisaduan Mar 31, 2023
a3bdf2e
rename tests
iisaduan Mar 31, 2023
228e0ae
more-readable code/comments in getLinkedEditingAtPosition
iisaduan Apr 3, 2023
32e43c8
add more tests + add `range` to function names
iisaduan Apr 4, 2023
da4f1cd
more tests
iisaduan Apr 6, 2023
db67aee
update `Body`, simplify call to `findAncestor`
iisaduan Apr 7, 2023
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
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();
}

getJsxLinkedEditAtPosition(_fileName: string, _currentCaretPosition: number): unknown {
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 @@ -3444,6 +3444,14 @@ export class TestState {
}
}

public verifyJsxLinkedEdit(map: {[markerName: string]: ts.JsxLinkedEditInfo | undefined}): void {
iisaduan marked this conversation as resolved.
Show resolved Hide resolved
for (const markerName in map) {
this.goToMarker(markerName);
const actual = this.languageService.getJsxLinkedEditAtPosition(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
5 changes: 5 additions & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ export class VerifyNegatable {
this.state.verifyJsxClosingTag(map);
}

public jsxLinkedEdit(map: { [markerName: string]: ts.JsxLinkedEditInfo | undefined }): void {
this.state.verifyJsxLinkedEdit(map);
}

iisaduan marked this conversation as resolved.
Show resolved Hide resolved

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.");
}
getJsxLinkedEditAtPosition(): 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
10 changes: 10 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
EndOfLineState,
FileExtensionInfo,
HighlightSpanKind,
JsxLinkedEditInfo,
MapLike,
OutliningSpanKind,
OutputFile,
Expand All @@ -23,6 +24,7 @@ import {

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

export interface JsxLinkedEditRequest extends FileLocationRequest {
readonly command: CommandTypes.JsxLinkedEdit;
}

export interface JsxLinkedEditResponse extends Response {
readonly info: JsxLinkedEditInfo;
}

iisaduan marked this conversation as resolved.
Show resolved Hide resolved

/**
* Get document highlights request; value of command field is
Expand Down
3 changes: 3 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3389,6 +3389,9 @@ export class Session<TMessage = string> implements EventSender {
[protocol.CommandTypes.JsxClosingTag]: (request: protocol.JsxClosingTagRequest) => {
return this.requiredResponse(this.getJsxClosingTag(request.arguments));
},
[protocol.CommandTypes.JsxLinkedEdit]: (request: protocol.JsxLinkedEditRequest) => {
return this.requiredResponse(this.getJsxClosingTag(request.arguments));
},
[protocol.CommandTypes.GetCodeFixes]: (request: protocol.CodeFixRequest) => {
return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ true));
},
Expand Down
61 changes: 61 additions & 0 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ import {
filter,
find,
FindAllReferences,
findAncestor,
findChildOfKind,
findNextToken,
findPrecedingToken,
first,
firstDefined,
Expand Down Expand Up @@ -153,10 +155,13 @@ import {
isJSDocCommentContainingNode,
isJsxAttributes,
isJsxClosingElement,
isJsxClosingFragment,
isJsxElement,
isJsxFragment,
isJsxOpeningElement,
isJsxOpeningFragment,
isJsxSelfClosingElement,
isJSXTagName,
isJsxText,
isLabelName,
isLiteralComputedPropertyDeclarationName,
Expand Down Expand Up @@ -185,10 +190,13 @@ import {
JSDocTagInfo,
JsonSourceFile,
JsxAttributes,
JsxClosingElement,
JsxClosingTagInfo,
JsxElement,
JsxEmit,
JsxFragment,
JsxLinkedEditInfo,
JsxOpeningElement,
LanguageService,
LanguageServiceHost,
LanguageServiceMode,
Expand Down Expand Up @@ -2478,6 +2486,58 @@ export function createLanguageService(
}
}

function getJsxLinkedEditAtPosition(fileName: string, position: number): JsxLinkedEditInfo | undefined {
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
const token = findPrecedingToken(position, sourceFile);
if (!token) return undefined;

let wordPattern: string | undefined;
let ranges: {start: number, end: number}[] = [];
iisaduan marked this conversation as resolved.
Show resolved Hide resolved

if ((isJsxOpeningFragment(token.parent) && token.kind===SyntaxKind.LessThanToken)
|| (isJsxClosingFragment(token.parent) && token.kind===SyntaxKind.SlashToken)){
iisaduan marked this conversation as resolved.
Show resolved Hide resolved

const openPos = token.parent.parent.openingFragment.pos + 1;
iisaduan marked this conversation as resolved.
Show resolved Hide resolved
const closePos = token.parent.parent.closingFragment.pos + 2;
iisaduan marked this conversation as resolved.
Show resolved Hide resolved

//TODO: fragments with whitespace?
ranges = ranges.concat({ start : openPos, end : openPos });
ranges = ranges.concat({ start : closePos, end : closePos });
iisaduan marked this conversation as resolved.
Show resolved Hide resolved
wordPattern = undefined;
}
else if (isJsxFragment(token.parent.parent)) {
return undefined;
}
else {
const tag = findAncestor(token,
iisaduan marked this conversation as resolved.
Show resolved Hide resolved
(n) => {
if (!n.parent.parent) return "quit";
else if (isJsxElement(n.parent.parent)) {
if ((isJSXTagName(n) && !isJsxSelfClosingElement(n.parent))
|| (n.kind===SyntaxKind.LessThanToken && isJSXTagName(findNextToken(n,n.parent,sourceFile) ?? n))
|| n.kind===SyntaxKind.SlashToken) {
return true;
}
return "quit";
}
return false;
}
)?.parent as JsxOpeningElement | JsxClosingElement | undefined;
iisaduan marked this conversation as resolved.
Show resolved Hide resolved
if (!tag) return undefined;

ranges = ranges.concat({ start : tag.parent.openingElement.tagName.getStart(), end : tag.parent.openingElement.tagName.end });
ranges = ranges.concat({ start : tag.parent.closingElement.tagName.getStart(), end : tag.parent.closingElement.tagName.end });
iisaduan marked this conversation as resolved.
Show resolved Hide resolved

if (!(ranges[0].start <= position && position <= ranges[0].end || ranges[1].start <= position && position <= ranges[1].end)) return undefined;
iisaduan marked this conversation as resolved.
Show resolved Hide resolved

if (tag.parent.openingElement.tagName.getText() === tag.parent.closingElement.tagName.getText()) {
iisaduan marked this conversation as resolved.
Show resolved Hide resolved
wordPattern = tag.tagName.getText();
}
else return undefined;
}
return { ranges, wordPattern };
}

function getLinesForRange(sourceFile: SourceFile, textRange: TextRange) {
return {
lineStarts: sourceFile.getLineStarts(),
Expand Down Expand Up @@ -3009,6 +3069,7 @@ export function createLanguageService(
getDocCommentTemplateAtPosition,
isValidBraceCompletionAtPosition,
getJsxClosingTagAtPosition,
getJsxLinkedEditAtPosition,
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;
getJsxLinkedEditAtPosition(fileName: string, currentCaretPosition: number): JsxLinkedEditInfo | unknown;

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 JsxLinkedEditInfo {
readonly ranges: {start: number, end: number}[];
wordPattern?: string;
}

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

export const enum OrganizeImportsMode {
Expand Down
15 changes: 15 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ declare namespace ts {
namespace protocol {
enum CommandTypes {
JsxClosingTag = "jsxClosingTag",
JsxLinkedEdit = "jsxLinkedEdit",
iisaduan marked this conversation as resolved.
Show resolved Hide resolved
Brace = "brace",
BraceCompletion = "braceCompletion",
GetSpanOfEnclosingComment = "getSpanOfEnclosingComment",
Expand Down Expand Up @@ -877,6 +878,12 @@ declare namespace ts {
interface JsxClosingTagResponse extends Response {
readonly body: TextInsertion;
}
interface JsxLinkedEditRequest extends FileLocationRequest {
readonly command: CommandTypes.JsxLinkedEdit;
}
interface JsxLinkedEditResponse extends Response {
readonly info: JsxLinkedEditInfo;
}
/**
* Get document highlights request; value of command field is
* "documentHighlights". Return response giving spans that are relevant
Expand Down Expand Up @@ -9977,6 +9984,7 @@ declare namespace ts {
* Editors should call this after `>` is typed.
*/
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
getJsxLinkedEditAtPosition(fileName: string, currentCaretPosition: number): JsxLinkedEditInfo | unknown;
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 @@ -10006,6 +10014,13 @@ declare namespace ts {
interface JsxClosingTagInfo {
readonly newText: string;
}
interface JsxLinkedEditInfo {
readonly ranges: {
start: number;
end: number;
}[];
wordPattern?: string;
}
interface CombinedCodeFixScope {
type: "file";
fileName: string;
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 @@ -6107,6 +6107,7 @@ declare namespace ts {
* Editors should call this after `>` is typed.
*/
getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined;
getJsxLinkedEditAtPosition(fileName: string, currentCaretPosition: number): JsxLinkedEditInfo | unknown;
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 @@ -6136,6 +6137,13 @@ declare namespace ts {
interface JsxClosingTagInfo {
readonly newText: string;
}
interface JsxLinkedEditInfo {
readonly ranges: {
start: number;
end: number;
}[];
wordPattern?: string;
}
interface CombinedCodeFixScope {
type: "file";
fileName: string;
Expand Down
3 changes: 3 additions & 0 deletions tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ declare namespace FourSlashInterface {
implementationListIsEmpty(): void;
isValidBraceCompletionAtPosition(openingBrace?: string): void;
jsxClosingTag(map: { [markerName: string]: { readonly newText: string } | undefined }): void;
jsxLinkedEdit(map: { [markerName: string]: {
readonly ranges : {start:number, end:number}[],
wordPattern? : string ;} | undefined }): void;
isInCommentAtPosition(onlyMultiLineDiverges?: boolean): void;
codeFix(options: {
description: string | [string, ...(string | number)[]] | DiagnosticIgnoredInterpolations,
Expand Down
55 changes: 55 additions & 0 deletions tests/cases/fourslash/jsxTagLinkedEdit1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/// <reference path='fourslash.ts' />

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

// @Filename: /basic.tsx
////const jsx = (
//// </*0*/d/*1*/iv/*2*/>/*7*/
//// </*6*///*3*/di/*4*/v/*5*/>/*8*/
////);

// @Filename: /whitespaceInvalidClosing.tsx
////const jsx = (
//// <div>
//// < /*9*/ /div>
////);

// @Filename: /whitespaceOpening.tsx
////const jsx3 = (
//// </*10*/ /*11*/div/*12*/ /*13*/> /*14*/
//// </di/*A*/v>
////);

// @Filename: /whitespaceClosing.tsx
////const jsx = (
//// <di/*B*/v>
//// <//*15*/ /*16*/div/*17*/ /*18*/> /*19*/
////);



const linkedCursors1 = {ranges: [{start: 19, end: 22},
{start: 30, end: 33}],
wordPattern : 'div'};

verify.jsxLinkedEdit( {
"0": linkedCursors1,
"1": linkedCursors1,
"2": linkedCursors1,
"3": linkedCursors1,
"4": linkedCursors1,
"5": linkedCursors1,
"6": undefined,
"7": undefined,
"8": undefined,
"9": undefined, // I believe this should be an invalid tag
"10": undefined,
"13": undefined,
"15": undefined,
"18": undefined,
});

24 changes: 24 additions & 0 deletions tests/cases/fourslash/jsxTagLinkedEdit2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// <reference path='fourslash.ts' />

// @Filename: /attrs.tsx
////const jsx = (
//// </*0*/div/*1*/ /*5*/styl/*2*/e={{ color: 'red' }}/*6*/>/*4*/
//// <p>
//// <img />
//// </p>
//// </di/*3*/v>
////);

const linkedCursors2 = {ranges: [{start: 18, end: 21},
{start: 91, end: 94}],
wordPattern : 'div'};

verify.jsxLinkedEdit( {
"0": linkedCursors2,
"1": linkedCursors2,
"2": undefined,
"3": linkedCursors2,
"4": undefined,
"5": undefined,
"6": undefined,
});
25 changes: 25 additions & 0 deletions tests/cases/fourslash/jsxTagLinkedEdit3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// <reference path='fourslash.ts' />

// @Filename: /selfClosing.tsx
////const jsx = (
//// <div>/*1*/
//// <p>/*4*/
//// /*5*/<img/*2*/ /*3*//>
//// /*6*/ </p>/*8*/
//// /*7*/</div>
////);

// const linkedCursors3 = {ranges: [{start: 18, end: 21},
// {start: 91, end: 94}],
// wordPattern : 'div'};

verify.jsxLinkedEdit( {
"1": undefined,
"2": undefined,
"3": undefined,
"4": undefined,
"5": undefined,
"6": undefined,
"7": undefined,
"8": undefined,
});
Loading