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 all 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://github.com/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://github.com/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();
}

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 @@ -3444,6 +3444,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 @@ -189,6 +189,10 @@ export class VerifyNegatable {
this.state.verifyJsxClosingTag(map);
}

public linkedEditing(map: { [markerName: string]: ts.LinkedEditingInfo | undefined }): void {
iisaduan marked this conversation as resolved.
Show resolved Hide resolved
this.state.verifyLinkedEditingRange(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.");
}
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 {

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;
}

iisaduan marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1802,6 +1803,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 @@ -3389,6 +3399,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 @@ -3644,6 +3657,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 @@ -34,6 +34,7 @@ import {
Completions,
computePositionOfLineAndCharacter,
computeSuggestionDiagnostics,
containsParseError,
createDocumentRegistry,
createGetCanonicalFileName,
createMultiMap,
Expand Down Expand Up @@ -71,6 +72,7 @@ import {
filter,
find,
FindAllReferences,
findAncestor,
findChildOfKind,
findPrecedingToken,
first,
Expand Down Expand Up @@ -197,6 +199,7 @@ import {
length,
LineAndCharacter,
lineBreakPart,
LinkedEditingInfo,
LiteralType,
map,
mapDefined,
Expand Down Expand Up @@ -2478,6 +2481,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 @@ -3009,6 +3063,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 @@ -89,6 +89,7 @@ declare namespace ts {
namespace protocol {
enum CommandTypes {
JsxClosingTag = "jsxClosingTag",
LinkedEditingRange = "linkedEditingRange",
Brace = "brace",
BraceCompletion = "braceCompletion",
GetSpanOfEnclosingComment = "getSpanOfEnclosingComment",
Expand Down Expand Up @@ -877,6 +878,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 @@ -3847,6 +3858,7 @@ declare namespace ts {
private getSemanticDiagnosticsSync;
private getSuggestionDiagnosticsSync;
private getJsxClosingTag;
private getLinkedEditingRange;
private getDocumentHighlights;
private provideInlayHints;
private setCompilerOptionsForInferredProjects;
Expand Down Expand Up @@ -9977,6 +9989,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 @@ -10006,6 +10019,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 @@ -6107,6 +6107,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 @@ -6136,6 +6137,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 @@ -260,6 +260,7 @@ declare namespace FourSlashInterface {
implementationListIsEmpty(): 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 @@ -759,6 +760,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