mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
Move md path completions and document links to language server (#155100)
This commit is contained in:
parent
06443bcc10
commit
bec36ce756
|
@ -10,17 +10,16 @@
|
|||
"main": "./out/node/main",
|
||||
"browser": "./dist/browser/main",
|
||||
"dependencies": {
|
||||
"vscode-languageserver": "^8.0.2-next.4",
|
||||
"vscode-uri": "^3.0.3",
|
||||
"vscode-languageserver": "^8.0.2-next.5`",
|
||||
"vscode-languageserver-textdocument": "^1.0.5",
|
||||
"vscode-languageserver-types": "^3.17.1",
|
||||
"vscode-markdown-languageservice": "microsoft/vscode-markdown-languageservice"
|
||||
"vscode-markdown-languageservice": "^0.0.0-alpha.5",
|
||||
"vscode-uri": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "16.x"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "cd node_modules/vscode-markdown-languageservice && yarn run compile-ext",
|
||||
"compile": "gulp compile-extension:markdown-language-features-server",
|
||||
"watch": "gulp watch-extension:markdown-language-features-server"
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
import { RequestType } from 'vscode-languageserver';
|
||||
import * as md from 'vscode-markdown-languageservice';
|
||||
|
||||
declare const TextDecoder: any;
|
||||
|
||||
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');
|
||||
|
|
|
@ -3,27 +3,35 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Connection, InitializeParams, InitializeResult, TextDocuments } from 'vscode-languageserver';
|
||||
import { 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 { LogFunctionLogger } from './logging';
|
||||
import { parseRequestType } from './protocol';
|
||||
import { VsCodeClientWorkspace } from './workspace';
|
||||
|
||||
declare const TextDecoder: any;
|
||||
|
||||
export function startServer(connection: Connection) {
|
||||
export async function startServer(connection: Connection) {
|
||||
const documents = new TextDocuments(TextDocument);
|
||||
documents.listen(connection);
|
||||
const notebooks = new NotebookDocuments(documents);
|
||||
|
||||
connection.onInitialize((_params: InitializeParams): InitializeResult => {
|
||||
connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
workspace.workspaceFolders = (params.workspaceFolders ?? []).map(x => URI.parse(x.uri));
|
||||
return {
|
||||
capabilities: {
|
||||
documentLinkProvider: { resolveProvider: true },
|
||||
documentSymbolProvider: true,
|
||||
completionProvider: { triggerCharacters: ['.', '/', '#'] },
|
||||
foldingRangeProvider: true,
|
||||
selectionRangeProvider: true,
|
||||
workspaceSymbolProvider: true,
|
||||
workspace: {
|
||||
workspaceFolders: {
|
||||
supported: true,
|
||||
changeNotifications: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -36,15 +44,36 @@ export function startServer(connection: Connection) {
|
|||
}
|
||||
};
|
||||
|
||||
const workspace = new VsCodeClientWorkspace(connection, documents);
|
||||
const workspace = new VsCodeClientWorkspace(connection, documents, notebooks);
|
||||
const logger = new LogFunctionLogger(connection.console.log.bind(connection.console));
|
||||
const provider = md.createLanguageService({ workspace, parser, logger });
|
||||
|
||||
connection.onDocumentLinks(async (params, token): Promise<lsp.DocumentLink[]> => {
|
||||
try {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return await provider.getDocumentLinks(document, token);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
connection.onDocumentLinkResolve(async (link, token): Promise<lsp.DocumentLink | undefined> => {
|
||||
try {
|
||||
return await provider.resolveDocumentLink(link, token);
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
connection.onDocumentSymbol(async (params, token): Promise<lsp.DocumentSymbol[]> => {
|
||||
try {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return await provider.provideDocumentSymbols(document, token);
|
||||
return await provider.getDocumentSymbols(document, token);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
|
@ -56,7 +85,7 @@ export function startServer(connection: Connection) {
|
|||
try {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return await provider.provideFoldingRanges(document, token);
|
||||
return await provider.getFoldingRanges(document, token);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
|
@ -68,7 +97,7 @@ export function startServer(connection: Connection) {
|
|||
try {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return await provider.provideSelectionRanges(document, params.positions, token);
|
||||
return await provider.getSelectionRanges(document, params.positions, token);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
|
@ -78,13 +107,26 @@ export function startServer(connection: Connection) {
|
|||
|
||||
connection.onWorkspaceSymbol(async (params, token): Promise<lsp.WorkspaceSymbol[]> => {
|
||||
try {
|
||||
return await provider.provideWorkspaceSymbols(params.query, token);
|
||||
return await provider.getWorkspaceSymbols(params.query, token);
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
connection.onCompletion(async (params, token): Promise<lsp.CompletionItem[]> => {
|
||||
try {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return await provider.getCompletionItems(document, params.position, params.context!, token);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e.stack);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
documents.listen(connection);
|
||||
notebooks.listen(connection);
|
||||
connection.listen();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const Schemes = Object.freeze({
|
||||
notebookCell: 'vscode-notebook-cell',
|
||||
});
|
|
@ -3,15 +3,17 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Connection, Emitter, FileChangeType, TextDocuments } from 'vscode-languageserver';
|
||||
import { Connection, Emitter, FileChangeType, NotebookDocuments, TextDocuments } from 'vscode-languageserver';
|
||||
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 * as protocol from './protocol';
|
||||
import { coalesce } from './util/arrays';
|
||||
import { isMarkdownDocument, looksLikeMarkdownPath } from './util/file';
|
||||
import { Limiter } from './util/limiter';
|
||||
import { ResourceMap } from './util/resourceMap';
|
||||
import { Schemes } from './util/schemes';
|
||||
|
||||
declare const TextDecoder: any;
|
||||
|
||||
|
@ -33,6 +35,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
|
|||
constructor(
|
||||
private readonly connection: Connection,
|
||||
private readonly documents: TextDocuments<TextDocument>,
|
||||
private readonly notebooks: NotebookDocuments<TextDocument>,
|
||||
) {
|
||||
documents.onDidOpen(e => {
|
||||
this._documentCache.delete(URI.parse(e.document.uri));
|
||||
|
@ -57,14 +60,14 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
|
|||
switch (change.type) {
|
||||
case FileChangeType.Changed: {
|
||||
this._documentCache.delete(resource);
|
||||
const document = await this.getOrLoadMarkdownDocument(resource);
|
||||
const document = await this.openMarkdownDocument(resource);
|
||||
if (document) {
|
||||
this._onDidChangeMarkdownDocument.fire(document);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FileChangeType.Created: {
|
||||
const document = await this.getOrLoadMarkdownDocument(resource);
|
||||
const document = await this.openMarkdownDocument(resource);
|
||||
if (document) {
|
||||
this._onDidCreateMarkdownDocument.fire(document);
|
||||
}
|
||||
|
@ -80,6 +83,22 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
|
|||
});
|
||||
}
|
||||
|
||||
public listen() {
|
||||
this.connection.workspace.onDidChangeWorkspaceFolders(async () => {
|
||||
this.workspaceFolders = (await this.connection.workspace.getWorkspaceFolders() ?? []).map(x => URI.parse(x.uri));
|
||||
});
|
||||
}
|
||||
|
||||
private _workspaceFolders: readonly URI[] = [];
|
||||
|
||||
get workspaceFolders(): readonly URI[] {
|
||||
return this._workspaceFolders;
|
||||
}
|
||||
|
||||
set workspaceFolders(value: readonly URI[]) {
|
||||
this._workspaceFolders = value;
|
||||
}
|
||||
|
||||
async getAllMarkdownDocuments(): Promise<Iterable<md.ITextDocument>> {
|
||||
const maxConcurrent = 20;
|
||||
|
||||
|
@ -91,7 +110,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
|
|||
const onDiskResults = await Promise.all(resources.map(strResource => {
|
||||
return limiter.queue(async () => {
|
||||
const resource = URI.parse(strResource);
|
||||
const doc = await this.getOrLoadMarkdownDocument(resource);
|
||||
const doc = await this.openMarkdownDocument(resource);
|
||||
if (doc) {
|
||||
foundFiles.set(resource);
|
||||
}
|
||||
|
@ -110,7 +129,7 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
|
|||
return !!this.documents.get(resource.toString());
|
||||
}
|
||||
|
||||
async getOrLoadMarkdownDocument(resource: URI): Promise<md.ITextDocument | undefined> {
|
||||
async openMarkdownDocument(resource: URI): Promise<md.ITextDocument | undefined> {
|
||||
const existing = this._documentCache.get(resource);
|
||||
if (existing) {
|
||||
return existing;
|
||||
|
@ -141,12 +160,25 @@ export class VsCodeClientWorkspace implements md.IWorkspace {
|
|||
}
|
||||
}
|
||||
|
||||
async pathExists(_resource: URI): Promise<boolean> {
|
||||
return false;
|
||||
stat(resource: URI): Promise<md.FileStat | undefined> {
|
||||
return this.connection.sendRequest(protocol.statFileRequestType, { uri: resource.toString() });
|
||||
}
|
||||
|
||||
async readDirectory(_resource: URI): Promise<[string, { isDir: boolean }][]> {
|
||||
return [];
|
||||
async readDirectory(resource: URI): Promise<[string, md.FileStat][]> {
|
||||
return this.connection.sendRequest(protocol.readDirectoryRequestType, { uri: resource.toString() });
|
||||
}
|
||||
|
||||
getContainingDocument(resource: URI): ContainingDocumentContext | undefined {
|
||||
if (resource.scheme === Schemes.notebookCell) {
|
||||
const nb = this.notebooks.findNotebookDocumentForCell(resource.toString());
|
||||
if (nb) {
|
||||
return {
|
||||
uri: URI.parse(nb.uri),
|
||||
children: nb.cells.map(cell => ({ uri: URI.parse(cell.document) })),
|
||||
};
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private isRelevantMarkdownDocument(doc: TextDocument) {
|
||||
|
|
|
@ -35,17 +35,19 @@ 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@^8.0.2-next.4:
|
||||
vscode-languageserver@^8.0.2-next.5`:
|
||||
version "8.0.2-next.5"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.0.2-next.5.tgz#39a2dd4c504fb88042375e7ac706a714bdaab4e5"
|
||||
integrity sha512-2ZDb7O/4atS9mJKufPPz637z+51kCyZfgnobFW5eSrUdS3c0UB/nMS4Ng1EavYTX84GVaVMKCrmP0f2ceLmR0A==
|
||||
dependencies:
|
||||
vscode-languageserver-protocol "3.17.2-next.6"
|
||||
|
||||
vscode-markdown-languageservice@microsoft/vscode-markdown-languageservice:
|
||||
version "0.0.0-alpha.2"
|
||||
resolved "https://codeload.github.com/microsoft/vscode-markdown-languageservice/tar.gz/db497ada376aae9a335519dbfb406c6a1f873446"
|
||||
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==
|
||||
dependencies:
|
||||
vscode-languageserver-textdocument "^1.0.5"
|
||||
vscode-languageserver-types "^3.17.1"
|
||||
vscode-uri "^3.0.3"
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseLanguageClient, LanguageClientOptions, RequestType } from 'vscode-languageclient';
|
||||
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType, RequestType } from 'vscode-languageclient';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { IMdParser } from './markdownEngine';
|
||||
import { markdownFileExtensions } from './util/file';
|
||||
|
@ -14,9 +14,9 @@ import { IMdWorkspace } from './workspace';
|
|||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const parseRequestType: RequestType<{ uri: string }, Token[], any> = new RequestType('markdown/parse');
|
||||
|
||||
const readFileRequestType: RequestType<{ uri: string }, number[], any> = new RequestType('markdown/readFile');
|
||||
|
||||
const statFileRequestType: RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any> = new RequestType('markdown/statFile');
|
||||
const readDirectoryRequestType: RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any> = new RequestType('markdown/readDirectory');
|
||||
const findFilesRequestTypes: RequestType<{}, string[], any> = new RequestType('markdown/findFiles');
|
||||
|
||||
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
|
||||
|
@ -33,13 +33,25 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
|
|||
configurationSection: ['markdown'],
|
||||
fileEvents: vscode.workspace.createFileSystemWatcher(mdFileGlob),
|
||||
},
|
||||
initializationOptions: {}
|
||||
};
|
||||
|
||||
const client = factory('markdown', localize('markdownServer.name', 'Markdown Language Server'), clientOptions);
|
||||
|
||||
client.registerProposedFeatures();
|
||||
|
||||
const notebookFeature = client.getFeature(NotebookDocumentSyncRegistrationType.method);
|
||||
if (notebookFeature !== undefined) {
|
||||
notebookFeature.register({
|
||||
id: String(Date.now()),
|
||||
registerOptions: {
|
||||
notebookSelector: [{
|
||||
notebook: '*',
|
||||
cells: [{ language: 'markdown' }]
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
client.onRequest(parseRequestType, async (e) => {
|
||||
const uri = vscode.Uri.parse(e.uri);
|
||||
const doc = await workspace.getOrLoadMarkdownDocument(uri);
|
||||
|
@ -55,6 +67,22 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
|
|||
return Array.from(await vscode.workspace.fs.readFile(uri));
|
||||
});
|
||||
|
||||
client.onRequest(statFileRequestType, async (e): Promise<{ isDirectory: boolean } | undefined> => {
|
||||
const uri = vscode.Uri.parse(e.uri);
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(uri);
|
||||
return { isDirectory: stat.type === vscode.FileType.Directory };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
client.onRequest(readDirectoryRequestType, async (e): Promise<[string, { isDirectory: boolean }][]> => {
|
||||
const uri = vscode.Uri.parse(e.uri);
|
||||
const result = await vscode.workspace.fs.readDirectory(uri);
|
||||
return result.map(([name, type]) => [name, { isDirectory: type === vscode.FileType.Directory }]);
|
||||
});
|
||||
|
||||
client.onRequest(findFilesRequestTypes, async (): Promise<string[]> => {
|
||||
return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString());
|
||||
});
|
||||
|
|
|
@ -9,10 +9,9 @@ import * as commands from './commands/index';
|
|||
import { registerPasteSupport } from './languageFeatures/copyPaste';
|
||||
import { registerDefinitionSupport } from './languageFeatures/definitions';
|
||||
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
|
||||
import { MdLinkProvider, registerDocumentLinkSupport } from './languageFeatures/documentLinks';
|
||||
import { MdLinkProvider } from './languageFeatures/documentLinks';
|
||||
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
|
||||
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
|
||||
import { registerPathCompletionSupport } from './languageFeatures/pathCompletions';
|
||||
import { MdReferencesProvider, registerReferencesSupport } from './languageFeatures/references';
|
||||
import { registerRenameSupport } from './languageFeatures/rename';
|
||||
import { ILogger } from './logging';
|
||||
|
@ -73,11 +72,9 @@ function registerMarkdownLanguageFeatures(
|
|||
// Language features
|
||||
registerDefinitionSupport(selector, referencesProvider),
|
||||
registerDiagnosticSupport(selector, workspace, linkProvider, commandManager, referencesProvider, tocProvider, logger),
|
||||
registerDocumentLinkSupport(selector, linkProvider),
|
||||
registerDropIntoEditorSupport(selector),
|
||||
registerFindFileReferenceSupport(commandManager, referencesProvider),
|
||||
registerPasteSupport(selector),
|
||||
registerPathCompletionSupport(selector, workspace, parser, linkProvider),
|
||||
registerReferencesSupport(selector, referencesProvider),
|
||||
registerRenameSupport(selector, workspace, referencesProvider, parser.slugifier),
|
||||
);
|
||||
|
|
|
@ -4,21 +4,16 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as uri from 'vscode-uri';
|
||||
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
|
||||
import { ILogger } from '../logging';
|
||||
import { IMdParser } from '../markdownEngine';
|
||||
import { getLine, ITextDocument } from '../types/textDocument';
|
||||
import { coalesce } from '../util/arrays';
|
||||
import { noopToken } from '../util/cancellation';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { Schemes } from '../util/schemes';
|
||||
import { MdDocumentInfoCache } from '../util/workspaceCache';
|
||||
import { IMdWorkspace } from '../workspace';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export interface ExternalHref {
|
||||
readonly kind: 'external';
|
||||
readonly uri: vscode.Uri;
|
||||
|
@ -543,62 +538,3 @@ export class LinkDefinitionSet implements Iterable<[string, MdLinkDefinition]> {
|
|||
return this._map.get(ref);
|
||||
}
|
||||
}
|
||||
|
||||
export class MdVsCodeLinkProvider implements vscode.DocumentLinkProvider {
|
||||
|
||||
constructor(
|
||||
private readonly _linkProvider: MdLinkProvider,
|
||||
) { }
|
||||
|
||||
public async provideDocumentLinks(
|
||||
document: ITextDocument,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.DocumentLink[]> {
|
||||
const { links, definitions } = await this._linkProvider.getLinks(document);
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return coalesce(links.map(data => this.toValidDocumentLink(data, definitions)));
|
||||
}
|
||||
|
||||
private toValidDocumentLink(link: MdLink, definitionSet: LinkDefinitionSet): vscode.DocumentLink | undefined {
|
||||
switch (link.href.kind) {
|
||||
case 'external': {
|
||||
let target = link.href.uri;
|
||||
// Normalize VS Code links to target currently running version
|
||||
if (link.href.uri.scheme === Schemes.vscode || link.href.uri.scheme === Schemes['vscode-insiders']) {
|
||||
target = target.with({ scheme: vscode.env.uriScheme });
|
||||
}
|
||||
return new vscode.DocumentLink(link.source.hrefRange, link.href.uri);
|
||||
}
|
||||
case 'internal': {
|
||||
const uri = OpenDocumentLinkCommand.createCommandUri(link.source.resource, link.href.path, link.href.fragment);
|
||||
const documentLink = new vscode.DocumentLink(link.source.hrefRange, uri);
|
||||
documentLink.tooltip = localize('documentLink.tooltip', 'Follow link');
|
||||
return documentLink;
|
||||
}
|
||||
case 'reference': {
|
||||
// We only render reference links in the editor if they are actually defined.
|
||||
// This matches how reference links are rendered by markdown-it.
|
||||
const def = definitionSet.lookup(link.href.ref);
|
||||
if (def) {
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
link.source.hrefRange,
|
||||
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([def.source.hrefRange.start.line, def.source.hrefRange.start.character]))}`));
|
||||
documentLink.tooltip = localize('documentLink.referenceTooltip', 'Go to link definition');
|
||||
return documentLink;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerDocumentLinkSupport(
|
||||
selector: vscode.DocumentSelector,
|
||||
linkProvider: MdLinkProvider,
|
||||
): vscode.Disposable {
|
||||
return vscode.languages.registerDocumentLinkProvider(selector, new MdVsCodeLinkProvider(linkProvider));
|
||||
}
|
||||
|
|
|
@ -1,369 +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 { dirname, resolve } from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { IMdParser } from '../markdownEngine';
|
||||
import { TableOfContents } from '../tableOfContents';
|
||||
import { getLine, ITextDocument } from '../types/textDocument';
|
||||
import { resolveUriToMarkdownFile } from '../util/openDocumentLink';
|
||||
import { Schemes } from '../util/schemes';
|
||||
import { IMdWorkspace } from '../workspace';
|
||||
import { MdLinkProvider } from './documentLinks';
|
||||
|
||||
enum CompletionContextKind {
|
||||
/** `[...](|)` */
|
||||
Link,
|
||||
|
||||
/** `[...][|]` */
|
||||
ReferenceLink,
|
||||
|
||||
/** `[]: |` */
|
||||
LinkDefinition,
|
||||
}
|
||||
|
||||
interface AnchorContext {
|
||||
/**
|
||||
* Link text before the `#`.
|
||||
*
|
||||
* For `[text](xy#z|abc)` this is `xy`.
|
||||
*/
|
||||
readonly beforeAnchor: string;
|
||||
|
||||
/**
|
||||
* Text of the anchor before the current position.
|
||||
*
|
||||
* For `[text](xy#z|abc)` this is `z`.
|
||||
*/
|
||||
readonly anchorPrefix: string;
|
||||
}
|
||||
|
||||
interface CompletionContext {
|
||||
readonly kind: CompletionContextKind;
|
||||
|
||||
/**
|
||||
* Text of the link before the current position
|
||||
*
|
||||
* For `[text](xy#z|abc)` this is `xy#z`.
|
||||
*/
|
||||
readonly linkPrefix: string;
|
||||
|
||||
/**
|
||||
* Position of the start of the link.
|
||||
*
|
||||
* For `[text](xy#z|abc)` this is the position before `xy`.
|
||||
*/
|
||||
readonly linkTextStartPosition: vscode.Position;
|
||||
|
||||
/**
|
||||
* Text of the link after the current position.
|
||||
*
|
||||
* For `[text](xy#z|abc)` this is `abc`.
|
||||
*/
|
||||
readonly linkSuffix: string;
|
||||
|
||||
/**
|
||||
* Info if the link looks like it is for an anchor: `[](#header)`
|
||||
*/
|
||||
readonly anchorInfo?: AnchorContext;
|
||||
|
||||
/**
|
||||
* Indicates that the completion does not require encoding.
|
||||
*/
|
||||
readonly skipEncoding?: boolean;
|
||||
}
|
||||
|
||||
function tryDecodeUriComponent(str: string): string {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds path completions in markdown files by implementing {@link vscode.CompletionItemProvider}.
|
||||
*/
|
||||
export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProvider {
|
||||
|
||||
constructor(
|
||||
private readonly workspace: IMdWorkspace,
|
||||
private readonly parser: IMdParser,
|
||||
private readonly linkProvider: MdLinkProvider,
|
||||
) { }
|
||||
|
||||
public async provideCompletionItems(document: ITextDocument, position: vscode.Position, _token: vscode.CancellationToken, _context: vscode.CompletionContext): Promise<vscode.CompletionItem[]> {
|
||||
if (!this.arePathSuggestionEnabled(document)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const context = this.getPathCompletionContext(document, position);
|
||||
if (!context) {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (context.kind) {
|
||||
case CompletionContextKind.ReferenceLink: {
|
||||
const items: vscode.CompletionItem[] = [];
|
||||
for await (const item of this.provideReferenceSuggestions(document, position, context)) {
|
||||
items.push(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
case CompletionContextKind.LinkDefinition:
|
||||
case CompletionContextKind.Link: {
|
||||
const items: vscode.CompletionItem[] = [];
|
||||
|
||||
const isAnchorInCurrentDoc = context.anchorInfo && context.anchorInfo.beforeAnchor.length === 0;
|
||||
|
||||
// Add anchor #links in current doc
|
||||
if (context.linkPrefix.length === 0 || isAnchorInCurrentDoc) {
|
||||
const insertRange = new vscode.Range(context.linkTextStartPosition, position);
|
||||
for await (const item of this.provideHeaderSuggestions(document, position, context, insertRange)) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAnchorInCurrentDoc) {
|
||||
if (context.anchorInfo) { // Anchor to a different document
|
||||
const rawUri = this.resolveReference(document, context.anchorInfo.beforeAnchor);
|
||||
if (rawUri) {
|
||||
const otherDoc = await resolveUriToMarkdownFile(this.workspace, rawUri);
|
||||
if (otherDoc) {
|
||||
const anchorStartPosition = position.translate({ characterDelta: -(context.anchorInfo.anchorPrefix.length + 1) });
|
||||
const range = new vscode.Range(anchorStartPosition, position);
|
||||
for await (const item of this.provideHeaderSuggestions(otherDoc, position, context, range)) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // Normal path suggestions
|
||||
for await (const item of this.providePathSuggestions(document, position, context)) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private arePathSuggestionEnabled(document: ITextDocument): boolean {
|
||||
const config = vscode.workspace.getConfiguration('markdown', document.uri);
|
||||
return config.get('suggest.paths.enabled', true);
|
||||
}
|
||||
|
||||
/// [...](...|
|
||||
private readonly linkStartPattern = /\[([^\]]*?)\]\(\s*(<[^\>\)]*|[^\s\(\)]*)$/;
|
||||
|
||||
/// [...][...|
|
||||
private readonly referenceLinkStartPattern = /\[([^\]]*?)\]\[\s*([^\s\(\)]*)$/;
|
||||
|
||||
/// [id]: |
|
||||
private readonly definitionPattern = /^\s*\[[\w\-]+\]:\s*([^\s]*)$/m;
|
||||
|
||||
private getPathCompletionContext(document: ITextDocument, position: vscode.Position): CompletionContext | undefined {
|
||||
const line = getLine(document, position.line);
|
||||
|
||||
const linePrefixText = line.slice(0, position.character);
|
||||
const lineSuffixText = line.slice(position.character);
|
||||
|
||||
const linkPrefixMatch = linePrefixText.match(this.linkStartPattern);
|
||||
if (linkPrefixMatch) {
|
||||
const isAngleBracketLink = linkPrefixMatch[2].startsWith('<');
|
||||
const prefix = linkPrefixMatch[2].slice(isAngleBracketLink ? 1 : 0);
|
||||
if (this.refLooksLikeUrl(prefix)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const suffix = lineSuffixText.match(/^[^\)\s][^\)\s\>]*/);
|
||||
return {
|
||||
kind: CompletionContextKind.Link,
|
||||
linkPrefix: tryDecodeUriComponent(prefix),
|
||||
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
|
||||
linkSuffix: suffix ? suffix[0] : '',
|
||||
anchorInfo: this.getAnchorContext(prefix),
|
||||
skipEncoding: isAngleBracketLink,
|
||||
};
|
||||
}
|
||||
|
||||
const definitionLinkPrefixMatch = linePrefixText.match(this.definitionPattern);
|
||||
if (definitionLinkPrefixMatch) {
|
||||
const isAngleBracketLink = definitionLinkPrefixMatch[1].startsWith('<');
|
||||
const prefix = definitionLinkPrefixMatch[1].slice(isAngleBracketLink ? 1 : 0);
|
||||
if (this.refLooksLikeUrl(prefix)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const suffix = lineSuffixText.match(/^[^\s]*/);
|
||||
return {
|
||||
kind: CompletionContextKind.LinkDefinition,
|
||||
linkPrefix: tryDecodeUriComponent(prefix),
|
||||
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
|
||||
linkSuffix: suffix ? suffix[0] : '',
|
||||
anchorInfo: this.getAnchorContext(prefix),
|
||||
skipEncoding: isAngleBracketLink,
|
||||
};
|
||||
}
|
||||
|
||||
const referenceLinkPrefixMatch = linePrefixText.match(this.referenceLinkStartPattern);
|
||||
if (referenceLinkPrefixMatch) {
|
||||
const prefix = referenceLinkPrefixMatch[2];
|
||||
const suffix = lineSuffixText.match(/^[^\]\s]*/);
|
||||
return {
|
||||
kind: CompletionContextKind.ReferenceLink,
|
||||
linkPrefix: prefix,
|
||||
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
|
||||
linkSuffix: suffix ? suffix[0] : '',
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if {@param ref} looks like a 'http:' style url.
|
||||
*/
|
||||
private refLooksLikeUrl(prefix: string): boolean {
|
||||
return /^\s*[\w\d\-]+:/.test(prefix);
|
||||
}
|
||||
|
||||
private getAnchorContext(prefix: string): AnchorContext | undefined {
|
||||
const anchorMatch = prefix.match(/^(.*)#([\w\d\-]*)$/);
|
||||
if (!anchorMatch) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
beforeAnchor: anchorMatch[1],
|
||||
anchorPrefix: anchorMatch[2],
|
||||
};
|
||||
}
|
||||
|
||||
private async *provideReferenceSuggestions(document: ITextDocument, position: vscode.Position, context: CompletionContext): AsyncIterable<vscode.CompletionItem> {
|
||||
const insertionRange = new vscode.Range(context.linkTextStartPosition, position);
|
||||
const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length }));
|
||||
|
||||
const { definitions } = await this.linkProvider.getLinks(document);
|
||||
for (const [_, def] of definitions) {
|
||||
yield {
|
||||
kind: vscode.CompletionItemKind.Reference,
|
||||
label: def.ref.text,
|
||||
range: {
|
||||
inserting: insertionRange,
|
||||
replacing: replacementRange,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async *provideHeaderSuggestions(document: ITextDocument, position: vscode.Position, context: CompletionContext, insertionRange: vscode.Range): AsyncIterable<vscode.CompletionItem> {
|
||||
const toc = await TableOfContents.createForDocumentOrNotebook(this.parser, document);
|
||||
for (const entry of toc.entries) {
|
||||
const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length }));
|
||||
yield {
|
||||
kind: vscode.CompletionItemKind.Reference,
|
||||
label: '#' + decodeURIComponent(entry.slug.value),
|
||||
range: {
|
||||
inserting: insertionRange,
|
||||
replacing: replacementRange,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async *providePathSuggestions(document: ITextDocument, position: vscode.Position, context: CompletionContext): AsyncIterable<vscode.CompletionItem> {
|
||||
const valueBeforeLastSlash = context.linkPrefix.substring(0, context.linkPrefix.lastIndexOf('/') + 1); // keep the last slash
|
||||
|
||||
const parentDir = this.resolveReference(document, valueBeforeLastSlash || '.');
|
||||
if (!parentDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathSegmentStart = position.translate({ characterDelta: valueBeforeLastSlash.length - context.linkPrefix.length });
|
||||
const insertRange = new vscode.Range(pathSegmentStart, position);
|
||||
|
||||
const pathSegmentEnd = position.translate({ characterDelta: context.linkSuffix.length });
|
||||
const replacementRange = new vscode.Range(pathSegmentStart, pathSegmentEnd);
|
||||
|
||||
let dirInfo: [string, vscode.FileType][];
|
||||
try {
|
||||
dirInfo = await this.workspace.readDirectory(parentDir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, type] of dirInfo) {
|
||||
// Exclude paths that start with `.`
|
||||
if (name.startsWith('.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isDir = type === vscode.FileType.Directory;
|
||||
yield {
|
||||
label: isDir ? name + '/' : name,
|
||||
insertText: (context.skipEncoding ? name : encodeURIComponent(name)) + (isDir ? '/' : ''),
|
||||
kind: isDir ? vscode.CompletionItemKind.Folder : vscode.CompletionItemKind.File,
|
||||
range: {
|
||||
inserting: insertRange,
|
||||
replacing: replacementRange,
|
||||
},
|
||||
command: isDir ? { command: 'editor.action.triggerSuggest', title: '' } : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private resolveReference(document: ITextDocument, ref: string): vscode.Uri | undefined {
|
||||
const docUri = this.getFileUriOfTextDocument(document);
|
||||
|
||||
if (ref.startsWith('/')) {
|
||||
const workspaceFolder = vscode.workspace.getWorkspaceFolder(docUri);
|
||||
if (workspaceFolder) {
|
||||
return vscode.Uri.joinPath(workspaceFolder.uri, ref);
|
||||
} else {
|
||||
return this.resolvePath(docUri, ref.slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
return this.resolvePath(docUri, ref);
|
||||
}
|
||||
|
||||
private resolvePath(root: vscode.Uri, ref: string): vscode.Uri | undefined {
|
||||
try {
|
||||
if (root.scheme === Schemes.file) {
|
||||
return vscode.Uri.file(resolve(dirname(root.fsPath), ref));
|
||||
} else {
|
||||
return root.with({
|
||||
path: resolve(dirname(root.path), ref),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getFileUriOfTextDocument(document: ITextDocument) {
|
||||
if (document.uri.scheme === 'vscode-notebook-cell') {
|
||||
const notebook = vscode.workspace.notebookDocuments
|
||||
.find(notebook => notebook.getCells().some(cell => cell.document === document));
|
||||
|
||||
if (notebook) {
|
||||
return notebook.uri;
|
||||
}
|
||||
}
|
||||
|
||||
return document.uri;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerPathCompletionSupport(
|
||||
selector: vscode.DocumentSelector,
|
||||
workspace: IMdWorkspace,
|
||||
parser: IMdParser,
|
||||
linkProvider: MdLinkProvider,
|
||||
): vscode.Disposable {
|
||||
return vscode.languages.registerCompletionItemProvider(selector, new MdVsCodePathCompletionProvider(workspace, parser, linkProvider), '.', '/', '#');
|
||||
}
|
|
@ -24,7 +24,7 @@ function workspaceFile(...segments: string[]) {
|
|||
|
||||
async function getLinksForFile(file: vscode.Uri): Promise<vscode.DocumentLink[]> {
|
||||
debugLog('getting links', file.toString(), Date.now());
|
||||
const r = (await vscode.commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', file))!;
|
||||
const r = (await vscode.commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', file, /*linkResolveCount*/ 100))!;
|
||||
debugLog('got links', file.toString(), Date.now());
|
||||
return r;
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ async function getLinksForFile(file: vscode.Uri): Promise<vscode.DocumentLink[]>
|
|||
}
|
||||
});
|
||||
|
||||
test('Should navigate to fragment within current untitled file', async () => {
|
||||
test.skip('Should navigate to fragment within current untitled file', async () => { // TODO: skip for now for ls migration
|
||||
const testFile = workspaceFile('x.md').with({ scheme: 'untitled' });
|
||||
await withFileContents(testFile, joinLines(
|
||||
'[](#second)',
|
||||
|
@ -171,7 +171,7 @@ async function withFileContents(file: vscode.Uri, contents: string): Promise<voi
|
|||
async function executeLink(link: vscode.DocumentLink) {
|
||||
debugLog('executeingLink', link.target?.toString(), Date.now());
|
||||
|
||||
const args = JSON.parse(decodeURIComponent(link.target!.query));
|
||||
await vscode.commands.executeCommand(link.target!.path, args);
|
||||
const args: any[] = JSON.parse(decodeURIComponent(link.target!.query));
|
||||
await vscode.commands.executeCommand(link.target!.path, vscode.Uri.from(args[0]), ...args.slice(1));
|
||||
debugLog('executedLink', vscode.window.activeTextEditor?.document.toString(), Date.now());
|
||||
}
|
||||
|
|
|
@ -1,539 +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 { MdLink, MdLinkComputer, MdLinkProvider, MdVsCodeLinkProvider } from '../languageFeatures/documentLinks';
|
||||
import { noopToken } from '../util/cancellation';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
|
||||
import { nulLogger } from './nulLogging';
|
||||
import { assertRangeEqual, joinLines, workspacePath } from './util';
|
||||
|
||||
|
||||
suite('Markdown: MdLinkComputer', () => {
|
||||
|
||||
function getLinksForFile(fileContents: string): Promise<MdLink[]> {
|
||||
const doc = new InMemoryDocument(workspacePath('x.md'), fileContents);
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkComputer(engine);
|
||||
return linkProvider.getAllLinks(doc, noopToken);
|
||||
}
|
||||
|
||||
function assertLinksEqual(actualLinks: readonly MdLink[], expected: ReadonlyArray<vscode.Range | { readonly range: vscode.Range; readonly sourceText: string }>) {
|
||||
assert.strictEqual(actualLinks.length, expected.length);
|
||||
|
||||
for (let i = 0; i < actualLinks.length; ++i) {
|
||||
const exp = expected[i];
|
||||
if ('range' in exp) {
|
||||
assertRangeEqual(actualLinks[i].source.hrefRange, exp.range, `Range ${i} to be equal`);
|
||||
assert.strictEqual(actualLinks[i].source.hrefText, exp.sourceText, `Source text ${i} to be equal`);
|
||||
} else {
|
||||
assertRangeEqual(actualLinks[i].source.hrefRange, exp, `Range ${i} to be equal`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('Should not return anything for empty document', async () => {
|
||||
const links = await getLinksForFile('');
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not return anything for simple document without links', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'# a',
|
||||
'fdasfdfsafsa',
|
||||
));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should detect basic http links', async () => {
|
||||
const links = await getLinksForFile('a [b](https://example.com) c');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 6, 0, 25)
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should detect basic workspace links', async () => {
|
||||
{
|
||||
const links = await getLinksForFile('a [b](./file) c');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 6, 0, 12)
|
||||
]);
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('a [b](file.png) c');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 6, 0, 14)
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should detect links with title', async () => {
|
||||
const links = await getLinksForFile('a [b](https://example.com "abc") c');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 6, 0, 25)
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should handle links with escaped characters in name (#35245)', async () => {
|
||||
const links = await getLinksForFile('a [b\\]](./file)');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 8, 0, 14)
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should handle links with balanced parens', async () => {
|
||||
{
|
||||
const links = await getLinksForFile('a [b](https://example.com/a()c) c');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 6, 0, 30)
|
||||
]);
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('a [b](https://example.com/a(b)c) c');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 6, 0, 31)
|
||||
]);
|
||||
}
|
||||
{
|
||||
// #49011
|
||||
const links = await getLinksForFile('[A link](http://ThisUrlhasParens/A_link(in_parens))');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 9, 0, 50)
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should ignore bracketed text inside link title (#150921)', async () => {
|
||||
{
|
||||
const links = await getLinksForFile('[some [inner] in title](link)');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 24, 0, 28),
|
||||
]);
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('[some [inner] in title](<link>)');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 25, 0, 29),
|
||||
]);
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('[some [inner with space] in title](link)');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 35, 0, 39),
|
||||
]);
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`# h`,
|
||||
`[[a]](http://example.com)`,
|
||||
));
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(1, 6, 1, 24),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should handle two links without space', async () => {
|
||||
const links = await getLinksForFile('a ([test](test)[test2](test2)) c');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 10, 0, 14),
|
||||
new vscode.Range(0, 23, 0, 28)
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle hyperlinked images (#49238)', async () => {
|
||||
{
|
||||
const links = await getLinksForFile('[![alt text](image.jpg)](https://example.com)');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 25, 0, 44),
|
||||
new vscode.Range(0, 13, 0, 22),
|
||||
]);
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('[![a]( whitespace.jpg )]( https://whitespace.com )');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 26, 0, 48),
|
||||
new vscode.Range(0, 7, 0, 21),
|
||||
]);
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('[![a](img1.jpg)](file1.txt) text [![a](img2.jpg)](file2.txt)');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 17, 0, 26),
|
||||
new vscode.Range(0, 6, 0, 14),
|
||||
new vscode.Range(0, 50, 0, 59),
|
||||
new vscode.Range(0, 39, 0, 47),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should not consider link references starting with ^ character valid (#107471)', async () => {
|
||||
const links = await getLinksForFile('[^reference]: https://example.com');
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should find definitions links with spaces in angle brackets (#136073)', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'[a]: <b c>',
|
||||
'[b]: <cd>',
|
||||
));
|
||||
|
||||
assertLinksEqual(links, [
|
||||
{ range: new vscode.Range(0, 6, 0, 9), sourceText: 'b c' },
|
||||
{ range: new vscode.Range(1, 6, 1, 8), sourceText: 'cd' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should only find one link for reference sources [a]: source (#141285)', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'[Works]: https://example.com',
|
||||
));
|
||||
|
||||
assertLinksEqual(links, [
|
||||
{ range: new vscode.Range(0, 9, 0, 28), sourceText: 'https://example.com' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should find reference link shorthand (#141285)', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'[ref]',
|
||||
'[ref]: https://example.com',
|
||||
));
|
||||
assertLinksEqual(links, [
|
||||
{ range: new vscode.Range(0, 1, 0, 4), sourceText: 'ref' },
|
||||
{ range: new vscode.Range(1, 7, 1, 26), sourceText: 'https://example.com' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should find reference link shorthand using empty closing brackets (#141285)', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'[ref][]',
|
||||
));
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 1, 0, 4),
|
||||
]);
|
||||
});
|
||||
|
||||
test.skip('Should find reference link shorthand for link with space in label (#141285)', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'[ref with space]',
|
||||
));
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 7, 0, 26),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should not include reference links with escaped leading brackets', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`\\[bad link][good]`,
|
||||
`\\[good]`,
|
||||
`[good]: http://example.com`,
|
||||
));
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(2, 8, 2, 26) // Should only find the definition
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should not consider links in code fenced with backticks', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'```',
|
||||
'[b](https://example.com)',
|
||||
'```'));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not consider links in code fenced with tilde', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'~~~',
|
||||
'[b](https://example.com)',
|
||||
'~~~'));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not consider links in indented code', async () => {
|
||||
const links = await getLinksForFile(' [b](https://example.com)');
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not consider links in inline code span', async () => {
|
||||
const links = await getLinksForFile('`[b](https://example.com)`');
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not consider links with code span inside', async () => {
|
||||
const links = await getLinksForFile('[li`nk](https://example.com`)');
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not consider links in multiline inline code span', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'`` ',
|
||||
'[b](https://example.com)',
|
||||
'``'));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not consider link references in code fenced with backticks (#146714)', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'```',
|
||||
'[a] [bb]',
|
||||
'```'));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not consider reference sources in code fenced with backticks (#146714)', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'```',
|
||||
'[a]: http://example.com;',
|
||||
'[b]: <http://example.com>;',
|
||||
'[c]: (http://example.com);',
|
||||
'```'));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not consider links in multiline inline code span between between text', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'[b](https://1.com) `[b](https://2.com)',
|
||||
'[b](https://3.com) ` [b](https://4.com)'));
|
||||
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 4, 0, 17),
|
||||
new vscode.Range(1, 25, 1, 38),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should not consider links in multiline inline code span with new line after the first backtick', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'`',
|
||||
'[b](https://example.com)`'));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not miss links in invalid multiline inline code span', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'`` ',
|
||||
'',
|
||||
'[b](https://example.com)',
|
||||
'',
|
||||
'``'));
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(2, 4, 2, 23)
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should find autolinks', async () => {
|
||||
const links = await getLinksForFile('pre <http://example.com> post');
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 5, 0, 23)
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should not detect links inside html comment blocks', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`<!-- <http://example.com> -->`,
|
||||
`<!-- [text](./foo.md) -->`,
|
||||
`<!-- [text]: ./foo.md -->`,
|
||||
``,
|
||||
`<!--`,
|
||||
`<http://example.com>`,
|
||||
`-->`,
|
||||
``,
|
||||
`<!--`,
|
||||
`[text](./foo.md)`,
|
||||
`-->`,
|
||||
``,
|
||||
`<!--`,
|
||||
`[text]: ./foo.md`,
|
||||
`-->`,
|
||||
));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test.skip('Should not detect links inside inline html comments', async () => {
|
||||
// See #149678
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`text <!-- <http://example.com> --> text`,
|
||||
`text <!-- [text](./foo.md) --> text`,
|
||||
`text <!-- [text]: ./foo.md --> text`,
|
||||
``,
|
||||
`text <!--`,
|
||||
`<http://example.com>`,
|
||||
`--> text`,
|
||||
``,
|
||||
`text <!--`,
|
||||
`[text](./foo.md)`,
|
||||
`--> text`,
|
||||
``,
|
||||
`text <!--`,
|
||||
`[text]: ./foo.md`,
|
||||
`--> text`,
|
||||
));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should not mark checkboxes as links', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'- [x]',
|
||||
'- [X]',
|
||||
'- [ ]',
|
||||
'* [x]',
|
||||
'* [X]',
|
||||
'* [ ]',
|
||||
``,
|
||||
`[x]: http://example.com`
|
||||
));
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(7, 5, 7, 23)
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should still find links on line with checkbox', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'- [x] [x]',
|
||||
'- [X] [x]',
|
||||
'- [] [x]',
|
||||
``,
|
||||
`[x]: http://example.com`
|
||||
));
|
||||
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 7, 0, 8),
|
||||
new vscode.Range(1, 7, 1, 8),
|
||||
new vscode.Range(2, 6, 2, 7),
|
||||
new vscode.Range(4, 5, 4, 23),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should find link only within angle brackets.', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`[link](<path>)`
|
||||
));
|
||||
assertLinksEqual(links, [new vscode.Range(0, 8, 0, 12)]);
|
||||
});
|
||||
|
||||
test('Should find link within angle brackets even with link title.', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`[link](<path> "test title")`
|
||||
));
|
||||
assertLinksEqual(links, [new vscode.Range(0, 8, 0, 12)]);
|
||||
});
|
||||
|
||||
test('Should find link within angle brackets even with surrounding spaces.', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`[link]( <path> )`
|
||||
));
|
||||
assertLinksEqual(links, [new vscode.Range(0, 9, 0, 13)]);
|
||||
});
|
||||
|
||||
test('Should find link within angle brackets for image hyperlinks.', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`![link](<path>)`
|
||||
));
|
||||
assertLinksEqual(links, [new vscode.Range(0, 9, 0, 13)]);
|
||||
});
|
||||
|
||||
test('Should find link with spaces in angle brackets for image hyperlinks with titles.', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`![link](< path > "test")`
|
||||
));
|
||||
assertLinksEqual(links, [new vscode.Range(0, 9, 0, 15)]);
|
||||
});
|
||||
|
||||
|
||||
test('Should not find link due to incorrect angle bracket notation or usage.', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`[link](<path )`,
|
||||
`[link](<> path>)`,
|
||||
`[link](> path)`,
|
||||
));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
|
||||
test('Should find link within angle brackets even with space inside link.', async () => {
|
||||
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`[link](<pa th>)`
|
||||
));
|
||||
|
||||
assertLinksEqual(links, [new vscode.Range(0, 8, 0, 13)]);
|
||||
});
|
||||
|
||||
test('Should find links with titles', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`[link](<no such.md> "text")`,
|
||||
`[link](<no such.md> 'text')`,
|
||||
`[link](<no such.md> (text))`,
|
||||
`[link](no-such.md "text")`,
|
||||
`[link](no-such.md 'text')`,
|
||||
`[link](no-such.md (text))`,
|
||||
));
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 8, 0, 18),
|
||||
new vscode.Range(1, 8, 1, 18),
|
||||
new vscode.Range(2, 8, 2, 18),
|
||||
new vscode.Range(3, 7, 3, 17),
|
||||
new vscode.Range(4, 7, 4, 17),
|
||||
new vscode.Range(5, 7, 5, 17),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should not include link with empty angle bracket', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
`[](<>)`,
|
||||
`[link](<>)`,
|
||||
`[link](<> "text")`,
|
||||
`[link](<> 'text')`,
|
||||
`[link](<> (text))`,
|
||||
));
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
suite('Markdown: VS Code DocumentLinkProvider', () => {
|
||||
|
||||
function getLinksForFile(fileContents: string) {
|
||||
const doc = new InMemoryDocument(workspacePath('x.md'), fileContents);
|
||||
const workspace = new InMemoryMdWorkspace([doc]);
|
||||
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkProvider(engine, workspace, nulLogger);
|
||||
const provider = new MdVsCodeLinkProvider(linkProvider);
|
||||
return provider.provideDocumentLinks(doc, noopToken);
|
||||
}
|
||||
|
||||
function assertLinksEqual(actualLinks: readonly vscode.DocumentLink[], expectedRanges: readonly vscode.Range[]) {
|
||||
assert.strictEqual(actualLinks.length, expectedRanges.length);
|
||||
|
||||
for (let i = 0; i < actualLinks.length; ++i) {
|
||||
assertRangeEqual(actualLinks[i].range, expectedRanges[i], `Range ${i} to be equal`);
|
||||
}
|
||||
}
|
||||
|
||||
test('Should include defined reference links (#141285)', async () => {
|
||||
const links = await getLinksForFile(joinLines(
|
||||
'[ref]',
|
||||
'[ref][]',
|
||||
'[ref][ref]',
|
||||
'',
|
||||
'[ref]: http://example.com'
|
||||
));
|
||||
assertLinksEqual(links, [
|
||||
new vscode.Range(0, 1, 0, 4),
|
||||
new vscode.Range(1, 1, 1, 4),
|
||||
new vscode.Range(2, 6, 2, 9),
|
||||
new vscode.Range(4, 7, 4, 25),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should not include reference link shorthand when definition does not exist (#141285)', async () => {
|
||||
const links = await getLinksForFile('[ref]');
|
||||
assertLinksEqual(links, []);
|
||||
});
|
||||
});
|
|
@ -1,313 +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 { MdLinkProvider } from '../languageFeatures/documentLinks';
|
||||
import { MdVsCodePathCompletionProvider } from '../languageFeatures/pathCompletions';
|
||||
import { noopToken } from '../util/cancellation';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { IMdWorkspace } from '../workspace';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryMdWorkspace } from './inMemoryWorkspace';
|
||||
import { nulLogger } from './nulLogging';
|
||||
import { CURSOR, getCursorPositions, joinLines, workspacePath } from './util';
|
||||
|
||||
|
||||
async function getCompletionsAtCursor(resource: vscode.Uri, fileContents: string, workspace?: IMdWorkspace) {
|
||||
const doc = new InMemoryDocument(resource, fileContents);
|
||||
|
||||
const engine = createNewMarkdownEngine();
|
||||
const ws = workspace ?? new InMemoryMdWorkspace([doc]);
|
||||
const linkProvider = new MdLinkProvider(engine, ws, nulLogger);
|
||||
const provider = new MdVsCodePathCompletionProvider(ws, engine, linkProvider);
|
||||
const cursorPositions = getCursorPositions(fileContents, doc);
|
||||
const completions = await provider.provideCompletionItems(doc, cursorPositions[0], noopToken, {
|
||||
triggerCharacter: undefined,
|
||||
triggerKind: vscode.CompletionTriggerKind.Invoke,
|
||||
});
|
||||
|
||||
return completions.sort((a, b) => (a.label as string).localeCompare(b.label as string));
|
||||
}
|
||||
|
||||
function assertCompletionsEqual(actual: readonly vscode.CompletionItem[], expected: readonly { label: string; insertText?: string }[]) {
|
||||
assert.strictEqual(actual.length, expected.length, 'Completion counts should be equal');
|
||||
|
||||
for (let i = 0; i < actual.length; ++i) {
|
||||
assert.strictEqual(actual[i].label, expected[i].label, `Completion labels ${i} should be equal`);
|
||||
if (typeof expected[i].insertText !== 'undefined') {
|
||||
assert.strictEqual(actual[i].insertText, expected[i].insertText, `Completion insert texts ${i} should be equal`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suite('Markdown: Path completions', () => {
|
||||
|
||||
test('Should not return anything when triggered in empty doc', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), `${CURSOR}`);
|
||||
assertCompletionsEqual(completions, []);
|
||||
});
|
||||
|
||||
test('Should return anchor completions', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](#${CURSOR}`,
|
||||
``,
|
||||
`# A b C`,
|
||||
`# x y Z`,
|
||||
));
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: '#a-b-c' },
|
||||
{ label: '#x-y-z' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should not return suggestions for http links', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](http:${CURSOR}`,
|
||||
``,
|
||||
`# http`,
|
||||
`# http:`,
|
||||
`# https:`,
|
||||
));
|
||||
|
||||
assertCompletionsEqual(completions, []);
|
||||
});
|
||||
|
||||
test('Should return relative path suggestions', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('a.md'), ''),
|
||||
new InMemoryDocument(workspacePath('b.md'), ''),
|
||||
new InMemoryDocument(workspacePath('sub/foo.md'), ''),
|
||||
]);
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](${CURSOR}`,
|
||||
``,
|
||||
`# A b C`,
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: '#a-b-c' },
|
||||
{ label: 'a.md' },
|
||||
{ label: 'b.md' },
|
||||
{ label: 'sub/' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should return relative path suggestions using ./', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('a.md'), ''),
|
||||
new InMemoryDocument(workspacePath('b.md'), ''),
|
||||
new InMemoryDocument(workspacePath('sub/foo.md'), ''),
|
||||
]);
|
||||
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](./${CURSOR}`,
|
||||
``,
|
||||
`# A b C`,
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'a.md' },
|
||||
{ label: 'b.md' },
|
||||
{ label: 'sub/' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should return absolute path suggestions using /', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('a.md'), ''),
|
||||
new InMemoryDocument(workspacePath('b.md'), ''),
|
||||
new InMemoryDocument(workspacePath('sub/c.md'), ''),
|
||||
]);
|
||||
|
||||
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
|
||||
`[](/${CURSOR}`,
|
||||
``,
|
||||
`# A b C`,
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'a.md' },
|
||||
{ label: 'b.md' },
|
||||
{ label: 'sub/' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should return anchor suggestions in other file', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('b.md'), joinLines(
|
||||
`# b`,
|
||||
``,
|
||||
`[./a](./a)`,
|
||||
``,
|
||||
`# header1`,
|
||||
)),
|
||||
]);
|
||||
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
|
||||
`[](/b.md#${CURSOR}`,
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: '#b' },
|
||||
{ label: '#header1' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should reference links for current file', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
|
||||
`[][${CURSOR}`,
|
||||
``,
|
||||
`[ref-1]: bla`,
|
||||
`[ref-2]: bla`,
|
||||
));
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'ref-1' },
|
||||
{ label: 'ref-2' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should complete headers in link definitions', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
|
||||
`# a B c`,
|
||||
`# x y Z`,
|
||||
`[ref-1]: ${CURSOR}`,
|
||||
));
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: '#a-b-c' },
|
||||
{ label: '#x-y-z' },
|
||||
{ label: 'new.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should complete relative paths in link definitions', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('a.md'), ''),
|
||||
new InMemoryDocument(workspacePath('b.md'), ''),
|
||||
new InMemoryDocument(workspacePath('sub/c.md'), ''),
|
||||
]);
|
||||
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`# a B c`,
|
||||
`[ref-1]: ${CURSOR}`,
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: '#a-b-c' },
|
||||
{ label: 'a.md' },
|
||||
{ label: 'b.md' },
|
||||
{ label: 'sub/' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should escape spaces in path names', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('a.md'), ''),
|
||||
new InMemoryDocument(workspacePath('b.md'), ''),
|
||||
new InMemoryDocument(workspacePath('sub/file with space.md'), ''),
|
||||
]);
|
||||
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](./sub/${CURSOR})`
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'file with space.md', insertText: 'file%20with%20space.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should support completions on angle bracket path with spaces', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('sub with space/a.md'), ''),
|
||||
new InMemoryDocument(workspacePath('b.md'), ''),
|
||||
]);
|
||||
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](</sub with space/${CURSOR}`
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'a.md', insertText: 'a.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should not escape spaces in path names that use angle brackets', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('sub/file with space.md'), ''),
|
||||
]);
|
||||
|
||||
{
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](<./sub/${CURSOR}`
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'file with space.md', insertText: 'file with space.md' },
|
||||
]);
|
||||
}
|
||||
{
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](<./sub/${CURSOR}>`
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'file with space.md', insertText: 'file with space.md' },
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should complete paths for path with encoded spaces', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('a.md'), ''),
|
||||
new InMemoryDocument(workspacePath('b.md'), ''),
|
||||
new InMemoryDocument(workspacePath('sub with space/file.md'), ''),
|
||||
]);
|
||||
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](./sub%20with%20space/${CURSOR})`
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'file.md', insertText: 'file.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should complete definition path for path with encoded spaces', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('a.md'), ''),
|
||||
new InMemoryDocument(workspacePath('b.md'), ''),
|
||||
new InMemoryDocument(workspacePath('sub with space/file.md'), ''),
|
||||
]);
|
||||
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[def]: ./sub%20with%20space/${CURSOR}`
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'file.md', insertText: 'file.md' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('Should support definition path with angle brackets', async () => {
|
||||
const workspace = new InMemoryMdWorkspace([
|
||||
new InMemoryDocument(workspacePath('a.md'), ''),
|
||||
new InMemoryDocument(workspacePath('b.md'), ''),
|
||||
new InMemoryDocument(workspacePath('sub with space/file.md'), ''),
|
||||
]);
|
||||
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[def]: <./${CURSOR}>`
|
||||
), workspace);
|
||||
|
||||
assertCompletionsEqual(completions, [
|
||||
{ label: 'a.md', insertText: 'a.md' },
|
||||
{ label: 'b.md', insertText: 'b.md' },
|
||||
{ label: 'sub with space/', insertText: 'sub with space/' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -6,28 +6,11 @@ import * as assert from 'assert';
|
|||
import * as os from 'os';
|
||||
import * as vscode from 'vscode';
|
||||
import { DisposableStore } from '../util/dispose';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
|
||||
export const joinLines = (...args: string[]) =>
|
||||
args.join(os.platform() === 'win32' ? '\r\n' : '\n');
|
||||
|
||||
|
||||
export const CURSOR = '$$CURSOR$$';
|
||||
|
||||
export function getCursorPositions(contents: string, doc: InMemoryDocument): vscode.Position[] {
|
||||
const positions: vscode.Position[] = [];
|
||||
let index = 0;
|
||||
let wordLength = 0;
|
||||
while (index !== -1) {
|
||||
index = contents.indexOf(CURSOR, index + wordLength);
|
||||
if (index !== -1) {
|
||||
positions.push(doc.positionAt(index));
|
||||
}
|
||||
wordLength = CURSOR.length;
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
export function workspacePath(...segments: string[]): vscode.Uri {
|
||||
return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, ...segments);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue