Rename markdown to markdown-language-features

This commit is contained in:
Matt Bierner
2018-03-19 21:08:39 -07:00
parent 17faeb913d
commit 468dc867cd
66 changed files with 26 additions and 26 deletions

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* 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';
export interface Command {
readonly id: string;
execute(...args: any[]): void;
}
export class CommandManager {
private readonly commands = new Map<string, vscode.Disposable>();
public dispose() {
for (const registration of this.commands.values()) {
registration.dispose();
}
this.commands.clear();
}
public register<T extends Command>(command: T): T {
this.registerCommand(command.id, command.execute, command);
return command;
}
private registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) {
if (this.commands.has(id)) {
return;
}
this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg));
}
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export { OpenDocumentLinkCommand } from './openDocumentLink';
export { OnPreviewStyleLoadErrorCommand } from './onPreviewStyleLoadError';
export { ShowPreviewCommand, ShowPreviewToSideCommand, ShowLockedPreviewToSideCommand } from './showPreview';
export { ShowSourceCommand } from './showSource';
export { RefreshPreviewCommand } from './refreshPreview';
export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
export { MoveCursorToPositionCommand } from './moveCursorToPosition';
export { ToggleLockCommand } from './toggleLock';

View File

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

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as vscode from 'vscode';
import { Command } from '../commandManager';
export class OnPreviewStyleLoadErrorCommand implements Command {
public readonly id = '_markdown.onPreviewStyleLoadError';
public execute(resources: string[]) {
vscode.window.showWarningMessage(localize('onPreviewStyleLoadError', "Could not load 'markdown.styles': {0}", resources.join(', ')));
}
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import { Command } from '../commandManager';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
import { isMarkdownFile } from '../util/file';
export interface OpenDocumentLinkArgs {
path: string;
fragment: string;
}
export class OpenDocumentLinkCommand implements Command {
private static readonly id = '_markdown.openDocumentLink';
public readonly id = OpenDocumentLinkCommand.id;
public static createCommandUri(
path: string,
fragment: string
): vscode.Uri {
return vscode.Uri.parse(`command:${OpenDocumentLinkCommand.id}?${encodeURIComponent(JSON.stringify({ path, fragment }))}`);
}
public constructor(
private readonly engine: MarkdownEngine
) { }
public execute(args: OpenDocumentLinkArgs) {
const p = decodeURIComponent(args.path);
return this.tryOpen(p, args).catch(() => {
if (path.extname(p) === '') {
return this.tryOpen(p + '.md', args);
}
const resource = vscode.Uri.file(p);
return Promise.resolve(void 0)
.then(() => vscode.commands.executeCommand('vscode.open', resource))
.then(() => void 0);
});
}
private async tryOpen(path: string, args: OpenDocumentLinkArgs) {
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document) && vscode.window.activeTextEditor.document.uri.fsPath === path) {
return this.tryRevealLine(vscode.window.activeTextEditor, args.fragment);
} else {
const resource = vscode.Uri.file(path);
return vscode.workspace.openTextDocument(resource)
.then(vscode.window.showTextDocument)
.then(editor => this.tryRevealLine(editor, args.fragment));
}
}
private async tryRevealLine(editor: vscode.TextEditor, fragment?: string) {
if (editor && fragment) {
const toc = new TableOfContentsProvider(this.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 lineNumberFragment = fragment.match(/^L(\d+)$/);
if (lineNumberFragment) {
const line = +lineNumberFragment[1] - 1;
if (!isNaN(line)) {
return editor.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.AtTop);
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Command } from '../commandManager';
import { MarkdownPreviewManager } from '../features/previewManager';
export class RefreshPreviewCommand implements Command {
public readonly id = 'markdown.preview.refresh';
public constructor(
private readonly webviewManager: MarkdownPreviewManager
) { }
public execute() {
this.webviewManager.refresh();
}
}

View File

@@ -0,0 +1,123 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownPreviewManager } from '../features/previewManager';
import { TelemetryReporter } from '../telemetryReporter';
import { PreviewSettings } from '../features/preview';
function getViewColumn(sideBySide: boolean): vscode.ViewColumn | undefined {
const active = vscode.window.activeTextEditor;
if (!active) {
return vscode.ViewColumn.One;
}
if (!sideBySide) {
return active.viewColumn;
}
switch (active.viewColumn) {
case vscode.ViewColumn.One:
return vscode.ViewColumn.Two;
case vscode.ViewColumn.Two:
return vscode.ViewColumn.Three;
}
return active.viewColumn;
}
interface ShowPreviewSettings {
readonly sideBySide?: boolean;
readonly locked?: boolean;
}
async function showPreview(
webviewManager: MarkdownPreviewManager,
telemetryReporter: TelemetryReporter,
uri: vscode.Uri | undefined,
previewSettings: ShowPreviewSettings,
): Promise<any> {
let resource = uri;
if (!(resource instanceof vscode.Uri)) {
if (vscode.window.activeTextEditor) {
// we are relaxed and don't check for markdown files
resource = vscode.window.activeTextEditor.document.uri;
}
}
if (!(resource instanceof vscode.Uri)) {
if (!vscode.window.activeTextEditor) {
// this is most likely toggling the preview
return vscode.commands.executeCommand('markdown.showSource');
}
// nothing found that could be shown or toggled
return;
}
webviewManager.preview(resource, {
resourceColumn: (vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One,
previewColumn: getViewColumn(!!previewSettings.sideBySide) || vscode.ViewColumn.Active,
locked: !!previewSettings.locked
});
telemetryReporter.sendTelemetryEvent('openPreview', {
where: previewSettings.sideBySide ? 'sideBySide' : 'inPlace',
how: (uri instanceof vscode.Uri) ? 'action' : 'pallete'
});
}
export class ShowPreviewCommand implements Command {
public readonly id = 'markdown.showPreview';
public constructor(
private readonly webviewManager: MarkdownPreviewManager,
private readonly telemetryReporter: TelemetryReporter
) { }
public execute(mainUri?: vscode.Uri, allUris?: vscode.Uri[], previewSettings?: PreviewSettings) {
for (const uri of (allUris || [mainUri])) {
showPreview(this.webviewManager, this.telemetryReporter, uri, {
sideBySide: false,
locked: previewSettings && previewSettings.locked
});
}
}
}
export class ShowPreviewToSideCommand implements Command {
public readonly id = 'markdown.showPreviewToSide';
public constructor(
private readonly webviewManager: MarkdownPreviewManager,
private readonly telemetryReporter: TelemetryReporter
) { }
public execute(uri?: vscode.Uri, previewSettings?: PreviewSettings) {
showPreview(this.webviewManager, this.telemetryReporter, uri, {
sideBySide: true,
locked: previewSettings && previewSettings.locked
});
}
}
export class ShowLockedPreviewToSideCommand implements Command {
public readonly id = 'markdown.showLockedPreviewToSide';
public constructor(
private readonly webviewManager: MarkdownPreviewManager,
private readonly telemetryReporter: TelemetryReporter
) { }
public execute(uri?: vscode.Uri) {
showPreview(this.webviewManager, this.telemetryReporter, uri, {
sideBySide: true,
locked: true
});
}
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* 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 { PreviewSecuritySelector } from '../security';
import { isMarkdownFile } from '../util/file';
import { MarkdownPreviewManager } from '../features/previewManager';
export class ShowPreviewSecuritySelectorCommand implements Command {
public readonly id = 'markdown.showPreviewSecuritySelector';
public constructor(
private readonly previewSecuritySelector: PreviewSecuritySelector,
private readonly previewManager: MarkdownPreviewManager
) { }
public execute(resource: string | undefined) {
if (resource) {
const source = vscode.Uri.parse(resource).query;
this.previewSecuritySelector.showSecutitySelectorForResource(vscode.Uri.parse(source));
} else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
this.previewSecuritySelector.showSecutitySelectorForResource(vscode.window.activeTextEditor.document.uri);
} else if (this.previewManager.activePreviewResource) {
this.previewSecuritySelector.showSecutitySelectorForResource(this.previewManager.activePreviewResource);
}
}
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownPreviewManager } from '../features/previewManager';
export class ShowSourceCommand implements Command {
public readonly id = 'markdown.showSource';
public constructor(
private readonly previewManager: MarkdownPreviewManager
) { }
public execute(docUri?: vscode.Uri) {
if (!docUri) {
return vscode.commands.executeCommand('workbench.action.navigateBack');
}
const resource = this.previewManager.getResourceForPreview(docUri);
if (resource) {
return vscode.workspace.openTextDocument(resource)
.then(document => vscode.window.showTextDocument(document));
}
return undefined;
}
}

View File

@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownPreviewManager } from '../features/previewManager';
export class ToggleLockCommand implements Command {
public readonly id = 'markdown.preview.toggleLock';
public constructor(
private readonly previewManager: MarkdownPreviewManager
) { }
public execute(previewUri?: vscode.Uri) {
this.previewManager.toggleLock(previewUri);
}
}

View File

@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownEngine } from './markdownEngine';
import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security';
import { Logger } from './logger';
import { CommandManager } from './commandManager';
import * as commands from './commands/index';
import { loadDefaultTelemetryReporter } from './telemetryReporter';
import { getMarkdownExtensionContributions } from './markdownExtensions';
import LinkProvider from './features/documentLinkProvider';
import MDDocumentSymbolProvider from './features/documentSymbolProvider';
import { MarkdownContentProvider } from './features/previewContentProvider';
import { MarkdownPreviewManager } from './features/previewManager';
import MarkdownFoldingProvider from './features/foldingProvider';
export function activate(context: vscode.ExtensionContext) {
const telemetryReporter = loadDefaultTelemetryReporter();
context.subscriptions.push(telemetryReporter);
const contributions = getMarkdownExtensionContributions();
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
const engine = new MarkdownEngine(contributions);
const logger = new Logger();
const selector = 'markdown';
const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, contributions, logger);
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions);
context.subscriptions.push(previewManager);
context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(selector, new MDDocumentSymbolProvider(engine)));
context.subscriptions.push(vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider()));
context.subscriptions.push(vscode.languages.registerFoldingProvider(selector, new MarkdownFoldingProvider(engine)));
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
const commandManager = new CommandManager();
context.subscriptions.push(commandManager);
commandManager.register(new commands.ShowPreviewCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowSourceCommand(previewManager));
commandManager.register(new commands.RefreshPreviewCommand(previewManager));
commandManager.register(new commands.MoveCursorToPositionCommand());
commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
commandManager.register(new commands.OnPreviewStyleLoadErrorCommand());
commandManager.register(new commands.OpenDocumentLinkCommand(engine));
commandManager.register(new commands.ToggleLockCommand(previewManager));
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
logger.updateConfiguration();
previewManager.updateConfiguration();
}));
}

View File

@@ -0,0 +1,158 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
function normalizeLink(
document: vscode.TextDocument,
link: string,
base: string
): vscode.Uri {
const uri = vscode.Uri.parse(link);
if (uri.scheme) {
return uri;
}
// assume it must be a file
let resourcePath = uri.path;
if (!uri.path) {
resourcePath = document.uri.path;
} else if (uri.path[0] === '/') {
const root = vscode.workspace.getWorkspaceFolder(document.uri);
if (root) {
resourcePath = path.join(root.uri.fsPath, uri.path);
}
} else {
resourcePath = path.join(base, uri.path);
}
return OpenDocumentLinkCommand.createCommandUri(resourcePath, uri.fragment);
}
function matchAll(
pattern: RegExp,
text: string
): Array<RegExpMatchArray> {
const out: RegExpMatchArray[] = [];
pattern.lastIndex = 0;
let match: RegExpMatchArray | null;
while ((match = pattern.exec(text))) {
out.push(match);
}
return out;
}
export default class LinkProvider implements vscode.DocumentLinkProvider {
private readonly linkPattern = /(\[[^\]]*\]\(\s*?)(((((?=.*\)\)+)|(?=.*\)\]+))[^\s\)]+?)|([^\s]+?)))\)/g;
private readonly referenceLinkPattern = /(\[([^\]]+)\]\[\s*?)([^\s\]]*?)\]/g;
private readonly definitionPattern = /^([\t ]*\[([^\]]+)\]:\s*)(\S+)/gm;
public provideDocumentLinks(
document: vscode.TextDocument,
_token: vscode.CancellationToken
): vscode.DocumentLink[] {
const base = path.dirname(document.uri.fsPath);
const text = document.getText();
return this.providerInlineLinks(text, document, base)
.concat(this.provideReferenceLinks(text, document, base));
}
private providerInlineLinks(
text: string,
document: vscode.TextDocument,
base: string
): vscode.DocumentLink[] {
const results: vscode.DocumentLink[] = [];
for (const match of matchAll(this.linkPattern, text)) {
const pre = match[1];
const link = match[2];
const offset = (match.index || 0) + pre.length;
const linkStart = document.positionAt(offset);
const linkEnd = document.positionAt(offset + link.length);
try {
results.push(new vscode.DocumentLink(
new vscode.Range(linkStart, linkEnd),
normalizeLink(document, link, base)));
} catch (e) {
// noop
}
}
return results;
}
private provideReferenceLinks(
text: string,
document: vscode.TextDocument,
base: string
): vscode.DocumentLink[] {
const results: vscode.DocumentLink[] = [];
const definitions = this.getDefinitions(text, document);
for (const match of matchAll(this.referenceLinkPattern, text)) {
let linkStart: vscode.Position;
let linkEnd: vscode.Position;
let reference = match[3];
if (reference) { // [text][ref]
const pre = match[1];
const offset = (match.index || 0) + pre.length;
linkStart = document.positionAt(offset);
linkEnd = document.positionAt(offset + reference.length);
} else if (match[2]) { // [ref][]
reference = match[2];
const offset = (match.index || 0) + 1;
linkStart = document.positionAt(offset);
linkEnd = document.positionAt(offset + match[2].length);
} else {
continue;
}
try {
const link = definitions.get(reference);
if (link) {
results.push(new vscode.DocumentLink(
new vscode.Range(linkStart, linkEnd),
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([link.linkRange.start.line, link.linkRange.start.character]))}`)));
}
} catch (e) {
// noop
}
}
for (const definition of Array.from(definitions.values())) {
try {
results.push(new vscode.DocumentLink(
definition.linkRange,
normalizeLink(document, definition.link, base)));
} catch (e) {
// noop
}
}
return results;
}
private getDefinitions(text: string, document: vscode.TextDocument) {
const out = new Map<string, { link: string, linkRange: vscode.Range }>();
for (const match of matchAll(this.definitionPattern, text)) {
const pre = match[1];
const reference = match[2];
const link = match[3].trim();
const offset = (match.index || 0) + pre.length;
const linkStart = document.positionAt(offset);
const linkEnd = document.positionAt(offset + link.length);
out.set(reference, {
link: link,
linkRange: new vscode.Range(linkStart, linkEnd)
});
}
return out;
}
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
constructor(
private readonly engine: MarkdownEngine
) { }
public async provideDocumentSymbols(document: vscode.TextDocument): Promise<vscode.SymbolInformation[]> {
const toc = await new TableOfContentsProvider(this.engine, document).getToc();
return toc.map(entry => {
return new vscode.SymbolInformation('#'.repeat(entry.level) + ' ' + entry.text, vscode.SymbolKind.Namespace, '', entry.location);
});
}
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
export default class MarkdownFoldingProvider implements vscode.FoldingProvider {
constructor(
private readonly engine: MarkdownEngine
) { }
public async provideFoldingRanges(
document: vscode.TextDocument,
context: vscode.FoldingContext,
_token: vscode.CancellationToken
): Promise<vscode.FoldingRangeList> {
const tocProvider = new TableOfContentsProvider(this.engine, document);
let toc = await tocProvider.getToc();
if (context.maxRanges && toc.length > context.maxRanges) {
toc = toc.slice(0, context.maxRanges);
}
const foldingRanges = toc.map((entry, startIndex) => {
const start = entry.line;
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;
if (document.lineAt(end).isEmptyOrWhitespace && end >= start + 1) {
end = end - 1;
}
break;
}
}
return new vscode.FoldingRange(
start,
typeof end === 'number' ? end : document.lineCount - 1);
});
return new vscode.FoldingRangeList(foldingRanges);
}
}

View File

@@ -0,0 +1,304 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import { Logger } from '../logger';
import { MarkdownContentProvider } from './previewContentProvider';
import { disposeAll } from '../util/dispose';
import * as nls from 'vscode-nls';
import { getVisibleLine, MarkdownFileTopmostLineMonitor } from '../util/topmostLineMonitor';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContributions } from '../markdownExtensions';
const localize = nls.loadMessageBundle();
export class MarkdownPreview {
public static previewScheme = 'vscode-markdown-preview';
private static previewCount = 0;
public readonly uri: vscode.Uri;
private readonly webview: vscode.Webview;
private throttleTimer: any;
private initialLine: number | undefined = undefined;
private readonly disposables: vscode.Disposable[] = [];
private firstUpdate = true;
private currentVersion?: { resource: vscode.Uri, version: number };
private forceUpdate = false;
private isScrolling = false;
constructor(
private _resource: vscode.Uri,
previewColumn: vscode.ViewColumn,
public locked: boolean,
private readonly contentProvider: MarkdownContentProvider,
private readonly previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly logger: Logger,
topmostLineMonitor: MarkdownFileTopmostLineMonitor,
private readonly contributions: MarkdownContributions
) {
this.uri = vscode.Uri.parse(`${MarkdownPreview.previewScheme}:${MarkdownPreview.previewCount++}`);
this.webview = vscode.window.createWebview(
this.uri,
this.getPreviewTitle(this._resource),
previewColumn, {
enableScripts: true,
enableCommandUris: true,
enableFindWidget: true,
localResourceRoots: this.getLocalResourceRoots(_resource)
});
this.webview.onDidDispose(() => {
this.dispose();
}, null, this.disposables);
this.webview.onDidChangeViewColumn(() => {
this._onDidChangeViewColumnEmitter.fire();
}, null, this.disposables);
this.webview.onDidReceiveMessage(e => {
if (e.source !== this._resource.toString()) {
return;
}
switch (e.type) {
case 'command':
vscode.commands.executeCommand(e.body.command, ...e.body.args);
break;
case 'revealLine':
this.onDidScrollPreview(e.body.line);
break;
case 'didClick':
this.onDidClickPreview(e.body.line);
break;
}
}, null, this.disposables);
vscode.workspace.onDidChangeTextDocument(event => {
if (this.isPreviewOf(event.document.uri)) {
this.refresh();
}
}, null, this.disposables);
topmostLineMonitor.onDidChangeTopmostLine(event => {
if (this.isPreviewOf(event.resource)) {
this.updateForView(event.resource, event.line);
}
}, null, this.disposables);
vscode.window.onDidChangeTextEditorSelection(event => {
if (this.isPreviewOf(event.textEditor.document.uri)) {
this.webview.postMessage({
type: 'onDidChangeTextEditorSelection',
line: event.selections[0].active.line,
source: this.resource.toString()
});
}
}, null, this.disposables);
}
private readonly _onDisposeEmitter = new vscode.EventEmitter<void>();
public readonly onDispose = this._onDisposeEmitter.event;
private readonly _onDidChangeViewColumnEmitter = new vscode.EventEmitter<vscode.ViewColumn>();
public readonly onDidChangeViewColumn = this._onDidChangeViewColumnEmitter.event;
public get resource(): vscode.Uri {
return this._resource;
}
public dispose() {
this._onDisposeEmitter.fire();
this._onDisposeEmitter.dispose();
this._onDidChangeViewColumnEmitter.dispose();
this.webview.dispose();
disposeAll(this.disposables);
}
public update(resource: vscode.Uri) {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.uri.fsPath === resource.fsPath) {
this.initialLine = getVisibleLine(editor);
} else {
this.initialLine = undefined;
}
// If we have changed resources, cancel any pending updates
const isResourceChange = resource.fsPath !== this._resource.fsPath;
if (isResourceChange) {
clearTimeout(this.throttleTimer);
this.throttleTimer = undefined;
}
this._resource = resource;
// Schedule update if none is pending
if (!this.throttleTimer) {
if (isResourceChange || this.firstUpdate) {
this.doUpdate();
} else {
this.throttleTimer = setTimeout(() => this.doUpdate(), 300);
}
}
this.firstUpdate = false;
}
public refresh() {
this.forceUpdate = true;
this.update(this._resource);
}
public updateConfiguration() {
if (this.previewConfigurations.hasConfigurationChanged(this._resource)) {
this.refresh();
}
}
public get viewColumn(): vscode.ViewColumn | undefined {
return this.webview.viewColumn;
}
public isPreviewOf(resource: vscode.Uri): boolean {
return this._resource.fsPath === resource.fsPath;
}
public matchesResource(
otherResource: vscode.Uri,
otherViewColumn: vscode.ViewColumn | undefined,
otherLocked: boolean
): boolean {
if (this.viewColumn !== otherViewColumn) {
return false;
}
if (this.locked) {
return otherLocked && this.isPreviewOf(otherResource);
} else {
return !otherLocked;
}
}
public matches(otherPreview: MarkdownPreview): boolean {
return this.matchesResource(otherPreview._resource, otherPreview.viewColumn, otherPreview.locked);
}
public show(viewColumn: vscode.ViewColumn) {
this.webview.show(viewColumn);
}
public toggleLock() {
this.locked = !this.locked;
this.webview.title = this.getPreviewTitle(this._resource);
}
private getPreviewTitle(resource: vscode.Uri): string {
return this.locked
? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath))
: localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath));
}
private updateForView(resource: vscode.Uri, topLine: number | undefined) {
if (!this.isPreviewOf(resource)) {
return;
}
if (this.isScrolling) {
this.isScrolling = false;
return;
}
if (typeof topLine === 'number') {
this.logger.log('updateForView', { markdownFile: resource });
this.initialLine = topLine;
this.webview.postMessage({
type: 'updateView',
line: topLine,
source: resource.toString()
});
}
}
private async doUpdate(): Promise<void> {
const resource = this._resource;
clearTimeout(this.throttleTimer);
this.throttleTimer = undefined;
const document = await vscode.workspace.openTextDocument(resource);
if (!this.forceUpdate && this.currentVersion && this.currentVersion.resource.fsPath === resource.fsPath && this.currentVersion.version === document.version) {
if (this.initialLine) {
this.updateForView(resource, this.initialLine);
}
return;
}
this.forceUpdate = false;
this.currentVersion = { resource, version: document.version };
this.contentProvider.provideTextDocumentContent(document, this.previewConfigurations, this.initialLine)
.then(content => {
if (this._resource === resource) {
this.webview.title = this.getPreviewTitle(this._resource);
this.webview.html = content;
}
});
}
private getLocalResourceRoots(resource: vscode.Uri): vscode.Uri[] {
const baseRoots = this.contributions.previewResourceRoots;
const folder = vscode.workspace.getWorkspaceFolder(resource);
if (folder) {
return baseRoots.concat(folder.uri);
}
if (!resource.scheme || resource.scheme === 'file') {
return baseRoots.concat(vscode.Uri.file(path.dirname(resource.fsPath)));
}
return baseRoots;
}
private onDidScrollPreview(line: number) {
for (const editor of vscode.window.visibleTextEditors) {
if (!this.isPreviewOf(editor.document.uri)) {
continue;
}
this.isScrolling = true;
const sourceLine = Math.floor(line);
const fraction = line - sourceLine;
const text = editor.document.lineAt(sourceLine).text;
const start = Math.floor(fraction * text.length);
editor.revealRange(
new vscode.Range(sourceLine, start, sourceLine + 1, 0),
vscode.TextEditorRevealType.AtTop);
}
}
private async onDidClickPreview(line: number): Promise<void> {
for (const visibleEditor of vscode.window.visibleTextEditors) {
if (this.isPreviewOf(visibleEditor.document.uri)) {
const editor = await vscode.window.showTextDocument(visibleEditor.document, visibleEditor.viewColumn);
const position = new vscode.Position(line, 0);
editor.selection = new vscode.Selection(position, position);
return;
}
}
}
}
export interface PreviewSettings {
readonly resourceColumn: vscode.ViewColumn;
readonly previewColumn: vscode.ViewColumn;
readonly locked: boolean;
}

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* 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';
export class MarkdownPreviewConfiguration {
public static getForResource(resource: vscode.Uri) {
return new MarkdownPreviewConfiguration(resource);
}
public readonly scrollBeyondLastLine: boolean;
public readonly wordWrap: boolean;
public readonly previewFrontMatter: string;
public readonly lineBreaks: boolean;
public readonly doubleClickToSwitchToEditor: boolean;
public readonly scrollEditorWithPreview: boolean;
public readonly scrollPreviewWithEditor: boolean;
public readonly markEditorSelection: boolean;
public readonly lineHeight: number;
public readonly fontSize: number;
public readonly fontFamily: string | undefined;
public readonly styles: string[];
private constructor(resource: vscode.Uri) {
const editorConfig = vscode.workspace.getConfiguration('editor', resource);
const markdownConfig = vscode.workspace.getConfiguration('markdown', resource);
const markdownEditorConfig = vscode.workspace.getConfiguration('[markdown]');
this.scrollBeyondLastLine = editorConfig.get<boolean>('scrollBeyondLastLine', false);
this.wordWrap = editorConfig.get<string>('wordWrap', 'off') !== 'off';
if (markdownEditorConfig && markdownEditorConfig['editor.wordWrap']) {
this.wordWrap = markdownEditorConfig['editor.wordWrap'] !== 'off';
}
this.previewFrontMatter = markdownConfig.get<string>('previewFrontMatter', 'hide');
this.scrollPreviewWithEditor = !!markdownConfig.get<boolean>('preview.scrollPreviewWithEditor', true);
this.scrollEditorWithPreview = !!markdownConfig.get<boolean>('preview.scrollEditorWithPreview', true);
this.lineBreaks = !!markdownConfig.get<boolean>('preview.breaks', false);
this.doubleClickToSwitchToEditor = !!markdownConfig.get<boolean>('preview.doubleClickToSwitchToEditor', true);
this.markEditorSelection = !!markdownConfig.get<boolean>('preview.markEditorSelection', true);
this.fontFamily = markdownConfig.get<string | undefined>('preview.fontFamily', undefined);
this.fontSize = Math.max(8, +markdownConfig.get<number>('preview.fontSize', NaN));
this.lineHeight = Math.max(0.6, +markdownConfig.get<number>('preview.lineHeight', NaN));
this.styles = markdownConfig.get<string[]>('styles', []);
}
public isEqualTo(otherConfig: MarkdownPreviewConfiguration) {
for (let key in this) {
if (this.hasOwnProperty(key) && key !== 'styles') {
if (this[key] !== otherConfig[key]) {
return false;
}
}
}
// Check styles
if (this.styles.length !== otherConfig.styles.length) {
return false;
}
for (let i = 0; i < this.styles.length; ++i) {
if (this.styles[i] !== otherConfig.styles[i]) {
return false;
}
}
return true;
}
[key: string]: any;
}
export class MarkdownPreviewConfigurationManager {
private readonly previewConfigurationsForWorkspaces = new Map<string, MarkdownPreviewConfiguration>();
public loadAndCacheConfiguration(
resource: vscode.Uri
): MarkdownPreviewConfiguration {
const config = MarkdownPreviewConfiguration.getForResource(resource);
this.previewConfigurationsForWorkspaces.set(this.getKey(resource), config);
return config;
}
public hasConfigurationChanged(
resource: vscode.Uri
): boolean {
const key = this.getKey(resource);
const currentConfig = this.previewConfigurationsForWorkspaces.get(key);
const newConfig = MarkdownPreviewConfiguration.getForResource(resource);
return (!currentConfig || !currentConfig.isEqualTo(newConfig));
}
private getKey(
resource: vscode.Uri
): string {
const folder = vscode.workspace.getWorkspaceFolder(resource);
return folder ? folder.uri.toString() : '';
}
}

View File

@@ -0,0 +1,175 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import { MarkdownEngine } from '../markdownEngine';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { Logger } from '../logger';
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security';
import { MarkdownPreviewConfigurationManager, MarkdownPreviewConfiguration } from './previewConfig';
import { MarkdownContributions } from '../markdownExtensions';
/**
* Strings used inside the markdown preview.
*
* Stored here and then injected in the preview so that they
* can be localized using our normal localization process.
*/
const previewStrings = {
cspAlertMessageText: localize(
'preview.securityMessage.text',
'Some content has been disabled in this document'),
cspAlertMessageTitle: localize(
'preview.securityMessage.title',
'Potentially unsafe or insecure content has been disabled in the markdown preview. Change the Markdown preview security setting to allow insecure content or enable scripts'),
cspAlertMessageLabel: localize(
'preview.securityMessage.label',
'Content Disabled Security Warning')
};
export class MarkdownContentProvider {
constructor(
private readonly engine: MarkdownEngine,
private readonly context: vscode.ExtensionContext,
private readonly cspArbiter: ContentSecurityPolicyArbiter,
private readonly contributions: MarkdownContributions,
private readonly logger: Logger
) { }
public async provideTextDocumentContent(
markdownDocument: vscode.TextDocument,
previewConfigurations: MarkdownPreviewConfigurationManager,
initialLine: number | undefined = undefined
): Promise<string> {
const sourceUri = markdownDocument.uri;
const config = previewConfigurations.loadAndCacheConfiguration(sourceUri);
const initialData = {
source: sourceUri.toString(),
line: initialLine,
lineCount: markdownDocument.lineCount,
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
scrollEditorWithPreview: config.scrollEditorWithPreview,
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,
disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings()
};
this.logger.log('provideTextDocumentContent', initialData);
// Content Security Policy
const nonce = new Date().getTime() + '' + new Date().getMilliseconds();
const csp = this.getCspForResource(sourceUri, nonce);
const body = await this.engine.render(sourceUri, config.previewFrontMatter === 'hide', markdownDocument.getText());
return `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
${csp}
<meta id="vscode-markdown-preview-data" data-settings="${JSON.stringify(initialData).replace(/"/g, '&quot;')}" data-strings="${JSON.stringify(previewStrings).replace(/"/g, '&quot;')}">
<script src="${this.extensionResourcePath('pre.js')}" nonce="${nonce}"></script>
${this.getStyles(sourceUri, nonce, config)}
<base href="${markdownDocument.uri.with({ scheme: 'vscode-resource' }).toString(true)}">
</head>
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
${body}
<div class="code-line" data-line="${markdownDocument.lineCount}"></div>
${this.getScripts(nonce)}
</body>
</html>`;
}
private extensionResourcePath(mediaFile: string): string {
return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile)))
.with({ scheme: 'vscode-resource' })
.toString();
}
private fixHref(resource: vscode.Uri, href: string): string {
if (!href) {
return href;
}
// Use href if it is already an URL
const hrefUri = vscode.Uri.parse(href);
if (['http', 'https'].indexOf(hrefUri.scheme) >= 0) {
return hrefUri.toString();
}
// Use href as file URI if it is absolute
if (path.isAbsolute(href) || hrefUri.scheme === 'file') {
return vscode.Uri.file(href)
.with({ scheme: 'vscode-resource' })
.toString();
}
// Use a workspace relative path if there is a workspace
let root = vscode.workspace.getWorkspaceFolder(resource);
if (root) {
return vscode.Uri.file(path.join(root.uri.fsPath, href))
.with({ scheme: 'vscode-resource' })
.toString();
}
// Otherwise look relative to the markdown file
return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))
.with({ scheme: 'vscode-resource' })
.toString();
}
private computeCustomStyleSheetIncludes(resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
if (Array.isArray(config.styles)) {
return config.styles.map(style => {
return `<link rel="stylesheet" class="code-user-style" data-source="${style.replace(/"/g, '&quot;')}" href="${this.fixHref(resource, style)}" type="text/css" media="screen">`;
}).join('\n');
}
return '';
}
private getSettingsOverrideStyles(nonce: string, config: MarkdownPreviewConfiguration): string {
return `<style nonce="${nonce}">
body {
${config.fontFamily ? `font-family: ${config.fontFamily};` : ''}
${isNaN(config.fontSize) ? '' : `font-size: ${config.fontSize}px;`}
${isNaN(config.lineHeight) ? '' : `line-height: ${config.lineHeight};`}
}
</style>`;
}
private getStyles(resource: vscode.Uri, nonce: string, config: MarkdownPreviewConfiguration): string {
const baseStyles = this.contributions.previewStyles
.map(resource => `<link rel="stylesheet" type="text/css" href="${resource.toString()}">`)
.join('\n');
return `${baseStyles}
${this.getSettingsOverrideStyles(nonce, config)}
${this.computeCustomStyleSheetIncludes(resource, config)}`;
}
private getScripts(nonce: string): string {
return this.contributions.previewScripts
.map(resource => `<script async src="${resource.toString()}" nonce="${nonce}" charset="UTF-8"></script>`)
.join('\n');
}
private getCspForResource(resource: vscode.Uri, nonce: string): string {
switch (this.cspArbiter.getSecurityLevelForResource(resource)) {
case MarkdownPreviewSecurityLevel.AllowInsecureContent:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: http: https: data:; media-src vscode-resource: http: https: data:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:; font-src vscode-resource: http: https: data:;">`;
case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:
return '';
case MarkdownPreviewSecurityLevel.Strict:
default:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https: data:; media-src vscode-resource: https: data:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' https: data:; font-src vscode-resource: https: data:;">`;
}
}
}

View File

@@ -0,0 +1,144 @@
/*---------------------------------------------------------------------------------------------
* 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 { Logger } from '../logger';
import { MarkdownContentProvider } from './previewContentProvider';
import { MarkdownPreview, PreviewSettings } from './preview';
import { disposeAll } from '../util/dispose';
import { MarkdownFileTopmostLineMonitor } from '../util/topmostLineMonitor';
import { isMarkdownFile } from '../util/file';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContributions } from '../markdownExtensions';
export class MarkdownPreviewManager {
private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus';
private readonly topmostLineMonitor = new MarkdownFileTopmostLineMonitor();
private readonly previewConfigurations = new MarkdownPreviewConfigurationManager();
private readonly previews: MarkdownPreview[] = [];
private activePreview: MarkdownPreview | undefined = undefined;
private readonly disposables: vscode.Disposable[] = [];
public constructor(
private readonly contentProvider: MarkdownContentProvider,
private readonly logger: Logger,
private readonly contributions: MarkdownContributions
) {
vscode.window.onDidChangeActiveEditor(editor => {
vscode.commands.executeCommand('setContext', MarkdownPreviewManager.markdownPreviewActiveContextKey,
editor && editor.editorType === 'webview' && editor.uri.scheme === MarkdownPreview.previewScheme);
this.activePreview = editor && editor.editorType === 'webview'
? this.previews.find(preview => editor.uri.toString() === preview.uri.toString())
: undefined;
if (editor && editor.editorType === 'texteditor') {
if (isMarkdownFile(editor.document)) {
for (const preview of this.previews.filter(preview => !preview.locked)) {
preview.update(editor.document.uri);
}
}
}
}, null, this.disposables);
}
public dispose(): void {
disposeAll(this.disposables);
disposeAll(this.previews);
}
public refresh() {
for (const preview of this.previews) {
preview.refresh();
}
}
public updateConfiguration() {
for (const preview of this.previews) {
preview.updateConfiguration();
}
}
public preview(
resource: vscode.Uri,
previewSettings: PreviewSettings
): void {
let preview = this.getExistingPreview(resource, previewSettings);
if (preview) {
preview.show(previewSettings.previewColumn);
} else {
preview = this.createNewPreview(resource, previewSettings);
this.previews.push(preview);
}
preview.update(resource);
}
public get activePreviewResource() {
return this.activePreview && this.activePreview.resource;
}
public getResourceForPreview(previewUri: vscode.Uri): vscode.Uri | undefined {
const preview = this.getPreviewWithUri(previewUri);
return preview && preview.resource;
}
public toggleLock(previewUri?: vscode.Uri) {
const preview = previewUri ? this.getPreviewWithUri(previewUri) : this.activePreview;
if (preview) {
preview.toggleLock();
// Close any previews that are now redundant, such as having two dynamic previews in the same editor group
for (const otherPreview of this.previews) {
if (otherPreview !== preview && preview.matches(otherPreview)) {
otherPreview.dispose();
}
}
}
}
private getExistingPreview(
resource: vscode.Uri,
previewSettings: PreviewSettings
): MarkdownPreview | undefined {
return this.previews.find(preview =>
preview.matchesResource(resource, previewSettings.previewColumn, previewSettings.locked));
}
private getPreviewWithUri(previewUri: vscode.Uri): MarkdownPreview | undefined {
return this.previews.find(preview => preview.uri.toString() === previewUri.toString());
}
private createNewPreview(
resource: vscode.Uri,
previewSettings: PreviewSettings
) {
const preview = new MarkdownPreview(
resource,
previewSettings.previewColumn,
previewSettings.locked,
this.contentProvider,
this.previewConfigurations,
this.logger,
this.topmostLineMonitor,
this.contributions);
preview.onDispose(() => {
const existing = this.previews.indexOf(preview!);
if (existing >= 0) {
this.previews.splice(existing, 1);
}
});
preview.onDidChangeViewColumn(() => {
disposeAll(this.previews.filter(otherPreview => preview !== otherPreview && preview!.matches(otherPreview)));
});
return preview;
}
}

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OutputChannel, window, workspace } from 'vscode';
enum Trace {
Off,
Verbose
}
namespace Trace {
export function fromString(value: string): Trace {
value = value.toLowerCase();
switch (value) {
case 'off':
return Trace.Off;
case 'verbose':
return Trace.Verbose;
default:
return Trace.Off;
}
}
}
function isString(value: any): value is string {
return Object.prototype.toString.call(value) === '[object String]';
}
export class Logger {
private trace?: Trace;
private _output?: OutputChannel;
constructor() {
this.updateConfiguration();
}
public log(message: string, data?: any): void {
if (this.trace === Trace.Verbose) {
this.output.appendLine(`[Log - ${(new Date().toLocaleTimeString())}] ${message}`);
if (data) {
this.output.appendLine(Logger.data2String(data));
}
}
}
public updateConfiguration() {
this.trace = this.readTrace();
}
private get output(): OutputChannel {
if (!this._output) {
this._output = window.createOutputChannel('Markdown');
}
return this._output;
}
private readTrace(): Trace {
return Trace.fromString(workspace.getConfiguration().get<string>('markdown.trace', 'off'));
}
private static data2String(data: any): string {
if (data instanceof Error) {
if (isString(data.stack)) {
return data.stack;
}
return (data as Error).message;
}
if (isString(data)) {
return data;
}
return JSON.stringify(data, undefined, 2);
}
}

View File

@@ -0,0 +1,171 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import { Slug } from './tableOfContentsProvider';
import { MarkdownIt, Token } from 'markdown-it';
import { MarkdownContributions } from './markdownExtensions';
const FrontMatterRegex = /^---\s*[^]*?(-{3}|\.{3})\s*/;
export class MarkdownEngine {
private md?: MarkdownIt;
private firstLine?: number;
private currentDocument?: vscode.Uri;
public constructor(
private readonly extensionPreviewResourceProvider: MarkdownContributions
) { }
private usePlugin(factory: (md: any) => any): void {
try {
this.md = factory(this.md);
} catch (e) {
// noop
}
}
private async getEngine(resource: vscode.Uri): Promise<MarkdownIt> {
if (!this.md) {
const hljs = await import('highlight.js');
const mdnh = await import('markdown-it-named-headers');
this.md = (await import('markdown-it'))({
html: true,
highlight: (str: string, lang: string) => {
// Workaround for highlight not supporting tsx: https://github.com/isagalaev/highlight.js/issues/1155
if (lang && ['tsx', 'typescriptreact'].indexOf(lang.toLocaleLowerCase()) >= 0) {
lang = 'jsx';
}
if (lang && hljs.getLanguage(lang)) {
try {
return `<pre class="hljs"><code><div>${hljs.highlight(lang, str, true).value}</div></code></pre>`;
} catch (error) { }
}
return `<pre class="hljs"><code><div>${this.md!.utils.escapeHtml(str)}</div></code></pre>`;
}
}).use(mdnh, {
slugify: (header: string) => Slug.fromHeading(header).value
});
for (const plugin of this.extensionPreviewResourceProvider.markdownItPlugins) {
this.usePlugin(await plugin);
}
for (const renderName of ['paragraph_open', 'heading_open', 'image', 'code_block', 'blockquote_open', 'list_item_open']) {
this.addLineNumberRenderer(this.md, renderName);
}
this.addLinkNormalizer(this.md);
this.addLinkValidator(this.md);
}
const config = vscode.workspace.getConfiguration('markdown', resource);
this.md.set({
breaks: config.get<boolean>('preview.breaks', false),
linkify: config.get<boolean>('preview.linkify', true)
});
return this.md;
}
private stripFrontmatter(text: string): { text: string, offset: number } {
let offset = 0;
const frontMatterMatch = FrontMatterRegex.exec(text);
if (frontMatterMatch) {
const frontMatter = frontMatterMatch[0];
offset = frontMatter.split(/\r\n|\n|\r/g).length - 1;
text = text.substr(frontMatter.length);
}
return { text, offset };
}
public async render(document: vscode.Uri, stripFrontmatter: boolean, text: string): Promise<string> {
let offset = 0;
if (stripFrontmatter) {
const markdownContent = this.stripFrontmatter(text);
offset = markdownContent.offset;
text = markdownContent.text;
}
this.currentDocument = document;
this.firstLine = offset;
const engine = await this.getEngine(document);
return engine.render(text);
}
public async parse(document: vscode.Uri, source: string): Promise<Token[]> {
const { text, offset } = this.stripFrontmatter(source);
this.currentDocument = document;
const engine = await this.getEngine(document);
return engine.parse(text, {}).map(token => {
if (token.map) {
token.map[0] += offset;
}
return token;
});
}
private addLineNumberRenderer(md: any, ruleName: string): void {
const original = md.renderer.rules[ruleName];
md.renderer.rules[ruleName] = (tokens: any, idx: number, options: any, env: any, self: any) => {
const token = tokens[idx];
if (token.map && token.map.length) {
token.attrSet('data-line', this.firstLine + token.map[0]);
token.attrJoin('class', 'code-line');
}
if (original) {
return original(tokens, idx, options, env, self);
} else {
return self.renderToken(tokens, idx, options, env, self);
}
};
}
private addLinkNormalizer(md: any): void {
const normalizeLink = md.normalizeLink;
md.normalizeLink = (link: string) => {
try {
let uri = vscode.Uri.parse(link);
if (!uri.scheme && uri.path) {
// Assume it must be a file
const fragment = uri.fragment;
if (uri.path[0] === '/') {
const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!);
if (root) {
uri = vscode.Uri.file(path.join(root.uri.fsPath, uri.path));
}
} else {
uri = vscode.Uri.file(path.join(path.dirname(this.currentDocument!.path), uri.path));
}
if (fragment) {
uri = uri.with({
fragment: Slug.fromHeading(fragment).value
});
}
return normalizeLink(uri.with({ scheme: 'vscode-resource' }).toString(true));
} else if (!uri.scheme && !uri.path && uri.fragment) {
return normalizeLink(uri.with({
fragment: Slug.fromHeading(uri.fragment).value
}).toString(true));
}
} catch (e) {
// noop
}
return normalizeLink(link);
};
}
private addLinkValidator(md: any): void {
const validateLink = md.validateLink;
md.validateLink = (link: string) => {
// support file:// links
return validateLink(link) || link.indexOf('file:') === 0;
};
}
}

View File

@@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
const resolveExtensionResource = (extension: vscode.Extension<any>, resourcePath: string): vscode.Uri => {
return vscode.Uri.file(path.join(extension.extensionPath, resourcePath))
.with({ scheme: 'vscode-resource' });
};
const resolveExtensionResources = (extension: vscode.Extension<any>, resourcePaths: any): vscode.Uri[] => {
const result: vscode.Uri[] = [];
if (Array.isArray(resourcePaths)) {
for (const resource of resourcePaths) {
try {
result.push(resolveExtensionResource(extension, resource));
} catch (e) {
// noop
}
}
}
return result;
};
export interface MarkdownContributions {
readonly previewScripts: vscode.Uri[];
readonly previewStyles: vscode.Uri[];
readonly markdownItPlugins: Thenable<(md: any) => any>[];
readonly previewResourceRoots: vscode.Uri[];
}
class MarkdownExtensionContributions implements MarkdownContributions {
private readonly _scripts: vscode.Uri[] = [];
private readonly _styles: vscode.Uri[] = [];
private readonly _previewResourceRoots: vscode.Uri[] = [];
private readonly _plugins: Thenable<(md: any) => any>[] = [];
private _loaded = false;
public get previewScripts(): vscode.Uri[] {
this.ensureLoaded();
return this._scripts;
}
public get previewStyles(): vscode.Uri[] {
this.ensureLoaded();
return this._styles;
}
public get previewResourceRoots(): vscode.Uri[] {
this.ensureLoaded();
return this._previewResourceRoots;
}
public get markdownItPlugins(): Thenable<(md: any) => any>[] {
this.ensureLoaded();
return this._plugins;
}
private ensureLoaded() {
if (this._loaded) {
return;
}
this._loaded = true;
for (const extension of vscode.extensions.all) {
const contributes = extension.packageJSON && extension.packageJSON.contributes;
if (!contributes) {
continue;
}
this.tryLoadPreviewStyles(contributes, extension);
this.tryLoadPreviewScripts(contributes, extension);
this.tryLoadMarkdownItPlugins(contributes, extension);
if (contributes['markdown.previewScripts'] || contributes['markdown.previewStyles']) {
this._previewResourceRoots.push(vscode.Uri.file(extension.extensionPath));
}
}
}
private tryLoadMarkdownItPlugins(
contributes: any,
extension: vscode.Extension<any>
) {
if (contributes['markdown.markdownItPlugins']) {
this._plugins.push(extension.activate().then(() => {
if (extension.exports && extension.exports.extendMarkdownIt) {
return (md: any) => extension.exports.extendMarkdownIt(md);
}
return (md: any) => md;
}));
}
}
private tryLoadPreviewScripts(
contributes: any,
extension: vscode.Extension<any>
) {
this._scripts.push(...resolveExtensionResources(extension, contributes['markdown.previewScripts']));
}
private tryLoadPreviewStyles(
contributes: any,
extension: vscode.Extension<any>
) {
this._styles.push(...resolveExtensionResources(extension, contributes['markdown.previewStyles']));
}
}
export function getMarkdownExtensionContributions(): MarkdownContributions {
return new MarkdownExtensionContributions();
}

View File

@@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownPreviewManager } from './features/previewManager';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export enum MarkdownPreviewSecurityLevel {
Strict = 0,
AllowInsecureContent = 1,
AllowScriptsAndAllContent = 2
}
export interface ContentSecurityPolicyArbiter {
getSecurityLevelForResource(resource: vscode.Uri): MarkdownPreviewSecurityLevel;
setSecurityLevelForResource(resource: vscode.Uri, level: MarkdownPreviewSecurityLevel): Thenable<void>;
shouldAllowSvgsForResource(resource: vscode.Uri): void;
shouldDisableSecurityWarnings(): boolean;
setShouldDisableSecurityWarning(shouldShow: boolean): Thenable<void>;
}
export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPolicyArbiter {
private readonly old_trusted_workspace_key = 'trusted_preview_workspace:';
private readonly security_level_key = 'preview_security_level:';
private readonly should_disable_security_warning_key = 'preview_should_show_security_warning:';
constructor(
private readonly globalState: vscode.Memento,
private readonly workspaceState: vscode.Memento
) { }
public getSecurityLevelForResource(resource: vscode.Uri): MarkdownPreviewSecurityLevel {
// Use new security level setting first
const level = this.globalState.get<MarkdownPreviewSecurityLevel | undefined>(this.security_level_key + this.getRoot(resource), undefined);
if (typeof level !== 'undefined') {
return level;
}
// Fallback to old trusted workspace setting
if (this.globalState.get<boolean>(this.old_trusted_workspace_key + this.getRoot(resource), false)) {
return MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent;
}
return MarkdownPreviewSecurityLevel.Strict;
}
public setSecurityLevelForResource(resource: vscode.Uri, level: MarkdownPreviewSecurityLevel): Thenable<void> {
return this.globalState.update(this.security_level_key + this.getRoot(resource), level);
}
public shouldAllowSvgsForResource(resource: vscode.Uri) {
const securityLevel = this.getSecurityLevelForResource(resource);
return securityLevel === MarkdownPreviewSecurityLevel.AllowInsecureContent || securityLevel === MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent;
}
public shouldDisableSecurityWarnings(): boolean {
return this.workspaceState.get<boolean>(this.should_disable_security_warning_key, false);
}
public setShouldDisableSecurityWarning(disabled: boolean): Thenable<void> {
return this.workspaceState.update(this.should_disable_security_warning_key, disabled);
}
private getRoot(resource: vscode.Uri): vscode.Uri {
if (vscode.workspace.workspaceFolders) {
const folderForResource = vscode.workspace.getWorkspaceFolder(resource);
if (folderForResource) {
return folderForResource.uri;
}
if (vscode.workspace.workspaceFolders.length) {
return vscode.workspace.workspaceFolders[0].uri;
}
}
return resource;
}
}
export class PreviewSecuritySelector {
public constructor(
private readonly cspArbiter: ContentSecurityPolicyArbiter,
private readonly webviewManager: MarkdownPreviewManager
) { }
public async showSecutitySelectorForResource(resource: vscode.Uri): Promise<void> {
interface PreviewSecurityPickItem extends vscode.QuickPickItem {
readonly type: 'moreinfo' | 'toggle' | MarkdownPreviewSecurityLevel;
}
function markActiveWhen(when: boolean): string {
return when ? '• ' : '';
}
const currentSecurityLevel = this.cspArbiter.getSecurityLevelForResource(resource);
const selection = await vscode.window.showQuickPick<PreviewSecurityPickItem>(
[
{
type: MarkdownPreviewSecurityLevel.Strict,
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.Strict) + localize('strict.title', 'Strict'),
description: localize('strict.description', 'Only load secure content'),
}, {
type: MarkdownPreviewSecurityLevel.AllowInsecureContent,
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowInsecureContent) + localize('insecureContent.title', 'Allow insecure content'),
description: localize('insecureContent.description', 'Enable loading content over http'),
}, {
type: MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent,
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent) + localize('disable.title', 'Disable'),
description: localize('disable.description', 'Allow all content and script execution. Not recommended'),
}, {
type: 'moreinfo',
label: localize('moreInfo.title', 'More Information'),
description: ''
}, {
type: 'toggle',
label: this.cspArbiter.shouldDisableSecurityWarnings()
? localize('enableSecurityWarning.title', "Enable preview security warnings in this workspace")
: localize('disableSecurityWarning.title', "Disable preview security warning in this workspace"),
description: localize('toggleSecurityWarning.description', 'Does not affect the content security level')
},
], {
placeHolder: localize(
'preview.showPreviewSecuritySelector.title',
'Select security settings for Markdown previews in this workspace'),
});
if (!selection) {
return;
}
if (selection.type === 'moreinfo') {
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=854414'));
return;
}
if (selection.type === 'toggle') {
this.cspArbiter.setShouldDisableSecurityWarning(!this.cspArbiter.shouldDisableSecurityWarnings());
return;
} else {
await this.cspArbiter.setSecurityLevelForResource(resource, selection.type);
}
this.webviewManager.refresh();
}
}

View File

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownEngine } from './markdownEngine';
export class Slug {
private static specialChars: any = { 'à': 'a', 'ä': 'a', 'ã': 'a', 'á': 'a', 'â': 'a', 'æ': 'a', 'å': 'a', 'ë': 'e', 'è': 'e', 'é': 'e', 'ê': 'e', 'î': 'i', 'ï': 'i', 'ì': 'i', 'í': 'i', 'ò': 'o', 'ó': 'o', 'ö': 'o', 'ô': 'o', 'ø': 'o', 'ù': 'o', 'ú': 'u', 'ü': 'u', 'û': 'u', 'ñ': 'n', 'ç': 'c', 'ß': 's', 'ÿ': 'y', 'œ': 'o', 'ŕ': 'r', 'ś': 's', 'ń': 'n', 'ṕ': 'p', 'ẃ': 'w', 'ǵ': 'g', 'ǹ': 'n', 'ḿ': 'm', 'ǘ': 'u', 'ẍ': 'x', 'ź': 'z', 'ḧ': 'h', '·': '-', '/': '-', '_': '-', ',': '-', ':': '-', ';': '-' };
public static fromHeading(heading: string): Slug {
const slugifiedHeading = encodeURI(heading.trim()
.toLowerCase()
.replace(/./g, c => Slug.specialChars[c] || c)
.replace(/[\]\[\!\'\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`]/g, '')
.replace(/\s+/g, '-') // Replace whitespace with -
.replace(/[^\w\-]+/g, '') // Remove remaining non-word chars
.replace(/^\-+/, '') // Remove leading -
.replace(/\-+$/, '') // Remove trailing -
);
return new Slug(slugifiedHeading);
}
private constructor(
public readonly value: string
) { }
public equals(other: Slug): boolean {
return this.value === other.value;
}
}
export interface TocEntry {
readonly slug: Slug;
readonly text: string;
readonly level: number;
readonly line: number;
readonly location: vscode.Location;
}
export class TableOfContentsProvider {
private toc?: TocEntry[];
public constructor(
private engine: MarkdownEngine,
private document: vscode.TextDocument
) { }
public async getToc(): Promise<TocEntry[]> {
if (!this.toc) {
try {
this.toc = await this.buildToc(this.document);
} catch (e) {
this.toc = [];
}
}
return this.toc;
}
public async lookup(fragment: string): Promise<TocEntry | undefined> {
const toc = await this.getToc();
const slug = Slug.fromHeading(fragment);
return toc.find(entry => entry.slug.equals(slug));
}
private async buildToc(document: vscode.TextDocument): Promise<TocEntry[]> {
const toc: TocEntry[] = [];
const tokens = await this.engine.parse(document.uri, document.getText());
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
const lineNumber = heading.map[0];
const line = document.lineAt(lineNumber);
toc.push({
slug: Slug.fromHeading(line.text),
text: TableOfContentsProvider.getHeaderText(line.text),
level: TableOfContentsProvider.getHeaderLevel(heading.markup),
line: lineNumber,
location: new vscode.Location(document.uri, line.range)
});
}
return toc;
}
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());
}
}

View File

@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* 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 { default as VSCodeTelemetryReporter } from 'vscode-extension-telemetry';
interface IPackageInfo {
name: string;
version: string;
aiKey: string;
}
export interface TelemetryReporter {
dispose(): void;
sendTelemetryEvent(eventName: string, properties?: {
[key: string]: string;
}): void;
}
const nullReporter = new class NullTelemetryReporter implements TelemetryReporter {
sendTelemetryEvent() { /** noop */ }
dispose() { /** noop */ }
};
class ExtensionReporter implements TelemetryReporter {
private readonly _reporter: VSCodeTelemetryReporter;
constructor(
packageInfo: IPackageInfo
) {
this._reporter = new VSCodeTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
}
sendTelemetryEvent(eventName: string, properties?: {
[key: string]: string;
}) {
this._reporter.sendTelemetryEvent(eventName, properties);
}
dispose() {
this._reporter.dispose();
}
}
export function loadDefaultTelemetryReporter(): TelemetryReporter {
const packageInfo = getPackageInfo();
return packageInfo ? new ExtensionReporter(packageInfo) : nullReporter;
}
function getPackageInfo(): IPackageInfo | null {
const extention = vscode.extensions.getExtension('Microsoft.vscode-markdown');
if (extention && extention.packageJSON) {
return {
name: extention.packageJSON.name,
version: extention.packageJSON.version,
aiKey: extention.packageJSON.aiKey
};
}
return null;
}

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* 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 vscode from 'vscode';
import 'mocha';
import { MarkdownEngine } from '../markdownEngine';
import { MarkdownContributions } from '../markdownExtensions';
import MarkdownFoldingProvider from '../features/foldingProvider';
import { InMemoryDocument } from './inMemoryDocument';
const testFileName = vscode.Uri.parse('test.md');
suite('markdown.FoldingProvider', () => {
test('Should not return anything for empty document', async () => {
const folds = await getFoldsForDocument(``);
assert.strictEqual(folds.ranges.length, 0);
});
test('Should not return anything for document without headers', async () => {
const folds = await getFoldsForDocument(`a
**b** afas
a#b
a`);
assert.strictEqual(folds.ranges.length, 0);
});
test('Should fold from header to end of document', async () => {
const folds = await getFoldsForDocument(`a
# b
c
d`);
assert.strictEqual(folds.ranges.length, 1);
const firstFold = folds.ranges[0];
assert.strictEqual(firstFold.startLine, 1);
assert.strictEqual(firstFold.endLine, 3);
});
test('Should leave single newline before next header', async () => {
const folds = await getFoldsForDocument(`
# a
x
# b
y`);
assert.strictEqual(folds.ranges.length, 2);
const firstFold = folds.ranges[0];
assert.strictEqual(firstFold.startLine, 1);
assert.strictEqual(firstFold.endLine, 3);
});
test('Should collapse multuple newlines to single newline before next header', async () => {
const folds = await getFoldsForDocument(`
# a
x
# b
y`);
assert.strictEqual(folds.ranges.length, 2);
const firstFold = folds.ranges[0];
assert.strictEqual(firstFold.startLine, 1);
assert.strictEqual(firstFold.endLine, 5);
});
test('Should not collapse if there is no newline before next header', async () => {
const folds = await getFoldsForDocument(`
# a
x
# b
y`);
assert.strictEqual(folds.ranges.length, 2);
const firstFold = folds.ranges[0];
assert.strictEqual(firstFold.startLine, 1);
assert.strictEqual(firstFold.endLine, 2);
});
});
async function getFoldsForDocument(contents: string) {
const doc = new InMemoryDocument(testFileName, contents);
const provider = new MarkdownFoldingProvider(newEngine());
return await provider.provideFoldingRanges(doc, {}, new vscode.CancellationTokenSource().token);
}
function newEngine(): MarkdownEngine {
return new MarkdownEngine(new class implements MarkdownContributions {
readonly previewScripts: vscode.Uri[] = [];
readonly previewStyles: vscode.Uri[] = [];
readonly previewResourceRoots: vscode.Uri[] = [];
readonly markdownItPlugins: Promise<(md: any) => any>[] = [];
});
}

View File

@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* 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';
export class InMemoryDocument implements vscode.TextDocument {
private readonly _lines: string[];
constructor(
public readonly uri: vscode.Uri,
private readonly _contents: string
) {
this._lines = this._contents.split(/\n/g);
}
fileName: string = '';
isUntitled: boolean = false;
languageId: string = '';
version: number = 1;
isDirty: boolean = false;
isClosed: boolean = false;
eol: vscode.EndOfLine = vscode.EndOfLine.LF;
get lineCount(): number {
return this._lines.length;
}
lineAt(line: any): vscode.TextLine {
return {
lineNumber: line,
text: this._lines[line],
range: new vscode.Range(0, 0, 0, 0),
firstNonWhitespaceCharacterIndex: 0,
rangeIncludingLineBreak: new vscode.Range(0, 0, 0, 0),
isEmptyOrWhitespace: false
};
}
offsetAt(_position: vscode.Position): never {
throw new Error('Method not implemented.');
}
positionAt(_offset: number): never {
throw new Error('Method not implemented.');
}
getText(_range?: vscode.Range | undefined): string {
return this._contents;
}
getWordRangeAtPosition(_position: vscode.Position, _regex?: RegExp | undefined): never {
throw new Error('Method not implemented.');
}
validateRange(_range: vscode.Range): never {
throw new Error('Method not implemented.');
}
validatePosition(_position: vscode.Position): never {
throw new Error('Method not implemented.');
}
save(): never {
throw new Error('Method not implemented.');
}
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//
// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
//
// This file is providing the test runner to use when running extension tests.
// By default the test runner in use is Mocha based.
//
// You can provide your own test runner if you want to override it by exporting
// a function run(testRoot: string, clb: (error:Error) => void) that the extension
// host can call to run the tests. The test runner is expected to use console.log
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.
const testRunner = require('vscode/lib/testrunner');
// You can directly control Mocha options by uncommenting the following lines
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
testRunner.configure({
ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
useColors: process.platform !== 'win32', // colored output from test results (only windows cannot handle)
timeout: 60000
});
export = testRunner;

View File

@@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* 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 vscode from 'vscode';
import 'mocha';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
import { MarkdownEngine } from '../markdownEngine';
import { MarkdownContributions } from '../markdownExtensions';
import { InMemoryDocument } from './inMemoryDocument';
const testFileName = vscode.Uri.parse('test.md');
suite('markdown.TableOfContentsProvider', () => {
test('Lookup should not return anything for empty document', async () => {
const doc = new InMemoryDocument(testFileName, '');
const provider = new TableOfContentsProvider(newEngine(), doc);
assert.strictEqual(await provider.lookup(''), undefined);
assert.strictEqual(await provider.lookup('foo'), undefined);
});
test('Lookup should not return anything for document with no headers', async () => {
const doc = new InMemoryDocument(testFileName, 'a *b*\nc');
const provider = new TableOfContentsProvider(newEngine(), doc);
assert.strictEqual(await provider.lookup(''), undefined);
assert.strictEqual(await provider.lookup('foo'), undefined);
assert.strictEqual(await provider.lookup('a'), undefined);
assert.strictEqual(await provider.lookup('b'), undefined);
});
test('Lookup should return basic #header', async () => {
const doc = new InMemoryDocument(testFileName, `# a\nx\n# c`);
const provider = new TableOfContentsProvider(newEngine(), doc);
{
const entry = await provider.lookup('a');
assert.ok(entry);
assert.strictEqual(entry!.line, 0);
}
{
assert.strictEqual(await provider.lookup('x'), undefined);
}
{
const entry = await provider.lookup('c');
assert.ok(entry);
assert.strictEqual(entry!.line, 2);
}
});
test('Lookups should be case in-sensitive', async () => {
const doc = new InMemoryDocument(testFileName, `# fOo\n`);
const provider = new TableOfContentsProvider(newEngine(), doc);
assert.strictEqual((await provider.lookup('fOo'))!.line, 0);
assert.strictEqual((await provider.lookup('foo'))!.line, 0);
assert.strictEqual((await provider.lookup('FOO'))!.line, 0);
});
test('Lookups should ignore leading and trailing white-space, and collapse internal whitespace', async () => {
const doc = new InMemoryDocument(testFileName, `# f o o \n`);
const provider = new TableOfContentsProvider(newEngine(), doc);
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
assert.strictEqual((await provider.lookup(' f o o'))!.line, 0);
assert.strictEqual((await provider.lookup(' f o o '))!.line, 0);
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
assert.strictEqual(await provider.lookup('f'), undefined);
assert.strictEqual(await provider.lookup('foo'), undefined);
assert.strictEqual(await provider.lookup('fo o'), undefined);
});
test('should normalize special characters #44779', async () => {
const doc = new InMemoryDocument(testFileName, `# Indentação\n`);
const provider = new TableOfContentsProvider(newEngine(), doc);
assert.strictEqual((await provider.lookup('indentacao'))!.line, 0);
});
});
function newEngine(): MarkdownEngine {
return new MarkdownEngine(new class implements MarkdownContributions {
readonly previewScripts: vscode.Uri[] = [];
readonly previewStyles: vscode.Uri[] = [];
readonly previewResourceRoots: vscode.Uri[] = [];
readonly markdownItPlugins: Promise<(md: any) => any>[] = [];
});
}

View File

@@ -0,0 +1,5 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'markdown-it-named-headers' { }

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.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
/// <reference types='@types/node'/>

View File

@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* 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';
export function disposeAll(disposables: vscode.Disposable[]) {
while (disposables.length) {
const item = disposables.pop();
if (item) {
item.dispose();
}
}
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* 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';
export function isMarkdownFile(document: vscode.TextDocument) {
return document.languageId === 'markdown';
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* 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 { disposeAll } from '../util/dispose';
import { isMarkdownFile } from './file';
export class MarkdownFileTopmostLineMonitor {
private readonly disposables: vscode.Disposable[] = [];
private readonly pendingUpdates = new Map<string, number>();
constructor() {
vscode.window.onDidChangeTextEditorVisibleRanges(event => {
if (isMarkdownFile(event.textEditor.document)) {
const line = getVisibleLine(event.textEditor);
if (line) {
this.updateLine(event.textEditor.document.uri, line);
}
}
}, null, this.disposables);
}
dispose() {
disposeAll(this.disposables);
}
private readonly _onDidChangeTopmostLineEmitter = new vscode.EventEmitter<{ resource: vscode.Uri, line: number }>();
public readonly onDidChangeTopmostLine = this._onDidChangeTopmostLineEmitter.event;
private updateLine(
resource: vscode.Uri,
line: number
) {
const key = resource.toString();
if (!this.pendingUpdates.has(key)) {
// schedule update
setTimeout(() => {
if (this.pendingUpdates.has(key)) {
this._onDidChangeTopmostLineEmitter.fire({
resource,
line: this.pendingUpdates.get(key) as number
});
this.pendingUpdates.delete(key);
}
}, 50);
}
this.pendingUpdates.set(key, line);
}
}
/**
* Get the top-most visible range of `editor`.
*
* Returns a fractional line number based the visible character within the line.
* Floor to get real line number
*/
export function getVisibleLine(
editor: vscode.TextEditor
): number | undefined {
if (!editor.visibleRanges.length) {
return undefined;
}
const firstVisiblePosition = editor.visibleRanges[0].start;
const lineNumber = firstVisiblePosition.line;
const line = editor.document.lineAt(lineNumber);
const progress = firstVisiblePosition.character / (line.text.length + 2);
return lineNumber + progress;
}