mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-28 09:35:38 -05:00
Merge from vscode merge-base (#22780)
* Revert "Revert "Merge from vscode merge-base (#22769)" (#22779)"
This reverts commit 47a1745180.
* Fix notebook download task
* Remove done call from extensions-ci
This commit is contained in:
114
extensions/markdown-language-features/src/client.ts
Normal file
114
extensions/markdown-language-features/src/client.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType } from 'vscode-languageclient';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { IMdParser } from './markdownEngine';
|
||||
import * as proto from './protocol';
|
||||
import { looksLikeMarkdownPath, markdownFileExtensions } from './util/file';
|
||||
import { IMdWorkspace } from './workspace';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
|
||||
|
||||
|
||||
export async function startClient(factory: LanguageClientConstructor, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
|
||||
|
||||
const mdFileGlob = `**/*.{${markdownFileExtensions.join(',')}}`;
|
||||
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector: [{ language: 'markdown' }],
|
||||
synchronize: {
|
||||
configurationSection: ['markdown'],
|
||||
fileEvents: vscode.workspace.createFileSystemWatcher(mdFileGlob),
|
||||
},
|
||||
initializationOptions: {
|
||||
markdownFileExtensions,
|
||||
},
|
||||
diagnosticPullOptions: {
|
||||
onChange: true,
|
||||
onSave: true,
|
||||
onTabs: true,
|
||||
match(_documentSelector, resource) {
|
||||
return looksLikeMarkdownPath(resource);
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
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(proto.parse, async (e) => {
|
||||
const uri = vscode.Uri.parse(e.uri);
|
||||
const doc = await workspace.getOrLoadMarkdownDocument(uri);
|
||||
if (doc) {
|
||||
return parser.tokenize(doc);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
client.onRequest(proto.fs_readFile, async (e): Promise<number[]> => {
|
||||
const uri = vscode.Uri.parse(e.uri);
|
||||
return Array.from(await vscode.workspace.fs.readFile(uri));
|
||||
});
|
||||
|
||||
client.onRequest(proto.fs_stat, 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(proto.fs_readDirectory, 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(proto.findMarkdownFilesInWorkspace, async (): Promise<string[]> => {
|
||||
return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString());
|
||||
});
|
||||
|
||||
const watchers = new Map<number, vscode.FileSystemWatcher>();
|
||||
|
||||
client.onRequest(proto.fs_watcher_create, async (params): Promise<void> => {
|
||||
const id = params.id;
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.parse(params.uri), '*'), params.options.ignoreCreate, params.options.ignoreChange, params.options.ignoreDelete);
|
||||
watchers.set(id, watcher);
|
||||
watcher.onDidCreate(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'create' }); });
|
||||
watcher.onDidChange(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'change' }); });
|
||||
watcher.onDidDelete(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'delete' }); });
|
||||
});
|
||||
|
||||
client.onRequest(proto.fs_watcher_delete, async (params): Promise<void> => {
|
||||
watchers.get(params.id)?.dispose();
|
||||
watchers.delete(params.id);
|
||||
});
|
||||
|
||||
await client.start();
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MdTableOfContentsProvider } from '../tableOfContents';
|
||||
import { openDocumentLink } from '../util/openDocumentLink';
|
||||
import { Schemes } from '../util/schemes';
|
||||
|
||||
type UriComponents = {
|
||||
readonly scheme?: string;
|
||||
@@ -48,18 +49,18 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
private readonly tocProvider: MdTableOfContentsProvider,
|
||||
) { }
|
||||
|
||||
public async execute(args: OpenDocumentLinkArgs) {
|
||||
const fromResource = vscode.Uri.parse('').with(args.fromResource);
|
||||
const targetResource = reviveUri(args.parts).with({ fragment: args.fragment });
|
||||
return openDocumentLink(this.engine, targetResource, fromResource);
|
||||
return openDocumentLink(this.tocProvider, targetResource, fromResource);
|
||||
}
|
||||
}
|
||||
|
||||
function reviveUri(parts: any) {
|
||||
if (parts.scheme === 'file') {
|
||||
if (parts.scheme === Schemes.file) {
|
||||
return vscode.Uri.file(parts.path);
|
||||
}
|
||||
return vscode.Uri.parse('').with(parts);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MarkdownItEngine } from '../markdownEngine';
|
||||
import { MarkdownPreviewManager } from '../preview/previewManager';
|
||||
|
||||
export class RefreshPreviewCommand implements Command {
|
||||
@@ -12,7 +12,7 @@ export class RefreshPreviewCommand implements Command {
|
||||
|
||||
public constructor(
|
||||
private readonly webviewManager: MarkdownPreviewManager,
|
||||
private readonly engine: MarkdownEngine
|
||||
private readonly engine: MarkdownItEngine
|
||||
) { }
|
||||
|
||||
public execute() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MarkdownItEngine } from '../markdownEngine';
|
||||
import { MarkdownPreviewManager } from '../preview/previewManager';
|
||||
|
||||
export class ReloadPlugins implements Command {
|
||||
@@ -12,7 +12,7 @@ export class ReloadPlugins implements Command {
|
||||
|
||||
public constructor(
|
||||
private readonly webviewManager: MarkdownPreviewManager,
|
||||
private readonly engine: MarkdownEngine,
|
||||
private readonly engine: MarkdownItEngine,
|
||||
) { }
|
||||
|
||||
public execute(): void {
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
import { MarkdownItEngine } from '../markdownEngine';
|
||||
import { ITextDocument } from '../types/textDocument';
|
||||
|
||||
export class RenderDocument implements Command {
|
||||
public readonly id = 'markdown.api.render';
|
||||
|
||||
public constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
private readonly engine: MarkdownItEngine
|
||||
) { }
|
||||
|
||||
public async execute(document: SkinnyTextDocument | string): Promise<string> {
|
||||
public async execute(document: ITextDocument | string): Promise<string> {
|
||||
return (await (this.engine.render(document))).html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseLanguageClient, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
|
||||
import { startClient } from './client';
|
||||
import { activateShared } from './extension.shared';
|
||||
import { VsCodeOutputLogger } from './logging';
|
||||
import { IMdParser, MarkdownItEngine } from './markdownEngine';
|
||||
import { getMarkdownExtensionContributions } from './markdownExtensions';
|
||||
import { githubSlugifier } from './slugify';
|
||||
import { IMdWorkspace, VsCodeMdWorkspace } from './workspace';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const contributions = getMarkdownExtensionContributions(context);
|
||||
context.subscriptions.push(contributions);
|
||||
|
||||
const logger = new VsCodeOutputLogger();
|
||||
context.subscriptions.push(logger);
|
||||
|
||||
const engine = new MarkdownItEngine(contributions, githubSlugifier, logger);
|
||||
|
||||
const workspace = new VsCodeMdWorkspace();
|
||||
context.subscriptions.push(workspace);
|
||||
|
||||
const client = await startServer(context, workspace, engine);
|
||||
context.subscriptions.push({
|
||||
dispose: () => client.stop()
|
||||
});
|
||||
activateShared(context, client, workspace, engine, logger, contributions);
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
return startClient((id: string, name: string, clientOptions: LanguageClientOptions) => {
|
||||
return new LanguageClient(id, name, clientOptions, worker);
|
||||
}, workspace, parser);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { registerDiagnosticSupport } from './languageFeatures/diagnostics';
|
||||
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
|
||||
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
|
||||
import { ILogger } from './logging';
|
||||
import { MarkdownItEngine, MdParsingProvider } from './markdownEngine';
|
||||
import { MarkdownContributionProvider } from './markdownExtensions';
|
||||
import { MdDocumentRenderer } from './preview/documentRenderer';
|
||||
import { MarkdownPreviewManager } from './preview/previewManager';
|
||||
import { ContentSecurityPolicyArbiter, ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './preview/security';
|
||||
import { MdTableOfContentsProvider } from './tableOfContents';
|
||||
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
|
||||
import { IMdWorkspace } from './workspace';
|
||||
|
||||
export function activateShared(
|
||||
context: vscode.ExtensionContext,
|
||||
client: BaseLanguageClient,
|
||||
workspace: IMdWorkspace,
|
||||
engine: MarkdownItEngine,
|
||||
logger: ILogger,
|
||||
contributions: MarkdownContributionProvider,
|
||||
) {
|
||||
const telemetryReporter = loadDefaultTelemetryReporter();
|
||||
context.subscriptions.push(telemetryReporter);
|
||||
|
||||
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
|
||||
const commandManager = new CommandManager();
|
||||
|
||||
const parser = new MdParsingProvider(engine, workspace);
|
||||
const tocProvider = new MdTableOfContentsProvider(parser, workspace, logger);
|
||||
context.subscriptions.push(parser, tocProvider);
|
||||
|
||||
const contentProvider = new MdDocumentRenderer(engine, context, cspArbiter, contributions, logger);
|
||||
const previewManager = new MarkdownPreviewManager(contentProvider, workspace, logger, contributions, tocProvider);
|
||||
context.subscriptions.push(previewManager);
|
||||
|
||||
context.subscriptions.push(registerMarkdownLanguageFeatures(client, commandManager));
|
||||
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine, tocProvider));
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
|
||||
previewManager.updateConfiguration();
|
||||
}));
|
||||
}
|
||||
|
||||
function registerMarkdownLanguageFeatures(
|
||||
client: BaseLanguageClient,
|
||||
commandManager: CommandManager,
|
||||
): vscode.Disposable {
|
||||
const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' };
|
||||
return vscode.Disposable.from(
|
||||
// Language features
|
||||
registerDiagnosticSupport(selector, commandManager),
|
||||
registerDropIntoEditorSupport(selector),
|
||||
registerFindFileReferenceSupport(commandManager, client),
|
||||
registerPasteSupport(selector),
|
||||
);
|
||||
}
|
||||
|
||||
function registerMarkdownCommands(
|
||||
commandManager: CommandManager,
|
||||
previewManager: MarkdownPreviewManager,
|
||||
telemetryReporter: TelemetryReporter,
|
||||
cspArbiter: ContentSecurityPolicyArbiter,
|
||||
engine: MarkdownItEngine,
|
||||
tocProvider: MdTableOfContentsProvider,
|
||||
): vscode.Disposable {
|
||||
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
|
||||
|
||||
commandManager.register(new commands.ShowPreviewCommand(previewManager, telemetryReporter));
|
||||
commandManager.register(new commands.ShowPreviewToSideCommand(previewManager, telemetryReporter));
|
||||
commandManager.register(new commands.ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
|
||||
commandManager.register(new commands.ShowSourceCommand(previewManager));
|
||||
commandManager.register(new commands.RefreshPreviewCommand(previewManager, engine));
|
||||
commandManager.register(new commands.MoveCursorToPositionCommand());
|
||||
commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
|
||||
commandManager.register(new commands.OpenDocumentLinkCommand(tocProvider));
|
||||
commandManager.register(new commands.ToggleLockCommand(previewManager));
|
||||
commandManager.register(new commands.RenderDocument(engine));
|
||||
commandManager.register(new commands.ReloadPlugins(previewManager, engine));
|
||||
return commandManager;
|
||||
}
|
||||
@@ -4,103 +4,50 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { CommandManager } from './commandManager';
|
||||
import * as commands from './commands/index';
|
||||
import { register as registerDiagnostics } from './languageFeatures/diagnostics';
|
||||
import { MdDefinitionProvider } from './languageFeatures/definitionProvider';
|
||||
import { MdLinkProvider } from './languageFeatures/documentLinkProvider';
|
||||
import { MdDocumentSymbolProvider } from './languageFeatures/documentSymbolProvider';
|
||||
import { registerDropIntoEditor } from './languageFeatures/dropIntoEditor';
|
||||
import { registerFindFileReferences } from './languageFeatures/fileReferences';
|
||||
import { MdFoldingProvider } from './languageFeatures/foldingProvider';
|
||||
import { MdPathCompletionProvider } from './languageFeatures/pathCompletions';
|
||||
import { MdReferencesProvider } from './languageFeatures/references';
|
||||
import { MdRenameProvider } from './languageFeatures/rename';
|
||||
import { MdSmartSelect } from './languageFeatures/smartSelect';
|
||||
import { MdWorkspaceSymbolProvider } from './languageFeatures/workspaceSymbolProvider';
|
||||
import { Logger } from './logger';
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
import { BaseLanguageClient, LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node';
|
||||
import { startClient } from './client';
|
||||
import { activateShared } from './extension.shared';
|
||||
import { VsCodeOutputLogger } from './logging';
|
||||
import { IMdParser, MarkdownItEngine } from './markdownEngine';
|
||||
import { getMarkdownExtensionContributions } from './markdownExtensions';
|
||||
import { MarkdownContentProvider } from './preview/previewContentProvider';
|
||||
import { MarkdownPreviewManager } from './preview/previewManager';
|
||||
import { ContentSecurityPolicyArbiter, ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './preview/security';
|
||||
import { githubSlugifier } from './slugify';
|
||||
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
|
||||
import { VsCodeMdWorkspaceContents } from './workspaceContents';
|
||||
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const telemetryReporter = loadDefaultTelemetryReporter();
|
||||
context.subscriptions.push(telemetryReporter);
|
||||
import { IMdWorkspace, VsCodeMdWorkspace } from './workspace';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const contributions = getMarkdownExtensionContributions(context);
|
||||
context.subscriptions.push(contributions);
|
||||
|
||||
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
|
||||
const engine = new MarkdownEngine(contributions, githubSlugifier);
|
||||
const logger = new Logger();
|
||||
const commandManager = new CommandManager();
|
||||
const logger = new VsCodeOutputLogger();
|
||||
context.subscriptions.push(logger);
|
||||
|
||||
const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, contributions, logger);
|
||||
const symbolProvider = new MdDocumentSymbolProvider(engine);
|
||||
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions, engine);
|
||||
context.subscriptions.push(previewManager);
|
||||
const engine = new MarkdownItEngine(contributions, githubSlugifier, logger);
|
||||
|
||||
context.subscriptions.push(registerMarkdownLanguageFeatures(commandManager, symbolProvider, engine));
|
||||
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine));
|
||||
const workspace = new VsCodeMdWorkspace();
|
||||
context.subscriptions.push(workspace);
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
|
||||
logger.updateConfiguration();
|
||||
previewManager.updateConfiguration();
|
||||
}));
|
||||
const client = await startServer(context, workspace, engine);
|
||||
context.subscriptions.push({
|
||||
dispose: () => client.stop()
|
||||
});
|
||||
activateShared(context, client, workspace, engine, logger, contributions);
|
||||
}
|
||||
|
||||
function registerMarkdownLanguageFeatures(
|
||||
commandManager: CommandManager,
|
||||
symbolProvider: MdDocumentSymbolProvider,
|
||||
engine: MarkdownEngine
|
||||
): vscode.Disposable {
|
||||
const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' };
|
||||
function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
|
||||
const clientMain = vscode.extensions.getExtension('vscode.markdown-language-features')?.packageJSON?.main || '';
|
||||
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
const workspaceContents = new VsCodeMdWorkspaceContents();
|
||||
const serverMain = `./server/${clientMain.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/main`;
|
||||
const serverModule = context.asAbsolutePath(serverMain);
|
||||
|
||||
const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier);
|
||||
return vscode.Disposable.from(
|
||||
vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider),
|
||||
vscode.languages.registerDocumentLinkProvider(selector, linkProvider),
|
||||
vscode.languages.registerFoldingRangeProvider(selector, new MdFoldingProvider(engine)),
|
||||
vscode.languages.registerSelectionRangeProvider(selector, new MdSmartSelect(engine)),
|
||||
vscode.languages.registerWorkspaceSymbolProvider(new MdWorkspaceSymbolProvider(symbolProvider, workspaceContents)),
|
||||
vscode.languages.registerReferenceProvider(selector, referencesProvider),
|
||||
vscode.languages.registerRenameProvider(selector, new MdRenameProvider(referencesProvider, workspaceContents, githubSlugifier)),
|
||||
vscode.languages.registerDefinitionProvider(selector, new MdDefinitionProvider(referencesProvider)),
|
||||
MdPathCompletionProvider.register(selector, engine, linkProvider),
|
||||
registerDiagnostics(engine, workspaceContents, linkProvider),
|
||||
registerDropIntoEditor(selector),
|
||||
registerFindFileReferences(commandManager, referencesProvider),
|
||||
);
|
||||
}
|
||||
|
||||
function registerMarkdownCommands(
|
||||
commandManager: CommandManager,
|
||||
previewManager: MarkdownPreviewManager,
|
||||
telemetryReporter: TelemetryReporter,
|
||||
cspArbiter: ContentSecurityPolicyArbiter,
|
||||
engine: MarkdownEngine
|
||||
): vscode.Disposable {
|
||||
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
|
||||
|
||||
commandManager.register(new commands.ShowPreviewCommand(previewManager, telemetryReporter));
|
||||
commandManager.register(new commands.ShowPreviewToSideCommand(previewManager, telemetryReporter));
|
||||
commandManager.register(new commands.ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
|
||||
commandManager.register(new commands.ShowSourceCommand(previewManager));
|
||||
commandManager.register(new commands.RefreshPreviewCommand(previewManager, engine));
|
||||
commandManager.register(new commands.MoveCursorToPositionCommand());
|
||||
commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
|
||||
commandManager.register(new commands.OpenDocumentLinkCommand(engine));
|
||||
commandManager.register(new commands.ToggleLockCommand(previewManager));
|
||||
commandManager.register(new commands.RenderDocument(engine));
|
||||
commandManager.register(new commands.ReloadPlugins(previewManager, engine));
|
||||
return commandManager;
|
||||
// The debug options for the server
|
||||
const debugOptions = { execArgv: ['--nolazy', '--inspect=' + (7000 + Math.round(Math.random() * 999))] };
|
||||
|
||||
// If the extension is launch in debug mode the debug server options are use
|
||||
// Otherwise the run options are used
|
||||
const serverOptions: ServerOptions = {
|
||||
run: { module: serverModule, transport: TransportKind.ipc },
|
||||
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
|
||||
};
|
||||
return startClient((id, name, clientOptions) => {
|
||||
return new LanguageClient(id, name, serverOptions, clientOptions);
|
||||
}, workspace, parser);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { tryGetUriListSnippet } from './dropIntoEditor';
|
||||
|
||||
export function registerPasteSupport(selector: vscode.DocumentSelector) {
|
||||
return vscode.languages.registerDocumentPasteEditProvider(selector, new class implements vscode.DocumentPasteEditProvider {
|
||||
|
||||
async provideDocumentPasteEdits(
|
||||
document: vscode.TextDocument,
|
||||
_ranges: readonly vscode.Range[],
|
||||
dataTransfer: vscode.DataTransfer,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<vscode.DocumentPasteEdit | undefined> {
|
||||
const enabled = vscode.workspace.getConfiguration('markdown', document).get('experimental.editor.pasteLinks.enabled', true);
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
|
||||
return snippet ? new vscode.DocumentPasteEdit(snippet) : undefined;
|
||||
}
|
||||
}, {
|
||||
pasteMimeTypes: ['text/uri-list']
|
||||
});
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
import { MdReferencesProvider } from './references';
|
||||
|
||||
export class MdDefinitionProvider extends Disposable implements vscode.DefinitionProvider {
|
||||
|
||||
constructor(private readonly referencesProvider: MdReferencesProvider) {
|
||||
super();
|
||||
}
|
||||
|
||||
async provideDefinition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
|
||||
const allRefs = await this.referencesProvider.getAllReferencesAtPosition(document, position, token);
|
||||
|
||||
return allRefs.find(ref => ref.kind === 'link' && ref.isDefinition)?.location;
|
||||
}
|
||||
}
|
||||
@@ -5,294 +5,79 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContents } from '../tableOfContents';
|
||||
import { noopToken } from '../test/util';
|
||||
import { Delayer } from '../util/async';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
||||
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider } from './documentLinkProvider';
|
||||
import { tryFindMdDocumentForLink } from './references';
|
||||
import { CommandManager } from '../commandManager';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export interface DiagnosticConfiguration {
|
||||
/**
|
||||
* Fired when the configuration changes.
|
||||
*/
|
||||
readonly onDidChange: vscode.Event<void>;
|
||||
|
||||
getOptions(resource: vscode.Uri): DiagnosticOptions;
|
||||
// Copied from markdown language service
|
||||
export enum DiagnosticCode {
|
||||
link_noSuchReferences = 'link.no-such-reference',
|
||||
link_noSuchHeaderInOwnFile = 'link.no-such-header-in-own-file',
|
||||
link_noSuchFile = 'link.no-such-file',
|
||||
link_noSuchHeaderInFile = 'link.no-such-header-in-file',
|
||||
}
|
||||
|
||||
export enum DiagnosticLevel {
|
||||
ignore = 'ignore',
|
||||
warning = 'warning',
|
||||
error = 'error',
|
||||
}
|
||||
|
||||
export interface DiagnosticOptions {
|
||||
readonly enabled: boolean;
|
||||
readonly validateReferences: DiagnosticLevel;
|
||||
readonly validateOwnHeaders: DiagnosticLevel;
|
||||
readonly validateFilePaths: DiagnosticLevel;
|
||||
}
|
||||
class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
|
||||
|
||||
function toSeverity(level: DiagnosticLevel): vscode.DiagnosticSeverity | undefined {
|
||||
switch (level) {
|
||||
case DiagnosticLevel.error: return vscode.DiagnosticSeverity.Error;
|
||||
case DiagnosticLevel.warning: return vscode.DiagnosticSeverity.Warning;
|
||||
case DiagnosticLevel.ignore: return undefined;
|
||||
}
|
||||
}
|
||||
private static readonly _addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks';
|
||||
|
||||
class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConfiguration {
|
||||
private static readonly metadata: vscode.CodeActionProviderMetadata = {
|
||||
providedCodeActionKinds: [
|
||||
vscode.CodeActionKind.QuickFix
|
||||
],
|
||||
};
|
||||
|
||||
private readonly _onDidChange = this._register(new vscode.EventEmitter<void>());
|
||||
public readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._register(vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('markdown.experimental.validate.enabled')) {
|
||||
this._onDidChange.fire();
|
||||
public static register(selector: vscode.DocumentSelector, commandManager: CommandManager): vscode.Disposable {
|
||||
const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider.metadata);
|
||||
const commandReg = commandManager.register({
|
||||
id: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
|
||||
execute(resource: vscode.Uri, path: string) {
|
||||
const settingId = 'experimental.validate.ignoreLinks';
|
||||
const config = vscode.workspace.getConfiguration('markdown', resource);
|
||||
const paths = new Set(config.get<string[]>(settingId, []));
|
||||
paths.add(path);
|
||||
config.update(settingId, [...paths], vscode.ConfigurationTarget.WorkspaceFolder);
|
||||
}
|
||||
}));
|
||||
});
|
||||
return vscode.Disposable.from(reg, commandReg);
|
||||
}
|
||||
|
||||
public getOptions(resource: vscode.Uri): DiagnosticOptions {
|
||||
const config = vscode.workspace.getConfiguration('markdown', resource);
|
||||
return {
|
||||
enabled: config.get<boolean>('experimental.validate.enabled', false),
|
||||
validateReferences: config.get<DiagnosticLevel>('experimental.validate.referenceLinks', DiagnosticLevel.ignore),
|
||||
validateOwnHeaders: config.get<DiagnosticLevel>('experimental.validate.headerLinks', DiagnosticLevel.ignore),
|
||||
validateFilePaths: config.get<DiagnosticLevel>('experimental.validate.fileLinks', DiagnosticLevel.ignore),
|
||||
};
|
||||
}
|
||||
}
|
||||
provideCodeActions(document: vscode.TextDocument, _range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, _token: vscode.CancellationToken): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
|
||||
const fixes: vscode.CodeAction[] = [];
|
||||
|
||||
export class DiagnosticManager extends Disposable {
|
||||
for (const diagnostic of context.diagnostics) {
|
||||
switch (diagnostic.code) {
|
||||
case DiagnosticCode.link_noSuchReferences:
|
||||
case DiagnosticCode.link_noSuchHeaderInOwnFile:
|
||||
case DiagnosticCode.link_noSuchFile:
|
||||
case DiagnosticCode.link_noSuchHeaderInFile: {
|
||||
const hrefText = (diagnostic as any).data?.hrefText;
|
||||
if (hrefText) {
|
||||
const fix = new vscode.CodeAction(
|
||||
localize('ignoreLinksQuickFix.title', "Exclude '{0}' from link validation.", hrefText),
|
||||
vscode.CodeActionKind.QuickFix);
|
||||
|
||||
private readonly collection: vscode.DiagnosticCollection;
|
||||
|
||||
private readonly pendingDiagnostics = new Set<vscode.Uri>();
|
||||
private readonly diagnosticDelayer: Delayer<void>;
|
||||
|
||||
constructor(
|
||||
private readonly computer: DiagnosticComputer,
|
||||
private readonly configuration: DiagnosticConfiguration,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.diagnosticDelayer = new Delayer(300);
|
||||
|
||||
this.collection = this._register(vscode.languages.createDiagnosticCollection('markdown'));
|
||||
|
||||
this._register(this.configuration.onDidChange(() => {
|
||||
this.rebuild();
|
||||
}));
|
||||
|
||||
const onDocUpdated = (doc: vscode.TextDocument) => {
|
||||
if (isMarkdownFile(doc)) {
|
||||
this.pendingDiagnostics.add(doc.uri);
|
||||
this.diagnosticDelayer.trigger(() => this.recomputePendingDiagnostics());
|
||||
}
|
||||
};
|
||||
|
||||
this._register(vscode.workspace.onDidOpenTextDocument(doc => {
|
||||
onDocUpdated(doc);
|
||||
}));
|
||||
|
||||
this._register(vscode.workspace.onDidChangeTextDocument(e => {
|
||||
onDocUpdated(e.document);
|
||||
}));
|
||||
|
||||
this._register(vscode.workspace.onDidCloseTextDocument(doc => {
|
||||
this.pendingDiagnostics.delete(doc.uri);
|
||||
this.collection.delete(doc.uri);
|
||||
}));
|
||||
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
private recomputePendingDiagnostics(): void {
|
||||
const pending = [...this.pendingDiagnostics];
|
||||
this.pendingDiagnostics.clear();
|
||||
|
||||
for (const resource of pending) {
|
||||
const doc = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === resource.fsPath);
|
||||
if (doc) {
|
||||
this.update(doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async rebuild() {
|
||||
this.collection.clear();
|
||||
|
||||
const allOpenedTabResources = this.getAllTabResources();
|
||||
await Promise.all(
|
||||
vscode.workspace.textDocuments
|
||||
.filter(doc => allOpenedTabResources.has(doc.uri.toString()) && isMarkdownFile(doc))
|
||||
.map(doc => this.update(doc)));
|
||||
}
|
||||
|
||||
private getAllTabResources() {
|
||||
const openedTabDocs = new Map<string, vscode.Uri>();
|
||||
for (const group of vscode.window.tabGroups.all) {
|
||||
for (const tab of group.tabs) {
|
||||
if (tab.input instanceof vscode.TabInputText) {
|
||||
openedTabDocs.set(tab.input.uri.toString(), tab.input.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
return openedTabDocs;
|
||||
}
|
||||
|
||||
private async update(doc: vscode.TextDocument): Promise<void> {
|
||||
const diagnostics = await this.getDiagnostics(doc, noopToken);
|
||||
this.collection.set(doc.uri, diagnostics);
|
||||
}
|
||||
|
||||
public async getDiagnostics(doc: SkinnyTextDocument, token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
|
||||
const config = this.configuration.getOptions(doc.uri);
|
||||
if (!config.enabled) {
|
||||
return [];
|
||||
}
|
||||
return this.computer.getDiagnostics(doc, config, token);
|
||||
}
|
||||
}
|
||||
|
||||
export class DiagnosticComputer {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine,
|
||||
private readonly workspaceContents: MdWorkspaceContents,
|
||||
private readonly linkProvider: MdLinkProvider,
|
||||
) { }
|
||||
|
||||
public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
|
||||
const links = await this.linkProvider.getAllLinks(doc, token);
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await Promise.all([
|
||||
this.validateFileLinks(doc, options, links, token),
|
||||
Array.from(this.validateReferenceLinks(options, links)),
|
||||
this.validateOwnHeaderLinks(doc, options, links, token),
|
||||
])).flat();
|
||||
}
|
||||
|
||||
private async validateOwnHeaderLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
|
||||
const severity = toSeverity(options.validateOwnHeaders);
|
||||
if (typeof severity === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const toc = await TableOfContents.create(this.engine, doc);
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const diagnostics: vscode.Diagnostic[] = [];
|
||||
for (const link of links) {
|
||||
if (link.href.kind === 'internal'
|
||||
&& link.href.path.toString() === doc.uri.toString()
|
||||
&& link.href.fragment
|
||||
&& !toc.lookup(link.href.fragment)
|
||||
) {
|
||||
diagnostics.push(new vscode.Diagnostic(
|
||||
link.source.hrefRange,
|
||||
localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment),
|
||||
severity));
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
private *validateReferenceLinks(options: DiagnosticOptions, links: readonly MdLink[]): Iterable<vscode.Diagnostic> {
|
||||
const severity = toSeverity(options.validateReferences);
|
||||
if (typeof severity === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const definitionSet = new LinkDefinitionSet(links);
|
||||
for (const link of links) {
|
||||
if (link.href.kind === 'reference' && !definitionSet.lookup(link.href.ref)) {
|
||||
yield new vscode.Diagnostic(
|
||||
link.source.hrefRange,
|
||||
localize('invalidReferenceLink', 'No link reference found: \'{0}\'', link.href.ref),
|
||||
severity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async validateFileLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
|
||||
const severity = toSeverity(options.validateFilePaths);
|
||||
if (typeof severity === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tocs = new Map<string, TableOfContents>();
|
||||
|
||||
// TODO: cache links so we don't recompute duplicate hrefs
|
||||
// TODO: parallelize
|
||||
|
||||
const diagnostics: vscode.Diagnostic[] = [];
|
||||
for (const link of links) {
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (link.href.kind !== 'internal') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hrefDoc = await tryFindMdDocumentForLink(link.href, this.workspaceContents);
|
||||
if (hrefDoc && hrefDoc.uri.toString() === doc.uri.toString()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hrefDoc && !await this.workspaceContents.pathExists(link.href.path)) {
|
||||
diagnostics.push(
|
||||
new vscode.Diagnostic(
|
||||
link.source.hrefRange,
|
||||
localize('invalidPathLink', 'File does not exist at path: {0}', (link.href as InternalHref).path.toString(true)),
|
||||
severity));
|
||||
} else if (hrefDoc) {
|
||||
if (link.href.fragment) {
|
||||
// validate fragment looks valid
|
||||
let hrefDocToc = tocs.get(link.href.path.toString());
|
||||
if (!hrefDocToc) {
|
||||
hrefDocToc = await TableOfContents.create(this.engine, hrefDoc);
|
||||
tocs.set(link.href.path.toString(), hrefDocToc);
|
||||
}
|
||||
|
||||
if (!hrefDocToc.lookup(link.href.fragment)) {
|
||||
diagnostics.push(
|
||||
new vscode.Diagnostic(
|
||||
link.source.hrefRange,
|
||||
localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', (link.href as InternalHref).path.fragment),
|
||||
severity));
|
||||
fix.command = {
|
||||
command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
|
||||
title: '',
|
||||
arguments: [document.uri, hrefText],
|
||||
};
|
||||
fixes.push(fix);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
return fixes;
|
||||
}
|
||||
}
|
||||
|
||||
export function register(
|
||||
engine: MarkdownEngine,
|
||||
workspaceContents: MdWorkspaceContents,
|
||||
linkProvider: MdLinkProvider,
|
||||
|
||||
export function registerDiagnosticSupport(
|
||||
selector: vscode.DocumentSelector,
|
||||
commandManager: CommandManager,
|
||||
): vscode.Disposable {
|
||||
const configuration = new VSCodeDiagnosticConfiguration();
|
||||
const manager = new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkProvider), configuration);
|
||||
return vscode.Disposable.from(configuration, manager);
|
||||
return AddToIgnoreLinksQuickFixProvider.register(selector, commandManager);
|
||||
}
|
||||
|
||||
@@ -1,421 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as uri from 'vscode-uri';
|
||||
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { coalesce } from '../util/arrays';
|
||||
import { getUriForLinkWithKnownExternalScheme, isOfScheme, Schemes } from '../util/schemes';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export interface ExternalHref {
|
||||
readonly kind: 'external';
|
||||
readonly uri: vscode.Uri;
|
||||
}
|
||||
|
||||
export interface InternalHref {
|
||||
readonly kind: 'internal';
|
||||
readonly path: vscode.Uri;
|
||||
readonly fragment: string;
|
||||
}
|
||||
|
||||
export interface ReferenceHref {
|
||||
readonly kind: 'reference';
|
||||
readonly ref: string;
|
||||
}
|
||||
|
||||
export type LinkHref = ExternalHref | InternalHref | ReferenceHref;
|
||||
|
||||
|
||||
function parseLink(
|
||||
document: SkinnyTextDocument,
|
||||
link: string,
|
||||
): ExternalHref | InternalHref | undefined {
|
||||
const cleanLink = stripAngleBrackets(link);
|
||||
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(cleanLink);
|
||||
if (externalSchemeUri) {
|
||||
// Normalize VS Code links to target currently running version
|
||||
if (isOfScheme(Schemes.vscode, link) || isOfScheme(Schemes['vscode-insiders'], link)) {
|
||||
return { kind: 'external', uri: vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }) };
|
||||
}
|
||||
return { kind: 'external', uri: externalSchemeUri };
|
||||
}
|
||||
|
||||
// Assume it must be an relative or absolute file path
|
||||
// Use a fake scheme to avoid parse warnings
|
||||
const tempUri = vscode.Uri.parse(`vscode-resource:${link}`);
|
||||
|
||||
let resourceUri: vscode.Uri | undefined;
|
||||
if (!tempUri.path) {
|
||||
resourceUri = document.uri;
|
||||
} else if (tempUri.path[0] === '/') {
|
||||
const root = getWorkspaceFolder(document);
|
||||
if (root) {
|
||||
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
|
||||
}
|
||||
} else {
|
||||
if (document.uri.scheme === Schemes.untitled) {
|
||||
const root = getWorkspaceFolder(document);
|
||||
if (root) {
|
||||
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
|
||||
}
|
||||
} else {
|
||||
const base = uri.Utils.dirname(document.uri);
|
||||
resourceUri = vscode.Uri.joinPath(base, tempUri.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!resourceUri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'internal',
|
||||
path: resourceUri.with({ fragment: '' }),
|
||||
fragment: tempUri.fragment,
|
||||
};
|
||||
}
|
||||
|
||||
function getWorkspaceFolder(document: SkinnyTextDocument) {
|
||||
return vscode.workspace.getWorkspaceFolder(document.uri)?.uri
|
||||
|| vscode.workspace.workspaceFolders?.[0]?.uri;
|
||||
}
|
||||
|
||||
interface MdLinkSource {
|
||||
readonly text: string;
|
||||
readonly resource: vscode.Uri;
|
||||
readonly hrefRange: vscode.Range;
|
||||
readonly fragmentRange: vscode.Range | undefined;
|
||||
}
|
||||
|
||||
export interface MdInlineLink {
|
||||
readonly kind: 'link';
|
||||
readonly source: MdLinkSource;
|
||||
readonly href: LinkHref;
|
||||
}
|
||||
|
||||
export interface MdLinkDefinition {
|
||||
readonly kind: 'definition';
|
||||
readonly source: MdLinkSource;
|
||||
readonly ref: {
|
||||
readonly range: vscode.Range;
|
||||
readonly text: string;
|
||||
};
|
||||
readonly href: ExternalHref | InternalHref;
|
||||
}
|
||||
|
||||
export type MdLink = MdInlineLink | MdLinkDefinition;
|
||||
|
||||
function extractDocumentLink(
|
||||
document: SkinnyTextDocument,
|
||||
pre: number,
|
||||
link: string,
|
||||
matchIndex: number | undefined
|
||||
): MdLink | undefined {
|
||||
const offset = (matchIndex || 0) + pre;
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
try {
|
||||
const linkTarget = parseLink(document, link);
|
||||
if (!linkTarget) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
kind: 'link',
|
||||
href: linkTarget,
|
||||
source: {
|
||||
text: link,
|
||||
resource: document.uri,
|
||||
hrefRange: new vscode.Range(linkStart, linkEnd),
|
||||
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getFragmentRange(text: string, start: vscode.Position, end: vscode.Position): vscode.Range | undefined {
|
||||
const index = text.indexOf('#');
|
||||
if (index < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return new vscode.Range(start.translate({ characterDelta: index + 1 }), end);
|
||||
}
|
||||
|
||||
const angleBracketLinkRe = /^<(.*)>$/;
|
||||
|
||||
/**
|
||||
* Used to strip brackets from the markdown link
|
||||
*
|
||||
* <http://example.com> will be transformed to http://example.com
|
||||
*/
|
||||
function stripAngleBrackets(link: string) {
|
||||
return link.replace(angleBracketLinkRe, '$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches `[text](link)`
|
||||
*/
|
||||
const linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g;
|
||||
|
||||
/**
|
||||
* Matches `[text][ref]`
|
||||
*/
|
||||
const referenceLinkPattern = /(?:(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]|\[\s*?([^\s\]]*?)\])(?![\:\(])/g;
|
||||
|
||||
/**
|
||||
* Matches `<http://example.com>`
|
||||
*/
|
||||
const autoLinkPattern = /\<(\w+:[^\>\s]+)\>/g;
|
||||
|
||||
/**
|
||||
* Matches `[text]: link`
|
||||
*/
|
||||
const definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)([^<]\S*|<[^>]+>)/gm;
|
||||
|
||||
const inlineCodePattern = /(?:^|[^`])(`+)(?:.+?|.*?(?:(?:\r?\n).+?)*?)(?:\r?\n)?\1(?:$|[^`])/gm;
|
||||
|
||||
interface CodeInDocument {
|
||||
/**
|
||||
* code blocks and fences each represented by [line_start,line_end).
|
||||
*/
|
||||
readonly multiline: ReadonlyArray<[number, number]>;
|
||||
|
||||
/**
|
||||
* inline code spans each represented by {@link vscode.Range}.
|
||||
*/
|
||||
readonly inline: readonly vscode.Range[];
|
||||
}
|
||||
|
||||
async function findCode(document: SkinnyTextDocument, engine: MarkdownEngine): Promise<CodeInDocument> {
|
||||
const tokens = await engine.parse(document);
|
||||
const multiline = tokens.filter(t => (t.type === 'code_block' || t.type === 'fence') && !!t.map).map(t => t.map) as [number, number][];
|
||||
|
||||
const text = document.getText();
|
||||
const inline = [...text.matchAll(inlineCodePattern)].map(match => {
|
||||
const start = match.index || 0;
|
||||
return new vscode.Range(document.positionAt(start), document.positionAt(start + match[0].length));
|
||||
});
|
||||
|
||||
return { multiline, inline };
|
||||
}
|
||||
|
||||
function isLinkInsideCode(code: CodeInDocument, linkHrefRange: vscode.Range) {
|
||||
return code.multiline.some(interval => linkHrefRange.start.line >= interval[0] && linkHrefRange.start.line < interval[1]) ||
|
||||
code.inline.some(position => position.intersection(linkHrefRange));
|
||||
}
|
||||
|
||||
export class MdLinkProvider implements vscode.DocumentLinkProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideDocumentLinks(
|
||||
document: SkinnyTextDocument,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.DocumentLink[]> {
|
||||
const allLinks = await this.getAllLinks(document, token);
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const definitionSet = new LinkDefinitionSet(allLinks);
|
||||
return coalesce(allLinks
|
||||
.map(data => this.toValidDocumentLink(data, definitionSet)));
|
||||
}
|
||||
|
||||
private toValidDocumentLink(link: MdLink, definitionSet: LinkDefinitionSet): vscode.DocumentLink | undefined {
|
||||
switch (link.href.kind) {
|
||||
case 'external': {
|
||||
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': {
|
||||
const def = definitionSet.lookup(link.href.ref);
|
||||
if (def) {
|
||||
return 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]))}`));
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllLinks(document: SkinnyTextDocument, token: vscode.CancellationToken): Promise<MdLink[]> {
|
||||
const codeInDocument = await findCode(document, this.engine);
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from([
|
||||
...this.getInlineLinks(document, codeInDocument),
|
||||
...this.getReferenceLinks(document, codeInDocument),
|
||||
...this.getLinkDefinitions2(document, codeInDocument),
|
||||
...this.getAutoLinks(document, codeInDocument),
|
||||
]);
|
||||
}
|
||||
|
||||
private *getInlineLinks(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable<MdLink> {
|
||||
const text = document.getText();
|
||||
|
||||
for (const match of text.matchAll(linkPattern)) {
|
||||
const matchImageData = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index);
|
||||
if (matchImageData && !isLinkInsideCode(codeInDocument, matchImageData.source.hrefRange)) {
|
||||
yield matchImageData;
|
||||
}
|
||||
const matchLinkData = extractDocumentLink(document, match[1].length, match[5], match.index);
|
||||
if (matchLinkData && !isLinkInsideCode(codeInDocument, matchLinkData.source.hrefRange)) {
|
||||
yield matchLinkData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private *getAutoLinks(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable<MdLink> {
|
||||
const text = document.getText();
|
||||
|
||||
for (const match of text.matchAll(autoLinkPattern)) {
|
||||
const link = match[1];
|
||||
const linkTarget = parseLink(document, link);
|
||||
if (linkTarget) {
|
||||
const offset = (match.index ?? 0) + 1;
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
const hrefRange = new vscode.Range(linkStart, linkEnd);
|
||||
if (isLinkInsideCode(codeInDocument, hrefRange)) {
|
||||
continue;
|
||||
}
|
||||
yield {
|
||||
kind: 'link',
|
||||
href: linkTarget,
|
||||
source: {
|
||||
text: link,
|
||||
resource: document.uri,
|
||||
hrefRange: new vscode.Range(linkStart, linkEnd),
|
||||
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private *getReferenceLinks(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable<MdLink> {
|
||||
const text = document.getText();
|
||||
for (const match of text.matchAll(referenceLinkPattern)) {
|
||||
let linkStart: vscode.Position;
|
||||
let linkEnd: vscode.Position;
|
||||
let reference = match[3];
|
||||
if (reference) { // [text][ref]
|
||||
const pre = match[1];
|
||||
const offset = (match.index || 0) + pre.length;
|
||||
linkStart = document.positionAt(offset);
|
||||
linkEnd = document.positionAt(offset + reference.length);
|
||||
} else if (match[4]) { // [ref][], [ref]
|
||||
reference = match[4];
|
||||
const offset = (match.index || 0) + 1;
|
||||
linkStart = document.positionAt(offset);
|
||||
linkEnd = document.positionAt(offset + reference.length);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hrefRange = new vscode.Range(linkStart, linkEnd);
|
||||
if (isLinkInsideCode(codeInDocument, hrefRange)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield {
|
||||
kind: 'link',
|
||||
source: {
|
||||
text: reference,
|
||||
resource: document.uri,
|
||||
hrefRange,
|
||||
fragmentRange: undefined,
|
||||
},
|
||||
href: {
|
||||
kind: 'reference',
|
||||
ref: reference,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async getLinkDefinitions(document: SkinnyTextDocument): Promise<Iterable<MdLinkDefinition>> {
|
||||
const codeInDocument = await findCode(document, this.engine);
|
||||
return this.getLinkDefinitions2(document, codeInDocument);
|
||||
}
|
||||
|
||||
private *getLinkDefinitions2(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable<MdLinkDefinition> {
|
||||
const text = document.getText();
|
||||
for (const match of text.matchAll(definitionPattern)) {
|
||||
const pre = match[1];
|
||||
const reference = match[2];
|
||||
const link = match[3].trim();
|
||||
const offset = (match.index || 0) + pre.length;
|
||||
|
||||
const refStart = document.positionAt((match.index ?? 0) + 1);
|
||||
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
|
||||
|
||||
let linkStart: vscode.Position;
|
||||
let linkEnd: vscode.Position;
|
||||
let text: string;
|
||||
if (angleBracketLinkRe.test(link)) {
|
||||
linkStart = document.positionAt(offset + 1);
|
||||
linkEnd = document.positionAt(offset + link.length - 1);
|
||||
text = link.substring(1, link.length - 1);
|
||||
} else {
|
||||
linkStart = document.positionAt(offset);
|
||||
linkEnd = document.positionAt(offset + link.length);
|
||||
text = link;
|
||||
}
|
||||
const hrefRange = new vscode.Range(linkStart, linkEnd);
|
||||
if (isLinkInsideCode(codeInDocument, hrefRange)) {
|
||||
continue;
|
||||
}
|
||||
const target = parseLink(document, text);
|
||||
if (target) {
|
||||
yield {
|
||||
kind: 'definition',
|
||||
source: {
|
||||
text: link,
|
||||
resource: document.uri,
|
||||
hrefRange,
|
||||
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
|
||||
},
|
||||
ref: { text: reference, range: refRange },
|
||||
href: target,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkDefinitionSet {
|
||||
private readonly _map = new Map<string, MdLinkDefinition>();
|
||||
|
||||
constructor(links: Iterable<MdLink>) {
|
||||
for (const link of links) {
|
||||
if (link.kind === 'definition') {
|
||||
this._map.set(link.ref.text, link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public lookup(ref: string): MdLinkDefinition | undefined {
|
||||
return this._map.get(ref);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContents, TocEntry } from '../tableOfContents';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
interface MarkdownSymbol {
|
||||
readonly level: number;
|
||||
readonly parent: MarkdownSymbol | undefined;
|
||||
readonly children: vscode.DocumentSymbol[];
|
||||
}
|
||||
|
||||
export class MdDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideDocumentSymbolInformation(document: SkinnyTextDocument): Promise<vscode.SymbolInformation[]> {
|
||||
const toc = await TableOfContents.create(this.engine, document);
|
||||
return toc.entries.map(entry => this.toSymbolInformation(entry));
|
||||
}
|
||||
|
||||
public async provideDocumentSymbols(document: SkinnyTextDocument): Promise<vscode.DocumentSymbol[]> {
|
||||
const toc = await TableOfContents.create(this.engine, document);
|
||||
const root: MarkdownSymbol = {
|
||||
level: -Infinity,
|
||||
children: [],
|
||||
parent: undefined
|
||||
};
|
||||
this.buildTree(root, toc.entries);
|
||||
return root.children;
|
||||
}
|
||||
|
||||
private buildTree(parent: MarkdownSymbol, entries: readonly TocEntry[]) {
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = entries[0];
|
||||
const symbol = this.toDocumentSymbol(entry);
|
||||
symbol.children = [];
|
||||
|
||||
while (parent && entry.level <= parent.level) {
|
||||
parent = parent.parent!;
|
||||
}
|
||||
parent.children.push(symbol);
|
||||
this.buildTree({ level: entry.level, children: symbol.children, parent }, entries.slice(1));
|
||||
}
|
||||
|
||||
|
||||
private toSymbolInformation(entry: TocEntry): vscode.SymbolInformation {
|
||||
return new vscode.SymbolInformation(
|
||||
this.getSymbolName(entry),
|
||||
vscode.SymbolKind.String,
|
||||
'',
|
||||
entry.sectionLocation);
|
||||
}
|
||||
|
||||
private toDocumentSymbol(entry: TocEntry) {
|
||||
return new vscode.DocumentSymbol(
|
||||
this.getSymbolName(entry),
|
||||
'',
|
||||
vscode.SymbolKind.String,
|
||||
entry.sectionLocation.range,
|
||||
entry.sectionLocation.range);
|
||||
}
|
||||
|
||||
private getSymbolName(entry: TocEntry): string {
|
||||
return '#'.repeat(entry.level) + ' ' + entry.text;
|
||||
}
|
||||
}
|
||||
@@ -23,49 +23,54 @@ const imageFileExtensions = new Set<string>([
|
||||
'.webp',
|
||||
]);
|
||||
|
||||
export function registerDropIntoEditor(selector: vscode.DocumentSelector) {
|
||||
return vscode.languages.registerDocumentOnDropProvider(selector, new class implements vscode.DocumentOnDropProvider {
|
||||
async provideDocumentOnDropEdits(document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise<vscode.SnippetTextEdit | undefined> {
|
||||
export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector) {
|
||||
return vscode.languages.registerDocumentDropEditProvider(selector, new class implements vscode.DocumentDropEditProvider {
|
||||
async provideDocumentDropEdits(document: vscode.TextDocument, _position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
|
||||
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const urlList = await dataTransfer.get('text/uri-list')?.asString();
|
||||
if (!urlList) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const uris: vscode.Uri[] = [];
|
||||
for (const resource of urlList.split('\n')) {
|
||||
try {
|
||||
uris.push(vscode.Uri.parse(resource));
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (!uris.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snippet = new vscode.SnippetString();
|
||||
uris.forEach((uri, i) => {
|
||||
const mdPath = document.uri.scheme === uri.scheme
|
||||
? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath))
|
||||
: uri.toString(false);
|
||||
|
||||
const ext = URI.Utils.extname(uri).toLowerCase();
|
||||
snippet.appendText(imageFileExtensions.has(ext) ? '`);
|
||||
|
||||
if (i <= uris.length - 1 && uris.length > 1) {
|
||||
snippet.appendText(' ');
|
||||
}
|
||||
});
|
||||
|
||||
return new vscode.SnippetTextEdit(new vscode.Range(position, position), snippet);
|
||||
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
|
||||
return snippet ? new vscode.DocumentDropEdit(snippet) : undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.SnippetString | undefined> {
|
||||
const urlList = await dataTransfer.get('text/uri-list')?.asString();
|
||||
if (!urlList || token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const uris: vscode.Uri[] = [];
|
||||
for (const resource of urlList.split('\n')) {
|
||||
try {
|
||||
uris.push(vscode.Uri.parse(resource));
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (!uris.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snippet = new vscode.SnippetString();
|
||||
uris.forEach((uri, i) => {
|
||||
const mdPath = document.uri.scheme === uri.scheme
|
||||
? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath).replace(/\\/g, '/'))
|
||||
: uri.toString(false);
|
||||
|
||||
const ext = URI.Utils.extname(uri).toLowerCase();
|
||||
snippet.appendText(imageFileExtensions.has(ext) ? '`);
|
||||
|
||||
if (i <= uris.length - 1 && uris.length > 1) {
|
||||
snippet.appendText(' ');
|
||||
}
|
||||
});
|
||||
|
||||
return snippet;
|
||||
}
|
||||
|
||||
@@ -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.getAllReferencesToFile(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');
|
||||
@@ -49,6 +51,9 @@ export class FindFileReferencesCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
export function registerFindFileReferences(commandManager: CommandManager, referencesProvider: MdReferencesProvider): vscode.Disposable {
|
||||
return commandManager.register(new FindFileReferencesCommand(referencesProvider));
|
||||
export function registerFindFileReferenceSupport(
|
||||
commandManager: CommandManager,
|
||||
client: BaseLanguageClient,
|
||||
): vscode.Disposable {
|
||||
return commandManager.register(new FindFileReferencesCommand(client));
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContents } from '../tableOfContents';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
const rangeLimit = 5000;
|
||||
|
||||
interface MarkdownItTokenWithMap extends Token {
|
||||
map: [number, number];
|
||||
}
|
||||
|
||||
export class MdFoldingProvider implements vscode.FoldingRangeProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideFoldingRanges(
|
||||
document: SkinnyTextDocument,
|
||||
_: vscode.FoldingContext,
|
||||
_token: vscode.CancellationToken
|
||||
): Promise<vscode.FoldingRange[]> {
|
||||
const foldables = await Promise.all([
|
||||
this.getRegions(document),
|
||||
this.getHeaderFoldingRanges(document),
|
||||
this.getBlockFoldingRanges(document)
|
||||
]);
|
||||
return foldables.flat().slice(0, rangeLimit);
|
||||
}
|
||||
|
||||
private async getRegions(document: SkinnyTextDocument): Promise<vscode.FoldingRange[]> {
|
||||
const tokens = await this.engine.parse(document);
|
||||
const regionMarkers = tokens.filter(isRegionMarker)
|
||||
.map(token => ({ line: token.map[0], isStart: isStartRegion(token.content) }));
|
||||
|
||||
const nestingStack: { line: number; isStart: boolean }[] = [];
|
||||
return regionMarkers
|
||||
.map(marker => {
|
||||
if (marker.isStart) {
|
||||
nestingStack.push(marker);
|
||||
} else if (nestingStack.length && nestingStack[nestingStack.length - 1].isStart) {
|
||||
return new vscode.FoldingRange(nestingStack.pop()!.line, marker.line, vscode.FoldingRangeKind.Region);
|
||||
} else {
|
||||
// noop: invalid nesting (i.e. [end, start] or [start, end, end])
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((region: vscode.FoldingRange | null): region is vscode.FoldingRange => !!region);
|
||||
}
|
||||
|
||||
private async getHeaderFoldingRanges(document: SkinnyTextDocument) {
|
||||
const toc = await TableOfContents.create(this.engine, document);
|
||||
return toc.entries.map(entry => {
|
||||
let endLine = entry.sectionLocation.range.end.line;
|
||||
if (document.lineAt(endLine).isEmptyOrWhitespace && endLine >= entry.line + 1) {
|
||||
endLine = endLine - 1;
|
||||
}
|
||||
return new vscode.FoldingRange(entry.line, endLine);
|
||||
});
|
||||
}
|
||||
|
||||
private async getBlockFoldingRanges(document: SkinnyTextDocument): Promise<vscode.FoldingRange[]> {
|
||||
const tokens = await this.engine.parse(document);
|
||||
const multiLineListItems = tokens.filter(isFoldableToken);
|
||||
return multiLineListItems.map(listItem => {
|
||||
const start = listItem.map[0];
|
||||
let end = listItem.map[1] - 1;
|
||||
if (document.lineAt(end).isEmptyOrWhitespace && end >= start + 1) {
|
||||
end = end - 1;
|
||||
}
|
||||
return new vscode.FoldingRange(start, end, this.getFoldingRangeKind(listItem));
|
||||
});
|
||||
}
|
||||
|
||||
private getFoldingRangeKind(listItem: Token): vscode.FoldingRangeKind | undefined {
|
||||
return listItem.type === 'html_block' && listItem.content.startsWith('<!--')
|
||||
? vscode.FoldingRangeKind.Comment
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const isStartRegion = (t: string) => /^\s*<!--\s*#?region\b.*-->/.test(t);
|
||||
const isEndRegion = (t: string) => /^\s*<!--\s*#?endregion\b.*-->/.test(t);
|
||||
|
||||
const isRegionMarker = (token: Token): token is MarkdownItTokenWithMap =>
|
||||
!!token.map && token.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content));
|
||||
|
||||
const isFoldableToken = (token: Token): token is MarkdownItTokenWithMap => {
|
||||
if (!token.map) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (token.type) {
|
||||
case 'fence':
|
||||
case 'list_item_open':
|
||||
return token.map[1] > token.map[0];
|
||||
|
||||
case 'html_block':
|
||||
if (isRegionMarker(token)) {
|
||||
return false;
|
||||
}
|
||||
return token.map[1] > token.map[0] + 1;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,353 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { dirname, resolve } from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContents } from '../tableOfContents';
|
||||
import { resolveUriToMarkdownFile } from '../util/openDocumentLink';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
import { MdLinkProvider } from './documentLinkProvider';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function tryDecodeUriComponent(str: string): string {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
export class MdPathCompletionProvider implements vscode.CompletionItemProvider {
|
||||
|
||||
public static register(
|
||||
selector: vscode.DocumentSelector,
|
||||
engine: MarkdownEngine,
|
||||
linkProvider: MdLinkProvider,
|
||||
): vscode.Disposable {
|
||||
return vscode.languages.registerCompletionItemProvider(selector, new MdPathCompletionProvider(engine, linkProvider), '.', '/', '#');
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine,
|
||||
private readonly linkProvider: MdLinkProvider,
|
||||
) { }
|
||||
|
||||
public async provideCompletionItems(document: SkinnyTextDocument, 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(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: SkinnyTextDocument): 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: SkinnyTextDocument, position: vscode.Position): CompletionContext | undefined {
|
||||
const line = document.lineAt(position.line).text;
|
||||
|
||||
const linePrefixText = line.slice(0, position.character);
|
||||
const lineSuffixText = line.slice(position.character);
|
||||
|
||||
const linkPrefixMatch = linePrefixText.match(this.linkStartPattern);
|
||||
if (linkPrefixMatch) {
|
||||
const prefix = linkPrefixMatch[2];
|
||||
if (this.refLooksLikeUrl(prefix)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const suffix = lineSuffixText.match(/^[^\)\s]*/);
|
||||
return {
|
||||
kind: CompletionContextKind.Link,
|
||||
linkPrefix: tryDecodeUriComponent(prefix),
|
||||
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
|
||||
linkSuffix: suffix ? suffix[0] : '',
|
||||
anchorInfo: this.getAnchorContext(prefix),
|
||||
};
|
||||
}
|
||||
|
||||
const definitionLinkPrefixMatch = linePrefixText.match(this.definitionPattern);
|
||||
if (definitionLinkPrefixMatch) {
|
||||
const prefix = definitionLinkPrefixMatch[1];
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
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: SkinnyTextDocument, 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.getLinkDefinitions(document);
|
||||
for (const def of definitions) {
|
||||
yield {
|
||||
kind: vscode.CompletionItemKind.Reference,
|
||||
label: def.ref.text,
|
||||
range: {
|
||||
inserting: insertionRange,
|
||||
replacing: replacementRange,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async *provideHeaderSuggestions(document: SkinnyTextDocument, position: vscode.Position, context: CompletionContext, insertionRange: vscode.Range): AsyncIterable<vscode.CompletionItem> {
|
||||
const toc = await TableOfContents.createForDocumentOrNotebook(this.engine, 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: SkinnyTextDocument, 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: Array<[string, vscode.FileType]>;
|
||||
try {
|
||||
dirInfo = await vscode.workspace.fs.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: isDir ? encodeURIComponent(name) + '/' : encodeURIComponent(name),
|
||||
kind: isDir ? vscode.CompletionItemKind.Folder : vscode.CompletionItemKind.File,
|
||||
range: {
|
||||
inserting: insertRange,
|
||||
replacing: replacementRange,
|
||||
},
|
||||
command: isDir ? { command: 'editor.action.triggerSuggest', title: '' } : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private resolveReference(document: SkinnyTextDocument, 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 === '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: SkinnyTextDocument) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
import * as uri from 'vscode-uri';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { Slugifier } from '../slugify';
|
||||
import { TableOfContents, TocEntry } from '../tableOfContents';
|
||||
import { noopToken } from '../test/util';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
||||
import { InternalHref, MdLink, MdLinkProvider } from './documentLinkProvider';
|
||||
import { MdWorkspaceCache } from './workspaceCache';
|
||||
|
||||
|
||||
/**
|
||||
* A link in a markdown file.
|
||||
*/
|
||||
export interface MdLinkReference {
|
||||
readonly kind: 'link';
|
||||
readonly isTriggerLocation: boolean;
|
||||
readonly isDefinition: boolean;
|
||||
readonly location: vscode.Location;
|
||||
|
||||
readonly link: MdLink;
|
||||
}
|
||||
|
||||
/**
|
||||
* A header in a markdown file.
|
||||
*/
|
||||
export interface MdHeaderReference {
|
||||
readonly kind: 'header';
|
||||
|
||||
readonly isTriggerLocation: boolean;
|
||||
readonly isDefinition: boolean;
|
||||
|
||||
/**
|
||||
* The range of the header.
|
||||
*
|
||||
* In `# a b c #` this would be the range of `# a b c #`
|
||||
*/
|
||||
readonly location: vscode.Location;
|
||||
|
||||
/**
|
||||
* The text of the header.
|
||||
*
|
||||
* In `# a b c #` this would be `a b c`
|
||||
*/
|
||||
readonly headerText: string;
|
||||
|
||||
/**
|
||||
* The range of the header text itself.
|
||||
*
|
||||
* In `# a b c #` this would be the range of `a b c`
|
||||
*/
|
||||
readonly headerTextLocation: vscode.Location;
|
||||
}
|
||||
|
||||
export type MdReference = MdLinkReference | MdHeaderReference;
|
||||
|
||||
export class MdReferencesProvider extends Disposable implements vscode.ReferenceProvider {
|
||||
|
||||
private readonly _linkCache: MdWorkspaceCache<readonly MdLink[]>;
|
||||
|
||||
public constructor(
|
||||
private readonly linkProvider: MdLinkProvider,
|
||||
private readonly workspaceContents: MdWorkspaceContents,
|
||||
private readonly engine: MarkdownEngine,
|
||||
private readonly slugifier: Slugifier,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._linkCache = this._register(new MdWorkspaceCache(workspaceContents, doc => linkProvider.getAllLinks(doc, noopToken)));
|
||||
}
|
||||
|
||||
async provideReferences(document: SkinnyTextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise<vscode.Location[] | undefined> {
|
||||
const allRefs = await this.getAllReferencesAtPosition(document, position, token);
|
||||
|
||||
return allRefs
|
||||
.filter(ref => context.includeDeclaration || !ref.isDefinition)
|
||||
.map(ref => ref.location);
|
||||
}
|
||||
|
||||
public async getAllReferencesAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
|
||||
const toc = await TableOfContents.create(this.engine, document);
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const header = toc.entries.find(entry => entry.line === position.line);
|
||||
if (header) {
|
||||
return this.getReferencesToHeader(document, header);
|
||||
} else {
|
||||
return this.getReferencesToLinkAtPosition(document, position, token);
|
||||
}
|
||||
}
|
||||
|
||||
private async getReferencesToHeader(document: SkinnyTextDocument, header: TocEntry): Promise<MdReference[]> {
|
||||
const links = (await this._linkCache.getAll()).flat();
|
||||
|
||||
const references: MdReference[] = [];
|
||||
|
||||
references.push({
|
||||
kind: 'header',
|
||||
isTriggerLocation: true,
|
||||
isDefinition: true,
|
||||
location: header.headerLocation,
|
||||
headerText: header.text,
|
||||
headerTextLocation: header.headerTextLocation
|
||||
});
|
||||
|
||||
for (const link of links) {
|
||||
if (link.href.kind === 'internal'
|
||||
&& this.looksLikeLinkToDoc(link.href, document.uri)
|
||||
&& this.slugifier.fromHeading(link.href.fragment).value === header.slug.value
|
||||
) {
|
||||
references.push({
|
||||
kind: 'link',
|
||||
isTriggerLocation: false,
|
||||
isDefinition: false,
|
||||
link,
|
||||
location: new vscode.Location(link.source.resource, link.source.hrefRange),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
private async getReferencesToLinkAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
|
||||
const docLinks = await this.linkProvider.getAllLinks(document, token);
|
||||
|
||||
for (const link of docLinks) {
|
||||
if (link.kind === 'definition') {
|
||||
// We could be in either the ref name or the definition
|
||||
if (link.ref.range.contains(position)) {
|
||||
return Array.from(this.getReferencesToLinkReference(docLinks, link.ref.text, { resource: document.uri, range: link.ref.range }));
|
||||
} else if (link.source.hrefRange.contains(position)) {
|
||||
return this.getReferencesToLink(link, position, token);
|
||||
}
|
||||
} else {
|
||||
if (link.source.hrefRange.contains(position)) {
|
||||
return this.getReferencesToLink(link, position, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private async getReferencesToLink(sourceLink: MdLink, triggerPosition: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
|
||||
const allLinksInWorkspace = (await this._linkCache.getAll()).flat();
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (sourceLink.href.kind === 'reference') {
|
||||
return Array.from(this.getReferencesToLinkReference(allLinksInWorkspace, sourceLink.href.ref, { resource: sourceLink.source.resource, range: sourceLink.source.hrefRange }));
|
||||
}
|
||||
|
||||
if (sourceLink.href.kind === 'external') {
|
||||
const references: MdReference[] = [];
|
||||
|
||||
for (const link of allLinksInWorkspace) {
|
||||
if (link.href.kind === 'external' && link.href.uri.toString() === sourceLink.href.uri.toString()) {
|
||||
const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
|
||||
references.push({
|
||||
kind: 'link',
|
||||
isTriggerLocation,
|
||||
isDefinition: false,
|
||||
link,
|
||||
location: new vscode.Location(link.source.resource, link.source.hrefRange),
|
||||
});
|
||||
}
|
||||
}
|
||||
return references;
|
||||
}
|
||||
|
||||
const targetDoc = await tryFindMdDocumentForLink(sourceLink.href, this.workspaceContents);
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const references: MdReference[] = [];
|
||||
|
||||
if (targetDoc && sourceLink.href.fragment && sourceLink.source.fragmentRange?.contains(triggerPosition)) {
|
||||
const toc = await TableOfContents.create(this.engine, targetDoc);
|
||||
const entry = toc.lookup(sourceLink.href.fragment);
|
||||
if (entry) {
|
||||
references.push({
|
||||
kind: 'header',
|
||||
isTriggerLocation: false,
|
||||
isDefinition: true,
|
||||
location: entry.headerLocation,
|
||||
headerText: entry.text,
|
||||
headerTextLocation: entry.headerTextLocation
|
||||
});
|
||||
}
|
||||
|
||||
for (const link of allLinksInWorkspace) {
|
||||
if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, targetDoc.uri)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.slugifier.fromHeading(link.href.fragment).equals(this.slugifier.fromHeading(sourceLink.href.fragment))) {
|
||||
const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
|
||||
references.push({
|
||||
kind: 'link',
|
||||
isTriggerLocation,
|
||||
isDefinition: false,
|
||||
link,
|
||||
location: new vscode.Location(link.source.resource, link.source.hrefRange),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else { // Triggered on a link without a fragment so we only require matching the file and ignore fragments
|
||||
references.push(...this.findAllLinksToFile(targetDoc?.uri ?? sourceLink.href.path, allLinksInWorkspace, sourceLink));
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
private looksLikeLinkToDoc(href: InternalHref, targetDoc: vscode.Uri) {
|
||||
return href.path.fsPath === targetDoc.fsPath
|
||||
|| uri.Utils.extname(href.path) === '' && href.path.with({ path: href.path.path + '.md' }).fsPath === targetDoc.fsPath;
|
||||
}
|
||||
|
||||
public async getAllReferencesToFile(resource: vscode.Uri, _token: vscode.CancellationToken): Promise<MdReference[]> {
|
||||
const allLinksInWorkspace = (await this._linkCache.getAll()).flat();
|
||||
return Array.from(this.findAllLinksToFile(resource, allLinksInWorkspace, undefined));
|
||||
}
|
||||
|
||||
private * findAllLinksToFile(resource: vscode.Uri, allLinksInWorkspace: readonly MdLink[], sourceLink: MdLink | undefined): Iterable<MdReference> {
|
||||
for (const link of allLinksInWorkspace) {
|
||||
if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, resource)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Exclude cases where the file is implicitly referencing itself
|
||||
if (link.source.text.startsWith('#') && link.source.resource.fsPath === resource.fsPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isTriggerLocation = !!sourceLink && sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
|
||||
const pathRange = this.getPathRange(link);
|
||||
yield {
|
||||
kind: 'link',
|
||||
isTriggerLocation,
|
||||
isDefinition: false,
|
||||
link,
|
||||
location: new vscode.Location(link.source.resource, pathRange),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private * getReferencesToLinkReference(allLinks: Iterable<MdLink>, refToFind: string, from: { resource: vscode.Uri; range: vscode.Range }): Iterable<MdReference> {
|
||||
for (const link of allLinks) {
|
||||
let ref: string;
|
||||
if (link.kind === 'definition') {
|
||||
ref = link.ref.text;
|
||||
} else if (link.href.kind === 'reference') {
|
||||
ref = link.href.ref;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ref === refToFind && link.source.resource.fsPath === from.resource.fsPath) {
|
||||
const isTriggerLocation = from.resource.fsPath === link.source.resource.fsPath && (
|
||||
(link.href.kind === 'reference' && from.range.isEqual(link.source.hrefRange)) || (link.kind === 'definition' && from.range.isEqual(link.ref.range)));
|
||||
|
||||
const pathRange = this.getPathRange(link);
|
||||
yield {
|
||||
kind: 'link',
|
||||
isTriggerLocation,
|
||||
isDefinition: link.kind === 'definition',
|
||||
link,
|
||||
location: new vscode.Location(from.resource, pathRange),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get just the range of the file path, dropping the fragment
|
||||
*/
|
||||
private getPathRange(link: MdLink): vscode.Range {
|
||||
return link.source.fragmentRange
|
||||
? link.source.hrefRange.with(undefined, link.source.fragmentRange.start.translate(0, -1))
|
||||
: link.source.hrefRange;
|
||||
}
|
||||
}
|
||||
|
||||
export async function tryFindMdDocumentForLink(href: InternalHref, workspaceContents: MdWorkspaceContents): Promise<SkinnyTextDocument | undefined> {
|
||||
const targetDoc = await workspaceContents.getMarkdownDocument(href.path);
|
||||
if (targetDoc) {
|
||||
return targetDoc;
|
||||
}
|
||||
|
||||
// We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead
|
||||
if (uri.Utils.extname(href.path) === '') {
|
||||
const dotMdResource = href.path.with({ path: href.path.path + '.md' });
|
||||
return workspaceContents.getMarkdownDocument(dotMdResource);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as URI from 'vscode-uri';
|
||||
import { Slugifier } from '../slugify';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { resolveDocumentLink } from '../util/openDocumentLink';
|
||||
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
||||
import { InternalHref } from './documentLinkProvider';
|
||||
import { MdHeaderReference, MdLinkReference, MdReference, MdReferencesProvider, tryFindMdDocumentForLink } from './references';
|
||||
|
||||
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 MdRenameProvider 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 referencesProvider: MdReferencesProvider,
|
||||
private readonly workspaceContents: MdWorkspaceContents,
|
||||
private readonly slugifier: Slugifier,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async prepareRename(document: SkinnyTextDocument, 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: SkinnyTextDocument, 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: SkinnyTextDocument, 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.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 targetDoc = await tryFindMdDocumentForLink(triggerHref, this.workspaceContents);
|
||||
const targetUri = targetDoc?.uri ?? 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.workspaceContents.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.text.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: SkinnyTextDocument, 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.getAllReferencesAtPosition(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContents, TocEntry } from '../tableOfContents';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
interface MarkdownItTokenWithMap extends Token {
|
||||
map: [number, number];
|
||||
}
|
||||
|
||||
export class MdSmartSelect implements vscode.SelectionRangeProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideSelectionRanges(document: SkinnyTextDocument, positions: vscode.Position[], _token: vscode.CancellationToken): Promise<vscode.SelectionRange[] | undefined> {
|
||||
const promises = await Promise.all(positions.map((position) => {
|
||||
return this.provideSelectionRange(document, position, _token);
|
||||
}));
|
||||
return promises.filter(item => item !== undefined) as vscode.SelectionRange[];
|
||||
}
|
||||
|
||||
private async provideSelectionRange(document: SkinnyTextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.SelectionRange | undefined> {
|
||||
const headerRange = await this.getHeaderSelectionRange(document, position);
|
||||
const blockRange = await this.getBlockSelectionRange(document, position, headerRange);
|
||||
const inlineRange = await this.getInlineSelectionRange(document, position, blockRange);
|
||||
return inlineRange || blockRange || headerRange;
|
||||
}
|
||||
private async getInlineSelectionRange(document: SkinnyTextDocument, position: vscode.Position, blockRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
|
||||
return createInlineRange(document, position, blockRange);
|
||||
}
|
||||
|
||||
private async getBlockSelectionRange(document: SkinnyTextDocument, position: vscode.Position, headerRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
|
||||
|
||||
const tokens = await this.engine.parse(document);
|
||||
|
||||
const blockTokens = getBlockTokensForPosition(tokens, position, headerRange);
|
||||
|
||||
if (blockTokens.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let currentRange: vscode.SelectionRange | undefined = headerRange ? headerRange : createBlockRange(blockTokens.shift()!, document, position.line);
|
||||
|
||||
for (let i = 0; i < blockTokens.length; i++) {
|
||||
currentRange = createBlockRange(blockTokens[i], document, position.line, currentRange);
|
||||
}
|
||||
return currentRange;
|
||||
}
|
||||
|
||||
private async getHeaderSelectionRange(document: SkinnyTextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
|
||||
const toc = await TableOfContents.create(this.engine, document);
|
||||
|
||||
const headerInfo = getHeadersForPosition(toc.entries, position);
|
||||
|
||||
const headers = headerInfo.headers;
|
||||
|
||||
let currentRange: vscode.SelectionRange | undefined;
|
||||
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
currentRange = createHeaderRange(headers[i], i === headers.length - 1, headerInfo.headerOnThisLine, currentRange, getFirstChildHeader(document, headers[i], toc.entries));
|
||||
}
|
||||
return currentRange;
|
||||
}
|
||||
}
|
||||
|
||||
function getHeadersForPosition(toc: readonly TocEntry[], position: vscode.Position): { headers: TocEntry[]; headerOnThisLine: boolean } {
|
||||
const enclosingHeaders = toc.filter(header => header.sectionLocation.range.start.line <= position.line && header.sectionLocation.range.end.line >= position.line);
|
||||
const sortedHeaders = enclosingHeaders.sort((header1, header2) => (header1.line - position.line) - (header2.line - position.line));
|
||||
const onThisLine = toc.find(header => header.line === position.line) !== undefined;
|
||||
return {
|
||||
headers: sortedHeaders,
|
||||
headerOnThisLine: onThisLine
|
||||
};
|
||||
}
|
||||
|
||||
function createHeaderRange(header: TocEntry, isClosestHeaderToPosition: boolean, onHeaderLine: boolean, parent?: vscode.SelectionRange, startOfChildRange?: vscode.Position): vscode.SelectionRange | undefined {
|
||||
const range = header.sectionLocation.range;
|
||||
const contentRange = new vscode.Range(range.start.translate(1), range.end);
|
||||
if (onHeaderLine && isClosestHeaderToPosition && startOfChildRange) {
|
||||
// selection was made on this header line, so select header and its content until the start of its first child
|
||||
// then all of its content
|
||||
return new vscode.SelectionRange(range.with(undefined, startOfChildRange), new vscode.SelectionRange(range, parent));
|
||||
} else if (onHeaderLine && isClosestHeaderToPosition) {
|
||||
// selection was made on this header line and no children so expand to all of its content
|
||||
return new vscode.SelectionRange(range, parent);
|
||||
} else if (isClosestHeaderToPosition && startOfChildRange) {
|
||||
// selection was made within content and has child so select content
|
||||
// of this header then all content then header
|
||||
return new vscode.SelectionRange(contentRange.with(undefined, startOfChildRange), new vscode.SelectionRange(contentRange, (new vscode.SelectionRange(range, parent))));
|
||||
} else {
|
||||
// not on this header line so select content then header
|
||||
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent));
|
||||
}
|
||||
}
|
||||
|
||||
function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, parent?: vscode.SelectionRange): MarkdownItTokenWithMap[] {
|
||||
const enclosingTokens = tokens.filter((token): token is MarkdownItTokenWithMap => !!token.map && (token.map[0] <= position.line && token.map[1] > position.line) && (!parent || (token.map[0] >= parent.range.start.line && token.map[1] <= parent.range.end.line + 1)) && isBlockElement(token));
|
||||
if (enclosingTokens.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const sortedTokens = enclosingTokens.sort((token1, token2) => (token2.map[1] - token2.map[0]) - (token1.map[1] - token1.map[0]));
|
||||
return sortedTokens;
|
||||
}
|
||||
|
||||
function createBlockRange(block: MarkdownItTokenWithMap, document: SkinnyTextDocument, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
if (block.type === 'fence') {
|
||||
return createFencedRange(block, cursorLine, document, parent);
|
||||
} else {
|
||||
let startLine = document.lineAt(block.map[0]).isEmptyOrWhitespace ? block.map[0] + 1 : block.map[0];
|
||||
let endLine = startLine === block.map[1] ? block.map[1] : block.map[1] - 1;
|
||||
if (block.type === 'paragraph_open' && block.map[1] - block.map[0] === 2) {
|
||||
startLine = endLine = cursorLine;
|
||||
} else if (isList(block) && document.lineAt(endLine).isEmptyOrWhitespace) {
|
||||
endLine = endLine - 1;
|
||||
}
|
||||
const range = new vscode.Range(startLine, 0, endLine, document.lineAt(endLine).text?.length ?? 0);
|
||||
if (parent?.range.contains(range) && !parent.range.isEqual(range)) {
|
||||
return new vscode.SelectionRange(range, parent);
|
||||
} else if (parent?.range.isEqual(range)) {
|
||||
return parent;
|
||||
} else {
|
||||
return new vscode.SelectionRange(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createInlineRange(document: SkinnyTextDocument, cursorPosition: vscode.Position, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
const lineText = document.lineAt(cursorPosition.line).text;
|
||||
const boldSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, parent);
|
||||
const italicSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, parent);
|
||||
let comboSelection: vscode.SelectionRange | undefined;
|
||||
if (boldSelection && italicSelection && !boldSelection.range.isEqual(italicSelection.range)) {
|
||||
if (boldSelection.range.contains(italicSelection.range)) {
|
||||
comboSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, boldSelection);
|
||||
} else if (italicSelection.range.contains(boldSelection.range)) {
|
||||
comboSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, italicSelection);
|
||||
}
|
||||
}
|
||||
const linkSelection = createLinkRange(lineText, cursorPosition.character, cursorPosition.line, comboSelection || boldSelection || italicSelection || parent);
|
||||
const inlineCodeBlockSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, false, linkSelection || parent);
|
||||
return inlineCodeBlockSelection || linkSelection || comboSelection || boldSelection || italicSelection;
|
||||
}
|
||||
|
||||
function createFencedRange(token: MarkdownItTokenWithMap, cursorLine: number, document: SkinnyTextDocument, parent?: vscode.SelectionRange): vscode.SelectionRange {
|
||||
const startLine = token.map[0];
|
||||
const endLine = token.map[1] - 1;
|
||||
const onFenceLine = cursorLine === startLine || cursorLine === endLine;
|
||||
const fenceRange = new vscode.Range(startLine, 0, endLine, document.lineAt(endLine).text.length);
|
||||
const contentRange = endLine - startLine > 2 && !onFenceLine ? new vscode.Range(startLine + 1, 0, endLine - 1, document.lineAt(endLine - 1).text.length) : undefined;
|
||||
if (contentRange) {
|
||||
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(fenceRange, parent));
|
||||
} else {
|
||||
if (parent?.range.isEqual(fenceRange)) {
|
||||
return parent;
|
||||
} else {
|
||||
return new vscode.SelectionRange(fenceRange, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createBoldRange(lineText: string, cursorChar: number, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
const regex = /\*\*([^*]+\*?[^*]+\*?[^*]+)\*\*/gim;
|
||||
const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
|
||||
if (matches.length) {
|
||||
// should only be one match, so select first and index 0 contains the entire match
|
||||
const bold = matches[0][0];
|
||||
const startIndex = lineText.indexOf(bold);
|
||||
const cursorOnStars = cursorChar === startIndex || cursorChar === startIndex + 1 || cursorChar === startIndex + bold.length || cursorChar === startIndex + bold.length - 1;
|
||||
const contentAndStars = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex, cursorLine, startIndex + bold.length), parent);
|
||||
const content = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex + 2, cursorLine, startIndex + bold.length - 2), contentAndStars);
|
||||
return cursorOnStars ? contentAndStars : content;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createOtherInlineRange(lineText: string, cursorChar: number, cursorLine: number, isItalic: boolean, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
const italicRegexes = [/(?:[^*]+)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]+)/g, /^(?:[^*]*)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]*)$/g];
|
||||
let matches = [];
|
||||
if (isItalic) {
|
||||
matches = [...lineText.matchAll(italicRegexes[0])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
|
||||
if (!matches.length) {
|
||||
matches = [...lineText.matchAll(italicRegexes[1])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
|
||||
}
|
||||
} else {
|
||||
matches = [...lineText.matchAll(/\`[^\`]*\`/g)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
|
||||
}
|
||||
if (matches.length) {
|
||||
// should only be one match, so select first and select group 1 for italics because that contains just the italic section
|
||||
// doesn't include the leading and trailing characters which are guaranteed to not be * so as not to be confused with bold
|
||||
const match = isItalic ? matches[0][1] : matches[0][0];
|
||||
const startIndex = lineText.indexOf(match);
|
||||
const cursorOnType = cursorChar === startIndex || cursorChar === startIndex + match.length;
|
||||
const contentAndType = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex, cursorLine, startIndex + match.length), parent);
|
||||
const content = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex + 1, cursorLine, startIndex + match.length - 1), contentAndType);
|
||||
return cursorOnType ? contentAndType : content;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createLinkRange(lineText: string, cursorChar: number, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
const regex = /(\[[^\(\)]*\])(\([^\[\]]*\))/g;
|
||||
const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length > cursorChar);
|
||||
|
||||
if (matches.length) {
|
||||
// should only be one match, so select first and index 0 contains the entire match, so match = [text](url)
|
||||
const link = matches[0][0];
|
||||
const linkRange = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(link), cursorLine, lineText.indexOf(link) + link.length), parent);
|
||||
|
||||
const linkText = matches[0][1];
|
||||
const url = matches[0][2];
|
||||
|
||||
// determine if cursor is within [text] or (url) in order to know which should be selected
|
||||
const nearestType = cursorChar >= lineText.indexOf(linkText) && cursorChar < lineText.indexOf(linkText) + linkText.length ? linkText : url;
|
||||
|
||||
const indexOfType = lineText.indexOf(nearestType);
|
||||
// determine if cursor is on a bracket or paren and if so, return the [content] or (content), skipping over the content range
|
||||
const cursorOnType = cursorChar === indexOfType || cursorChar === indexOfType + nearestType.length;
|
||||
|
||||
const contentAndNearestType = new vscode.SelectionRange(new vscode.Range(cursorLine, indexOfType, cursorLine, indexOfType + nearestType.length), linkRange);
|
||||
const content = new vscode.SelectionRange(new vscode.Range(cursorLine, indexOfType + 1, cursorLine, indexOfType + nearestType.length - 1), contentAndNearestType);
|
||||
return cursorOnType ? contentAndNearestType : content;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isList(token: Token): boolean {
|
||||
return token.type ? ['ordered_list_open', 'list_item_open', 'bullet_list_open'].includes(token.type) : false;
|
||||
}
|
||||
|
||||
function isBlockElement(token: Token): boolean {
|
||||
return !['list_item_close', 'paragraph_close', 'bullet_list_close', 'inline', 'heading_close', 'heading_open'].includes(token.type);
|
||||
}
|
||||
|
||||
function getFirstChildHeader(document: SkinnyTextDocument, header?: TocEntry, toc?: readonly TocEntry[]): vscode.Position | undefined {
|
||||
let childRange: vscode.Position | undefined;
|
||||
if (header && toc) {
|
||||
let children = toc.filter(t => header.sectionLocation.range.contains(t.sectionLocation.range) && t.sectionLocation.range.start.line > header.sectionLocation.range.start.line).sort((t1, t2) => t1.line - t2.line);
|
||||
if (children.length > 0) {
|
||||
childRange = children[0].sectionLocation.range.start;
|
||||
const lineText = document.lineAt(childRange.line - 1).text;
|
||||
return childRange ? childRange.translate(-1, lineText.length) : undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { Lazy, lazy } from '../util/lazy';
|
||||
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
/**
|
||||
* Cache of information for markdown files in the workspace.
|
||||
*/
|
||||
export class MdWorkspaceCache<T> extends Disposable {
|
||||
|
||||
private readonly _cache = new Map<string, Lazy<Promise<T>>>();
|
||||
private _hasPopulatedCache = false;
|
||||
|
||||
public constructor(
|
||||
private readonly workspaceContents: MdWorkspaceContents,
|
||||
private readonly getValue: (document: SkinnyTextDocument) => Promise<T>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async getAll(): Promise<T[]> {
|
||||
if (!this._hasPopulatedCache) {
|
||||
await this.populateCache();
|
||||
this._hasPopulatedCache = true;
|
||||
|
||||
this.workspaceContents.onDidChangeMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
|
||||
this.workspaceContents.onDidCreateMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
|
||||
this.workspaceContents.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this, this._disposables);
|
||||
}
|
||||
|
||||
return Promise.all(Array.from(this._cache.values(), x => x.value));
|
||||
}
|
||||
|
||||
private async populateCache(): Promise<void> {
|
||||
const markdownDocumentUris = await this.workspaceContents.getAllMarkdownDocuments();
|
||||
for (const document of markdownDocumentUris) {
|
||||
this.update(document);
|
||||
}
|
||||
}
|
||||
|
||||
private key(resource: vscode.Uri): string {
|
||||
return resource.toString();
|
||||
}
|
||||
|
||||
private update(document: SkinnyTextDocument): void {
|
||||
this._cache.set(this.key(document.uri), lazy(() => this.getValue(document)));
|
||||
}
|
||||
|
||||
private onDidChangeDocument(document: SkinnyTextDocument) {
|
||||
this.update(document);
|
||||
}
|
||||
|
||||
private onDidDeleteDocument(resource: vscode.Uri) {
|
||||
this._cache.delete(this.key(resource));
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { MdWorkspaceContents } from '../workspaceContents';
|
||||
import { MdDocumentSymbolProvider } from './documentSymbolProvider';
|
||||
import { MdWorkspaceCache } from './workspaceCache';
|
||||
|
||||
export class MdWorkspaceSymbolProvider extends Disposable implements vscode.WorkspaceSymbolProvider {
|
||||
|
||||
private readonly _cache: MdWorkspaceCache<vscode.SymbolInformation[]>;
|
||||
|
||||
public constructor(
|
||||
symbolProvider: MdDocumentSymbolProvider,
|
||||
workspaceContents: MdWorkspaceContents,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._cache = this._register(new MdWorkspaceCache(workspaceContents, doc => symbolProvider.provideDocumentSymbolInformation(doc)));
|
||||
}
|
||||
|
||||
public async provideWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
|
||||
const allSymbols = (await this._cache.getAll()).flat();
|
||||
return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Disposable } from './util/dispose';
|
||||
import { lazy } from './util/lazy';
|
||||
|
||||
enum Trace {
|
||||
@@ -25,57 +26,61 @@ namespace Trace {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isString(value: any): value is string {
|
||||
return Object.prototype.toString.call(value) === '[object String]';
|
||||
export interface ILogger {
|
||||
verbose(title: string, message: string, data?: any): void;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
export class VsCodeOutputLogger extends Disposable implements ILogger {
|
||||
private trace?: Trace;
|
||||
|
||||
private readonly outputChannel = lazy(() => vscode.window.createOutputChannel('Markdown'));
|
||||
private readonly outputChannel = lazy(() => this._register(vscode.window.createOutputChannel('Markdown')));
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._register(vscode.workspace.onDidChangeConfiguration(() => {
|
||||
this.updateConfiguration();
|
||||
}));
|
||||
|
||||
this.updateConfiguration();
|
||||
}
|
||||
|
||||
public log(message: string, data?: any): void {
|
||||
public verbose(title: string, message: string, data?: any): void {
|
||||
if (this.trace === Trace.Verbose) {
|
||||
this.appendLine(`[Log - ${this.now()}] ${message}`);
|
||||
this.appendLine(`[Verbose ${this.now()}] ${title}: ${message}`);
|
||||
if (data) {
|
||||
this.appendLine(Logger.data2String(data));
|
||||
this.appendLine(VsCodeOutputLogger.data2String(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private now(): string {
|
||||
const now = new Date();
|
||||
return String(now.getUTCHours()).padStart(2, '0')
|
||||
+ ':' + String(now.getMinutes()).padStart(2, '0')
|
||||
+ ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + now.getMilliseconds();
|
||||
+ ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + String(now.getMilliseconds()).padStart(3, '0');
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
private updateConfiguration(): void {
|
||||
this.trace = this.readTrace();
|
||||
}
|
||||
|
||||
private appendLine(value: string) {
|
||||
return this.outputChannel.value.appendLine(value);
|
||||
private appendLine(value: string): void {
|
||||
this.outputChannel.value.appendLine(value);
|
||||
}
|
||||
|
||||
private readTrace(): Trace {
|
||||
return Trace.fromString(vscode.workspace.getConfiguration().get<string>('markdown.trace', 'off'));
|
||||
return Trace.fromString(vscode.workspace.getConfiguration().get<string>('markdown.trace.extension', 'off'));
|
||||
}
|
||||
|
||||
private static data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
if (isString(data.stack)) {
|
||||
if (typeof data.stack === 'string') {
|
||||
return data.stack;
|
||||
}
|
||||
return (data as Error).message;
|
||||
return data.message;
|
||||
}
|
||||
if (isString(data)) {
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
return JSON.stringify(data, undefined, 2);
|
||||
@@ -3,15 +3,19 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import MarkdownIt = require('markdown-it');
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import type MarkdownIt = require('markdown-it');
|
||||
import type Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { ILogger } from './logging';
|
||||
import { MarkdownContributionProvider } from './markdownExtensions';
|
||||
import { Slugifier } from './slugify';
|
||||
import { ITextDocument } from './types/textDocument';
|
||||
import { Disposable } from './util/dispose';
|
||||
import { stringHash } from './util/hash';
|
||||
import { WebviewResourceProvider } from './util/resources';
|
||||
import { isOfScheme, Schemes } from './util/schemes';
|
||||
import { SkinnyTextDocument } from './workspaceContents';
|
||||
import { MdDocumentInfoCache } from './util/workspaceCache';
|
||||
import { IMdWorkspace } from './workspace';
|
||||
|
||||
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
|
||||
|
||||
@@ -53,7 +57,7 @@ class TokenCache {
|
||||
};
|
||||
private tokens?: Token[];
|
||||
|
||||
public tryGetCached(document: SkinnyTextDocument, config: MarkdownItConfig): Token[] | undefined {
|
||||
public tryGetCached(document: ITextDocument, config: MarkdownItConfig): Token[] | undefined {
|
||||
if (this.cachedDocument
|
||||
&& this.cachedDocument.uri.toString() === document.uri.toString()
|
||||
&& this.cachedDocument.version === document.version
|
||||
@@ -65,7 +69,7 @@ class TokenCache {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public update(document: SkinnyTextDocument, config: MarkdownItConfig, tokens: Token[]) {
|
||||
public update(document: ITextDocument, config: MarkdownItConfig, tokens: Token[]) {
|
||||
this.cachedDocument = {
|
||||
uri: document.uri,
|
||||
version: document.version,
|
||||
@@ -91,17 +95,28 @@ interface RenderEnv {
|
||||
resourceProvider: WebviewResourceProvider | undefined;
|
||||
}
|
||||
|
||||
export class MarkdownEngine {
|
||||
export interface IMdParser {
|
||||
readonly slugifier: Slugifier;
|
||||
|
||||
tokenize(document: ITextDocument): Promise<Token[]>;
|
||||
}
|
||||
|
||||
export class MarkdownItEngine implements IMdParser {
|
||||
|
||||
private md?: Promise<MarkdownIt>;
|
||||
|
||||
private _slugCount = new Map<string, number>();
|
||||
private _tokenCache = new TokenCache();
|
||||
|
||||
public readonly slugifier: Slugifier;
|
||||
|
||||
public constructor(
|
||||
private readonly contributionProvider: MarkdownContributionProvider,
|
||||
private readonly slugifier: Slugifier,
|
||||
slugifier: Slugifier,
|
||||
private readonly logger: ILogger,
|
||||
) {
|
||||
this.slugifier = slugifier;
|
||||
|
||||
contributionProvider.onContributionsChanged(() => {
|
||||
// Markdown plugin contributions may have changed
|
||||
this.md = undefined;
|
||||
@@ -159,7 +174,7 @@ export class MarkdownEngine {
|
||||
}
|
||||
|
||||
private tokenizeDocument(
|
||||
document: SkinnyTextDocument,
|
||||
document: ITextDocument,
|
||||
config: MarkdownItConfig,
|
||||
engine: MarkdownIt
|
||||
): Token[] {
|
||||
@@ -169,6 +184,7 @@ export class MarkdownEngine {
|
||||
return cached;
|
||||
}
|
||||
|
||||
this.logger.verbose('MarkdownItEngine', `tokenizeDocument - ${document.uri}`);
|
||||
const tokens = this.tokenizeString(document.getText(), engine);
|
||||
this._tokenCache.update(document, config, tokens);
|
||||
return tokens;
|
||||
@@ -184,7 +200,7 @@ export class MarkdownEngine {
|
||||
this._slugCount = new Map<string, number>();
|
||||
}
|
||||
|
||||
public async render(input: SkinnyTextDocument | string, resourceProvider?: WebviewResourceProvider): Promise<RenderOutput> {
|
||||
public async render(input: ITextDocument | string, resourceProvider?: WebviewResourceProvider): Promise<RenderOutput> {
|
||||
const config = this.getConfig(typeof input === 'string' ? undefined : input.uri);
|
||||
const engine = await this.getEngine(config);
|
||||
|
||||
@@ -209,7 +225,7 @@ export class MarkdownEngine {
|
||||
};
|
||||
}
|
||||
|
||||
public async parse(document: SkinnyTextDocument): Promise<Token[]> {
|
||||
public async tokenize(document: ITextDocument): Promise<Token[]> {
|
||||
const config = this.getConfig(document.uri);
|
||||
const engine = await this.getEngine(config);
|
||||
return this.tokenizeDocument(document, config, engine);
|
||||
@@ -423,3 +439,27 @@ function normalizeHighlightLang(lang: string | undefined) {
|
||||
return lang;
|
||||
}
|
||||
}
|
||||
|
||||
export class MdParsingProvider extends Disposable implements IMdParser {
|
||||
|
||||
private readonly _cache: MdDocumentInfoCache<Token[]>;
|
||||
|
||||
public readonly slugifier: Slugifier;
|
||||
|
||||
constructor(
|
||||
engine: MarkdownItEngine,
|
||||
workspace: IMdWorkspace,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.slugifier = engine.slugifier;
|
||||
|
||||
this._cache = this._register(new MdDocumentInfoCache<Token[]>(workspace, doc => {
|
||||
return engine.tokenize(doc);
|
||||
}));
|
||||
}
|
||||
|
||||
public tokenize(document: ITextDocument): Promise<Token[]> {
|
||||
return this._cache.getForDocument(document);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as uri from 'vscode-uri';
|
||||
import { Logger } from '../logger';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { ILogger } from '../logging';
|
||||
import { MarkdownItEngine } from '../markdownEngine';
|
||||
import { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { escapeAttribute, getNonce } from '../util/dom';
|
||||
import { WebviewResourceProvider } from '../util/resources';
|
||||
import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security';
|
||||
@@ -35,23 +36,19 @@ const previewStrings = {
|
||||
'Content Disabled Security Warning')
|
||||
};
|
||||
|
||||
function escapeAttribute(value: string | vscode.Uri): string {
|
||||
return value.toString().replace(/"/g, '"');
|
||||
}
|
||||
|
||||
export interface MarkdownContentProviderOutput {
|
||||
html: string;
|
||||
containingImages: { src: string }[];
|
||||
}
|
||||
|
||||
|
||||
export class MarkdownContentProvider {
|
||||
export class MdDocumentRenderer {
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine,
|
||||
private readonly engine: MarkdownItEngine,
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private readonly contributionProvider: MarkdownContributionProvider,
|
||||
private readonly logger: Logger
|
||||
private readonly logger: ILogger
|
||||
) {
|
||||
this.iconPath = {
|
||||
dark: vscode.Uri.joinPath(this.context.extensionUri, 'media', 'preview-dark.svg'),
|
||||
@@ -61,12 +58,13 @@ export class MarkdownContentProvider {
|
||||
|
||||
public readonly iconPath: { light: vscode.Uri; dark: vscode.Uri };
|
||||
|
||||
public async provideTextDocumentContent(
|
||||
public async renderDocument(
|
||||
markdownDocument: vscode.TextDocument,
|
||||
resourceProvider: WebviewResourceProvider,
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
initialLine: number | undefined = undefined,
|
||||
state?: any
|
||||
state: any | undefined,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<MarkdownContentProviderOutput> {
|
||||
const sourceUri = markdownDocument.uri;
|
||||
const config = previewConfigurations.loadAndCacheConfiguration(sourceUri);
|
||||
@@ -82,13 +80,17 @@ export class MarkdownContentProvider {
|
||||
webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(),
|
||||
};
|
||||
|
||||
this.logger.log('provideTextDocumentContent', initialData);
|
||||
this.logger.verbose('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData);
|
||||
|
||||
// Content Security Policy
|
||||
const nonce = getNonce();
|
||||
const csp = this.getCsp(resourceProvider, sourceUri, nonce);
|
||||
|
||||
const body = await this.markdownBody(markdownDocument, resourceProvider);
|
||||
const body = await this.renderBody(markdownDocument, resourceProvider);
|
||||
if (token.isCancellationRequested) {
|
||||
return { html: '', containingImages: [] };
|
||||
}
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}">
|
||||
<head>
|
||||
@@ -113,7 +115,7 @@ export class MarkdownContentProvider {
|
||||
};
|
||||
}
|
||||
|
||||
public async markdownBody(
|
||||
public async renderBody(
|
||||
markdownDocument: vscode.TextDocument,
|
||||
resourceProvider: WebviewResourceProvider,
|
||||
): Promise<MarkdownContentProviderOutput> {
|
||||
@@ -125,9 +127,7 @@ export class MarkdownContentProvider {
|
||||
};
|
||||
}
|
||||
|
||||
public provideFileNotFoundContent(
|
||||
resource: vscode.Uri,
|
||||
): string {
|
||||
public renderFileNotFoundDocument(resource: vscode.Uri): string {
|
||||
const resourcePath = uri.Utils.basename(resource);
|
||||
const body = localize('preview.notFound', '{0} cannot be found', resourcePath);
|
||||
return `<!DOCTYPE html>
|
||||
@@ -246,12 +246,3 @@ export class MarkdownContentProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNonce() {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 64; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -6,16 +6,17 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as uri from 'vscode-uri';
|
||||
import { Logger } from '../logger';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { ILogger } from '../logging';
|
||||
import { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { MdTableOfContentsProvider } from '../tableOfContents';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { openDocumentLink, resolveDocumentLink, resolveUriToMarkdownFile } from '../util/openDocumentLink';
|
||||
import { WebviewResourceProvider } from '../util/resources';
|
||||
import { urlToUri } from '../util/url';
|
||||
import { IMdWorkspace } from '../workspace';
|
||||
import { MdDocumentRenderer } from './documentRenderer';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
import { scrollEditorToLine, StartingScrollFragment, StartingScrollLine, StartingScrollLocation } from './scrolling';
|
||||
import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from './topmostLineMonitor';
|
||||
|
||||
@@ -109,16 +110,19 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
private readonly _onScrollEmitter = this._register(new vscode.EventEmitter<LastScrollLocation>());
|
||||
public readonly onScroll = this._onScrollEmitter.event;
|
||||
|
||||
private readonly _disposeCts = this._register(new vscode.CancellationTokenSource());
|
||||
|
||||
constructor(
|
||||
webview: vscode.WebviewPanel,
|
||||
resource: vscode.Uri,
|
||||
startingScroll: StartingScrollLocation | undefined,
|
||||
private readonly delegate: MarkdownPreviewDelegate,
|
||||
private readonly engine: MarkdownEngine,
|
||||
private readonly _contentProvider: MarkdownContentProvider,
|
||||
private readonly _contentProvider: MdDocumentRenderer,
|
||||
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
private readonly _logger: Logger,
|
||||
private readonly _workspace: IMdWorkspace,
|
||||
private readonly _logger: ILogger,
|
||||
private readonly _contributionProvider: MarkdownContributionProvider,
|
||||
private readonly _tocProvider: MdTableOfContentsProvider,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -202,6 +206,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
this._disposeCts.cancel();
|
||||
|
||||
super.dispose();
|
||||
|
||||
this._disposed = true;
|
||||
@@ -265,7 +271,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
this._logger.log('updateForView', { markdownFile: this._resource });
|
||||
this._logger.verbose('MarkdownPreview', 'updateForView', { markdownFile: this._resource });
|
||||
this.line = topLine;
|
||||
this.postMessage({
|
||||
type: 'updateView',
|
||||
@@ -286,7 +292,9 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
try {
|
||||
document = await vscode.workspace.openTextDocument(this._resource);
|
||||
} catch {
|
||||
await this.showFileNotFoundError();
|
||||
if (!this._disposed) {
|
||||
await this.showFileNotFoundError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,8 +314,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
this.currentVersion = pendingVersion;
|
||||
|
||||
const content = await (shouldReloadPage
|
||||
? this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state)
|
||||
: this._contentProvider.markdownBody(document, this));
|
||||
? this._contentProvider.renderDocument(document, this, this._previewConfigurations, this.line, this.state, this._disposeCts.token)
|
||||
: this._contentProvider.renderBody(document, this));
|
||||
|
||||
// Another call to `doUpdate` may have happened.
|
||||
// Make sure we are still updating for the correct document
|
||||
@@ -364,7 +372,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
}
|
||||
|
||||
private async showFileNotFoundError() {
|
||||
this._webviewPanel.webview.html = this._contentProvider.provideFileNotFoundContent(this._resource);
|
||||
this._webviewPanel.webview.html = this._contentProvider.renderFileNotFoundDocument(this._resource);
|
||||
}
|
||||
|
||||
private updateWebviewContent(html: string, reloadPage: boolean): void {
|
||||
@@ -443,14 +451,14 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
const config = vscode.workspace.getConfiguration('markdown', this.resource);
|
||||
const openLinks = config.get<string>('preview.openMarkdownLinks', 'inPreview');
|
||||
if (openLinks === 'inPreview') {
|
||||
const linkedDoc = await resolveUriToMarkdownFile(targetResource);
|
||||
const linkedDoc = await resolveUriToMarkdownFile(this._workspace, targetResource);
|
||||
if (linkedDoc) {
|
||||
this.delegate.openPreviewLinkToMarkdownFile(linkedDoc.uri, targetResource.fragment);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return openDocumentLink(this.engine, targetResource, this.resource);
|
||||
return openDocumentLink(this._tocProvider, targetResource, this.resource);
|
||||
}
|
||||
|
||||
//#region WebviewResourceProvider
|
||||
@@ -466,7 +474,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export interface ManagedMarkdownPreview {
|
||||
export interface IManagedMarkdownPreview {
|
||||
|
||||
readonly resource: vscode.Uri;
|
||||
readonly resourceColumn: vscode.ViewColumn;
|
||||
@@ -486,22 +494,23 @@ export interface ManagedMarkdownPreview {
|
||||
): boolean;
|
||||
}
|
||||
|
||||
export class StaticMarkdownPreview extends Disposable implements ManagedMarkdownPreview {
|
||||
export class StaticMarkdownPreview extends Disposable implements IManagedMarkdownPreview {
|
||||
|
||||
public static readonly customEditorViewType = 'vscode.markdown.preview.editor';
|
||||
|
||||
public static revive(
|
||||
resource: vscode.Uri,
|
||||
webview: vscode.WebviewPanel,
|
||||
contentProvider: MarkdownContentProvider,
|
||||
contentProvider: MdDocumentRenderer,
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
topmostLineMonitor: TopmostLineMonitor,
|
||||
logger: Logger,
|
||||
workspace: IMdWorkspace,
|
||||
logger: ILogger,
|
||||
contributionProvider: MarkdownContributionProvider,
|
||||
engine: MarkdownEngine,
|
||||
tocProvider: MdTableOfContentsProvider,
|
||||
scrollLine?: number,
|
||||
): StaticMarkdownPreview {
|
||||
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, engine, scrollLine);
|
||||
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, workspace, logger, contributionProvider, tocProvider, scrollLine);
|
||||
}
|
||||
|
||||
private readonly preview: MarkdownPreview;
|
||||
@@ -509,12 +518,13 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
|
||||
private constructor(
|
||||
private readonly _webviewPanel: vscode.WebviewPanel,
|
||||
resource: vscode.Uri,
|
||||
contentProvider: MarkdownContentProvider,
|
||||
contentProvider: MdDocumentRenderer,
|
||||
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
topmostLineMonitor: TopmostLineMonitor,
|
||||
logger: Logger,
|
||||
workspace: IMdWorkspace,
|
||||
logger: ILogger,
|
||||
contributionProvider: MarkdownContributionProvider,
|
||||
engine: MarkdownEngine,
|
||||
tocProvider: MdTableOfContentsProvider,
|
||||
scrollLine?: number,
|
||||
) {
|
||||
super();
|
||||
@@ -526,7 +536,7 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
|
||||
fragment
|
||||
}), StaticMarkdownPreview.customEditorViewType, this._webviewPanel.viewColumn);
|
||||
}
|
||||
}, engine, contentProvider, _previewConfigurations, logger, contributionProvider));
|
||||
}, contentProvider, _previewConfigurations, workspace, logger, contributionProvider, tocProvider));
|
||||
|
||||
this._register(this._webviewPanel.onDidDispose(() => {
|
||||
this.dispose();
|
||||
@@ -592,7 +602,7 @@ interface DynamicPreviewInput {
|
||||
readonly line?: number;
|
||||
}
|
||||
|
||||
export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdownPreview {
|
||||
export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdownPreview {
|
||||
|
||||
public static readonly viewType = 'markdown.preview';
|
||||
|
||||
@@ -605,28 +615,30 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
public static revive(
|
||||
input: DynamicPreviewInput,
|
||||
webview: vscode.WebviewPanel,
|
||||
contentProvider: MarkdownContentProvider,
|
||||
contentProvider: MdDocumentRenderer,
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
logger: Logger,
|
||||
workspace: IMdWorkspace,
|
||||
logger: ILogger,
|
||||
topmostLineMonitor: TopmostLineMonitor,
|
||||
contributionProvider: MarkdownContributionProvider,
|
||||
engine: MarkdownEngine,
|
||||
tocProvider: MdTableOfContentsProvider,
|
||||
): DynamicMarkdownPreview {
|
||||
webview.iconPath = contentProvider.iconPath;
|
||||
|
||||
return new DynamicMarkdownPreview(webview, input,
|
||||
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
|
||||
contentProvider, previewConfigurations, workspace, logger, topmostLineMonitor, contributionProvider, tocProvider);
|
||||
}
|
||||
|
||||
public static create(
|
||||
input: DynamicPreviewInput,
|
||||
previewColumn: vscode.ViewColumn,
|
||||
contentProvider: MarkdownContentProvider,
|
||||
contentProvider: MdDocumentRenderer,
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
logger: Logger,
|
||||
workspace: IMdWorkspace,
|
||||
logger: ILogger,
|
||||
topmostLineMonitor: TopmostLineMonitor,
|
||||
contributionProvider: MarkdownContributionProvider,
|
||||
engine: MarkdownEngine,
|
||||
tocProvider: MdTableOfContentsProvider,
|
||||
): DynamicMarkdownPreview {
|
||||
const webview = vscode.window.createWebviewPanel(
|
||||
DynamicMarkdownPreview.viewType,
|
||||
@@ -636,18 +648,19 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
webview.iconPath = contentProvider.iconPath;
|
||||
|
||||
return new DynamicMarkdownPreview(webview, input,
|
||||
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
|
||||
contentProvider, previewConfigurations, workspace, logger, topmostLineMonitor, contributionProvider, tocProvider);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
webview: vscode.WebviewPanel,
|
||||
input: DynamicPreviewInput,
|
||||
private readonly _contentProvider: MarkdownContentProvider,
|
||||
private readonly _contentProvider: MdDocumentRenderer,
|
||||
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
private readonly _logger: Logger,
|
||||
private readonly _workspace: IMdWorkspace,
|
||||
private readonly _logger: ILogger,
|
||||
private readonly _topmostLineMonitor: TopmostLineMonitor,
|
||||
private readonly _contributionProvider: MarkdownContributionProvider,
|
||||
private readonly _engine: MarkdownEngine,
|
||||
private readonly _tocProvider: MdTableOfContentsProvider,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -799,10 +812,11 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
this.update(link, fragment ? new StartingScrollFragment(fragment) : undefined);
|
||||
}
|
||||
},
|
||||
this._engine,
|
||||
this._contentProvider,
|
||||
this._previewConfigurations,
|
||||
this._workspace,
|
||||
this._logger,
|
||||
this._contributionProvider);
|
||||
this._contributionProvider,
|
||||
this._tocProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Logger } from '../logger';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { ILogger } from '../logging';
|
||||
import { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { MdTableOfContentsProvider } from '../tableOfContents';
|
||||
import { Disposable, disposeAll } from '../util/dispose';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { DynamicMarkdownPreview, ManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
|
||||
import { IMdWorkspace } from '../workspace';
|
||||
import { MdDocumentRenderer } from './documentRenderer';
|
||||
import { DynamicMarkdownPreview, IManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
import { scrollEditorToLine, StartingScrollFragment } from './scrolling';
|
||||
import { TopmostLineMonitor } from './topmostLineMonitor';
|
||||
|
||||
@@ -21,7 +22,7 @@ export interface DynamicPreviewSettings {
|
||||
readonly locked: boolean;
|
||||
}
|
||||
|
||||
class PreviewStore<T extends ManagedMarkdownPreview> extends Disposable {
|
||||
class PreviewStore<T extends IManagedMarkdownPreview> extends Disposable {
|
||||
|
||||
private readonly _previews = new Set<T>();
|
||||
|
||||
@@ -65,13 +66,14 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
private readonly _dynamicPreviews = this._register(new PreviewStore<DynamicMarkdownPreview>());
|
||||
private readonly _staticPreviews = this._register(new PreviewStore<StaticMarkdownPreview>());
|
||||
|
||||
private _activePreview: ManagedMarkdownPreview | undefined = undefined;
|
||||
private _activePreview: IManagedMarkdownPreview | undefined = undefined;
|
||||
|
||||
public constructor(
|
||||
private readonly _contentProvider: MarkdownContentProvider,
|
||||
private readonly _logger: Logger,
|
||||
private readonly _contentProvider: MdDocumentRenderer,
|
||||
private readonly _workspace: IMdWorkspace,
|
||||
private readonly _logger: ILogger,
|
||||
private readonly _contributions: MarkdownContributionProvider,
|
||||
private readonly _engine: MarkdownEngine,
|
||||
private readonly _tocProvider: MdTableOfContentsProvider,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -163,10 +165,11 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
webview,
|
||||
this._contentProvider,
|
||||
this._previewConfigurations,
|
||||
this._workspace,
|
||||
this._logger,
|
||||
this._topmostLineMonitor,
|
||||
this._contributions,
|
||||
this._engine);
|
||||
this._tocProvider);
|
||||
|
||||
this.registerDynamicPreview(preview);
|
||||
}
|
||||
@@ -182,9 +185,10 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
this._contentProvider,
|
||||
this._previewConfigurations,
|
||||
this._topmostLineMonitor,
|
||||
this._workspace,
|
||||
this._logger,
|
||||
this._contributions,
|
||||
this._engine,
|
||||
this._tocProvider,
|
||||
lineNumber
|
||||
);
|
||||
this.registerStaticPreview(preview);
|
||||
@@ -206,10 +210,11 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
previewSettings.previewColumn,
|
||||
this._contentProvider,
|
||||
this._previewConfigurations,
|
||||
this._workspace,
|
||||
this._logger,
|
||||
this._topmostLineMonitor,
|
||||
this._contributions,
|
||||
this._engine);
|
||||
this._tocProvider);
|
||||
|
||||
this.setPreviewActiveContext(true);
|
||||
this._activePreview = preview;
|
||||
@@ -243,7 +248,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
return preview;
|
||||
}
|
||||
|
||||
private trackActive(preview: ManagedMarkdownPreview): void {
|
||||
private trackActive(preview: IManagedMarkdownPreview): void {
|
||||
preview.onDidChangeViewState(({ webviewPanel }) => {
|
||||
this.setPreviewActiveContext(webviewPanel.active);
|
||||
this._activePreview = webviewPanel.active ? preview : undefined;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { ResourceMap } from '../util/resourceMap';
|
||||
|
||||
export interface LastScrollLocation {
|
||||
readonly line: number;
|
||||
@@ -14,10 +15,10 @@ export interface LastScrollLocation {
|
||||
|
||||
export class TopmostLineMonitor extends Disposable {
|
||||
|
||||
private readonly pendingUpdates = new Map<string, number>();
|
||||
private readonly pendingUpdates = new ResourceMap<number>();
|
||||
private readonly throttle = 50;
|
||||
private previousTextEditorInfo = new Map<string, LastScrollLocation>();
|
||||
private previousStaticEditorInfo = new Map<string, LastScrollLocation>();
|
||||
private previousTextEditorInfo = new ResourceMap<LastScrollLocation>();
|
||||
private previousStaticEditorInfo = new ResourceMap<LastScrollLocation>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -42,28 +43,28 @@ export class TopmostLineMonitor extends Disposable {
|
||||
public readonly onDidChanged = this._onChanged.event;
|
||||
|
||||
public setPreviousStaticEditorLine(scrollLocation: LastScrollLocation): void {
|
||||
this.previousStaticEditorInfo.set(scrollLocation.uri.toString(), scrollLocation);
|
||||
this.previousStaticEditorInfo.set(scrollLocation.uri, scrollLocation);
|
||||
}
|
||||
|
||||
public getPreviousStaticEditorLineByUri(resource: vscode.Uri): number | undefined {
|
||||
const scrollLoc = this.previousStaticEditorInfo.get(resource.toString());
|
||||
this.previousStaticEditorInfo.delete(resource.toString());
|
||||
const scrollLoc = this.previousStaticEditorInfo.get(resource);
|
||||
this.previousStaticEditorInfo.delete(resource);
|
||||
return scrollLoc?.line;
|
||||
}
|
||||
|
||||
|
||||
public setPreviousTextEditorLine(scrollLocation: LastScrollLocation): void {
|
||||
this.previousTextEditorInfo.set(scrollLocation.uri.toString(), scrollLocation);
|
||||
this.previousTextEditorInfo.set(scrollLocation.uri, scrollLocation);
|
||||
}
|
||||
|
||||
public getPreviousTextEditorLineByUri(resource: vscode.Uri): number | undefined {
|
||||
const scrollLoc = this.previousTextEditorInfo.get(resource.toString());
|
||||
this.previousTextEditorInfo.delete(resource.toString());
|
||||
const scrollLoc = this.previousTextEditorInfo.get(resource);
|
||||
this.previousTextEditorInfo.delete(resource);
|
||||
return scrollLoc?.line;
|
||||
}
|
||||
|
||||
public getPreviousStaticTextEditorLineByUri(resource: vscode.Uri): number | undefined {
|
||||
const state = this.previousStaticEditorInfo.get(resource.toString());
|
||||
const state = this.previousStaticEditorInfo.get(resource);
|
||||
return state?.line;
|
||||
}
|
||||
|
||||
@@ -71,21 +72,20 @@ export class TopmostLineMonitor extends Disposable {
|
||||
resource: vscode.Uri,
|
||||
line: number
|
||||
) {
|
||||
const key = resource.toString();
|
||||
if (!this.pendingUpdates.has(key)) {
|
||||
if (!this.pendingUpdates.has(resource)) {
|
||||
// schedule update
|
||||
setTimeout(() => {
|
||||
if (this.pendingUpdates.has(key)) {
|
||||
if (this.pendingUpdates.has(resource)) {
|
||||
this._onChanged.fire({
|
||||
resource,
|
||||
line: this.pendingUpdates.get(key) as number
|
||||
line: this.pendingUpdates.get(resource) as number
|
||||
});
|
||||
this.pendingUpdates.delete(key);
|
||||
this.pendingUpdates.delete(resource);
|
||||
}
|
||||
}, this.throttle);
|
||||
}
|
||||
|
||||
this.pendingUpdates.set(key, line);
|
||||
this.pendingUpdates.set(resource, line);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
extensions/markdown-language-features/src/protocol.ts
Normal file
28
extensions/markdown-language-features/src/protocol.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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';
|
||||
import type * as md from 'vscode-markdown-languageservice';
|
||||
|
||||
//#region From server
|
||||
export const parse = new RequestType<{ uri: string }, Token[], any>('markdown/parse');
|
||||
|
||||
export const fs_readFile = new RequestType<{ uri: string }, number[], any>('markdown/fs/readFile');
|
||||
export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory');
|
||||
export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat');
|
||||
|
||||
export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any>('markdown/fs/watcher/create');
|
||||
export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete');
|
||||
|
||||
export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace');
|
||||
//#endregion
|
||||
|
||||
//#region To server
|
||||
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
|
||||
|
||||
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
|
||||
//#endregion
|
||||
@@ -24,7 +24,7 @@ export const githubSlugifier: Slugifier = new class implements Slugifier {
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-') // Replace whitespace with -
|
||||
// allow-any-unicode-next-line
|
||||
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
|
||||
.replace(/[\]\[\!\/\'\"\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
|
||||
.replace(/^\-+/, '') // Remove leading -
|
||||
.replace(/\-+$/, '') // Remove trailing -
|
||||
);
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
import { githubSlugifier, Slug } from './slugify';
|
||||
import { ILogger } from './logging';
|
||||
import { IMdParser } from './markdownEngine';
|
||||
import { githubSlugifier, Slug, Slugifier } from './slugify';
|
||||
import { getLine, ITextDocument } from './types/textDocument';
|
||||
import { Disposable } from './util/dispose';
|
||||
import { isMarkdownFile } from './util/file';
|
||||
import { SkinnyTextDocument } from './workspaceContents';
|
||||
import { Schemes } from './util/schemes';
|
||||
import { MdDocumentInfoCache } from './util/workspaceCache';
|
||||
import { IMdWorkspace } from './workspace';
|
||||
|
||||
export interface TocEntry {
|
||||
readonly slug: Slug;
|
||||
@@ -61,35 +66,39 @@ export interface TocEntry {
|
||||
|
||||
export class TableOfContents {
|
||||
|
||||
public static async create(engine: MarkdownEngine, document: SkinnyTextDocument,): Promise<TableOfContents> {
|
||||
const entries = await this.buildToc(engine, document);
|
||||
return new TableOfContents(entries);
|
||||
public static async create(parser: IMdParser, document: ITextDocument,): Promise<TableOfContents> {
|
||||
const entries = await this.buildToc(parser, document);
|
||||
return new TableOfContents(entries, parser.slugifier);
|
||||
}
|
||||
|
||||
public static async createForDocumentOrNotebook(engine: MarkdownEngine, document: SkinnyTextDocument): Promise<TableOfContents> {
|
||||
if (document.uri.scheme === 'vscode-notebook-cell') {
|
||||
public static async createForDocumentOrNotebook(parser: IMdParser, document: ITextDocument): Promise<TableOfContents> {
|
||||
if (document.uri.scheme === Schemes.notebookCell) {
|
||||
const notebook = vscode.workspace.notebookDocuments
|
||||
.find(notebook => notebook.getCells().some(cell => cell.document === document));
|
||||
|
||||
if (notebook) {
|
||||
const entries: TocEntry[] = [];
|
||||
|
||||
for (const cell of notebook.getCells()) {
|
||||
if (cell.kind === vscode.NotebookCellKind.Markup && isMarkdownFile(cell.document)) {
|
||||
entries.push(...(await this.buildToc(engine, cell.document)));
|
||||
}
|
||||
}
|
||||
|
||||
return new TableOfContents(entries);
|
||||
return TableOfContents.createForNotebook(parser, notebook);
|
||||
}
|
||||
}
|
||||
|
||||
return this.create(engine, document);
|
||||
return this.create(parser, document);
|
||||
}
|
||||
|
||||
private static async buildToc(engine: MarkdownEngine, document: SkinnyTextDocument): Promise<TocEntry[]> {
|
||||
public static async createForNotebook(parser: IMdParser, notebook: vscode.NotebookDocument): Promise<TableOfContents> {
|
||||
const entries: TocEntry[] = [];
|
||||
|
||||
for (const cell of notebook.getCells()) {
|
||||
if (cell.kind === vscode.NotebookCellKind.Markup && isMarkdownFile(cell.document)) {
|
||||
entries.push(...(await this.buildToc(parser, cell.document)));
|
||||
}
|
||||
}
|
||||
|
||||
return new TableOfContents(entries, parser.slugifier);
|
||||
}
|
||||
|
||||
private static async buildToc(parser: IMdParser, document: ITextDocument): Promise<TocEntry[]> {
|
||||
const toc: TocEntry[] = [];
|
||||
const tokens = await engine.parse(document);
|
||||
const tokens = await parser.tokenize(document);
|
||||
|
||||
const existingSlugEntries = new Map<string, { count: number }>();
|
||||
|
||||
@@ -99,26 +108,26 @@ export class TableOfContents {
|
||||
}
|
||||
|
||||
const lineNumber = heading.map[0];
|
||||
const line = document.lineAt(lineNumber);
|
||||
const line = getLine(document, lineNumber);
|
||||
|
||||
let slug = githubSlugifier.fromHeading(line.text);
|
||||
let slug = parser.slugifier.fromHeading(line);
|
||||
const existingSlugEntry = existingSlugEntries.get(slug.value);
|
||||
if (existingSlugEntry) {
|
||||
++existingSlugEntry.count;
|
||||
slug = githubSlugifier.fromHeading(slug.value + '-' + existingSlugEntry.count);
|
||||
slug = parser.slugifier.fromHeading(slug.value + '-' + existingSlugEntry.count);
|
||||
} else {
|
||||
existingSlugEntries.set(slug.value, { count: 0 });
|
||||
}
|
||||
|
||||
const headerLocation = new vscode.Location(document.uri,
|
||||
new vscode.Range(lineNumber, 0, lineNumber, line.text.length));
|
||||
new vscode.Range(lineNumber, 0, lineNumber, line.length));
|
||||
|
||||
const headerTextLocation = new vscode.Location(document.uri,
|
||||
new vscode.Range(lineNumber, line.text.match(/^#+\s*/)?.[0].length ?? 0, lineNumber, line.text.length - (line.text.match(/\s*#*$/)?.[0].length ?? 0)));
|
||||
new vscode.Range(lineNumber, line.match(/^#+\s*/)?.[0].length ?? 0, lineNumber, line.length - (line.match(/\s*#*$/)?.[0].length ?? 0)));
|
||||
|
||||
toc.push({
|
||||
slug,
|
||||
text: TableOfContents.getHeaderText(line.text),
|
||||
text: TableOfContents.getHeaderText(line),
|
||||
level: TableOfContents.getHeaderLevel(heading.markup),
|
||||
line: lineNumber,
|
||||
sectionLocation: headerLocation, // Populated in next steps
|
||||
@@ -142,7 +151,7 @@ export class TableOfContents {
|
||||
sectionLocation: new vscode.Location(document.uri,
|
||||
new vscode.Range(
|
||||
entry.sectionLocation.range.start,
|
||||
new vscode.Position(endLine, document.lineAt(endLine).text.length)))
|
||||
new vscode.Position(endLine, getLine(document, endLine).length)))
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -161,12 +170,44 @@ export class TableOfContents {
|
||||
return header.replace(/^\s*#+\s*(.*?)(\s+#+)?$/, (_, word) => word.trim());
|
||||
}
|
||||
|
||||
public static readonly empty = new TableOfContents([], githubSlugifier);
|
||||
|
||||
private constructor(
|
||||
public readonly entries: readonly TocEntry[],
|
||||
private readonly slugifier: Slugifier,
|
||||
) { }
|
||||
|
||||
public lookup(fragment: string): TocEntry | undefined {
|
||||
const slug = githubSlugifier.fromHeading(fragment);
|
||||
const slug = this.slugifier.fromHeading(fragment);
|
||||
return this.entries.find(entry => entry.slug.equals(slug));
|
||||
}
|
||||
}
|
||||
|
||||
export class MdTableOfContentsProvider extends Disposable {
|
||||
|
||||
private readonly _cache: MdDocumentInfoCache<TableOfContents>;
|
||||
|
||||
constructor(
|
||||
private readonly parser: IMdParser,
|
||||
workspace: IMdWorkspace,
|
||||
private readonly logger: ILogger,
|
||||
) {
|
||||
super();
|
||||
this._cache = this._register(new MdDocumentInfoCache<TableOfContents>(workspace, doc => {
|
||||
this.logger.verbose('TableOfContentsProvider', `create - ${doc.uri}`);
|
||||
return TableOfContents.create(parser, doc);
|
||||
}));
|
||||
}
|
||||
|
||||
public async get(resource: vscode.Uri): Promise<TableOfContents> {
|
||||
return await this._cache.get(resource) ?? TableOfContents.empty;
|
||||
}
|
||||
|
||||
public getForDocument(doc: ITextDocument): Promise<TableOfContents> {
|
||||
return this._cache.getForDocument(doc);
|
||||
}
|
||||
|
||||
public createForNotebook(notebook: vscode.NotebookDocument): Promise<TableOfContents> {
|
||||
return TableOfContents.createForNotebook(this.parser, notebook);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdDefinitionProvider } from '../languageFeatures/definitionProvider';
|
||||
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
|
||||
import { MdReferencesProvider } from '../languageFeatures/references';
|
||||
import { githubSlugifier } from '../slugify';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { MdWorkspaceContents } from '../workspaceContents';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
|
||||
import { joinLines, noopToken, workspacePath } from './util';
|
||||
|
||||
|
||||
function getDefinition(doc: InMemoryDocument, pos: vscode.Position, workspaceContents: MdWorkspaceContents) {
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier);
|
||||
const provider = new MdDefinitionProvider(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', async () => {
|
||||
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
|
||||
`[ref](#abc)`,
|
||||
`[ref]: http://example.com`,
|
||||
));
|
||||
|
||||
const defs = await getDefinition(doc, new vscode.Position(0, 1), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(defs, undefined);
|
||||
});
|
||||
|
||||
test('Should find definition links within file from link', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`[link 1][abc]`, // trigger here
|
||||
``,
|
||||
`[abc]: https://example.com`,
|
||||
));
|
||||
|
||||
const defs = await getDefinition(doc, new vscode.Position(0, 12), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertDefinitionsEqual(defs!,
|
||||
{ uri: docUri, line: 2 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should find definition links using shorthand', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`[ref]`, // trigger 1
|
||||
``,
|
||||
`[yes][ref]`, // trigger 2
|
||||
``,
|
||||
`[ref]: /Hello.md` // trigger 3
|
||||
));
|
||||
|
||||
{
|
||||
const defs = await getDefinition(doc, new vscode.Position(0, 2), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertDefinitionsEqual(defs!,
|
||||
{ uri: docUri, line: 4 },
|
||||
);
|
||||
}
|
||||
{
|
||||
const defs = await getDefinition(doc, new vscode.Position(2, 7), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertDefinitionsEqual(defs!,
|
||||
{ uri: docUri, line: 4 },
|
||||
);
|
||||
}
|
||||
{
|
||||
const defs = await getDefinition(doc, new vscode.Position(4, 2), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertDefinitionsEqual(defs!,
|
||||
{ uri: docUri, line: 4 },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should find definition links within file from definition', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`[link 1][abc]`,
|
||||
``,
|
||||
`[abc]: https://example.com`, // trigger here
|
||||
));
|
||||
|
||||
const defs = await getDefinition(doc, new vscode.Position(2, 3), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertDefinitionsEqual(defs!,
|
||||
{ uri: docUri, line: 2 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should not find definition links across files', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`[link 1][abc]`,
|
||||
``,
|
||||
`[abc]: https://example.com`,
|
||||
));
|
||||
|
||||
const defs = await getDefinition(doc, new vscode.Position(0, 12), new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc,
|
||||
new InMemoryDocument(workspacePath('other.md'), joinLines(
|
||||
`[link 1][abc]`,
|
||||
``,
|
||||
`[abc]: https://example.com?bad`,
|
||||
))
|
||||
]));
|
||||
assertDefinitionsEqual(defs!,
|
||||
{ uri: docUri, line: 2 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,160 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import 'mocha';
|
||||
import { DiagnosticComputer, DiagnosticConfiguration, DiagnosticLevel, DiagnosticManager, DiagnosticOptions } from '../languageFeatures/diagnostics';
|
||||
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { MdWorkspaceContents } from '../workspaceContents';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
|
||||
import { assertRangeEqual, joinLines, noopToken, workspacePath } from './util';
|
||||
|
||||
|
||||
function getComputedDiagnostics(doc: InMemoryDocument, workspaceContents: MdWorkspaceContents) {
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
const computer = new DiagnosticComputer(engine, workspaceContents, linkProvider);
|
||||
return computer.getDiagnostics(doc, {
|
||||
enabled: true,
|
||||
validateFilePaths: DiagnosticLevel.warning,
|
||||
validateOwnHeaders: DiagnosticLevel.warning,
|
||||
validateReferences: DiagnosticLevel.warning,
|
||||
}, noopToken);
|
||||
}
|
||||
|
||||
function createDiagnosticsManager(workspaceContents: MdWorkspaceContents, configuration = new MemoryDiagnosticConfiguration()) {
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
return new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkProvider), configuration);
|
||||
}
|
||||
|
||||
class MemoryDiagnosticConfiguration implements DiagnosticConfiguration {
|
||||
|
||||
private readonly _onDidChange = new vscode.EventEmitter<void>();
|
||||
public readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
constructor(
|
||||
private readonly enabled: boolean = true,
|
||||
) { }
|
||||
|
||||
getOptions(_resource: vscode.Uri): DiagnosticOptions {
|
||||
if (!this.enabled) {
|
||||
return {
|
||||
enabled: false,
|
||||
validateFilePaths: DiagnosticLevel.ignore,
|
||||
validateOwnHeaders: DiagnosticLevel.ignore,
|
||||
validateReferences: DiagnosticLevel.ignore,
|
||||
};
|
||||
}
|
||||
return {
|
||||
enabled: true,
|
||||
validateFilePaths: DiagnosticLevel.warning,
|
||||
validateOwnHeaders: DiagnosticLevel.warning,
|
||||
validateReferences: DiagnosticLevel.warning,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suite('markdown: Diagnostics', () => {
|
||||
test('Should not return any diagnostics for empty document', async () => {
|
||||
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
|
||||
`text`,
|
||||
));
|
||||
|
||||
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(diagnostics, []);
|
||||
});
|
||||
|
||||
test('Should generate diagnostic for link to file that does not exist', async () => {
|
||||
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
|
||||
`[bad](/no/such/file.md)`,
|
||||
`[good](/doc.md)`,
|
||||
`[good-ref]: /doc.md`,
|
||||
`[bad-ref]: /no/such/file.md`,
|
||||
));
|
||||
|
||||
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(diagnostics.length, 2);
|
||||
assertRangeEqual(new vscode.Range(0, 6, 0, 22), diagnostics[0].range);
|
||||
assertRangeEqual(new vscode.Range(3, 11, 3, 27), diagnostics[1].range);
|
||||
});
|
||||
|
||||
test('Should generate diagnostics for links to header that does not exist in current file', async () => {
|
||||
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
|
||||
`[good](#good-header)`,
|
||||
`# Good Header`,
|
||||
`[bad](#no-such-header)`,
|
||||
`[good](#good-header)`,
|
||||
`[good-ref]: #good-header`,
|
||||
`[bad-ref]: #no-such-header`,
|
||||
));
|
||||
|
||||
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(diagnostics.length, 2);
|
||||
assertRangeEqual(new vscode.Range(2, 6, 2, 21), diagnostics[0].range);
|
||||
assertRangeEqual(new vscode.Range(5, 11, 5, 26), diagnostics[1].range);
|
||||
});
|
||||
|
||||
test('Should generate diagnostics for links to non-existent headers in other files', async () => {
|
||||
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
|
||||
`# My header`,
|
||||
`[good](#my-header)`,
|
||||
`[good](/doc1.md#my-header)`,
|
||||
`[good](doc1.md#my-header)`,
|
||||
`[good](/doc2.md#other-header)`,
|
||||
`[bad](/doc2.md#no-such-other-header)`,
|
||||
));
|
||||
|
||||
const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines(
|
||||
`# Other header`,
|
||||
));
|
||||
|
||||
const diagnostics = await getComputedDiagnostics(doc1, new InMemoryWorkspaceMarkdownDocuments([doc1, doc2]));
|
||||
assert.deepStrictEqual(diagnostics.length, 1);
|
||||
assertRangeEqual(new vscode.Range(5, 6, 5, 35), diagnostics[0].range);
|
||||
});
|
||||
|
||||
test('Should support links both with and without .md file extension', async () => {
|
||||
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
|
||||
`# My header`,
|
||||
`[good](#my-header)`,
|
||||
`[good](/doc.md#my-header)`,
|
||||
`[good](doc.md#my-header)`,
|
||||
`[good](/doc#my-header)`,
|
||||
`[good](doc#my-header)`,
|
||||
));
|
||||
|
||||
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(diagnostics.length, 0);
|
||||
});
|
||||
|
||||
test('Should generate diagnostics for non-existent link reference', async () => {
|
||||
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
|
||||
`[good link][good]`,
|
||||
`[bad link][no-such]`,
|
||||
``,
|
||||
`[good]: http://example.com`,
|
||||
));
|
||||
|
||||
const diagnostics = await getComputedDiagnostics(doc, new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(diagnostics.length, 1);
|
||||
assertRangeEqual(new vscode.Range(1, 11, 1, 18), diagnostics[0].range);
|
||||
});
|
||||
|
||||
test('Should not generate diagnostics when validate is disabled', async () => {
|
||||
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
|
||||
`[text](#no-such-header)`,
|
||||
`[text][no-such-ref]`,
|
||||
));
|
||||
|
||||
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(false));
|
||||
const diagnostics = await manager.getDiagnostics(doc1, noopToken);
|
||||
assert.deepStrictEqual(diagnostics.length, 0);
|
||||
});
|
||||
});
|
||||
@@ -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('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,270 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { assertRangeEqual, joinLines, noopToken } from './util';
|
||||
|
||||
|
||||
const testFile = vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, 'x.md');
|
||||
|
||||
function getLinksForFile(fileContents: string) {
|
||||
const doc = new InMemoryDocument(testFile, fileContents);
|
||||
const provider = new MdLinkProvider(createNewMarkdownEngine());
|
||||
return provider.provideDocumentLinks(doc, noopToken);
|
||||
}
|
||||
|
||||
suite('markdown.DocumentLinkProvider', () => {
|
||||
test('Should not return anything for empty document', async () => {
|
||||
const links = await getLinksForFile('');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not return anything for simple document without links', async () => {
|
||||
const links = await getLinksForFile('# a\nfdasfdfsafsa');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should detect basic http links', async () => {
|
||||
const links = await getLinksForFile('a [b](https://example.com) c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 25));
|
||||
});
|
||||
|
||||
test('Should detect basic workspace links', async () => {
|
||||
{
|
||||
const links = await getLinksForFile('a [b](./file) c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 12));
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('a [b](file.png) c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, 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');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 25));
|
||||
});
|
||||
|
||||
// #35245
|
||||
test('Should handle links with escaped characters in name', async () => {
|
||||
const links = await getLinksForFile('a [b\\]](./file)');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, 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');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 30));
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('a [b](https://example.com/a(b)c) c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 31));
|
||||
|
||||
}
|
||||
{
|
||||
// #49011
|
||||
const links = await getLinksForFile('[A link](http://ThisUrlhasParens/A_link(in_parens))');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 9, 0, 50));
|
||||
}
|
||||
});
|
||||
|
||||
test('Should handle two links without space', async () => {
|
||||
const links = await getLinksForFile('a ([test](test)[test2](test2)) c');
|
||||
assert.strictEqual(links.length, 2);
|
||||
const [link1, link2] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 10, 0, 14));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 23, 0, 28));
|
||||
});
|
||||
|
||||
// #49238
|
||||
test('should handle hyperlinked images', async () => {
|
||||
{
|
||||
const links = await getLinksForFile('[](https://example.com)');
|
||||
assert.strictEqual(links.length, 2);
|
||||
const [link1, link2] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 13, 0, 22));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 25, 0, 44));
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('[]( https://whitespace.com )');
|
||||
assert.strictEqual(links.length, 2);
|
||||
const [link1, link2] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 7, 0, 21));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 26, 0, 48));
|
||||
}
|
||||
{
|
||||
const links = await getLinksForFile('[](file1.txt) text [](file2.txt)');
|
||||
assert.strictEqual(links.length, 4);
|
||||
const [link1, link2, link3, link4] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 6, 0, 14));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 17, 0, 26));
|
||||
assertRangeEqual(link3.range, new vscode.Range(0, 39, 0, 47));
|
||||
assertRangeEqual(link4.range, new vscode.Range(0, 50, 0, 59));
|
||||
}
|
||||
});
|
||||
|
||||
test('Should not consider link references starting with ^ character valid (#107471)', async () => {
|
||||
const links = await getLinksForFile('[^reference]: https://example.com');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should find definitions links with spaces in angle brackets (#136073)', async () => {
|
||||
const links = await getLinksForFile([
|
||||
'[a]: <b c>',
|
||||
'[b]: <cd>',
|
||||
].join('\n'));
|
||||
assert.strictEqual(links.length, 2);
|
||||
|
||||
const [link1, link2] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 6, 0, 9));
|
||||
assertRangeEqual(link2.range, new vscode.Range(1, 6, 1, 8));
|
||||
});
|
||||
|
||||
test('Should only find one link for reference sources [a]: source (#141285)', async () => {
|
||||
const links = await getLinksForFile([
|
||||
'[Works]: https://microsoft.com',
|
||||
].join('\n'));
|
||||
|
||||
assert.strictEqual(links.length, 1);
|
||||
});
|
||||
|
||||
test('Should find links for referees with only one [] (#141285)', async () => {
|
||||
let links = await getLinksForFile([
|
||||
'[ref]',
|
||||
'[ref]: https://microsoft.com',
|
||||
].join('\n'));
|
||||
assert.strictEqual(links.length, 2);
|
||||
|
||||
links = await getLinksForFile([
|
||||
'[Does Not Work]',
|
||||
'[def]: https://microsoft.com',
|
||||
].join('\n'));
|
||||
assert.strictEqual(links.length, 1);
|
||||
});
|
||||
|
||||
test('Should not find link for reference using one [] when source does not exist (#141285)', async () => {
|
||||
const links = await getLinksForFile('[Works]');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not consider links in code fenced with backticks', async () => {
|
||||
const text = joinLines(
|
||||
'```',
|
||||
'[b](https://example.com)',
|
||||
'```');
|
||||
const links = await getLinksForFile(text);
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not consider links in code fenced with tilda', async () => {
|
||||
const text = joinLines(
|
||||
'~~~',
|
||||
'[b](https://example.com)',
|
||||
'~~~');
|
||||
const links = await getLinksForFile(text);
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not consider links in indented code', async () => {
|
||||
const links = await getLinksForFile(' [b](https://example.com)');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not consider links in inline code span', async () => {
|
||||
const links = await getLinksForFile('`[b](https://example.com)`');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not consider links with code span inside', async () => {
|
||||
const links = await getLinksForFile('[li`nk](https://example.com`)');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not consider links in multiline inline code span', async () => {
|
||||
const text = joinLines(
|
||||
'`` ',
|
||||
'[b](https://example.com)',
|
||||
'``');
|
||||
const links = await getLinksForFile(text);
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not consider link references in code fenced with backticks (#146714)', async () => {
|
||||
const text = joinLines(
|
||||
'```',
|
||||
'[a] [bb]',
|
||||
'```');
|
||||
const links = await getLinksForFile(text);
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not consider reference sources in code fenced with backticks (#146714)', async () => {
|
||||
const text = joinLines(
|
||||
'```',
|
||||
'[a]: http://example.com;',
|
||||
'[b]: <http://example.com>;',
|
||||
'[c]: (http://example.com);',
|
||||
'```');
|
||||
const links = await getLinksForFile(text);
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not consider links in multiline inline code span between between text', async () => {
|
||||
const text = joinLines(
|
||||
'[b](https://1.com) `[b](https://2.com)',
|
||||
'` [b](https://3.com)');
|
||||
const links = await getLinksForFile(text);
|
||||
assert.deepStrictEqual(links.map(l => l.target?.authority), ['1.com', '3.com']);
|
||||
});
|
||||
|
||||
test('Should not consider links in multiline inline code span with new line after the first backtick', async () => {
|
||||
const text = joinLines(
|
||||
'`',
|
||||
'[b](https://example.com)`');
|
||||
const links = await getLinksForFile(text);
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not miss links in invalid multiline inline code span', async () => {
|
||||
const text = joinLines(
|
||||
'`` ',
|
||||
'',
|
||||
'[b](https://example.com)',
|
||||
'',
|
||||
'``');
|
||||
const links = await getLinksForFile(text);
|
||||
assert.strictEqual(links.length, 1);
|
||||
});
|
||||
|
||||
test('Should find autolinks', async () => {
|
||||
const links = await getLinksForFile('pre <http://example.com> post');
|
||||
assert.strictEqual(links.length, 1);
|
||||
|
||||
const link = links[0];
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 5, 0, 23));
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdDocumentSymbolProvider } from '../languageFeatures/documentSymbolProvider';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
|
||||
function getSymbolsForFile(fileContents: string) {
|
||||
const doc = new InMemoryDocument(testFileName, fileContents);
|
||||
const provider = new MdDocumentSymbolProvider(createNewMarkdownEngine());
|
||||
return provider.provideDocumentSymbols(doc);
|
||||
}
|
||||
|
||||
|
||||
suite('markdown.DocumentSymbolProvider', () => {
|
||||
test('Should not return anything for empty document', async () => {
|
||||
const symbols = await getSymbolsForFile('');
|
||||
assert.strictEqual(symbols.length, 0);
|
||||
});
|
||||
|
||||
test('Should not return anything for document with no headers', async () => {
|
||||
const symbols = await getSymbolsForFile('a\na');
|
||||
assert.strictEqual(symbols.length, 0);
|
||||
});
|
||||
|
||||
test('Should not return anything for document with # but no real headers', async () => {
|
||||
const symbols = await getSymbolsForFile('a#a\na#');
|
||||
assert.strictEqual(symbols.length, 0);
|
||||
});
|
||||
|
||||
test('Should return single symbol for single header', async () => {
|
||||
const symbols = await getSymbolsForFile('# h');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '# h');
|
||||
});
|
||||
|
||||
test('Should not care about symbol level for single header', async () => {
|
||||
const symbols = await getSymbolsForFile('### h');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '### h');
|
||||
});
|
||||
|
||||
test('Should put symbols of same level in flat list', async () => {
|
||||
const symbols = await getSymbolsForFile('## h\n## h2');
|
||||
assert.strictEqual(symbols.length, 2);
|
||||
assert.strictEqual(symbols[0].name, '## h');
|
||||
assert.strictEqual(symbols[1].name, '## h2');
|
||||
});
|
||||
|
||||
test('Should nest symbol of level - 1 under parent', async () => {
|
||||
|
||||
const symbols = await getSymbolsForFile('# h\n## h2\n## h3');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '# h');
|
||||
assert.strictEqual(symbols[0].children.length, 2);
|
||||
assert.strictEqual(symbols[0].children[0].name, '## h2');
|
||||
assert.strictEqual(symbols[0].children[1].name, '## h3');
|
||||
});
|
||||
|
||||
test('Should nest symbol of level - n under parent', async () => {
|
||||
const symbols = await getSymbolsForFile('# h\n#### h2');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '# h');
|
||||
assert.strictEqual(symbols[0].children.length, 1);
|
||||
assert.strictEqual(symbols[0].children[0].name, '#### h2');
|
||||
});
|
||||
|
||||
test('Should flatten children where lower level occurs first', async () => {
|
||||
const symbols = await getSymbolsForFile('# h\n### h2\n## h3');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '# h');
|
||||
assert.strictEqual(symbols[0].children.length, 2);
|
||||
assert.strictEqual(symbols[0].children[0].name, '### h2');
|
||||
assert.strictEqual(symbols[0].children[1].name, '## h3');
|
||||
});
|
||||
|
||||
test('Should handle line separator in file. Issue #63749', async () => {
|
||||
const symbols = await getSymbolsForFile(`# A
|
||||
- foo
|
||||
|
||||
# B
|
||||
- bar`);
|
||||
assert.strictEqual(symbols.length, 2);
|
||||
assert.strictEqual(symbols[0].name, '# A');
|
||||
assert.strictEqual(symbols[1].name, '# B');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MarkdownItEngine } from '../markdownEngine';
|
||||
import { MarkdownContributionProvider, MarkdownContributions } from '../markdownExtensions';
|
||||
import { githubSlugifier } from '../slugify';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { nulLogger } from './nulLogging';
|
||||
|
||||
const emptyContributions = new class extends Disposable implements MarkdownContributionProvider {
|
||||
readonly extensionUri = vscode.Uri.file('/');
|
||||
@@ -15,6 +16,6 @@ const emptyContributions = new class extends Disposable implements MarkdownContr
|
||||
readonly onContributionsChanged = this._register(new vscode.EventEmitter<this>()).event;
|
||||
};
|
||||
|
||||
export function createNewMarkdownEngine(): MarkdownEngine {
|
||||
return new MarkdownEngine(emptyContributions, githubSlugifier);
|
||||
export function createNewMarkdownEngine(): MarkdownItEngine {
|
||||
return new MarkdownItEngine(emptyContributions, githubSlugifier, nulLogger);
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
|
||||
import { MdReference, MdReferencesProvider } from '../languageFeatures/references';
|
||||
import { githubSlugifier } from '../slugify';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { MdWorkspaceContents } from '../workspaceContents';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
|
||||
import { joinLines, noopToken, workspacePath } from './util';
|
||||
|
||||
|
||||
function getFileReferences(resource: vscode.Uri, workspaceContents: MdWorkspaceContents) {
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
const provider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier);
|
||||
return provider.getAllReferencesToFile(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', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const otherUri = workspacePath('other.md');
|
||||
|
||||
const refs = await getFileReferences(otherUri, new InMemoryWorkspaceMarkdownDocuments([
|
||||
new InMemoryDocument(docUri, joinLines(
|
||||
`# header`,
|
||||
`[link 1](./other.md)`,
|
||||
`[link 2](./other.md)`,
|
||||
)),
|
||||
new InMemoryDocument(otherUri, joinLines(
|
||||
`# header`,
|
||||
`pre`,
|
||||
`[link 3](./other.md)`,
|
||||
`post`,
|
||||
)),
|
||||
]));
|
||||
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: docUri, line: 1 },
|
||||
{ uri: docUri, line: 2 },
|
||||
{ uri: otherUri, line: 2 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should find references with and without file extensions', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const otherUri = workspacePath('other.md');
|
||||
|
||||
const refs = await getFileReferences(otherUri, new InMemoryWorkspaceMarkdownDocuments([
|
||||
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`,
|
||||
)),
|
||||
]));
|
||||
|
||||
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', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const otherUri = workspacePath('other.md');
|
||||
|
||||
const refs = await getFileReferences(otherUri, new InMemoryWorkspaceMarkdownDocuments([
|
||||
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`,
|
||||
)),
|
||||
]));
|
||||
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: docUri, line: 1 },
|
||||
{ uri: docUri, line: 2 },
|
||||
{ uri: otherUri, line: 2 },
|
||||
{ uri: otherUri, line: 3 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,223 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdFoldingProvider } from '../languageFeatures/foldingProvider';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { joinLines } from './util';
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
suite('markdown.FoldingProvider', () => {
|
||||
test('Should not return anything for empty document', async () => {
|
||||
const folds = await getFoldsForDocument(``);
|
||||
assert.strictEqual(folds.length, 0);
|
||||
});
|
||||
|
||||
test('Should not return anything for document without headers', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`a`,
|
||||
`**b** afas`,
|
||||
`a#b`,
|
||||
`a`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 0);
|
||||
});
|
||||
|
||||
test('Should fold from header to end of document', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`a`,
|
||||
`# b`,
|
||||
`c`,
|
||||
`d`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('Should leave single newline before next header', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
``,
|
||||
`# a`,
|
||||
`x`,
|
||||
``,
|
||||
`# b`,
|
||||
`y`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 2);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 2);
|
||||
});
|
||||
|
||||
test('Should collapse multiple newlines to single newline before next header', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
``,
|
||||
`# a`,
|
||||
`x`,
|
||||
``,
|
||||
``,
|
||||
``,
|
||||
`# b`,
|
||||
`y`
|
||||
));
|
||||
assert.strictEqual(folds.length, 2);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 4);
|
||||
});
|
||||
|
||||
test('Should not collapse if there is no newline before next header', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
``,
|
||||
`# a`,
|
||||
`x`,
|
||||
`# b`,
|
||||
`y`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 2);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 2);
|
||||
});
|
||||
|
||||
test('Should fold nested <!-- #region --> markers', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`a`,
|
||||
`<!-- #region -->`,
|
||||
`b`,
|
||||
`<!-- #region hello!-->`,
|
||||
`b.a`,
|
||||
`<!-- #endregion -->`,
|
||||
`b`,
|
||||
`<!-- #region: foo! -->`,
|
||||
`b.b`,
|
||||
`<!-- #endregion: foo -->`,
|
||||
`b`,
|
||||
`<!-- #endregion -->`,
|
||||
`a`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 3);
|
||||
const [outer, first, second] = folds.sort((a, b) => a.start - b.start);
|
||||
|
||||
assert.strictEqual(outer.start, 1);
|
||||
assert.strictEqual(outer.end, 11);
|
||||
assert.strictEqual(first.start, 3);
|
||||
assert.strictEqual(first.end, 5);
|
||||
assert.strictEqual(second.start, 7);
|
||||
assert.strictEqual(second.end, 9);
|
||||
});
|
||||
|
||||
test('Should fold from list to end of document', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`a`,
|
||||
`- b`,
|
||||
`c`,
|
||||
`d`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('lists folds should span multiple lines of content', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`a`,
|
||||
`- This list item\n spans multiple\n lines.`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('List should leave single blankline before new element', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`- a`,
|
||||
`a`,
|
||||
``,
|
||||
``,
|
||||
`b`
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 0);
|
||||
assert.strictEqual(firstFold.end, 2);
|
||||
});
|
||||
|
||||
test('Should fold fenced code blocks', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`~~~ts`,
|
||||
`a`,
|
||||
`~~~`,
|
||||
`b`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 0);
|
||||
assert.strictEqual(firstFold.end, 2);
|
||||
});
|
||||
|
||||
test('Should fold fenced code blocks with yaml front matter', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`---`,
|
||||
`title: bla`,
|
||||
`---`,
|
||||
``,
|
||||
`~~~ts`,
|
||||
`a`,
|
||||
`~~~`,
|
||||
``,
|
||||
`a`,
|
||||
`a`,
|
||||
`b`,
|
||||
`a`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 4);
|
||||
assert.strictEqual(firstFold.end, 6);
|
||||
});
|
||||
|
||||
test('Should fold html blocks', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`x`,
|
||||
`<div>`,
|
||||
` fa`,
|
||||
`</div>`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('Should fold html block comments', async () => {
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`x`,
|
||||
`<!--`,
|
||||
`fa`,
|
||||
`-->`
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
assert.strictEqual(firstFold.kind, vscode.FoldingRangeKind.Comment);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function getFoldsForDocument(contents: string) {
|
||||
const doc = new InMemoryDocument(testFileName, contents);
|
||||
const provider = new MdFoldingProvider(createNewMarkdownEngine());
|
||||
return await provider.provideFoldingRanges(doc, {}, new vscode.CancellationTokenSource().token);
|
||||
}
|
||||
@@ -4,58 +4,80 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
||||
import { ITextDocument } from '../types/textDocument';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { ResourceMap } from '../util/resourceMap';
|
||||
import { IMdWorkspace } from '../workspace';
|
||||
|
||||
|
||||
export class InMemoryWorkspaceMarkdownDocuments implements MdWorkspaceContents {
|
||||
private readonly _documents = new Map<string, SkinnyTextDocument>();
|
||||
export class InMemoryMdWorkspace extends Disposable implements IMdWorkspace {
|
||||
private readonly _documents = new ResourceMap<ITextDocument>(uri => uri.fsPath);
|
||||
|
||||
constructor(documents: SkinnyTextDocument[]) {
|
||||
constructor(documents: ITextDocument[]) {
|
||||
super();
|
||||
for (const doc of documents) {
|
||||
this._documents.set(this.getKey(doc.uri), doc);
|
||||
this._documents.set(doc.uri, doc);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllMarkdownDocuments() {
|
||||
public values() {
|
||||
return Array.from(this._documents.values());
|
||||
}
|
||||
|
||||
public async getMarkdownDocument(resource: vscode.Uri): Promise<SkinnyTextDocument | undefined> {
|
||||
return this._documents.get(this.getKey(resource));
|
||||
public async getAllMarkdownDocuments() {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
|
||||
return this._documents.get(resource);
|
||||
}
|
||||
|
||||
public hasMarkdownDocument(resolvedHrefPath: vscode.Uri): boolean {
|
||||
return this._documents.has(resolvedHrefPath);
|
||||
}
|
||||
|
||||
public async pathExists(resource: vscode.Uri): Promise<boolean> {
|
||||
return this._documents.has(this.getKey(resource));
|
||||
return this._documents.has(resource);
|
||||
}
|
||||
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = new vscode.EventEmitter<SkinnyTextDocument>();
|
||||
public async readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]> {
|
||||
const files = new Map<string, vscode.FileType>();
|
||||
const pathPrefix = resource.fsPath + (resource.fsPath.endsWith('/') || resource.fsPath.endsWith('\\') ? '' : path.sep);
|
||||
for (const doc of this._documents.values()) {
|
||||
const path = doc.uri.fsPath;
|
||||
if (path.startsWith(pathPrefix)) {
|
||||
const parts = path.slice(pathPrefix.length).split(/\/|\\/g);
|
||||
files.set(parts[0], parts.length > 1 ? vscode.FileType.Directory : vscode.FileType.File);
|
||||
}
|
||||
}
|
||||
return Array.from(files.entries());
|
||||
}
|
||||
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
|
||||
public onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocumentEmitter.event;
|
||||
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = new vscode.EventEmitter<SkinnyTextDocument>();
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
|
||||
public onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocumentEmitter.event;
|
||||
|
||||
private readonly _onDidDeleteMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.Uri>();
|
||||
private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<vscode.Uri>());
|
||||
public onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocumentEmitter.event;
|
||||
|
||||
public updateDocument(document: SkinnyTextDocument) {
|
||||
this._documents.set(this.getKey(document.uri), document);
|
||||
public updateDocument(document: ITextDocument) {
|
||||
this._documents.set(document.uri, document);
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
|
||||
public createDocument(document: SkinnyTextDocument) {
|
||||
assert.ok(!this._documents.has(this.getKey(document.uri)));
|
||||
public createDocument(document: ITextDocument) {
|
||||
assert.ok(!this._documents.has(document.uri));
|
||||
|
||||
this._documents.set(this.getKey(document.uri), document);
|
||||
this._documents.set(document.uri, document);
|
||||
this._onDidCreateMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
|
||||
public deleteDocument(resource: vscode.Uri) {
|
||||
this._documents.delete(this.getKey(resource));
|
||||
this._documents.delete(resource);
|
||||
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
|
||||
}
|
||||
|
||||
private getKey(resource: vscode.Uri): string {
|
||||
return resource.fsPath;
|
||||
}
|
||||
}
|
||||
|
||||
12
extensions/markdown-language-features/src/test/nulLogging.ts
Normal file
12
extensions/markdown-language-features/src/test/nulLogging.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ILogger } from '../logging';
|
||||
|
||||
export const nulLogger = new class implements ILogger {
|
||||
verbose(): void {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
@@ -1,169 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
|
||||
import { MdPathCompletionProvider } from '../languageFeatures/pathCompletions';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { CURSOR, getCursorPositions, joinLines, noopToken, workspacePath } from './util';
|
||||
|
||||
|
||||
function getCompletionsAtCursor(resource: vscode.Uri, fileContents: string) {
|
||||
const doc = new InMemoryDocument(resource, fileContents);
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
const provider = new MdPathCompletionProvider(engine, linkProvider);
|
||||
const cursorPositions = getCursorPositions(fileContents, doc);
|
||||
return provider.provideCompletionItems(doc, cursorPositions[0], noopToken, {
|
||||
triggerCharacter: undefined,
|
||||
triggerKind: vscode.CompletionTriggerKind.Invoke,
|
||||
});
|
||||
}
|
||||
|
||||
suite('Markdown path completion provider', () => {
|
||||
|
||||
setup(async () => {
|
||||
// These tests assume that the markdown completion provider is already registered
|
||||
await vscode.extensions.getExtension('vscode.markdown-language-features')!.activate();
|
||||
});
|
||||
|
||||
test('Should not return anything when triggered in empty doc', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), `${CURSOR}`);
|
||||
assert.strictEqual(completions.length, 0);
|
||||
});
|
||||
|
||||
test('Should return anchor completions', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](#${CURSOR}`,
|
||||
``,
|
||||
`# A b C`,
|
||||
`# x y Z`,
|
||||
));
|
||||
|
||||
assert.strictEqual(completions.length, 2);
|
||||
assert.ok(completions.some(x => x.label === '#a-b-c'), 'Has a-b-c anchor completion');
|
||||
assert.ok(completions.some(x => x.label === '#x-y-z'), 'Has x-y-z anchor completion');
|
||||
});
|
||||
|
||||
test('Should not return suggestions for http links', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](http:${CURSOR}`,
|
||||
``,
|
||||
`# http`,
|
||||
`# http:`,
|
||||
`# https:`,
|
||||
));
|
||||
|
||||
assert.strictEqual(completions.length, 0);
|
||||
});
|
||||
|
||||
test('Should return relative path suggestions', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](${CURSOR}`,
|
||||
``,
|
||||
`# A b C`,
|
||||
));
|
||||
|
||||
assert.ok(completions.some(x => x.label === 'a.md'), 'Has a.md file completion');
|
||||
assert.ok(completions.some(x => x.label === 'b.md'), 'Has b.md file completion');
|
||||
assert.ok(completions.some(x => x.label === 'sub/'), 'Has sub folder completion');
|
||||
});
|
||||
|
||||
test('Should return relative path suggestions using ./', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](./${CURSOR}`,
|
||||
``,
|
||||
`# A b C`,
|
||||
));
|
||||
|
||||
assert.ok(completions.some(x => x.label === 'a.md'), 'Has a.md file completion');
|
||||
assert.ok(completions.some(x => x.label === 'b.md'), 'Has b.md file completion');
|
||||
assert.ok(completions.some(x => x.label === 'sub/'), 'Has sub folder completion');
|
||||
});
|
||||
|
||||
test('Should return absolute path suggestions using /', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
|
||||
`[](/${CURSOR}`,
|
||||
``,
|
||||
`# A b C`,
|
||||
));
|
||||
|
||||
assert.ok(completions.some(x => x.label === 'a.md'), 'Has a.md file completion');
|
||||
assert.ok(completions.some(x => x.label === 'b.md'), 'Has b.md file completion');
|
||||
assert.ok(completions.some(x => x.label === 'sub/'), 'Has sub folder completion');
|
||||
assert.ok(!completions.some(x => x.label === 'c.md'), 'Should not have c.md from sub folder');
|
||||
});
|
||||
|
||||
test('Should return anchor suggestions in other file', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
|
||||
`[](/b.md#${CURSOR}`,
|
||||
));
|
||||
|
||||
assert.ok(completions.some(x => x.label === '#b'), 'Has #b header completion');
|
||||
assert.ok(completions.some(x => x.label === '#header1'), 'Has #header1 header completion');
|
||||
});
|
||||
|
||||
test('Should reference links for current file', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('sub', 'new.md'), joinLines(
|
||||
`[][${CURSOR}`,
|
||||
``,
|
||||
`[ref-1]: bla`,
|
||||
`[ref-2]: bla`,
|
||||
));
|
||||
|
||||
assert.strictEqual(completions.length, 2);
|
||||
assert.ok(completions.some(x => x.label === 'ref-1'), 'Has ref-1 reference completion');
|
||||
assert.ok(completions.some(x => x.label === 'ref-2'), 'Has ref-2 reference completion');
|
||||
});
|
||||
|
||||
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}`,
|
||||
));
|
||||
|
||||
assert.ok(completions.some(x => x.label === '#a-b-c'), 'Has #a-b-c header completion');
|
||||
assert.ok(completions.some(x => x.label === '#x-y-z'), 'Has #x-y-z header completion');
|
||||
});
|
||||
|
||||
test('Should complete relative paths in link definitions', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`# a B c`,
|
||||
`[ref-1]: ${CURSOR}`,
|
||||
));
|
||||
|
||||
assert.ok(completions.some(x => x.label === 'a.md'), 'Has a.md file completion');
|
||||
assert.ok(completions.some(x => x.label === 'b.md'), 'Has b.md file completion');
|
||||
assert.ok(completions.some(x => x.label === 'sub/'), 'Has sub folder completion');
|
||||
});
|
||||
|
||||
test('Should escape spaces in path names', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](./sub/${CURSOR})`
|
||||
));
|
||||
|
||||
assert.ok(completions.some(x => x.insertText === 'file%20with%20space.md'), 'Has encoded path completion');
|
||||
});
|
||||
|
||||
test('Should complete paths for path with encoded spaces', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[](./sub%20with%20space/${CURSOR})`
|
||||
));
|
||||
|
||||
assert.ok(completions.some(x => x.insertText === 'file.md'), 'Has file from space');
|
||||
});
|
||||
|
||||
test('Should complete definition path for path with encoded spaces', async () => {
|
||||
const completions = await getCompletionsAtCursor(workspacePath('new.md'), joinLines(
|
||||
`[def]: ./sub%20with%20space/${CURSOR}`
|
||||
));
|
||||
|
||||
assert.ok(completions.some(x => x.insertText === 'file.md'), 'Has file from space');
|
||||
});
|
||||
});
|
||||
@@ -1,580 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
|
||||
import { MdReferencesProvider } from '../languageFeatures/references';
|
||||
import { githubSlugifier } from '../slugify';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { MdWorkspaceContents } from '../workspaceContents';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
|
||||
import { joinLines, noopToken, workspacePath } from './util';
|
||||
|
||||
|
||||
function getReferences(doc: InMemoryDocument, pos: vscode.Position, workspaceContents: MdWorkspaceContents) {
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
const provider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier);
|
||||
return provider.provideReferences(doc, pos, { includeDeclaration: true }, noopToken);
|
||||
}
|
||||
|
||||
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', async () => {
|
||||
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
|
||||
`# abc`,
|
||||
``,
|
||||
`[link 1](#abc)`,
|
||||
`text`,
|
||||
));
|
||||
|
||||
{
|
||||
const refs = await getReferences(doc, new vscode.Position(1, 0), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(refs, []);
|
||||
}
|
||||
{
|
||||
const refs = await getReferences(doc, new vscode.Position(3, 2), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(refs, []);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should find references from header within same file', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`# abc`,
|
||||
``,
|
||||
`[link 1](#abc)`,
|
||||
`[not link](#noabc)`,
|
||||
`[link 2](#abc)`,
|
||||
));
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 3), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri, line: 0 },
|
||||
{ uri, line: 2 },
|
||||
{ uri, line: 4 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should not return references when on link text', async () => {
|
||||
const doc = new InMemoryDocument(workspacePath('doc.md'), joinLines(
|
||||
`[ref](#abc)`,
|
||||
`[ref]: http://example.com`,
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 1), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(refs, []);
|
||||
});
|
||||
|
||||
test('Should find references using normalized slug', async () => {
|
||||
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)`,
|
||||
));
|
||||
|
||||
{
|
||||
// Trigger header
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 0), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(refs!.length, 4);
|
||||
}
|
||||
{
|
||||
// Trigger on line 1
|
||||
const refs = await getReferences(doc, new vscode.Position(1, 12), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(refs!.length, 4);
|
||||
}
|
||||
{
|
||||
// Trigger on line 2
|
||||
const refs = await getReferences(doc, new vscode.Position(2, 24), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(refs!.length, 4);
|
||||
}
|
||||
{
|
||||
// Trigger on line 3
|
||||
const refs = await getReferences(doc, new vscode.Position(3, 20), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.deepStrictEqual(refs!.length, 4);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should find references from header across files', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const other1Uri = workspacePath('sub', 'other.md');
|
||||
const other2Uri = workspacePath('other2.md');
|
||||
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`# abc`,
|
||||
``,
|
||||
`[link 1](#abc)`,
|
||||
));
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 3), new InMemoryWorkspaceMarkdownDocuments([
|
||||
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)`,
|
||||
))
|
||||
]));
|
||||
|
||||
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 ', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`# abc`,
|
||||
``,
|
||||
`[bla]: #abc`
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 3), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri, line: 0 }, // Header definition
|
||||
{ uri, line: 2 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should find header references from link definition', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`# A b C`,
|
||||
`[text][bla]`,
|
||||
`[bla]: #a-b-c`, // trigger here
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(2, 9), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri, line: 0 }, // Header definition
|
||||
{ uri, line: 2 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should find references from link within same file', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`# abc`,
|
||||
``,
|
||||
`[link 1](#abc)`,
|
||||
`[not link](#noabc)`,
|
||||
`[link 2](#abc)`,
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(2, 10), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri, line: 0 }, // Header definition
|
||||
{ uri, line: 2 },
|
||||
{ uri, line: 4 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should find references from link across files', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const other1Uri = workspacePath('sub', 'other.md');
|
||||
const other2Uri = workspacePath('other2.md');
|
||||
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`# abc`,
|
||||
``,
|
||||
`[link 1](#abc)`,
|
||||
));
|
||||
const refs = await getReferences(doc, new vscode.Position(2, 10), new InMemoryWorkspaceMarkdownDocuments([
|
||||
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)`,
|
||||
))
|
||||
]));
|
||||
|
||||
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', async () => {
|
||||
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 refs = await getReferences(doc, new vscode.Position(2, 10), new InMemoryWorkspaceMarkdownDocuments([
|
||||
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)`,
|
||||
)),
|
||||
]));
|
||||
|
||||
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', async () => {
|
||||
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 refs = await getReferences(doc, new vscode.Position(0, 23), new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc,
|
||||
new InMemoryDocument(other1Uri, joinLines(
|
||||
`pre`,
|
||||
`# header`,
|
||||
`post`,
|
||||
)),
|
||||
]));
|
||||
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: other1Uri, line: 1 }, // Header definition
|
||||
{ uri: docUri, line: 0 },
|
||||
{ uri: docUri, line: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should include header references when triggered on file link', async () => {
|
||||
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 refs = await getReferences(doc, new vscode.Position(0, 15), new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc,
|
||||
new InMemoryDocument(otherUri, joinLines(
|
||||
`pre`,
|
||||
`# header`, // Definition should not be included since we triggered on a file link
|
||||
`post`,
|
||||
)),
|
||||
]));
|
||||
|
||||
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', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const otherUri = workspacePath('sub', 'other.md');
|
||||
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`[other](./sub/other)`, // trigger here
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 15), new InMemoryWorkspaceMarkdownDocuments([
|
||||
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
|
||||
)),
|
||||
]));
|
||||
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: docUri, line: 0 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should find explicit references to own file ', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[bare](doc.md)`, // trigger here
|
||||
`[rel](./doc.md)`,
|
||||
`[abs](/doc.md)`,
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 12), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri, line: 0 },
|
||||
{ uri, line: 1 },
|
||||
{ uri, line: 2 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should support finding references to http uri', async () => {
|
||||
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 refs = await getReferences(doc, new vscode.Position(0, 13), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri, line: 0 },
|
||||
{ uri, line: 2 },
|
||||
{ uri, line: 3 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should consider authority, scheme and paths when finding references to http uri', async () => {
|
||||
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 refs = await getReferences(doc, new vscode.Position(0, 13), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri, line: 0 },
|
||||
{ uri, line: 4 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should support finding references to http uri across files', async () => {
|
||||
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 refs = await getReferences(doc, new vscode.Position(0, 13), new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc,
|
||||
new InMemoryDocument(uri2, joinLines(
|
||||
`[other](http://example.com)`,
|
||||
))
|
||||
]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: uri1, line: 0 },
|
||||
{ uri: uri1, line: 1 },
|
||||
{ uri: uri2, line: 0 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should support finding references to autolinked http links', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[1](http://example.com)`,
|
||||
`<http://example.com>`,
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 13), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri, line: 0 },
|
||||
{ uri, line: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should distinguish between references to file and to header within file', async () => {
|
||||
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 workspaceContents = new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc,
|
||||
otherDoc,
|
||||
]);
|
||||
{
|
||||
// Check refs to header fragment
|
||||
const headerRefs = await getReferences(otherDoc, new vscode.Position(0, 16), workspaceContents);
|
||||
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(otherDoc, new vscode.Position(0, 9), workspaceContents);
|
||||
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(otherDoc, new vscode.Position(1, 17), workspaceContents);
|
||||
assertReferencesEqual(fileRefs!,
|
||||
{ uri: other1Uri, line: 0 },
|
||||
{ uri: other1Uri, line: 1 },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should support finding references to unknown file', async () => {
|
||||
const uri1 = workspacePath('doc1.md');
|
||||
const doc1 = new InMemoryDocument(uri1, joinLines(
|
||||
``,
|
||||
``,
|
||||
`[ref]: /images/more/image.png`,
|
||||
));
|
||||
|
||||
const uri2 = workspacePath('sub', 'doc2.md');
|
||||
const doc2 = new InMemoryDocument(uri2, joinLines(
|
||||
``,
|
||||
));
|
||||
|
||||
|
||||
const refs = await getReferences(doc1, new vscode.Position(0, 10), new InMemoryWorkspaceMarkdownDocuments([doc1, doc2]));
|
||||
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', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`[link 1][abc]`, // trigger here
|
||||
``,
|
||||
`[abc]: https://example.com`,
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 12), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: docUri, line: 0 },
|
||||
{ uri: docUri, line: 2 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should find reference links using shorthand', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`[ref]`, // trigger 1
|
||||
``,
|
||||
`[yes][ref]`, // trigger 2
|
||||
``,
|
||||
`[ref]: /Hello.md` // trigger 3
|
||||
));
|
||||
|
||||
{
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 2), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: docUri, line: 0 },
|
||||
{ uri: docUri, line: 2 },
|
||||
{ uri: docUri, line: 4 },
|
||||
);
|
||||
}
|
||||
{
|
||||
const refs = await getReferences(doc, new vscode.Position(2, 7), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: docUri, line: 0 },
|
||||
{ uri: docUri, line: 2 },
|
||||
{ uri: docUri, line: 4 },
|
||||
);
|
||||
}
|
||||
{
|
||||
const refs = await getReferences(doc, new vscode.Position(4, 2), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: docUri, line: 0 },
|
||||
{ uri: docUri, line: 2 },
|
||||
{ uri: docUri, line: 4 },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should find reference links within file from definition', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`[link 1][abc]`,
|
||||
``,
|
||||
`[abc]: https://example.com`, // trigger here
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(2, 3), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: docUri, line: 0 },
|
||||
{ uri: docUri, line: 2 },
|
||||
);
|
||||
});
|
||||
|
||||
test('Should not find reference links across files', async () => {
|
||||
const docUri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(docUri, joinLines(
|
||||
`[link 1][abc]`,
|
||||
``,
|
||||
`[abc]: https://example.com`,
|
||||
));
|
||||
|
||||
const refs = await getReferences(doc, new vscode.Position(0, 12), new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc,
|
||||
new InMemoryDocument(workspacePath('other.md'), joinLines(
|
||||
`[link 1][abc]`,
|
||||
``,
|
||||
`[abc]: https://example.com?bad`,
|
||||
))
|
||||
]));
|
||||
assertReferencesEqual(refs!,
|
||||
{ uri: docUri, line: 0 },
|
||||
{ uri: docUri, line: 2 },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,616 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdLinkProvider } from '../languageFeatures/documentLinkProvider';
|
||||
import { MdReferencesProvider } from '../languageFeatures/references';
|
||||
import { MdRenameProvider, MdWorkspaceEdit } from '../languageFeatures/rename';
|
||||
import { githubSlugifier } from '../slugify';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { MdWorkspaceContents } from '../workspaceContents';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
|
||||
import { assertRangeEqual, joinLines, noopToken, workspacePath } from './util';
|
||||
|
||||
|
||||
/**
|
||||
* Get prepare rename info.
|
||||
*/
|
||||
function prepareRename(doc: InMemoryDocument, pos: vscode.Position, workspaceContents: MdWorkspaceContents): Promise<undefined | { readonly range: vscode.Range; readonly placeholder: string }> {
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier);
|
||||
const renameProvider = new MdRenameProvider(referencesProvider, workspaceContents, githubSlugifier);
|
||||
return renameProvider.prepareRename(doc, pos, noopToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the edits for the rename.
|
||||
*/
|
||||
function getRenameEdits(doc: InMemoryDocument, pos: vscode.Position, newName: string, workspaceContents: MdWorkspaceContents): Promise<MdWorkspaceEdit | undefined> {
|
||||
const engine = createNewMarkdownEngine();
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier);
|
||||
const renameProvider = new MdRenameProvider(referencesProvider, workspaceContents, 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 #', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`# abc`
|
||||
));
|
||||
|
||||
const info = await prepareRename(doc, new vscode.Position(0, 0), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertRangeEqual(info!.range, new vscode.Range(0, 2, 0, 5));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(0, 0), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertEditsEqual(edit!, {
|
||||
uri, edits: [
|
||||
new vscode.TextEdit(new vscode.Range(0, 2, 0, 5), 'New Header')
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('Rename on header should include leading or trailing #s', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`### abc ###`
|
||||
));
|
||||
|
||||
const info = await prepareRename(doc, new vscode.Position(0, 0), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertRangeEqual(info!.range, new vscode.Range(0, 4, 0, 7));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(0, 0), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertEditsEqual(edit!, {
|
||||
uri, edits: [
|
||||
new vscode.TextEdit(new vscode.Range(0, 4, 0, 7), 'New Header')
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('Rename on header should pick up links in doc', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`### A b C`, // rename here
|
||||
`[text](#a-b-c)`,
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(0, 0), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertEditsEqual(edit!, {
|
||||
uri, edits: [
|
||||
new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'),
|
||||
new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'),
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('Rename on link should use slug for link', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`### A b C`,
|
||||
`[text](#a-b-c)`, // rename here
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(1, 10), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertEditsEqual(edit!, {
|
||||
uri, edits: [
|
||||
new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'),
|
||||
new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'),
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('Rename on link definition should work', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`### A b C`,
|
||||
`[text](#a-b-c)`,
|
||||
`[ref]: #a-b-c`// rename here
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(2, 10), "New Header", new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assertEditsEqual(edit!, {
|
||||
uri, edits: [
|
||||
new vscode.TextEdit(new vscode.Range(0, 4, 0, 9), 'New Header'),
|
||||
new vscode.TextEdit(new vscode.Range(1, 8, 1, 13), 'new-header'),
|
||||
new vscode.TextEdit(new vscode.Range(2, 8, 2, 13), 'new-header'),
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('Rename on header should pick up links across files', async () => {
|
||||
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(doc, new vscode.Position(0, 0), "New Header", new InMemoryWorkspaceMarkdownDocuments([
|
||||
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', async () => {
|
||||
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(doc, new vscode.Position(1, 10), "New Header", new InMemoryWorkspaceMarkdownDocuments([
|
||||
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', async () => {
|
||||
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(otherDoc, new vscode.Position(1, 17), "New Header", new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc,
|
||||
otherDoc
|
||||
]));
|
||||
assertEditsEqual(edit!, ...expectedEdits);
|
||||
}
|
||||
{
|
||||
// Rename on header without extension
|
||||
const edit = await getRenameEdits(otherDoc, new vscode.Position(2, 15), "New Header", new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc,
|
||||
otherDoc
|
||||
]));
|
||||
assertEditsEqual(edit!, ...expectedEdits);
|
||||
}
|
||||
});
|
||||
|
||||
test('Rename on reference should rename references and definition', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[text][ref]`, // rename here
|
||||
`[other][ref]`,
|
||||
``,
|
||||
`[ref]: https://example.com`,
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(0, 8), "new ref", new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
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', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[text][ref]`,
|
||||
`[other][ref]`,
|
||||
``,
|
||||
`[ref]: https://example.com`, // rename here
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(3, 3), "new ref", new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
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', async () => {
|
||||
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 preparedInfo = await prepareRename(doc, new vscode.Position(3, 10), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.strictEqual(preparedInfo!.placeholder, 'a B c');
|
||||
assertRangeEqual(preparedInfo!.range, new vscode.Range(3, 8, 3, 13));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(3, 10), "x Y z", new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
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', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`# Header`,
|
||||
`[text](#header)`,
|
||||
));
|
||||
|
||||
await assert.rejects(prepareRename(doc, new vscode.Position(1, 2), new InMemoryWorkspaceMarkdownDocuments([doc])));
|
||||
});
|
||||
|
||||
test('Path rename should use file path as range', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[text](./doc.md)`,
|
||||
`[ref]: ./doc.md`,
|
||||
));
|
||||
|
||||
const info = await prepareRename(doc, new vscode.Position(0, 10), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.strictEqual(info!.placeholder, './doc.md');
|
||||
assertRangeEqual(info!.range, new vscode.Range(0, 7, 0, 15));
|
||||
});
|
||||
|
||||
test('Path rename\'s range should excludes fragment', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[text](./doc.md#some-header)`,
|
||||
`[ref]: ./doc.md#some-header`,
|
||||
));
|
||||
|
||||
const info = await prepareRename(doc, new vscode.Position(0, 10), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
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', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[text](./doc.md)`,
|
||||
`[ref]: ./doc.md`,
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(0, 10), './sub/newDoc.md', new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
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', async () => {
|
||||
const uri = workspacePath('sub', 'doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[text](/sub/doc.md)`,
|
||||
`[ref]: /sub/doc.md`,
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(0, 10), '/newSub/newDoc.md', new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
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', async () => {
|
||||
const uri = workspacePath('sub', 'doc with spaces.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[text](/sub/doc%20with%20spaces.md)`,
|
||||
));
|
||||
|
||||
const info = await prepareRename(doc, new vscode.Position(0, 10), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.strictEqual(info!.placeholder, '/sub/doc with spaces.md');
|
||||
});
|
||||
|
||||
test('Path rename should encode paths', async () => {
|
||||
const uri = workspacePath('sub', 'doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[text](/sub/doc.md)`,
|
||||
`[ref]: /sub/doc.md`,
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(0, 10), '/NEW sub/new DOC.md', new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
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', async () => {
|
||||
const uri1 = workspacePath('doc1.md');
|
||||
const doc1 = new InMemoryDocument(uri1, joinLines(
|
||||
``,
|
||||
``,
|
||||
`[ref]: /images/more/image.png`,
|
||||
));
|
||||
|
||||
const uri2 = workspacePath('sub', 'doc2.md');
|
||||
const doc2 = new InMemoryDocument(uri2, joinLines(
|
||||
``,
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc1, new vscode.Position(0, 10), '/img/test/new.png', new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc1,
|
||||
doc2
|
||||
]));
|
||||
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', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`[text](/doc#header)`,
|
||||
`[ref]: /doc#other`,
|
||||
));
|
||||
|
||||
const edit = await getRenameEdits(doc, new vscode.Position(0, 10), '/new File', new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
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', async () => {
|
||||
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 edit = await getRenameEdits(doc1, new vscode.Position(0, 10), './new/new-doc.md', new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc1, doc2, doc3, doc4,
|
||||
]));
|
||||
assertEditsEqual(edit!, {
|
||||
originalUri: uri1,
|
||||
newUri: workspacePath('sub', 'new', 'new-doc.md'),
|
||||
}, {
|
||||
uri: uri1, edits: [
|
||||
new vscode.TextEdit(new vscode.Range(0, 7, 0, 15), './new/new-doc.md'),
|
||||
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', async () => {
|
||||
const uri1 = workspacePath('sub', 'doc.md');
|
||||
const doc1 = new InMemoryDocument(uri1, joinLines(
|
||||
``,
|
||||
));
|
||||
|
||||
const uri2 = workspacePath('doc2.md');
|
||||
const doc2 = new InMemoryDocument(uri2, joinLines(
|
||||
``,
|
||||
));
|
||||
|
||||
const uri3 = workspacePath('sub', 'sub2', 'doc3.md');
|
||||
const doc3 = new InMemoryDocument(uri3, joinLines());
|
||||
|
||||
const edit = await getRenameEdits(doc1, new vscode.Position(0, 10), 'sub2/cat.md', new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc1, doc2, doc3
|
||||
]));
|
||||
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', async () => {
|
||||
const uri = workspacePath('doc.md');
|
||||
const doc = new InMemoryDocument(uri, joinLines(
|
||||
`### a B c ###`,
|
||||
`[text](#a-b-c)`,
|
||||
));
|
||||
|
||||
const info = await prepareRename(doc, new vscode.Position(1, 10), new InMemoryWorkspaceMarkdownDocuments([doc]));
|
||||
assert.strictEqual(info!.placeholder, 'a B c');
|
||||
assertRangeEqual(info!.range, new vscode.Range(1, 8, 1, 13));
|
||||
});
|
||||
|
||||
test('Rename on http uri should work', async () => {
|
||||
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 edit = await getRenameEdits(doc, new vscode.Position(1, 10), "https://example.com/sub", new InMemoryWorkspaceMarkdownDocuments([
|
||||
doc,
|
||||
new InMemoryDocument(uri2, joinLines(
|
||||
`[4](http://example.com)`,
|
||||
))
|
||||
]));
|
||||
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'),
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,726 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdSmartSelect } from '../languageFeatures/smartSelect';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { CURSOR, getCursorPositions, joinLines } from './util';
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
suite('markdown.SmartSelect', () => {
|
||||
test('Smart select single word', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(`Hel${CURSOR}lo`);
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 0]);
|
||||
});
|
||||
|
||||
test('Smart select multi-line paragraph', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`Many of the core components and extensions to ${CURSOR}VS Code live in their own repositories on GitHub. `,
|
||||
`For example, the[node debug adapter](https://github.com/microsoft/vscode-node-debug) and the [mono debug adapter]`,
|
||||
`(https://github.com/microsoft/vscode-mono-debug) have their own repositories. For a complete list, please visit the [Related Projects](https://github.com/microsoft/vscode/wiki/Related-Projects) page on our [wiki](https://github.com/microsoft/vscode/wiki).`
|
||||
));
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
|
||||
test('Smart select paragraph', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(`Many of the core components and extensions to ${CURSOR}VS Code live in their own repositories on GitHub. For example, the [node debug adapter](https://github.com/microsoft/vscode-node-debug) and the [mono debug adapter](https://github.com/microsoft/vscode-mono-debug) have their own repositories. For a complete list, please visit the [Related Projects](https://github.com/microsoft/vscode/wiki/Related-Projects) page on our [wiki](https://github.com/microsoft/vscode/wiki).`);
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 0]);
|
||||
});
|
||||
|
||||
test('Smart select html block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`<p align="center">`,
|
||||
`${CURSOR}<img alt="VS Code in action" src="https://user-images.githubusercontent.com/1487073/58344409-70473b80-7e0a-11e9-8570-b2efc6f8fa44.png">`,
|
||||
`</p>`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
|
||||
test('Smart select header on header line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# Header${CURSOR}`,
|
||||
`Hello`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 1]);
|
||||
|
||||
});
|
||||
|
||||
test('Smart select single word w grandparent header on text line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`## ParentHeader`,
|
||||
`# Header`,
|
||||
`${CURSOR}Hello`
|
||||
));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [1, 2]);
|
||||
});
|
||||
|
||||
test('Smart select html block w parent header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# Header`,
|
||||
`${CURSOR}<p align="center">`,
|
||||
`<img alt="VS Code in action" src="https://user-images.githubusercontent.com/1487073/58344409-70473b80-7e0a-11e9-8570-b2efc6f8fa44.png">`,
|
||||
`</p>`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select fenced code block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`~~~`,
|
||||
`a${CURSOR}`,
|
||||
`~~~`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
|
||||
test('Smart select list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`- item 1`,
|
||||
`- ${CURSOR}item 2`,
|
||||
`- item 3`,
|
||||
`- item 4`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select list with fenced code block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`- item 1`,
|
||||
`- ~~~`,
|
||||
` ${CURSOR}a`,
|
||||
` ~~~`,
|
||||
`- item 3`,
|
||||
`- item 4`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 3], [0, 5]);
|
||||
});
|
||||
|
||||
test('Smart select multi cursor', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`- ${CURSOR}item 1`,
|
||||
`- ~~~`,
|
||||
` a`,
|
||||
` ~~~`,
|
||||
`- ${CURSOR}item 3`,
|
||||
`- item 4`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 0], [0, 5]);
|
||||
assertNestedLineNumbersEqual(ranges![1], [4, 4], [0, 5]);
|
||||
});
|
||||
|
||||
test('Smart select nested block quotes', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`> item 1`,
|
||||
`> item 2`,
|
||||
`>> ${CURSOR}item 3`,
|
||||
`>> item 4`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select multi nested block quotes', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`> item 1`,
|
||||
`>> item 2`,
|
||||
`>>> ${CURSOR}item 3`,
|
||||
`>>>> item 4`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select subheader content', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
`content 1`,
|
||||
`## sub header 1`,
|
||||
`${CURSOR}content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [2, 3], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select subheader line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
`content 1`,
|
||||
`## sub header 1${CURSOR}`,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 3], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select blank line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
`content 1`,
|
||||
`${CURSOR} `,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select line between paragraphs', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`paragraph 1`,
|
||||
`${CURSOR}`,
|
||||
`paragraph 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
|
||||
test('Smart select empty document', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(``, [new vscode.Position(0, 0)]);
|
||||
assert.strictEqual(ranges!.length, 0);
|
||||
});
|
||||
|
||||
test('Smart select fenced code block then list then subheader content then subheader then header content then header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
/* 00 */ `# main header 1`,
|
||||
/* 01 */ `content 1`,
|
||||
/* 02 */ `## sub header 1`,
|
||||
/* 03 */ `- item 1`,
|
||||
/* 04 */ `- ~~~`,
|
||||
/* 05 */ ` ${CURSOR}a`,
|
||||
/* 06 */ ` ~~~`,
|
||||
/* 07 */ `- item 3`,
|
||||
/* 08 */ `- item 4`,
|
||||
/* 09 */ ``,
|
||||
/* 10 */ `more content`,
|
||||
/* 11 */ `# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [4, 6], [3, 8], [3, 10], [2, 10], [1, 10], [0, 10]);
|
||||
});
|
||||
|
||||
test('Smart select list with one element without selecting child subheader', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
/* 00 */ `# main header 1`,
|
||||
/* 01 */ ``,
|
||||
/* 02 */ `- list ${CURSOR}`,
|
||||
/* 03 */ ``,
|
||||
/* 04 */ `## sub header`,
|
||||
/* 05 */ ``,
|
||||
/* 06 */ `content 2`,
|
||||
/* 07 */ `# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [1, 3], [1, 6], [0, 6]);
|
||||
});
|
||||
|
||||
test('Smart select content under header then subheaders and their content', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main ${CURSOR}header 1`,
|
||||
``,
|
||||
`- list`,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 3], [0, 6]);
|
||||
});
|
||||
|
||||
test('Smart select last blockquote element under header then subheaders and their content', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> ${CURSOR}block`,
|
||||
``,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [5, 5], [4, 5], [2, 5], [1, 7], [1, 10], [0, 10]);
|
||||
});
|
||||
|
||||
test('Smart select content of subheader then subheader then content of main header then main header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
``,
|
||||
`${CURSOR}`,
|
||||
``,
|
||||
`### main header 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`content 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [11, 11], [9, 12], [9, 17], [8, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
|
||||
test('Smart select last line content of subheader then subheader then content of main header then main header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
``,
|
||||
``,
|
||||
``,
|
||||
`### main header 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- ${CURSOR}content 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [17, 17], [14, 17], [13, 17], [9, 17], [8, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
|
||||
test('Smart select last line content after content of subheader then subheader then content of main header then main header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
``,
|
||||
``,
|
||||
``,
|
||||
`### main header 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2${CURSOR}`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [17, 17], [14, 17], [13, 17], [9, 17], [8, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
|
||||
test('Smart select fenced code block then list then rest of content', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`- paragraph`,
|
||||
`- ~~~`,
|
||||
` my`,
|
||||
` ${CURSOR}code`,
|
||||
` goes here`,
|
||||
` ~~~`,
|
||||
`- content`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [9, 11], [8, 12], [8, 12], [7, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
|
||||
test('Smart select fenced code block then list then rest of content on fenced line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`- paragraph`,
|
||||
`- ~~~${CURSOR}`,
|
||||
` my`,
|
||||
` code`,
|
||||
` goes here`,
|
||||
` ~~~`,
|
||||
`- content`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [8, 12], [7, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
|
||||
test('Smart select without multiple ranges', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
``,
|
||||
`- ${CURSOR}paragraph`,
|
||||
`- content`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [3, 4], [1, 4], [0, 4]);
|
||||
});
|
||||
|
||||
test('Smart select on second level of a list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 0`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
` * level 2`,
|
||||
` * level 1`,
|
||||
` * level ${CURSOR}1`,
|
||||
`* level 0`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [5, 5], [1, 5], [0, 5], [0, 6]);
|
||||
});
|
||||
|
||||
test('Smart select on third level of a list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 0`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
` * level ${CURSOR}2`,
|
||||
` * level 2`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
`* level 0`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [3, 4], [2, 4], [1, 6], [0, 6], [0, 7]);
|
||||
});
|
||||
|
||||
test('Smart select level 2 then level 1', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 1`,
|
||||
` * level ${CURSOR}2`,
|
||||
` * level 2`,
|
||||
`* level 1`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [1, 2], [0, 2], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select last list item', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`- level 1`,
|
||||
`- level 2`,
|
||||
`- level 2`,
|
||||
`- level ${CURSOR}1`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select without multiple ranges', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
``,
|
||||
`- ${CURSOR}paragraph`,
|
||||
`- content`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [3, 4], [1, 4], [0, 4]);
|
||||
});
|
||||
|
||||
test('Smart select on second level of a list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 0`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
` * level 2`,
|
||||
` * level 1`,
|
||||
` * level ${CURSOR}1`,
|
||||
`* level 0`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [5, 5], [1, 5], [0, 5], [0, 6]);
|
||||
});
|
||||
|
||||
test('Smart select on third level of a list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 0`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
` * level ${CURSOR}2`,
|
||||
` * level 2`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
`* level 0`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [3, 4], [2, 4], [1, 6], [0, 6], [0, 7]);
|
||||
});
|
||||
|
||||
test('Smart select level 2 then level 1', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 1`,
|
||||
` * level ${CURSOR}2`,
|
||||
` * level 2`,
|
||||
`* level 1`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [1, 2], [0, 2], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select bold', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`stuff here **new${CURSOR}item** and here`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 13, 0, 30], [0, 11, 0, 32], [0, 0, 0, 41]);
|
||||
});
|
||||
|
||||
test('Smart select link', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`stuff here [text](https${CURSOR}://google.com) and here`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 18, 0, 46], [0, 17, 0, 47], [0, 11, 0, 47], [0, 0, 0, 56]);
|
||||
});
|
||||
|
||||
test('Smart select brackets', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`stuff here [te${CURSOR}xt](https://google.com) and here`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 12, 0, 26], [0, 11, 0, 27], [0, 11, 0, 47], [0, 0, 0, 56]);
|
||||
});
|
||||
|
||||
test('Smart select brackets under header in list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`- list`,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
`- list`,
|
||||
`- stuff here [te${CURSOR}xt](https://google.com) and here`,
|
||||
`- list`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [6, 14, 6, 28], [6, 13, 6, 29], [6, 13, 6, 49], [6, 0, 6, 58], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]);
|
||||
});
|
||||
|
||||
test('Smart select link under header in list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`- list`,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
`- list`,
|
||||
`- stuff here [text](${CURSOR}https://google.com) and here`,
|
||||
`- list`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [6, 20, 6, 48], [6, 19, 6, 49], [6, 13, 6, 49], [6, 0, 6, 58], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]);
|
||||
});
|
||||
|
||||
test('Smart select bold within list where multiple bold elements exists', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`- list`,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
`- list`,
|
||||
`- stuff here [text] **${CURSOR}items in here** and **here**`,
|
||||
`- list`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [6, 22, 6, 45], [6, 20, 6, 47], [6, 0, 6, 60], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]);
|
||||
});
|
||||
|
||||
test('Smart select link in paragraph with multiple links', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`This[extension](https://marketplace.visualstudio.com/items?itemName=meganrogge.template-string-converter) addresses this [requ${CURSOR}est](https://github.com/microsoft/vscode/issues/56704) to convert Javascript/Typescript quotes to backticks when has been entered within a string.`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 123, 0, 140], [0, 122, 0, 141], [0, 122, 0, 191], [0, 0, 0, 283]);
|
||||
});
|
||||
|
||||
test('Smart select bold link', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`**[extens${CURSOR}ion](https://google.com)**`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 3, 0, 22], [0, 2, 0, 23], [0, 2, 0, 43], [0, 2, 0, 43], [0, 0, 0, 45], [0, 0, 0, 45]);
|
||||
});
|
||||
|
||||
test('Smart select inline code block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`[\`code ${CURSOR} link\`]`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 2, 0, 22], [0, 1, 0, 23], [0, 0, 0, 24]);
|
||||
});
|
||||
|
||||
test('Smart select link with inline code block text', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`[\`code ${CURSOR} link\`](http://example.com)`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 2, 0, 22], [0, 1, 0, 23], [0, 1, 0, 23], [0, 0, 0, 24], [0, 0, 0, 44], [0, 0, 0, 44]);
|
||||
});
|
||||
|
||||
test('Smart select italic', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`*some nice ${CURSOR}text*`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 1, 0, 25], [0, 0, 0, 26], [0, 0, 0, 26]);
|
||||
});
|
||||
|
||||
test('Smart select italic link', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`*[extens${CURSOR}ion](https://google.com)*`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 2, 0, 21], [0, 1, 0, 22], [0, 1, 0, 42], [0, 1, 0, 42], [0, 0, 0, 43], [0, 0, 0, 43]);
|
||||
});
|
||||
|
||||
test('Smart select italic on end', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`*word1 word2 word3${CURSOR}*`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 1, 0, 28], [0, 0, 0, 29], [0, 0, 0, 29]);
|
||||
});
|
||||
|
||||
test('Smart select italic then bold', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`outer text **bold words *italic ${CURSOR} words* bold words** outer text`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 25, 0, 48], [0, 24, 0, 49], [0, 13, 0, 60], [0, 11, 0, 62], [0, 0, 0, 73]);
|
||||
});
|
||||
|
||||
test('Smart select bold then italic', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`outer text *italic words **bold ${CURSOR} words** italic words* outer text`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 27, 0, 48], [0, 25, 0, 50], [0, 12, 0, 63], [0, 11, 0, 64], [0, 0, 0, 75]);
|
||||
});
|
||||
|
||||
test('Third level header from release notes', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`---`,
|
||||
`Order: 60`,
|
||||
`TOCTitle: October 2020`,
|
||||
`PageTitle: Visual Studio Code October 2020`,
|
||||
`MetaDescription: Learn what is new in the Visual Studio Code October 2020 Release (1.51)`,
|
||||
`MetaSocialImage: 1_51/release-highlights.png`,
|
||||
`Date: 2020-11-6`,
|
||||
`DownloadVersion: 1.51.1`,
|
||||
`---`,
|
||||
`# October 2020 (version 1.51)`,
|
||||
``,
|
||||
`**Update 1.51.1**: The update addresses these [issues](https://github.com/microsoft/vscode/issues?q=is%3Aissue+milestone%3A%22October+2020+Recovery%22+is%3Aclosed+).`,
|
||||
``,
|
||||
`<!-- DOWNLOAD_LINKS_PLACEHOLDER -->`,
|
||||
``,
|
||||
`---`,
|
||||
``,
|
||||
`Welcome to the October 2020 release of Visual Studio Code. As announced in the [October iteration plan](https://github.com/microsoft/vscode/issues/108473), we focused on housekeeping GitHub issues and pull requests as documented in our issue grooming guide.`,
|
||||
``,
|
||||
`We also worked with our partners at GitHub on GitHub Codespaces, which ended up being more involved than originally anticipated. To that end, we'll continue working on housekeeping for part of the November iteration.`,
|
||||
``,
|
||||
`During this housekeeping milestone, we also addressed several feature requests and community [pull requests](#thank-you). Read on to learn about new features and settings.`,
|
||||
``,
|
||||
`## Workbench`,
|
||||
``,
|
||||
`### More prominent pinned tabs`,
|
||||
``,
|
||||
`${CURSOR}Pinned tabs will now always show their pin icon, even while inactive, to make them easier to identify. If an editor is both pinned and contains unsaved changes, the icon reflects both states.`,
|
||||
``,
|
||||
``
|
||||
)
|
||||
);
|
||||
assertNestedRangesEqual(ranges![0], [27, 0, 27, 201], [26, 0, 29, 70], [25, 0, 29, 70], [24, 0, 29, 70], [23, 0, 29, 70], [10, 0, 29, 70], [9, 0, 29, 70]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
function assertNestedLineNumbersEqual(range: vscode.SelectionRange, ...expectedRanges: [number, number][]) {
|
||||
const lineage = getLineage(range);
|
||||
assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was ${lineage.length} ${getValues(lineage)}`);
|
||||
for (let i = 0; i < lineage.length; i++) {
|
||||
assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][1], `parent at a depth of ${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNestedRangesEqual(range: vscode.SelectionRange, ...expectedRanges: [number, number, number, number][]) {
|
||||
const lineage = getLineage(range);
|
||||
assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was ${lineage.length} ${getValues(lineage)}`);
|
||||
for (let i = 0; i < lineage.length; i++) {
|
||||
assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][2], `parent at a depth of ${i}`);
|
||||
assert(lineage[i].range.start.character === expectedRanges[i][1], `parent at a depth of ${i} on start char`);
|
||||
assert(lineage[i].range.end.character === expectedRanges[i][3], `parent at a depth of ${i} on end char`);
|
||||
}
|
||||
}
|
||||
|
||||
function getLineage(range: vscode.SelectionRange): vscode.SelectionRange[] {
|
||||
const result: vscode.SelectionRange[] = [];
|
||||
let currentRange: vscode.SelectionRange | undefined = range;
|
||||
while (currentRange) {
|
||||
result.push(currentRange);
|
||||
currentRange = currentRange.parent;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getValues(ranges: vscode.SelectionRange[]): string[] {
|
||||
return ranges.map(range => {
|
||||
return range.range.start.line + ' ' + range.range.start.character + ' ' + range.range.end.line + ' ' + range.range.end.character;
|
||||
});
|
||||
}
|
||||
|
||||
function assertLineNumbersEqual(selectionRange: vscode.SelectionRange, startLine: number, endLine: number, message: string) {
|
||||
assert.strictEqual(selectionRange.range.start.line, startLine, `failed on start line ${message}`);
|
||||
assert.strictEqual(selectionRange.range.end.line, endLine, `failed on end line ${message}`);
|
||||
}
|
||||
|
||||
function getSelectionRangesForDocument(contents: string, pos?: vscode.Position[]): Promise<vscode.SelectionRange[] | undefined> {
|
||||
const doc = new InMemoryDocument(testFileName, contents);
|
||||
const provider = new MdSmartSelect(createNewMarkdownEngine());
|
||||
const positions = pos ? pos : getCursorPositions(contents, doc);
|
||||
return provider.provideSelectionRanges(doc, positions, new vscode.CancellationTokenSource().token);
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { TableOfContents } from '../tableOfContents';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
suite('markdown.TableOfContentsProvider', () => {
|
||||
test('Lookup should not return anything for empty document', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, '');
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual(provider.lookup(''), undefined);
|
||||
assert.strictEqual(provider.lookup('foo'), undefined);
|
||||
});
|
||||
|
||||
test('Lookup should not return anything for document with no headers', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, 'a *b*\nc');
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual(provider.lookup(''), undefined);
|
||||
assert.strictEqual(provider.lookup('foo'), undefined);
|
||||
assert.strictEqual(provider.lookup('a'), undefined);
|
||||
assert.strictEqual(provider.lookup('b'), undefined);
|
||||
});
|
||||
|
||||
test('Lookup should return basic #header', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# a\nx\n# c`);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
{
|
||||
const entry = provider.lookup('a');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 0);
|
||||
}
|
||||
{
|
||||
assert.strictEqual(provider.lookup('x'), undefined);
|
||||
}
|
||||
{
|
||||
const entry = provider.lookup('c');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 2);
|
||||
}
|
||||
});
|
||||
|
||||
test('Lookups should be case in-sensitive', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# fOo\n`);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((provider.lookup('fOo'))!.line, 0);
|
||||
assert.strictEqual((provider.lookup('foo'))!.line, 0);
|
||||
assert.strictEqual((provider.lookup('FOO'))!.line, 0);
|
||||
});
|
||||
|
||||
test('Lookups should ignore leading and trailing white-space, and collapse internal whitespace', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# f o o \n`);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((provider.lookup('f o o'))!.line, 0);
|
||||
assert.strictEqual((provider.lookup(' f o o'))!.line, 0);
|
||||
assert.strictEqual((provider.lookup(' f o o '))!.line, 0);
|
||||
assert.strictEqual((provider.lookup('f o o'))!.line, 0);
|
||||
assert.strictEqual((provider.lookup('f o o'))!.line, 0);
|
||||
|
||||
assert.strictEqual(provider.lookup('f'), undefined);
|
||||
assert.strictEqual(provider.lookup('foo'), undefined);
|
||||
assert.strictEqual(provider.lookup('fo o'), undefined);
|
||||
});
|
||||
|
||||
test('should handle special characters #44779', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# Indentação\n`);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((provider.lookup('indentação'))!.line, 0);
|
||||
});
|
||||
|
||||
test('should handle special characters 2, #48482', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# Инструкция - Делай Раз, Делай Два\n`);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((provider.lookup('инструкция---делай-раз-делай-два'))!.line, 0);
|
||||
});
|
||||
|
||||
test('should handle special characters 3, #37079', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `## Header 2
|
||||
### Header 3
|
||||
## Заголовок 2
|
||||
### Заголовок 3
|
||||
### Заголовок Header 3
|
||||
## Заголовок`);
|
||||
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((provider.lookup('header-2'))!.line, 0);
|
||||
assert.strictEqual((provider.lookup('header-3'))!.line, 1);
|
||||
assert.strictEqual((provider.lookup('Заголовок-2'))!.line, 2);
|
||||
assert.strictEqual((provider.lookup('Заголовок-3'))!.line, 3);
|
||||
assert.strictEqual((provider.lookup('Заголовок-header-3'))!.line, 4);
|
||||
assert.strictEqual((provider.lookup('Заголовок'))!.line, 5);
|
||||
});
|
||||
|
||||
test('Lookup should support suffixes for repeated headers', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# a\n# a\n## a`);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
{
|
||||
const entry = provider.lookup('a');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 0);
|
||||
}
|
||||
{
|
||||
const entry = provider.lookup('a-1');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 1);
|
||||
}
|
||||
{
|
||||
const entry = provider.lookup('a-2');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 2);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -5,33 +5,11 @@
|
||||
import * as assert from 'assert';
|
||||
import * as os from 'os';
|
||||
import * as vscode from 'vscode';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { DisposableStore } from '../util/dispose';
|
||||
|
||||
export const joinLines = (...args: string[]) =>
|
||||
args.join(os.platform() === 'win32' ? '\r\n' : '\n');
|
||||
|
||||
export const noopToken = new class implements vscode.CancellationToken {
|
||||
_onCancellationRequestedEmitter = new vscode.EventEmitter<void>();
|
||||
onCancellationRequested = this._onCancellationRequestedEmitter.event;
|
||||
|
||||
get isCancellationRequested() { return false; }
|
||||
};
|
||||
|
||||
export const CURSOR = '$$CURSOR$$';
|
||||
|
||||
export function getCursorPositions(contents: string, doc: InMemoryDocument): vscode.Position[] {
|
||||
let 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);
|
||||
@@ -43,3 +21,14 @@ export function assertRangeEqual(expected: vscode.Range, actual: vscode.Range, m
|
||||
assert.strictEqual(expected.end.line, actual.end.line, message);
|
||||
assert.strictEqual(expected.end.character, actual.end.character, message);
|
||||
}
|
||||
|
||||
export function withStore<R>(fn: (this: Mocha.Context, store: DisposableStore) => Promise<R>) {
|
||||
return async function (this: Mocha.Context): Promise<R> {
|
||||
const store = new DisposableStore();
|
||||
try {
|
||||
return await fn.call(this, store);
|
||||
} finally {
|
||||
store.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { MdDocumentSymbolProvider } from '../languageFeatures/documentSymbolProvider';
|
||||
import { MdWorkspaceSymbolProvider } from '../languageFeatures/workspaceSymbolProvider';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
|
||||
|
||||
|
||||
const symbolProvider = new MdDocumentSymbolProvider(createNewMarkdownEngine());
|
||||
|
||||
suite('markdown.WorkspaceSymbolProvider', () => {
|
||||
test('Should not return anything for empty workspace', async () => {
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocuments([]));
|
||||
|
||||
assert.deepStrictEqual(await provider.provideWorkspaceSymbols(''), []);
|
||||
});
|
||||
|
||||
test('Should return symbols from workspace with one markdown file', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocuments([
|
||||
new InMemoryDocument(testFileName, `# header1\nabc\n## header2`)
|
||||
]));
|
||||
|
||||
const symbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(symbols.length, 2);
|
||||
assert.strictEqual(symbols[0].name, '# header1');
|
||||
assert.strictEqual(symbols[1].name, '## header2');
|
||||
});
|
||||
|
||||
test('Should return all content basic workspace', async () => {
|
||||
const fileNameCount = 10;
|
||||
const files: SkinnyTextDocument[] = [];
|
||||
for (let i = 0; i < fileNameCount; ++i) {
|
||||
const testFileName = vscode.Uri.file(`test${i}.md`);
|
||||
files.push(new InMemoryDocument(testFileName, `# common\nabc\n## header${i}`));
|
||||
}
|
||||
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocuments(files));
|
||||
|
||||
const symbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(symbols.length, fileNameCount * 2);
|
||||
});
|
||||
|
||||
test('Should update results when markdown file changes symbols', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocuments([
|
||||
new InMemoryDocument(testFileName, `# header1`, 1 /* version */)
|
||||
]);
|
||||
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
|
||||
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
|
||||
|
||||
// Update file
|
||||
workspaceFileProvider.updateDocument(new InMemoryDocument(testFileName, `# new header\nabc\n## header2`, 2 /* version */));
|
||||
const newSymbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(newSymbols.length, 2);
|
||||
assert.strictEqual(newSymbols[0].name, '# new header');
|
||||
assert.strictEqual(newSymbols[1].name, '## header2');
|
||||
});
|
||||
|
||||
test('Should remove results when file is deleted', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocuments([
|
||||
new InMemoryDocument(testFileName, `# header1`)
|
||||
]);
|
||||
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
|
||||
|
||||
// delete file
|
||||
workspaceFileProvider.deleteDocument(testFileName);
|
||||
const newSymbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(newSymbols.length, 0);
|
||||
});
|
||||
|
||||
test('Should update results when markdown file is created', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocuments([
|
||||
new InMemoryDocument(testFileName, `# header1`)
|
||||
]);
|
||||
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
|
||||
|
||||
// Creat file
|
||||
workspaceFileProvider.createDocument(new InMemoryDocument(vscode.Uri.file('test2.md'), `# new header\nabc\n## header2`));
|
||||
const newSymbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(newSymbols.length, 3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
/**
|
||||
* Minimal version of {@link vscode.TextDocument}.
|
||||
*/
|
||||
export interface ITextDocument {
|
||||
readonly uri: vscode.Uri;
|
||||
readonly version: number;
|
||||
readonly lineCount: number;
|
||||
|
||||
getText(range?: vscode.Range): string;
|
||||
positionAt(offset: number): vscode.Position;
|
||||
}
|
||||
|
||||
export function getLine(doc: ITextDocument, line: number): string {
|
||||
return doc.getText(new vscode.Range(line, 0, line, Number.MAX_VALUE)).replace(/\r?\n$/, '');
|
||||
}
|
||||
@@ -25,6 +25,10 @@ export class Delayer<T> {
|
||||
this.task = null;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.cancelTimeout();
|
||||
}
|
||||
|
||||
public trigger(task: ITask<T>, delay: number = this.defaultDelay): Promise<T | null> {
|
||||
this.task = task;
|
||||
if (delay >= 0) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export const noopToken = new class implements vscode.CancellationToken {
|
||||
_onCancellationRequestedEmitter = new vscode.EventEmitter<void>();
|
||||
onCancellationRequested = this._onCancellationRequestedEmitter.event;
|
||||
|
||||
get isCancellationRequested() { return false; }
|
||||
};
|
||||
@@ -5,13 +5,36 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function disposeAll(disposables: vscode.Disposable[]) {
|
||||
while (disposables.length) {
|
||||
const item = disposables.pop();
|
||||
item?.dispose();
|
||||
export class MultiDisposeError extends Error {
|
||||
constructor(
|
||||
public readonly errors: any[]
|
||||
) {
|
||||
super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`);
|
||||
}
|
||||
}
|
||||
|
||||
export function disposeAll(disposables: Iterable<vscode.Disposable>) {
|
||||
const errors: any[] = [];
|
||||
|
||||
for (const disposable of disposables) {
|
||||
try {
|
||||
disposable.dispose();
|
||||
} catch (e) {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length === 1) {
|
||||
throw errors[0];
|
||||
} else if (errors.length > 1) {
|
||||
throw new MultiDisposeError(errors);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export abstract class Disposable {
|
||||
private _isDisposed = false;
|
||||
|
||||
@@ -25,7 +48,7 @@ export abstract class Disposable {
|
||||
disposeAll(this._disposables);
|
||||
}
|
||||
|
||||
protected _register<T extends vscode.Disposable>(value: T): T {
|
||||
protected _register<T extends IDisposable>(value: T): T {
|
||||
if (this._isDisposed) {
|
||||
value.dispose();
|
||||
} else {
|
||||
@@ -38,3 +61,22 @@ export abstract class Disposable {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
|
||||
export class DisposableStore extends Disposable {
|
||||
private readonly items = new Set<IDisposable>();
|
||||
|
||||
public override dispose() {
|
||||
super.dispose();
|
||||
disposeAll(this.items);
|
||||
this.items.clear();
|
||||
}
|
||||
|
||||
public add<T extends IDisposable>(item: T): T {
|
||||
if (this.isDisposed) {
|
||||
console.warn('Adding to disposed store. Item will be leaked');
|
||||
}
|
||||
|
||||
this.items.add(item);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
18
extensions/markdown-language-features/src/util/dom.ts
Normal file
18
extensions/markdown-language-features/src/util/dom.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function escapeAttribute(value: string | vscode.Uri): string {
|
||||
return value.toString().replace(/"/g, '"');
|
||||
}
|
||||
|
||||
export function getNonce() {
|
||||
let text = '';
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < 64; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -4,7 +4,24 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as URI from 'vscode-uri';
|
||||
|
||||
export const markdownFileExtensions = Object.freeze<string[]>([
|
||||
'md',
|
||||
'mkd',
|
||||
'mdwn',
|
||||
'mdown',
|
||||
'markdown',
|
||||
'markdn',
|
||||
'mdtxt',
|
||||
'mdtext',
|
||||
'workbook',
|
||||
]);
|
||||
|
||||
export function isMarkdownFile(document: vscode.TextDocument) {
|
||||
return document.languageId === 'markdown';
|
||||
}
|
||||
|
||||
export function looksLikeMarkdownPath(resolvedHrefPath: vscode.Uri) {
|
||||
return markdownFileExtensions.includes(URI.Utils.extname(resolvedHrefPath).toLowerCase().replace('.', ''));
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { TextDocument } from 'vscode-languageserver-textdocument';
|
||||
import { SkinnyTextDocument, SkinnyTextLine } from '../workspaceContents';
|
||||
import { ITextDocument } from '../types/textDocument';
|
||||
|
||||
export class InMemoryDocument implements SkinnyTextDocument {
|
||||
export class InMemoryDocument implements ITextDocument {
|
||||
|
||||
private readonly _doc: TextDocument;
|
||||
|
||||
private lines: SkinnyTextLine[] | undefined;
|
||||
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri, contents: string,
|
||||
public readonly version = 0,
|
||||
@@ -25,16 +23,6 @@ export class InMemoryDocument implements SkinnyTextDocument {
|
||||
return this._doc.lineCount;
|
||||
}
|
||||
|
||||
lineAt(index: any): SkinnyTextLine {
|
||||
if (!this.lines) {
|
||||
this.lines = this._doc.getText().split(/\r?\n/).map(text => ({
|
||||
text,
|
||||
get isEmptyOrWhitespace() { return /^\s*$/.test(text); }
|
||||
}));
|
||||
}
|
||||
return this.lines[index];
|
||||
}
|
||||
|
||||
positionAt(offset: number): vscode.Position {
|
||||
const pos = this._doc.positionAt(offset);
|
||||
return new vscode.Position(pos.line, pos.character);
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as uri from 'vscode-uri';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContents } from '../tableOfContents';
|
||||
import { MdTableOfContentsProvider } from '../tableOfContents';
|
||||
import { ITextDocument } from '../types/textDocument';
|
||||
import { IMdWorkspace } from '../workspace';
|
||||
import { isMarkdownFile } from './file';
|
||||
|
||||
export interface OpenDocumentLinkArgs {
|
||||
@@ -22,7 +23,7 @@ enum OpenMarkdownLinks {
|
||||
}
|
||||
|
||||
export function resolveDocumentLink(href: string, markdownFile: vscode.Uri): vscode.Uri {
|
||||
let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
|
||||
const [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
|
||||
|
||||
if (hrefPath[0] === '/') {
|
||||
// Absolute path. Try to resolve relative to the workspace
|
||||
@@ -37,10 +38,10 @@ export function resolveDocumentLink(href: string, markdownFile: vscode.Uri): vsc
|
||||
return vscode.Uri.joinPath(dirnameUri, hrefPath).with({ fragment });
|
||||
}
|
||||
|
||||
export async function openDocumentLink(engine: MarkdownEngine, targetResource: vscode.Uri, fromResource: vscode.Uri): Promise<void> {
|
||||
export async function openDocumentLink(tocProvider: MdTableOfContentsProvider, targetResource: vscode.Uri, fromResource: vscode.Uri): Promise<void> {
|
||||
const column = getViewColumn(fromResource);
|
||||
|
||||
if (await tryNavigateToFragmentInActiveEditor(engine, targetResource)) {
|
||||
if (await tryNavigateToFragmentInActiveEditor(tocProvider, targetResource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ export async function openDocumentLink(engine: MarkdownEngine, targetResource: v
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(dotMdResource);
|
||||
if (stat.type === vscode.FileType.File) {
|
||||
await tryOpenMdFile(engine, dotMdResource, column);
|
||||
await tryOpenMdFile(tocProvider, dotMdResource, column);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
@@ -69,25 +70,33 @@ export async function openDocumentLink(engine: MarkdownEngine, targetResource: v
|
||||
return vscode.commands.executeCommand('revealInExplorer', targetResource);
|
||||
}
|
||||
|
||||
await tryOpenMdFile(engine, targetResource, column);
|
||||
await tryOpenMdFile(tocProvider, targetResource, column);
|
||||
}
|
||||
|
||||
async function tryOpenMdFile(engine: MarkdownEngine, resource: vscode.Uri, column: vscode.ViewColumn): Promise<boolean> {
|
||||
async function tryOpenMdFile(tocProvider: MdTableOfContentsProvider, resource: vscode.Uri, column: vscode.ViewColumn): Promise<boolean> {
|
||||
await vscode.commands.executeCommand('vscode.open', resource.with({ fragment: '' }), column);
|
||||
return tryNavigateToFragmentInActiveEditor(engine, resource);
|
||||
return tryNavigateToFragmentInActiveEditor(tocProvider, resource);
|
||||
}
|
||||
|
||||
async function tryNavigateToFragmentInActiveEditor(engine: MarkdownEngine, resource: vscode.Uri): Promise<boolean> {
|
||||
async function tryNavigateToFragmentInActiveEditor(tocProvider: MdTableOfContentsProvider, resource: vscode.Uri): Promise<boolean> {
|
||||
const notebookEditor = vscode.window.activeNotebookEditor;
|
||||
if (notebookEditor?.notebook.uri.fsPath === resource.fsPath) {
|
||||
if (await tryRevealLineInNotebook(tocProvider, notebookEditor, resource.fragment)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
if (activeEditor?.document.uri.fsPath === resource.fsPath) {
|
||||
if (isMarkdownFile(activeEditor.document)) {
|
||||
if (await tryRevealLineUsingTocFragment(engine, activeEditor, resource.fragment)) {
|
||||
if (await tryRevealLineUsingTocFragment(tocProvider, activeEditor, resource.fragment)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
tryRevealLineUsingLineFragment(activeEditor, resource.fragment);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -103,8 +112,26 @@ function getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
|
||||
}
|
||||
}
|
||||
|
||||
async function tryRevealLineUsingTocFragment(engine: MarkdownEngine, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
|
||||
const toc = await TableOfContents.create(engine, editor.document);
|
||||
async function tryRevealLineInNotebook(tocProvider: MdTableOfContentsProvider, editor: vscode.NotebookEditor, fragment: string): Promise<boolean> {
|
||||
const toc = await tocProvider.createForNotebook(editor.notebook);
|
||||
const entry = toc.lookup(fragment);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cell = editor.notebook.getCells().find(cell => cell.document.uri.toString() === entry.sectionLocation.uri.toString());
|
||||
if (!cell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const range = new vscode.NotebookRange(cell.index, cell.index);
|
||||
editor.selection = range;
|
||||
editor.revealRange(range);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function tryRevealLineUsingTocFragment(tocProvider: MdTableOfContentsProvider, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
|
||||
const toc = await tocProvider.getForDocument(editor.document);
|
||||
const entry = toc.lookup(fragment);
|
||||
if (entry) {
|
||||
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
|
||||
@@ -129,9 +156,9 @@ function tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: str
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolveUriToMarkdownFile(resource: vscode.Uri): Promise<vscode.TextDocument | undefined> {
|
||||
export async function resolveUriToMarkdownFile(workspace: IMdWorkspace, resource: vscode.Uri): Promise<ITextDocument | undefined> {
|
||||
try {
|
||||
const doc = await tryResolveUriToMarkdownFile(resource);
|
||||
const doc = await workspace.getOrLoadMarkdownDocument(resource);
|
||||
if (doc) {
|
||||
return doc;
|
||||
}
|
||||
@@ -141,21 +168,8 @@ export async function resolveUriToMarkdownFile(resource: vscode.Uri): Promise<vs
|
||||
|
||||
// If no extension, try with `.md` extension
|
||||
if (uri.Utils.extname(resource) === '') {
|
||||
return tryResolveUriToMarkdownFile(resource.with({ path: resource.path + '.md' }));
|
||||
return workspace.getOrLoadMarkdownDocument(resource.with({ path: resource.path + '.md' }));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function tryResolveUriToMarkdownFile(resource: vscode.Uri): Promise<vscode.TextDocument | undefined> {
|
||||
let document: vscode.TextDocument;
|
||||
try {
|
||||
document = await vscode.workspace.openTextDocument(resource);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (isMarkdownFile(document)) {
|
||||
return document;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
type ResourceToKey = (uri: vscode.Uri) => string;
|
||||
|
||||
const defaultResourceToKey = (resource: vscode.Uri): string => resource.toString();
|
||||
|
||||
export class ResourceMap<T> {
|
||||
|
||||
private readonly map = new Map<string, { readonly uri: vscode.Uri; readonly value: T }>();
|
||||
|
||||
private readonly toKey: ResourceToKey;
|
||||
|
||||
constructor(toKey: ResourceToKey = defaultResourceToKey) {
|
||||
this.toKey = toKey;
|
||||
}
|
||||
|
||||
public set(uri: vscode.Uri, value: T): this {
|
||||
this.map.set(this.toKey(uri), { uri, value });
|
||||
return this;
|
||||
}
|
||||
|
||||
public get(resource: vscode.Uri): T | undefined {
|
||||
return this.map.get(this.toKey(resource))?.value;
|
||||
}
|
||||
|
||||
public has(resource: vscode.Uri): boolean {
|
||||
return this.map.has(this.toKey(resource));
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.map.size;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.map.clear();
|
||||
}
|
||||
|
||||
public delete(resource: vscode.Uri): boolean {
|
||||
return this.map.delete(this.toKey(resource));
|
||||
}
|
||||
|
||||
public *values(): IterableIterator<T> {
|
||||
for (const entry of this.map.values()) {
|
||||
yield entry.value;
|
||||
}
|
||||
}
|
||||
|
||||
public *keys(): IterableIterator<vscode.Uri> {
|
||||
for (const entry of this.map.values()) {
|
||||
yield entry.uri;
|
||||
}
|
||||
}
|
||||
|
||||
public *entries(): IterableIterator<[vscode.Uri, T]> {
|
||||
for (const entry of this.map.values()) {
|
||||
yield [entry.uri, entry.value];
|
||||
}
|
||||
}
|
||||
|
||||
public [Symbol.iterator](): IterableIterator<[vscode.Uri, T]> {
|
||||
return this.entries();
|
||||
}
|
||||
}
|
||||
@@ -3,44 +3,15 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export const Schemes = {
|
||||
http: 'http:',
|
||||
https: 'https:',
|
||||
file: 'file:',
|
||||
export const Schemes = Object.freeze({
|
||||
file: 'file',
|
||||
untitled: 'untitled',
|
||||
mailto: 'mailto:',
|
||||
data: 'data:',
|
||||
vscode: 'vscode:',
|
||||
'vscode-insiders': 'vscode-insiders:',
|
||||
};
|
||||
|
||||
const knownSchemes = [
|
||||
...Object.values(Schemes),
|
||||
`${vscode.env.uriScheme}:`
|
||||
];
|
||||
|
||||
export function getUriForLinkWithKnownExternalScheme(link: string): vscode.Uri | undefined {
|
||||
if (knownSchemes.some(knownScheme => isOfScheme(knownScheme, link))) {
|
||||
return vscode.Uri.parse(link);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
mailto: 'mailto',
|
||||
vscode: 'vscode',
|
||||
'vscode-insiders': 'vscode-insiders',
|
||||
notebookCell: 'vscode-notebook-cell',
|
||||
});
|
||||
|
||||
export function isOfScheme(scheme: string, link: string): boolean {
|
||||
return link.toLowerCase().startsWith(scheme);
|
||||
return link.toLowerCase().startsWith(scheme + ':');
|
||||
}
|
||||
|
||||
export const MarkdownFileExtensions: readonly string[] = [
|
||||
'.md',
|
||||
'.mkd',
|
||||
'.mdwn',
|
||||
'.mdown',
|
||||
'.markdown',
|
||||
'.markdn',
|
||||
'.mdtxt',
|
||||
'.mdtext',
|
||||
'.workbook',
|
||||
];
|
||||
|
||||
8
extensions/markdown-language-features/src/util/string.ts
Normal file
8
extensions/markdown-language-features/src/util/string.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function isEmptyOrWhitespace(str: string): boolean {
|
||||
return /^\s*$/.test(str);
|
||||
}
|
||||
116
extensions/markdown-language-features/src/util/workspaceCache.ts
Normal file
116
extensions/markdown-language-features/src/util/workspaceCache.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ITextDocument } from '../types/textDocument';
|
||||
import { IMdWorkspace } from '../workspace';
|
||||
import { Disposable } from './dispose';
|
||||
import { Lazy, lazy } from './lazy';
|
||||
import { ResourceMap } from './resourceMap';
|
||||
|
||||
class LazyResourceMap<T> {
|
||||
private readonly _map = new ResourceMap<Lazy<Promise<T>>>();
|
||||
|
||||
public has(resource: vscode.Uri): boolean {
|
||||
return this._map.has(resource);
|
||||
}
|
||||
|
||||
public get(resource: vscode.Uri): Promise<T> | undefined {
|
||||
return this._map.get(resource)?.value;
|
||||
}
|
||||
|
||||
public set(resource: vscode.Uri, value: Lazy<Promise<T>>) {
|
||||
this._map.set(resource, value);
|
||||
}
|
||||
|
||||
public delete(resource: vscode.Uri) {
|
||||
this._map.delete(resource);
|
||||
}
|
||||
|
||||
public entries(): Promise<Array<[vscode.Uri, T]>> {
|
||||
return Promise.all(Array.from(this._map.entries(), async ([key, entry]) => {
|
||||
return [key, await entry.value] as [vscode.Uri, T]; // {{SQL CARBON EDIT}} lewissanchez - Added strict typing
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache of information per-document in the workspace.
|
||||
*
|
||||
* The values are computed lazily and invalidated when the document changes.
|
||||
*/
|
||||
export class MdDocumentInfoCache<T> extends Disposable {
|
||||
|
||||
private readonly _cache = new LazyResourceMap<T>();
|
||||
private readonly _loadingDocuments = new ResourceMap<Promise<ITextDocument | undefined>>();
|
||||
|
||||
public constructor(
|
||||
private readonly workspace: IMdWorkspace,
|
||||
private readonly getValue: (document: ITextDocument) => Promise<T>,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(this.workspace.onDidChangeMarkdownDocument(doc => this.invalidate(doc)));
|
||||
this._register(this.workspace.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
|
||||
}
|
||||
|
||||
public async get(resource: vscode.Uri): Promise<T | undefined> {
|
||||
let existing = this._cache.get(resource);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const doc = await this.loadDocument(resource);
|
||||
if (!doc) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if we have invalidated
|
||||
existing = this._cache.get(resource);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return this.resetEntry(doc)?.value;
|
||||
}
|
||||
|
||||
public async getForDocument(document: ITextDocument): Promise<T> {
|
||||
const existing = this._cache.get(document.uri);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
return this.resetEntry(document).value;
|
||||
}
|
||||
|
||||
private loadDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
|
||||
const existing = this._loadingDocuments.get(resource);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const p = this.workspace.getOrLoadMarkdownDocument(resource);
|
||||
this._loadingDocuments.set(resource, p);
|
||||
p.finally(() => {
|
||||
this._loadingDocuments.delete(resource);
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
private resetEntry(document: ITextDocument): Lazy<Promise<T>> {
|
||||
const value = lazy(() => this.getValue(document));
|
||||
this._cache.set(document.uri, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private invalidate(document: ITextDocument): void {
|
||||
if (this._cache.has(document.uri)) {
|
||||
this.resetEntry(document);
|
||||
}
|
||||
}
|
||||
|
||||
private onDidDeleteDocument(resource: vscode.Uri) {
|
||||
this._cache.delete(resource);
|
||||
}
|
||||
}
|
||||
@@ -4,48 +4,36 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ITextDocument } from './types/textDocument';
|
||||
import { coalesce } from './util/arrays';
|
||||
import { Disposable } from './util/dispose';
|
||||
import { isMarkdownFile } from './util/file';
|
||||
import { isMarkdownFile, looksLikeMarkdownPath } from './util/file';
|
||||
import { InMemoryDocument } from './util/inMemoryDocument';
|
||||
import { Limiter } from './util/limiter';
|
||||
|
||||
/**
|
||||
* Minimal version of {@link vscode.TextLine}. Used for mocking out in testing.
|
||||
*/
|
||||
export interface SkinnyTextLine {
|
||||
readonly text: string;
|
||||
readonly isEmptyOrWhitespace: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal version of {@link vscode.TextDocument}. Used for mocking out in testing.
|
||||
*/
|
||||
export interface SkinnyTextDocument {
|
||||
readonly uri: vscode.Uri;
|
||||
readonly version: number;
|
||||
readonly lineCount: number;
|
||||
|
||||
getText(range?: vscode.Range): string;
|
||||
lineAt(line: number): SkinnyTextLine;
|
||||
positionAt(offset: number): vscode.Position;
|
||||
}
|
||||
import { ResourceMap } from './util/resourceMap';
|
||||
|
||||
/**
|
||||
* Provides set of markdown files in the current workspace.
|
||||
*/
|
||||
export interface MdWorkspaceContents {
|
||||
export interface IMdWorkspace {
|
||||
/**
|
||||
* Get list of all known markdown files.
|
||||
*/
|
||||
getAllMarkdownDocuments(): Promise<Iterable<SkinnyTextDocument>>;
|
||||
getAllMarkdownDocuments(): Promise<Iterable<ITextDocument>>;
|
||||
|
||||
getMarkdownDocument(resource: vscode.Uri): Promise<SkinnyTextDocument | undefined>;
|
||||
/**
|
||||
* Check if a document already exists in the workspace contents.
|
||||
*/
|
||||
hasMarkdownDocument(resource: vscode.Uri): boolean;
|
||||
|
||||
getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined>;
|
||||
|
||||
pathExists(resource: vscode.Uri): Promise<boolean>;
|
||||
|
||||
readonly onDidChangeMarkdownDocument: vscode.Event<SkinnyTextDocument>;
|
||||
readonly onDidCreateMarkdownDocument: vscode.Event<SkinnyTextDocument>;
|
||||
readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]>;
|
||||
|
||||
readonly onDidChangeMarkdownDocument: vscode.Event<ITextDocument>;
|
||||
readonly onDidCreateMarkdownDocument: vscode.Event<ITextDocument>;
|
||||
readonly onDidDeleteMarkdownDocument: vscode.Event<vscode.Uri>;
|
||||
}
|
||||
|
||||
@@ -54,14 +42,16 @@ export interface MdWorkspaceContents {
|
||||
*
|
||||
* This includes both opened text documents and markdown files in the workspace.
|
||||
*/
|
||||
export class VsCodeMdWorkspaceContents extends Disposable implements MdWorkspaceContents {
|
||||
export class VsCodeMdWorkspace extends Disposable implements IMdWorkspace {
|
||||
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<SkinnyTextDocument>());
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<SkinnyTextDocument>());
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
|
||||
private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<vscode.Uri>());
|
||||
|
||||
private _watcher: vscode.FileSystemWatcher | undefined;
|
||||
|
||||
private readonly _documentCache = new ResourceMap<ITextDocument>();
|
||||
|
||||
private readonly utf8Decoder = new TextDecoder('utf-8');
|
||||
|
||||
/**
|
||||
@@ -70,19 +60,19 @@ export class VsCodeMdWorkspaceContents extends Disposable implements MdWorkspace
|
||||
*
|
||||
* @returns Array of processed .md files.
|
||||
*/
|
||||
async getAllMarkdownDocuments(): Promise<SkinnyTextDocument[]> {
|
||||
async getAllMarkdownDocuments(): Promise<ITextDocument[]> {
|
||||
const maxConcurrent = 20;
|
||||
|
||||
const foundFiles = new Set<string>();
|
||||
const limiter = new Limiter<SkinnyTextDocument | undefined>(maxConcurrent);
|
||||
const foundFiles = new ResourceMap<void>();
|
||||
const limiter = new Limiter<ITextDocument | undefined>(maxConcurrent);
|
||||
|
||||
// Add files on disk
|
||||
const resources = await vscode.workspace.findFiles('**/*.md', '**/node_modules/**');
|
||||
const onDiskResults = await Promise.all(resources.map(resource => {
|
||||
return limiter.queue(async () => {
|
||||
const doc = await this.getMarkdownDocument(resource);
|
||||
const doc = await this.getOrLoadMarkdownDocument(resource);
|
||||
if (doc) {
|
||||
foundFiles.add(doc.uri.toString());
|
||||
foundFiles.set(resource);
|
||||
}
|
||||
return doc;
|
||||
});
|
||||
@@ -90,7 +80,7 @@ export class VsCodeMdWorkspaceContents extends Disposable implements MdWorkspace
|
||||
|
||||
// Add opened files (such as untitled files)
|
||||
const openTextDocumentResults = await Promise.all(vscode.workspace.textDocuments
|
||||
.filter(doc => !foundFiles.has(doc.uri.toString()) && isMarkdownFile(doc)));
|
||||
.filter(doc => !foundFiles.has(doc.uri) && this.isRelevantMarkdownDocument(doc)));
|
||||
|
||||
return coalesce([...onDiskResults, ...openTextDocumentResults]);
|
||||
}
|
||||
@@ -118,47 +108,80 @@ export class VsCodeMdWorkspaceContents extends Disposable implements MdWorkspace
|
||||
this._watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md'));
|
||||
|
||||
this._register(this._watcher.onDidChange(async resource => {
|
||||
const document = await this.getMarkdownDocument(resource);
|
||||
this._documentCache.delete(resource);
|
||||
const document = await this.getOrLoadMarkdownDocument(resource);
|
||||
if (document) {
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._watcher.onDidCreate(async resource => {
|
||||
const document = await this.getMarkdownDocument(resource);
|
||||
const document = await this.getOrLoadMarkdownDocument(resource);
|
||||
if (document) {
|
||||
this._onDidCreateMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._watcher.onDidDelete(resource => {
|
||||
this._documentCache.delete(resource);
|
||||
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
|
||||
}));
|
||||
|
||||
this._register(vscode.workspace.onDidOpenTextDocument(e => {
|
||||
this._documentCache.delete(e.uri);
|
||||
if (this.isRelevantMarkdownDocument(e)) {
|
||||
this._onDidCreateMarkdownDocumentEmitter.fire(e);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(vscode.workspace.onDidChangeTextDocument(e => {
|
||||
if (isMarkdownFile(e.document)) {
|
||||
if (this.isRelevantMarkdownDocument(e.document)) {
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(e.document);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(vscode.workspace.onDidCloseTextDocument(e => {
|
||||
this._documentCache.delete(e.uri);
|
||||
}));
|
||||
}
|
||||
|
||||
public async getMarkdownDocument(resource: vscode.Uri): Promise<SkinnyTextDocument | undefined> {
|
||||
const matchingDocument = vscode.workspace.textDocuments.find((doc) => doc.uri.toString() === resource.toString());
|
||||
private isRelevantMarkdownDocument(doc: vscode.TextDocument) {
|
||||
return isMarkdownFile(doc) && doc.uri.scheme !== 'vscode-bulkeditpreview';
|
||||
}
|
||||
|
||||
public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
|
||||
const existing = this._documentCache.get(resource);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const matchingDocument = vscode.workspace.textDocuments.find((doc) => this.isRelevantMarkdownDocument(doc) && doc.uri.toString() === resource.toString());
|
||||
if (matchingDocument) {
|
||||
this._documentCache.set(resource, matchingDocument);
|
||||
return matchingDocument;
|
||||
}
|
||||
|
||||
if (!looksLikeMarkdownPath(resource)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const bytes = await vscode.workspace.fs.readFile(resource);
|
||||
|
||||
// We assume that markdown is in UTF-8
|
||||
const text = this.utf8Decoder.decode(bytes);
|
||||
return new InMemoryDocument(resource, text, 0);
|
||||
const doc = new InMemoryDocument(resource, text, 0);
|
||||
this._documentCache.set(resource, doc);
|
||||
return doc;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public hasMarkdownDocument(resolvedHrefPath: vscode.Uri): boolean {
|
||||
return this._documentCache.has(resolvedHrefPath);
|
||||
}
|
||||
|
||||
public async pathExists(target: vscode.Uri): Promise<boolean> {
|
||||
let targetResourceStat: vscode.FileStat | undefined;
|
||||
try {
|
||||
@@ -168,4 +191,8 @@ export class VsCodeMdWorkspaceContents extends Disposable implements MdWorkspace
|
||||
}
|
||||
return targetResourceStat.type === vscode.FileType.File || targetResourceStat.type === vscode.FileType.Directory;
|
||||
}
|
||||
|
||||
public async readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]> {
|
||||
return vscode.workspace.fs.readDirectory(resource);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user