Working on supporting markdown links in serverless+web

For #101203
This commit is contained in:
Matt Bierner 2020-07-09 19:46:16 -07:00
parent 362d345448
commit fdf23dc5f9
12 changed files with 159 additions and 69 deletions

View file

@ -13,9 +13,9 @@ import { isMarkdownFile } from '../util/file';
export interface OpenDocumentLinkArgs {
readonly path: string;
readonly path: {};
readonly fragment: string;
readonly fromResource: any;
readonly fromResource: {};
}
enum OpenMarkdownLinks {
@ -29,13 +29,13 @@ export class OpenDocumentLinkCommand implements Command {
public static createCommandUri(
fromResource: vscode.Uri,
path: string,
path: vscode.Uri,
fragment: string,
): vscode.Uri {
return vscode.Uri.parse(`command:${OpenDocumentLinkCommand.id}?${encodeURIComponent(JSON.stringify(<OpenDocumentLinkArgs>{
path: encodeURIComponent(path),
path: path.toJSON(),
fragment,
fromResource: encodeURIComponent(fromResource.toString(true)),
fromResource: fromResource.toJSON(),
}))}`);
}
@ -44,25 +44,28 @@ export class OpenDocumentLinkCommand implements Command {
) { }
public async execute(args: OpenDocumentLinkArgs) {
const fromResource = vscode.Uri.parse(decodeURIComponent(args.fromResource));
const targetPath = decodeURIComponent(args.path);
const targetResource = vscode.Uri.file(targetPath);
return OpenDocumentLinkCommand.execute(this.engine, args);
}
public static async execute(engine: MarkdownEngine, args: OpenDocumentLinkArgs) {
const fromResource = vscode.Uri.parse('').with(args.fromResource);
const targetResource = vscode.Uri.parse('').with(args.path);
const column = this.getViewColumn(fromResource);
try {
return await this.tryOpen(targetResource, args, column);
return await this.tryOpen(engine, targetResource, args, column);
} catch {
if (extname(targetResource.path) === '') {
return this.tryOpen(targetResource.with({ path: targetResource.path + '.md' }), args, column);
return this.tryOpen(engine, targetResource.with({ path: targetResource.path + '.md' }), args, column);
}
await vscode.commands.executeCommand('vscode.open', targetResource, column);
return undefined;
}
}
private async tryOpen(resource: vscode.Uri, args: OpenDocumentLinkArgs, column: vscode.ViewColumn) {
private static async tryOpen(engine: MarkdownEngine, resource: vscode.Uri, args: OpenDocumentLinkArgs, column: vscode.ViewColumn) {
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
if (vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) {
return this.tryRevealLine(vscode.window.activeTextEditor, args.fragment);
return this.tryRevealLine(engine, vscode.window.activeTextEditor, args.fragment);
}
}
@ -73,10 +76,10 @@ export class OpenDocumentLinkCommand implements Command {
return vscode.workspace.openTextDocument(resource)
.then(document => vscode.window.showTextDocument(document, column))
.then(editor => this.tryRevealLine(editor, args.fragment));
.then(editor => this.tryRevealLine(engine, editor, args.fragment));
}
private getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
private static getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
const config = vscode.workspace.getConfiguration('markdown', resource);
const openLinks = config.get<OpenMarkdownLinks>('links.openLocation', OpenMarkdownLinks.currentGroup);
switch (openLinks) {
@ -88,18 +91,22 @@ export class OpenDocumentLinkCommand implements Command {
}
}
private async tryRevealLine(editor: vscode.TextEditor, fragment?: string) {
private static async tryRevealLine(engine: MarkdownEngine, editor: vscode.TextEditor, fragment?: string) {
if (editor && fragment) {
const toc = new TableOfContentsProvider(this.engine, editor.document);
const toc = new TableOfContentsProvider(engine, editor.document);
const entry = await toc.lookup(fragment);
if (entry) {
return editor.revealRange(new vscode.Range(entry.line, 0, entry.line, 0), vscode.TextEditorRevealType.AtTop);
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
return editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
}
const lineNumberFragment = fragment.match(/^L(\d+)$/i);
if (lineNumberFragment) {
const line = +lineNumberFragment[1] - 1;
if (!isNaN(line)) {
return editor.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.AtTop);
const lineStart = new vscode.Range(line, 0, line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
return editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
}
}
}

View file

@ -15,9 +15,9 @@ import MarkdownWorkspaceSymbolProvider from './features/workspaceSymbolProvider'
import { Logger } from './logger';
import { MarkdownEngine } from './markdownEngine';
import { getMarkdownExtensionContributions } from './markdownExtensions';
import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector, ContentSecurityPolicyArbiter } from './security';
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
import { ContentSecurityPolicyArbiter, ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security';
import { githubSlugifier } from './slugify';
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
export function activate(context: vscode.ExtensionContext) {
@ -33,7 +33,7 @@ export function activate(context: vscode.ExtensionContext) {
const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, contributions, logger);
const symbolProvider = new MDDocumentSymbolProvider(engine);
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions);
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions, engine);
context.subscriptions.push(previewManager);
context.subscriptions.push(registerMarkdownLanguageFeatures(symbolProvider, engine));

View file

@ -14,8 +14,7 @@ const localize = nls.loadMessageBundle();
function parseLink(
document: vscode.TextDocument,
link: string,
base: string
): { uri: vscode.Uri, tooltip?: string } {
): { uri: vscode.Uri, tooltip?: string } | undefined {
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(link);
if (externalSchemeUri) {
// Normalize VS Code links to target currently running version
@ -29,24 +28,43 @@ function parseLink(
// Use a fake scheme to avoid parse warnings
const tempUri = vscode.Uri.parse(`vscode-resource:${link}`);
let resourcePath = tempUri.path;
if (!tempUri.path && document.uri.scheme === 'file') {
resourcePath = document.uri.path;
let resourceUri: vscode.Uri | undefined;
if (!tempUri.path) {
resourceUri = document.uri;
} else if (tempUri.path[0] === '/') {
const root = vscode.workspace.getWorkspaceFolder(document.uri);
const root = getWorkspaceFolder(document);
if (root) {
resourcePath = path.join(root.uri.fsPath, tempUri.path);
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
}
} else {
resourcePath = base ? path.join(base, tempUri.path) : tempUri.path;
if (document.uri.scheme === Schemes.untitled) {
const root = getWorkspaceFolder(document);
if (root) {
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
}
} else {
const base = document.uri.with({ path: path.dirname(document.uri.fsPath) });
resourceUri = vscode.Uri.joinPath(base, tempUri.path);
}
}
if (!resourceUri) {
return undefined;
}
resourceUri = resourceUri.with({ fragment: tempUri.fragment });
return {
uri: OpenDocumentLinkCommand.createCommandUri(document.uri, resourcePath, tempUri.fragment),
uri: OpenDocumentLinkCommand.createCommandUri(document.uri, resourceUri, tempUri.fragment),
tooltip: localize('documentLink.tooltip', 'Follow link')
};
}
function getWorkspaceFolder(document: vscode.TextDocument) {
return vscode.workspace.getWorkspaceFolder(document.uri)?.uri
|| vscode.workspace.workspaceFolders?.[0]?.uri;
}
function matchAll(
pattern: RegExp,
text: string
@ -62,7 +80,6 @@ function matchAll(
function extractDocumentLink(
document: vscode.TextDocument,
base: string,
pre: number,
link: string,
matchIndex: number | undefined
@ -71,11 +88,14 @@ function extractDocumentLink(
const linkStart = document.positionAt(offset);
const linkEnd = document.positionAt(offset + link.length);
try {
const { uri, tooltip } = parseLink(document, link, base);
const linkData = parseLink(document, link);
if (!linkData) {
return undefined;
}
const documentLink = new vscode.DocumentLink(
new vscode.Range(linkStart, linkEnd),
uri);
documentLink.tooltip = tooltip;
linkData.uri);
documentLink.tooltip = linkData.tooltip;
return documentLink;
} catch (e) {
return undefined;
@ -91,27 +111,25 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
document: vscode.TextDocument,
_token: vscode.CancellationToken
): vscode.DocumentLink[] {
const base = document.uri.scheme === 'file' ? path.dirname(document.uri.fsPath) : '';
const text = document.getText();
return [
...this.providerInlineLinks(text, document, base),
...this.provideReferenceLinks(text, document, base)
...this.providerInlineLinks(text, document),
...this.provideReferenceLinks(text, document)
];
}
private providerInlineLinks(
text: string,
document: vscode.TextDocument,
base: string
): vscode.DocumentLink[] {
const results: vscode.DocumentLink[] = [];
for (const match of matchAll(this.linkPattern, text)) {
const matchImage = match[4] && extractDocumentLink(document, base, match[3].length + 1, match[4], match.index);
const matchImage = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index);
if (matchImage) {
results.push(matchImage);
}
const matchLink = extractDocumentLink(document, base, match[1].length, match[5], match.index);
const matchLink = extractDocumentLink(document, match[1].length, match[5], match.index);
if (matchLink) {
results.push(matchLink);
}
@ -122,7 +140,6 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
private provideReferenceLinks(
text: string,
document: vscode.TextDocument,
base: string
): vscode.DocumentLink[] {
const results: vscode.DocumentLink[] = [];
@ -159,8 +176,10 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
for (const definition of definitions.values()) {
try {
const { uri } = parseLink(document, definition.link, base);
results.push(new vscode.DocumentLink(definition.linkRange, uri));
const linkData = parseLink(document, definition.link);
if (linkData) {
results.push(new vscode.DocumentLink(definition.linkRange, linkData.uri));
}
} catch (e) {
// noop
}

View file

@ -3,20 +3,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import { Logger } from '../logger';
import { MarkdownContentProvider } from './previewContentProvider';
import { Disposable } from '../util/dispose';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { OpenDocumentLinkCommand, resolveLinkToMarkdownFile } from '../commands/openDocumentLink';
import { Logger } from '../logger';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { normalizeResource, WebviewResourceProvider } from '../util/resources';
import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { isMarkdownFile } from '../util/file';
import { resolveLinkToMarkdownFile } from '../commands/openDocumentLink';
import { WebviewResourceProvider, normalizeResource } from '../util/resources';
import { MarkdownContentProvider } from './previewContentProvider';
import { MarkdownEngine } from '../markdownEngine';
const localize = nls.loadMessageBundle();
interface WebviewMessage {
@ -123,6 +123,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
resource: vscode.Uri,
startingScroll: StartingScrollLocation | undefined,
private readonly delegate: MarkdownPreviewDelegate,
private readonly engine: MarkdownEngine,
private readonly _contentProvider: MarkdownContentProvider,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _logger: Logger,
@ -407,7 +408,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
}
vscode.commands.executeCommand('_markdown.openDocumentLink', { path: hrefPath, fragment, fromResource: this.resource });
OpenDocumentLinkCommand.execute(this.engine, { path: hrefPath, fragment, fromResource: this.resource.toJSON() });
}
//#region WebviewResourceProvider
@ -452,8 +453,9 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
): StaticMarkdownPreview {
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider);
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider, engine);
}
private readonly preview: MarkdownPreview;
@ -465,13 +467,14 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
) {
super();
this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, undefined, {
getAdditionalState: () => { return {}; },
openPreviewLinkToMarkdownFile: () => { /* todo */ }
}, contentProvider, _previewConfigurations, logger, contributionProvider));
}, engine, contentProvider, _previewConfigurations, logger, contributionProvider));
this._register(this._webviewPanel.onDidDispose(() => {
this.dispose();
@ -548,9 +551,10 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
logger: Logger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
): DynamicMarkdownPreview {
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider);
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
}
public static create(
@ -560,7 +564,8 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
): DynamicMarkdownPreview {
const webview = vscode.window.createWebviewPanel(
DynamicMarkdownPreview.viewType,
@ -568,7 +573,7 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
previewColumn, { enableFindWidget: true, });
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider);
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
}
private constructor(
@ -579,6 +584,7 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
private readonly _logger: Logger,
private readonly _topmostLineMonitor: TopmostLineMonitor,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _engine: MarkdownEngine,
) {
super();
@ -729,6 +735,7 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
this.update(link, fragment ? new StartingScrollFragment(fragment) : undefined);
}
},
this._engine,
this._contentProvider,
this._previewConfigurations,
this._logger,

View file

@ -5,10 +5,11 @@
import * as vscode from 'vscode';
import { Logger } from '../logger';
import { MarkdownEngine } from '../markdownEngine';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { disposeAll, Disposable } from '../util/dispose';
import { Disposable, disposeAll } from '../util/dispose';
import { TopmostLineMonitor } from '../util/topmostLineMonitor';
import { DynamicMarkdownPreview, StaticMarkdownPreview, ManagedMarkdownPreview } from './preview';
import { DynamicMarkdownPreview, ManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContentProvider } from './previewContentProvider';
@ -68,7 +69,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
public constructor(
private readonly _contentProvider: MarkdownContentProvider,
private readonly _logger: Logger,
private readonly _contributions: MarkdownContributionProvider
private readonly _contributions: MarkdownContributionProvider,
private readonly _engine: MarkdownEngine,
) {
super();
this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this));
@ -145,7 +147,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this._previewConfigurations,
this._logger,
this._topmostLineMonitor,
this._contributions);
this._contributions,
this._engine);
this.registerDynamicPreview(preview);
}
@ -160,7 +163,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this._contentProvider,
this._previewConfigurations,
this._logger,
this._contributions);
this._contributions,
this._engine);
this.registerStaticPreview(preview);
}
@ -179,7 +183,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this._previewConfigurations,
this._logger,
this._topmostLineMonitor,
this._contributions);
this._contributions,
this._engine);
this.setPreviewActiveContext(true);
this._activePreview = preview;

View file

@ -70,7 +70,7 @@ export namespace MarkdownContributions {
const previewStyles = getContributedStyles(contributions, extension);
const previewScripts = getContributedScripts(contributions, extension);
const previewResourceRoots = previewStyles.length || previewScripts.length ? [vscode.Uri.file(extension.extensionPath)] : [];
const previewResourceRoots = previewStyles.length || previewScripts.length ? [extension.extensionUri] : [];
const markdownItPlugins = getContributedMarkdownItPlugins(contributions, extension);
return {

View file

@ -6,6 +6,7 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { joinLines } from './util';
const testFileA = workspaceFile('a.md');
@ -75,7 +76,7 @@ suite('Markdown Document links', () => {
await executeLink(link);
assertActiveDocumentUri(workspaceFile('sub', 'c.md'));
assert.strictEqual( vscode.window.activeTextEditor!.selection.start.line, 1);
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
});
test('Should navigate to fragment by line', async () => {
@ -85,7 +86,39 @@ suite('Markdown Document links', () => {
await executeLink(link);
assertActiveDocumentUri(workspaceFile('sub', 'c.md'));
assert.strictEqual( vscode.window.activeTextEditor!.selection.start.line, 1);
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
});
test('Should navigate to fragment within current file', async () => {
await withFileContents(testFileA, joinLines(
'[](a#header)',
'[](#header)',
'# Header'));
const links = await getLinksForFile(testFileA);
{
await executeLink(links[0]);
assertActiveDocumentUri(workspaceFile('a.md'));
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 2);
}
{
await executeLink(links[1]);
assertActiveDocumentUri(workspaceFile('a.md'));
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 2);
}
});
test('Should navigate to fragment within current untitled file', async () => {
const testFile = workspaceFile('x.md').with({ scheme: 'untitled' });
await withFileContents(testFile, joinLines(
'[](#second)',
'# Second'));
const [link] = await getLinksForFile(testFile);
await executeLink(link);
assertActiveDocumentUri(testFile);
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
});
});

View file

@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
export const joinLines = (...args: string[]) =>
args.join(os.platform() === 'win32' ? '\r\n' : '\n');

View file

@ -9,6 +9,7 @@ export const Schemes = {
http: 'http:',
https: 'https:',
file: 'file:',
untitled: 'untitled',
mailto: 'mailto:',
data: 'data:',
vscode: 'vscode:',

View file

@ -0,0 +1,4 @@
[b](b)
[b](b.md)
[b](./b.md)
[b](/b.md)

View file

@ -1 +1,3 @@
![](./a)
# b
[](./a)

View file

@ -1,2 +1,6 @@
# First
# Second
[b](/b.md)
[b](../b.md)
[b](./../b.md)