mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-04 09:35:38 -05:00
Merge vscode 1.67 (#20883)
* Fix initial build breaks from 1.67 merge (#2514) * Update yarn lock files * Update build scripts * Fix tsconfig * Build breaks * WIP * Update yarn lock files * Misc breaks * Updates to package.json * Breaks * Update yarn * Fix breaks * Breaks * Build breaks * Breaks * Breaks * Breaks * Breaks * Breaks * Missing file * Breaks * Breaks * Breaks * Breaks * Breaks * Fix several runtime breaks (#2515) * Missing files * Runtime breaks * Fix proxy ordering issue * Remove commented code * Fix breaks with opening query editor * Fix post merge break * Updates related to setup build and other breaks (#2516) * Fix bundle build issues * Update distro * Fix distro merge and update build JS files * Disable pipeline steps * Remove stats call * Update license name * Make new RPM dependencies a warning * Fix extension manager version checks * Update JS file * Fix a few runtime breaks * Fixes * Fix runtime issues * Fix build breaks * Update notebook tests (part 1) * Fix broken tests * Linting errors * Fix hygiene * Disable lint rules * Bump distro * Turn off smoke tests * Disable integration tests * Remove failing "activate" test * Remove failed test assertion * Disable other broken test * Disable query history tests * Disable extension unit tests * Disable failing tasks
This commit is contained in:
@@ -22,9 +22,11 @@ export class CommandManager {
|
||||
this.commands.clear();
|
||||
}
|
||||
|
||||
public register<T extends Command>(command: T): T {
|
||||
public register<T extends Command>(command: T): vscode.Disposable {
|
||||
this.registerCommand(command.id, command.execute, command);
|
||||
return command;
|
||||
return new vscode.Disposable(() => {
|
||||
this.commands.delete(command.id);
|
||||
});
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownPreviewManager } from '../features/previewManager';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MarkdownPreviewManager } from '../preview/previewManager';
|
||||
|
||||
export class RefreshPreviewCommand implements Command {
|
||||
public readonly id = 'markdown.preview.refresh';
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownPreviewManager } from '../features/previewManager';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MarkdownPreviewManager } from '../preview/previewManager';
|
||||
|
||||
export class ReloadPlugins implements Command {
|
||||
public readonly id = 'markdown.api.reloadPlugins';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { SkinnyTextDocument } from '../tableOfContentsProvider';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
export class RenderDocument implements Command {
|
||||
public readonly id = 'markdown.api.render';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Command } from '../commandManager';
|
||||
import { DynamicPreviewSettings, MarkdownPreviewManager } from '../features/previewManager';
|
||||
import { DynamicPreviewSettings, MarkdownPreviewManager } from '../preview/previewManager';
|
||||
import { TelemetryReporter } from '../telemetryReporter';
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownPreviewManager } from '../features/previewManager';
|
||||
import { PreviewSecuritySelector } from '../security';
|
||||
import { MarkdownPreviewManager } from '../preview/previewManager';
|
||||
import { PreviewSecuritySelector } from '../preview/security';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
|
||||
export class ShowPreviewSecuritySelectorCommand implements Command {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownPreviewManager } from '../features/previewManager';
|
||||
import { MarkdownPreviewManager } from '../preview/previewManager';
|
||||
|
||||
export class ShowSourceCommand implements Command {
|
||||
public readonly id = 'markdown.showSource';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Command } from '../commandManager';
|
||||
import { MarkdownPreviewManager } from '../features/previewManager';
|
||||
import { MarkdownPreviewManager } from '../preview/previewManager';
|
||||
|
||||
export class ToggleLockCommand implements Command {
|
||||
public readonly id = 'markdown.preview.toggleLock';
|
||||
|
||||
@@ -6,19 +6,27 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { CommandManager } from './commandManager';
|
||||
import * as commands from './commands/index';
|
||||
import LinkProvider from './features/documentLinkProvider';
|
||||
import MDDocumentSymbolProvider from './features/documentSymbolProvider';
|
||||
import MarkdownFoldingProvider from './features/foldingProvider';
|
||||
import { MarkdownContentProvider } from './features/previewContentProvider';
|
||||
import { MarkdownPreviewManager } from './features/previewManager';
|
||||
import MarkdownSmartSelect from './features/smartSelect';
|
||||
import MarkdownWorkspaceSymbolProvider from './features/workspaceSymbolProvider';
|
||||
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 { getMarkdownExtensionContributions } from './markdownExtensions';
|
||||
import { ContentSecurityPolicyArbiter, ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security';
|
||||
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) {
|
||||
@@ -31,14 +39,15 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
|
||||
const engine = new MarkdownEngine(contributions, githubSlugifier);
|
||||
const logger = new Logger();
|
||||
const commandManager = new CommandManager();
|
||||
|
||||
const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, contributions, logger);
|
||||
const symbolProvider = new MDDocumentSymbolProvider(engine);
|
||||
const symbolProvider = new MdDocumentSymbolProvider(engine);
|
||||
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions, engine);
|
||||
context.subscriptions.push(previewManager);
|
||||
|
||||
context.subscriptions.push(registerMarkdownLanguageFeatures(symbolProvider, engine));
|
||||
context.subscriptions.push(registerMarkdownCommands(previewManager, telemetryReporter, cspArbiter, engine));
|
||||
context.subscriptions.push(registerMarkdownLanguageFeatures(commandManager, symbolProvider, engine));
|
||||
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine));
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
|
||||
logger.updateConfiguration();
|
||||
@@ -47,21 +56,34 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}
|
||||
|
||||
function registerMarkdownLanguageFeatures(
|
||||
symbolProvider: MDDocumentSymbolProvider,
|
||||
commandManager: CommandManager,
|
||||
symbolProvider: MdDocumentSymbolProvider,
|
||||
engine: MarkdownEngine
|
||||
): vscode.Disposable {
|
||||
const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' };
|
||||
|
||||
const linkProvider = new MdLinkProvider(engine);
|
||||
const workspaceContents = new VsCodeMdWorkspaceContents();
|
||||
|
||||
const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier);
|
||||
return vscode.Disposable.from(
|
||||
vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider),
|
||||
vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider()),
|
||||
vscode.languages.registerFoldingRangeProvider(selector, new MarkdownFoldingProvider(engine)),
|
||||
vscode.languages.registerSelectionRangeProvider(selector, new MarkdownSmartSelect(engine)),
|
||||
vscode.languages.registerWorkspaceSymbolProvider(new MarkdownWorkspaceSymbolProvider(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,
|
||||
@@ -69,7 +91,6 @@ function registerMarkdownCommands(
|
||||
): vscode.Disposable {
|
||||
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
|
||||
|
||||
const commandManager = new CommandManager();
|
||||
commandManager.register(new commands.ShowPreviewCommand(previewManager, telemetryReporter));
|
||||
commandManager.register(new commands.ShowPreviewToSideCommand(previewManager, telemetryReporter));
|
||||
commandManager.register(new commands.ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
|
||||
@@ -83,4 +104,3 @@ function registerMarkdownCommands(
|
||||
commandManager.register(new commands.ReloadPlugins(previewManager, engine));
|
||||
return commandManager;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,207 +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 { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
|
||||
import { getUriForLinkWithKnownExternalScheme, isOfScheme, Schemes } from '../util/links';
|
||||
import { dirname } from '../util/path';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
function parseLink(
|
||||
document: vscode.TextDocument,
|
||||
link: string,
|
||||
): { uri: vscode.Uri, tooltip?: string } | 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 { uri: vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }) };
|
||||
}
|
||||
return { 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 = document.uri.with({ path: dirname(document.uri.fsPath) });
|
||||
resourceUri = vscode.Uri.joinPath(base, tempUri.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!resourceUri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
resourceUri = resourceUri.with({ fragment: tempUri.fragment });
|
||||
|
||||
return {
|
||||
uri: OpenDocumentLinkCommand.createCommandUri(document.uri, resourceUri, tempUri.fragment),
|
||||
tooltip: localize('documentLink.tooltip', 'Follow link')
|
||||
};
|
||||
}
|
||||
|
||||
function getWorkspaceFolder(document: vscode.TextDocument) {
|
||||
return vscode.workspace.getWorkspaceFolder(document.uri)?.uri
|
||||
|| vscode.workspace.workspaceFolders?.[0]?.uri;
|
||||
}
|
||||
|
||||
function extractDocumentLink(
|
||||
document: vscode.TextDocument,
|
||||
pre: number,
|
||||
link: string,
|
||||
matchIndex: number | undefined
|
||||
): vscode.DocumentLink | undefined {
|
||||
const offset = (matchIndex || 0) + pre;
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
try {
|
||||
const linkData = parseLink(document, link);
|
||||
if (!linkData) {
|
||||
return undefined;
|
||||
}
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
new vscode.Range(linkStart, linkEnd),
|
||||
linkData.uri);
|
||||
documentLink.tooltip = linkData.tooltip;
|
||||
return documentLink;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/* Used to strip brackets from the markdown link
|
||||
<http://example.com> will be transformed to
|
||||
http://example.com
|
||||
*/
|
||||
export function stripAngleBrackets(link: string) {
|
||||
const bracketMatcher = /^<(.*)>$/;
|
||||
return link.replace(bracketMatcher, '$1');
|
||||
}
|
||||
|
||||
export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
private readonly linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g;
|
||||
private readonly referenceLinkPattern = /(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]/g;
|
||||
private readonly definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)(\S+)/gm;
|
||||
|
||||
public provideDocumentLinks(
|
||||
document: vscode.TextDocument,
|
||||
_token: vscode.CancellationToken
|
||||
): vscode.DocumentLink[] {
|
||||
const text = document.getText();
|
||||
|
||||
return [
|
||||
...this.providerInlineLinks(text, document),
|
||||
...this.provideReferenceLinks(text, document)
|
||||
];
|
||||
}
|
||||
|
||||
private providerInlineLinks(
|
||||
text: string,
|
||||
document: vscode.TextDocument,
|
||||
): vscode.DocumentLink[] {
|
||||
const results: vscode.DocumentLink[] = [];
|
||||
for (const match of text.matchAll(this.linkPattern)) {
|
||||
const matchImage = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index);
|
||||
if (matchImage) {
|
||||
results.push(matchImage);
|
||||
}
|
||||
const matchLink = extractDocumentLink(document, match[1].length, match[5], match.index);
|
||||
if (matchLink) {
|
||||
results.push(matchLink);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private provideReferenceLinks(
|
||||
text: string,
|
||||
document: vscode.TextDocument,
|
||||
): vscode.DocumentLink[] {
|
||||
const results: vscode.DocumentLink[] = [];
|
||||
|
||||
const definitions = this.getDefinitions(text, document);
|
||||
for (const match of text.matchAll(this.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[2]) { // [ref][]
|
||||
reference = match[2];
|
||||
const offset = (match.index || 0) + 1;
|
||||
linkStart = document.positionAt(offset);
|
||||
linkEnd = document.positionAt(offset + match[2].length);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const link = definitions.get(reference);
|
||||
if (link) {
|
||||
results.push(new vscode.DocumentLink(
|
||||
new vscode.Range(linkStart, linkEnd),
|
||||
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([link.linkRange.start.line, link.linkRange.start.character]))}`)));
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
for (const definition of definitions.values()) {
|
||||
try {
|
||||
const linkData = parseLink(document, definition.link);
|
||||
if (linkData) {
|
||||
results.push(new vscode.DocumentLink(definition.linkRange, linkData.uri));
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private getDefinitions(text: string, document: vscode.TextDocument) {
|
||||
const out = new Map<string, { link: string, linkRange: vscode.Range }>();
|
||||
for (const match of text.matchAll(this.definitionPattern)) {
|
||||
const pre = match[1];
|
||||
const reference = match[2];
|
||||
const link = match[3].trim();
|
||||
|
||||
const offset = (match.index || 0) + pre.length;
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
|
||||
out.set(reference, {
|
||||
link: link,
|
||||
linkRange: new vscode.Range(linkStart, linkEnd)
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -1,177 +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 { SkinnyTextDocument, SkinnyTextLine } from '../tableOfContentsProvider';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { Lazy, lazy } from '../util/lazy';
|
||||
import MDDocumentSymbolProvider from './documentSymbolProvider';
|
||||
|
||||
export interface WorkspaceMarkdownDocumentProvider {
|
||||
getAllMarkdownDocuments(): Thenable<Iterable<SkinnyTextDocument>>;
|
||||
|
||||
readonly onDidChangeMarkdownDocument: vscode.Event<SkinnyTextDocument>;
|
||||
readonly onDidCreateMarkdownDocument: vscode.Event<SkinnyTextDocument>;
|
||||
readonly onDidDeleteMarkdownDocument: vscode.Event<vscode.Uri>;
|
||||
}
|
||||
|
||||
class VSCodeWorkspaceMarkdownDocumentProvider extends Disposable implements WorkspaceMarkdownDocumentProvider {
|
||||
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<SkinnyTextDocument>());
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<SkinnyTextDocument>());
|
||||
private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<vscode.Uri>());
|
||||
|
||||
private _watcher: vscode.FileSystemWatcher | undefined;
|
||||
|
||||
private readonly utf8Decoder = new TextDecoder('utf-8');
|
||||
|
||||
/**
|
||||
* Reads and parses all .md documents in the workspace.
|
||||
* Files are processed in batches, to keep the number of open files small.
|
||||
*
|
||||
* @returns Array of processed .md files.
|
||||
*/
|
||||
async getAllMarkdownDocuments(): Promise<SkinnyTextDocument[]> {
|
||||
const maxConcurrent = 20;
|
||||
const docList: SkinnyTextDocument[] = [];
|
||||
const resources = await vscode.workspace.findFiles('**/*.md', '**/node_modules/**');
|
||||
|
||||
for (let i = 0; i < resources.length; i += maxConcurrent) {
|
||||
const resourceBatch = resources.slice(i, i + maxConcurrent);
|
||||
const documentBatch = (await Promise.all(resourceBatch.map(x => this.getMarkdownDocument(x)))).filter((doc) => !!doc) as SkinnyTextDocument[];
|
||||
docList.push(...documentBatch);
|
||||
}
|
||||
return docList;
|
||||
}
|
||||
|
||||
public get onDidChangeMarkdownDocument() {
|
||||
this.ensureWatcher();
|
||||
return this._onDidChangeMarkdownDocumentEmitter.event;
|
||||
}
|
||||
|
||||
public get onDidCreateMarkdownDocument() {
|
||||
this.ensureWatcher();
|
||||
return this._onDidCreateMarkdownDocumentEmitter.event;
|
||||
}
|
||||
|
||||
public get onDidDeleteMarkdownDocument() {
|
||||
this.ensureWatcher();
|
||||
return this._onDidDeleteMarkdownDocumentEmitter.event;
|
||||
}
|
||||
|
||||
private ensureWatcher(): void {
|
||||
if (this._watcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md'));
|
||||
|
||||
this._watcher.onDidChange(async resource => {
|
||||
const document = await this.getMarkdownDocument(resource);
|
||||
if (document) {
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
}, null, this._disposables);
|
||||
|
||||
this._watcher.onDidCreate(async resource => {
|
||||
const document = await this.getMarkdownDocument(resource);
|
||||
if (document) {
|
||||
this._onDidCreateMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
}, null, this._disposables);
|
||||
|
||||
this._watcher.onDidDelete(async resource => {
|
||||
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
|
||||
}, null, this._disposables);
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument(e => {
|
||||
if (isMarkdownFile(e.document)) {
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(e.document);
|
||||
}
|
||||
}, null, this._disposables);
|
||||
}
|
||||
|
||||
private async getMarkdownDocument(resource: vscode.Uri): Promise<SkinnyTextDocument | undefined> {
|
||||
const matchingDocuments = vscode.workspace.textDocuments.filter((doc) => doc.uri.toString() === resource.toString());
|
||||
if (matchingDocuments.length !== 0) {
|
||||
return matchingDocuments[0];
|
||||
}
|
||||
|
||||
const bytes = await vscode.workspace.fs.readFile(resource);
|
||||
|
||||
// We assume that markdown is in UTF-8
|
||||
const text = this.utf8Decoder.decode(bytes);
|
||||
|
||||
const lines: SkinnyTextLine[] = [];
|
||||
const parts = text.split(/(\r?\n)/);
|
||||
const lineCount = Math.floor(parts.length / 2) + 1;
|
||||
for (let line = 0; line < lineCount; line++) {
|
||||
lines.push({
|
||||
text: parts[line * 2]
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
uri: resource,
|
||||
version: 0,
|
||||
lineCount: lineCount,
|
||||
lineAt: (index) => {
|
||||
return lines[index];
|
||||
},
|
||||
getText: () => {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default class MarkdownWorkspaceSymbolProvider extends Disposable implements vscode.WorkspaceSymbolProvider {
|
||||
private _symbolCache = new Map<string, Lazy<Thenable<vscode.SymbolInformation[]>>>();
|
||||
private _symbolCachePopulated: boolean = false;
|
||||
|
||||
public constructor(
|
||||
private _symbolProvider: MDDocumentSymbolProvider,
|
||||
private _workspaceMarkdownDocumentProvider: WorkspaceMarkdownDocumentProvider = new VSCodeWorkspaceMarkdownDocumentProvider()
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async provideWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
|
||||
if (!this._symbolCachePopulated) {
|
||||
await this.populateSymbolCache();
|
||||
this._symbolCachePopulated = true;
|
||||
|
||||
this._workspaceMarkdownDocumentProvider.onDidChangeMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
|
||||
this._workspaceMarkdownDocumentProvider.onDidCreateMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
|
||||
this._workspaceMarkdownDocumentProvider.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this, this._disposables);
|
||||
}
|
||||
|
||||
const allSymbolsSets = await Promise.all(Array.from(this._symbolCache.values(), x => x.value));
|
||||
const allSymbols = allSymbolsSets.flat();
|
||||
return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1);
|
||||
}
|
||||
|
||||
public async populateSymbolCache(): Promise<void> {
|
||||
const markdownDocumentUris = await this._workspaceMarkdownDocumentProvider.getAllMarkdownDocuments();
|
||||
for (const document of markdownDocumentUris) {
|
||||
this._symbolCache.set(document.uri.fsPath, this.getSymbols(document));
|
||||
}
|
||||
}
|
||||
|
||||
private getSymbols(document: SkinnyTextDocument): Lazy<Thenable<vscode.SymbolInformation[]>> {
|
||||
return lazy(async () => {
|
||||
return this._symbolProvider.provideDocumentSymbolInformation(document);
|
||||
});
|
||||
}
|
||||
|
||||
private onDidChangeDocument(document: SkinnyTextDocument) {
|
||||
this._symbolCache.set(document.uri.fsPath, this.getSymbols(document));
|
||||
}
|
||||
|
||||
private onDidDeleteDocument(resource: vscode.Uri) {
|
||||
this._symbolCache.delete(resource.fsPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { 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';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export interface DiagnosticConfiguration {
|
||||
/**
|
||||
* Fired when the configuration changes.
|
||||
*/
|
||||
readonly onDidChange: vscode.Event<void>;
|
||||
|
||||
getOptions(resource: vscode.Uri): DiagnosticOptions;
|
||||
}
|
||||
|
||||
export enum DiagnosticLevel {
|
||||
ignore = 'ignore',
|
||||
warning = 'warning',
|
||||
error = 'error',
|
||||
}
|
||||
|
||||
export interface DiagnosticOptions {
|
||||
readonly enabled: boolean;
|
||||
readonly validateReferences: DiagnosticLevel;
|
||||
readonly validateOwnHeaders: DiagnosticLevel;
|
||||
readonly validateFilePaths: DiagnosticLevel;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConfiguration {
|
||||
|
||||
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 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class DiagnosticManager extends Disposable {
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
}
|
||||
|
||||
export function register(
|
||||
engine: MarkdownEngine,
|
||||
workspaceContents: MdWorkspaceContents,
|
||||
linkProvider: MdLinkProvider,
|
||||
): vscode.Disposable {
|
||||
const configuration = new VSCodeDiagnosticConfiguration();
|
||||
const manager = new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkProvider), configuration);
|
||||
return vscode.Disposable.from(configuration, manager);
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { SkinnyTextDocument, TableOfContentsProvider, TocEntry } from '../tableOfContentsProvider';
|
||||
import { TableOfContents, TocEntry } from '../tableOfContents';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
interface MarkdownSymbol {
|
||||
readonly level: number;
|
||||
@@ -13,29 +14,29 @@ interface MarkdownSymbol {
|
||||
readonly children: vscode.DocumentSymbol[];
|
||||
}
|
||||
|
||||
export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||
export class MdDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideDocumentSymbolInformation(document: SkinnyTextDocument): Promise<vscode.SymbolInformation[]> {
|
||||
const toc = await new TableOfContentsProvider(this.engine, document).getToc();
|
||||
return toc.map(entry => this.toSymbolInformation(entry));
|
||||
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 new TableOfContentsProvider(this.engine, document).getToc();
|
||||
const toc = await TableOfContents.create(this.engine, document);
|
||||
const root: MarkdownSymbol = {
|
||||
level: -Infinity,
|
||||
children: [],
|
||||
parent: undefined
|
||||
};
|
||||
this.buildTree(root, toc);
|
||||
this.buildTree(root, toc.entries);
|
||||
return root.children;
|
||||
}
|
||||
|
||||
private buildTree(parent: MarkdownSymbol, entries: TocEntry[]) {
|
||||
private buildTree(parent: MarkdownSymbol, entries: readonly TocEntry[]) {
|
||||
if (!entries.length) {
|
||||
return;
|
||||
}
|
||||
@@ -57,7 +58,7 @@ export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolPr
|
||||
this.getSymbolName(entry),
|
||||
vscode.SymbolKind.String,
|
||||
'',
|
||||
entry.location);
|
||||
entry.sectionLocation);
|
||||
}
|
||||
|
||||
private toDocumentSymbol(entry: TocEntry) {
|
||||
@@ -65,8 +66,8 @@ export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolPr
|
||||
this.getSymbolName(entry),
|
||||
'',
|
||||
vscode.SymbolKind.String,
|
||||
entry.location.range,
|
||||
entry.location.range);
|
||||
entry.sectionLocation.range,
|
||||
entry.sectionLocation.range);
|
||||
}
|
||||
|
||||
private getSymbolName(entry: TocEntry): string {
|
||||
@@ -0,0 +1,71 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 URI from 'vscode-uri';
|
||||
|
||||
const imageFileExtensions = new Set<string>([
|
||||
'.bmp',
|
||||
'.gif',
|
||||
'.ico',
|
||||
'.jpe',
|
||||
'.jpeg',
|
||||
'.jpg',
|
||||
'.png',
|
||||
'.psd',
|
||||
'.svg',
|
||||
'.tga',
|
||||
'.tif',
|
||||
'.tiff',
|
||||
'.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> {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Command, CommandManager } from '../commandManager';
|
||||
import { MdReferencesProvider } from './references';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
|
||||
export class FindFileReferencesCommand implements Command {
|
||||
|
||||
public readonly id = 'markdown.findAllFileReferences';
|
||||
|
||||
constructor(
|
||||
private readonly referencesProvider: MdReferencesProvider,
|
||||
) { }
|
||||
|
||||
public async execute(resource?: vscode.Uri) {
|
||||
if (!resource) {
|
||||
resource = vscode.window.activeTextEditor?.document.uri;
|
||||
}
|
||||
|
||||
if (!resource) {
|
||||
vscode.window.showErrorMessage(localize('error.noResource', "Find file references failed. No resource provided."));
|
||||
return;
|
||||
}
|
||||
|
||||
await vscode.window.withProgress({
|
||||
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 config = vscode.workspace.getConfiguration('references');
|
||||
const existingSetting = config.inspect<string>('preferredLocation');
|
||||
|
||||
await config.update('preferredLocation', 'view');
|
||||
try {
|
||||
await vscode.commands.executeCommand('editor.action.showReferences', resource, new vscode.Position(0, 0), locations);
|
||||
} finally {
|
||||
await config.update('preferredLocation', existingSetting?.workspaceFolderValue ?? existingSetting?.workspaceValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function registerFindFileReferences(commandManager: CommandManager, referencesProvider: MdReferencesProvider): vscode.Disposable {
|
||||
return commandManager.register(new FindFileReferencesCommand(referencesProvider));
|
||||
}
|
||||
@@ -6,7 +6,8 @@
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContentsProvider } from '../tableOfContentsProvider';
|
||||
import { TableOfContents } from '../tableOfContents';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
const rangeLimit = 5000;
|
||||
|
||||
@@ -14,14 +15,14 @@ interface MarkdownItTokenWithMap extends Token {
|
||||
map: [number, number];
|
||||
}
|
||||
|
||||
export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvider {
|
||||
export class MdFoldingProvider implements vscode.FoldingRangeProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideFoldingRanges(
|
||||
document: vscode.TextDocument,
|
||||
document: SkinnyTextDocument,
|
||||
_: vscode.FoldingContext,
|
||||
_token: vscode.CancellationToken
|
||||
): Promise<vscode.FoldingRange[]> {
|
||||
@@ -33,12 +34,12 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi
|
||||
return foldables.flat().slice(0, rangeLimit);
|
||||
}
|
||||
|
||||
private async getRegions(document: vscode.TextDocument): Promise<vscode.FoldingRange[]> {
|
||||
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 }[] = [];
|
||||
const nestingStack: { line: number; isStart: boolean }[] = [];
|
||||
return regionMarkers
|
||||
.map(marker => {
|
||||
if (marker.isStart) {
|
||||
@@ -53,11 +54,10 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi
|
||||
.filter((region: vscode.FoldingRange | null): region is vscode.FoldingRange => !!region);
|
||||
}
|
||||
|
||||
private async getHeaderFoldingRanges(document: vscode.TextDocument) {
|
||||
const tocProvider = new TableOfContentsProvider(this.engine, document);
|
||||
const toc = await tocProvider.getToc();
|
||||
return toc.map(entry => {
|
||||
let endLine = entry.location.range.end.line;
|
||||
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;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi
|
||||
});
|
||||
}
|
||||
|
||||
private async getBlockFoldingRanges(document: vscode.TextDocument): Promise<vscode.FoldingRange[]> {
|
||||
private async getBlockFoldingRanges(document: SkinnyTextDocument): Promise<vscode.FoldingRange[]> {
|
||||
const tokens = await this.engine.parse(document);
|
||||
const multiLineListItems = tokens.filter(isFoldableToken);
|
||||
return multiLineListItems.map(listItem => {
|
||||
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,37 @@
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContentsProvider, TocEntry } from '../tableOfContentsProvider';
|
||||
import { TableOfContents, TocEntry } from '../tableOfContents';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
interface MarkdownItTokenWithMap extends Token {
|
||||
map: [number, number];
|
||||
}
|
||||
|
||||
export default class MarkdownSmartSelect implements vscode.SelectionRangeProvider {
|
||||
export class MdSmartSelect implements vscode.SelectionRangeProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideSelectionRanges(document: vscode.TextDocument, positions: vscode.Position[], _token: vscode.CancellationToken): Promise<vscode.SelectionRange[] | undefined> {
|
||||
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: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.SelectionRange | undefined> {
|
||||
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: vscode.TextDocument, position: vscode.Position, blockRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
|
||||
private async getInlineSelectionRange(document: SkinnyTextDocument, position: vscode.Position, blockRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
|
||||
return createInlineRange(document, position, blockRange);
|
||||
}
|
||||
|
||||
private async getBlockSelectionRange(document: vscode.TextDocument, position: vscode.Position, headerRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
|
||||
private async getBlockSelectionRange(document: SkinnyTextDocument, position: vscode.Position, headerRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
|
||||
|
||||
const tokens = await this.engine.parse(document);
|
||||
|
||||
@@ -52,26 +53,24 @@ export default class MarkdownSmartSelect implements vscode.SelectionRangeProvide
|
||||
return currentRange;
|
||||
}
|
||||
|
||||
private async getHeaderSelectionRange(document: vscode.TextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
|
||||
private async getHeaderSelectionRange(document: SkinnyTextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
|
||||
const toc = await TableOfContents.create(this.engine, document);
|
||||
|
||||
const tocProvider = new TableOfContentsProvider(this.engine, document);
|
||||
const toc = await tocProvider.getToc();
|
||||
|
||||
const headerInfo = getHeadersForPosition(toc, position);
|
||||
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));
|
||||
currentRange = createHeaderRange(headers[i], i === headers.length - 1, headerInfo.headerOnThisLine, currentRange, getFirstChildHeader(document, headers[i], toc.entries));
|
||||
}
|
||||
return currentRange;
|
||||
}
|
||||
}
|
||||
|
||||
function getHeadersForPosition(toc: TocEntry[], position: vscode.Position): { headers: TocEntry[], headerOnThisLine: boolean } {
|
||||
const enclosingHeaders = toc.filter(header => header.location.range.start.line <= position.line && header.location.range.end.line >= position.line);
|
||||
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 {
|
||||
@@ -81,7 +80,7 @@ function getHeadersForPosition(toc: TocEntry[], position: vscode.Position): { he
|
||||
}
|
||||
|
||||
function createHeaderRange(header: TocEntry, isClosestHeaderToPosition: boolean, onHeaderLine: boolean, parent?: vscode.SelectionRange, startOfChildRange?: vscode.Position): vscode.SelectionRange | undefined {
|
||||
const range = header.location.range;
|
||||
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
|
||||
@@ -109,7 +108,7 @@ function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, p
|
||||
return sortedTokens;
|
||||
}
|
||||
|
||||
function createBlockRange(block: MarkdownItTokenWithMap, document: vscode.TextDocument, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
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 {
|
||||
@@ -131,7 +130,7 @@ function createBlockRange(block: MarkdownItTokenWithMap, document: vscode.TextDo
|
||||
}
|
||||
}
|
||||
|
||||
function createInlineRange(document: vscode.TextDocument, cursorPosition: vscode.Position, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
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);
|
||||
@@ -148,7 +147,7 @@ function createInlineRange(document: vscode.TextDocument, cursorPosition: vscode
|
||||
return inlineCodeBlockSelection || linkSelection || comboSelection || boldSelection || italicSelection;
|
||||
}
|
||||
|
||||
function createFencedRange(token: MarkdownItTokenWithMap, cursorLine: number, document: vscode.TextDocument, parent?: vscode.SelectionRange): vscode.SelectionRange {
|
||||
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;
|
||||
@@ -166,7 +165,7 @@ function createFencedRange(token: MarkdownItTokenWithMap, cursorLine: number, do
|
||||
}
|
||||
|
||||
function createBoldRange(lineText: string, cursorChar: number, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
const regex = /(?:\*\*([^*]+)(?:\*([^*]+)([^*]+)\*)*([^*]+)\*\*)/g;
|
||||
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
|
||||
@@ -238,12 +237,12 @@ 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: vscode.TextDocument, header?: TocEntry, toc?: TocEntry[]): vscode.Position | undefined {
|
||||
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.location.range.contains(t.location.range) && t.location.range.start.line > header.location.range.start.line).sort((t1, t2) => t1.line - t2.line);
|
||||
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].location.range.start;
|
||||
childRange = children[0].sectionLocation.range.start;
|
||||
const lineText = document.lineAt(childRange.line - 1).text;
|
||||
return childRange ? childRange.translate(-1, lineText.length) : undefined;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
@@ -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 { 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);
|
||||
}
|
||||
}
|
||||
@@ -51,9 +51,9 @@ export class Logger {
|
||||
|
||||
private now(): string {
|
||||
const now = new Date();
|
||||
return padLeft(now.getUTCHours() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
|
||||
return String(now.getUTCHours()).padStart(2, '0')
|
||||
+ ':' + String(now.getMinutes()).padStart(2, '0')
|
||||
+ ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + now.getMilliseconds();
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
@@ -81,7 +81,3 @@ export class Logger {
|
||||
return JSON.stringify(data, undefined, 2);
|
||||
}
|
||||
}
|
||||
|
||||
function padLeft(s: string, n: number, pad = ' ') {
|
||||
return pad.repeat(Math.max(0, n - s.length)) + s;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
import MarkdownIt = require('markdown-it');
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownContributionProvider as MarkdownContributionProvider } from './markdownExtensions';
|
||||
import { MarkdownContributionProvider } from './markdownExtensions';
|
||||
import { Slugifier } from './slugify';
|
||||
import { SkinnyTextDocument } from './tableOfContentsProvider';
|
||||
import { hash } from './util/hash';
|
||||
import { isOfScheme, Schemes } from './util/links';
|
||||
import { stringHash } from './util/hash';
|
||||
import { WebviewResourceProvider } from './util/resources';
|
||||
import { isOfScheme, Schemes } from './util/schemes';
|
||||
import { SkinnyTextDocument } from './workspaceContents';
|
||||
|
||||
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
|
||||
|
||||
@@ -25,6 +25,7 @@ const pluginSourceMap: MarkdownIt.PluginSimple = (md): void => {
|
||||
if (token.map && token.type !== 'inline') {
|
||||
token.attrSet('data-line', String(token.map[0]));
|
||||
token.attrJoin('class', 'code-line');
|
||||
token.attrJoin('dir', 'auto');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -112,6 +113,7 @@ export class MarkdownEngine {
|
||||
this.md = (async () => {
|
||||
const markdownIt = await import('markdown-it');
|
||||
let md: MarkdownIt = markdownIt(await getMarkdownOptions(() => md));
|
||||
md.linkify.set({ fuzzyLink: false });
|
||||
|
||||
for (const plugin of this.contributionProvider.contributions.markdownItPlugins.values()) {
|
||||
try {
|
||||
@@ -163,6 +165,7 @@ export class MarkdownEngine {
|
||||
): Token[] {
|
||||
const cached = this._tokenCache.tryGetCached(document, config);
|
||||
if (cached) {
|
||||
this.resetSlugCount();
|
||||
return cached;
|
||||
}
|
||||
|
||||
@@ -172,11 +175,15 @@ export class MarkdownEngine {
|
||||
}
|
||||
|
||||
private tokenizeString(text: string, engine: MarkdownIt) {
|
||||
this._slugCount = new Map<string, number>();
|
||||
this.resetSlugCount();
|
||||
|
||||
return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {});
|
||||
}
|
||||
|
||||
private resetSlugCount(): void {
|
||||
this._slugCount = new Map<string, number>();
|
||||
}
|
||||
|
||||
public async render(input: SkinnyTextDocument | string, resourceProvider?: WebviewResourceProvider): Promise<RenderOutput> {
|
||||
const config = this.getConfig(typeof input === 'string' ? undefined : input.uri);
|
||||
const engine = await this.getEngine(config);
|
||||
@@ -230,7 +237,7 @@ export class MarkdownEngine {
|
||||
const src = token.attrGet('src');
|
||||
if (src) {
|
||||
env.containingImages?.push({ src });
|
||||
const imgHash = hash(src);
|
||||
const imgHash = stringHash(src);
|
||||
token.attrSet('id', `image-hash-${imgHash}`);
|
||||
|
||||
if (!token.attrGet('data-src')) {
|
||||
@@ -377,14 +384,18 @@ export class MarkdownEngine {
|
||||
}
|
||||
|
||||
async function getMarkdownOptions(md: () => MarkdownIt): Promise<MarkdownIt.Options> {
|
||||
const hljs = await import('highlight.js');
|
||||
const hljs = (await import('highlight.js')).default;
|
||||
return {
|
||||
html: true,
|
||||
highlight: (str: string, lang?: string) => {
|
||||
lang = normalizeHighlightLang(lang);
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return `<div>${hljs.highlight(lang, str, true).value}</div>`;
|
||||
const highlighted = hljs.highlight(str, {
|
||||
language: lang,
|
||||
ignoreIllegals: true,
|
||||
}).value;
|
||||
return `<div>${highlighted}</div>`;
|
||||
}
|
||||
catch (error) { }
|
||||
}
|
||||
|
||||
@@ -7,29 +7,27 @@ import * as vscode from 'vscode';
|
||||
import * as arrays from './util/arrays';
|
||||
import { Disposable } from './util/dispose';
|
||||
|
||||
const resolveExtensionResource = (extension: vscode.Extension<any>, resourcePath: string): vscode.Uri => {
|
||||
function resolveExtensionResource(extension: vscode.Extension<any>, resourcePath: string): vscode.Uri {
|
||||
return vscode.Uri.joinPath(extension.extensionUri, resourcePath);
|
||||
};
|
||||
}
|
||||
|
||||
const resolveExtensionResources = (extension: vscode.Extension<any>, resourcePaths: unknown): vscode.Uri[] => {
|
||||
const result: vscode.Uri[] = [];
|
||||
function* resolveExtensionResources(extension: vscode.Extension<any>, resourcePaths: unknown): Iterable<vscode.Uri> {
|
||||
if (Array.isArray(resourcePaths)) {
|
||||
for (const resource of resourcePaths) {
|
||||
try {
|
||||
result.push(resolveExtensionResource(extension, resource));
|
||||
} catch (e) {
|
||||
yield resolveExtensionResource(extension, resource);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MarkdownContributions {
|
||||
readonly previewScripts: ReadonlyArray<vscode.Uri>;
|
||||
readonly previewStyles: ReadonlyArray<vscode.Uri>;
|
||||
readonly previewResourceRoots: ReadonlyArray<vscode.Uri>;
|
||||
readonly markdownItPlugins: Map<string, Thenable<(md: any) => any>>;
|
||||
readonly previewScripts: readonly vscode.Uri[];
|
||||
readonly previewStyles: readonly vscode.Uri[];
|
||||
readonly previewResourceRoots: readonly vscode.Uri[];
|
||||
readonly markdownItPlugins: ReadonlyMap<string, Thenable<(md: any) => any>>;
|
||||
}
|
||||
|
||||
export namespace MarkdownContributions {
|
||||
@@ -60,16 +58,14 @@ export namespace MarkdownContributions {
|
||||
&& arrays.equals(Array.from(a.markdownItPlugins.keys()), Array.from(b.markdownItPlugins.keys()));
|
||||
}
|
||||
|
||||
export function fromExtension(
|
||||
extension: vscode.Extension<any>
|
||||
): MarkdownContributions {
|
||||
const contributions = extension.packageJSON && extension.packageJSON.contributes;
|
||||
export function fromExtension(extension: vscode.Extension<any>): MarkdownContributions {
|
||||
const contributions = extension.packageJSON?.contributes;
|
||||
if (!contributions) {
|
||||
return MarkdownContributions.Empty;
|
||||
}
|
||||
|
||||
const previewStyles = getContributedStyles(contributions, extension);
|
||||
const previewScripts = getContributedScripts(contributions, extension);
|
||||
const previewStyles = Array.from(getContributedStyles(contributions, extension));
|
||||
const previewScripts = Array.from(getContributedScripts(contributions, extension));
|
||||
const previewResourceRoots = previewStyles.length || previewScripts.length ? [extension.extensionUri] : [];
|
||||
const markdownItPlugins = getContributedMarkdownItPlugins(contributions, extension);
|
||||
|
||||
@@ -122,6 +118,7 @@ export interface MarkdownContributionProvider {
|
||||
}
|
||||
|
||||
class VSCodeExtensionMarkdownContributionProvider extends Disposable implements MarkdownContributionProvider {
|
||||
|
||||
private _contributions?: MarkdownContributions;
|
||||
|
||||
public constructor(
|
||||
@@ -129,17 +126,19 @@ class VSCodeExtensionMarkdownContributionProvider extends Disposable implements
|
||||
) {
|
||||
super();
|
||||
|
||||
vscode.extensions.onDidChange(() => {
|
||||
this._register(vscode.extensions.onDidChange(() => {
|
||||
const currentContributions = this.getCurrentContributions();
|
||||
const existingContributions = this._contributions || MarkdownContributions.Empty;
|
||||
if (!MarkdownContributions.equal(existingContributions, currentContributions)) {
|
||||
this._contributions = currentContributions;
|
||||
this._onContributionsChanged.fire(this);
|
||||
}
|
||||
}, undefined, this._disposables);
|
||||
}));
|
||||
}
|
||||
|
||||
public get extensionUri() { return this._extensionContext.extensionUri; }
|
||||
public get extensionUri() {
|
||||
return this._extensionContext.extensionUri;
|
||||
}
|
||||
|
||||
private readonly _onContributionsChanged = this._register(new vscode.EventEmitter<this>());
|
||||
public readonly onContributionsChanged = this._onContributionsChanged.event;
|
||||
|
||||
@@ -5,18 +5,19 @@
|
||||
|
||||
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 { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { openDocumentLink, resolveDocumentLink, resolveLinkToMarkdownFile } from '../util/openDocumentLink';
|
||||
import * as path from '../util/path';
|
||||
import { openDocumentLink, resolveDocumentLink, resolveUriToMarkdownFile } from '../util/openDocumentLink';
|
||||
import { WebviewResourceProvider } from '../util/resources';
|
||||
import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor';
|
||||
import { urlToUri } from '../util/url';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
import { scrollEditorToLine, StartingScrollFragment, StartingScrollLine, StartingScrollLocation } from './scrolling';
|
||||
import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from './topmostLineMonitor';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -26,7 +27,7 @@ interface WebviewMessage {
|
||||
|
||||
interface CacheImageSizesMessage extends WebviewMessage {
|
||||
readonly type: 'cacheImageSizes';
|
||||
readonly body: { id: string, width: number, height: number; }[];
|
||||
readonly body: { id: string; width: number; height: number }[];
|
||||
}
|
||||
|
||||
interface RevealLineMessage extends WebviewMessage {
|
||||
@@ -63,7 +64,7 @@ interface PreviewStyleLoadErrorMessage extends WebviewMessage {
|
||||
|
||||
export class PreviewDocumentVersion {
|
||||
|
||||
private readonly resource: vscode.Uri;
|
||||
public readonly resource: vscode.Uri;
|
||||
private readonly version: number;
|
||||
|
||||
public constructor(document: vscode.TextDocument) {
|
||||
@@ -79,47 +80,32 @@ export class PreviewDocumentVersion {
|
||||
|
||||
interface MarkdownPreviewDelegate {
|
||||
getTitle?(resource: vscode.Uri): string;
|
||||
getAdditionalState(): {},
|
||||
getAdditionalState(): {};
|
||||
openPreviewLinkToMarkdownFile(markdownLink: vscode.Uri, fragment: string): void;
|
||||
}
|
||||
|
||||
class StartingScrollLine {
|
||||
public readonly type = 'line';
|
||||
|
||||
constructor(
|
||||
public readonly line: number,
|
||||
) { }
|
||||
}
|
||||
|
||||
export class StartingScrollFragment {
|
||||
public readonly type = 'fragment';
|
||||
|
||||
constructor(
|
||||
public readonly fragment: string,
|
||||
) { }
|
||||
}
|
||||
|
||||
type StartingScrollLocation = StartingScrollLine | StartingScrollFragment;
|
||||
|
||||
class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
|
||||
private static readonly unwatchedImageSchemes = new Set(['https', 'http', 'data']);
|
||||
|
||||
private _disposed: boolean = false;
|
||||
|
||||
private readonly delay = 300;
|
||||
private throttleTimer: any;
|
||||
|
||||
private readonly _resource: vscode.Uri;
|
||||
private readonly _webviewPanel: vscode.WebviewPanel;
|
||||
|
||||
private throttleTimer: any;
|
||||
|
||||
private line: number | undefined;
|
||||
private scrollToFragment: string | undefined;
|
||||
|
||||
private firstUpdate = true;
|
||||
private currentVersion?: PreviewDocumentVersion;
|
||||
private isScrolling = false;
|
||||
private _disposed: boolean = false;
|
||||
private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = [];
|
||||
|
||||
private imageInfo: { readonly id: string; readonly width: number; readonly height: number }[] = [];
|
||||
private readonly _fileWatchersBySrc = new Map</* src: */ string, vscode.FileSystemWatcher>();
|
||||
|
||||
private readonly _onScrollEmitter = this._register(new vscode.EventEmitter<LastScrollLocation>());
|
||||
public readonly onScroll = this._onScrollEmitter.event;
|
||||
|
||||
@@ -161,7 +147,13 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
}
|
||||
}));
|
||||
|
||||
const watcher = this._register(vscode.workspace.createFileSystemWatcher(resource.fsPath));
|
||||
this._register(vscode.workspace.onDidOpenTextDocument(document => {
|
||||
if (this.isPreviewOf(document.uri)) {
|
||||
this.refresh();
|
||||
}
|
||||
}));
|
||||
|
||||
const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(resource, '*')));
|
||||
this._register(watcher.onDidChange(uri => {
|
||||
if (this.isPreviewOf(uri)) {
|
||||
// Only use the file system event when VS Code does not already know about the file
|
||||
@@ -211,11 +203,14 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
|
||||
override dispose() {
|
||||
super.dispose();
|
||||
|
||||
this._disposed = true;
|
||||
|
||||
clearTimeout(this.throttleTimer);
|
||||
for (const entry of this._fileWatchersBySrc.values()) {
|
||||
entry.dispose();
|
||||
}
|
||||
this._fileWatchersBySrc.clear();
|
||||
}
|
||||
|
||||
public get resource(): vscode.Uri {
|
||||
@@ -236,26 +231,19 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
* The first call immediately refreshes the preview,
|
||||
* calls happening shortly thereafter are debounced.
|
||||
*/
|
||||
public refresh() {
|
||||
public refresh(forceUpdate: boolean = false) {
|
||||
// Schedule update if none is pending
|
||||
if (!this.throttleTimer) {
|
||||
if (this.firstUpdate) {
|
||||
this.updatePreview(true);
|
||||
} else {
|
||||
this.throttleTimer = setTimeout(() => this.updatePreview(true), this.delay);
|
||||
this.throttleTimer = setTimeout(() => this.updatePreview(forceUpdate), this.delay);
|
||||
}
|
||||
}
|
||||
|
||||
this.firstUpdate = false;
|
||||
}
|
||||
|
||||
private get iconPath() {
|
||||
const root = vscode.Uri.joinPath(this._contributionProvider.extensionUri, 'media');
|
||||
return {
|
||||
light: vscode.Uri.joinPath(root, 'preview-light.svg'),
|
||||
dark: vscode.Uri.joinPath(root, 'preview-dark.svg'),
|
||||
};
|
||||
}
|
||||
|
||||
public isPreviewOf(resource: vscode.Uri): boolean {
|
||||
return this._resource.fsPath === resource.fsPath;
|
||||
@@ -314,13 +302,18 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldReloadPage = forceUpdate || !this.currentVersion || this.currentVersion.resource.toString() !== pendingVersion.resource.toString() || !this._webviewPanel.visible;
|
||||
this.currentVersion = pendingVersion;
|
||||
const content = await this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state);
|
||||
|
||||
const content = await (shouldReloadPage
|
||||
? this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state)
|
||||
: this._contentProvider.markdownBody(document, this));
|
||||
|
||||
// Another call to `doUpdate` may have happened.
|
||||
// Make sure we are still updating for the correct document
|
||||
if (this.currentVersion?.equals(pendingVersion)) {
|
||||
this.setContent(content);
|
||||
this.updateWebviewContent(content.html, shouldReloadPage);
|
||||
this.updateImageWatchers(content.containingImages);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,18 +339,26 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
// fix #82457, find currently opened but unfocused source tab
|
||||
await vscode.commands.executeCommand('markdown.showSource');
|
||||
|
||||
const revealLineInEditor = (editor: vscode.TextEditor) => {
|
||||
const position = new vscode.Position(line, 0);
|
||||
const newSelection = new vscode.Selection(position, position);
|
||||
editor.selection = newSelection;
|
||||
editor.revealRange(newSelection, vscode.TextEditorRevealType.InCenterIfOutsideViewport);
|
||||
};
|
||||
|
||||
for (const visibleEditor of vscode.window.visibleTextEditors) {
|
||||
if (this.isPreviewOf(visibleEditor.document.uri)) {
|
||||
const editor = await vscode.window.showTextDocument(visibleEditor.document, visibleEditor.viewColumn);
|
||||
const position = new vscode.Position(line, 0);
|
||||
editor.selection = new vscode.Selection(position, position);
|
||||
revealLineInEditor(editor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await vscode.workspace.openTextDocument(this._resource)
|
||||
.then(vscode.window.showTextDocument)
|
||||
.then(undefined, () => {
|
||||
.then((editor) => {
|
||||
revealLineInEditor(editor);
|
||||
}, () => {
|
||||
vscode.window.showErrorMessage(localize('preview.clickOpenFailed', 'Could not open {0}', this._resource.toString()));
|
||||
});
|
||||
}
|
||||
@@ -366,7 +367,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
this._webviewPanel.webview.html = this._contentProvider.provideFileNotFoundContent(this._resource);
|
||||
}
|
||||
|
||||
private setContent(content: MarkdownContentProviderOutput): void {
|
||||
private updateWebviewContent(html: string, reloadPage: boolean): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
@@ -374,15 +375,24 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
if (this.delegate.getTitle) {
|
||||
this._webviewPanel.title = this.delegate.getTitle(this._resource);
|
||||
}
|
||||
this._webviewPanel.iconPath = this.iconPath;
|
||||
this._webviewPanel.webview.options = this.getWebviewOptions();
|
||||
|
||||
this._webviewPanel.webview.html = content.html;
|
||||
if (reloadPage) {
|
||||
this._webviewPanel.webview.html = html;
|
||||
} else {
|
||||
this._webviewPanel.webview.postMessage({
|
||||
type: 'updateContent',
|
||||
content: html,
|
||||
source: this._resource.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const srcs = new Set(content.containingImages.map(img => img.src));
|
||||
private updateImageWatchers(containingImages: { src: string }[]) {
|
||||
const srcs = new Set(containingImages.map(img => img.src));
|
||||
|
||||
// Delete stale file watchers.
|
||||
for (const [src, watcher] of [...this._fileWatchersBySrc]) {
|
||||
for (const [src, watcher] of this._fileWatchersBySrc) {
|
||||
if (!srcs.has(src)) {
|
||||
watcher.dispose();
|
||||
this._fileWatchersBySrc.delete(src);
|
||||
@@ -393,10 +403,10 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
const root = vscode.Uri.joinPath(this._resource, '../');
|
||||
for (const src of srcs) {
|
||||
const uri = urlToUri(src, root);
|
||||
if (uri && uri.scheme === 'file' && !this._fileWatchersBySrc.has(src)) {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(uri.fsPath);
|
||||
if (uri && !MarkdownPreview.unwatchedImageSchemes.has(uri.scheme) && !this._fileWatchersBySrc.has(src)) {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, '*'));
|
||||
watcher.onDidChange(() => {
|
||||
this.refresh();
|
||||
this.refresh(true);
|
||||
});
|
||||
this._fileWatchersBySrc.set(src, watcher);
|
||||
}
|
||||
@@ -420,23 +430,22 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
if (workspaceRoots) {
|
||||
baseRoots.push(...workspaceRoots);
|
||||
}
|
||||
} else if (!this._resource.scheme || this._resource.scheme === 'file') {
|
||||
baseRoots.push(vscode.Uri.file(path.dirname(this._resource.fsPath)));
|
||||
} else {
|
||||
baseRoots.push(uri.Utils.dirname(this._resource));
|
||||
}
|
||||
|
||||
return baseRoots;
|
||||
}
|
||||
|
||||
|
||||
private async onDidClickPreviewLink(href: string) {
|
||||
const targetResource = resolveDocumentLink(href, this.resource);
|
||||
|
||||
const config = vscode.workspace.getConfiguration('markdown', this.resource);
|
||||
const openLinks = config.get<string>('preview.openMarkdownLinks', 'inPreview');
|
||||
if (openLinks === 'inPreview') {
|
||||
const markdownLink = await resolveLinkToMarkdownFile(targetResource);
|
||||
if (markdownLink) {
|
||||
this.delegate.openPreviewLinkToMarkdownFile(markdownLink, targetResource.fragment);
|
||||
const linkedDoc = await resolveUriToMarkdownFile(targetResource);
|
||||
if (linkedDoc) {
|
||||
this.delegate.openPreviewLinkToMarkdownFile(linkedDoc.uri, targetResource.fragment);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -479,6 +488,8 @@ export interface ManagedMarkdownPreview {
|
||||
|
||||
export class StaticMarkdownPreview extends Disposable implements ManagedMarkdownPreview {
|
||||
|
||||
public static readonly customEditorViewType = 'vscode.markdown.preview.editor';
|
||||
|
||||
public static revive(
|
||||
resource: vscode.Uri,
|
||||
webview: vscode.WebviewPanel,
|
||||
@@ -510,7 +521,11 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
|
||||
const topScrollLocation = scrollLine ? new StartingScrollLine(scrollLine) : undefined;
|
||||
this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, topScrollLocation, {
|
||||
getAdditionalState: () => { return {}; },
|
||||
openPreviewLinkToMarkdownFile: () => { /* todo */ }
|
||||
openPreviewLinkToMarkdownFile: (markdownLink, fragment) => {
|
||||
return vscode.commands.executeCommand('vscode.openWith', markdownLink.with({
|
||||
fragment
|
||||
}), StaticMarkdownPreview.customEditorViewType, this._webviewPanel.viewColumn);
|
||||
}
|
||||
}, engine, contentProvider, _previewConfigurations, logger, contributionProvider));
|
||||
|
||||
this._register(this._webviewPanel.onDidDispose(() => {
|
||||
@@ -552,7 +567,7 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
this.preview.refresh();
|
||||
this.preview.refresh(true);
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
@@ -577,9 +592,6 @@ interface DynamicPreviewInput {
|
||||
readonly line?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A
|
||||
*/
|
||||
export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdownPreview {
|
||||
|
||||
public static readonly viewType = 'markdown.preview';
|
||||
@@ -600,6 +612,8 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
contributionProvider: MarkdownContributionProvider,
|
||||
engine: MarkdownEngine,
|
||||
): DynamicMarkdownPreview {
|
||||
webview.iconPath = contentProvider.iconPath;
|
||||
|
||||
return new DynamicMarkdownPreview(webview, input,
|
||||
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
|
||||
}
|
||||
@@ -619,6 +633,8 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
DynamicMarkdownPreview.getPreviewTitle(input.resource, input.locked),
|
||||
previewColumn, { enableFindWidget: true, });
|
||||
|
||||
webview.iconPath = contentProvider.iconPath;
|
||||
|
||||
return new DynamicMarkdownPreview(webview, input,
|
||||
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
|
||||
}
|
||||
@@ -705,7 +721,7 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
this._preview.refresh();
|
||||
this._preview.refresh(true);
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
@@ -740,9 +756,10 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
}
|
||||
|
||||
private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string {
|
||||
const resourceLabel = uri.Utils.basename(resource);
|
||||
return locked
|
||||
? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath))
|
||||
: localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath));
|
||||
? localize('lockedPreviewTitle', '[Preview] {0}', resourceLabel)
|
||||
: localize('previewTitle', 'Preview {0}', resourceLabel);
|
||||
}
|
||||
|
||||
public get position(): vscode.ViewColumn | undefined {
|
||||
@@ -789,19 +806,3 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
this._contributionProvider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the top-most visible line of `editor` to be at `line`
|
||||
*/
|
||||
export function scrollEditorToLine(
|
||||
line: number,
|
||||
editor: vscode.TextEditor
|
||||
) {
|
||||
const sourceLine = Math.floor(line);
|
||||
const fraction = line - sourceLine;
|
||||
const text = editor.document.lineAt(sourceLine).text;
|
||||
const start = Math.floor(fraction * text.length);
|
||||
editor.revealRange(
|
||||
new vscode.Range(sourceLine, start, sourceLine + 1, 0),
|
||||
vscode.TextEditorRevealType.AtTop);
|
||||
}
|
||||
@@ -5,13 +5,13 @@
|
||||
|
||||
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 { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security';
|
||||
import { basename, dirname, isAbsolute, join } from '../util/path';
|
||||
import { WebviewResourceProvider } from '../util/resources';
|
||||
import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -52,7 +52,14 @@ export class MarkdownContentProvider {
|
||||
private readonly cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private readonly contributionProvider: MarkdownContributionProvider,
|
||||
private readonly logger: Logger
|
||||
) { }
|
||||
) {
|
||||
this.iconPath = {
|
||||
dark: vscode.Uri.joinPath(this.context.extensionUri, 'media', 'preview-dark.svg'),
|
||||
light: vscode.Uri.joinPath(this.context.extensionUri, 'media', 'preview-light.svg'),
|
||||
};
|
||||
}
|
||||
|
||||
public readonly iconPath: { light: vscode.Uri; dark: vscode.Uri };
|
||||
|
||||
public async provideTextDocumentContent(
|
||||
markdownDocument: vscode.TextDocument,
|
||||
@@ -81,7 +88,7 @@ export class MarkdownContentProvider {
|
||||
const nonce = getNonce();
|
||||
const csp = this.getCsp(resourceProvider, sourceUri, nonce);
|
||||
|
||||
const body = await this.engine.render(markdownDocument, resourceProvider);
|
||||
const body = await this.markdownBody(markdownDocument, resourceProvider);
|
||||
const html = `<!DOCTYPE html>
|
||||
<html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}">
|
||||
<head>
|
||||
@@ -97,7 +104,6 @@ export class MarkdownContentProvider {
|
||||
</head>
|
||||
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
|
||||
${body.html}
|
||||
<div class="code-line" data-line="${markdownDocument.lineCount}"></div>
|
||||
${this.getScripts(resourceProvider, nonce)}
|
||||
</body>
|
||||
</html>`;
|
||||
@@ -107,10 +113,22 @@ export class MarkdownContentProvider {
|
||||
};
|
||||
}
|
||||
|
||||
public async markdownBody(
|
||||
markdownDocument: vscode.TextDocument,
|
||||
resourceProvider: WebviewResourceProvider,
|
||||
): Promise<MarkdownContentProviderOutput> {
|
||||
const rendered = await this.engine.render(markdownDocument, resourceProvider);
|
||||
const html = `<div class="markdown-body" dir="auto">${rendered.html}<div class="code-line" data-line="${markdownDocument.lineCount}"></div></div>`;
|
||||
return {
|
||||
html,
|
||||
containingImages: rendered.containingImages
|
||||
};
|
||||
}
|
||||
|
||||
public provideFileNotFoundContent(
|
||||
resource: vscode.Uri,
|
||||
): string {
|
||||
const resourcePath = basename(resource.fsPath);
|
||||
const resourcePath = uri.Utils.basename(resource);
|
||||
const body = localize('preview.notFound', '{0} cannot be found', resourcePath);
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -136,7 +154,7 @@ export class MarkdownContentProvider {
|
||||
}
|
||||
|
||||
// Assume it must be a local file
|
||||
if (isAbsolute(href)) {
|
||||
if (href.startsWith('/') || /^[a-z]:\\/i.test(href)) {
|
||||
return resourceProvider.asWebviewUri(vscode.Uri.file(href)).toString();
|
||||
}
|
||||
|
||||
@@ -147,7 +165,7 @@ export class MarkdownContentProvider {
|
||||
}
|
||||
|
||||
// Otherwise look relative to the markdown file
|
||||
return resourceProvider.asWebviewUri(vscode.Uri.file(join(dirname(resource.fsPath), href))).toString();
|
||||
return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString();
|
||||
}
|
||||
|
||||
private computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
|
||||
@@ -9,10 +9,11 @@ import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { Disposable, disposeAll } from '../util/dispose';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { TopmostLineMonitor } from '../util/topmostLineMonitor';
|
||||
import { DynamicMarkdownPreview, ManagedMarkdownPreview, scrollEditorToLine, StartingScrollFragment, StaticMarkdownPreview } from './preview';
|
||||
import { DynamicMarkdownPreview, ManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
import { scrollEditorToLine, StartingScrollFragment } from './scrolling';
|
||||
import { TopmostLineMonitor } from './topmostLineMonitor';
|
||||
|
||||
export interface DynamicPreviewSettings {
|
||||
readonly resourceColumn: vscode.ViewColumn;
|
||||
@@ -55,6 +56,7 @@ class PreviewStore<T extends ManagedMarkdownPreview> extends Disposable {
|
||||
}
|
||||
|
||||
export class MarkdownPreviewManager extends Disposable implements vscode.WebviewPanelSerializer, vscode.CustomTextEditorProvider {
|
||||
|
||||
private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus';
|
||||
|
||||
private readonly _topmostLineMonitor = new TopmostLineMonitor();
|
||||
@@ -65,8 +67,6 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
|
||||
private _activePreview: ManagedMarkdownPreview | undefined = undefined;
|
||||
|
||||
private readonly customEditorViewType = 'vscode.markdown.preview.editor';
|
||||
|
||||
public constructor(
|
||||
private readonly _contentProvider: MarkdownContentProvider,
|
||||
private readonly _logger: Logger,
|
||||
@@ -74,15 +74,18 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
private readonly _engine: MarkdownEngine,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this));
|
||||
this._register(vscode.window.registerCustomEditorProvider(this.customEditorViewType, this));
|
||||
|
||||
this._register(vscode.window.registerCustomEditorProvider(StaticMarkdownPreview.customEditorViewType, this, {
|
||||
webviewOptions: { enableFindWidget: true }
|
||||
}));
|
||||
|
||||
this._register(vscode.window.onDidChangeActiveTextEditor(textEditor => {
|
||||
|
||||
// When at a markdown file, apply existing scroll settings
|
||||
if (textEditor && textEditor.document && isMarkdownFile(textEditor.document)) {
|
||||
if (textEditor?.document && isMarkdownFile(textEditor.document)) {
|
||||
const line = this._topmostLineMonitor.getPreviousStaticEditorLineByUri(textEditor.document.uri);
|
||||
if (line) {
|
||||
if (typeof line === 'number') {
|
||||
scrollEditorToLine(line, textEditor);
|
||||
}
|
||||
}
|
||||
@@ -172,7 +175,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
document: vscode.TextDocument,
|
||||
webview: vscode.WebviewPanel
|
||||
): Promise<void> {
|
||||
const lineNumber = this._topmostLineMonitor.getPreviousTextEditorLineByUri(document.uri);
|
||||
const lineNumber = this._topmostLineMonitor.getPreviousStaticTextEditorLineByUri(document.uri);
|
||||
const preview = StaticMarkdownPreview.revive(
|
||||
document.uri,
|
||||
webview,
|
||||
@@ -258,4 +261,3 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
vscode.commands.executeCommand('setContext', MarkdownPreviewManager.markdownPreviewActiveContextKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Change the top-most visible line of `editor` to be at `line`
|
||||
*/
|
||||
export function scrollEditorToLine(
|
||||
line: number,
|
||||
editor: vscode.TextEditor
|
||||
) {
|
||||
const sourceLine = Math.floor(line);
|
||||
const fraction = line - sourceLine;
|
||||
const text = editor.document.lineAt(sourceLine).text;
|
||||
const start = Math.floor(fraction * text.length);
|
||||
editor.revealRange(
|
||||
new vscode.Range(sourceLine, start, sourceLine + 1, 0),
|
||||
vscode.TextEditorRevealType.AtTop);
|
||||
}
|
||||
|
||||
export class StartingScrollFragment {
|
||||
public readonly type = 'fragment';
|
||||
|
||||
constructor(
|
||||
public readonly fragment: string,
|
||||
) { }
|
||||
}
|
||||
|
||||
export class StartingScrollLine {
|
||||
public readonly type = 'line';
|
||||
|
||||
constructor(
|
||||
public readonly line: number,
|
||||
) { }
|
||||
}
|
||||
|
||||
export type StartingScrollLocation = StartingScrollLine | StartingScrollFragment;
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { MarkdownPreviewManager } from './features/previewManager';
|
||||
import { MarkdownPreviewManager } from './previewManager';
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { isMarkdownFile } from './file';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
|
||||
export interface LastScrollLocation {
|
||||
readonly line: number;
|
||||
@@ -38,7 +38,7 @@ export class TopmostLineMonitor extends Disposable {
|
||||
}));
|
||||
}
|
||||
|
||||
private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>());
|
||||
private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri; readonly line: number }>());
|
||||
public readonly onDidChanged = this._onChanged.event;
|
||||
|
||||
public setPreviousStaticEditorLine(scrollLocation: LastScrollLocation): void {
|
||||
@@ -62,6 +62,11 @@ export class TopmostLineMonitor extends Disposable {
|
||||
return scrollLoc?.line;
|
||||
}
|
||||
|
||||
public getPreviousStaticTextEditorLineByUri(resource: vscode.Uri): number | undefined {
|
||||
const state = this.previousStaticEditorInfo.get(resource.toString());
|
||||
return state?.line;
|
||||
}
|
||||
|
||||
public updateLine(
|
||||
resource: vscode.Uri,
|
||||
line: number
|
||||
@@ -23,6 +23,7 @@ export const githubSlugifier: Slugifier = new class implements Slugifier {
|
||||
heading.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-') // Replace whitespace with -
|
||||
// allow-any-unicode-next-line
|
||||
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
|
||||
.replace(/^\-+/, '') // Remove leading -
|
||||
.replace(/\-+$/, '') // Remove trailing -
|
||||
|
||||
172
extensions/markdown-language-features/src/tableOfContents.ts
Normal file
172
extensions/markdown-language-features/src/tableOfContents.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { githubSlugifier, Slug } from './slugify';
|
||||
import { isMarkdownFile } from './util/file';
|
||||
import { SkinnyTextDocument } from './workspaceContents';
|
||||
|
||||
export interface TocEntry {
|
||||
readonly slug: Slug;
|
||||
readonly text: string;
|
||||
readonly level: number;
|
||||
readonly line: number;
|
||||
|
||||
/**
|
||||
* The entire range of the header section.
|
||||
*
|
||||
* For the doc:
|
||||
*
|
||||
* ```md
|
||||
* # Head #
|
||||
* text
|
||||
* # Next head #
|
||||
* ```
|
||||
*
|
||||
* This is the range from `# Head #` to `# Next head #`
|
||||
*/
|
||||
readonly sectionLocation: vscode.Location;
|
||||
|
||||
/**
|
||||
* The range of the header declaration.
|
||||
*
|
||||
* For the doc:
|
||||
*
|
||||
* ```md
|
||||
* # Head #
|
||||
* text
|
||||
* ```
|
||||
*
|
||||
* This is the range of `# Head #`
|
||||
*/
|
||||
readonly headerLocation: vscode.Location;
|
||||
|
||||
/**
|
||||
* The range of the header text.
|
||||
*
|
||||
* For the doc:
|
||||
*
|
||||
* ```md
|
||||
* # Head #
|
||||
* text
|
||||
* ```
|
||||
*
|
||||
* This is the range of `Head`
|
||||
*/
|
||||
readonly headerTextLocation: vscode.Location;
|
||||
}
|
||||
|
||||
export class TableOfContents {
|
||||
|
||||
public static async create(engine: MarkdownEngine, document: SkinnyTextDocument,): Promise<TableOfContents> {
|
||||
const entries = await this.buildToc(engine, document);
|
||||
return new TableOfContents(entries);
|
||||
}
|
||||
|
||||
public static async createForDocumentOrNotebook(engine: MarkdownEngine, document: SkinnyTextDocument): Promise<TableOfContents> {
|
||||
if (document.uri.scheme === 'vscode-notebook-cell') {
|
||||
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 this.create(engine, document);
|
||||
}
|
||||
|
||||
private static async buildToc(engine: MarkdownEngine, document: SkinnyTextDocument): Promise<TocEntry[]> {
|
||||
const toc: TocEntry[] = [];
|
||||
const tokens = await engine.parse(document);
|
||||
|
||||
const existingSlugEntries = new Map<string, { count: number }>();
|
||||
|
||||
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
|
||||
if (!heading.map) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineNumber = heading.map[0];
|
||||
const line = document.lineAt(lineNumber);
|
||||
|
||||
let slug = githubSlugifier.fromHeading(line.text);
|
||||
const existingSlugEntry = existingSlugEntries.get(slug.value);
|
||||
if (existingSlugEntry) {
|
||||
++existingSlugEntry.count;
|
||||
slug = githubSlugifier.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));
|
||||
|
||||
const headerTextLocation = new vscode.Location(document.uri,
|
||||
new vscode.Range(lineNumber, line.text.match(/^#+\s*/)?.[0].length ?? 0, lineNumber, line.text.length - (line.text.match(/\s*#*$/)?.[0].length ?? 0)));
|
||||
|
||||
toc.push({
|
||||
slug,
|
||||
text: TableOfContents.getHeaderText(line.text),
|
||||
level: TableOfContents.getHeaderLevel(heading.markup),
|
||||
line: lineNumber,
|
||||
sectionLocation: headerLocation, // Populated in next steps
|
||||
headerLocation,
|
||||
headerTextLocation
|
||||
});
|
||||
}
|
||||
|
||||
// Get full range of section
|
||||
return toc.map((entry, startIndex): TocEntry => {
|
||||
let end: number | undefined = undefined;
|
||||
for (let i = startIndex + 1; i < toc.length; ++i) {
|
||||
if (toc[i].level <= entry.level) {
|
||||
end = toc[i].line - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const endLine = end ?? document.lineCount - 1;
|
||||
return {
|
||||
...entry,
|
||||
sectionLocation: new vscode.Location(document.uri,
|
||||
new vscode.Range(
|
||||
entry.sectionLocation.range.start,
|
||||
new vscode.Position(endLine, document.lineAt(endLine).text.length)))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static getHeaderLevel(markup: string): number {
|
||||
if (markup === '=') {
|
||||
return 1;
|
||||
} else if (markup === '-') {
|
||||
return 2;
|
||||
} else { // '#', '##', ...
|
||||
return markup.length;
|
||||
}
|
||||
}
|
||||
|
||||
private static getHeaderText(header: string): string {
|
||||
return header.replace(/^\s*#+\s*(.*?)(\s+#+)?$/, (_, word) => word.trim());
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public readonly entries: readonly TocEntry[],
|
||||
) { }
|
||||
|
||||
public lookup(fragment: string): TocEntry | undefined {
|
||||
const slug = githubSlugifier.fromHeading(fragment);
|
||||
return this.entries.find(entry => entry.slug.equals(slug));
|
||||
}
|
||||
}
|
||||
@@ -1,122 +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 { githubSlugifier, Slug } from './slugify';
|
||||
|
||||
export interface TocEntry {
|
||||
readonly slug: Slug;
|
||||
readonly text: string;
|
||||
readonly level: number;
|
||||
readonly line: number;
|
||||
readonly location: vscode.Location;
|
||||
}
|
||||
|
||||
export interface SkinnyTextLine {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface SkinnyTextDocument {
|
||||
readonly uri: vscode.Uri;
|
||||
readonly version: number;
|
||||
readonly lineCount: number;
|
||||
|
||||
lineAt(line: number): SkinnyTextLine;
|
||||
getText(): string;
|
||||
}
|
||||
|
||||
export class TableOfContentsProvider {
|
||||
private toc?: TocEntry[];
|
||||
|
||||
public constructor(
|
||||
private engine: MarkdownEngine,
|
||||
private document: SkinnyTextDocument
|
||||
) { }
|
||||
|
||||
public async getToc(): Promise<TocEntry[]> {
|
||||
if (!this.toc) {
|
||||
try {
|
||||
this.toc = await this.buildToc(this.document);
|
||||
} catch (e) {
|
||||
this.toc = [];
|
||||
}
|
||||
}
|
||||
return this.toc;
|
||||
}
|
||||
|
||||
public async lookup(fragment: string): Promise<TocEntry | undefined> {
|
||||
const toc = await this.getToc();
|
||||
const slug = githubSlugifier.fromHeading(fragment);
|
||||
return toc.find(entry => entry.slug.equals(slug));
|
||||
}
|
||||
|
||||
private async buildToc(document: SkinnyTextDocument): Promise<TocEntry[]> {
|
||||
const toc: TocEntry[] = [];
|
||||
const tokens = await this.engine.parse(document);
|
||||
|
||||
const existingSlugEntries = new Map<string, { count: number }>();
|
||||
|
||||
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
|
||||
if (!heading.map) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineNumber = heading.map[0];
|
||||
const line = document.lineAt(lineNumber);
|
||||
|
||||
let slug = githubSlugifier.fromHeading(line.text);
|
||||
const existingSlugEntry = existingSlugEntries.get(slug.value);
|
||||
if (existingSlugEntry) {
|
||||
++existingSlugEntry.count;
|
||||
slug = githubSlugifier.fromHeading(slug.value + '-' + existingSlugEntry.count);
|
||||
} else {
|
||||
existingSlugEntries.set(slug.value, { count: 0 });
|
||||
}
|
||||
|
||||
toc.push({
|
||||
slug,
|
||||
text: TableOfContentsProvider.getHeaderText(line.text),
|
||||
level: TableOfContentsProvider.getHeaderLevel(heading.markup),
|
||||
line: lineNumber,
|
||||
location: new vscode.Location(document.uri,
|
||||
new vscode.Range(lineNumber, 0, lineNumber, line.text.length))
|
||||
});
|
||||
}
|
||||
|
||||
// Get full range of section
|
||||
return toc.map((entry, startIndex): TocEntry => {
|
||||
let end: number | undefined = undefined;
|
||||
for (let i = startIndex + 1; i < toc.length; ++i) {
|
||||
if (toc[i].level <= entry.level) {
|
||||
end = toc[i].line - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const endLine = end ?? document.lineCount - 1;
|
||||
return {
|
||||
...entry,
|
||||
location: new vscode.Location(document.uri,
|
||||
new vscode.Range(
|
||||
entry.location.range.start,
|
||||
new vscode.Position(endLine, document.lineAt(endLine).text.length)))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static getHeaderLevel(markup: string): number {
|
||||
if (markup === '=') {
|
||||
return 1;
|
||||
} else if (markup === '-') {
|
||||
return 2;
|
||||
} else { // '#', '##', ...
|
||||
return markup.length;
|
||||
}
|
||||
}
|
||||
|
||||
private static getHeaderText(header: string): string {
|
||||
return header.replace(/^\s*#+\s*(.*?)\s*#*$/, (_, word) => word.trim());
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { default as VSCodeTelemetryReporter } from '@vscode/extension-telemetry';
|
||||
import * as vscode from 'vscode';
|
||||
import { default as VSCodeTelemetryReporter } from 'vscode-extension-telemetry';
|
||||
|
||||
interface IPackageInfo {
|
||||
name: string;
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -10,15 +10,26 @@ import { joinLines } from './util';
|
||||
|
||||
const testFileA = workspaceFile('a.md');
|
||||
|
||||
const debug = false;
|
||||
|
||||
function debugLog(...args: any[]) {
|
||||
if (debug) {
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function workspaceFile(...segments: string[]) {
|
||||
return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, ...segments);
|
||||
}
|
||||
|
||||
async function getLinksForFile(file: vscode.Uri): Promise<vscode.DocumentLink[]> {
|
||||
return (await vscode.commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', file))!;
|
||||
debugLog('getting links', file.toString(), Date.now());
|
||||
const r = (await vscode.commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', file))!;
|
||||
debugLog('got links', file.toString(), Date.now());
|
||||
return r;
|
||||
}
|
||||
|
||||
suite('Markdown Document links', () => {
|
||||
(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('Markdown Document links', () => {
|
||||
|
||||
setup(async () => {
|
||||
// the tests make the assumption that link providers are already registered
|
||||
@@ -94,7 +105,6 @@ suite('Markdown Document links', () => {
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
|
||||
});
|
||||
|
||||
|
||||
test('Should navigate to line number within non-md file', async () => {
|
||||
await withFileContents(testFileA, '[b](sub/foo.txt#L3)');
|
||||
|
||||
@@ -147,15 +157,21 @@ function assertActiveDocumentUri(expectedUri: vscode.Uri) {
|
||||
}
|
||||
|
||||
async function withFileContents(file: vscode.Uri, contents: string): Promise<void> {
|
||||
debugLog('openTextDocument', file.toString(), Date.now());
|
||||
const document = await vscode.workspace.openTextDocument(file);
|
||||
debugLog('showTextDocument', file.toString(), Date.now());
|
||||
const editor = await vscode.window.showTextDocument(document);
|
||||
debugLog('editTextDocument', file.toString(), Date.now());
|
||||
await editor.edit(edit => {
|
||||
edit.replace(new vscode.Range(0, 0, 1000, 0), contents);
|
||||
});
|
||||
debugLog('opened done', vscode.window.activeTextEditor?.document.toString(), Date.now());
|
||||
}
|
||||
|
||||
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);
|
||||
debugLog('executedLink', vscode.window.activeTextEditor?.document.toString(), Date.now());
|
||||
}
|
||||
|
||||
|
||||
@@ -6,90 +6,78 @@
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import LinkProvider from '../features/documentLinkProvider';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
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');
|
||||
|
||||
const noopToken = new class implements vscode.CancellationToken {
|
||||
private _onCancellationRequestedEmitter = new vscode.EventEmitter<void>();
|
||||
public onCancellationRequested = this._onCancellationRequestedEmitter.event;
|
||||
|
||||
get isCancellationRequested() { return false; }
|
||||
};
|
||||
|
||||
function getLinksForFile(fileContents: string) {
|
||||
const doc = new InMemoryDocument(testFile, fileContents);
|
||||
const provider = new LinkProvider();
|
||||
const provider = new MdLinkProvider(createNewMarkdownEngine());
|
||||
return provider.provideDocumentLinks(doc, noopToken);
|
||||
}
|
||||
|
||||
function assertRangeEqual(expected: vscode.Range, actual: vscode.Range) {
|
||||
assert.strictEqual(expected.start.line, actual.start.line);
|
||||
assert.strictEqual(expected.start.character, actual.start.character);
|
||||
assert.strictEqual(expected.end.line, actual.end.line);
|
||||
assert.strictEqual(expected.end.character, actual.end.character);
|
||||
}
|
||||
|
||||
suite('markdown.DocumentLinkProvider', () => {
|
||||
test('Should not return anything for empty document', () => {
|
||||
const links = getLinksForFile('');
|
||||
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', () => {
|
||||
const links = getLinksForFile('# a\nfdasfdfsafsa');
|
||||
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', () => {
|
||||
const links = getLinksForFile('a [b](https://example.com) c');
|
||||
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', () => {
|
||||
test('Should detect basic workspace links', async () => {
|
||||
{
|
||||
const links = getLinksForFile('a [b](./file) c');
|
||||
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 = getLinksForFile('a [b](file.png) c');
|
||||
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', () => {
|
||||
const links = getLinksForFile('a [b](https://example.com "abc") c');
|
||||
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', () => {
|
||||
const links = getLinksForFile('a [b\\]](./file)');
|
||||
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', () => {
|
||||
test('Should handle links with balanced parens', async () => {
|
||||
{
|
||||
const links = getLinksForFile('a [b](https://example.com/a()c) c');
|
||||
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 = getLinksForFile('a [b](https://example.com/a(b)c) c');
|
||||
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));
|
||||
@@ -97,15 +85,15 @@ suite('markdown.DocumentLinkProvider', () => {
|
||||
}
|
||||
{
|
||||
// #49011
|
||||
const links = getLinksForFile('[A link](http://ThisUrlhasParens/A_link(in_parens))');
|
||||
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', () => {
|
||||
const links = getLinksForFile('a ([test](test)[test2](test2)) c');
|
||||
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));
|
||||
@@ -113,23 +101,23 @@ suite('markdown.DocumentLinkProvider', () => {
|
||||
});
|
||||
|
||||
// #49238
|
||||
test('should handle hyperlinked images', () => {
|
||||
test('should handle hyperlinked images', async () => {
|
||||
{
|
||||
const links = getLinksForFile('[](https://example.com)');
|
||||
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 = getLinksForFile('[]( https://whitespace.com )');
|
||||
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 = getLinksForFile('[](file1.txt) text [](file2.txt)');
|
||||
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));
|
||||
@@ -139,11 +127,144 @@ suite('markdown.DocumentLinkProvider', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// #107471
|
||||
test('Should not consider link references starting with ^ character valid', () => {
|
||||
const links = getLinksForFile('[^reference]: https://example.com');
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import SymbolProvider from '../features/documentSymbolProvider';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { MdDocumentSymbolProvider } from '../languageFeatures/documentSymbolProvider';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
@@ -16,7 +16,7 @@ const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
function getSymbolsForFile(fileContents: string) {
|
||||
const doc = new InMemoryDocument(testFileName, fileContents);
|
||||
const provider = new SymbolProvider(createNewMarkdownEngine());
|
||||
const provider = new MdDocumentSymbolProvider(createNewMarkdownEngine());
|
||||
return provider.provideDocumentSymbols(doc);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ suite('markdown.DocumentSymbolProvider', () => {
|
||||
|
||||
test('Should handle line separator in file. Issue #63749', async () => {
|
||||
const symbols = await getSymbolsForFile(`# A
|
||||
- foo
|
||||
- foo
|
||||
|
||||
# B
|
||||
- bar`);
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
@@ -15,8 +15,8 @@ const testFileName = vscode.Uri.file('test.md');
|
||||
suite('markdown.engine', () => {
|
||||
suite('rendering', () => {
|
||||
const input = '# hello\n\nworld!';
|
||||
const output = '<h1 data-line="0" class="code-line" id="hello">hello</h1>\n'
|
||||
+ '<p data-line="2" class="code-line">world!</p>\n';
|
||||
const output = '<h1 data-line="0" class="code-line" dir="auto" id="hello">hello</h1>\n'
|
||||
+ '<p data-line="2" class="code-line" dir="auto">world!</p>\n';
|
||||
|
||||
test('Renders a document', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, input);
|
||||
@@ -36,7 +36,7 @@ suite('markdown.engine', () => {
|
||||
test('Extracts all images', async () => {
|
||||
const engine = createNewMarkdownEngine();
|
||||
assert.deepStrictEqual((await engine.render(input)), {
|
||||
html: '<p data-line="0" class="code-line">'
|
||||
html: '<p data-line="0" class="code-line" dir="auto">'
|
||||
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435" data-src="img.png"> '
|
||||
+ '<a href="no-img.png" data-href="no-img.png"></a> '
|
||||
+ '<img src="http://example.org/img.png" alt="" class="loading" id="image-hash--1903814170" data-src="http://example.org/img.png"> '
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -6,10 +6,10 @@
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import MarkdownFoldingProvider from '../features/foldingProvider';
|
||||
import { MdFoldingProvider } from '../languageFeatures/foldingProvider';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
|
||||
import { joinLines } from './util';
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
@@ -20,18 +20,22 @@ suite('markdown.FoldingProvider', () => {
|
||||
});
|
||||
|
||||
test('Should not return anything for document without headers', async () => {
|
||||
const folds = await getFoldsForDocument(`a
|
||||
**b** afas
|
||||
a#b
|
||||
a`);
|
||||
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(`a
|
||||
# b
|
||||
c
|
||||
d`);
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`a`,
|
||||
`# b`,
|
||||
`c`,
|
||||
`d`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
@@ -39,39 +43,45 @@ d`);
|
||||
});
|
||||
|
||||
test('Should leave single newline before next header', async () => {
|
||||
const folds = await getFoldsForDocument(`
|
||||
# a
|
||||
x
|
||||
|
||||
# b
|
||||
y`);
|
||||
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, 3);
|
||||
assert.strictEqual(firstFold.end, 2);
|
||||
});
|
||||
|
||||
test('Should collapse multuple newlines to single newline before next header', async () => {
|
||||
const folds = await getFoldsForDocument(`
|
||||
# a
|
||||
x
|
||||
|
||||
|
||||
|
||||
# b
|
||||
y`);
|
||||
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, 5);
|
||||
assert.strictEqual(firstFold.end, 4);
|
||||
});
|
||||
|
||||
test('Should not collapse if there is no newline before next header', async () => {
|
||||
const folds = await getFoldsForDocument(`
|
||||
# a
|
||||
x
|
||||
# b
|
||||
y`);
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
``,
|
||||
`# a`,
|
||||
`x`,
|
||||
`# b`,
|
||||
`y`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 2);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
@@ -79,19 +89,21 @@ y`);
|
||||
});
|
||||
|
||||
test('Should fold nested <!-- #region --> markers', async () => {
|
||||
const folds = await getFoldsForDocument(`a
|
||||
<!-- #region -->
|
||||
b
|
||||
<!-- #region hello!-->
|
||||
b.a
|
||||
<!-- #endregion -->
|
||||
b
|
||||
<!-- #region: foo! -->
|
||||
b.b
|
||||
<!-- #endregion: foo -->
|
||||
b
|
||||
<!-- #endregion -->
|
||||
a`);
|
||||
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);
|
||||
|
||||
@@ -104,10 +116,12 @@ a`);
|
||||
});
|
||||
|
||||
test('Should fold from list to end of document', async () => {
|
||||
const folds = await getFoldsForDocument(`a
|
||||
- b
|
||||
c
|
||||
d`);
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`a`,
|
||||
`- b`,
|
||||
`c`,
|
||||
`d`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
@@ -115,8 +129,10 @@ d`);
|
||||
});
|
||||
|
||||
test('lists folds should span multiple lines of content', async () => {
|
||||
const folds = await getFoldsForDocument(`a
|
||||
- This list item\n spans multiple\n lines.`);
|
||||
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);
|
||||
@@ -124,22 +140,26 @@ d`);
|
||||
});
|
||||
|
||||
test('List should leave single blankline before new element', async () => {
|
||||
const folds = await getFoldsForDocument(`- a
|
||||
a
|
||||
|
||||
|
||||
b`);
|
||||
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, 3);
|
||||
assert.strictEqual(firstFold.end, 2);
|
||||
});
|
||||
|
||||
test('Should fold fenced code blocks', async () => {
|
||||
const folds = await getFoldsForDocument(`~~~ts
|
||||
a
|
||||
~~~
|
||||
b`);
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`~~~ts`,
|
||||
`a`,
|
||||
`~~~`,
|
||||
`b`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 0);
|
||||
@@ -147,18 +167,20 @@ b`);
|
||||
});
|
||||
|
||||
test('Should fold fenced code blocks with yaml front matter', async () => {
|
||||
const folds = await getFoldsForDocument(`---
|
||||
title: bla
|
||||
---
|
||||
|
||||
~~~ts
|
||||
a
|
||||
~~~
|
||||
|
||||
a
|
||||
a
|
||||
b
|
||||
a`);
|
||||
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);
|
||||
@@ -166,10 +188,12 @@ a`);
|
||||
});
|
||||
|
||||
test('Should fold html blocks', async () => {
|
||||
const folds = await getFoldsForDocument(`x
|
||||
<div>
|
||||
fa
|
||||
</div>`);
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`x`,
|
||||
`<div>`,
|
||||
` fa`,
|
||||
`</div>`,
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
@@ -177,10 +201,12 @@ a`);
|
||||
});
|
||||
|
||||
test('Should fold html block comments', async () => {
|
||||
const folds = await getFoldsForDocument(`x
|
||||
<!--
|
||||
fa
|
||||
-->`);
|
||||
const folds = await getFoldsForDocument(joinLines(
|
||||
`x`,
|
||||
`<!--`,
|
||||
`fa`,
|
||||
`-->`
|
||||
));
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
@@ -192,6 +218,6 @@ fa
|
||||
|
||||
async function getFoldsForDocument(contents: string) {
|
||||
const doc = new InMemoryDocument(testFileName, contents);
|
||||
const provider = new MarkdownFoldingProvider(createNewMarkdownEngine());
|
||||
const provider = new MdFoldingProvider(createNewMarkdownEngine());
|
||||
return await provider.provideFoldingRanges(doc, {}, new vscode.CancellationTokenSource().token);
|
||||
}
|
||||
|
||||
@@ -1,70 +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 os from 'os';
|
||||
import * as vscode from 'vscode';
|
||||
export class InMemoryDocument implements vscode.TextDocument {
|
||||
private readonly _lines: string[];
|
||||
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri,
|
||||
private readonly _contents: string,
|
||||
public readonly version = 1,
|
||||
) {
|
||||
this._lines = this._contents.split(/\r\n|\n/g);
|
||||
}
|
||||
|
||||
|
||||
isUntitled: boolean = false;
|
||||
languageId: string = '';
|
||||
isDirty: boolean = false;
|
||||
isClosed: boolean = false;
|
||||
eol: vscode.EndOfLine = os.platform() === 'win32' ? vscode.EndOfLine.CRLF : vscode.EndOfLine.LF;
|
||||
notebook: undefined;
|
||||
|
||||
get fileName(): string {
|
||||
return this.uri.fsPath;
|
||||
}
|
||||
|
||||
get lineCount(): number {
|
||||
return this._lines.length;
|
||||
}
|
||||
|
||||
lineAt(line: any): vscode.TextLine {
|
||||
return {
|
||||
lineNumber: line,
|
||||
text: this._lines[line],
|
||||
range: new vscode.Range(0, 0, 0, 0),
|
||||
firstNonWhitespaceCharacterIndex: 0,
|
||||
rangeIncludingLineBreak: new vscode.Range(0, 0, 0, 0),
|
||||
isEmptyOrWhitespace: false
|
||||
};
|
||||
}
|
||||
offsetAt(_position: vscode.Position): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
positionAt(offset: number): vscode.Position {
|
||||
const before = this._contents.slice(0, offset);
|
||||
const newLines = before.match(/\r\n|\n/g);
|
||||
const line = newLines ? newLines.length : 0;
|
||||
const preCharacters = before.match(/(\r\n|\n|^).*$/g);
|
||||
return new vscode.Position(line, preCharacters ? preCharacters[0].length : 0);
|
||||
}
|
||||
getText(_range?: vscode.Range | undefined): string {
|
||||
return this._contents;
|
||||
}
|
||||
getWordRangeAtPosition(_position: vscode.Position, _regex?: RegExp | undefined): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
validateRange(_range: vscode.Range): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
validatePosition(_position: vscode.Position): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
save(): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
|
||||
|
||||
|
||||
export class InMemoryWorkspaceMarkdownDocuments implements MdWorkspaceContents {
|
||||
private readonly _documents = new Map<string, SkinnyTextDocument>();
|
||||
|
||||
constructor(documents: SkinnyTextDocument[]) {
|
||||
for (const doc of documents) {
|
||||
this._documents.set(this.getKey(doc.uri), doc);
|
||||
}
|
||||
}
|
||||
|
||||
public async getAllMarkdownDocuments() {
|
||||
return Array.from(this._documents.values());
|
||||
}
|
||||
|
||||
public async getMarkdownDocument(resource: vscode.Uri): Promise<SkinnyTextDocument | undefined> {
|
||||
return this._documents.get(this.getKey(resource));
|
||||
}
|
||||
|
||||
public async pathExists(resource: vscode.Uri): Promise<boolean> {
|
||||
return this._documents.has(this.getKey(resource));
|
||||
}
|
||||
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = new vscode.EventEmitter<SkinnyTextDocument>();
|
||||
public onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocumentEmitter.event;
|
||||
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = new vscode.EventEmitter<SkinnyTextDocument>();
|
||||
public onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocumentEmitter.event;
|
||||
|
||||
private readonly _onDidDeleteMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.Uri>();
|
||||
public onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocumentEmitter.event;
|
||||
|
||||
public updateDocument(document: SkinnyTextDocument) {
|
||||
this._documents.set(this.getKey(document.uri), document);
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
|
||||
public createDocument(document: SkinnyTextDocument) {
|
||||
assert.ok(!this._documents.has(this.getKey(document.uri)));
|
||||
|
||||
this._documents.set(this.getKey(document.uri), document);
|
||||
this._onDidCreateMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
|
||||
public deleteDocument(resource: vscode.Uri) {
|
||||
this._documents.delete(this.getKey(resource));
|
||||
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
|
||||
}
|
||||
|
||||
private getKey(resource: vscode.Uri): string {
|
||||
return resource.fsPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,580 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
616
extensions/markdown-language-features/src/test/rename.test.ts
Normal file
616
extensions/markdown-language-features/src/test/rename.test.ts
Normal file
@@ -0,0 +1,616 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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'),
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,12 +5,10 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import MarkdownSmartSelect from '../features/smartSelect';
|
||||
import { MdSmartSelect } from '../languageFeatures/smartSelect';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { joinLines } from './util';
|
||||
|
||||
const CURSOR = '$$CURSOR$$';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { CURSOR, getCursorPositions, joinLines } from './util';
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
@@ -19,6 +17,7 @@ suite('markdown.SmartSelect', () => {
|
||||
const ranges = await getSelectionRangesForDocument(`Hel${CURSOR}lo`);
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 0]);
|
||||
});
|
||||
|
||||
test('Smart select multi-line paragraph', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -28,11 +27,13 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -42,6 +43,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
|
||||
test('Smart select header on header line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -51,6 +53,7 @@ suite('markdown.SmartSelect', () => {
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 1]);
|
||||
|
||||
});
|
||||
|
||||
test('Smart select single word w grandparent header on text line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -61,6 +64,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [1, 2]);
|
||||
});
|
||||
|
||||
test('Smart select html block w parent header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -71,6 +75,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select fenced code block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -80,6 +85,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
|
||||
test('Smart select list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -89,6 +95,7 @@ suite('markdown.SmartSelect', () => {
|
||||
`- item 4`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select list with fenced code block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -101,6 +108,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 3], [0, 5]);
|
||||
});
|
||||
|
||||
test('Smart select multi cursor', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -114,6 +122,7 @@ suite('markdown.SmartSelect', () => {
|
||||
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(
|
||||
@@ -123,6 +132,7 @@ suite('markdown.SmartSelect', () => {
|
||||
`>> item 4`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select multi nested block quotes', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -132,6 +142,7 @@ suite('markdown.SmartSelect', () => {
|
||||
`>>>> item 4`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select subheader content', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -143,6 +154,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [2, 3], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select subheader line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -154,6 +166,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 3], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select blank line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -165,6 +178,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select line between paragraphs', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -174,41 +188,46 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
`# main header 1`,
|
||||
`content 1`,
|
||||
`## sub header 1`,
|
||||
`- item 1`,
|
||||
`- ~~~`,
|
||||
` ${CURSOR}a`,
|
||||
` ~~~`,
|
||||
`- item 3`,
|
||||
`- item 4`,
|
||||
``,
|
||||
`more content`,
|
||||
`# main header 2`));
|
||||
/* 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, 9], [3, 10], [2, 10], [1, 10], [0, 10]);
|
||||
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(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`- list ${CURSOR}`,
|
||||
``,
|
||||
`## sub header`,
|
||||
``,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [1, 3], [1, 6], [0, 6]);
|
||||
/* 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(
|
||||
@@ -223,6 +242,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -241,6 +261,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -265,6 +286,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -289,6 +311,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -313,6 +336,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -337,6 +361,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -361,6 +386,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [8, 12], [7, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
|
||||
test('Smart select without multiple ranges', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -372,6 +398,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -385,6 +412,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -398,6 +426,7 @@ suite('markdown.SmartSelect', () => {
|
||||
`* 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(
|
||||
@@ -407,6 +436,7 @@ suite('markdown.SmartSelect', () => {
|
||||
`* 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(
|
||||
@@ -416,6 +446,7 @@ suite('markdown.SmartSelect', () => {
|
||||
`- level ${CURSOR}1`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select without multiple ranges', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -427,6 +458,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -440,6 +472,7 @@ suite('markdown.SmartSelect', () => {
|
||||
|
||||
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(
|
||||
@@ -453,6 +486,7 @@ suite('markdown.SmartSelect', () => {
|
||||
`* 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(
|
||||
@@ -462,6 +496,7 @@ suite('markdown.SmartSelect', () => {
|
||||
`* level 1`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [1, 2], [0, 2], [0, 3]);
|
||||
});
|
||||
|
||||
test('Smart select bold', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
@@ -469,6 +504,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -476,6 +512,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -483,6 +520,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -497,6 +535,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -511,6 +550,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -525,6 +565,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -532,6 +573,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -539,6 +581,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -546,6 +589,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -553,6 +597,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -560,6 +605,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -567,6 +613,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -574,6 +621,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -581,6 +629,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -588,6 +637,7 @@ suite('markdown.SmartSelect', () => {
|
||||
));
|
||||
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(
|
||||
@@ -625,8 +675,10 @@ suite('markdown.SmartSelect', () => {
|
||||
);
|
||||
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)}`);
|
||||
@@ -666,23 +718,9 @@ function assertLineNumbersEqual(selectionRange: vscode.SelectionRange, startLine
|
||||
assert.strictEqual(selectionRange.range.end.line, endLine, `failed on end line ${message}`);
|
||||
}
|
||||
|
||||
async function getSelectionRangesForDocument(contents: string, pos?: vscode.Position[]) {
|
||||
function getSelectionRangesForDocument(contents: string, pos?: vscode.Position[]): Promise<vscode.SelectionRange[] | undefined> {
|
||||
const doc = new InMemoryDocument(testFileName, contents);
|
||||
const provider = new MarkdownSmartSelect(createNewMarkdownEngine());
|
||||
const provider = new MdSmartSelect(createNewMarkdownEngine());
|
||||
const positions = pos ? pos : getCursorPositions(contents, doc);
|
||||
return await provider.provideSelectionRanges(doc, positions, new vscode.CancellationTokenSource().token);
|
||||
return provider.provideSelectionRanges(doc, positions, new vscode.CancellationTokenSource().token);
|
||||
}
|
||||
|
||||
let 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;
|
||||
};
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { TableOfContentsProvider } from '../tableOfContentsProvider';
|
||||
import { TableOfContents } from '../tableOfContents';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
@@ -16,36 +16,36 @@ 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 = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual(await provider.lookup(''), undefined);
|
||||
assert.strictEqual(await provider.lookup('foo'), undefined);
|
||||
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 = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual(await provider.lookup(''), undefined);
|
||||
assert.strictEqual(await provider.lookup('foo'), undefined);
|
||||
assert.strictEqual(await provider.lookup('a'), undefined);
|
||||
assert.strictEqual(await provider.lookup('b'), undefined);
|
||||
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 = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
{
|
||||
const entry = await provider.lookup('a');
|
||||
const entry = provider.lookup('a');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 0);
|
||||
}
|
||||
{
|
||||
assert.strictEqual(await provider.lookup('x'), undefined);
|
||||
assert.strictEqual(provider.lookup('x'), undefined);
|
||||
}
|
||||
{
|
||||
const entry = await provider.lookup('c');
|
||||
const entry = provider.lookup('c');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 2);
|
||||
}
|
||||
@@ -53,40 +53,40 @@ suite('markdown.TableOfContentsProvider', () => {
|
||||
|
||||
test('Lookups should be case in-sensitive', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# fOo\n`);
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('fOo'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup('foo'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup('FOO'))!.line, 0);
|
||||
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 = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup(' f o o'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup(' f o o '))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
|
||||
assert.strictEqual((await 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 o o'))!.line, 0);
|
||||
|
||||
assert.strictEqual(await provider.lookup('f'), undefined);
|
||||
assert.strictEqual(await provider.lookup('foo'), undefined);
|
||||
assert.strictEqual(await provider.lookup('fo o'), undefined);
|
||||
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 = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('indentação'))!.line, 0);
|
||||
assert.strictEqual((provider.lookup('indentação'))!.line, 0);
|
||||
});
|
||||
|
||||
test('should handle special characters 2, #48482', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# Инструкция - Делай Раз, Делай Два\n`);
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('инструкция---делай-раз-делай-два'))!.line, 0);
|
||||
assert.strictEqual((provider.lookup('инструкция---делай-раз-делай-два'))!.line, 0);
|
||||
});
|
||||
|
||||
test('should handle special characters 3, #37079', async () => {
|
||||
@@ -97,32 +97,32 @@ suite('markdown.TableOfContentsProvider', () => {
|
||||
### Заголовок Header 3
|
||||
## Заголовок`);
|
||||
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('header-2'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup('header-3'))!.line, 1);
|
||||
assert.strictEqual((await provider.lookup('Заголовок-2'))!.line, 2);
|
||||
assert.strictEqual((await provider.lookup('Заголовок-3'))!.line, 3);
|
||||
assert.strictEqual((await provider.lookup('Заголовок-header-3'))!.line, 4);
|
||||
assert.strictEqual((await provider.lookup('Заголовок'))!.line, 5);
|
||||
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 = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
|
||||
|
||||
{
|
||||
const entry = await provider.lookup('a');
|
||||
const entry = provider.lookup('a');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 0);
|
||||
}
|
||||
{
|
||||
const entry = await provider.lookup('a-1');
|
||||
const entry = provider.lookup('a-1');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 1);
|
||||
}
|
||||
{
|
||||
const entry = await provider.lookup('a-2');
|
||||
const entry = provider.lookup('a-2');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 2);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,44 @@
|
||||
* 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 os from 'os';
|
||||
import * as vscode from 'vscode';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function assertRangeEqual(expected: vscode.Range, actual: vscode.Range, message?: string) {
|
||||
assert.strictEqual(expected.start.line, actual.start.line, message);
|
||||
assert.strictEqual(expected.start.character, actual.start.character, message);
|
||||
assert.strictEqual(expected.end.line, actual.end.line, message);
|
||||
assert.strictEqual(expected.end.character, actual.end.character, message);
|
||||
}
|
||||
|
||||
@@ -6,17 +6,19 @@
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import MDDocumentSymbolProvider from '../features/documentSymbolProvider';
|
||||
import MarkdownWorkspaceSymbolProvider, { WorkspaceMarkdownDocumentProvider } from '../features/workspaceSymbolProvider';
|
||||
import { MdDocumentSymbolProvider } from '../languageFeatures/documentSymbolProvider';
|
||||
import { MdWorkspaceSymbolProvider } from '../languageFeatures/workspaceSymbolProvider';
|
||||
import { SkinnyTextDocument } from '../workspaceContents';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { InMemoryDocument } from '../util/inMemoryDocument';
|
||||
import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace';
|
||||
|
||||
|
||||
const symbolProvider = new MDDocumentSymbolProvider(createNewMarkdownEngine());
|
||||
const symbolProvider = new MdDocumentSymbolProvider(createNewMarkdownEngine());
|
||||
|
||||
suite('markdown.WorkspaceSymbolProvider', () => {
|
||||
test('Should not return anything for empty workspace', async () => {
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([]));
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocuments([]));
|
||||
|
||||
assert.deepStrictEqual(await provider.provideWorkspaceSymbols(''), []);
|
||||
});
|
||||
@@ -24,7 +26,7 @@ suite('markdown.WorkspaceSymbolProvider', () => {
|
||||
test('Should return symbols from workspace with one markdown file', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocuments([
|
||||
new InMemoryDocument(testFileName, `# header1\nabc\n## header2`)
|
||||
]));
|
||||
|
||||
@@ -36,13 +38,13 @@ suite('markdown.WorkspaceSymbolProvider', () => {
|
||||
|
||||
test('Should return all content basic workspace', async () => {
|
||||
const fileNameCount = 10;
|
||||
const files: vscode.TextDocument[] = [];
|
||||
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 MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider(files));
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocuments(files));
|
||||
|
||||
const symbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(symbols.length, fileNameCount * 2);
|
||||
@@ -51,11 +53,11 @@ suite('markdown.WorkspaceSymbolProvider', () => {
|
||||
test('Should update results when markdown file changes symbols', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocuments([
|
||||
new InMemoryDocument(testFileName, `# header1`, 1 /* version */)
|
||||
]);
|
||||
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
|
||||
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
|
||||
|
||||
@@ -70,11 +72,11 @@ suite('markdown.WorkspaceSymbolProvider', () => {
|
||||
test('Should remove results when file is deleted', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocuments([
|
||||
new InMemoryDocument(testFileName, `# header1`)
|
||||
]);
|
||||
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
|
||||
|
||||
// delete file
|
||||
@@ -86,11 +88,11 @@ suite('markdown.WorkspaceSymbolProvider', () => {
|
||||
test('Should update results when markdown file is created', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocuments([
|
||||
new InMemoryDocument(testFileName, `# header1`)
|
||||
]);
|
||||
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
const provider = new MdWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
|
||||
|
||||
// Creat file
|
||||
@@ -99,44 +101,3 @@ suite('markdown.WorkspaceSymbolProvider', () => {
|
||||
assert.strictEqual(newSymbols.length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class InMemoryWorkspaceMarkdownDocumentProvider implements WorkspaceMarkdownDocumentProvider {
|
||||
private readonly _documents = new Map<string, vscode.TextDocument>();
|
||||
|
||||
constructor(documents: vscode.TextDocument[]) {
|
||||
for (const doc of documents) {
|
||||
this._documents.set(doc.fileName, doc);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllMarkdownDocuments() {
|
||||
return Array.from(this._documents.values());
|
||||
}
|
||||
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.TextDocument>();
|
||||
public onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocumentEmitter.event;
|
||||
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.TextDocument>();
|
||||
public onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocumentEmitter.event;
|
||||
|
||||
private readonly _onDidDeleteMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.Uri>();
|
||||
public onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocumentEmitter.event;
|
||||
|
||||
public updateDocument(document: vscode.TextDocument) {
|
||||
this._documents.set(document.fileName, document);
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
|
||||
public createDocument(document: vscode.TextDocument) {
|
||||
assert.ok(!this._documents.has(document.uri.fsPath));
|
||||
|
||||
this._documents.set(document.uri.fsPath, document);
|
||||
this._onDidCreateMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
|
||||
public deleteDocument(resource: vscode.Uri) {
|
||||
this._documents.delete(resource.fsPath);
|
||||
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,4 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
|
||||
declare module 'markdown-it-front-matter';
|
||||
|
||||
@@ -16,3 +16,10 @@ export function equals<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, itemEq
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns New array with all falsy values removed. The original array IS NOT modified.
|
||||
*/
|
||||
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
|
||||
return <T[]>array.filter(e => !!e);
|
||||
}
|
||||
|
||||
72
extensions/markdown-language-features/src/util/async.ts
Normal file
72
extensions/markdown-language-features/src/util/async.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vscode';
|
||||
|
||||
export interface ITask<T> {
|
||||
(): T;
|
||||
}
|
||||
|
||||
export class Delayer<T> {
|
||||
|
||||
public defaultDelay: number;
|
||||
private timeout: any; // Timer
|
||||
private completionPromise: Promise<T | null> | null;
|
||||
private onSuccess: ((value: T | PromiseLike<T> | undefined) => void) | null;
|
||||
private task: ITask<T> | null;
|
||||
|
||||
constructor(defaultDelay: number) {
|
||||
this.defaultDelay = defaultDelay;
|
||||
this.timeout = null;
|
||||
this.completionPromise = null;
|
||||
this.onSuccess = null;
|
||||
this.task = null;
|
||||
}
|
||||
|
||||
public trigger(task: ITask<T>, delay: number = this.defaultDelay): Promise<T | null> {
|
||||
this.task = task;
|
||||
if (delay >= 0) {
|
||||
this.cancelTimeout();
|
||||
}
|
||||
|
||||
if (!this.completionPromise) {
|
||||
this.completionPromise = new Promise<T | undefined>((resolve) => {
|
||||
this.onSuccess = resolve;
|
||||
}).then(() => {
|
||||
this.completionPromise = null;
|
||||
this.onSuccess = null;
|
||||
const result = this.task && this.task();
|
||||
this.task = null;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
if (delay >= 0 || this.timeout === null) {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.timeout = null;
|
||||
this.onSuccess?.(undefined);
|
||||
}, delay >= 0 ? delay : this.defaultDelay);
|
||||
}
|
||||
|
||||
return this.completionPromise;
|
||||
}
|
||||
|
||||
private cancelTimeout(): void {
|
||||
if (this.timeout !== null) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable {
|
||||
if (global.setImmediate) {
|
||||
const handle = global.setImmediate(callback, ...args);
|
||||
return { dispose: () => global.clearImmediate(handle) };
|
||||
} else {
|
||||
const handle = setTimeout(callback, 0, ...args);
|
||||
return { dispose: () => clearTimeout(handle) };
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,7 @@ import * as vscode from 'vscode';
|
||||
export function disposeAll(disposables: vscode.Disposable[]) {
|
||||
while (disposables.length) {
|
||||
const item = disposables.pop();
|
||||
if (item) {
|
||||
item.dispose();
|
||||
}
|
||||
item?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,56 +3,14 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Return a hash value for an object.
|
||||
*/
|
||||
export function hash(obj: any, hashVal = 0): number {
|
||||
switch (typeof obj) {
|
||||
case 'object':
|
||||
if (obj === null) {
|
||||
return numberHash(349, hashVal);
|
||||
} else if (Array.isArray(obj)) {
|
||||
return arrayHash(obj, hashVal);
|
||||
}
|
||||
return objectHash(obj, hashVal);
|
||||
case 'string':
|
||||
return stringHash(obj, hashVal);
|
||||
case 'boolean':
|
||||
return booleanHash(obj, hashVal);
|
||||
case 'number':
|
||||
return numberHash(obj, hashVal);
|
||||
case 'undefined':
|
||||
return 937 * 31;
|
||||
default:
|
||||
return numberHash(obj, 617);
|
||||
}
|
||||
}
|
||||
|
||||
function numberHash(val: number, initialHashVal: number): number {
|
||||
return (((initialHashVal << 5) - initialHashVal) + val) | 0; // hashVal * 31 + ch, keep as int32
|
||||
}
|
||||
|
||||
function booleanHash(b: boolean, initialHashVal: number): number {
|
||||
return numberHash(b ? 433 : 863, initialHashVal);
|
||||
}
|
||||
|
||||
function stringHash(s: string, hashVal: number) {
|
||||
hashVal = numberHash(149417, hashVal);
|
||||
export function stringHash(s: string) {
|
||||
let hashVal = numberHash(149417, 0);
|
||||
for (let i = 0, length = s.length; i < length; i++) {
|
||||
hashVal = numberHash(s.charCodeAt(i), hashVal);
|
||||
}
|
||||
return hashVal;
|
||||
}
|
||||
|
||||
function arrayHash(arr: any[], initialHashVal: number): number {
|
||||
initialHashVal = numberHash(104579, initialHashVal);
|
||||
return arr.reduce((hashVal, item) => hash(item, hashVal), initialHashVal);
|
||||
}
|
||||
|
||||
function objectHash(obj: any, initialHashVal: number): number {
|
||||
initialHashVal = numberHash(181387, initialHashVal);
|
||||
return Object.keys(obj).sort().reduce((hashVal, key) => {
|
||||
hashVal = stringHash(key, hashVal);
|
||||
return hash(obj[key], hashVal);
|
||||
}, initialHashVal);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { TextDocument } from 'vscode-languageserver-textdocument';
|
||||
import { SkinnyTextDocument, SkinnyTextLine } from '../workspaceContents';
|
||||
|
||||
export class InMemoryDocument implements SkinnyTextDocument {
|
||||
|
||||
private readonly _doc: TextDocument;
|
||||
|
||||
private lines: SkinnyTextLine[] | undefined;
|
||||
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri, contents: string,
|
||||
public readonly version = 0,
|
||||
) {
|
||||
|
||||
this._doc = TextDocument.create(uri.toString(), 'markdown', version, contents);
|
||||
}
|
||||
|
||||
get lineCount(): number {
|
||||
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);
|
||||
}
|
||||
|
||||
getText(range?: vscode.Range): string {
|
||||
return this._doc.getText(range);
|
||||
}
|
||||
}
|
||||
67
extensions/markdown-language-features/src/util/limiter.ts
Normal file
67
extensions/markdown-language-features/src/util/limiter.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface ILimitedTaskFactory<T> {
|
||||
factory: ITask<Promise<T>>;
|
||||
c: (value: T | Promise<T>) => void;
|
||||
e: (error?: unknown) => void;
|
||||
}
|
||||
|
||||
interface ITask<T> {
|
||||
(): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to queue N promises and run them all with a max degree of parallelism. The helper
|
||||
* ensures that at any time no more than M promises are running at the same time.
|
||||
*
|
||||
* Taken from 'src/vs/base/common/async.ts'
|
||||
*/
|
||||
export class Limiter<T> {
|
||||
|
||||
private _size = 0;
|
||||
private runningPromises: number;
|
||||
private readonly maxDegreeOfParalellism: number;
|
||||
private readonly outstandingPromises: ILimitedTaskFactory<T>[];
|
||||
|
||||
constructor(maxDegreeOfParalellism: number) {
|
||||
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
|
||||
this.outstandingPromises = [];
|
||||
this.runningPromises = 0;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
queue(factory: ITask<Promise<T>>): Promise<T> {
|
||||
this._size++;
|
||||
|
||||
return new Promise<T>((c, e) => {
|
||||
this.outstandingPromises.push({ factory, c, e });
|
||||
this.consume();
|
||||
});
|
||||
}
|
||||
|
||||
private consume(): void {
|
||||
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
|
||||
const iLimitedTask = this.outstandingPromises.shift()!;
|
||||
this.runningPromises++;
|
||||
|
||||
const promise = iLimitedTask.factory();
|
||||
promise.then(iLimitedTask.c, iLimitedTask.e);
|
||||
promise.then(() => this.consumed(), () => this.consumed());
|
||||
}
|
||||
}
|
||||
|
||||
private consumed(): void {
|
||||
this._size--;
|
||||
this.runningPromises--;
|
||||
|
||||
if (this.outstandingPromises.length > 0) {
|
||||
this.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as uri from 'vscode-uri';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContentsProvider } from '../tableOfContentsProvider';
|
||||
import { TableOfContents } from '../tableOfContents';
|
||||
import { isMarkdownFile } from './file';
|
||||
import { extname } from './path';
|
||||
|
||||
export interface OpenDocumentLinkArgs {
|
||||
readonly parts: vscode.Uri;
|
||||
@@ -53,7 +53,7 @@ export async function openDocumentLink(engine: MarkdownEngine, targetResource: v
|
||||
|
||||
if (typeof targetResourceStat === 'undefined') {
|
||||
// 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 (extname(targetResource.path) === '') {
|
||||
if (uri.Utils.extname(targetResource) === '') {
|
||||
const dotMdResource = targetResource.with({ path: targetResource.path + '.md' });
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(dotMdResource);
|
||||
@@ -104,8 +104,8 @@ function getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
|
||||
}
|
||||
|
||||
async function tryRevealLineUsingTocFragment(engine: MarkdownEngine, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
|
||||
const toc = new TableOfContentsProvider(engine, editor.document);
|
||||
const entry = await toc.lookup(fragment);
|
||||
const toc = await TableOfContents.create(engine, editor.document);
|
||||
const entry = toc.lookup(fragment);
|
||||
if (entry) {
|
||||
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
|
||||
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
|
||||
@@ -129,25 +129,25 @@ function tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: str
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function resolveLinkToMarkdownFile(resource: vscode.Uri): Promise<vscode.Uri | undefined> {
|
||||
export async function resolveUriToMarkdownFile(resource: vscode.Uri): Promise<vscode.TextDocument | undefined> {
|
||||
try {
|
||||
const standardLink = await tryResolveLinkToMarkdownFile(resource);
|
||||
if (standardLink) {
|
||||
return standardLink;
|
||||
const doc = await tryResolveUriToMarkdownFile(resource);
|
||||
if (doc) {
|
||||
return doc;
|
||||
}
|
||||
} catch {
|
||||
// Noop
|
||||
}
|
||||
|
||||
// If no extension, try with `.md` extension
|
||||
if (extname(resource.path) === '') {
|
||||
return tryResolveLinkToMarkdownFile(resource.with({ path: resource.path + '.md' }));
|
||||
if (uri.Utils.extname(resource) === '') {
|
||||
return tryResolveUriToMarkdownFile(resource.with({ path: resource.path + '.md' }));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function tryResolveLinkToMarkdownFile(resource: vscode.Uri): Promise<vscode.Uri | undefined> {
|
||||
async function tryResolveUriToMarkdownFile(resource: vscode.Uri): Promise<vscode.TextDocument | undefined> {
|
||||
let document: vscode.TextDocument;
|
||||
try {
|
||||
document = await vscode.workspace.openTextDocument(resource);
|
||||
@@ -155,7 +155,7 @@ async function tryResolveLinkToMarkdownFile(resource: vscode.Uri): Promise<vscod
|
||||
return undefined;
|
||||
}
|
||||
if (isMarkdownFile(document)) {
|
||||
return document.uri;
|
||||
return document;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference types='@types/node'/>
|
||||
|
||||
export { basename, dirname, extname, isAbsolute, join } from 'path';
|
||||
171
extensions/markdown-language-features/src/workspaceContents.ts
Normal file
171
extensions/markdown-language-features/src/workspaceContents.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { coalesce } from './util/arrays';
|
||||
import { Disposable } from './util/dispose';
|
||||
import { isMarkdownFile } 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides set of markdown files in the current workspace.
|
||||
*/
|
||||
export interface MdWorkspaceContents {
|
||||
/**
|
||||
* Get list of all known markdown files.
|
||||
*/
|
||||
getAllMarkdownDocuments(): Promise<Iterable<SkinnyTextDocument>>;
|
||||
|
||||
getMarkdownDocument(resource: vscode.Uri): Promise<SkinnyTextDocument | undefined>;
|
||||
|
||||
pathExists(resource: vscode.Uri): Promise<boolean>;
|
||||
|
||||
readonly onDidChangeMarkdownDocument: vscode.Event<SkinnyTextDocument>;
|
||||
readonly onDidCreateMarkdownDocument: vscode.Event<SkinnyTextDocument>;
|
||||
readonly onDidDeleteMarkdownDocument: vscode.Event<vscode.Uri>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides set of markdown files known to VS Code.
|
||||
*
|
||||
* This includes both opened text documents and markdown files in the workspace.
|
||||
*/
|
||||
export class VsCodeMdWorkspaceContents extends Disposable implements MdWorkspaceContents {
|
||||
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<SkinnyTextDocument>());
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<SkinnyTextDocument>());
|
||||
private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<vscode.Uri>());
|
||||
|
||||
private _watcher: vscode.FileSystemWatcher | undefined;
|
||||
|
||||
private readonly utf8Decoder = new TextDecoder('utf-8');
|
||||
|
||||
/**
|
||||
* Reads and parses all .md documents in the workspace.
|
||||
* Files are processed in batches, to keep the number of open files small.
|
||||
*
|
||||
* @returns Array of processed .md files.
|
||||
*/
|
||||
async getAllMarkdownDocuments(): Promise<SkinnyTextDocument[]> {
|
||||
const maxConcurrent = 20;
|
||||
|
||||
const foundFiles = new Set<string>();
|
||||
const limiter = new Limiter<SkinnyTextDocument | 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);
|
||||
if (doc) {
|
||||
foundFiles.add(doc.uri.toString());
|
||||
}
|
||||
return doc;
|
||||
});
|
||||
}));
|
||||
|
||||
// Add opened files (such as untitled files)
|
||||
const openTextDocumentResults = await Promise.all(vscode.workspace.textDocuments
|
||||
.filter(doc => !foundFiles.has(doc.uri.toString()) && isMarkdownFile(doc)));
|
||||
|
||||
return coalesce([...onDiskResults, ...openTextDocumentResults]);
|
||||
}
|
||||
|
||||
public get onDidChangeMarkdownDocument() {
|
||||
this.ensureWatcher();
|
||||
return this._onDidChangeMarkdownDocumentEmitter.event;
|
||||
}
|
||||
|
||||
public get onDidCreateMarkdownDocument() {
|
||||
this.ensureWatcher();
|
||||
return this._onDidCreateMarkdownDocumentEmitter.event;
|
||||
}
|
||||
|
||||
public get onDidDeleteMarkdownDocument() {
|
||||
this.ensureWatcher();
|
||||
return this._onDidDeleteMarkdownDocumentEmitter.event;
|
||||
}
|
||||
|
||||
private ensureWatcher(): void {
|
||||
if (this._watcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md'));
|
||||
|
||||
this._register(this._watcher.onDidChange(async resource => {
|
||||
const document = await this.getMarkdownDocument(resource);
|
||||
if (document) {
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._watcher.onDidCreate(async resource => {
|
||||
const document = await this.getMarkdownDocument(resource);
|
||||
if (document) {
|
||||
this._onDidCreateMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._watcher.onDidDelete(resource => {
|
||||
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
|
||||
}));
|
||||
|
||||
this._register(vscode.workspace.onDidChangeTextDocument(e => {
|
||||
if (isMarkdownFile(e.document)) {
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(e.document);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public async getMarkdownDocument(resource: vscode.Uri): Promise<SkinnyTextDocument | undefined> {
|
||||
const matchingDocument = vscode.workspace.textDocuments.find((doc) => doc.uri.toString() === resource.toString());
|
||||
if (matchingDocument) {
|
||||
return matchingDocument;
|
||||
}
|
||||
|
||||
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);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async pathExists(target: vscode.Uri): Promise<boolean> {
|
||||
let targetResourceStat: vscode.FileStat | undefined;
|
||||
try {
|
||||
targetResourceStat = await vscode.workspace.fs.stat(target);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return targetResourceStat.type === vscode.FileType.File || targetResourceStat.type === vscode.FileType.Directory;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user