diff --git a/src/harness/client.ts b/src/harness/client.ts index 22db85eb4be92..a74e616072302 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -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(); } diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 7c7c43db639e7..2ee881e653d56 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -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); diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 4cf5c58d542b7..ca086dc1b2799 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -189,6 +189,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); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 84eb84e330af6..1f1cf4d0abac2 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -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)); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 50546bb8dce8f..fbd7f04088ae4 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -23,6 +23,7 @@ import { export const enum CommandTypes { JsxClosingTag = "jsxClosingTag", + LinkedEditingRange = "linkedEditingRange", Brace = "brace", /** @internal */ BraceFull = "brace-full", @@ -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 diff --git a/src/server/session.ts b/src/server/session.ts index f7e37522a8719..853bfadc38882 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -83,6 +83,7 @@ import { JSDocTagInfo, LanguageServiceMode, LineAndCharacter, + LinkedEditingInfo, map, mapDefined, mapDefinedIterator, @@ -1802,6 +1803,15 @@ export class Session 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); @@ -3389,6 +3399,9 @@ export class Session 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)); }, @@ -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 }; } diff --git a/src/services/services.ts b/src/services/services.ts index 4bf03927fb3bf..938f946a0d614 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -34,6 +34,7 @@ import { Completions, computePositionOfLineAndCharacter, computeSuggestionDiagnostics, + containsParseError, createDocumentRegistry, createGetCanonicalFileName, createMultiMap, @@ -71,6 +72,7 @@ import { filter, find, FindAllReferences, + findAncestor, findChildOfKind, findPrecedingToken, first, @@ -197,6 +199,7 @@ import { length, LineAndCharacter, lineBreakPart, + LinkedEditingInfo, LiteralType, map, mapDefined, @@ -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; // " + 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(), @@ -3009,6 +3063,7 @@ export function createLanguageService( getDocCommentTemplateAtPosition, isValidBraceCompletionAtPosition, getJsxClosingTagAtPosition, + getLinkedEditingRangeAtPosition, getSpanOfEnclosingComment, getCodeFixesAtPosition, getCombinedCodeFix, diff --git a/src/services/types.ts b/src/services/types.ts index de17b02ac2c4d..cf3205a88afb0 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -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; @@ -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 { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 6f8b2365c3511..c60b924ffb3e0 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -89,6 +89,7 @@ declare namespace ts { namespace protocol { enum CommandTypes { JsxClosingTag = "jsxClosingTag", + LinkedEditingRange = "linkedEditingRange", Brace = "brace", BraceCompletion = "braceCompletion", GetSpanOfEnclosingComment = "getSpanOfEnclosingComment", @@ -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 @@ -3847,6 +3858,7 @@ declare namespace ts { private getSemanticDiagnosticsSync; private getSuggestionDiagnosticsSync; private getJsxClosingTag; + private getLinkedEditingRange; private getDocumentHighlights; private provideInlayHints; private setCompilerOptionsForInferredProjects; @@ -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[]; @@ -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; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 35f824d416f0b..bff422b6466da 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -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[]; @@ -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; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 53352c8573dc1..51a0eca5b1a18 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -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, @@ -759,6 +760,11 @@ declare namespace FourSlashInterface { generateReturnInDocTemplate?: boolean; } + type LinkedEditingInfo = { + readonly ranges: { start: number, length: number }[]; + wordPattern?: string; + } + export type SignatureHelpTriggerReason = | SignatureHelpInvokedReason | SignatureHelpCharacterTypedReason diff --git a/tests/cases/fourslash/linkedEditingJsxTag1.ts b/tests/cases/fourslash/linkedEditingJsxTag1.ts new file mode 100644 index 0000000000000..ab0183c9c1e15 --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag1.ts @@ -0,0 +1,57 @@ +/// + +// the content of basic.tsx +//const jsx = ( +//
+//
+//); + +// @Filename: /basic.tsx +/////*a*/const j/*b*/sx = ( +//// /*c*//*3*/ +//// /*8*/ +////); +////const jsx2 = ( +//// +//// +////

+//// +//// +//// +////);/*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, +}); + diff --git a/tests/cases/fourslash/linkedEditingJsxTag10.ts b/tests/cases/fourslash/linkedEditingJsxTag10.ts new file mode 100644 index 0000000000000..0cc40d35da978 --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag10.ts @@ -0,0 +1,88 @@ +/// + +// @Filename: /jsx0.tsx +////const jsx = + +// @Filename: /jsx1.tsx +////const jsx = + +// @Filename: /jsx2.tsx +////const jsx = + +// @Filename: /jsx3.tsx +////const jsx = + +// @Filename: /jsx4.tsx +////const jsx = ; + +// @Filename: /jsx5.tsx +////const jsx = ; + +// @Filename: /jsx6.tsx +////const jsx = /*6*/div> ; + +// @Filename: /jsx7.tsx +////const jsx = //*7a*/div>; + +// @Filename: /jsx8.tsx +////const jsx = ; + +// @Filename: /jsx9.tsx +////const jsx = ; + +// @Filename: /jsx12.tsx +////const jsx = /*12*/> ; + +// @Filename: /jsx13.tsx +////const jsx = //*13a*/>; + +// @Filename: /jsx14.tsx +////const jsx = ; + +// @Filename: /jsx15.tsx +////const jsx = ; + +const linkedCursors9 = { + ranges: [{ start: test.markerByName("9").position, length: 3 }, { start: test.markerByName("9a").position, length: 3 }], +}; + +verify.linkedEditing( { + "0": undefined, + "1": undefined, + "2": undefined, + "3": undefined, + "4": undefined, + "4a": undefined, + "5": undefined, + "5a": undefined, + "6": undefined, + "6a": undefined, + "7": undefined, + "7a": undefined, + "8": undefined, + "8a": undefined, + "9": linkedCursors9, + "9a": linkedCursors9, + "10": undefined, + "10a": undefined, + "11": undefined, + "11a": undefined, + "12": undefined, + "12a": undefined, + "13": undefined, + "13a": undefined, + "14": undefined, + "14a": undefined, + "14b": undefined, + "14c": undefined, + "15": undefined, + "15a": undefined, + "15b": undefined, + "15c": undefined, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/linkedEditingJsxTag2.ts b/tests/cases/fourslash/linkedEditingJsxTag2.ts new file mode 100644 index 0000000000000..35c5a119653c9 --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag2.ts @@ -0,0 +1,50 @@ +/// + +// for readability +////const jsx = ( +////

+////

+//// +////

+////
+////); + +// @Filename: /attrs.tsx +////const jsx = ( +//// /*5*/ +////

+//// +////

+//// +////); + +// this case is missing a closing brace in the attributes +// @Filename: /attrsError.tsx +////const jsx = ( +//// /*15*/ +//// +//// +////); + +const startPos = test.markerByName("0").position; +const endPos = test.markerByName("6").position - 2; +const linkedCursors = { + ranges: [{ start: startPos, length: 3 }, { start: endPos, length: 3 }], +}; + +verify.linkedEditing( { + "0": linkedCursors, + "1": linkedCursors, + "2": undefined, + "3": undefined, + "4": undefined, + "5": undefined, + "6": linkedCursors, + "10": undefined, + "11": undefined, + "12": undefined, + "13": undefined, + "14": undefined, + "15": undefined, + "16": undefined, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/linkedEditingJsxTag3.ts b/tests/cases/fourslash/linkedEditingJsxTag3.ts new file mode 100644 index 0000000000000..2dfaa175e4cf9 --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag3.ts @@ -0,0 +1,30 @@ +/// + +// @Filename: /selfClosing.tsx +/////*0*/const jsx = /*1*/( +////
/*2*/ +////

/*3*/ +//// No lin/*4*/ked cursors here! +//// /*5*/ +//// /*10*/

/*11*/ +//// /*12*/
+/////*13*/)/*14*/;/*15*/ + +verify.linkedEditing( { + "0": undefined, + "1": undefined, + "2": undefined, + "3": undefined, + "4": undefined, + "5": undefined, + "6": undefined, + "7": undefined, + "8": undefined, + "9": undefined, + "10": undefined, + "11": undefined, + "12": undefined, + "13": undefined, + "14": undefined, + "15": undefined, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/linkedEditingJsxTag4.ts b/tests/cases/fourslash/linkedEditingJsxTag4.ts new file mode 100644 index 0000000000000..b130c55e024f9 --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag4.ts @@ -0,0 +1,49 @@ +/// + +// for readability purposes +//const jsx = ( +// > +//

+// +//

+// +//); + +// @Filename: /typeTag.tsx +////const jsx = ( +//// /*4*/>/*5*/ +////

+//// +////

+//// +////); + +// @Filename: /typeTagError.tsx +////const jsx = ( +//// /*14*/ +//// +//// +////); + +const startPos = test.markerByName("0").position; +const endPos = test.markerByName("6").position; +const linkedCursors = { + ranges: [{ start: startPos, length: 3 }, { start: endPos, length: 3 }], +}; + +verify.linkedEditing( { + "0": linkedCursors, + "1": linkedCursors, + "2": undefined, + "3": undefined, + "4": undefined, + "5": undefined, + "6": linkedCursors, + "10": undefined, + "11": undefined, + "12": undefined, + "13": undefined, + "14": undefined, + "15": undefined, + "16": undefined, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/linkedEditingJsxTag5.ts b/tests/cases/fourslash/linkedEditingJsxTag5.ts new file mode 100644 index 0000000000000..2e7dbe56acea2 --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag5.ts @@ -0,0 +1,50 @@ +/// + +// @FileName: /unclosedElement.tsx +////const jsx = ( +////
+////
+////
/*3*/ +////);/*4*/ + +// @FileName: /mismatchedElement.tsx +////const jsx = ( +//// /*5*/
+////
+//// /*10*/ +////); + +// @Filename: /invalidClosing.tsx +////const jsx = ( +//// +//// +////); + +const startPos1 = test.markerByName("1").position - 3; +const endPos1 = test.markerByName("2").position - 3; +const linkedCursors1 = { + ranges: [{ start: startPos1, length: 3 }, { start: endPos1, length: 3 }], +}; + +const startPos2 = test.markerByName("6").position - 3; +const endPos2 = test.markerByName("7").position - 3; +const linkedCursors2 = { + ranges: [{ start: startPos2, length: 3 }, { start: endPos2, length: 3 }], +}; + +verify.linkedEditing( { + "0": undefined, + "1": linkedCursors1, + "2": linkedCursors1, + "3": undefined, + "4": undefined, + "5": undefined, + "6": linkedCursors2, + "7": linkedCursors2, + "8": undefined, + "9": undefined, + "10": undefined, + "11": undefined, // this tag does not parse as a closing tag + "12": undefined, + "13": undefined, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/linkedEditingJsxTag6.ts b/tests/cases/fourslash/linkedEditingJsxTag6.ts new file mode 100644 index 0000000000000..8cac987be18cb --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag6.ts @@ -0,0 +1,65 @@ +/// + +// @Filename: /namespace.tsx +////const jsx = ( +//// +//// +////); +//// const jsx1 = ; +//// const jsx2 = ; +//// const jsx3 = +//// ; +//// let jsx4 = +//// +//// + +const startPos1 = test.markerByName("start").position; +const endPos1 = test.markerByName("end").position; +const linkedCursors1 = { + ranges: [{ start: startPos1, length: 19 }, { start: endPos1, length: 19 }], +}; + +const startPos2 = test.markerByName("26").position; +const endPos2 = test.markerByName("30").position; +const linkedCursors2 = { + ranges: [{ start: startPos2, length: 21 }, { start: endPos2, length: 21 }], +}; + +verify.linkedEditing( { + "1": linkedCursors1, + "2": linkedCursors1, + "3": linkedCursors1, + "4": undefined, + "5": undefined, + "6": undefined, + "7": undefined, + "8": undefined, + "9": undefined, + "10": undefined, + "11": undefined, + "12": undefined, + "13": undefined, + "14": undefined, + "15": undefined, + "16": undefined, + "17": undefined, + "18": undefined, + "19": undefined, + "20": undefined, + "21": undefined, + "22": undefined, + "23": undefined, + "24": undefined, + "25": undefined, + "26": linkedCursors2, + "27": linkedCursors2, + "28": linkedCursors2, + "29": linkedCursors2, + "30": linkedCursors2, + "31": linkedCursors2, + "32": linkedCursors2, + "33": linkedCursors2, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/linkedEditingJsxTag7.ts b/tests/cases/fourslash/linkedEditingJsxTag7.ts new file mode 100644 index 0000000000000..c401de04cb5d0 --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag7.ts @@ -0,0 +1,71 @@ +/// + +// plaintext +// const jsx = ( +// <> +// +// +// ); +// const jsx2 = ( +// /* this is comment */Hello +// +// ); +// const jsx3 = ( +// <> +// +// ); + +// @FileName: /fragment.tsx +/////*a*/const j/*b*/sx =/*c*/ ( +//// /*5*//*1*/ +//// +//// /*6*//*4*/ +////)/*d*/; +////const jsx2 = ( +//// /* this is comment *//*13*//*8*/Hello/*9*/ +//// +////); +////const jsx3 = ( +//// <>/*7*/ +//// +////);/*e*/ + +const startPos1 = test.markerByName("0").position; +const endPos1 = test.markerByName("3").position; +const linkedCursors1 = { + ranges: [{ start: startPos1, length: 0 }, { start: endPos1, length: 0 }], + // wordPattern : undefined +}; + +const startPos2 = test.markerByName("10").position; +const endPos2 = test.markerByName("14").position; +const linkedCursors2 = { + ranges: [{ start: startPos2, length: 0 }, { start: endPos2, length: 0 }], +}; + +verify.linkedEditing({ + "0": linkedCursors1, + "1": undefined, + "2": undefined, + "3": linkedCursors1, + "4": undefined, + "5": undefined, + "6": undefined, + "7": undefined, + "8": undefined, + "9": undefined, + "10": linkedCursors2, + "11": undefined, + "12": undefined, + "13": undefined, + "14": linkedCursors2, + "15": undefined, + "16": undefined, + "17": undefined, + "18": undefined, + "a": undefined, + "b": undefined, + "c": undefined, + "d": undefined, + "e": undefined, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/linkedEditingJsxTag8.ts b/tests/cases/fourslash/linkedEditingJsxTag8.ts new file mode 100644 index 0000000000000..c65d547deedf6 --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag8.ts @@ -0,0 +1,13 @@ +/// + +// @FileName: /mismatchedNames.tsx +////const A = thing; +////const B = thing; +////const jsx = ( +//// +//// +////); + +verify.linkedEditing( { + "8": undefined, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/linkedEditingJsxTag9.ts b/tests/cases/fourslash/linkedEditingJsxTag9.ts new file mode 100644 index 0000000000000..67bd28d1fc5e7 --- /dev/null +++ b/tests/cases/fourslash/linkedEditingJsxTag9.ts @@ -0,0 +1,74 @@ +/// + +// the content of whitespace.tsx +//const whitespaceOpening = ( +// < div > +//
+//); +//const whitespaceClosing = ( +// +// +//); +//const triviaOpening = ( +// /* this is comment */Hello +// +//); + +// @Filename: /whitespace.tsx +////const whitespaceOpening = ( +//// /*4*/ +//// +////); +////const whitespaceClosing = ( +//// +//// /*13*/ +////); +////const triviaOpening = ( +//// /* this is/*14*/ comment *//*15*//*21*/Hello/*22*/ +//// +////); + +const markers = test.markers(); +const linkedCursors1 = { + ranges: [{ start: markers[1].position, length: 3 }, { start: markers[5].position, length: 3 }], +}; +const linkedCursors2 = { + ranges: [{ start: markers[7].position, length: 3 }, { start: markers[10].position, length: 3 }], +}; +const linkedCursors3 = { + ranges: [{ start: markers[20].position - 2, length: 3 }, { start: markers[28].position - 1, length: 3 }], +}; + +verify.linkedEditing( { + "0": undefined, + "1": linkedCursors1, + "2": linkedCursors1, + "3": undefined, + "4": undefined, + "5": linkedCursors1, + "6": linkedCursors1, + "7": linkedCursors2, + "8": linkedCursors2, + "9": undefined, + "10": linkedCursors2, + "11": linkedCursors2, + "12": undefined, + "13": undefined, + "14": undefined, + "15": undefined, + "16": undefined, + "17": undefined, + "18": undefined, + "19": undefined, + "20": linkedCursors3, + "21": undefined, + "22": undefined, + "23": undefined, + "24": undefined, + "25": undefined, + "26": undefined, + "27": undefined, + "28": linkedCursors3, + "29": undefined +}); +