Use MD LS for resolving all document links (#160238)

* Use MD LS for resolving all document links

This switches the markdown extension to use the markdown language service when resolving the link. This lets us delete a lot of code that was duplicated between the extension and the LS

* Pick up new ls version
This commit is contained in:
Matt Bierner 2022-09-07 20:55:14 -07:00 committed by GitHub
parent 0d6bf703ce
commit 2d27f8db6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 84 additions and 980 deletions

View file

@ -13,7 +13,7 @@
"vscode-languageserver": "^8.0.2",
"vscode-languageserver-textdocument": "^1.0.5",
"vscode-languageserver-types": "^3.17.1",
"vscode-markdown-languageservice": "^0.1.0-alpha.1",
"vscode-markdown-languageservice": "^0.1.0-alpha.2",
"vscode-nls": "^5.0.1",
"vscode-uri": "^3.0.3"
},

View file

@ -25,4 +25,6 @@ export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, l
export const getEditForFileRenames = new RequestType<Array<{ oldUri: string; newUri: string }>, lsp.WorkspaceEdit, any>('markdown/getEditForFileRenames');
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
export const resolveLinkTarget = new RequestType<{ linkText: string; uri: string }, md.ResolvedDocumentLinkTarget, any>('markdown/resolveLinkTarget');
//#endregion

View file

@ -207,6 +207,10 @@ export async function startServer(connection: Connection) {
return mdLs!.getRenameFilesInWorkspaceEdit(params.map(x => ({ oldUri: URI.parse(x.oldUri), newUri: URI.parse(x.newUri) })), token);
}));
connection.onRequest(protocol.resolveLinkTarget, (async (params, token: CancellationToken) => {
return mdLs!.resolveLinkTarget(params.linkText, URI.parse(params.uri), token);
}));
documents.listen(connection);
notebooks.listen(connection);
connection.listen();

View file

@ -42,10 +42,10 @@ vscode-languageserver@^8.0.2:
dependencies:
vscode-languageserver-protocol "3.17.2"
vscode-markdown-languageservice@^0.1.0-alpha.1:
version "0.1.0-alpha.1"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.1.0-alpha.1.tgz#60a9b445240eb2f90b5f2cfe203f9cdf1773d674"
integrity sha512-2detAtQRLGdc6MgdQI/8/+Bypa3enw6SA/ia4PCBctwO422kvYjBlyICnqP12ju6DVUNxfLQg5aNqa90xO1H2A==
vscode-markdown-languageservice@^0.1.0-alpha.2:
version "0.1.0-alpha.2"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.1.0-alpha.2.tgz#e74f92e5e0805cf2107af5043911caad01e58d68"
integrity sha512-MKvp1dtZ4ZKNOL8bAvRKWvaayqBw1Ai6JY3zApqFwYGE0sWLrMZZBmFCkyb+boRJ3k55cepkgW5cQNVY13295w==
dependencies:
picomatch "^2.3.1"
vscode-languageserver-textdocument "^1.0.5"

View file

@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export { MoveCursorToPositionCommand } from './moveCursorToPosition';
export { OpenDocumentLinkCommand } from './openDocumentLink';
export { RefreshPreviewCommand } from './refreshPreview';
export { ReloadPlugins } from './reloadPlugins';
export { RenderDocument } from './renderDocument';

View file

@ -1,21 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Command } from '../commandManager';
export class MoveCursorToPositionCommand implements Command {
public readonly id = '_markdown.moveCursorToPosition';
public execute(line: number, character: number) {
if (!vscode.window.activeTextEditor) {
return;
}
const position = new vscode.Position(line, character);
const selection = new vscode.Selection(position, position);
vscode.window.activeTextEditor.revealRange(selection);
vscode.window.activeTextEditor.selection = selection;
}
}

View file

@ -1,67 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Command } from '../commandManager';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { openDocumentLink } from '../util/openDocumentLink';
import { Schemes } from '../util/schemes';
type UriComponents = {
readonly scheme?: string;
readonly path: string;
readonly fragment?: string;
readonly authority?: string;
readonly query?: string;
};
export interface OpenDocumentLinkArgs {
readonly parts: UriComponents;
readonly fragment: string;
readonly fromResource: UriComponents;
}
export class OpenDocumentLinkCommand implements Command {
private static readonly id = '_markdown.openDocumentLink';
public readonly id = OpenDocumentLinkCommand.id;
public static createCommandUri(
fromResource: vscode.Uri,
path: vscode.Uri,
fragment: string,
): vscode.Uri {
const toJson = (uri: vscode.Uri): UriComponents => {
return {
scheme: uri.scheme,
authority: uri.authority,
path: uri.path,
fragment: uri.fragment,
query: uri.query,
};
};
return vscode.Uri.parse(`command:${OpenDocumentLinkCommand.id}?${encodeURIComponent(JSON.stringify(<OpenDocumentLinkArgs>{
parts: toJson(path),
fragment,
fromResource: toJson(fromResource),
}))}`);
}
public constructor(
private readonly tocProvider: MdTableOfContentsProvider,
) { }
public async execute(args: OpenDocumentLinkArgs) {
const fromResource = vscode.Uri.parse('').with(args.fromResource);
const targetResource = reviveUri(args.parts).with({ fragment: args.fragment });
return openDocumentLink(this.tocProvider, targetResource, fromResource);
}
}
function reviveUri(parts: any) {
if (parts.scheme === Schemes.file) {
return vscode.Uri.file(parts.path);
}
return vscode.Uri.parse('').with(parts);
}

View file

@ -29,7 +29,7 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push({
dispose: () => client.stop()
});
activateShared(context, client, workspace, engine, logger, contributions);
activateShared(context, client, engine, logger, contributions);
}
function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {

View file

@ -13,19 +13,17 @@ import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
import { registerUpdateLinksOnRename } from './languageFeatures/linkUpdater';
import { ILogger } from './logging';
import { MarkdownItEngine, MdParsingProvider } from './markdownEngine';
import { MarkdownItEngine } from './markdownEngine';
import { MarkdownContributionProvider } from './markdownExtensions';
import { MdDocumentRenderer } from './preview/documentRenderer';
import { MarkdownPreviewManager } from './preview/previewManager';
import { ContentSecurityPolicyArbiter, ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './preview/security';
import { MdTableOfContentsProvider } from './tableOfContents';
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
import { IMdWorkspace } from './workspace';
import { MdLinkOpener } from './util/openDocumentLink';
export function activateShared(
context: vscode.ExtensionContext,
client: BaseLanguageClient,
workspace: IMdWorkspace,
engine: MarkdownItEngine,
logger: ILogger,
contributions: MarkdownContributionProvider,
@ -36,16 +34,14 @@ export function activateShared(
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
const commandManager = new CommandManager();
const parser = new MdParsingProvider(engine, workspace);
const tocProvider = new MdTableOfContentsProvider(parser, workspace, logger);
context.subscriptions.push(parser, tocProvider);
const opener = new MdLinkOpener(client);
const contentProvider = new MdDocumentRenderer(engine, context, cspArbiter, contributions, logger);
const previewManager = new MarkdownPreviewManager(contentProvider, workspace, logger, contributions, tocProvider);
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions, opener);
context.subscriptions.push(previewManager);
context.subscriptions.push(registerMarkdownLanguageFeatures(client, commandManager));
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine, tocProvider));
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine));
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
previewManager.updateConfiguration();
@ -73,7 +69,6 @@ function registerMarkdownCommands(
telemetryReporter: TelemetryReporter,
cspArbiter: ContentSecurityPolicyArbiter,
engine: MarkdownItEngine,
tocProvider: MdTableOfContentsProvider,
): vscode.Disposable {
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
@ -82,9 +77,7 @@ function registerMarkdownCommands(
commandManager.register(new commands.ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowSourceCommand(previewManager));
commandManager.register(new commands.RefreshPreviewCommand(previewManager, engine));
commandManager.register(new commands.MoveCursorToPositionCommand());
commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
commandManager.register(new commands.OpenDocumentLinkCommand(tocProvider));
commandManager.register(new commands.ToggleLockCommand(previewManager));
commandManager.register(new commands.RenderDocument(engine));
commandManager.register(new commands.ReloadPlugins(previewManager, engine));

View file

@ -29,7 +29,7 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push({
dispose: () => client.stop()
});
activateShared(context, client, workspace, engine, logger, contributions);
activateShared(context, client, engine, logger, contributions);
}
function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {

View file

@ -10,11 +10,8 @@ import { ILogger } from './logging';
import { MarkdownContributionProvider } from './markdownExtensions';
import { Slugifier } from './slugify';
import { ITextDocument } from './types/textDocument';
import { Disposable } from './util/dispose';
import { WebviewResourceProvider } from './util/resources';
import { isOfScheme, Schemes } from './util/schemes';
import { MdDocumentInfoCache } from './util/workspaceCache';
import { IMdWorkspace } from './workspace';
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
@ -434,27 +431,3 @@ function normalizeHighlightLang(lang: string | undefined) {
return lang;
}
}
export class MdParsingProvider extends Disposable implements IMdParser {
private readonly _cache: MdDocumentInfoCache<Token[]>;
public readonly slugifier: Slugifier;
constructor(
engine: MarkdownItEngine,
workspace: IMdWorkspace,
) {
super();
this.slugifier = engine.slugifier;
this._cache = this._register(new MdDocumentInfoCache<Token[]>(workspace, doc => {
return engine.tokenize(doc);
}));
}
public tokenize(document: ITextDocument): Promise<Token[]> {
return this._cache.getForDocument(document);
}
}

View file

@ -8,13 +8,11 @@ import * as nls from 'vscode-nls';
import * as uri from 'vscode-uri';
import { ILogger } from '../logging';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { openDocumentLink, resolveDocumentLink, resolveUriToMarkdownFile } from '../util/openDocumentLink';
import { MdLinkOpener } from '../util/openDocumentLink';
import { WebviewResourceProvider } from '../util/resources';
import { urlToUri } from '../util/url';
import { IMdWorkspace } from '../workspace';
import { MdDocumentRenderer } from './documentRenderer';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { scrollEditorToLine, StartingScrollFragment, StartingScrollLine, StartingScrollLocation } from './scrolling';
@ -119,10 +117,9 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
private readonly delegate: MarkdownPreviewDelegate,
private readonly _contentProvider: MdDocumentRenderer,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _workspace: IMdWorkspace,
private readonly _logger: ILogger,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _tocProvider: MdTableOfContentsProvider,
private readonly _opener: MdLinkOpener,
) {
super();
@ -444,19 +441,23 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
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 linkedDoc = await resolveUriToMarkdownFile(this._workspace, targetResource);
if (linkedDoc) {
this.delegate.openPreviewLinkToMarkdownFile(linkedDoc.uri, targetResource.fragment);
return;
const resolved = await this._opener.resolveDocumentLink(href, this.resource);
if (resolved.kind === 'file') {
try {
const doc = await vscode.workspace.openTextDocument(vscode.Uri.from(resolved.uri));
if (isMarkdownFile(doc)) {
return this.delegate.openPreviewLinkToMarkdownFile(doc.uri, resolved.fragment ?? '');
}
} catch {
// Noop
}
}
}
return openDocumentLink(this._tocProvider, targetResource, this.resource);
return this._opener.openDocumentLink(href, this.resource);
}
//#region WebviewResourceProvider
@ -502,13 +503,12 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
contentProvider: MdDocumentRenderer,
previewConfigurations: MarkdownPreviewConfigurationManager,
topmostLineMonitor: TopmostLineMonitor,
workspace: IMdWorkspace,
logger: ILogger,
contributionProvider: MarkdownContributionProvider,
tocProvider: MdTableOfContentsProvider,
opener: MdLinkOpener,
scrollLine?: number,
): StaticMarkdownPreview {
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, workspace, logger, contributionProvider, tocProvider, scrollLine);
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, opener, scrollLine);
}
private readonly preview: MarkdownPreview;
@ -519,10 +519,9 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
contentProvider: MdDocumentRenderer,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
topmostLineMonitor: TopmostLineMonitor,
workspace: IMdWorkspace,
logger: ILogger,
contributionProvider: MarkdownContributionProvider,
tocProvider: MdTableOfContentsProvider,
opener: MdLinkOpener,
scrollLine?: number,
) {
super();
@ -534,7 +533,7 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
fragment
}), StaticMarkdownPreview.customEditorViewType, this._webviewPanel.viewColumn);
}
}, contentProvider, _previewConfigurations, workspace, logger, contributionProvider, tocProvider));
}, contentProvider, _previewConfigurations, logger, contributionProvider, opener));
this._register(this._webviewPanel.onDidDispose(() => {
this.dispose();
@ -615,16 +614,15 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
webview: vscode.WebviewPanel,
contentProvider: MdDocumentRenderer,
previewConfigurations: MarkdownPreviewConfigurationManager,
workspace: IMdWorkspace,
logger: ILogger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider,
tocProvider: MdTableOfContentsProvider,
opener: MdLinkOpener,
): DynamicMarkdownPreview {
webview.iconPath = contentProvider.iconPath;
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, workspace, logger, topmostLineMonitor, contributionProvider, tocProvider);
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, opener);
}
public static create(
@ -632,11 +630,10 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
previewColumn: vscode.ViewColumn,
contentProvider: MdDocumentRenderer,
previewConfigurations: MarkdownPreviewConfigurationManager,
workspace: IMdWorkspace,
logger: ILogger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider,
tocProvider: MdTableOfContentsProvider,
opener: MdLinkOpener,
): DynamicMarkdownPreview {
const webview = vscode.window.createWebviewPanel(
DynamicMarkdownPreview.viewType,
@ -646,7 +643,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
webview.iconPath = contentProvider.iconPath;
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, workspace, logger, topmostLineMonitor, contributionProvider, tocProvider);
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, opener);
}
private constructor(
@ -654,11 +651,10 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
input: DynamicPreviewInput,
private readonly _contentProvider: MdDocumentRenderer,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _workspace: IMdWorkspace,
private readonly _logger: ILogger,
private readonly _topmostLineMonitor: TopmostLineMonitor,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _tocProvider: MdTableOfContentsProvider,
private readonly _opener: MdLinkOpener,
) {
super();
@ -812,9 +808,8 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
},
this._contentProvider,
this._previewConfigurations,
this._workspace,
this._logger,
this._contributionProvider,
this._tocProvider);
this._opener);
}
}

View file

@ -4,18 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { ILogger } from '../logging';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { Disposable, disposeAll } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { IMdWorkspace } from '../workspace';
import { MdLinkOpener } from '../util/openDocumentLink';
import { MdDocumentRenderer } from './documentRenderer';
import { DynamicMarkdownPreview, IManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { scrollEditorToLine, StartingScrollFragment } from './scrolling';
import { TopmostLineMonitor } from './topmostLineMonitor';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
@ -72,10 +71,9 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
public constructor(
private readonly _contentProvider: MdDocumentRenderer,
private readonly _workspace: IMdWorkspace,
private readonly _logger: ILogger,
private readonly _contributions: MarkdownContributionProvider,
private readonly _tocProvider: MdTableOfContentsProvider,
private readonly _opener: MdLinkOpener,
) {
super();
@ -168,11 +166,10 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
webview,
this._contentProvider,
this._previewConfigurations,
this._workspace,
this._logger,
this._topmostLineMonitor,
this._contributions,
this._tocProvider);
this._opener);
this.registerDynamicPreview(preview);
} catch (e) {
@ -223,10 +220,9 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this._contentProvider,
this._previewConfigurations,
this._topmostLineMonitor,
this._workspace,
this._logger,
this._contributions,
this._tocProvider,
this._opener,
lineNumber
);
this.registerStaticPreview(preview);
@ -248,11 +244,10 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
previewSettings.previewColumn,
this._contentProvider,
this._previewConfigurations,
this._workspace,
this._logger,
this._topmostLineMonitor,
this._contributions,
this._tocProvider);
this._opener);
this._activePreview = preview;
return this.registerDynamicPreview(preview);

View file

@ -4,10 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import type Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { RequestType } from 'vscode-languageclient';
import type * as lsp from 'vscode-languageserver-types';
import type * as md from 'vscode-markdown-languageservice';
export type ResolvedDocumentLinkTarget =
| { readonly kind: 'file'; readonly uri: vscode.Uri; position?: lsp.Position; fragment?: string }
| { readonly kind: 'folder'; readonly uri: vscode.Uri }
| { readonly kind: 'external'; readonly uri: vscode.Uri };
//#region From server
export const parse = new RequestType<{ uri: string }, Token[], any>('markdown/parse');
@ -26,4 +33,6 @@ export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, l
export const getEditForFileRenames = new RequestType<Array<{ oldUri: string; newUri: string }>, lsp.WorkspaceEdit, any>('markdown/getEditForFileRenames');
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
export const resolveLinkTarget = new RequestType<{ linkText: string; uri: string }, ResolvedDocumentLinkTarget, any>('markdown/resolveLinkTarget');
//#endregion

View file

@ -1,213 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ILogger } from './logging';
import { IMdParser } from './markdownEngine';
import { githubSlugifier, Slug, Slugifier } from './slugify';
import { getLine, ITextDocument } from './types/textDocument';
import { Disposable } from './util/dispose';
import { isMarkdownFile } from './util/file';
import { Schemes } from './util/schemes';
import { MdDocumentInfoCache } from './util/workspaceCache';
import { IMdWorkspace } from './workspace';
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(parser: IMdParser, document: ITextDocument,): Promise<TableOfContents> {
const entries = await this.buildToc(parser, document);
return new TableOfContents(entries, parser.slugifier);
}
public static async createForDocumentOrNotebook(parser: IMdParser, document: ITextDocument): Promise<TableOfContents> {
if (document.uri.scheme === Schemes.notebookCell) {
const notebook = vscode.workspace.notebookDocuments
.find(notebook => notebook.getCells().some(cell => cell.document === document));
if (notebook) {
return TableOfContents.createForNotebook(parser, notebook);
}
}
return this.create(parser, document);
}
public static async createForNotebook(parser: IMdParser, notebook: vscode.NotebookDocument): Promise<TableOfContents> {
const entries: TocEntry[] = [];
for (const cell of notebook.getCells()) {
if (cell.kind === vscode.NotebookCellKind.Markup && isMarkdownFile(cell.document)) {
entries.push(...(await this.buildToc(parser, cell.document)));
}
}
return new TableOfContents(entries, parser.slugifier);
}
private static async buildToc(parser: IMdParser, document: ITextDocument): Promise<TocEntry[]> {
const toc: TocEntry[] = [];
const tokens = await parser.tokenize(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 = getLine(document, lineNumber);
let slug = parser.slugifier.fromHeading(line);
const existingSlugEntry = existingSlugEntries.get(slug.value);
if (existingSlugEntry) {
++existingSlugEntry.count;
slug = parser.slugifier.fromHeading(slug.value + '-' + existingSlugEntry.count);
} else {
existingSlugEntries.set(slug.value, { count: 0 });
}
const headerLocation = new vscode.Location(document.uri,
new vscode.Range(lineNumber, 0, lineNumber, line.length));
const headerTextLocation = new vscode.Location(document.uri,
new vscode.Range(lineNumber, line.match(/^#+\s*/)?.[0].length ?? 0, lineNumber, line.length - (line.match(/\s*#*$/)?.[0].length ?? 0)));
toc.push({
slug,
text: TableOfContents.getHeaderText(line),
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, getLine(document, endLine).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());
}
public static readonly empty = new TableOfContents([], githubSlugifier);
private constructor(
public readonly entries: readonly TocEntry[],
private readonly slugifier: Slugifier,
) { }
public lookup(fragment: string): TocEntry | undefined {
const slug = this.slugifier.fromHeading(fragment);
return this.entries.find(entry => entry.slug.equals(slug));
}
}
export class MdTableOfContentsProvider extends Disposable {
private readonly _cache: MdDocumentInfoCache<TableOfContents>;
constructor(
private readonly parser: IMdParser,
workspace: IMdWorkspace,
private readonly logger: ILogger,
) {
super();
this._cache = this._register(new MdDocumentInfoCache<TableOfContents>(workspace, doc => {
this.logger.verbose('TableOfContentsProvider', `create - ${doc.uri}`);
return TableOfContents.create(parser, doc);
}));
}
public async get(resource: vscode.Uri): Promise<TableOfContents> {
return await this._cache.get(resource) ?? TableOfContents.empty;
}
public getForDocument(doc: ITextDocument): Promise<TableOfContents> {
return this._cache.getForDocument(doc);
}
public createForNotebook(notebook: vscode.NotebookDocument): Promise<TableOfContents> {
return TableOfContents.createForNotebook(this.parser, notebook);
}
}

View file

@ -1,83 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as path from 'path';
import * as vscode from 'vscode';
import { ITextDocument } from '../types/textDocument';
import { Disposable } from '../util/dispose';
import { ResourceMap } from '../util/resourceMap';
import { IMdWorkspace } from '../workspace';
export class InMemoryMdWorkspace extends Disposable implements IMdWorkspace {
private readonly _documents = new ResourceMap<ITextDocument>(uri => uri.fsPath);
constructor(documents: ITextDocument[]) {
super();
for (const doc of documents) {
this._documents.set(doc.uri, doc);
}
}
public values() {
return Array.from(this._documents.values());
}
public async getAllMarkdownDocuments() {
return this.values();
}
public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
return this._documents.get(resource);
}
public hasMarkdownDocument(resolvedHrefPath: vscode.Uri): boolean {
return this._documents.has(resolvedHrefPath);
}
public async pathExists(resource: vscode.Uri): Promise<boolean> {
return this._documents.has(resource);
}
public async readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]> {
const files = new Map<string, vscode.FileType>();
const pathPrefix = resource.fsPath + (resource.fsPath.endsWith('/') || resource.fsPath.endsWith('\\') ? '' : path.sep);
for (const doc of this._documents.values()) {
const path = doc.uri.fsPath;
if (path.startsWith(pathPrefix)) {
const parts = path.slice(pathPrefix.length).split(/\/|\\/g);
files.set(parts[0], parts.length > 1 ? vscode.FileType.Directory : vscode.FileType.File);
}
}
return Array.from(files.entries());
}
private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
public onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocumentEmitter.event;
private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
public onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocumentEmitter.event;
private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<vscode.Uri>());
public onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocumentEmitter.event;
public updateDocument(document: ITextDocument) {
this._documents.set(document.uri, document);
this._onDidChangeMarkdownDocumentEmitter.fire(document);
}
public createDocument(document: ITextDocument) {
assert.ok(!this._documents.has(document.uri));
this._documents.set(document.uri, document);
this._onDidCreateMarkdownDocumentEmitter.fire(document);
}
public deleteDocument(resource: vscode.Uri) {
this._documents.delete(resource);
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
}
}

View file

@ -2,33 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as os from 'os';
import * as vscode from 'vscode';
import { DisposableStore } from '../util/dispose';
export const joinLines = (...args: string[]) =>
args.join(os.platform() === 'win32' ? '\r\n' : '\n');
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);
}
export function withStore<R>(fn: (this: Mocha.Context, store: DisposableStore) => Promise<R>) {
return async function (this: Mocha.Context): Promise<R> {
const store = new DisposableStore();
try {
return await fn.call(this, store);
} finally {
store.dispose();
}
};
}

View file

@ -16,10 +16,3 @@ 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);
}

View file

@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
export interface ITask<T> {
(): T;
}
@ -64,13 +62,3 @@ export class Delayer<T> {
}
}
}
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) };
}
}

View file

@ -53,22 +53,3 @@ export abstract class Disposable {
return this._isDisposed;
}
}
export class DisposableStore extends Disposable {
private readonly items = new Set<IDisposable>();
public override dispose() {
super.dispose();
disposeAll(this.items);
this.items.clear();
}
public add<T extends IDisposable>(item: T): T {
if (this.isDisposed) {
console.warn('Adding to disposed store. Item will be leaked');
}
this.items.add(item);
return item;
}
}

View file

@ -1,67 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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();
}
}
}

View file

@ -3,101 +3,47 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import * as uri from 'vscode-uri';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { ITextDocument } from '../types/textDocument';
import { IMdWorkspace } from '../workspace';
import { isMarkdownFile } from './file';
export interface OpenDocumentLinkArgs {
readonly parts: vscode.Uri;
readonly fragment: string;
readonly fromResource: vscode.Uri;
}
import { BaseLanguageClient } from 'vscode-languageclient';
import * as proto from '../protocol';
enum OpenMarkdownLinks {
beside = 'beside',
currentGroup = 'currentGroup',
}
export function resolveDocumentLink(href: string, markdownFile: vscode.Uri): vscode.Uri {
const [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
export class MdLinkOpener {
if (hrefPath[0] === '/') {
// Absolute path. Try to resolve relative to the workspace
const workspace = vscode.workspace.getWorkspaceFolder(markdownFile);
if (workspace) {
return vscode.Uri.joinPath(workspace.uri, hrefPath.slice(1)).with({ fragment });
constructor(
private readonly client: BaseLanguageClient,
) { }
public async resolveDocumentLink(linkText: string, fromResource: vscode.Uri): Promise<proto.ResolvedDocumentLinkTarget> {
return this.client.sendRequest(proto.resolveLinkTarget, { linkText, uri: fromResource.toString() });
}
public async openDocumentLink(linkText: string, fromResource: vscode.Uri, viewColumn?: vscode.ViewColumn): Promise<void> {
const resolved = await this.client.sendRequest(proto.resolveLinkTarget, { linkText, uri: fromResource.toString() });
if (!resolved) {
return;
}
}
// Relative path. Resolve relative to the md file
const dirnameUri = markdownFile.with({ path: path.dirname(markdownFile.path) });
return vscode.Uri.joinPath(dirnameUri, hrefPath).with({ fragment });
}
const uri = vscode.Uri.from(resolved.uri);
switch (resolved.kind) {
case 'external':
return vscode.commands.executeCommand('vscode.open', uri);
export async function openDocumentLink(tocProvider: MdTableOfContentsProvider, targetResource: vscode.Uri, fromResource: vscode.Uri): Promise<void> {
const column = getViewColumn(fromResource);
case 'folder':
return vscode.commands.executeCommand('revealInExplorer', uri);
if (await tryNavigateToFragmentInActiveEditor(tocProvider, targetResource)) {
return;
}
let targetResourceStat: vscode.FileStat | undefined;
try {
targetResourceStat = await vscode.workspace.fs.stat(targetResource);
} catch {
// noop
}
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 (uri.Utils.extname(targetResource) === '') {
const dotMdResource = targetResource.with({ path: targetResource.path + '.md' });
try {
const stat = await vscode.workspace.fs.stat(dotMdResource);
if (stat.type === vscode.FileType.File) {
await tryOpenMdFile(tocProvider, dotMdResource, column);
return;
}
} catch {
// noop
case 'file': {
return vscode.commands.executeCommand('vscode.open', uri, <vscode.TextDocumentShowOptions>{
selection: resolved.position ? new vscode.Range(resolved.position.line, resolved.position.character, resolved.position.line, resolved.position.character) : undefined,
viewColumn: viewColumn ?? getViewColumn(fromResource),
});
}
}
} else if (targetResourceStat.type === vscode.FileType.Directory) {
return vscode.commands.executeCommand('revealInExplorer', targetResource);
}
await tryOpenMdFile(tocProvider, targetResource, column);
}
async function tryOpenMdFile(tocProvider: MdTableOfContentsProvider, resource: vscode.Uri, column: vscode.ViewColumn): Promise<boolean> {
await vscode.commands.executeCommand('vscode.open', resource.with({ fragment: '' }), column);
return tryNavigateToFragmentInActiveEditor(tocProvider, resource);
}
async function tryNavigateToFragmentInActiveEditor(tocProvider: MdTableOfContentsProvider, resource: vscode.Uri): Promise<boolean> {
const notebookEditor = vscode.window.activeNotebookEditor;
if (notebookEditor?.notebook.uri.fsPath === resource.fsPath) {
if (await tryRevealLineInNotebook(tocProvider, notebookEditor, resource.fragment)) {
return true;
}
}
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor?.document.uri.fsPath === resource.fsPath) {
if (isMarkdownFile(activeEditor.document)) {
if (await tryRevealLineUsingTocFragment(tocProvider, activeEditor, resource.fragment)) {
return true;
}
}
tryRevealLineUsingLineFragment(activeEditor, resource.fragment);
return true;
}
return false;
}
function getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
@ -112,64 +58,3 @@ function getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
}
}
async function tryRevealLineInNotebook(tocProvider: MdTableOfContentsProvider, editor: vscode.NotebookEditor, fragment: string): Promise<boolean> {
const toc = await tocProvider.createForNotebook(editor.notebook);
const entry = toc.lookup(fragment);
if (!entry) {
return false;
}
const cell = editor.notebook.getCells().find(cell => cell.document.uri.toString() === entry.sectionLocation.uri.toString());
if (!cell) {
return false;
}
const range = new vscode.NotebookRange(cell.index, cell.index);
editor.selection = range;
editor.revealRange(range);
return true;
}
async function tryRevealLineUsingTocFragment(tocProvider: MdTableOfContentsProvider, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
const toc = await tocProvider.getForDocument(editor.document);
const entry = toc.lookup(fragment);
if (entry) {
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
return true;
}
return false;
}
function tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: string): boolean {
const lineNumberFragment = fragment.match(/^L(\d+)$/i);
if (lineNumberFragment) {
const line = +lineNumberFragment[1] - 1;
if (!isNaN(line)) {
const lineStart = new vscode.Range(line, 0, line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
return true;
}
}
return false;
}
export async function resolveUriToMarkdownFile(workspace: IMdWorkspace, resource: vscode.Uri): Promise<ITextDocument | undefined> {
try {
const doc = await workspace.getOrLoadMarkdownDocument(resource);
if (doc) {
return doc;
}
} catch {
// Noop
}
// If no extension, try with `.md` extension
if (uri.Utils.extname(resource) === '') {
return workspace.getOrLoadMarkdownDocument(resource.with({ path: resource.path + '.md' }));
}
return undefined;
}

View file

@ -1,8 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function isEmptyOrWhitespace(str: string): boolean {
return /^\s*$/.test(str);
}

View file

@ -1,116 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ITextDocument } from '../types/textDocument';
import { IMdWorkspace } from '../workspace';
import { Disposable } from './dispose';
import { Lazy, lazy } from './lazy';
import { ResourceMap } from './resourceMap';
class LazyResourceMap<T> {
private readonly _map = new ResourceMap<Lazy<Promise<T>>>();
public has(resource: vscode.Uri): boolean {
return this._map.has(resource);
}
public get(resource: vscode.Uri): Promise<T> | undefined {
return this._map.get(resource)?.value;
}
public set(resource: vscode.Uri, value: Lazy<Promise<T>>) {
this._map.set(resource, value);
}
public delete(resource: vscode.Uri) {
this._map.delete(resource);
}
public entries(): Promise<Array<[vscode.Uri, T]>> {
return Promise.all(Array.from(this._map.entries(), async ([key, entry]) => {
return [key, await entry.value];
}));
}
}
/**
* Cache of information per-document in the workspace.
*
* The values are computed lazily and invalidated when the document changes.
*/
export class MdDocumentInfoCache<T> extends Disposable {
private readonly _cache = new LazyResourceMap<T>();
private readonly _loadingDocuments = new ResourceMap<Promise<ITextDocument | undefined>>();
public constructor(
private readonly workspace: IMdWorkspace,
private readonly getValue: (document: ITextDocument) => Promise<T>,
) {
super();
this._register(this.workspace.onDidChangeMarkdownDocument(doc => this.invalidate(doc)));
this._register(this.workspace.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
}
public async get(resource: vscode.Uri): Promise<T | undefined> {
let existing = this._cache.get(resource);
if (existing) {
return existing;
}
const doc = await this.loadDocument(resource);
if (!doc) {
return undefined;
}
// Check if we have invalidated
existing = this._cache.get(resource);
if (existing) {
return existing;
}
return this.resetEntry(doc)?.value;
}
public async getForDocument(document: ITextDocument): Promise<T> {
const existing = this._cache.get(document.uri);
if (existing) {
return existing;
}
return this.resetEntry(document).value;
}
private loadDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
const existing = this._loadingDocuments.get(resource);
if (existing) {
return existing;
}
const p = this.workspace.getOrLoadMarkdownDocument(resource);
this._loadingDocuments.set(resource, p);
p.finally(() => {
this._loadingDocuments.delete(resource);
});
return p;
}
private resetEntry(document: ITextDocument): Lazy<Promise<T>> {
const value = lazy(() => this.getValue(document));
this._cache.set(document.uri, value);
return value;
}
private invalidate(document: ITextDocument): void {
if (this._cache.has(document.uri)) {
this.resetEntry(document);
}
}
private onDidDeleteDocument(resource: vscode.Uri) {
this._cache.delete(resource);
}
}

View file

@ -5,36 +5,16 @@
import * as vscode from 'vscode';
import { ITextDocument } from './types/textDocument';
import { coalesce } from './util/arrays';
import { Disposable } from './util/dispose';
import { isMarkdownFile, looksLikeMarkdownPath } from './util/file';
import { InMemoryDocument } from './util/inMemoryDocument';
import { Limiter } from './util/limiter';
import { ResourceMap } from './util/resourceMap';
/**
* Provides set of markdown files in the current workspace.
*/
export interface IMdWorkspace {
/**
* Get list of all known markdown files.
*/
getAllMarkdownDocuments(): Promise<Iterable<ITextDocument>>;
/**
* Check if a document already exists in the workspace contents.
*/
hasMarkdownDocument(resource: vscode.Uri): boolean;
getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined>;
pathExists(resource: vscode.Uri): Promise<boolean>;
readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]>;
readonly onDidChangeMarkdownDocument: vscode.Event<ITextDocument>;
readonly onDidCreateMarkdownDocument: vscode.Event<ITextDocument>;
readonly onDidDeleteMarkdownDocument: vscode.Event<vscode.Uri>;
}
/**
@ -44,100 +24,27 @@ export interface IMdWorkspace {
*/
export class VsCodeMdWorkspace extends Disposable implements IMdWorkspace {
private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<vscode.Uri>());
private _watcher: vscode.FileSystemWatcher | undefined;
private readonly _documentCache = new ResourceMap<ITextDocument>();
private readonly utf8Decoder = new TextDecoder('utf-8');
/**
* 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<ITextDocument[]> {
const maxConcurrent = 20;
const foundFiles = new ResourceMap<void>();
const limiter = new Limiter<ITextDocument | undefined>(maxConcurrent);
// Add files on disk
const resources = await vscode.workspace.findFiles('**/*.md', '**/node_modules/**');
const onDiskResults = await Promise.all(resources.map(resource => {
return limiter.queue(async () => {
const doc = await this.getOrLoadMarkdownDocument(resource);
if (doc) {
foundFiles.set(resource);
}
return doc;
});
}));
// Add opened files (such as untitled files)
const openTextDocumentResults = await Promise.all(vscode.workspace.textDocuments
.filter(doc => !foundFiles.has(doc.uri) && this.isRelevantMarkdownDocument(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;
}
constructor() {
super();
this._watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md'));
this._register(this._watcher.onDidChange(async resource => {
this._documentCache.delete(resource);
const document = await this.getOrLoadMarkdownDocument(resource);
if (document) {
this._onDidChangeMarkdownDocumentEmitter.fire(document);
}
}));
this._register(this._watcher.onDidCreate(async resource => {
const document = await this.getOrLoadMarkdownDocument(resource);
if (document) {
this._onDidCreateMarkdownDocumentEmitter.fire(document);
}
}));
this._register(this._watcher.onDidDelete(resource => {
this._documentCache.delete(resource);
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
}));
this._register(vscode.workspace.onDidOpenTextDocument(e => {
this._documentCache.delete(e.uri);
if (this.isRelevantMarkdownDocument(e)) {
this._onDidCreateMarkdownDocumentEmitter.fire(e);
}
}));
this._register(vscode.workspace.onDidChangeTextDocument(e => {
if (this.isRelevantMarkdownDocument(e.document)) {
this._onDidChangeMarkdownDocumentEmitter.fire(e.document);
}
}));
this._register(vscode.workspace.onDidCloseTextDocument(e => {
@ -177,22 +84,4 @@ export class VsCodeMdWorkspace extends Disposable implements IMdWorkspace {
return undefined;
}
}
public hasMarkdownDocument(resolvedHrefPath: vscode.Uri): boolean {
return this._documentCache.has(resolvedHrefPath);
}
public async pathExists(target: vscode.Uri): Promise<boolean> {
let targetResourceStat: vscode.FileStat | undefined;
try {
targetResourceStat = await vscode.workspace.fs.stat(target);
} catch {
return false;
}
return targetResourceStat.type === vscode.FileType.File || targetResourceStat.type === vscode.FileType.Directory;
}
public async readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]> {
return vscode.workspace.fs.readDirectory(resource);
}
}