Move MD references, rename, and definition support to md LS (#155127)

This commit is contained in:
Matt Bierner 2022-07-13 23:32:27 -07:00 committed by GitHub
parent f992a90e32
commit 9ee8961347
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 194 additions and 2029 deletions

View file

@ -562,7 +562,8 @@
"@types/picomatch": "^2.3.0",
"@types/vscode-notebook-renderer": "^1.60.0",
"@types/vscode-webview": "^1.57.0",
"lodash.throttle": "^4.1.1"
"lodash.throttle": "^4.1.1",
"vscode-languageserver-types": "^3.17.2"
},
"repository": {
"type": "git",

View file

@ -6,7 +6,7 @@
"name": "Attach",
"type": "node",
"request": "attach",
"port": 7675,
"port": 7692,
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js"]
}

View file

@ -13,7 +13,7 @@
"vscode-languageserver": "^8.0.2-next.5`",
"vscode-languageserver-textdocument": "^1.0.5",
"vscode-languageserver-types": "^3.17.1",
"vscode-markdown-languageservice": "^0.0.0-alpha.5",
"vscode-markdown-languageservice": "^0.0.0-alpha.8",
"vscode-uri": "^3.0.3"
},
"devDependencies": {

View file

@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface LsConfiguration {
/**
* List of file extensions should be considered as markdown.
*
* These should not include the leading `.`.
*/
readonly markdownFileExtensions: readonly string[];
}
const defaultConfig: LsConfiguration = {
markdownFileExtensions: ['md'],
};
export function getLsConfiguration(overrides: Partial<LsConfiguration>): LsConfiguration {
return {
...defaultConfig,
...overrides,
};
}

View file

@ -5,13 +5,14 @@
import { RequestType } from 'vscode-languageserver';
import * as md from 'vscode-markdown-languageservice';
import * as lsp from 'vscode-languageserver-types';
// From server
export const parseRequestType: RequestType<{ uri: string }, md.Token[], any> = new RequestType('markdown/parse');
export const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
export const statFileRequestType: RequestType<{ uri: string }, md.FileStat | undefined, any> = new RequestType('markdown/statFile');
export const readDirectoryRequestType: RequestType<{ uri: string }, [string, md.FileStat][], any> = new RequestType('markdown/readDirectory');
export const findFilesRequestTypes: RequestType<{}, string[], any> = new RequestType('markdown/findFiles');
// To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');

View file

@ -3,13 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Connection, InitializeParams, InitializeResult, NotebookDocuments, TextDocuments } from 'vscode-languageserver';
import { CancellationToken, Connection, InitializeParams, InitializeResult, NotebookDocuments, TextDocuments } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as lsp from 'vscode-languageserver-types';
import * as md from 'vscode-markdown-languageservice';
import { URI } from 'vscode-uri';
import { getLsConfiguration } from './config';
import { LogFunctionLogger } from './logging';
import { parseRequestType } from './protocol';
import * as protocol from './protocol';
import { VsCodeClientWorkspace } from './workspace';
export async function startServer(connection: Connection) {
@ -17,13 +18,36 @@ export async function startServer(connection: Connection) {
const notebooks = new NotebookDocuments(documents);
connection.onInitialize((params: InitializeParams): InitializeResult => {
const parser = new class implements md.IMdParser {
slugifier = md.githubSlugifier;
async tokenize(document: md.ITextDocument): Promise<md.Token[]> {
return await connection.sendRequest(protocol.parseRequestType, { uri: document.uri.toString() });
}
};
const config = getLsConfiguration({
markdownFileExtensions: params.initializationOptions.markdownFileExtensions,
});
const workspace = new VsCodeClientWorkspace(connection, config, documents, notebooks);
const logger = new LogFunctionLogger(connection.console.log.bind(connection.console));
provider = md.createLanguageService({
workspace,
parser,
logger,
markdownFileExtensions: config.markdownFileExtensions,
});
workspace.workspaceFolders = (params.workspaceFolders ?? []).map(x => URI.parse(x.uri));
return {
capabilities: {
completionProvider: { triggerCharacters: ['.', '/', '#'] },
definitionProvider: true,
documentLinkProvider: { resolveProvider: true },
documentSymbolProvider: true,
completionProvider: { triggerCharacters: ['.', '/', '#'] },
foldingRangeProvider: true,
renameProvider: { prepareProvider: true, },
selectionRangeProvider: true,
workspaceSymbolProvider: true,
workspace: {
@ -36,23 +60,14 @@ export async function startServer(connection: Connection) {
};
});
const parser = new class implements md.IMdParser {
slugifier = md.githubSlugifier;
async tokenize(document: md.ITextDocument): Promise<md.Token[]> {
return await connection.sendRequest(parseRequestType, { uri: document.uri.toString() });
}
};
const workspace = new VsCodeClientWorkspace(connection, documents, notebooks);
const logger = new LogFunctionLogger(connection.console.log.bind(connection.console));
const provider = md.createLanguageService({ workspace, parser, logger });
let provider: md.IMdLanguageService | undefined;
connection.onDocumentLinks(async (params, token): Promise<lsp.DocumentLink[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.getDocumentLinks(document, token);
return await provider!.getDocumentLinks(document, token);
}
} catch (e) {
console.error(e.stack);
@ -62,7 +77,7 @@ export async function startServer(connection: Connection) {
connection.onDocumentLinkResolve(async (link, token): Promise<lsp.DocumentLink | undefined> => {
try {
return await provider.resolveDocumentLink(link, token);
return await provider!.resolveDocumentLink(link, token);
} catch (e) {
console.error(e.stack);
}
@ -73,7 +88,7 @@ export async function startServer(connection: Connection) {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.getDocumentSymbols(document, token);
return await provider!.getDocumentSymbols(document, token);
}
} catch (e) {
console.error(e.stack);
@ -85,7 +100,7 @@ export async function startServer(connection: Connection) {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.getFoldingRanges(document, token);
return await provider!.getFoldingRanges(document, token);
}
} catch (e) {
console.error(e.stack);
@ -97,7 +112,7 @@ export async function startServer(connection: Connection) {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.getSelectionRanges(document, params.positions, token);
return await provider!.getSelectionRanges(document, params.positions, token);
}
} catch (e) {
console.error(e.stack);
@ -107,7 +122,7 @@ export async function startServer(connection: Connection) {
connection.onWorkspaceSymbol(async (params, token): Promise<lsp.WorkspaceSymbol[]> => {
try {
return await provider.getWorkspaceSymbols(params.query, token);
return await provider!.getWorkspaceSymbols(params.query, token);
} catch (e) {
console.error(e.stack);
}
@ -118,7 +133,7 @@ export async function startServer(connection: Connection) {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider.getCompletionItems(document, params.position, params.context!, token);
return await provider!.getCompletionItems(document, params.position, params.context!, token);
}
} catch (e) {
console.error(e.stack);
@ -126,6 +141,65 @@ export async function startServer(connection: Connection) {
return [];
});
connection.onReferences(async (params, token): Promise<lsp.Location[]> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getReferences(document, params.position, params.context, token);
}
} catch (e) {
console.error(e.stack);
}
return [];
});
connection.onDefinition(async (params, token): Promise<lsp.Definition | undefined> => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.getDefinition(document, params.position, token);
}
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onPrepareRename(async (params, token) => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
return await provider!.prepareRename(document, params.position, token);
}
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onRenameRequest(async (params, token) => {
try {
const document = documents.get(params.textDocument.uri);
if (document) {
const edit = await provider!.getRenameEdit(document, params.position, params.newName, token);
console.log(JSON.stringify(edit));
return edit;
}
} catch (e) {
console.error(e.stack);
}
return undefined;
});
connection.onRequest(protocol.getReferencesToFileInWorkspace, (async (params: { uri: string }, token: CancellationToken) => {
try {
return await provider!.getFileReferences(URI.parse(params.uri), token);
} catch (e) {
console.error(e.stack);
}
return undefined;
}));
documents.listen(connection);
notebooks.listen(connection);
connection.listen();

View file

@ -4,24 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { TextDocument } from 'vscode-languageserver-textdocument';
import * as URI from 'vscode-uri';
import { URI, Utils } from 'vscode-uri';
import { LsConfiguration } from '../config';
const markdownFileExtensions = Object.freeze<string[]>([
'.md',
'.mkd',
'.mdwn',
'.mdown',
'.markdown',
'.markdn',
'.mdtxt',
'.mdtext',
'.workbook',
]);
export function looksLikeMarkdownPath(resolvedHrefPath: URI.URI) {
return markdownFileExtensions.includes(URI.Utils.extname(URI.URI.from(resolvedHrefPath)).toLowerCase());
export function looksLikeMarkdownPath(config: LsConfiguration, resolvedHrefPath: URI) {
return config.markdownFileExtensions.includes(Utils.extname(URI.from(resolvedHrefPath)).toLowerCase().replace('.', ''));
}
export function isMarkdownDocument(document: TextDocument): boolean {
export function isMarkdownFile(document: TextDocument) {
return document.languageId === 'markdown';
}

View file

@ -8,9 +8,10 @@ import { TextDocument } from 'vscode-languageserver-textdocument';
import * as md from 'vscode-markdown-languageservice';
import { ContainingDocumentContext } from 'vscode-markdown-languageservice/out/workspace';
import { URI } from 'vscode-uri';
import { LsConfiguration } from './config';
import * as protocol from './protocol';
import { coalesce } from './util/arrays';
import { isMarkdownDocument, looksLikeMarkdownPath } from './util/file';
import { isMarkdownFile, looksLikeMarkdownPath } from './util/file';
import { Limiter } from './util/limiter';
import { ResourceMap } from './util/resourceMap';
import { Schemes } from './util/schemes';
@ -34,6 +35,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
constructor(
private readonly connection: Connection,
private readonly config: LsConfiguration,
private readonly documents: TextDocuments<TextDocument>,
private readonly notebooks: NotebookDocuments<TextDocument>,
) {
@ -141,7 +143,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
return matchingDocument;
}
if (!looksLikeMarkdownPath(resource)) {
if (!looksLikeMarkdownPath(this.config, resource)) {
return undefined;
}
@ -182,6 +184,6 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
}
private isRelevantMarkdownDocument(doc: TextDocument) {
return isMarkdownDocument(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview';
return isMarkdownFile(doc) && URI.parse(doc.uri).scheme !== 'vscode-bulkeditpreview';
}
}

View file

@ -42,10 +42,10 @@ vscode-languageserver@^8.0.2-next.5`:
dependencies:
vscode-languageserver-protocol "3.17.2-next.6"
vscode-markdown-languageservice@^0.0.0-alpha.5:
version "0.0.0-alpha.5"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.5.tgz#fb3042f3ee79589606154c19b15565541337bceb"
integrity sha512-vy8UVa1jtm3CwkifRn3fEWM710JC4AYEECNd5KQthSCoFSfT5pOshJNFWs5yzBeVrohiy4deOdhSrfbDMg/Hyg==
vscode-markdown-languageservice@^0.0.0-alpha.8:
version "0.0.0-alpha.8"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.8.tgz#05d4f86cf0514fd71479847eef742fcc8cdbe87f"
integrity sha512-si8weZsY4LtyonyZwxpFYk8WucRFiKJisErNTt1HDjUCglSDIZqsMNuMIcz3t0nVNfG0LrpdMFVLGhmET5D71Q==
dependencies:
vscode-languageserver-textdocument "^1.0.5"
vscode-languageserver-types "^3.17.1"

View file

@ -24,15 +24,17 @@ export type LanguageClientConstructor = (name: string, description: string, clie
export async function startClient(factory: LanguageClientConstructor, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
const documentSelector = ['markdown'];
const mdFileGlob = `**/*.{${markdownFileExtensions.join(',')}}`;
const clientOptions: LanguageClientOptions = {
documentSelector,
documentSelector: [{ language: 'markdown' }],
synchronize: {
configurationSection: ['markdown'],
fileEvents: vscode.workspace.createFileSystemWatcher(mdFileGlob),
},
initializationOptions: {
markdownFileExtensions,
}
};
const client = factory('markdown', localize('markdownServer.name', 'Markdown Language Server'), clientOptions);

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
import { BaseLanguageClient, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
import { startClient } from './client';
import { activateShared } from './extension.shared';
import { VsCodeOutputLogger } from './logging';
@ -13,7 +13,7 @@ import { getMarkdownExtensionContributions } from './markdownExtensions';
import { githubSlugifier } from './slugify';
import { IMdWorkspace, VsCodeMdWorkspace } from './workspace';
export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {
const contributions = getMarkdownExtensionContributions(context);
context.subscriptions.push(contributions);
@ -25,15 +25,15 @@ export function activate(context: vscode.ExtensionContext) {
const workspace = new VsCodeMdWorkspace();
context.subscriptions.push(workspace);
activateShared(context, workspace, engine, logger, contributions);
startServer(context, workspace, engine);
const client = await startServer(context, workspace, engine);
activateShared(context, client, workspace, engine, logger, contributions);
}
async function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<void> {
function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
const serverMain = vscode.Uri.joinPath(context.extensionUri, 'server/dist/browser/main.js');
const worker = new Worker(serverMain.toString());
await startClient((id: string, name: string, clientOptions: LanguageClientOptions) => {
return startClient((id: string, name: string, clientOptions: LanguageClientOptions) => {
return new LanguageClient(id, name, clientOptions, worker);
}, workspace, parser);
}

View file

@ -4,16 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient } from 'vscode-languageclient';
import { CommandManager } from './commandManager';
import * as commands from './commands/index';
import { registerPasteSupport } from './languageFeatures/copyPaste';
import { registerDefinitionSupport } from './languageFeatures/definitions';
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
import { MdLinkProvider } from './languageFeatures/documentLinks';
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
import { MdReferencesProvider, registerReferencesSupport } from './languageFeatures/references';
import { registerRenameSupport } from './languageFeatures/rename';
import { MdReferencesProvider } from './languageFeatures/references';
import { ILogger } from './logging';
import { IMdParser, MarkdownItEngine, MdParsingProvider } from './markdownEngine';
import { MarkdownContributionProvider } from './markdownExtensions';
@ -26,6 +25,7 @@ import { IMdWorkspace } from './workspace';
export function activateShared(
context: vscode.ExtensionContext,
client: BaseLanguageClient,
workspace: IMdWorkspace,
engine: MarkdownItEngine,
logger: ILogger,
@ -45,7 +45,7 @@ export function activateShared(
const previewManager = new MarkdownPreviewManager(contentProvider, workspace, logger, contributions, tocProvider);
context.subscriptions.push(previewManager);
context.subscriptions.push(registerMarkdownLanguageFeatures(parser, workspace, commandManager, tocProvider, logger));
context.subscriptions.push(registerMarkdownLanguageFeatures(client, parser, workspace, commandManager, tocProvider, logger));
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine, tocProvider));
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
@ -54,6 +54,7 @@ export function activateShared(
}
function registerMarkdownLanguageFeatures(
client: BaseLanguageClient,
parser: IMdParser,
workspace: IMdWorkspace,
commandManager: CommandManager,
@ -70,13 +71,10 @@ function registerMarkdownLanguageFeatures(
referencesProvider,
// Language features
registerDefinitionSupport(selector, referencesProvider),
registerDiagnosticSupport(selector, workspace, linkProvider, commandManager, referencesProvider, tocProvider, logger),
registerDropIntoEditorSupport(selector),
registerFindFileReferenceSupport(commandManager, referencesProvider),
registerFindFileReferenceSupport(commandManager, client),
registerPasteSupport(selector),
registerReferencesSupport(selector, referencesProvider),
registerRenameSupport(selector, workspace, referencesProvider, parser.slugifier),
);
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node';
import { BaseLanguageClient, LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node';
import { startClient } from './client';
import { activateShared } from './extension.shared';
import { VsCodeOutputLogger } from './logging';
@ -13,7 +13,7 @@ import { getMarkdownExtensionContributions } from './markdownExtensions';
import { githubSlugifier } from './slugify';
import { IMdWorkspace, VsCodeMdWorkspace } from './workspace';
export function activate(context: vscode.ExtensionContext) {
export async function activate(context: vscode.ExtensionContext) {
const contributions = getMarkdownExtensionContributions(context);
context.subscriptions.push(contributions);
@ -25,11 +25,11 @@ export function activate(context: vscode.ExtensionContext) {
const workspace = new VsCodeMdWorkspace();
context.subscriptions.push(workspace);
activateShared(context, workspace, engine, logger, contributions);
startServer(context, workspace, engine);
const client = await startServer(context, workspace, engine);
activateShared(context, client, workspace, engine, logger, contributions);
}
async function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<void> {
function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
const clientMain = vscode.extensions.getExtension('vscode.markdown-language-features')?.packageJSON?.main || '';
const serverMain = `./server/${clientMain.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/main`;
@ -44,7 +44,7 @@ async function startServer(context: vscode.ExtensionContext, workspace: IMdWorks
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
};
await startClient((id, name, clientOptions) => {
return startClient((id, name, clientOptions) => {
return new LanguageClient(id, name, serverOptions, clientOptions);
}, workspace, parser);
}

View file

@ -1,27 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { ITextDocument } from '../types/textDocument';
import { MdReferencesProvider } from './references';
export class MdVsCodeDefinitionProvider implements vscode.DefinitionProvider {
constructor(
private readonly referencesProvider: MdReferencesProvider,
) { }
async provideDefinition(document: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
const allRefs = await this.referencesProvider.getReferencesAtPosition(document, position, token);
return allRefs.find(ref => ref.kind === 'link' && ref.isDefinition)?.location;
}
}
export function registerDefinitionSupport(
selector: vscode.DocumentSelector,
referencesProvider: MdReferencesProvider,
): vscode.Disposable {
return vscode.languages.registerDefinitionProvider(selector, new MdVsCodeDefinitionProvider(referencesProvider));
}

View file

@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commandManager';
import { MdReferencesProvider } from './references';
import { getReferencesToFileInWorkspace } from '../protocol';
const localize = nls.loadMessageBundle();
@ -16,7 +17,7 @@ export class FindFileReferencesCommand implements Command {
public readonly id = 'markdown.findAllFileReferences';
constructor(
private readonly referencesProvider: MdReferencesProvider,
private readonly client: BaseLanguageClient,
) { }
public async execute(resource?: vscode.Uri) {
@ -33,8 +34,9 @@ export class FindFileReferencesCommand implements Command {
location: vscode.ProgressLocation.Window,
title: localize('progress.title', "Finding file references")
}, async (_progress, token) => {
const references = await this.referencesProvider.getReferencesToFileInWorkspace(resource!, token);
const locations = references.map(ref => ref.location);
const locations = (await this.client.sendRequest(getReferencesToFileInWorkspace, { uri: resource!.toString() }, token)).map(loc => {
return new vscode.Location(vscode.Uri.parse(loc.uri), new vscode.Range(loc.range.start.line, loc.range.start.character, loc.range.end.line, loc.range.end.character));
});
const config = vscode.workspace.getConfiguration('references');
const existingSetting = config.inspect<string>('preferredLocation');
@ -51,7 +53,7 @@ export class FindFileReferencesCommand implements Command {
export function registerFindFileReferenceSupport(
commandManager: CommandManager,
referencesProvider: MdReferencesProvider
client: BaseLanguageClient,
): vscode.Disposable {
return commandManager.register(new FindFileReferencesCommand(referencesProvider));
return commandManager.register(new FindFileReferencesCommand(client));
}

View file

@ -312,30 +312,6 @@ export class MdReferencesProvider extends Disposable {
}
}
/**
* Implements {@link vscode.ReferenceProvider} for markdown documents.
*/
export class MdVsCodeReferencesProvider implements vscode.ReferenceProvider {
public constructor(
private readonly referencesProvider: MdReferencesProvider
) { }
async provideReferences(document: ITextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise<vscode.Location[]> {
const allRefs = await this.referencesProvider.getReferencesAtPosition(document, position, token);
return allRefs
.filter(ref => context.includeDeclaration || !ref.isDefinition)
.map(ref => ref.location);
}
}
export function registerReferencesSupport(
selector: vscode.DocumentSelector,
referencesProvider: MdReferencesProvider,
): vscode.Disposable {
return vscode.languages.registerReferenceProvider(selector, new MdVsCodeReferencesProvider(referencesProvider));
}
export async function tryResolveLinkPath(originalUri: vscode.Uri, workspace: IMdWorkspace): Promise<vscode.Uri | undefined> {
if (await workspace.pathExists(originalUri)) {
return originalUri;

View file

@ -1,281 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { ITextDocument } from '../types/textDocument';
import { Disposable } from '../util/dispose';
import { resolveDocumentLink } from '../util/openDocumentLink';
import { IMdWorkspace } from '../workspace';
import { InternalHref } from './documentLinks';
import { MdHeaderReference, MdLinkReference, MdReference, MdReferencesProvider, tryResolveLinkPath } 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 MdVsCodeRenameProvider 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 workspace: IMdWorkspace,
private readonly referencesProvider: MdReferencesProvider,
private readonly slugifier: Slugifier,
) {
super();
}
public async prepareRename(document: ITextDocument, 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 (!allRefsInfo || !allRefsInfo.references.length) {
throw new Error(this.renameNotSupportedText);
}
const triggerRef = allRefsInfo.triggerRef;
switch (triggerRef.kind) {
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.ref.range.contains(position)) {
return { range: triggerRef.link.ref.range, placeholder: triggerRef.link.ref.text };
}
}
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 };
}
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: ITextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise<vscode.WorkspaceEdit | undefined> {
return (await this.provideRenameEditsImpl(document, position, newName, token))?.edit;
}
public async provideRenameEditsImpl(document: ITextDocument, 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 = 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.kind === 'definition') && triggerRef.link.href.kind === 'internal') {
return this.renameFilePath(triggerRef.link.source.resource, triggerRef.link.href, allRefsInfo, newName);
}
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 targetUri = await tryResolveLinkPath(triggerHref.path, this.workspace) ?? 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.workspace.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.hrefText.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 allRefsInfo.references) {
switch (ref.kind) {
case 'header':
edit.replace(ref.location.uri, ref.headerTextLocation.range, newName);
break;
case 'link':
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 };
}
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 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: ITextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReferencesResponse | undefined> {
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;
}
const references = await this.referencesProvider.getReferencesAtPosition(document, position, token);
const triggerRef = references.find(ref => ref.isTriggerLocation);
if (!triggerRef) {
return undefined;
}
this.cachedRefs = {
resource: document.uri,
version,
position,
references,
triggerRef
};
return this.cachedRefs;
}
}
export function registerRenameSupport(
selector: vscode.DocumentSelector,
workspace: IMdWorkspace,
referencesProvider: MdReferencesProvider,
slugifier: Slugifier,
): vscode.Disposable {
return vscode.languages.registerRenameProvider(selector, new MdVsCodeRenameProvider(workspace, referencesProvider, slugifier));
}

View file

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Token = require('markdown-it/lib/token');
import { RequestType } from 'vscode-languageclient';
import type * as lsp from 'vscode-languageserver-types';
// From server
export const parseRequestType: RequestType<{ uri: string }, Token[], any> = new RequestType('markdown/parse');
export const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
export const statFileRequestType: RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any> = new RequestType('markdown/statFile');
export const readDirectoryRequestType: RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any> = new RequestType('markdown/readDirectory');
export const findFilesRequestTypes = new RequestType<{}, string[], any>('markdown/findFiles');
// To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');

View file

@ -1,144 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { MdVsCodeDefinitionProvider } from '../languageFeatures/definitions';
import { MdReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { joinLines, withStore, workspacePath } from './util';
function getDefinition(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, workspace: IMdWorkspace) {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const referencesProvider = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
const provider = new MdVsCodeDefinitionProvider(referencesProvider);
return provider.provideDefinition(doc, pos, noopToken);
}
function assertDefinitionsEqual(actualDef: vscode.Definition, ...expectedDefs: { uri: vscode.Uri; line: number; startCharacter?: number; endCharacter?: number }[]) {
const actualDefsArr = Array.isArray(actualDef) ? actualDef : [actualDef];
assert.strictEqual(actualDefsArr.length, expectedDefs.length, `Definition counts should match`);
for (let i = 0; i < actualDefsArr.length; ++i) {
const actual = actualDefsArr[i];
const expected = expectedDefs[i];
assert.strictEqual(actual.uri.toString(), expected.uri.toString(), `Definition '${i}' has expected document`);
assert.strictEqual(actual.range.start.line, expected.line, `Definition '${i}' has expected start line`);
assert.strictEqual(actual.range.end.line, expected.line, `Definition '${i}' has expected end line`);
if (typeof expected.startCharacter !== 'undefined') {
assert.strictEqual(actual.range.start.character, expected.startCharacter, `Definition '${i}' has expected start character`);
}
if (typeof expected.endCharacter !== 'undefined') {
assert.strictEqual(actual.range.end.character, expected.endCharacter, `Definition '${i}' has expected end character`);
}
}
}
suite('markdown: Go to definition', () => {
test('Should not return definition when on link text', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[ref](#abc)`,
`[ref]: http://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const defs = await getDefinition(store, doc, new vscode.Position(0, 1), workspace);
assert.deepStrictEqual(defs, undefined);
}));
test('Should find definition links within file from link', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`, // trigger here
``,
`[abc]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const defs = await getDefinition(store, doc, new vscode.Position(0, 12), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 2 },
);
}));
test('Should find definition links using shorthand', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[ref]`, // trigger 1
``,
`[yes][ref]`, // trigger 2
``,
`[ref]: /Hello.md` // trigger 3
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
{
const defs = await getDefinition(store, doc, new vscode.Position(0, 2), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 4 },
);
}
{
const defs = await getDefinition(store, doc, new vscode.Position(2, 7), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 4 },
);
}
{
const defs = await getDefinition(store, doc, new vscode.Position(4, 2), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 4 },
);
}
}));
test('Should find definition links within file from definition', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com`, // trigger here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const defs = await getDefinition(store, doc, new vscode.Position(2, 3), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 2 },
);
}));
test('Should not find definition links across files', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(workspacePath('other.md'), joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com?bad`
))
]));
const defs = await getDefinition(store, doc, new vscode.Position(0, 12), workspace);
assertDefinitionsEqual(defs!,
{ uri: docUri, line: 2 },
);
}));
});

View file

@ -1,120 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { MdReference, MdReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { joinLines, withStore, workspacePath } from './util';
function getFileReferences(store: DisposableStore, resource: vscode.Uri, workspace: IMdWorkspace) {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const computer = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
return computer.getReferencesToFileInWorkspace(resource, noopToken);
}
function assertReferencesEqual(actualRefs: readonly MdReference[], ...expectedRefs: { uri: vscode.Uri; line: number }[]) {
assert.strictEqual(actualRefs.length, expectedRefs.length, `Reference counts should match`);
for (let i = 0; i < actualRefs.length; ++i) {
const actual = actualRefs[i].location;
const expected = expectedRefs[i];
assert.strictEqual(actual.uri.toString(), expected.uri.toString(), `Ref '${i}' has expected document`);
assert.strictEqual(actual.range.start.line, expected.line, `Ref '${i}' has expected start line`);
assert.strictEqual(actual.range.end.line, expected.line, `Ref '${i}' has expected end line`);
}
}
suite('markdown: find file references', () => {
test('Should find basic references', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(docUri, joinLines(
`# header`,
`[link 1](./other.md)`,
`[link 2](./other.md)`
)),
new InMemoryDocument(otherUri, joinLines(
`# header`,
`pre`,
`[link 3](./other.md)`,
`post`
)),
]));
const refs = await getFileReferences(store, otherUri, workspace);
assertReferencesEqual(refs,
{ uri: docUri, line: 1 },
{ uri: docUri, line: 2 },
{ uri: otherUri, line: 2 },
);
}));
test('Should find references with and without file extensions', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(docUri, joinLines(
`# header`,
`[link 1](./other.md)`,
`[link 2](./other)`
)),
new InMemoryDocument(otherUri, joinLines(
`# header`,
`pre`,
`[link 3](./other.md)`,
`[link 4](./other)`,
`post`
)),
]));
const refs = await getFileReferences(store, otherUri, workspace);
assertReferencesEqual(refs,
{ uri: docUri, line: 1 },
{ uri: docUri, line: 2 },
{ uri: otherUri, line: 2 },
{ uri: otherUri, line: 3 },
);
}));
test('Should find references with headers on links', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const workspace = store.add(new InMemoryMdWorkspace([
new InMemoryDocument(docUri, joinLines(
`# header`,
`[link 1](./other.md#sub-bla)`,
`[link 2](./other#sub-bla)`
)),
new InMemoryDocument(otherUri, joinLines(
`# header`,
`pre`,
`[link 3](./other.md#sub-bla)`,
`[link 4](./other#sub-bla)`,
`post`
)),
]));
const refs = await getFileReferences(store, otherUri, workspace);
assertReferencesEqual(refs,
{ uri: docUri, line: 1 },
{ uri: docUri, line: 2 },
{ uri: otherUri, line: 2 },
{ uri: otherUri, line: 3 },
);
}));
});

View file

@ -1,635 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { MdReferencesProvider, MdVsCodeReferencesProvider } from '../languageFeatures/references';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { joinLines, withStore, workspacePath } from './util';
async function getReferences(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, workspace: IMdWorkspace) {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const computer = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
const provider = new MdVsCodeReferencesProvider(computer);
const refs = await provider.provideReferences(doc, pos, { includeDeclaration: true }, noopToken);
return refs.sort((a, b) => {
const pathCompare = a.uri.toString().localeCompare(b.uri.toString());
if (pathCompare !== 0) {
return pathCompare;
}
return a.range.start.compareTo(b.range.start);
});
}
function assertReferencesEqual(actualRefs: readonly vscode.Location[], ...expectedRefs: { uri: vscode.Uri; line: number; startCharacter?: number; endCharacter?: number }[]) {
assert.strictEqual(actualRefs.length, expectedRefs.length, `Reference counts should match`);
for (let i = 0; i < actualRefs.length; ++i) {
const actual = actualRefs[i];
const expected = expectedRefs[i];
assert.strictEqual(actual.uri.toString(), expected.uri.toString(), `Ref '${i}' has expected document`);
assert.strictEqual(actual.range.start.line, expected.line, `Ref '${i}' has expected start line`);
assert.strictEqual(actual.range.end.line, expected.line, `Ref '${i}' has expected end line`);
if (typeof expected.startCharacter !== 'undefined') {
assert.strictEqual(actual.range.start.character, expected.startCharacter, `Ref '${i}' has expected start character`);
}
if (typeof expected.endCharacter !== 'undefined') {
assert.strictEqual(actual.range.end.character, expected.endCharacter, `Ref '${i}' has expected end character`);
}
}
}
suite('Markdown: Find all references', () => {
test('Should not return references when not on header or link', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`# abc`,
``,
`[link 1](#abc)`,
`text`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
{
const refs = await getReferences(store, doc, new vscode.Position(1, 0), workspace);
assert.deepStrictEqual(refs, []);
}
{
const refs = await getReferences(store, doc, new vscode.Position(3, 2), workspace);
assert.deepStrictEqual(refs, []);
}
}));
test('Should find references from header within same file', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
`[not link](#noabc)`,
`[link 2](#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 3), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 2 },
{ uri, line: 4 },
);
}));
test('Should not return references when on link text', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`[ref](#abc)`,
`[ref]: http://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 1), workspace);
assert.deepStrictEqual(refs, []);
}));
test('Should find references using normalized slug', withStore(async (store) => {
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
`# a B c`,
`[simple](#a-b-c)`,
`[start underscore](#_a-b-c)`,
`[different case](#a-B-C)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
{
// Trigger header
const refs = await getReferences(store, doc, new vscode.Position(0, 0), workspace);
assert.deepStrictEqual(refs!.length, 4);
}
{
// Trigger on line 1
const refs = await getReferences(store, doc, new vscode.Position(1, 12), workspace);
assert.deepStrictEqual(refs!.length, 4);
}
{
// Trigger on line 2
const refs = await getReferences(store, doc, new vscode.Position(2, 24), workspace);
assert.deepStrictEqual(refs!.length, 4);
}
{
// Trigger on line 3
const refs = await getReferences(store, doc, new vscode.Position(3, 20), workspace);
assert.deepStrictEqual(refs!.length, 4);
}
}));
test('Should find references from header across files', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('sub', 'other.md');
const other2Uri = workspacePath('zOther2.md');
const doc = new InMemoryDocument(docUri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(other1Uri, joinLines(
`[not link](#abc)`,
`[not link](/doc.md#abz)`,
`[link](/doc.md#abc)`
)),
new InMemoryDocument(other2Uri, joinLines(
`[not link](#abc)`,
`[not link](./doc.md#abz)`,
`[link](./doc.md#abc)`
))
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 3), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 }, // Header definition
{ uri: docUri, line: 2 },
{ uri: other1Uri, line: 2 },
{ uri: other2Uri, line: 2 },
);
}));
test('Should find references from header to link definitions ', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# abc`,
``,
`[bla]: #abc`
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 3), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 }, // Header definition
{ uri, line: 2 },
);
}));
test('Should find header references from link definition', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# A b C`,
`[text][bla]`,
`[bla]: #a-b-c`, // trigger here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(2, 9), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 }, // Header definition
{ uri, line: 2 },
);
}));
test('Should find references from link within same file', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
`[not link](#noabc)`,
`[link 2](#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(2, 10), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 }, // Header definition
{ uri, line: 2 },
{ uri, line: 4 },
);
}));
test('Should find references from link across files', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('sub', 'other.md');
const other2Uri = workspacePath('zOther2.md');
const doc = new InMemoryDocument(docUri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(other1Uri, joinLines(
`[not link](#abc)`,
`[not link](/doc.md#abz)`,
`[with ext](/doc.md#abc)`,
`[without ext](/doc#abc)`
)),
new InMemoryDocument(other2Uri, joinLines(
`[not link](#abc)`,
`[not link](./doc.md#abz)`,
`[link](./doc.md#abc)`
))
]));
const refs = await getReferences(store, doc, new vscode.Position(2, 10), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 }, // Header definition
{ uri: docUri, line: 2 },
{ uri: other1Uri, line: 2 }, // Other with ext
{ uri: other1Uri, line: 3 }, // Other without ext
{ uri: other2Uri, line: 2 }, // Other2
);
}));
test('Should find references without requiring file extensions', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`# a B c`,
``,
`[link 1](#a-b-c)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(other1Uri, joinLines(
`[not link](#a-b-c)`,
`[not link](/doc.md#a-b-z)`,
`[with ext](/doc.md#a-b-c)`,
`[without ext](/doc#a-b-c)`,
`[rel with ext](./doc.md#a-b-c)`,
`[rel without ext](./doc#a-b-c)`
)),
]));
const refs = await getReferences(store, doc, new vscode.Position(2, 10), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 }, // Header definition
{ uri: docUri, line: 2 },
{ uri: other1Uri, line: 2 }, // Other with ext
{ uri: other1Uri, line: 3 }, // Other without ext
{ uri: other1Uri, line: 4 }, // Other relative link with ext
{ uri: other1Uri, line: 5 }, // Other relative link without ext
);
}));
test('Should find references from link across files when triggered on link without file extension', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('sub', 'other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[with ext](./sub/other#header)`,
`[without ext](./sub/other.md#header)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(other1Uri, joinLines(
`pre`,
`# header`,
`post`
)),
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 23), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 1 },
{ uri: other1Uri, line: 1 }, // Header definition
);
}));
test('Should include header references when triggered on file link', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('sub', 'other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[with ext](./sub/other)`,
`[with ext](./sub/other#header)`,
`[without ext](./sub/other.md#no-such-header)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(otherUri, joinLines(
`pre`,
`# header`,
`post`
)),
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 15), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 1 },
{ uri: docUri, line: 2 },
);
}));
test('Should not include refs from other file to own header', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const otherUri = workspacePath('sub', 'other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[other](./sub/other)`, // trigger here
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(otherUri, joinLines(
`# header`, // Definition should not be included since we triggered on a file link
`[text](#header)`, // Ref should not be included since it is to own file
)),
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 15), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
);
}));
test('Should find explicit references to own file ', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[bare](doc.md)`, // trigger here
`[rel](./doc.md)`,
`[abs](/doc.md)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 12), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 1 },
{ uri, line: 2 },
);
}));
test('Should support finding references to http uri', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[1](http://example.com)`,
`[no](https://example.com)`,
`[2](http://example.com)`,
`[3]: http://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 13), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 2 },
{ uri, line: 3 },
);
}));
test('Should consider authority, scheme and paths when finding references to http uri', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[1](http://example.com/cat)`,
`[2](http://example.com)`,
`[3](http://example.com/dog)`,
`[4](http://example.com/cat/looong)`,
`[5](http://example.com/cat)`,
`[6](http://other.com/cat)`,
`[7](https://example.com/cat)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 13), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 4 },
);
}));
test('Should support finding references to http uri across files', withStore(async (store) => {
const uri1 = workspacePath('doc.md');
const uri2 = workspacePath('doc2.md');
const doc = new InMemoryDocument(uri1, joinLines(
`[1](http://example.com)`,
`[3]: http://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(uri2, joinLines(
`[other](http://example.com)`
))
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 13), workspace);
assertReferencesEqual(refs!,
{ uri: uri1, line: 0 },
{ uri: uri1, line: 1 },
{ uri: uri2, line: 0 },
);
}));
test('Should support finding references to autolinked http links', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[1](http://example.com)`,
`<http://example.com>`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 13), workspace);
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 1 },
);
}));
test('Should distinguish between references to file and to header within file', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const other1Uri = workspacePath('sub', 'other.md');
const doc = new InMemoryDocument(docUri, joinLines(
`# abc`,
``,
`[link 1](#abc)`,
));
const otherDoc = new InMemoryDocument(other1Uri, joinLines(
`[link](/doc.md#abc)`,
`[link no text](/doc#abc)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
otherDoc,
]));
{
// Check refs to header fragment
const headerRefs = await getReferences(store, otherDoc, new vscode.Position(0, 16), workspace);
assertReferencesEqual(headerRefs,
{ uri: docUri, line: 0 }, // Header definition
{ uri: docUri, line: 2 },
{ uri: other1Uri, line: 0 },
{ uri: other1Uri, line: 1 },
);
}
{
// Check refs to file itself from link with ext
const fileRefs = await getReferences(store, otherDoc, new vscode.Position(0, 9), workspace);
assertReferencesEqual(fileRefs,
{ uri: other1Uri, line: 0, endCharacter: 14 },
{ uri: other1Uri, line: 1, endCharacter: 19 },
);
}
{
// Check refs to file itself from link without ext
const fileRefs = await getReferences(store, otherDoc, new vscode.Position(1, 17), workspace);
assertReferencesEqual(fileRefs,
{ uri: other1Uri, line: 0 },
{ uri: other1Uri, line: 1 },
);
}
}));
test('Should support finding references to unknown file', withStore(async (store) => {
const uri1 = workspacePath('doc1.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`![img](/images/more/image.png)`,
``,
`[ref]: /images/more/image.png`,
));
const uri2 = workspacePath('sub', 'doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines(
`![img](/images/more/image.png)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2]));
const refs = await getReferences(store, doc1, new vscode.Position(0, 10), workspace);
assertReferencesEqual(refs!,
{ uri: uri1, line: 0 },
{ uri: uri1, line: 2 },
{ uri: uri2, line: 0 },
);
}));
suite('Reference links', () => {
test('Should find reference links within file from link', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`, // trigger here
``,
`[abc]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 12), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
);
}));
test('Should find reference links using shorthand', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[ref]`, // trigger 1
``,
`[yes][ref]`, // trigger 2
``,
`[ref]: /Hello.md` // trigger 3
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
{
const refs = await getReferences(store, doc, new vscode.Position(0, 2), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
{ uri: docUri, line: 4 },
);
}
{
const refs = await getReferences(store, doc, new vscode.Position(2, 7), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
{ uri: docUri, line: 4 },
);
}
{
const refs = await getReferences(store, doc, new vscode.Position(4, 2), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
{ uri: docUri, line: 4 },
);
}
}));
test('Should find reference links within file from definition', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com`, // trigger here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(2, 3), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
);
}));
test('Should not find reference links across files', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(workspacePath('other.md'), joinLines(
`[link 1][abc]`,
``,
`[abc]: https://example.com?bad`
))
]));
const refs = await getReferences(store, doc, new vscode.Position(0, 12), workspace);
assertReferencesEqual(refs!,
{ uri: docUri, line: 0 },
{ uri: docUri, line: 2 },
);
}));
test('Should not consider checkboxes as reference links', withStore(async (store) => {
const docUri = workspacePath('doc.md');
const doc = new InMemoryDocument(docUri, joinLines(
`- [x]`,
`- [X]`,
`- [ ]`,
``,
`[x]: https://example.com`
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const refs = await getReferences(store, doc, new vscode.Position(0, 4), workspace);
assert.strictEqual(refs?.length!, 0);
}));
});
});

View file

@ -1,720 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { MdReferencesProvider } from '../languageFeatures/references';
import { MdVsCodeRenameProvider, MdWorkspaceEdit } from '../languageFeatures/rename';
import { githubSlugifier } from '../slugify';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { noopToken } from '../util/cancellation';
import { DisposableStore } from '../util/dispose';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { IMdWorkspace } from '../workspace';
import { createNewMarkdownEngine } from './engine';
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
import { nulLogger } from './nulLogging';
import { assertRangeEqual, joinLines, withStore, workspacePath } from './util';
/**
* Get prepare rename info.
*/
function prepareRename(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, workspace: IMdWorkspace): Promise<undefined | { readonly range: vscode.Range; readonly placeholder: string }> {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const referenceComputer = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
const renameProvider = store.add(new MdVsCodeRenameProvider(workspace, referenceComputer, githubSlugifier));
return renameProvider.prepareRename(doc, pos, noopToken);
}
/**
* Get all the edits for the rename.
*/
function getRenameEdits(store: DisposableStore, doc: InMemoryDocument, pos: vscode.Position, newName: string, workspace: IMdWorkspace): Promise<MdWorkspaceEdit | undefined> {
const engine = createNewMarkdownEngine();
const tocProvider = store.add(new MdTableOfContentsProvider(engine, workspace, nulLogger));
const referencesProvider = store.add(new MdReferencesProvider(engine, workspace, tocProvider, nulLogger));
const renameProvider = store.add(new MdVsCodeRenameProvider(workspace, referencesProvider, githubSlugifier));
return renameProvider.provideRenameEditsImpl(doc, pos, newName, noopToken);
}
interface ExpectedTextEdit {
readonly uri: vscode.Uri;
readonly edits: readonly vscode.TextEdit[];
}
interface ExpectedFileRename {
readonly originalUri: vscode.Uri;
readonly newUri: vscode.Uri;
}
function assertEditsEqual(actualEdit: MdWorkspaceEdit, ...expectedEdits: ReadonlyArray<ExpectedTextEdit | ExpectedFileRename>) {
// Check file renames
const expectedFileRenames = expectedEdits.filter(expected => 'originalUri' in expected) as ExpectedFileRename[];
const actualFileRenames = actualEdit.fileRenames ?? [];
assert.strictEqual(actualFileRenames.length, expectedFileRenames.length, `File rename count should match`);
for (let i = 0; i < actualFileRenames.length; ++i) {
const expected = expectedFileRenames[i];
const actual = actualFileRenames[i];
assert.strictEqual(actual.from.toString(), expected.originalUri.toString(), `File rename '${i}' should have expected 'from' resource`);
assert.strictEqual(actual.to.toString(), expected.newUri.toString(), `File rename '${i}' should have expected 'to' resource`);
}
// Check text edits
const actualTextEdits = actualEdit.edit.entries();
const expectedTextEdits = expectedEdits.filter(expected => 'edits' in expected) as ExpectedTextEdit[];
assert.strictEqual(actualTextEdits.length, expectedTextEdits.length, `Reference counts should match`);
for (let i = 0; i < actualTextEdits.length; ++i) {
const expected = expectedTextEdits[i];
const actual = actualTextEdits[i];
if ('edits' in expected) {
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('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 #', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# abc`
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 0), workspace);
assertRangeEqual(info!.range, new vscode.Range(0, 2, 0, 5));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 0), "New Header", workspace);
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', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`### abc ###`
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 0), workspace);
assertRangeEqual(info!.range, new vscode.Range(0, 4, 0, 7));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 0), "New Header", workspace);
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', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`, // rename here
`[text](#a-b-c)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 0), "New Header", workspace);
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', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`,
`[text](#a-b-c)`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(1, 10), "New Header", workspace);
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', withStore(async (store) => {
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 workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(2, 10), "New Header", workspace);
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'),
]
});
}));
test('Rename on header should pick up links across files', withStore(async (store) => {
const uri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`, // rename here
`[text](#a-b-c)`,
));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 0), "New Header", new InMemoryMdWorkspace([
doc,
new InMemoryDocument(otherUri, joinLines(
`[text](#a-b-c)`, // Should not find this
`[text](./doc.md#a-b-c)`, // But should find this
`[text](./doc#a-b-c)`, // And this
))
]));
assertEditsEqual(edit!, {
uri: 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'),
]
}, {
uri: otherUri, edits: [
new vscode.TextEdit(new vscode.Range(1, 16, 1, 21), 'new-header'),
new vscode.TextEdit(new vscode.Range(2, 13, 2, 18), 'new-header'),
]
});
}));
test('Rename on link should pick up links across files', withStore(async (store) => {
const uri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`,
`[text](#a-b-c)`, // rename here
));
const edit = await getRenameEdits(store, doc, new vscode.Position(1, 10), "New Header", new InMemoryMdWorkspace([
doc,
new InMemoryDocument(otherUri, joinLines(
`[text](#a-b-c)`, // Should not find this
`[text](./doc.md#a-b-c)`, // But should find this
`[text](./doc#a-b-c)`, // And this
))
]));
assertEditsEqual(edit!, {
uri: 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'),
]
}, {
uri: otherUri, edits: [
new vscode.TextEdit(new vscode.Range(1, 16, 1, 21), 'new-header'),
new vscode.TextEdit(new vscode.Range(2, 13, 2, 18), 'new-header'),
]
});
}));
test('Rename on link in other file should pick up all refs', withStore(async (store) => {
const uri = workspacePath('doc.md');
const otherUri = workspacePath('other.md');
const doc = new InMemoryDocument(uri, joinLines(
`### A b C`,
`[text](#a-b-c)`,
));
const otherDoc = new InMemoryDocument(otherUri, joinLines(
`[text](#a-b-c)`,
`[text](./doc.md#a-b-c)`,
`[text](./doc#a-b-c)`
));
const expectedEdits = [
{
uri: 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'),
]
}, {
uri: otherUri, edits: [
new vscode.TextEdit(new vscode.Range(1, 16, 1, 21), 'new-header'),
new vscode.TextEdit(new vscode.Range(2, 13, 2, 18), 'new-header'),
]
}
];
{
// Rename on header with file extension
const edit = await getRenameEdits(store, otherDoc, new vscode.Position(1, 17), "New Header", new InMemoryMdWorkspace([
doc,
otherDoc
]));
assertEditsEqual(edit!, ...expectedEdits);
}
{
// Rename on header without extension
const edit = await getRenameEdits(store, otherDoc, new vscode.Position(2, 15), "New Header", new InMemoryMdWorkspace([
doc,
otherDoc
]));
assertEditsEqual(edit!, ...expectedEdits);
}
}));
test('Rename on reference should rename references and definition', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text][ref]`, // rename here
`[other][ref]`,
``,
`[ref]: https://example.com`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 8), "new ref", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 10), 'new ref'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 11), 'new ref'),
new vscode.TextEdit(new vscode.Range(3, 1, 3, 4), 'new ref'),
]
});
}));
test('Rename on definition should rename references and definitions', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text][ref]`,
`[other][ref]`,
``,
`[ref]: https://example.com`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(3, 3), "new ref", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 10), 'new ref'),
new vscode.TextEdit(new vscode.Range(1, 8, 1, 11), 'new ref'),
new vscode.TextEdit(new vscode.Range(3, 1, 3, 4), 'new ref'),
]
});
}));
test('Rename on definition entry should rename header and references', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# a B c`,
`[ref text][ref]`,
`[direct](#a-b-c)`,
`[ref]: #a-b-c`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const preparedInfo = await prepareRename(store, doc, new vscode.Position(3, 10), workspace);
assert.strictEqual(preparedInfo!.placeholder, 'a B c');
assertRangeEqual(preparedInfo!.range, new vscode.Range(3, 8, 3, 13));
const edit = await getRenameEdits(store, doc, new vscode.Position(3, 10), "x Y z", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 2, 0, 7), 'x Y z'),
new vscode.TextEdit(new vscode.Range(2, 10, 2, 15), 'x-y-z'),
new vscode.TextEdit(new vscode.Range(3, 8, 3, 13), 'x-y-z'),
]
});
}));
test('Rename should not be supported on link text', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`# Header`,
`[text](#header)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
await assert.rejects(prepareRename(store, doc, new vscode.Position(1, 2), workspace));
}));
test('Path rename should use file path as range', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](./doc.md)`,
`[ref]: ./doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 10), workspace);
assert.strictEqual(info!.placeholder, './doc.md');
assertRangeEqual(info!.range, new vscode.Range(0, 7, 0, 15));
}));
test('Path rename\'s range should excludes fragment', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](./doc.md#some-header)`,
`[ref]: ./doc.md#some-header`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 10), workspace);
assert.strictEqual(info!.placeholder, './doc.md');
assertRangeEqual(info!.range, new vscode.Range(0, 7, 0, 15));
}));
test('Path rename should update file and all refs', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](./doc.md)`,
`[ref]: ./doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 10), './sub/newDoc.md', workspace);
assertEditsEqual(edit!, {
originalUri: uri,
newUri: workspacePath('sub', 'newDoc.md'),
}, {
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 15), './sub/newDoc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 15), './sub/newDoc.md'),
]
});
}));
test('Path rename using absolute file path should anchor to workspace root', withStore(async (store) => {
const uri = workspacePath('sub', 'doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](/sub/doc.md)`,
`[ref]: /sub/doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 10), '/newSub/newDoc.md', workspace);
assertEditsEqual(edit!, {
originalUri: uri,
newUri: workspacePath('newSub', 'newDoc.md'),
}, {
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/newSub/newDoc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 18), '/newSub/newDoc.md'),
]
});
}));
test('Path rename should use un-encoded paths as placeholder', withStore(async (store) => {
const uri = workspacePath('sub', 'doc with spaces.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](/sub/doc%20with%20spaces.md)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(0, 10), workspace);
assert.strictEqual(info!.placeholder, '/sub/doc with spaces.md');
}));
test('Path rename should encode paths', withStore(async (store) => {
const uri = workspacePath('sub', 'doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](/sub/doc.md)`,
`[ref]: /sub/doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 10), '/NEW sub/new DOC.md', workspace);
assertEditsEqual(edit!, {
originalUri: uri,
newUri: workspacePath('NEW sub', 'new DOC.md'),
}, {
uri: uri, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/NEW%20sub/new%20DOC.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 18), '/NEW%20sub/new%20DOC.md'),
]
});
}));
test('Path rename should work with unknown files', withStore(async (store) => {
const uri1 = workspacePath('doc1.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`![img](/images/more/image.png)`,
``,
`[ref]: /images/more/image.png`,
));
const uri2 = workspacePath('sub', 'doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines(
`![img](/images/more/image.png)`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc1,
doc2
]));
const edit = await getRenameEdits(store, doc1, new vscode.Position(0, 10), '/img/test/new.png', workspace);
assertEditsEqual(edit!,
// Should not have file edits since the files don't exist here
{
uri: uri1, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 29), '/img/test/new.png'),
new vscode.TextEdit(new vscode.Range(2, 7, 2, 29), '/img/test/new.png'),
]
},
{
uri: uri2, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 29), '/img/test/new.png'),
]
});
}));
test('Path rename should use .md extension on extension-less link', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[text](/doc#header)`,
`[ref]: /doc#other`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const edit = await getRenameEdits(store, doc, new vscode.Position(0, 10), '/new File', workspace);
assertEditsEqual(edit!, {
originalUri: uri,
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'), // Links should continue to use extension-less paths
new vscode.TextEdit(new vscode.Range(1, 7, 1, 11), '/new%20File'),
]
});
}));
// TODO: fails on windows
test.skip('Path rename should use correctly resolved paths across files', withStore(async (store) => {
const uri1 = workspacePath('sub', 'doc.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`[text](./doc.md)`,
`[ref]: ./doc.md`,
));
const uri2 = workspacePath('doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines(
`[text](./sub/doc.md)`,
`[ref]: ./sub/doc.md`,
));
const uri3 = workspacePath('sub2', 'doc3.md');
const doc3 = new InMemoryDocument(uri3, joinLines(
`[text](../sub/doc.md)`,
`[ref]: ../sub/doc.md`,
));
const uri4 = workspacePath('sub2', 'doc4.md');
const doc4 = new InMemoryDocument(uri4, joinLines(
`[text](/sub/doc.md)`,
`[ref]: /sub/doc.md`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc1, doc2, doc3, doc4,
]));
const edit = await getRenameEdits(store, doc1, new vscode.Position(0, 10), './new/new-doc.md', workspace);
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'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 15), './new/new-doc.md'),
]
}, {
uri: uri2, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 19), './sub/new/new-doc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 19), './sub/new/new-doc.md'),
]
}, {
uri: uri3, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 20), '../sub/new/new-doc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 20), '../sub/new/new-doc.md'),
]
}, {
uri: uri4, edits: [
new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/sub/new/new-doc.md'),
new vscode.TextEdit(new vscode.Range(1, 7, 1, 18), '/sub/new/new-doc.md'),
]
});
}));
test('Path rename should resolve on links without prefix', withStore(async (store) => {
const uri1 = workspacePath('sub', 'doc.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`![text](sub2/doc3.md)`,
));
const uri2 = workspacePath('doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines(
`![text](sub/sub2/doc3.md)`,
));
const uri3 = workspacePath('sub', 'sub2', 'doc3.md');
const doc3 = new InMemoryDocument(uri3, joinLines());
const workspace = store.add(new InMemoryMdWorkspace([
doc1, doc2, doc3
]));
const edit = await getRenameEdits(store, doc1, new vscode.Position(0, 10), 'sub2/cat.md', workspace);
assertEditsEqual(edit!, {
originalUri: workspacePath('sub', 'sub2', 'doc3.md'),
newUri: workspacePath('sub', 'sub2', 'cat.md'),
}, {
uri: uri1, edits: [new vscode.TextEdit(new vscode.Range(0, 8, 0, 20), 'sub2/cat.md')]
}, {
uri: uri2, edits: [new vscode.TextEdit(new vscode.Range(0, 8, 0, 24), 'sub/sub2/cat.md')]
});
}));
test('Rename on link should use header text as placeholder', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`### a B c ###`,
`[text](#a-b-c)`,
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const info = await prepareRename(store, doc, new vscode.Position(1, 10), workspace);
assert.strictEqual(info!.placeholder, 'a B c');
assertRangeEqual(info!.range, new vscode.Range(1, 8, 1, 13));
}));
test('Rename on http uri should work', withStore(async (store) => {
const uri1 = workspacePath('doc.md');
const uri2 = workspacePath('doc2.md');
const doc = new InMemoryDocument(uri1, joinLines(
`[1](http://example.com)`,
`[2]: http://example.com`,
`<http://example.com>`,
));
const workspace = store.add(new InMemoryMdWorkspace([
doc,
new InMemoryDocument(uri2, joinLines(
`[4](http://example.com)`
))
]));
const edit = await getRenameEdits(store, doc, new vscode.Position(1, 10), "https://example.com/sub", workspace);
assertEditsEqual(edit!, {
uri: uri1, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 22), 'https://example.com/sub'),
new vscode.TextEdit(new vscode.Range(1, 5, 1, 23), 'https://example.com/sub'),
new vscode.TextEdit(new vscode.Range(2, 1, 2, 19), 'https://example.com/sub'),
]
}, {
uri: uri2, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 22), 'https://example.com/sub'),
]
});
}));
test('Rename on definition path should update all references to path', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[ref text][ref]`,
`[direct](/file)`,
`[ref]: /file`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const preparedInfo = await prepareRename(store, doc, new vscode.Position(2, 10), workspace);
assert.strictEqual(preparedInfo!.placeholder, '/file');
assertRangeEqual(preparedInfo!.range, new vscode.Range(2, 7, 2, 12));
const edit = await getRenameEdits(store, doc, new vscode.Position(2, 10), "/newFile", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(1, 9, 1, 14), '/newFile'),
new vscode.TextEdit(new vscode.Range(2, 7, 2, 12), '/newFile'),
]
});
}));
test('Rename on definition path where file exists should also update file', withStore(async (store) => {
const uri1 = workspacePath('doc.md');
const doc1 = new InMemoryDocument(uri1, joinLines(
`[ref text][ref]`,
`[direct](/doc2)`,
`[ref]: /doc2`, // rename here
));
const uri2 = workspacePath('doc2.md');
const doc2 = new InMemoryDocument(uri2, joinLines());
const workspace = store.add(new InMemoryMdWorkspace([doc1, doc2]));
const preparedInfo = await prepareRename(store, doc1, new vscode.Position(2, 10), workspace);
assert.strictEqual(preparedInfo!.placeholder, '/doc2');
assertRangeEqual(preparedInfo!.range, new vscode.Range(2, 7, 2, 12));
const edit = await getRenameEdits(store, doc1, new vscode.Position(2, 10), "/new-doc", workspace);
assertEditsEqual(edit!, {
uri: uri1, edits: [
new vscode.TextEdit(new vscode.Range(1, 9, 1, 14), '/new-doc'),
new vscode.TextEdit(new vscode.Range(2, 7, 2, 12), '/new-doc'),
]
}, {
originalUri: uri2,
newUri: workspacePath('new-doc.md')
});
}));
test('Rename on definition path header should update all references to header', withStore(async (store) => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[ref text][ref]`,
`[direct](/file#header)`,
`[ref]: /file#header`, // rename here
));
const workspace = store.add(new InMemoryMdWorkspace([doc]));
const preparedInfo = await prepareRename(store, doc, new vscode.Position(2, 16), workspace);
assert.strictEqual(preparedInfo!.placeholder, 'header');
assertRangeEqual(preparedInfo!.range, new vscode.Range(2, 13, 2, 19));
const edit = await getRenameEdits(store, doc, new vscode.Position(2, 16), "New Header", workspace);
assertEditsEqual(edit!, {
uri, edits: [
new vscode.TextEdit(new vscode.Range(1, 15, 1, 21), 'new-header'),
new vscode.TextEdit(new vscode.Range(2, 13, 2, 19), 'new-header'),
]
});
}));
});

View file

@ -242,6 +242,11 @@ vscode-languageserver-types@3.17.1:
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16"
integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ==
vscode-languageserver-types@^3.17.2:
version "3.17.2"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz#b2c2e7de405ad3d73a883e91989b850170ffc4f2"
integrity sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==
vscode-nls@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840"