Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/microsoft/vscode.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/markdown-language-features/src/languageFeatures/rename.ts')
-rw-r--r--extensions/markdown-language-features/src/languageFeatures/rename.ts243
1 files changed, 192 insertions, 51 deletions
diff --git a/extensions/markdown-language-features/src/languageFeatures/rename.ts b/extensions/markdown-language-features/src/languageFeatures/rename.ts
index ab11f2b210d..db06b7eeb9c 100644
--- a/extensions/markdown-language-features/src/languageFeatures/rename.ts
+++ b/extensions/markdown-language-features/src/languageFeatures/rename.ts
@@ -2,113 +2,247 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
+import * as path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
+import * as URI from 'vscode-uri';
import { Slugifier } from '../slugify';
import { Disposable } from '../util/dispose';
-import { SkinnyTextDocument } from '../workspaceContents';
-import { MdReference, MdReferencesProvider } from './references';
+import { resolveDocumentLink } from '../util/openDocumentLink';
+import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
+import { InternalHref } from './documentLinkProvider';
+import { MdHeaderReference, MdLinkReference, MdReference, MdReferencesProvider, tryFindMdDocumentForLink } from './references';
const localize = nls.loadMessageBundle();
+export interface MdReferencesResponse {
+ references: MdReference[];
+ triggerRef: MdReference;
+}
+
+interface MdFileRenameEdit {
+ readonly from: vscode.Uri;
+ readonly to: vscode.Uri;
+}
+
+/**
+ * Type with additional metadata about the edits for testing
+ *
+ * This is needed since `vscode.WorkspaceEdit` does not expose info on file renames.
+ */
+export interface MdWorkspaceEdit {
+ readonly edit: vscode.WorkspaceEdit;
+
+ readonly fileRenames?: ReadonlyArray<MdFileRenameEdit>;
+}
+
+function tryDecodeUri(str: string): string {
+ try {
+ return decodeURI(str);
+ } catch {
+ return str;
+ }
+}
+
export class MdRenameProvider extends Disposable implements vscode.RenameProvider {
private cachedRefs?: {
readonly resource: vscode.Uri;
readonly version: number;
readonly position: vscode.Position;
+ readonly triggerRef: MdReference;
readonly references: MdReference[];
} | undefined;
+ private readonly renameNotSupportedText = localize('invalidRenameLocation', "Rename not supported at location");
+
public constructor(
private readonly referencesProvider: MdReferencesProvider,
+ private readonly workspaceContents: MdWorkspaceContents,
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);
+ public async prepareRename(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<undefined | { readonly range: vscode.Range; readonly placeholder: string }> {
+ const allRefsInfo = await this.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 (!allRefsInfo || !allRefsInfo.references.length) {
+ throw new Error(this.renameNotSupportedText);
}
+ const triggerRef = allRefsInfo.triggerRef;
switch (triggerRef.kind) {
- case 'header':
- return triggerRef.headerTextLocation.range;
-
- case 'link':
+ case 'header': {
+ return { range: triggerRef.headerTextLocation.range, placeholder: triggerRef.headerText };
+ }
+ case 'link': {
if (triggerRef.link.kind === 'definition') {
// We may have been triggered on the ref or the definition itself
- if (triggerRef.link.refRange.contains(position)) {
- return triggerRef.link.refRange;
- } else {
- if (triggerRef.fragmentLocation) {
- return triggerRef.fragmentLocation.range;
- }
- throw new Error(localize('renameNoFiles', "Renaming files is currently not supported"));
+ if (triggerRef.link.ref.range.contains(position)) {
+ return { range: triggerRef.link.ref.range, placeholder: triggerRef.link.ref.text };
}
- } else {
- if (triggerRef.fragmentLocation) {
- return triggerRef.fragmentLocation.range;
+ }
+
+ if (triggerRef.link.href.kind === 'external') {
+ return { range: triggerRef.link.source.hrefRange, placeholder: document.getText(triggerRef.link.source.hrefRange) };
+ }
+
+ // See if we are renaming the fragment or the path
+ const { fragmentRange } = triggerRef.link.source;
+ if (fragmentRange?.contains(position)) {
+ const declaration = this.findHeaderDeclaration(allRefsInfo.references);
+ if (declaration) {
+ return { range: fragmentRange, placeholder: declaration.headerText };
}
- throw new Error(localize('renameNoFiles', "Renaming files is currently not supported"));
+ return { range: fragmentRange, placeholder: document.getText(fragmentRange) };
}
+
+ const range = this.getFilePathRange(triggerRef);
+ if (!range) {
+ throw new Error(this.renameNotSupportedText);
+ }
+ return { range, placeholder: tryDecodeUri(document.getText(range)) };
+ }
+ }
+ }
+
+ private getFilePathRange(ref: MdLinkReference): vscode.Range {
+ if (ref.link.source.fragmentRange) {
+ return ref.link.source.hrefRange.with(undefined, ref.link.source.fragmentRange.start.translate(0, -1));
}
+ return ref.link.source.hrefRange;
+ }
+
+ private findHeaderDeclaration(references: readonly MdReference[]): MdHeaderReference | undefined {
+ return references.find(ref => ref.isDefinition && ref.kind === 'header') as MdHeaderReference | undefined;
}
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 (await this.provideRenameEditsImpl(document, position, newName, token))?.edit;
+ }
+
+ public async provideRenameEditsImpl(document: SkinnyTextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise<MdWorkspaceEdit | undefined> {
+ const allRefsInfo = await this.getAllReferences(document, position, token);
+ if (token.isCancellationRequested || !allRefsInfo || !allRefsInfo.references.length) {
return undefined;
}
- const triggerRef = references.find(ref => ref.isTriggerLocation);
- if (!triggerRef) {
- return undefined;
+ const triggerRef = allRefsInfo.triggerRef;
+
+ if (triggerRef.kind === 'link' && (
+ (triggerRef.link.kind === 'definition' && triggerRef.link.ref.range.contains(position)) || triggerRef.link.href.kind === 'reference'
+ )) {
+ return this.renameReferenceLinks(allRefsInfo, newName);
+ } else if (triggerRef.kind === 'link' && triggerRef.link.href.kind === 'external') {
+ return this.renameExternalLink(allRefsInfo, newName);
+ } else if (triggerRef.kind === 'header' || (triggerRef.kind === 'link' && triggerRef.link.source.fragmentRange?.contains(position) && (triggerRef.link.kind === 'definition' || triggerRef.link.kind === 'link' && triggerRef.link.href.kind === 'internal'))) {
+ return this.renameFragment(allRefsInfo, newName);
+ } else if (triggerRef.kind === 'link' && !triggerRef.link.source.fragmentRange?.contains(position) && triggerRef.link.kind === 'link' && triggerRef.link.href.kind === 'internal') {
+ return this.renameFilePath(triggerRef.link.source.resource, triggerRef.link.href, allRefsInfo, newName);
}
- const isRefRename = triggerRef.kind === 'link' && (
- (triggerRef.link.kind === 'definition' && triggerRef.link.refRange.contains(position)) || triggerRef.link.href.kind === 'reference'
- );
+ return undefined;
+ }
+
+ private async renameFilePath(triggerDocument: vscode.Uri, triggerHref: InternalHref, allRefsInfo: MdReferencesResponse, newName: string): Promise<MdWorkspaceEdit> {
+ const edit = new vscode.WorkspaceEdit();
+ const fileRenames: MdFileRenameEdit[] = [];
+
+ const targetDoc = await tryFindMdDocumentForLink(triggerHref, this.workspaceContents);
+ const targetUri = targetDoc?.uri ?? triggerHref.path;
+
+ const rawNewFilePath = resolveDocumentLink(newName, triggerDocument);
+ let resolvedNewFilePath = rawNewFilePath;
+ if (!URI.Utils.extname(resolvedNewFilePath)) {
+ // If the newly entered path doesn't have a file extension but the original file did
+ // tack on a .md file extension
+ if (URI.Utils.extname(targetUri)) {
+ resolvedNewFilePath = resolvedNewFilePath.with({
+ path: resolvedNewFilePath.path + '.md'
+ });
+ }
+ }
+
+ // First rename the file
+ if (await this.workspaceContents.pathExists(targetUri)) {
+ fileRenames.push({ from: targetUri, to: resolvedNewFilePath });
+ edit.renameFile(targetUri, resolvedNewFilePath);
+ }
+
+ // Then update all refs to it
+ for (const ref of allRefsInfo.references) {
+ if (ref.kind === 'link') {
+ // Try to preserve style of existing links
+ let newPath: string;
+ if (ref.link.source.text.startsWith('/')) {
+ const root = resolveDocumentLink('/', ref.link.source.resource);
+ newPath = '/' + path.relative(root.toString(true), rawNewFilePath.toString(true));
+ } else {
+ const rootDir = URI.Utils.dirname(ref.link.source.resource);
+ if (rootDir.scheme === rawNewFilePath.scheme && rootDir.scheme !== 'untitled') {
+ newPath = path.relative(rootDir.toString(true), rawNewFilePath.toString(true));
+ if (newName.startsWith('./') && !newPath.startsWith('../') || newName.startsWith('.\\') && !newPath.startsWith('..\\')) {
+ newPath = './' + newPath;
+ }
+ } else {
+ newPath = newName;
+ }
+ }
+ edit.replace(ref.link.source.resource, this.getFilePathRange(ref), encodeURI(newPath.replace(/\\/g, '/')));
+ }
+ }
+
+ return { edit, fileRenames };
+ }
+
+ private renameFragment(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit {
const slug = this.slugifier.fromHeading(newName).value;
const edit = new vscode.WorkspaceEdit();
- for (const ref of references) {
+ for (const ref of allRefsInfo.references) {
switch (ref.kind) {
case 'header':
edit.replace(ref.location.uri, ref.headerTextLocation.range, newName);
break;
case 'link':
- if (ref.link.kind === 'definition') {
- // We may be renaming either the reference or the definition itself
- if (isRefRename) {
- edit.replace(ref.link.sourceResource, ref.link.refRange, newName);
- } else {
- edit.replace(ref.link.sourceResource, ref.fragmentLocation?.range ?? ref.link.sourceHrefRange, ref.fragmentLocation ? slug : newName);
- }
- } else {
- edit.replace(ref.location.uri, ref.fragmentLocation?.range ?? ref.location.range, ref.link.href.kind === 'reference' ? newName : slug);
- }
+ edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, !ref.link.source.fragmentRange || ref.link.href.kind === 'external' ? newName : slug);
break;
}
}
+ return { edit };
+ }
- return edit;
+ private renameExternalLink(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit {
+ const edit = new vscode.WorkspaceEdit();
+ for (const ref of allRefsInfo.references) {
+ if (ref.kind === 'link') {
+ edit.replace(ref.link.source.resource, ref.location.range, newName);
+ }
+ }
+ return { edit };
}
- private async getAllReferences(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken) {
+ private renameReferenceLinks(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit {
+ const edit = new vscode.WorkspaceEdit();
+ for (const ref of allRefsInfo.references) {
+ if (ref.kind === 'link') {
+ if (ref.link.kind === 'definition') {
+ edit.replace(ref.link.source.resource, ref.link.ref.range, newName);
+ } else {
+ edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, newName);
+ }
+ }
+ }
+ return { edit };
+ }
+
+ private async getAllReferences(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReferencesResponse | undefined> {
const version = document.version;
if (this.cachedRefs
@@ -116,16 +250,23 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide
&& this.cachedRefs.version === document.version
&& this.cachedRefs.position.isEqual(position)
) {
- return this.cachedRefs.references;
+ return this.cachedRefs;
+ }
+
+ const references = await this.referencesProvider.getAllReferencesAtPosition(document, position, token);
+ const triggerRef = references.find(ref => ref.isTriggerLocation);
+ if (!triggerRef) {
+ return undefined;
}
- const references = await this.referencesProvider.getAllReferences(document, position, token);
this.cachedRefs = {
resource: document.uri,
version,
position,
- references
+ references,
+ triggerRef
};
- return references;
+ return this.cachedRefs;
}
}
+