Initial work on rename in markdown

For #146291

Also fixes references triggered on a definition link
This commit is contained in:
Matt Bierner
2022-03-31 14:49:50 -07:00
parent 2431a2940a
commit 0e65adbda8
9 changed files with 408 additions and 20 deletions

View File

@@ -95,6 +95,8 @@ function getWorkspaceFolder(document: SkinnyTextDocument) {
export interface LinkData {
readonly target: LinkTarget;
readonly sourceText: string;
readonly sourceResource: vscode.Uri;
readonly sourceRange: vscode.Range;
}
@@ -115,6 +117,7 @@ function extractDocumentLink(
}
return {
target: linkTarget,
sourceText: link,
sourceResource: document.uri,
sourceRange: new vscode.Range(linkStart, linkEnd)
};
@@ -223,7 +226,12 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
}
}
case 'definition':
return this.toValidDocumentLink({ sourceRange: link.sourceRange, sourceResource: link.sourceResource, target: link.target.target }, definitionSet);
return this.toValidDocumentLink({
sourceText: link.sourceText,
sourceRange: link.sourceRange,
sourceResource: link.sourceResource,
target: link.target.target
}, definitionSet);
}
}
@@ -274,6 +282,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
}
yield {
sourceText: reference,
sourceRange: new vscode.Range(linkStart, linkEnd),
sourceResource: document.uri,
target: {
@@ -295,9 +304,11 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
if (angleBracketLinkRe.test(link)) {
const linkStart = document.positionAt(offset + 1);
const linkEnd = document.positionAt(offset + link.length - 1);
const target = parseLink(document, link.substring(1, link.length - 1));
const text = link.substring(1, link.length - 1);
const target = parseLink(document, text);
if (target) {
yield {
sourceText: link,
sourceResource: document.uri,
sourceRange: new vscode.Range(linkStart, linkEnd),
target: {
@@ -313,6 +324,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
const target = parseLink(document, link);
if (target) {
yield {
sourceText: link,
sourceResource: document.uri,
sourceRange: new vscode.Range(linkStart, linkEnd),
target: {

View File

@@ -9,7 +9,7 @@ import { Slugifier } from '../slugify';
import { TableOfContents, TocEntry } from '../tableOfContents';
import { Disposable } from '../util/dispose';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalLinkTarget, LinkData, LinkTarget, MdLinkProvider } from './documentLinkProvider';
import { DefinitionLinkTarget, InternalLinkTarget, LinkData, LinkTarget, MdLinkProvider } from './documentLinkProvider';
import { MdWorkspaceCache } from './workspaceCache';
@@ -20,10 +20,53 @@ function isLinkToHeader(target: LinkTarget, header: TocEntry, headerDocument: vs
}
export interface MdReference {
/**
* A link in a markdown file.
*/
interface MdLinkReference {
readonly kind: 'link';
readonly isTriggerLocation: boolean;
readonly isDefinition: boolean;
readonly location: vscode.Location;
readonly fragmentLocation: vscode.Location | undefined;
}
/**
* A header in a markdown file.
*/
interface MdHeaderReference {
readonly kind: 'header';
readonly isTriggerLocation: boolean;
readonly isDefinition: boolean;
/**
* The range of the header.
*
* In `# a b c #` this would be the range of `# a b c #`
*/
readonly location: vscode.Location;
/**
* The range of the header text itself.
*
* In `# a b c #` this would be the range of `a b c`
*/
readonly headerTextLocation: vscode.Location;
}
export type MdReference = MdLinkReference | MdHeaderReference;
function getFragmentLocation(link: LinkData): vscode.Location | undefined {
const index = link.sourceText.indexOf('#');
if (index < 0) {
return undefined;
}
return new vscode.Location(link.sourceResource, link.sourceRange.with({
start: link.sourceRange.start.translate({ characterDelta: index + 1 }),
}));
}
export class MdReferencesProvider extends Disposable implements vscode.ReferenceProvider {
@@ -70,23 +113,29 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
const line = document.lineAt(header.line);
references.push({
kind: 'header',
isTriggerLocation: true,
isDefinition: true,
location: new vscode.Location(document.uri, new vscode.Range(header.line, 0, header.line, line.text.length)),
headerTextLocation: header.headerTextLocation
});
for (const link of links) {
if (isLinkToHeader(link.target, header, document.uri, this.slugifier)) {
references.push({
kind: 'link',
isTriggerLocation: false,
isDefinition: false,
location: new vscode.Location(link.sourceResource, link.sourceRange)
location: new vscode.Location(link.sourceResource, link.sourceRange),
fragmentLocation: getFragmentLocation(link),
});
} else if (link.target.kind === 'definition' && isLinkToHeader(link.target.target, header, document.uri, this.slugifier)) {
references.push({
kind: 'link',
isTriggerLocation: false,
isDefinition: false,
location: new vscode.Location(link.sourceResource, link.sourceRange)
location: new vscode.Location(link.sourceResource, link.sourceRange),
fragmentLocation: getFragmentLocation(link),
});
}
}
@@ -101,6 +150,10 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
}
private async getReferencesToLink(sourceLink: LinkData): Promise<MdReference[]> {
if (sourceLink.target.kind === 'definition') {
return this.getReferencesToLink(this.getInnerLink(sourceLink, sourceLink.target));
}
const allLinksInWorkspace = (await this._linkCache.getAll()).flat();
if (sourceLink.target.kind === 'reference') {
@@ -131,14 +184,20 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
const entry = toc.lookup(sourceLink.target.fragment);
if (entry) {
references.push({
kind: 'header',
isTriggerLocation: false,
isDefinition: true,
location: entry.headerLocation,
headerTextLocation: entry.headerTextLocation
});
}
}
for (const link of allLinksInWorkspace) {
for (let link of allLinksInWorkspace) {
if (link.target.kind === 'definition') {
link = this.getInnerLink(link, link.target);
}
if (link.target.kind !== 'internal') {
continue;
}
@@ -155,9 +214,11 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
if (sourceLink.target.fragment) {
if (this.slugifier.fromHeading(link.target.fragment).equals(this.slugifier.fromHeading(sourceLink.target.fragment))) {
references.push({
kind: 'link',
isTriggerLocation,
isDefinition: false,
location: new vscode.Location(link.sourceResource, link.sourceRange),
fragmentLocation: getFragmentLocation(link),
});
}
} else { // Triggered on a link without a fragment so we only require matching the file and ignore fragments
@@ -165,9 +226,11 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
// But exclude cases where the file is referencing itself
if (link.sourceResource.fsPath !== targetDoc.uri.fsPath) {
references.push({
kind: 'link',
isTriggerLocation,
isDefinition: false,
location: new vscode.Location(link.sourceResource, link.sourceRange),
fragmentLocation: getFragmentLocation(link),
});
}
}
@@ -176,6 +239,15 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
return references;
}
private getInnerLink(sourceLink: LinkData, target: DefinitionLinkTarget): LinkData {
return {
sourceText: sourceLink.sourceText, // This is not correct
sourceResource: sourceLink.sourceResource,
sourceRange: sourceLink.sourceRange,
target: target.target,
};
}
private * getReferencesToReferenceLink(allLinks: Iterable<LinkData>, sourceLink: LinkData): Iterable<MdReference> {
if (sourceLink.target.kind !== 'reference') {
return;
@@ -186,9 +258,11 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
if (link.target.ref === sourceLink.target.ref && link.sourceResource.fsPath === sourceLink.sourceResource.fsPath) {
const isTriggerLocation = sourceLink.sourceResource.fsPath === link.sourceResource.fsPath && sourceLink.sourceRange.isEqual(link.sourceRange);
yield {
kind: 'link',
isTriggerLocation,
isDefinition: false,
location: new vscode.Location(sourceLink.sourceResource, link.sourceRange)
isDefinition: link.target.kind === 'definition',
location: new vscode.Location(sourceLink.sourceResource, link.sourceRange),
fragmentLocation: getFragmentLocation(link),
};
}
}

View File

@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { Slugifier } from '../slugify';
import { Disposable } from '../util/dispose';
import { SkinnyTextDocument } from '../workspaceContents';
import { MdReference, MdReferencesProvider } from './references';
const localize = nls.loadMessageBundle();
export class MdRenameProvider extends Disposable implements vscode.RenameProvider {
private cachedRefs?: {
readonly resource: vscode.Uri;
readonly version: number;
readonly position: vscode.Position;
readonly references: MdReference[];
} | undefined;
public constructor(
private readonly referencesProvider: MdReferencesProvider,
private readonly slugifier: Slugifier,
) {
super();
}
public async prepareRename(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<undefined | vscode.Range> {
const references = await this.referencesProvider.getAllReferences(document, position, token);
if (token.isCancellationRequested) {
return undefined;
}
if (!references?.length) {
throw new Error(localize('invalidRenameLocation', "Rename not supported at location"));
}
const triggerRef = references.find(ref => ref.isTriggerLocation);
if (!triggerRef) {
return undefined;
}
if (triggerRef.kind === 'header') {
return triggerRef.headerTextLocation.range;
} else {
return triggerRef.fragmentLocation?.range ?? triggerRef.location.range;
}
}
public async provideRenameEdits(document: SkinnyTextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise<vscode.WorkspaceEdit | undefined> {
const references = await this.getAllReferences(document, position, token);
if (token.isCancellationRequested || !references?.length) {
return undefined;
}
const edit = new vscode.WorkspaceEdit();
const slug = this.slugifier.fromHeading(newName);
for (const ref of references) {
if (ref.kind === 'header') {
edit.replace(ref.location.uri, ref.headerTextLocation.range, newName);
} else {
edit.replace(ref.location.uri, ref.fragmentLocation?.range ?? ref.location.range, slug.value);
}
}
return edit;
}
private async getAllReferences(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken) {
const version = document.version;
if (this.cachedRefs
&& this.cachedRefs.resource.fsPath === document.uri.fsPath
&& this.cachedRefs.version === document.version
&& this.cachedRefs.position.isEqual(position)
) {
return this.cachedRefs.references;
}
const references = await this.referencesProvider.getAllReferences(document, position, token);
this.cachedRefs = {
resource: document.uri,
version,
position,
references
};
return references;
}
}