From 2d27f8db6a19f93529787731b0efa8f001478f6d Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 7 Sep 2022 20:55:14 -0700 Subject: [PATCH] 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 --- .../server/package.json | 2 +- .../server/src/protocol.ts | 2 + .../server/src/server.ts | 4 + .../server/yarn.lock | 8 +- .../src/commands/index.ts | 2 - .../src/commands/moveCursorToPosition.ts | 21 -- .../src/commands/openDocumentLink.ts | 67 ------ .../src/extension.browser.ts | 2 +- .../src/extension.shared.ts | 17 +- .../src/extension.ts | 2 +- .../src/markdownEngine.ts | 27 --- .../src/preview/preview.ts | 51 ++--- .../src/preview/previewManager.ts | 17 +- .../src/protocol.ts | 9 + .../src/tableOfContents.ts | 213 ------------------ .../src/test/inMemoryWorkspace.ts | 83 ------- .../src/test/util.ts | 26 --- .../src/util/arrays.ts | 7 - .../src/util/async.ts | 12 - .../src/util/dispose.ts | 19 -- .../src/util/limiter.ts | 67 ------ .../src/util/openDocumentLink.ts | 167 +++----------- .../src/util/string.ts | 8 - .../src/util/workspaceCache.ts | 116 ---------- .../src/workspace.ts | 115 +--------- 25 files changed, 84 insertions(+), 980 deletions(-) delete mode 100644 extensions/markdown-language-features/src/commands/moveCursorToPosition.ts delete mode 100644 extensions/markdown-language-features/src/commands/openDocumentLink.ts delete mode 100644 extensions/markdown-language-features/src/tableOfContents.ts delete mode 100644 extensions/markdown-language-features/src/test/inMemoryWorkspace.ts delete mode 100644 extensions/markdown-language-features/src/util/limiter.ts delete mode 100644 extensions/markdown-language-features/src/util/string.ts delete mode 100644 extensions/markdown-language-features/src/util/workspaceCache.ts diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json index 1e4a7831f8e..a027b548215 100644 --- a/extensions/markdown-language-features/server/package.json +++ b/extensions/markdown-language-features/server/package.json @@ -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" }, diff --git a/extensions/markdown-language-features/server/src/protocol.ts b/extensions/markdown-language-features/server/src/protocol.ts index efb723d5f67..ab407b4afc5 100644 --- a/extensions/markdown-language-features/server/src/protocol.ts +++ b/extensions/markdown-language-features/server/src/protocol.ts @@ -25,4 +25,6 @@ export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, l export const getEditForFileRenames = new RequestType, 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 diff --git a/extensions/markdown-language-features/server/src/server.ts b/extensions/markdown-language-features/server/src/server.ts index bf918945d87..3f67d4117c7 100644 --- a/extensions/markdown-language-features/server/src/server.ts +++ b/extensions/markdown-language-features/server/src/server.ts @@ -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(); diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock index 62275748baa..803846a6d76 100644 --- a/extensions/markdown-language-features/server/yarn.lock +++ b/extensions/markdown-language-features/server/yarn.lock @@ -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" diff --git a/extensions/markdown-language-features/src/commands/index.ts b/extensions/markdown-language-features/src/commands/index.ts index d810fab4931..b93a9d2ed2a 100644 --- a/extensions/markdown-language-features/src/commands/index.ts +++ b/extensions/markdown-language-features/src/commands/index.ts @@ -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'; diff --git a/extensions/markdown-language-features/src/commands/moveCursorToPosition.ts b/extensions/markdown-language-features/src/commands/moveCursorToPosition.ts deleted file mode 100644 index c0d175f370d..00000000000 --- a/extensions/markdown-language-features/src/commands/moveCursorToPosition.ts +++ /dev/null @@ -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; - } -} diff --git a/extensions/markdown-language-features/src/commands/openDocumentLink.ts b/extensions/markdown-language-features/src/commands/openDocumentLink.ts deleted file mode 100644 index 1b061c9c4e4..00000000000 --- a/extensions/markdown-language-features/src/commands/openDocumentLink.ts +++ /dev/null @@ -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({ - 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); -} diff --git a/extensions/markdown-language-features/src/extension.browser.ts b/extensions/markdown-language-features/src/extension.browser.ts index 456a3811e45..f1c860fbc86 100644 --- a/extensions/markdown-language-features/src/extension.browser.ts +++ b/extensions/markdown-language-features/src/extension.browser.ts @@ -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 { diff --git a/extensions/markdown-language-features/src/extension.shared.ts b/extensions/markdown-language-features/src/extension.shared.ts index 5bdcbc2214a..2ce5a5010e1 100644 --- a/extensions/markdown-language-features/src/extension.shared.ts +++ b/extensions/markdown-language-features/src/extension.shared.ts @@ -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)); diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 9f68ef2c1f2..5d8cba9200b 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -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 { diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 9f7142fc91a..de06df4ffeb 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -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; - - public readonly slugifier: Slugifier; - - constructor( - engine: MarkdownItEngine, - workspace: IMdWorkspace, - ) { - super(); - - this.slugifier = engine.slugifier; - - this._cache = this._register(new MdDocumentInfoCache(workspace, doc => { - return engine.tokenize(doc); - })); - } - - public tokenize(document: ITextDocument): Promise { - return this._cache.getForDocument(document); - } -} diff --git a/extensions/markdown-language-features/src/preview/preview.ts b/extensions/markdown-language-features/src/preview/preview.ts index 6ce2b644f1c..b4f4cb7f3b2 100644 --- a/extensions/markdown-language-features/src/preview/preview.ts +++ b/extensions/markdown-language-features/src/preview/preview.ts @@ -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('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); } } diff --git a/extensions/markdown-language-features/src/preview/previewManager.ts b/extensions/markdown-language-features/src/preview/previewManager.ts index ca20542577c..50196ac882e 100644 --- a/extensions/markdown-language-features/src/preview/previewManager.ts +++ b/extensions/markdown-language-features/src/preview/previewManager.ts @@ -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); diff --git a/extensions/markdown-language-features/src/protocol.ts b/extensions/markdown-language-features/src/protocol.ts index 61a13a8bd88..92ec2c0e80a 100644 --- a/extensions/markdown-language-features/src/protocol.ts +++ b/extensions/markdown-language-features/src/protocol.ts @@ -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, 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 diff --git a/extensions/markdown-language-features/src/tableOfContents.ts b/extensions/markdown-language-features/src/tableOfContents.ts deleted file mode 100644 index 5e0a0cc3684..00000000000 --- a/extensions/markdown-language-features/src/tableOfContents.ts +++ /dev/null @@ -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 { - const entries = await this.buildToc(parser, document); - return new TableOfContents(entries, parser.slugifier); - } - - public static async createForDocumentOrNotebook(parser: IMdParser, document: ITextDocument): Promise { - 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 { - 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 { - const toc: TocEntry[] = []; - const tokens = await parser.tokenize(document); - - const existingSlugEntries = new Map(); - - 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; - - constructor( - private readonly parser: IMdParser, - workspace: IMdWorkspace, - private readonly logger: ILogger, - ) { - super(); - this._cache = this._register(new MdDocumentInfoCache(workspace, doc => { - this.logger.verbose('TableOfContentsProvider', `create - ${doc.uri}`); - return TableOfContents.create(parser, doc); - })); - } - - public async get(resource: vscode.Uri): Promise { - return await this._cache.get(resource) ?? TableOfContents.empty; - } - - public getForDocument(doc: ITextDocument): Promise { - return this._cache.getForDocument(doc); - } - - public createForNotebook(notebook: vscode.NotebookDocument): Promise { - return TableOfContents.createForNotebook(this.parser, notebook); - } -} diff --git a/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts b/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts deleted file mode 100644 index a383a73335a..00000000000 --- a/extensions/markdown-language-features/src/test/inMemoryWorkspace.ts +++ /dev/null @@ -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(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 { - return this._documents.get(resource); - } - - public hasMarkdownDocument(resolvedHrefPath: vscode.Uri): boolean { - return this._documents.has(resolvedHrefPath); - } - - public async pathExists(resource: vscode.Uri): Promise { - return this._documents.has(resource); - } - - public async readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]> { - const files = new Map(); - 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()); - public onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocumentEmitter.event; - - private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter()); - public onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocumentEmitter.event; - - private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter()); - 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); - } -} diff --git a/extensions/markdown-language-features/src/test/util.ts b/extensions/markdown-language-features/src/test/util.ts index 220e79e2f60..d50e9ca5db2 100644 --- a/extensions/markdown-language-features/src/test/util.ts +++ b/extensions/markdown-language-features/src/test/util.ts @@ -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(fn: (this: Mocha.Context, store: DisposableStore) => Promise) { - return async function (this: Mocha.Context): Promise { - const store = new DisposableStore(); - try { - return await fn.call(this, store); - } finally { - store.dispose(); - } - }; -} diff --git a/extensions/markdown-language-features/src/util/arrays.ts b/extensions/markdown-language-features/src/util/arrays.ts index 9e65508178e..10599259901 100644 --- a/extensions/markdown-language-features/src/util/arrays.ts +++ b/extensions/markdown-language-features/src/util/arrays.ts @@ -16,10 +16,3 @@ export function equals(one: ReadonlyArray, other: ReadonlyArray, itemEq return true; } - -/** - * @returns New array with all falsy values removed. The original array IS NOT modified. - */ -export function coalesce(array: ReadonlyArray): T[] { - return array.filter(e => !!e); -} diff --git a/extensions/markdown-language-features/src/util/async.ts b/extensions/markdown-language-features/src/util/async.ts index a3bca46d63c..e0de21edca6 100644 --- a/extensions/markdown-language-features/src/util/async.ts +++ b/extensions/markdown-language-features/src/util/async.ts @@ -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; } @@ -64,13 +62,3 @@ export class Delayer { } } } - -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) }; - } -} diff --git a/extensions/markdown-language-features/src/util/dispose.ts b/extensions/markdown-language-features/src/util/dispose.ts index 483fab30638..f222642908e 100644 --- a/extensions/markdown-language-features/src/util/dispose.ts +++ b/extensions/markdown-language-features/src/util/dispose.ts @@ -53,22 +53,3 @@ export abstract class Disposable { return this._isDisposed; } } - -export class DisposableStore extends Disposable { - private readonly items = new Set(); - - public override dispose() { - super.dispose(); - disposeAll(this.items); - this.items.clear(); - } - - public add(item: T): T { - if (this.isDisposed) { - console.warn('Adding to disposed store. Item will be leaked'); - } - - this.items.add(item); - return item; - } -} diff --git a/extensions/markdown-language-features/src/util/limiter.ts b/extensions/markdown-language-features/src/util/limiter.ts deleted file mode 100644 index bd4153cd08b..00000000000 --- a/extensions/markdown-language-features/src/util/limiter.ts +++ /dev/null @@ -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 { - factory: ITask>; - c: (value: T | Promise) => void; - e: (error?: unknown) => void; -} - -interface ITask { - (): 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 { - - private _size = 0; - private runningPromises: number; - private readonly maxDegreeOfParalellism: number; - private readonly outstandingPromises: ILimitedTaskFactory[]; - - constructor(maxDegreeOfParalellism: number) { - this.maxDegreeOfParalellism = maxDegreeOfParalellism; - this.outstandingPromises = []; - this.runningPromises = 0; - } - - get size(): number { - return this._size; - } - - queue(factory: ITask>): Promise { - this._size++; - - return new Promise((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(); - } - } -} diff --git a/extensions/markdown-language-features/src/util/openDocumentLink.ts b/extensions/markdown-language-features/src/util/openDocumentLink.ts index d117aa15c3e..4e5ece8f328 100644 --- a/extensions/markdown-language-features/src/util/openDocumentLink.ts +++ b/extensions/markdown-language-features/src/util/openDocumentLink.ts @@ -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 { + return this.client.sendRequest(proto.resolveLinkTarget, { linkText, uri: fromResource.toString() }); + } + + public async openDocumentLink(linkText: string, fromResource: vscode.Uri, viewColumn?: vscode.ViewColumn): Promise { + 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 { - 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, { + 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 { - await vscode.commands.executeCommand('vscode.open', resource.with({ fragment: '' }), column); - return tryNavigateToFragmentInActiveEditor(tocProvider, resource); -} - -async function tryNavigateToFragmentInActiveEditor(tocProvider: MdTableOfContentsProvider, resource: vscode.Uri): Promise { - 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 { - 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 { - 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 { - 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; -} diff --git a/extensions/markdown-language-features/src/util/string.ts b/extensions/markdown-language-features/src/util/string.ts deleted file mode 100644 index dd9733e9ffd..00000000000 --- a/extensions/markdown-language-features/src/util/string.ts +++ /dev/null @@ -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); -} diff --git a/extensions/markdown-language-features/src/util/workspaceCache.ts b/extensions/markdown-language-features/src/util/workspaceCache.ts deleted file mode 100644 index 2569dbee2b4..00000000000 --- a/extensions/markdown-language-features/src/util/workspaceCache.ts +++ /dev/null @@ -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 { - private readonly _map = new ResourceMap>>(); - - public has(resource: vscode.Uri): boolean { - return this._map.has(resource); - } - - public get(resource: vscode.Uri): Promise | undefined { - return this._map.get(resource)?.value; - } - - public set(resource: vscode.Uri, value: Lazy>) { - this._map.set(resource, value); - } - - public delete(resource: vscode.Uri) { - this._map.delete(resource); - } - - public entries(): Promise> { - 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 extends Disposable { - - private readonly _cache = new LazyResourceMap(); - private readonly _loadingDocuments = new ResourceMap>(); - - public constructor( - private readonly workspace: IMdWorkspace, - private readonly getValue: (document: ITextDocument) => Promise, - ) { - 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 { - 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 { - const existing = this._cache.get(document.uri); - if (existing) { - return existing; - } - return this.resetEntry(document).value; - } - - private loadDocument(resource: vscode.Uri): Promise { - 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> { - 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); - } -} diff --git a/extensions/markdown-language-features/src/workspace.ts b/extensions/markdown-language-features/src/workspace.ts index 6d89b79e35d..c74a487c540 100644 --- a/extensions/markdown-language-features/src/workspace.ts +++ b/extensions/markdown-language-features/src/workspace.ts @@ -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>; - - /** - * Check if a document already exists in the workspace contents. - */ - hasMarkdownDocument(resource: vscode.Uri): boolean; - getOrLoadMarkdownDocument(resource: vscode.Uri): Promise; - - pathExists(resource: vscode.Uri): Promise; - - readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]>; - - readonly onDidChangeMarkdownDocument: vscode.Event; - readonly onDidCreateMarkdownDocument: vscode.Event; - readonly onDidDeleteMarkdownDocument: vscode.Event; } /** @@ -44,100 +24,27 @@ export interface IMdWorkspace { */ export class VsCodeMdWorkspace extends Disposable implements IMdWorkspace { - private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter()); - private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter()); - private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter()); - private _watcher: vscode.FileSystemWatcher | undefined; private readonly _documentCache = new ResourceMap(); 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 { - const maxConcurrent = 20; - - const foundFiles = new ResourceMap(); - const limiter = new Limiter(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 { - 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); - } }