diff --git a/extensions/markdown-language-features/src/languageFeatures/rename.ts b/extensions/markdown-language-features/src/languageFeatures/rename.ts index 55a723d8db4..716f8e41651 100644 --- a/extensions/markdown-language-features/src/languageFeatures/rename.ts +++ b/extensions/markdown-language-features/src/languageFeatures/rename.ts @@ -2,15 +2,16 @@ * 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 { resolveDocumentLink } from '../util/openDocumentLink'; import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents'; import { InternalHref } from './documentLinkProvider'; import { MdHeaderReference, MdLinkReference, MdReference, MdReferencesProvider, tryFindMdDocumentForLink } from './references'; -import * as URI from 'vscode-uri'; const localize = nls.loadMessageBundle(); @@ -147,25 +148,37 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide const targetDoc = await tryFindMdDocumentForLink(triggerHref, this.workspaceContents); const targetUri = targetDoc?.uri ?? triggerHref.path; - let newFilePath = resolveDocumentLink(newName, triggerHref.path); - if (!URI.Utils.extname(newFilePath)) { + let rawNewFilePath = resolveDocumentLink(newName, triggerHref.path); + 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)) { - newFilePath = newFilePath.with({ - path: newFilePath.path + '.md' + resolvedNewFilePath = resolvedNewFilePath.with({ + path: resolvedNewFilePath.path + '.md' }); } } // First rename the file - fileRenames.push({ from: targetUri.toString(), to: newFilePath.toString() }); - edit.renameFile(targetUri, newFilePath); + fileRenames.push({ from: targetUri.toString(), to: resolvedNewFilePath.toString() }); + edit.renameFile(targetUri, resolvedNewFilePath); // Then update all refs to it for (const ref of allRefsInfo.references) { if (ref.kind === 'link') { - edit.replace(ref.link.source.resource, this.getFilePathRange(ref), encodeURI(newName)); + // 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 { + newPath = path.relative(URI.Utils.dirname(ref.link.source.resource).toString(true), rawNewFilePath.toString(true)); + if (newName.startsWith('./') && !newPath.startsWith('../')) { + newPath = './' + newPath; + } + } + edit.replace(ref.link.source.resource, this.getFilePathRange(ref), encodeURI(newPath)); } } diff --git a/extensions/markdown-language-features/src/test/rename.test.ts b/extensions/markdown-language-features/src/test/rename.test.ts index 8ad8672edc8..e24405012af 100644 --- a/extensions/markdown-language-features/src/test/rename.test.ts +++ b/extensions/markdown-language-features/src/test/rename.test.ts @@ -473,15 +473,53 @@ suite('markdown: rename', () => { const edit = await getRenameEdits(doc, new vscode.Position(0, 10), '/new File', new InMemoryWorkspaceMarkdownDocuments([doc])); assertEditsEqual(edit!, { originalUri: uri, - newUri: workspacePath('new File.md'), + newUri: workspacePath('new File.md'), // Rename on disk should use file extension }, { uri: uri, edits: [ - new vscode.TextEdit(new vscode.Range(0, 7, 0, 11), '/new%20File'), + new vscode.TextEdit(new vscode.Range(0, 7, 0, 11), '/new%20File'), // Links should continue to use extension-less paths new vscode.TextEdit(new vscode.Range(1, 7, 1, 11), '/new%20File'), ] }); }); + test('Path rename should use correctly resolved paths across files', async () => { + const uri1 = workspacePath('sub', 'doc.md'); + const doc1 = new InMemoryDocument(uri1, joinLines( + `[text](./doc.md)`, + )); + + const uri2 = workspacePath('doc2.md'); + const doc2 = new InMemoryDocument(uri2, joinLines( + `[text](./sub/doc.md)`, + )); + + const uri3 = workspacePath('sub2', 'doc3.md'); + const doc3 = new InMemoryDocument(uri3, joinLines( + `[text](../sub/doc.md)`, + )); + + const uri4 = workspacePath('sub2', 'doc4.md'); + const doc4 = new InMemoryDocument(uri4, joinLines( + `[text](/sub/doc.md)`, + )); + + const edit = await getRenameEdits(doc1, new vscode.Position(0, 10), './new/new-doc.md', new InMemoryWorkspaceMarkdownDocuments([ + doc1, doc2, doc3, doc4, + ])); + assertEditsEqual(edit!, { + originalUri: uri1, + newUri: workspacePath('sub', 'new', 'new-doc.md'), + }, { + uri: uri1, edits: [new vscode.TextEdit(new vscode.Range(0, 7, 0, 15), './new/new-doc.md')] + }, { + uri: uri2, edits: [new vscode.TextEdit(new vscode.Range(0, 7, 0, 19), './sub/new/new-doc.md')] + }, { + uri: uri3, edits: [new vscode.TextEdit(new vscode.Range(0, 7, 0, 20), '../sub/new/new-doc.md')] + }, { + uri: uri4, edits: [new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/sub/new/new-doc.md')] + }); + }); + test('Rename on link should use header text as placeholder', async () => { const uri = workspacePath('doc.md'); const doc = new InMemoryDocument(uri, joinLines(