Skip to content

Commit 0e65adb

Browse files
committed
Initial work on rename in markdown
For microsoft#146291 Also fixes references triggered on a definition link
1 parent 2431a29 commit 0e65adb

File tree

9 files changed

+408
-20
lines changed

9 files changed

+408
-20
lines changed

extensions/markdown-language-features/src/extension.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { registerDropIntoEditor } from './languageFeatures/dropIntoEditor';
1212
import { MdFoldingProvider } from './languageFeatures/foldingProvider';
1313
import { MdPathCompletionProvider } from './languageFeatures/pathCompletions';
1414
import { MdReferencesProvider } from './languageFeatures/references';
15+
import { MdRenameProvider } from './languageFeatures/rename';
1516
import { MdSmartSelect } from './languageFeatures/smartSelect';
1617
import { MdWorkspaceSymbolProvider } from './languageFeatures/workspaceSymbolProvider';
1718
import { Logger } from './logger';
@@ -61,13 +62,15 @@ function registerMarkdownLanguageFeatures(
6162
const linkProvider = new MdLinkProvider(engine);
6263
const workspaceContents = new VsCodeMdWorkspaceContents();
6364

65+
const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier);
6466
return vscode.Disposable.from(
6567
vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider),
6668
vscode.languages.registerDocumentLinkProvider(selector, linkProvider),
6769
vscode.languages.registerFoldingRangeProvider(selector, new MdFoldingProvider(engine)),
6870
vscode.languages.registerSelectionRangeProvider(selector, new MdSmartSelect(engine)),
6971
vscode.languages.registerWorkspaceSymbolProvider(new MdWorkspaceSymbolProvider(symbolProvider, workspaceContents)),
70-
vscode.languages.registerReferenceProvider(selector, new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier)),
72+
vscode.languages.registerReferenceProvider(selector, referencesProvider),
73+
vscode.languages.registerRenameProvider(selector, new MdRenameProvider(referencesProvider, githubSlugifier)),
7174
MdPathCompletionProvider.register(selector, engine, linkProvider),
7275
);
7376
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ function getWorkspaceFolder(document: SkinnyTextDocument) {
9595

9696
export interface LinkData {
9797
readonly target: LinkTarget;
98+
99+
readonly sourceText: string;
98100
readonly sourceResource: vscode.Uri;
99101
readonly sourceRange: vscode.Range;
100102
}
@@ -115,6 +117,7 @@ function extractDocumentLink(
115117
}
116118
return {
117119
target: linkTarget,
120+
sourceText: link,
118121
sourceResource: document.uri,
119122
sourceRange: new vscode.Range(linkStart, linkEnd)
120123
};
@@ -223,7 +226,12 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
223226
}
224227
}
225228
case 'definition':
226-
return this.toValidDocumentLink({ sourceRange: link.sourceRange, sourceResource: link.sourceResource, target: link.target.target }, definitionSet);
229+
return this.toValidDocumentLink({
230+
sourceText: link.sourceText,
231+
sourceRange: link.sourceRange,
232+
sourceResource: link.sourceResource,
233+
target: link.target.target
234+
}, definitionSet);
227235
}
228236
}
229237

@@ -274,6 +282,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
274282
}
275283

276284
yield {
285+
sourceText: reference,
277286
sourceRange: new vscode.Range(linkStart, linkEnd),
278287
sourceResource: document.uri,
279288
target: {
@@ -295,9 +304,11 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
295304
if (angleBracketLinkRe.test(link)) {
296305
const linkStart = document.positionAt(offset + 1);
297306
const linkEnd = document.positionAt(offset + link.length - 1);
298-
const target = parseLink(document, link.substring(1, link.length - 1));
307+
const text = link.substring(1, link.length - 1);
308+
const target = parseLink(document, text);
299309
if (target) {
300310
yield {
311+
sourceText: link,
301312
sourceResource: document.uri,
302313
sourceRange: new vscode.Range(linkStart, linkEnd),
303314
target: {
@@ -313,6 +324,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
313324
const target = parseLink(document, link);
314325
if (target) {
315326
yield {
327+
sourceText: link,
316328
sourceResource: document.uri,
317329
sourceRange: new vscode.Range(linkStart, linkEnd),
318330
target: {

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

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Slugifier } from '../slugify';
99
import { TableOfContents, TocEntry } from '../tableOfContents';
1010
import { Disposable } from '../util/dispose';
1111
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
12-
import { InternalLinkTarget, LinkData, LinkTarget, MdLinkProvider } from './documentLinkProvider';
12+
import { DefinitionLinkTarget, InternalLinkTarget, LinkData, LinkTarget, MdLinkProvider } from './documentLinkProvider';
1313
import { MdWorkspaceCache } from './workspaceCache';
1414

1515

@@ -20,10 +20,53 @@ function isLinkToHeader(target: LinkTarget, header: TocEntry, headerDocument: vs
2020
}
2121

2222

23-
export interface MdReference {
23+
/**
24+
* A link in a markdown file.
25+
*/
26+
interface MdLinkReference {
27+
readonly kind: 'link';
2428
readonly isTriggerLocation: boolean;
2529
readonly isDefinition: boolean;
2630
readonly location: vscode.Location;
31+
32+
readonly fragmentLocation: vscode.Location | undefined;
33+
}
34+
35+
/**
36+
* A header in a markdown file.
37+
*/
38+
interface MdHeaderReference {
39+
readonly kind: 'header';
40+
41+
readonly isTriggerLocation: boolean;
42+
readonly isDefinition: boolean;
43+
44+
/**
45+
* The range of the header.
46+
*
47+
* In `# a b c #` this would be the range of `# a b c #`
48+
*/
49+
readonly location: vscode.Location;
50+
51+
/**
52+
* The range of the header text itself.
53+
*
54+
* In `# a b c #` this would be the range of `a b c`
55+
*/
56+
readonly headerTextLocation: vscode.Location;
57+
}
58+
59+
export type MdReference = MdLinkReference | MdHeaderReference;
60+
61+
62+
function getFragmentLocation(link: LinkData): vscode.Location | undefined {
63+
const index = link.sourceText.indexOf('#');
64+
if (index < 0) {
65+
return undefined;
66+
}
67+
return new vscode.Location(link.sourceResource, link.sourceRange.with({
68+
start: link.sourceRange.start.translate({ characterDelta: index + 1 }),
69+
}));
2770
}
2871

2972
export class MdReferencesProvider extends Disposable implements vscode.ReferenceProvider {
@@ -70,23 +113,29 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
70113

71114
const line = document.lineAt(header.line);
72115
references.push({
116+
kind: 'header',
73117
isTriggerLocation: true,
74118
isDefinition: true,
75119
location: new vscode.Location(document.uri, new vscode.Range(header.line, 0, header.line, line.text.length)),
120+
headerTextLocation: header.headerTextLocation
76121
});
77122

78123
for (const link of links) {
79124
if (isLinkToHeader(link.target, header, document.uri, this.slugifier)) {
80125
references.push({
126+
kind: 'link',
81127
isTriggerLocation: false,
82128
isDefinition: false,
83-
location: new vscode.Location(link.sourceResource, link.sourceRange)
129+
location: new vscode.Location(link.sourceResource, link.sourceRange),
130+
fragmentLocation: getFragmentLocation(link),
84131
});
85132
} else if (link.target.kind === 'definition' && isLinkToHeader(link.target.target, header, document.uri, this.slugifier)) {
86133
references.push({
134+
kind: 'link',
87135
isTriggerLocation: false,
88136
isDefinition: false,
89-
location: new vscode.Location(link.sourceResource, link.sourceRange)
137+
location: new vscode.Location(link.sourceResource, link.sourceRange),
138+
fragmentLocation: getFragmentLocation(link),
90139
});
91140
}
92141
}
@@ -101,6 +150,10 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
101150
}
102151

103152
private async getReferencesToLink(sourceLink: LinkData): Promise<MdReference[]> {
153+
if (sourceLink.target.kind === 'definition') {
154+
return this.getReferencesToLink(this.getInnerLink(sourceLink, sourceLink.target));
155+
}
156+
104157
const allLinksInWorkspace = (await this._linkCache.getAll()).flat();
105158

106159
if (sourceLink.target.kind === 'reference') {
@@ -131,14 +184,20 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
131184
const entry = toc.lookup(sourceLink.target.fragment);
132185
if (entry) {
133186
references.push({
187+
kind: 'header',
134188
isTriggerLocation: false,
135189
isDefinition: true,
136190
location: entry.headerLocation,
191+
headerTextLocation: entry.headerTextLocation
137192
});
138193
}
139194
}
140195

141-
for (const link of allLinksInWorkspace) {
196+
for (let link of allLinksInWorkspace) {
197+
if (link.target.kind === 'definition') {
198+
link = this.getInnerLink(link, link.target);
199+
}
200+
142201
if (link.target.kind !== 'internal') {
143202
continue;
144203
}
@@ -155,19 +214,23 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
155214
if (sourceLink.target.fragment) {
156215
if (this.slugifier.fromHeading(link.target.fragment).equals(this.slugifier.fromHeading(sourceLink.target.fragment))) {
157216
references.push({
217+
kind: 'link',
158218
isTriggerLocation,
159219
isDefinition: false,
160220
location: new vscode.Location(link.sourceResource, link.sourceRange),
221+
fragmentLocation: getFragmentLocation(link),
161222
});
162223
}
163224
} else { // Triggered on a link without a fragment so we only require matching the file and ignore fragments
164225

165226
// But exclude cases where the file is referencing itself
166227
if (link.sourceResource.fsPath !== targetDoc.uri.fsPath) {
167228
references.push({
229+
kind: 'link',
168230
isTriggerLocation,
169231
isDefinition: false,
170232
location: new vscode.Location(link.sourceResource, link.sourceRange),
233+
fragmentLocation: getFragmentLocation(link),
171234
});
172235
}
173236
}
@@ -176,6 +239,15 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
176239
return references;
177240
}
178241

242+
private getInnerLink(sourceLink: LinkData, target: DefinitionLinkTarget): LinkData {
243+
return {
244+
sourceText: sourceLink.sourceText, // This is not correct
245+
sourceResource: sourceLink.sourceResource,
246+
sourceRange: sourceLink.sourceRange,
247+
target: target.target,
248+
};
249+
}
250+
179251
private * getReferencesToReferenceLink(allLinks: Iterable<LinkData>, sourceLink: LinkData): Iterable<MdReference> {
180252
if (sourceLink.target.kind !== 'reference') {
181253
return;
@@ -186,9 +258,11 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
186258
if (link.target.ref === sourceLink.target.ref && link.sourceResource.fsPath === sourceLink.sourceResource.fsPath) {
187259
const isTriggerLocation = sourceLink.sourceResource.fsPath === link.sourceResource.fsPath && sourceLink.sourceRange.isEqual(link.sourceRange);
188260
yield {
261+
kind: 'link',
189262
isTriggerLocation,
190-
isDefinition: false,
191-
location: new vscode.Location(sourceLink.sourceResource, link.sourceRange)
263+
isDefinition: link.target.kind === 'definition',
264+
location: new vscode.Location(sourceLink.sourceResource, link.sourceRange),
265+
fragmentLocation: getFragmentLocation(link),
192266
};
193267
}
194268
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import * as vscode from 'vscode';
6+
import * as nls from 'vscode-nls';
7+
import { Slugifier } from '../slugify';
8+
import { Disposable } from '../util/dispose';
9+
import { SkinnyTextDocument } from '../workspaceContents';
10+
import { MdReference, MdReferencesProvider } from './references';
11+
12+
const localize = nls.loadMessageBundle();
13+
14+
15+
export class MdRenameProvider extends Disposable implements vscode.RenameProvider {
16+
17+
private cachedRefs?: {
18+
readonly resource: vscode.Uri;
19+
readonly version: number;
20+
readonly position: vscode.Position;
21+
readonly references: MdReference[];
22+
} | undefined;
23+
24+
public constructor(
25+
private readonly referencesProvider: MdReferencesProvider,
26+
private readonly slugifier: Slugifier,
27+
) {
28+
super();
29+
}
30+
31+
public async prepareRename(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<undefined | vscode.Range> {
32+
const references = await this.referencesProvider.getAllReferences(document, position, token);
33+
if (token.isCancellationRequested) {
34+
return undefined;
35+
}
36+
37+
if (!references?.length) {
38+
throw new Error(localize('invalidRenameLocation', "Rename not supported at location"));
39+
}
40+
41+
const triggerRef = references.find(ref => ref.isTriggerLocation);
42+
if (!triggerRef) {
43+
return undefined;
44+
}
45+
46+
if (triggerRef.kind === 'header') {
47+
return triggerRef.headerTextLocation.range;
48+
} else {
49+
return triggerRef.fragmentLocation?.range ?? triggerRef.location.range;
50+
}
51+
}
52+
53+
public async provideRenameEdits(document: SkinnyTextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise<vscode.WorkspaceEdit | undefined> {
54+
const references = await this.getAllReferences(document, position, token);
55+
if (token.isCancellationRequested || !references?.length) {
56+
return undefined;
57+
}
58+
59+
const edit = new vscode.WorkspaceEdit();
60+
61+
const slug = this.slugifier.fromHeading(newName);
62+
63+
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);
68+
}
69+
}
70+
71+
return edit;
72+
}
73+
74+
private async getAllReferences(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken) {
75+
const version = document.version;
76+
77+
if (this.cachedRefs
78+
&& this.cachedRefs.resource.fsPath === document.uri.fsPath
79+
&& this.cachedRefs.version === document.version
80+
&& this.cachedRefs.position.isEqual(position)
81+
) {
82+
return this.cachedRefs.references;
83+
}
84+
85+
const references = await this.referencesProvider.getAllReferences(document, position, token);
86+
this.cachedRefs = {
87+
resource: document.uri,
88+
version,
89+
position,
90+
references
91+
};
92+
return references;
93+
}
94+
}

0 commit comments

Comments
 (0)