mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 21:55:38 +00:00
Initial work on rename in markdown
For #146291 Also fixes references triggered on a definition link
This commit is contained in:
parent
2431a2940a
commit
0e65adbda8
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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('');
|
||||
|
|
|
@ -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(
|
||||
|
|
152
extensions/markdown-language-features/src/test/rename.test.ts
Normal file
152
extensions/markdown-language-features/src/test/rename.test.ts
Normal file
|
@ -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'),
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue