mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
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:
parent
0d6bf703ce
commit
2d27f8db6a
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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> {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue