diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 1260da1fc32..eac0433c22b 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -12,6 +12,7 @@ import { registerDropIntoEditor } from './languageFeatures/dropIntoEditor'; import { MdFoldingProvider } from './languageFeatures/foldingProvider'; import { MdPathCompletionProvider } from './languageFeatures/pathCompletions'; import { MdReferencesProvider } from './languageFeatures/references'; +import { MdRenameProvider } from './languageFeatures/rename'; import { MdSmartSelect } from './languageFeatures/smartSelect'; import { MdWorkspaceSymbolProvider } from './languageFeatures/workspaceSymbolProvider'; import { Logger } from './logger'; @@ -61,13 +62,15 @@ function registerMarkdownLanguageFeatures( const linkProvider = new MdLinkProvider(engine); const workspaceContents = new VsCodeMdWorkspaceContents(); + const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier); return vscode.Disposable.from( vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider), vscode.languages.registerDocumentLinkProvider(selector, linkProvider), vscode.languages.registerFoldingRangeProvider(selector, new MdFoldingProvider(engine)), vscode.languages.registerSelectionRangeProvider(selector, new MdSmartSelect(engine)), vscode.languages.registerWorkspaceSymbolProvider(new MdWorkspaceSymbolProvider(symbolProvider, workspaceContents)), - vscode.languages.registerReferenceProvider(selector, new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier)), + vscode.languages.registerReferenceProvider(selector, referencesProvider), + vscode.languages.registerRenameProvider(selector, new MdRenameProvider(referencesProvider, githubSlugifier)), MdPathCompletionProvider.register(selector, engine, linkProvider), ); } diff --git a/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts b/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts index 14f9dd62c89..00fe13e5d0d 100644 --- a/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts @@ -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: { diff --git a/extensions/markdown-language-features/src/languageFeatures/references.ts b/extensions/markdown-language-features/src/languageFeatures/references.ts index 2db4fae1c36..0ff28f6acaf 100644 --- a/extensions/markdown-language-features/src/languageFeatures/references.ts +++ b/extensions/markdown-language-features/src/languageFeatures/references.ts @@ -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 { + 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, sourceLink: LinkData): Iterable { 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), }; } } diff --git a/extensions/markdown-language-features/src/languageFeatures/rename.ts b/extensions/markdown-language-features/src/languageFeatures/rename.ts new file mode 100644 index 00000000000..0bbc7c2e740 --- /dev/null +++ b/extensions/markdown-language-features/src/languageFeatures/rename.ts @@ -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 { + 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 { + 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; + } +} diff --git a/extensions/markdown-language-features/src/tableOfContents.ts b/extensions/markdown-language-features/src/tableOfContents.ts index 318641315b6..33a65d997b5 100644 --- a/extensions/markdown-language-features/src/tableOfContents.ts +++ b/extensions/markdown-language-features/src/tableOfContents.ts @@ -16,14 +16,47 @@ export interface TocEntry { readonly line: number; /** - * The entire range of the header section + * The entire range of the header section. + * + * For the doc: + * + * ```md + * # Head # + * text + * # Next head # + * ``` + * + * This is the range from `# Head #` to `# Next head #` */ readonly sectionLocation: vscode.Location; /** - * The range of the header itself + * The range of the header declaration. + * + * For the doc: + * + * ```md + * # Head # + * text + * ``` + * + * This is the range of `# Head #` */ readonly headerLocation: vscode.Location; + + /** + * The range of the header text. + * + * For the doc: + * + * ```md + * # Head # + * text + * ``` + * + * This is the range of `Head` + */ + readonly headerTextLocation: vscode.Location; } export class TableOfContents { @@ -80,6 +113,9 @@ export class TableOfContents { const headerLocation = new vscode.Location(document.uri, new vscode.Range(lineNumber, 0, lineNumber, line.text.length)); + const headerTextLocation = new vscode.Location(document.uri, + new vscode.Range(lineNumber, line.text.match(/^#+\s*/)?.[0].length ?? 0, lineNumber, line.text.length - (line.text.match(/\s*#*$/)?.[0].length ?? 0))); + toc.push({ slug, text: TableOfContents.getHeaderText(line.text), @@ -87,6 +123,7 @@ export class TableOfContents { line: lineNumber, sectionLocation: headerLocation, // Populated in next steps headerLocation, + headerTextLocation }); } diff --git a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts index 4bd7a9add4c..0dae55de9cc 100644 --- a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts +++ b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts @@ -9,7 +9,7 @@ import * as vscode from 'vscode'; import { MdLinkProvider } from '../languageFeatures/documentLinkProvider'; import { createNewMarkdownEngine } from './engine'; import { InMemoryDocument } from '../util/inMemoryDocument'; -import { joinLines, noopToken } from './util'; +import { assertRangeEqual, joinLines, noopToken } from './util'; const testFile = vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, 'x.md'); @@ -20,13 +20,6 @@ function getLinksForFile(fileContents: string) { return provider.provideDocumentLinks(doc, noopToken); } -function assertRangeEqual(expected: vscode.Range, actual: vscode.Range) { - assert.strictEqual(expected.start.line, actual.start.line); - assert.strictEqual(expected.start.character, actual.start.character); - assert.strictEqual(expected.end.line, actual.end.line); - assert.strictEqual(expected.end.character, actual.end.character); -} - suite('markdown.DocumentLinkProvider', () => { test('Should not return anything for empty document', async () => { const links = await getLinksForFile(''); diff --git a/extensions/markdown-language-features/src/test/references.test.ts b/extensions/markdown-language-features/src/test/references.test.ts index b05ae175791..0a563e07129 100644 --- a/extensions/markdown-language-features/src/test/references.test.ts +++ b/extensions/markdown-language-features/src/test/references.test.ts @@ -148,6 +148,21 @@ suite('markdown: find all references', () => { ); }); + test('Should find references from link definition', async () => { + const uri = workspacePath('doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `# A b C`, + `[text][bla]`, + `[bla]: #a-b-c`, // trigger here + )); + + const refs = await getReferences(doc, new vscode.Position(2, 9), new InMemoryWorkspaceMarkdownDocuments([doc])); + assertReferencesEqual(refs!, + { uri, line: 0 }, // Header definition + { uri, line: 2 }, + ); + }); + test('Should find references from link within same file', async () => { const uri = workspacePath('doc.md'); const doc = new InMemoryDocument(uri, joinLines( diff --git a/extensions/markdown-language-features/src/test/rename.test.ts b/extensions/markdown-language-features/src/test/rename.test.ts new file mode 100644 index 00000000000..7e4b70829b4 --- /dev/null +++ b/extensions/markdown-language-features/src/test/rename.test.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import 'mocha'; +import * as vscode from 'vscode'; +import { MdLinkProvider } from '../languageFeatures/documentLinkProvider'; +import { MdReferencesProvider } from '../languageFeatures/references'; +import { MdRenameProvider } from '../languageFeatures/rename'; +import { githubSlugifier } from '../slugify'; +import { InMemoryDocument } from '../util/inMemoryDocument'; +import { MdWorkspaceContents } from '../workspaceContents'; +import { createNewMarkdownEngine } from './engine'; +import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace'; +import { assertRangeEqual, joinLines, noopToken, workspacePath } from './util'; + + +/** + * Get the range that the rename should happen on. + */ +function getRenameRange(doc: InMemoryDocument, pos: vscode.Position, workspaceContents: MdWorkspaceContents) { + const engine = createNewMarkdownEngine(); + const linkProvider = new MdLinkProvider(engine); + const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier); + const renameProvider = new MdRenameProvider(referencesProvider, githubSlugifier); + return renameProvider.prepareRename(doc, pos, noopToken); +} + +/** + * Get all the edits for the rename. + */ +function getRenameEdits(doc: InMemoryDocument, pos: vscode.Position, newName: string, workspaceContents: MdWorkspaceContents) { + const engine = createNewMarkdownEngine(); + const linkProvider = new MdLinkProvider(engine); + const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier); + const renameProvider = new MdRenameProvider(referencesProvider, githubSlugifier); + return renameProvider.provideRenameEdits(doc, pos, newName, noopToken); +} + +function assertEditsEqual(actualEdit: vscode.WorkspaceEdit, ...expectedEdits: { uri: vscode.Uri; edits: vscode.TextEdit[] }[]) { + const actualEntries = actualEdit.entries(); + assert.strictEqual(actualEntries.length, expectedEdits.length, `Reference counts should match`); + + for (let i = 0; i < actualEntries.length; ++i) { + const actual = actualEntries[i]; + const expected = expectedEdits[i]; + assert.strictEqual(actual[0].toString(), expected.uri.toString(), `Ref '${i}' has expected document`); + + const actualEditForDoc = actual[1]; + const expectedEditsForDoc = expected.edits; + assert.strictEqual(actualEditForDoc.length, expectedEditsForDoc.length, `Edit counts for '${actual[0]}' should match`); + + for (let g = 0; g < actualEditForDoc.length; ++g) { + assertRangeEqual(actualEditForDoc[g].range, expectedEditsForDoc[g].range, `Edit '${g}' of '${actual[0]}' has expected expected range. Expected range: ${JSON.stringify(actualEditForDoc[g].range)}. Actual range: ${JSON.stringify(expectedEditsForDoc[g].range)}`); + assert.strictEqual(actualEditForDoc[g].newText, expectedEditsForDoc[g].newText, `Edit '${g}' of '${actual[0]}' has expected edits`); + } + } +} + +suite.only('markdown: rename', () => { + + setup(async () => { + // the tests make the assumption that link providers are already registered + await vscode.extensions.getExtension('vscode.markdown-language-features')!.activate(); + }); + + test('Rename on header should not include leading #', async () => { + const uri = workspacePath('doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `# abc` + )); + + const range = await getRenameRange(doc, new vscode.Position(0, 0), new InMemoryWorkspaceMarkdownDocuments([doc])); + assertRangeEqual(range!, new vscode.Range(0, 2, 0, 5)); + + const edit = await getRenameEdits(doc, new vscode.Position(0, 0), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc])); + assertEditsEqual(edit!, { + uri, edits: [ + new vscode.TextEdit(new vscode.Range(0, 2, 0, 5), 'New Header') + ] + }); + }); + + test('Rename on header should include leading or trailing #s', async () => { + const uri = workspacePath('doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `### abc ###` + )); + + const range = await getRenameRange(doc, new vscode.Position(0, 0), new InMemoryWorkspaceMarkdownDocuments([doc])); + assertRangeEqual(range!, new vscode.Range(0, 4, 0, 7)); + + const edit = await getRenameEdits(doc, new vscode.Position(0, 0), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc])); + assertEditsEqual(edit!, { + uri, edits: [ + new vscode.TextEdit(new vscode.Range(0, 4, 0, 7), 'New Header') + ] + }); + }); + + test('Rename on header should pick up links in doc', async () => { + const uri = workspacePath('doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `### A b C`, // rename here + `[text](#a-b-c)`, + )); + + const edit = await getRenameEdits(doc, new vscode.Position(0, 0), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc])); + assertEditsEqual(edit!, { + uri, edits: [ + new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'), + new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'), + ] + }); + }); + + test('Rename on link should use slug for link', async () => { + const uri = workspacePath('doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `### A b C`, + `[text](#a-b-c)`, // rename here + )); + + const edit = await getRenameEdits(doc, new vscode.Position(1, 10), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc])); + assertEditsEqual(edit!, { + uri, edits: [ + new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'), + new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'), + ] + }); + }); + + test('Rename on link definition should work', async () => { + const uri = workspacePath('doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `### A b C`, + `[text](#a-b-c)`, + `[ref]: #a-b-c`// rename here + )); + + const edit = await getRenameEdits(doc, new vscode.Position(2, 10), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc])); + assertEditsEqual(edit!, { + uri, edits: [ + new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'), + new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'), + new vscode.TextEdit(new vscode.Range(2, 8, 2, 13), 'new-header'), + ] + }); + }); +}); diff --git a/extensions/markdown-language-features/src/test/util.ts b/extensions/markdown-language-features/src/test/util.ts index 71249efd568..9d83e488651 100644 --- a/extensions/markdown-language-features/src/test/util.ts +++ b/extensions/markdown-language-features/src/test/util.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; import * as os from 'os'; import * as vscode from 'vscode'; import { InMemoryDocument } from '../util/inMemoryDocument'; @@ -35,3 +36,10 @@ export function getCursorPositions(contents: string, doc: InMemoryDocument): vsc export function workspacePath(...segments: string[]): vscode.Uri { return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, ...segments); } + +export function assertRangeEqual(expected: vscode.Range, actual: vscode.Range, message?: string) { + assert.strictEqual(expected.start.line, actual.start.line, message); + assert.strictEqual(expected.start.character, actual.start.character, message); + assert.strictEqual(expected.end.line, actual.end.line, message); + assert.strictEqual(expected.end.character, actual.end.character, message); +}