Skip to content

Commit c39d09a

Browse files
committed
Working on initial support for renaming refs in md
For microsoft#146291
1 parent 0d1530c commit c39d09a

File tree

5 files changed

+150
-41
lines changed

5 files changed

+150
-41
lines changed

extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,17 @@ export interface MdInlineLink {
9494

9595
readonly sourceText: string;
9696
readonly sourceResource: vscode.Uri;
97-
readonly sourceRange: vscode.Range;
97+
readonly sourceHrefRange: vscode.Range;
9898
}
9999

100100
export interface MdLinkDefinition {
101101
readonly kind: 'definition';
102102

103103
readonly sourceText: string;
104104
readonly sourceResource: vscode.Uri;
105-
readonly sourceRange: vscode.Range;
105+
readonly sourceHrefRange: vscode.Range;
106+
107+
readonly refRange: vscode.Range;
106108

107109
readonly ref: string;
108110
readonly href: ExternalHref | InternalHref;
@@ -129,7 +131,7 @@ function extractDocumentLink(
129131
href: linkTarget,
130132
sourceText: link,
131133
sourceResource: document.uri,
132-
sourceRange: new vscode.Range(linkStart, linkEnd)
134+
sourceHrefRange: new vscode.Range(linkStart, linkEnd)
133135
};
134136
} catch {
135137
return undefined;
@@ -190,8 +192,8 @@ async function findCode(document: SkinnyTextDocument, engine: MarkdownEngine): P
190192
}
191193

192194
function isLinkInsideCode(code: CodeInDocument, link: MdLink) {
193-
return code.multiline.some(interval => link.sourceRange.start.line >= interval[0] && link.sourceRange.start.line < interval[1]) ||
194-
code.inline.some(position => position.intersection(link.sourceRange));
195+
return code.multiline.some(interval => link.sourceHrefRange.start.line >= interval[0] && link.sourceHrefRange.start.line < interval[1]) ||
196+
code.inline.some(position => position.intersection(link.sourceHrefRange));
195197
}
196198

197199
export class MdLinkProvider implements vscode.DocumentLinkProvider {
@@ -217,20 +219,20 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
217219
private toValidDocumentLink(link: MdLink, definitionSet: LinkDefinitionSet): vscode.DocumentLink | undefined {
218220
switch (link.href.kind) {
219221
case 'external': {
220-
return new vscode.DocumentLink(link.sourceRange, link.href.uri);
222+
return new vscode.DocumentLink(link.sourceHrefRange, link.href.uri);
221223
}
222224
case 'internal': {
223225
const uri = OpenDocumentLinkCommand.createCommandUri(link.sourceResource, link.href.path, link.href.fragment);
224-
const documentLink = new vscode.DocumentLink(link.sourceRange, uri);
226+
const documentLink = new vscode.DocumentLink(link.sourceHrefRange, uri);
225227
documentLink.tooltip = localize('documentLink.tooltip', 'Follow link');
226228
return documentLink;
227229
}
228230
case 'reference': {
229231
const def = definitionSet.lookup(link.href.ref);
230232
if (def) {
231233
return new vscode.DocumentLink(
232-
link.sourceRange,
233-
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([def.sourceRange.start.line, def.sourceRange.start.character]))}`));
234+
link.sourceHrefRange,
235+
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([def.sourceHrefRange.start.line, def.sourceHrefRange.start.character]))}`));
234236
} else {
235237
return undefined;
236238
}
@@ -287,7 +289,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
287289
yield {
288290
kind: 'link',
289291
sourceText: reference,
290-
sourceRange: new vscode.Range(linkStart, linkEnd),
292+
sourceHrefRange: new vscode.Range(linkStart, linkEnd),
291293
sourceResource: document.uri,
292294
href: {
293295
kind: 'reference',
@@ -305,6 +307,9 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
305307
const link = match[3].trim();
306308
const offset = (match.index || 0) + pre.length;
307309

310+
const refStart = document.positionAt((match.index ?? 0) + 1);
311+
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
312+
308313
if (angleBracketLinkRe.test(link)) {
309314
const linkStart = document.positionAt(offset + 1);
310315
const linkEnd = document.positionAt(offset + link.length - 1);
@@ -315,7 +320,8 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
315320
kind: 'definition',
316321
sourceText: link,
317322
sourceResource: document.uri,
318-
sourceRange: new vscode.Range(linkStart, linkEnd),
323+
sourceHrefRange: new vscode.Range(linkStart, linkEnd),
324+
refRange,
319325
ref: reference,
320326
href: target,
321327
};
@@ -329,7 +335,8 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
329335
kind: 'definition',
330336
sourceText: link,
331337
sourceResource: document.uri,
332-
sourceRange: new vscode.Range(linkStart, linkEnd),
338+
sourceHrefRange: new vscode.Range(linkStart, linkEnd),
339+
refRange,
333340
ref: reference,
334341
href: target,
335342
};

extensions/markdown-language-features/src/languageFeatures/references.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ interface MdLinkReference {
2222
readonly isDefinition: boolean;
2323
readonly location: vscode.Location;
2424

25+
readonly link: MdLink;
26+
2527
readonly fragmentLocation: vscode.Location | undefined;
2628
}
2729

@@ -57,8 +59,8 @@ function getFragmentLocation(link: MdLink): vscode.Location | undefined {
5759
if (index < 0) {
5860
return undefined;
5961
}
60-
return new vscode.Location(link.sourceResource, link.sourceRange.with({
61-
start: link.sourceRange.start.translate({ characterDelta: index + 1 }),
62+
return new vscode.Location(link.sourceResource, link.sourceHrefRange.with({
63+
start: link.sourceHrefRange.start.translate({ characterDelta: index + 1 }),
6264
}));
6365
}
6466

@@ -122,7 +124,8 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
122124
kind: 'link',
123125
isTriggerLocation: false,
124126
isDefinition: false,
125-
location: new vscode.Location(link.sourceResource, link.sourceRange),
127+
link,
128+
location: new vscode.Location(link.sourceResource, link.sourceHrefRange),
126129
fragmentLocation: getFragmentLocation(link),
127130
});
128131
}
@@ -133,15 +136,30 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
133136

134137
private async getReferencesToLinkAtPosition(document: SkinnyTextDocument, position: vscode.Position): Promise<MdReference[]> {
135138
const docLinks = await this.linkProvider.getAllLinks(document);
136-
const sourceLink = docLinks.find(link => link.sourceRange.contains(position));
137-
return sourceLink ? this.getReferencesToLink(sourceLink) : [];
139+
140+
for (const link of docLinks) {
141+
if (link.kind === 'definition') {
142+
// We could be in either the ref name or the definition
143+
if (link.refRange.contains(position)) {
144+
return Array.from(this.getReferencesToLinkReference(docLinks, link.ref, { resource: document.uri, range: link.refRange }));
145+
} else if (link.sourceHrefRange.contains(position)) {
146+
return this.getReferencesToLink(link);
147+
}
148+
} else {
149+
if (link.sourceHrefRange.contains(position)) {
150+
return this.getReferencesToLink(link);
151+
}
152+
}
153+
}
154+
155+
return [];
138156
}
139157

140158
private async getReferencesToLink(sourceLink: MdLink): Promise<MdReference[]> {
141159
const allLinksInWorkspace = (await this._linkCache.getAll()).flat();
142160

143161
if (sourceLink.href.kind === 'reference') {
144-
return Array.from(this.getReferencesToReferenceLink(allLinksInWorkspace, sourceLink));
162+
return Array.from(this.getReferencesToLinkReference(allLinksInWorkspace, sourceLink.href.ref, { resource: sourceLink.sourceResource, range: sourceLink.sourceHrefRange }));
145163
}
146164

147165
if (sourceLink.href.kind !== 'internal') {
@@ -186,15 +204,16 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
186204
continue;
187205
}
188206

189-
const isTriggerLocation = sourceLink.sourceResource.fsPath === link.sourceResource.fsPath && sourceLink.sourceRange.isEqual(link.sourceRange);
207+
const isTriggerLocation = sourceLink.sourceResource.fsPath === link.sourceResource.fsPath && sourceLink.sourceHrefRange.isEqual(link.sourceHrefRange);
190208

191209
if (sourceLink.href.fragment) {
192210
if (this.slugifier.fromHeading(link.href.fragment).equals(this.slugifier.fromHeading(sourceLink.href.fragment))) {
193211
references.push({
194212
kind: 'link',
195213
isTriggerLocation,
196214
isDefinition: false,
197-
location: new vscode.Location(link.sourceResource, link.sourceRange),
215+
link,
216+
location: new vscode.Location(link.sourceResource, link.sourceHrefRange),
198217
fragmentLocation: getFragmentLocation(link),
199218
});
200219
}
@@ -206,7 +225,8 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
206225
kind: 'link',
207226
isTriggerLocation,
208227
isDefinition: false,
209-
location: new vscode.Location(link.sourceResource, link.sourceRange),
228+
link,
229+
location: new vscode.Location(link.sourceResource, link.sourceHrefRange),
210230
fragmentLocation: getFragmentLocation(link),
211231
});
212232
}
@@ -221,11 +241,7 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
221241
|| uri.Utils.extname(href.path) === '' && href.path.with({ path: href.path.path + '.md' }).fsPath === targetDoc.uri.fsPath;
222242
}
223243

224-
private * getReferencesToReferenceLink(allLinks: Iterable<MdLink>, sourceLink: MdLink): Iterable<MdReference> {
225-
if (sourceLink.href.kind !== 'reference') {
226-
return;
227-
}
228-
244+
private *getReferencesToLinkReference(allLinks: Iterable<MdLink>, refToFind: string, from: { resource: vscode.Uri; range: vscode.Range }): Iterable<MdReference> {
229245
for (const link of allLinks) {
230246
let ref: string;
231247
if (link.kind === 'definition') {
@@ -236,13 +252,15 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
236252
continue;
237253
}
238254

239-
if (ref === sourceLink.href.ref && link.sourceResource.fsPath === sourceLink.sourceResource.fsPath) {
240-
const isTriggerLocation = sourceLink.sourceResource.fsPath === link.sourceResource.fsPath && sourceLink.sourceRange.isEqual(link.sourceRange);
255+
if (ref === refToFind && link.sourceResource.fsPath === from.resource.fsPath) {
256+
const isTriggerLocation = from.resource.fsPath === link.sourceResource.fsPath && (
257+
(link.href.kind === 'reference' && from.range.isEqual(link.sourceHrefRange)) || (link.kind === 'definition' && from.range.isEqual(link.refRange)));
241258
yield {
242259
kind: 'link',
243260
isTriggerLocation,
244261
isDefinition: link.kind === 'definition',
245-
location: new vscode.Location(sourceLink.sourceResource, link.sourceRange),
262+
link,
263+
location: new vscode.Location(from.resource, link.sourceHrefRange),
246264
fragmentLocation: getFragmentLocation(link),
247265
};
248266
}

extensions/markdown-language-features/src/languageFeatures/rename.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,21 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide
4343
return undefined;
4444
}
4545

46-
if (triggerRef.kind === 'header') {
47-
return triggerRef.headerTextLocation.range;
48-
} else {
49-
return triggerRef.fragmentLocation?.range ?? triggerRef.location.range;
46+
switch (triggerRef.kind) {
47+
case 'header':
48+
return triggerRef.headerTextLocation.range;
49+
50+
case 'link':
51+
if (triggerRef.link.kind === 'definition') {
52+
// We may have been triggered on the ref or the definition itself
53+
if (triggerRef.link.refRange.contains(position)) {
54+
return triggerRef.link.refRange;
55+
} else {
56+
return triggerRef.link.sourceHrefRange;
57+
}
58+
} else {
59+
return triggerRef.fragmentLocation?.range ?? triggerRef.location.range;
60+
}
5061
}
5162
}
5263

@@ -56,15 +67,35 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide
5667
return undefined;
5768
}
5869

59-
const edit = new vscode.WorkspaceEdit();
70+
const triggerRef = references.find(ref => ref.isTriggerLocation);
71+
if (!triggerRef) {
72+
return undefined;
73+
}
6074

61-
const slug = this.slugifier.fromHeading(newName);
75+
const isRefRename = triggerRef.kind === 'link' && (
76+
(triggerRef.link.kind === 'definition' && triggerRef.link.refRange.contains(position)) || triggerRef.link.href.kind === 'reference'
77+
);
78+
const slug = this.slugifier.fromHeading(newName).value;
6279

80+
const edit = new vscode.WorkspaceEdit();
6381
for (const ref of references) {
64-
if (ref.kind === 'header') {
65-
edit.replace(ref.location.uri, ref.headerTextLocation.range, newName);
66-
} else {
67-
edit.replace(ref.location.uri, ref.fragmentLocation?.range ?? ref.location.range, slug.value);
82+
switch (ref.kind) {
83+
case 'header':
84+
edit.replace(ref.location.uri, ref.headerTextLocation.range, newName);
85+
break;
86+
87+
case 'link':
88+
if (ref.link.kind === 'definition') {
89+
// We may be renaming either the reference or the definition itself
90+
if (isRefRename) {
91+
edit.replace(ref.link.sourceResource, ref.link.refRange, newName);
92+
} else {
93+
edit.replace(ref.link.sourceResource, ref.fragmentLocation?.range ?? ref.link.sourceHrefRange, ref.fragmentLocation ? slug : newName);
94+
}
95+
} else {
96+
edit.replace(ref.location.uri, ref.fragmentLocation?.range ?? ref.location.range, ref.link.href.kind === 'reference' ? newName : slug);
97+
}
98+
break;
6899
}
69100
}
70101

extensions/markdown-language-features/src/test/references.test.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,10 @@ suite('markdown: find all references', () => {
319319
});
320320

321321
suite('Reference links', () => {
322-
test('Should find reference links within file', async () => {
322+
test('Should find reference links within file from link', async () => {
323323
const docUri = workspacePath('doc.md');
324324
const doc = new InMemoryDocument(docUri, joinLines(
325-
`[link 1][abc]`,
325+
`[link 1][abc]`, // trigger here
326326
``,
327327
`[abc]: https://example.com`,
328328
));
@@ -334,6 +334,21 @@ suite('markdown: find all references', () => {
334334
);
335335
});
336336

337+
test('Should find reference links within file from definition', async () => {
338+
const docUri = workspacePath('doc.md');
339+
const doc = new InMemoryDocument(docUri, joinLines(
340+
`[link 1][abc]`,
341+
``,
342+
`[abc]: https://example.com`, // trigger here
343+
));
344+
345+
const refs = await getReferences(doc, new vscode.Position(2, 3), new InMemoryWorkspaceMarkdownDocuments([doc]));
346+
assertReferencesEqual(refs!,
347+
{ uri: docUri, line: 0 },
348+
{ uri: docUri, line: 2 },
349+
);
350+
});
351+
337352
test('Should not find reference links across files', async () => {
338353
const docUri = workspacePath('doc.md');
339354
const doc = new InMemoryDocument(docUri, joinLines(

extensions/markdown-language-features/src/test/rename.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,42 @@ suite('markdown: rename', () => {
207207
]
208208
});
209209
});
210+
211+
test('Rename on ref should rename refs and def', async () => {
212+
const uri = workspacePath('doc.md');
213+
const doc = new InMemoryDocument(uri, joinLines(
214+
`[text][ref]`, // rename here
215+
`[other][ref]`,
216+
``,
217+
`[ref]: https://example.com`,
218+
));
219+
220+
const edit = await getRenameEdits(doc, new vscode.Position(0, 8), "new ref", new InMemoryWorkspaceMarkdownDocuments([doc]));
221+
assertEditsEqual(edit!, {
222+
uri, edits: [
223+
new vscode.TextEdit(new vscode.Range(0, 7, 0, 10), 'new ref'),
224+
new vscode.TextEdit(new vscode.Range(1, 8, 1, 11), 'new ref'),
225+
new vscode.TextEdit(new vscode.Range(3, 1, 3, 4), 'new ref'),
226+
]
227+
});
228+
});
229+
230+
test('Rename on def should rename refs and def', async () => {
231+
const uri = workspacePath('doc.md');
232+
const doc = new InMemoryDocument(uri, joinLines(
233+
`[text][ref]`,
234+
`[other][ref]`,
235+
``,
236+
`[ref]: https://example.com`, // rename here
237+
));
238+
239+
const edit = await getRenameEdits(doc, new vscode.Position(3, 3), "new ref", new InMemoryWorkspaceMarkdownDocuments([doc]));
240+
assertEditsEqual(edit!, {
241+
uri, edits: [
242+
new vscode.TextEdit(new vscode.Range(0, 7, 0, 10), 'new ref'),
243+
new vscode.TextEdit(new vscode.Range(1, 8, 1, 11), 'new ref'),
244+
new vscode.TextEdit(new vscode.Range(3, 1, 3, 4), 'new ref'),
245+
]
246+
});
247+
});
210248
});

0 commit comments

Comments
 (0)