mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Merge VS Code 1.21 source code (#1067)
* Initial VS Code 1.21 file copy with patches * A few more merges * Post npm install * Fix batch of build breaks * Fix more build breaks * Fix more build errors * Fix more build breaks * Runtime fixes 1 * Get connection dialog working with some todos * Fix a few packaging issues * Copy several node_modules to package build to fix loader issues * Fix breaks from master * A few more fixes * Make tests pass * First pass of license header updates * Second pass of license header updates * Fix restore dialog issues * Remove add additional themes menu items * fix select box issues where the list doesn't show up * formatting * Fix editor dispose issue * Copy over node modules to correct location on all platforms
This commit is contained in:
@@ -1,291 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.config(process.env.VSCODE_NLS_CONFIG)();
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Command } from './commandManager';
|
||||
import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security';
|
||||
import { getMarkdownUri, MDDocumentContentProvider, isMarkdownFile } from './features/previewContentProvider';
|
||||
import { Logger } from './logger';
|
||||
import { TableOfContentsProvider } from './tableOfContentsProvider';
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
import { TelemetryReporter } from './telemetryReporter';
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function showPreview(
|
||||
cspArbiter: ExtensionContentSecurityPolicyArbiter,
|
||||
telemetryReporter: TelemetryReporter,
|
||||
uri?: vscode.Uri,
|
||||
sideBySide: boolean = false,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
const thenable = vscode.commands.executeCommand('vscode.previewHtml',
|
||||
getMarkdownUri(resource),
|
||||
getViewColumn(sideBySide),
|
||||
localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)),
|
||||
{
|
||||
allowScripts: true,
|
||||
allowSvgs: cspArbiter.shouldAllowSvgsForResource(resource)
|
||||
});
|
||||
|
||||
telemetryReporter.sendTelemetryEvent('openPreview', {
|
||||
where: sideBySide ? 'sideBySide' : 'inPlace',
|
||||
how: (uri instanceof vscode.Uri) ? 'action' : 'pallete'
|
||||
});
|
||||
|
||||
return thenable;
|
||||
}
|
||||
|
||||
export class ShowPreviewCommand implements Command {
|
||||
public readonly id = 'markdown.showPreview';
|
||||
|
||||
public constructor(
|
||||
private readonly cspArbiter: ExtensionContentSecurityPolicyArbiter,
|
||||
private readonly telemetryReporter: TelemetryReporter
|
||||
) { }
|
||||
|
||||
public execute(uri?: vscode.Uri) {
|
||||
showPreview(this.cspArbiter, this.telemetryReporter, uri, false);
|
||||
}
|
||||
}
|
||||
|
||||
export class ShowPreviewToSideCommand implements Command {
|
||||
public readonly id = 'markdown.showPreviewToSide';
|
||||
|
||||
public constructor(
|
||||
private readonly cspArbiter: ExtensionContentSecurityPolicyArbiter,
|
||||
private readonly telemetryReporter: TelemetryReporter
|
||||
) { }
|
||||
|
||||
public execute(uri?: vscode.Uri) {
|
||||
showPreview(this.cspArbiter, this.telemetryReporter, uri, true);
|
||||
}
|
||||
}
|
||||
|
||||
export class ShowSourceCommand implements Command {
|
||||
public readonly id = 'markdown.showSource';
|
||||
|
||||
public execute(mdUri?: vscode.Uri) {
|
||||
if (!mdUri) {
|
||||
return vscode.commands.executeCommand('workbench.action.navigateBack');
|
||||
}
|
||||
|
||||
const docUri = vscode.Uri.parse(mdUri.query);
|
||||
for (const editor of vscode.window.visibleTextEditors) {
|
||||
if (editor.document.uri.scheme === docUri.scheme && editor.document.uri.toString() === docUri.toString()) {
|
||||
return vscode.window.showTextDocument(editor.document, editor.viewColumn);
|
||||
}
|
||||
}
|
||||
|
||||
return vscode.workspace.openTextDocument(docUri)
|
||||
.then(vscode.window.showTextDocument);
|
||||
}
|
||||
}
|
||||
|
||||
export class RefreshPreviewCommand implements Command {
|
||||
public readonly id = 'markdown.refreshPreview';
|
||||
|
||||
public constructor(
|
||||
private readonly contentProvider: MDDocumentContentProvider
|
||||
) { }
|
||||
|
||||
public execute(resource: string | undefined) {
|
||||
if (resource) {
|
||||
const source = vscode.Uri.parse(resource);
|
||||
this.contentProvider.update(source);
|
||||
} else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
|
||||
this.contentProvider.update(getMarkdownUri(vscode.window.activeTextEditor.document.uri));
|
||||
} else {
|
||||
// update all generated md documents
|
||||
for (const document of vscode.workspace.textDocuments) {
|
||||
if (document.uri.scheme === 'markdown') {
|
||||
this.contentProvider.update(document.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ShowPreviewSecuritySelectorCommand implements Command {
|
||||
public readonly id = 'markdown.showPreviewSecuritySelector';
|
||||
|
||||
public constructor(
|
||||
private readonly previewSecuritySelector: PreviewSecuritySelector
|
||||
) { }
|
||||
|
||||
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 && vscode.window.activeTextEditor.document.languageId === 'markdown') {
|
||||
this.previewSecuritySelector.showSecutitySelectorForResource(vscode.window.activeTextEditor.document.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RevealLineCommand implements Command {
|
||||
public readonly id = '_markdown.revealLine';
|
||||
|
||||
public constructor(
|
||||
private logger: Logger
|
||||
) { }
|
||||
|
||||
public execute(uri: string, line: number) {
|
||||
const sourceUri = vscode.Uri.parse(decodeURIComponent(uri));
|
||||
this.logger.log('revealLine', { uri, sourceUri: sourceUri.toString(), line });
|
||||
|
||||
vscode.window.visibleTextEditors
|
||||
.filter(editor => isMarkdownFile(editor.document) && editor.document.uri.toString() === sourceUri.toString())
|
||||
.forEach(editor => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DidClickCommand implements Command {
|
||||
public readonly id = '_markdown.didClick';
|
||||
|
||||
public execute(uri: string, line: number) {
|
||||
const sourceUri = vscode.Uri.parse(decodeURIComponent(uri));
|
||||
return vscode.workspace.openTextDocument(sourceUri)
|
||||
.then(document => vscode.window.showTextDocument(document))
|
||||
.then(editor =>
|
||||
vscode.commands.executeCommand('revealLine', { lineNumber: Math.floor(line), at: 'center' })
|
||||
.then(() => editor))
|
||||
.then(editor => {
|
||||
if (editor) {
|
||||
editor.selection = new vscode.Selection(
|
||||
new vscode.Position(Math.floor(line), 0),
|
||||
new vscode.Position(Math.floor(line), 0));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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(', ')));
|
||||
}
|
||||
}
|
||||
|
||||
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 tryRevealLine = async (editor: vscode.TextEditor) => {
|
||||
if (editor && args.fragment) {
|
||||
const toc = new TableOfContentsProvider(this.engine, editor.document);
|
||||
const line = await toc.lookup(args.fragment);
|
||||
if (!isNaN(line)) {
|
||||
return editor.revealRange(
|
||||
new vscode.Range(line, 0, line, 0),
|
||||
vscode.TextEditorRevealType.AtTop);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tryOpen = async (path: string) => {
|
||||
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document) && vscode.window.activeTextEditor.document.uri.fsPath === path) {
|
||||
return tryRevealLine(vscode.window.activeTextEditor);
|
||||
} else {
|
||||
const resource = vscode.Uri.file(path);
|
||||
return vscode.workspace.openTextDocument(resource)
|
||||
.then(vscode.window.showTextDocument)
|
||||
.then(tryRevealLine);
|
||||
}
|
||||
};
|
||||
|
||||
return tryOpen(args.path).catch(() => {
|
||||
if (path.extname(args.path) === '') {
|
||||
return tryOpen(args.path + '.md');
|
||||
}
|
||||
const resource = vscode.Uri.file(args.path);
|
||||
return Promise.resolve(void 0)
|
||||
.then(() => vscode.commands.executeCommand('vscode.open', resource))
|
||||
.then(() => void 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
13
extensions/markdown/src/commands/index.ts
Normal file
13
extensions/markdown/src/commands/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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';
|
||||
22
extensions/markdown/src/commands/moveCursorToPosition.ts
Normal file
22
extensions/markdown/src/commands/moveCursorToPosition.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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;
|
||||
}
|
||||
}
|
||||
18
extensions/markdown/src/commands/onPreviewStyleLoadError.ts
Normal file
18
extensions/markdown/src/commands/onPreviewStyleLoadError.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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(', ')));
|
||||
}
|
||||
}
|
||||
74
extensions/markdown/src/commands/openDocumentLink.ts
Normal file
74
extensions/markdown/src/commands/openDocumentLink.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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) {
|
||||
return this.tryOpen(args.path, args).catch(() => {
|
||||
if (path.extname(args.path) === '') {
|
||||
return this.tryOpen(args.path + '.md', args);
|
||||
}
|
||||
const resource = vscode.Uri.file(args.path);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
extensions/markdown/src/commands/refreshPreview.ts
Normal file
19
extensions/markdown/src/commands/refreshPreview.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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();
|
||||
}
|
||||
}
|
||||
123
extensions/markdown/src/commands/showPreview.ts
Normal file
123
extensions/markdown/src/commands/showPreview.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
extensions/markdown/src/commands/showSource.ts
Normal file
30
extensions/markdown/src/commands/showSource.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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;
|
||||
}
|
||||
}
|
||||
20
extensions/markdown/src/commands/toggleLock.ts
Normal file
20
extensions/markdown/src/commands/toggleLock.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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);
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
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 vscode.Uri.parse(`command:_markdown.openDocumentLink?${encodeURIComponent(JSON.stringify({ fragment: uri.fragment, path: resourcePath }))}`);
|
||||
}
|
||||
|
||||
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 linkPattern = /(\[[^\]]*\]\(\s*?)(((((?=.*\)\)+)|(?=.*\)\]+))[^\s\)]+?)|([^\s]+)))\)/g;
|
||||
private referenceLinkPattern = /(\[([^\]]+)\]\[\s*?)([^\s\]]*?)\]/g;
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
import { TableOfContentsProvider } from './tableOfContentsProvider';
|
||||
|
||||
export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||
|
||||
constructor(
|
||||
private 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,13 @@ import { MarkdownEngine } from './markdownEngine';
|
||||
import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security';
|
||||
import { Logger } from './logger';
|
||||
import { CommandManager } from './commandManager';
|
||||
import * as commands from './commands';
|
||||
import * as commands from './commands/index';
|
||||
import { loadDefaultTelemetryReporter } from './telemetryReporter';
|
||||
import { loadMarkdownExtensions } from './markdownExtensions';
|
||||
import LinkProvider from './features/documentLinkProvider';
|
||||
import MDDocumentSymbolProvider from './features/documentSymbolProvider';
|
||||
import { MDDocumentContentProvider, getMarkdownUri, isMarkdownFile } from './features/previewContentProvider';
|
||||
import { MarkdownContentProvider } from './features/previewContentProvider';
|
||||
import { MarkdownPreviewManager } from './features/previewManager';
|
||||
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
@@ -27,58 +28,32 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
const selector = 'markdown';
|
||||
|
||||
const contentProvider = new MDDocumentContentProvider(engine, context, cspArbiter, logger);
|
||||
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(selector, contentProvider));
|
||||
|
||||
const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, logger);
|
||||
loadMarkdownExtensions(contentProvider, engine);
|
||||
|
||||
const previewManager = new MarkdownPreviewManager(contentProvider, logger);
|
||||
context.subscriptions.push(previewManager);
|
||||
|
||||
context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(selector, new MDDocumentSymbolProvider(engine)));
|
||||
context.subscriptions.push(vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider()));
|
||||
|
||||
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, contentProvider);
|
||||
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
|
||||
|
||||
const commandManager = new CommandManager();
|
||||
context.subscriptions.push(commandManager);
|
||||
commandManager.register(new commands.ShowPreviewCommand(cspArbiter, telemetryReporter));
|
||||
commandManager.register(new commands.ShowPreviewToSideCommand(cspArbiter, telemetryReporter));
|
||||
commandManager.register(new commands.ShowSourceCommand());
|
||||
commandManager.register(new commands.RefreshPreviewCommand(contentProvider));
|
||||
commandManager.register(new commands.RevealLineCommand(logger));
|
||||
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));
|
||||
commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
|
||||
commandManager.register(new commands.OnPreviewStyleLoadErrorCommand());
|
||||
commandManager.register(new commands.DidClickCommand());
|
||||
commandManager.register(new commands.OpenDocumentLinkCommand(engine));
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => {
|
||||
if (isMarkdownFile(document)) {
|
||||
const uri = getMarkdownUri(document.uri);
|
||||
contentProvider.update(uri);
|
||||
}
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(event => {
|
||||
if (isMarkdownFile(event.document)) {
|
||||
const uri = getMarkdownUri(event.document.uri);
|
||||
contentProvider.update(uri);
|
||||
}
|
||||
}));
|
||||
commandManager.register(new commands.ToggleLockCommand(previewManager));
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
|
||||
logger.updateConfiguration();
|
||||
contentProvider.updateConfiguration();
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(event => {
|
||||
if (isMarkdownFile(event.textEditor.document)) {
|
||||
const markdownFile = getMarkdownUri(event.textEditor.document.uri);
|
||||
logger.log('updatePreviewForSelection', { markdownFile: markdownFile.toString() });
|
||||
|
||||
vscode.commands.executeCommand('_workbench.htmlPreview.postMessage',
|
||||
markdownFile,
|
||||
{
|
||||
line: event.selections[0].active.line
|
||||
});
|
||||
}
|
||||
previewManager.updateConfiguration();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { OpenDocumentLinkCommand } from '../commands';
|
||||
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
|
||||
|
||||
function normalizeLink(
|
||||
document: vscode.TextDocument,
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
@@ -13,7 +11,7 @@ import { TableOfContentsProvider } from '../tableOfContentsProvider';
|
||||
export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||
|
||||
constructor(
|
||||
private engine: MarkdownEngine
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideDocumentSymbols(document: vscode.TextDocument): Promise<vscode.SymbolInformation[]> {
|
||||
|
||||
299
extensions/markdown/src/features/preview.ts
Normal file
299
extensions/markdown/src/features/preview.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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';
|
||||
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
|
||||
) {
|
||||
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,
|
||||
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 folder = vscode.workspace.getWorkspaceFolder(resource);
|
||||
if (folder) {
|
||||
return [folder.uri];
|
||||
}
|
||||
|
||||
if (!resource.scheme || resource.scheme === 'file') {
|
||||
return [vscode.Uri.file(path.dirname(resource.fsPath))];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
104
extensions/markdown/src/features/previewConfig.ts
Normal file
104
extensions/markdown/src/features/previewConfig.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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() : '';
|
||||
}
|
||||
}
|
||||
@@ -3,154 +3,46 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Logger } from '../logger';
|
||||
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { Logger } from '../logger';
|
||||
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security';
|
||||
import { MarkdownPreviewConfigurationManager, MarkdownPreviewConfiguration } from './previewConfig';
|
||||
|
||||
/**
|
||||
* 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')
|
||||
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 function isMarkdownFile(document: vscode.TextDocument) {
|
||||
return document.languageId === 'markdown'
|
||||
&& document.uri.scheme !== 'markdown'; // prevent processing of own documents
|
||||
}
|
||||
|
||||
export function getMarkdownUri(uri: vscode.Uri) {
|
||||
if (uri.scheme === 'markdown') {
|
||||
return uri;
|
||||
}
|
||||
|
||||
return uri.with({
|
||||
scheme: 'markdown',
|
||||
path: uri.path + '.rendered',
|
||||
query: uri.toString()
|
||||
});
|
||||
}
|
||||
|
||||
class MarkdownPreviewConfig {
|
||||
public static getConfigForResource(resource: vscode.Uri) {
|
||||
return new MarkdownPreviewConfig(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 scrollPreviewWithEditorSelection: 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.scrollPreviewWithEditorSelection = !!markdownConfig.get<boolean>('preview.scrollPreviewWithEditorSelection', 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: MarkdownPreviewConfig) {
|
||||
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;
|
||||
}
|
||||
|
||||
class PreviewConfigManager {
|
||||
private previewConfigurationsForWorkspaces = new Map<string, MarkdownPreviewConfig>();
|
||||
|
||||
public loadAndCacheConfiguration(
|
||||
resource: vscode.Uri
|
||||
) {
|
||||
const config = MarkdownPreviewConfig.getConfigForResource(resource);
|
||||
this.previewConfigurationsForWorkspaces.set(this.getKey(resource), config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public shouldUpdateConfiguration(
|
||||
resource: vscode.Uri
|
||||
): boolean {
|
||||
const key = this.getKey(resource);
|
||||
const currentConfig = this.previewConfigurationsForWorkspaces.get(key);
|
||||
const newConfig = MarkdownPreviewConfig.getConfigForResource(resource);
|
||||
return (!currentConfig || !currentConfig.isEqualTo(newConfig));
|
||||
}
|
||||
|
||||
private getKey(
|
||||
resource: vscode.Uri
|
||||
): string {
|
||||
const folder = vscode.workspace.getWorkspaceFolder(resource);
|
||||
if (!folder) {
|
||||
return '';
|
||||
}
|
||||
return folder.uri.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export class MDDocumentContentProvider implements vscode.TextDocumentContentProvider {
|
||||
private _onDidChange = new vscode.EventEmitter<vscode.Uri>();
|
||||
private _waiting: boolean = false;
|
||||
private previewConfigurations = new PreviewConfigManager();
|
||||
|
||||
private extraStyles: Array<vscode.Uri> = [];
|
||||
private extraScripts: Array<vscode.Uri> = [];
|
||||
export class MarkdownContentProvider {
|
||||
private readonly extraStyles: Array<vscode.Uri> = [];
|
||||
private readonly extraScripts: Array<vscode.Uri> = [];
|
||||
|
||||
constructor(
|
||||
private engine: MarkdownEngine,
|
||||
private context: vscode.ExtensionContext,
|
||||
private cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private logger: Logger
|
||||
private readonly engine: MarkdownEngine,
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private readonly logger: Logger
|
||||
) { }
|
||||
|
||||
public addScript(resource: vscode.Uri): void {
|
||||
@@ -161,90 +53,18 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv
|
||||
this.extraStyles.push(resource);
|
||||
}
|
||||
|
||||
private getMediaPath(mediaFile: string): string {
|
||||
return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))).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 (['file', 'http', 'https'].indexOf(hrefUri.scheme) >= 0) {
|
||||
return hrefUri.toString();
|
||||
}
|
||||
|
||||
// Use href as file URI if it is absolute
|
||||
if (path.isAbsolute(href)) {
|
||||
return vscode.Uri.file(href).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)).toString();
|
||||
}
|
||||
|
||||
// otherwise look relative to the markdown file
|
||||
return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)).toString();
|
||||
}
|
||||
|
||||
private computeCustomStyleSheetIncludes(resource: vscode.Uri, config: MarkdownPreviewConfig): string {
|
||||
if (config.styles && Array.isArray(config.styles)) {
|
||||
return config.styles.map(style => {
|
||||
return `<link rel="stylesheet" class="code-user-style" data-source="${style.replace(/"/g, '"')}" href="${this.fixHref(resource, style)}" type="text/css" media="screen">`;
|
||||
}).join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private getSettingsOverrideStyles(nonce: string, config: MarkdownPreviewConfig): 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: MarkdownPreviewConfig): string {
|
||||
const baseStyles = [
|
||||
this.getMediaPath('markdown.css'),
|
||||
this.getMediaPath('tomorrow.css')
|
||||
].concat(this.extraStyles.map(resource => resource.toString()));
|
||||
|
||||
return `${baseStyles.map(href => `<link rel="stylesheet" type="text/css" href="${href}">`).join('\n')}
|
||||
${this.getSettingsOverrideStyles(nonce, config)}
|
||||
${this.computeCustomStyleSheetIncludes(resource, config)}`;
|
||||
}
|
||||
|
||||
private getScripts(nonce: string): string {
|
||||
const scripts = [this.getMediaPath('main.js')].concat(this.extraScripts.map(resource => resource.toString()));
|
||||
return scripts
|
||||
.map(source => `<script async src="${source}" nonce="${nonce}" charset="UTF-8"></script>`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
public async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
|
||||
const sourceUri = vscode.Uri.parse(uri.query);
|
||||
|
||||
let initialLine: number | undefined = undefined;
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && editor.document.uri.toString() === sourceUri.toString()) {
|
||||
initialLine = editor.selection.active.line;
|
||||
}
|
||||
|
||||
const document = await vscode.workspace.openTextDocument(sourceUri);
|
||||
const config = this.previewConfigurations.loadAndCacheConfiguration(sourceUri);
|
||||
|
||||
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 = {
|
||||
previewUri: uri.toString(),
|
||||
source: sourceUri.toString(),
|
||||
line: initialLine,
|
||||
scrollPreviewWithEditorSelection: config.scrollPreviewWithEditorSelection,
|
||||
lineCount: markdownDocument.lineCount,
|
||||
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
|
||||
scrollEditorWithPreview: config.scrollEditorWithPreview,
|
||||
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,
|
||||
disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings()
|
||||
@@ -256,63 +76,112 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv
|
||||
const nonce = new Date().getTime() + '' + new Date().getMilliseconds();
|
||||
const csp = this.getCspForResource(sourceUri, nonce);
|
||||
|
||||
const body = await this.engine.render(sourceUri, config.previewFrontMatter === 'hide', document.getText());
|
||||
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, '"')}" data-strings="${JSON.stringify(previewStrings).replace(/"/g, '"')}">
|
||||
<script src="${this.getMediaPath('csp.js')}" nonce="${nonce}"></script>
|
||||
<script src="${this.getMediaPath('loading.js')}" nonce="${nonce}"></script>
|
||||
<script src="${this.extensionResourcePath('csp.js')}" nonce="${nonce}"></script>
|
||||
<script src="${this.extensionResourcePath('loading.js')}" nonce="${nonce}"></script>
|
||||
${this.getStyles(sourceUri, nonce, config)}
|
||||
<base href="${document.uri.toString(true)}">
|
||||
<base href="${markdownDocument.uri.with({ scheme: 'vscode-workspace-resource' }).toString(true)}">
|
||||
</head>
|
||||
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
|
||||
${body}
|
||||
<div class="code-line" data-line="${document.lineCount}"></div>
|
||||
<div class="code-line" data-line="${markdownDocument.lineCount}"></div>
|
||||
${this.getScripts(nonce)}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
// update all generated md documents
|
||||
for (const document of vscode.workspace.textDocuments) {
|
||||
if (document.uri.scheme === 'markdown') {
|
||||
const sourceUri = vscode.Uri.parse(document.uri.query);
|
||||
if (this.previewConfigurations.shouldUpdateConfiguration(sourceUri)) {
|
||||
this.update(document.uri);
|
||||
}
|
||||
private extensionResourcePath(mediaFile: string): string {
|
||||
return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile)))
|
||||
.with({ scheme: 'vscode-extension-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-workspace-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-workspace-resource' })
|
||||
.toString();
|
||||
}
|
||||
|
||||
// Otherwise look relative to the markdown file
|
||||
return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))
|
||||
.with({ scheme: 'vscode-workspace-resource' })
|
||||
.toString();
|
||||
}
|
||||
|
||||
private computeCustomStyleSheetIncludes(resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
|
||||
if (config.styles && Array.isArray(config.styles)) {
|
||||
return config.styles.map(style => {
|
||||
return `<link rel="stylesheet" class="code-user-style" data-source="${style.replace(/"/g, '"')}" 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>`;
|
||||
}
|
||||
|
||||
get onDidChange(): vscode.Event<vscode.Uri> {
|
||||
return this._onDidChange.event;
|
||||
private getStyles(resource: vscode.Uri, nonce: string, config: MarkdownPreviewConfiguration): string {
|
||||
const baseStyles = [
|
||||
this.extensionResourcePath('markdown.css'),
|
||||
this.extensionResourcePath('tomorrow.css')
|
||||
].concat(this.extraStyles.map(resource => resource.toString()));
|
||||
|
||||
return `${baseStyles.map(href => `<link rel="stylesheet" type="text/css" href="${href}">`).join('\n')}
|
||||
${this.getSettingsOverrideStyles(nonce, config)}
|
||||
${this.computeCustomStyleSheetIncludes(resource, config)}`;
|
||||
}
|
||||
|
||||
public update(uri: vscode.Uri) {
|
||||
if (!this._waiting) {
|
||||
this._waiting = true;
|
||||
setTimeout(() => {
|
||||
this._waiting = false;
|
||||
this._onDidChange.fire(uri);
|
||||
}, 300);
|
||||
}
|
||||
private getScripts(nonce: string): string {
|
||||
const scripts = [this.extensionResourcePath('main.js')].concat(this.extraScripts.map(resource => resource.toString()));
|
||||
return scripts
|
||||
.map(source => `<script async src="${source}" 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 'self' http: https: data:; media-src 'self' http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' 'unsafe-inline' http: https: data:; font-src 'self' http: https: data:;">`;
|
||||
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-workspace-resource: vscode-extension-resource: http: https: data:; media-src vscode-workspace-resource: vscode-extension-resource: http: https: data:; script-src 'nonce-${nonce}'; style-src vscode-workspace-resource: 'unsafe-inline' http: https: data: vscode-extension-resource:; font-src vscode-workspace-resource: vscode-extension-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 'self' https: data:; media-src 'self' https: data:; script-src 'nonce-${nonce}'; style-src 'self' 'unsafe-inline' https: data:; font-src 'self' https: data:;">`;
|
||||
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-workspace-resource: vscode-extension-resource: https: data:; media-src vscode-workspace-resource: vscode-extension-resource: https: data:; script-src 'nonce-${nonce}'; style-src vscode-workspace-resource: 'unsafe-inline' https: data: vscode-extension-resource:; font-src vscode-workspace-resource: vscode-extension-resource: https: data:;">`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
141
extensions/markdown/src/features/previewManager.ts
Normal file
141
extensions/markdown/src/features/previewManager.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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';
|
||||
|
||||
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
|
||||
) {
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { OutputChannel, window, workspace } from 'vscode';
|
||||
|
||||
enum Trace {
|
||||
@@ -32,8 +30,8 @@ function isString(value: any): value is string {
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
private trace: Trace;
|
||||
private _output: OutputChannel;
|
||||
private trace?: Trace;
|
||||
private _output?: OutputChannel;
|
||||
|
||||
constructor() {
|
||||
this.updateConfiguration();
|
||||
@@ -43,7 +41,7 @@ export class Logger {
|
||||
if (this.trace === Trace.Verbose) {
|
||||
this.output.appendLine(`[Log - ${(new Date().toLocaleTimeString())}] ${message}`);
|
||||
if (data) {
|
||||
this.output.appendLine(this.data2String(data));
|
||||
this.output.appendLine(Logger.data2String(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +61,7 @@ export class Logger {
|
||||
return Trace.fromString(workspace.getConfiguration().get<string>('markdown.trace', 'off'));
|
||||
}
|
||||
|
||||
private data2String(data: any): string {
|
||||
private static data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
if (isString(data.stack)) {
|
||||
return data.stack;
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { TableOfContentsProvider } from './tableOfContentsProvider';
|
||||
import { Slug } from './tableOfContentsProvider';
|
||||
import { MarkdownIt, Token } from 'markdown-it';
|
||||
|
||||
const FrontMatterRegex = /^---\s*[^]*?(-{3}|\.{3})\s*/;
|
||||
|
||||
export class MarkdownEngine {
|
||||
private md: MarkdownIt;
|
||||
private md?: MarkdownIt;
|
||||
|
||||
private firstLine: number;
|
||||
private firstLine?: number;
|
||||
|
||||
private currentDocument: vscode.Uri;
|
||||
private currentDocument?: vscode.Uri;
|
||||
|
||||
private plugins: Array<(md: any) => any> = [];
|
||||
|
||||
@@ -51,10 +51,10 @@ export class MarkdownEngine {
|
||||
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>`;
|
||||
return `<pre class="hljs"><code><div>${this.md!.utils.escapeHtml(str)}</div></code></pre>`;
|
||||
}
|
||||
}).use(mdnh, {
|
||||
slugify: (header: string) => TableOfContentsProvider.slugify(header)
|
||||
slugify: (header: string) => Slug.fromHeading(header).value
|
||||
});
|
||||
|
||||
for (const plugin of this.plugins) {
|
||||
@@ -137,17 +137,22 @@ export class MarkdownEngine {
|
||||
md.normalizeLink = (link: string) => {
|
||||
try {
|
||||
let uri = vscode.Uri.parse(link);
|
||||
if (!uri.scheme && uri.path && !uri.fragment) {
|
||||
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);
|
||||
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));
|
||||
uri = vscode.Uri.file(path.join(path.dirname(this.currentDocument!.path), uri.path));
|
||||
}
|
||||
return normalizeLink(uri.toString(true));
|
||||
|
||||
if (fragment) {
|
||||
uri = uri.with({ fragment });
|
||||
}
|
||||
return normalizeLink(uri.with({ scheme: 'vscode-workspace-resource' }).toString(true));
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
|
||||
@@ -6,20 +6,17 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { MDDocumentContentProvider } from './features/previewContentProvider';
|
||||
import { MarkdownContentProvider } from './features/previewContentProvider';
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
|
||||
const resolveExtensionResources = (extension: vscode.Extension<any>, stylePath: string): vscode.Uri => {
|
||||
const resource = vscode.Uri.parse(stylePath);
|
||||
if (resource.scheme) {
|
||||
return resource;
|
||||
}
|
||||
return vscode.Uri.file(path.join(extension.extensionPath, stylePath));
|
||||
const resolveExtensionResources = (extension: vscode.Extension<any>, resourcePath: string): vscode.Uri => {
|
||||
return vscode.Uri.file(path.join(extension.extensionPath, resourcePath))
|
||||
.with({ scheme: 'vscode-extension-resource' });
|
||||
};
|
||||
|
||||
|
||||
export function loadMarkdownExtensions(
|
||||
contentProvider: MDDocumentContentProvider,
|
||||
contentProvider: MarkdownContentProvider,
|
||||
engine: MarkdownEngine
|
||||
) {
|
||||
for (const extension of vscode.extensions.all) {
|
||||
@@ -50,7 +47,7 @@ function tryLoadMarkdownItPlugins(
|
||||
|
||||
function tryLoadPreviewScripts(
|
||||
contributes: any,
|
||||
contentProvider: MDDocumentContentProvider,
|
||||
contentProvider: MarkdownContentProvider,
|
||||
extension: vscode.Extension<any>
|
||||
) {
|
||||
const scripts = contributes['markdown.previewScripts'];
|
||||
@@ -58,8 +55,7 @@ function tryLoadPreviewScripts(
|
||||
for (const script of scripts) {
|
||||
try {
|
||||
contentProvider.addScript(resolveExtensionResources(extension, script));
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -68,7 +64,7 @@ function tryLoadPreviewScripts(
|
||||
|
||||
function tryLoadPreviewStyles(
|
||||
contributes: any,
|
||||
contentProvider: MDDocumentContentProvider,
|
||||
contentProvider: MarkdownContentProvider,
|
||||
extension: vscode.Extension<any>
|
||||
) {
|
||||
const styles = contributes['markdown.previewStyles'];
|
||||
@@ -76,8 +72,7 @@ function tryLoadPreviewStyles(
|
||||
for (const style of styles) {
|
||||
try {
|
||||
contentProvider.addStyle(resolveExtensionResources(extension, style));
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Logger } from './logger';
|
||||
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
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 function isMarkdownFile(document: vscode.TextDocument) {
|
||||
return document.languageId === 'markdown'
|
||||
&& document.uri.scheme !== 'markdown'; // prevent processing of own documents
|
||||
}
|
||||
|
||||
export function getMarkdownUri(uri: vscode.Uri) {
|
||||
if (uri.scheme === 'markdown') {
|
||||
return uri;
|
||||
}
|
||||
|
||||
return uri.with({
|
||||
scheme: 'markdown',
|
||||
path: uri.path + '.rendered',
|
||||
query: uri.toString()
|
||||
});
|
||||
}
|
||||
|
||||
class MarkdownPreviewConfig {
|
||||
public static getConfigForResource(resource: vscode.Uri) {
|
||||
return new MarkdownPreviewConfig(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 scrollPreviewWithEditorSelection: 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.scrollPreviewWithEditorSelection = !!markdownConfig.get<boolean>('preview.scrollPreviewWithEditorSelection', 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: MarkdownPreviewConfig) {
|
||||
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;
|
||||
}
|
||||
|
||||
class PreviewConfigManager {
|
||||
private previewConfigurationsForWorkspaces = new Map<string, MarkdownPreviewConfig>();
|
||||
|
||||
public loadAndCacheConfiguration(
|
||||
resource: vscode.Uri
|
||||
) {
|
||||
const config = MarkdownPreviewConfig.getConfigForResource(resource);
|
||||
this.previewConfigurationsForWorkspaces.set(this.getKey(resource), config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public shouldUpdateConfiguration(
|
||||
resource: vscode.Uri
|
||||
): boolean {
|
||||
const key = this.getKey(resource);
|
||||
const currentConfig = this.previewConfigurationsForWorkspaces.get(key);
|
||||
const newConfig = MarkdownPreviewConfig.getConfigForResource(resource);
|
||||
return (!currentConfig || !currentConfig.isEqualTo(newConfig));
|
||||
}
|
||||
|
||||
private getKey(
|
||||
resource: vscode.Uri
|
||||
): string {
|
||||
const folder = vscode.workspace.getWorkspaceFolder(resource);
|
||||
if (!folder) {
|
||||
return '';
|
||||
}
|
||||
return folder.uri.toString();
|
||||
}
|
||||
}
|
||||
|
||||
export class MDDocumentContentProvider implements vscode.TextDocumentContentProvider {
|
||||
private _onDidChange = new vscode.EventEmitter<vscode.Uri>();
|
||||
private _waiting: boolean = false;
|
||||
private previewConfigurations = new PreviewConfigManager();
|
||||
|
||||
private extraStyles: Array<vscode.Uri> = [];
|
||||
private extraScripts: Array<vscode.Uri> = [];
|
||||
|
||||
constructor(
|
||||
private engine: MarkdownEngine,
|
||||
private context: vscode.ExtensionContext,
|
||||
private cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private logger: Logger
|
||||
) { }
|
||||
|
||||
public addScript(resource: vscode.Uri): void {
|
||||
this.extraScripts.push(resource);
|
||||
}
|
||||
|
||||
public addStyle(resource: vscode.Uri): void {
|
||||
this.extraStyles.push(resource);
|
||||
}
|
||||
|
||||
private getMediaPath(mediaFile: string): string {
|
||||
return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))).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 (['file', 'http', 'https'].indexOf(hrefUri.scheme) >= 0) {
|
||||
return hrefUri.toString();
|
||||
}
|
||||
|
||||
// Use href as file URI if it is absolute
|
||||
if (path.isAbsolute(href)) {
|
||||
return vscode.Uri.file(href).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)).toString();
|
||||
}
|
||||
|
||||
// otherwise look relative to the markdown file
|
||||
return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)).toString();
|
||||
}
|
||||
|
||||
private computeCustomStyleSheetIncludes(resource: vscode.Uri, config: MarkdownPreviewConfig): string {
|
||||
if (config.styles && Array.isArray(config.styles)) {
|
||||
return config.styles.map(style => {
|
||||
return `<link rel="stylesheet" class="code-user-style" data-source="${style.replace(/"/g, '"')}" href="${this.fixHref(resource, style)}" type="text/css" media="screen">`;
|
||||
}).join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private getSettingsOverrideStyles(nonce: string, config: MarkdownPreviewConfig): 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: MarkdownPreviewConfig): string {
|
||||
const baseStyles = [
|
||||
this.getMediaPath('markdown.css'),
|
||||
this.getMediaPath('tomorrow.css')
|
||||
].concat(this.extraStyles.map(resource => resource.toString()));
|
||||
|
||||
return `${baseStyles.map(href => `<link rel="stylesheet" type="text/css" href="${href}">`).join('\n')}
|
||||
${this.getSettingsOverrideStyles(nonce, config)}
|
||||
${this.computeCustomStyleSheetIncludes(resource, config)}`;
|
||||
}
|
||||
|
||||
private getScripts(nonce: string): string {
|
||||
const scripts = [this.getMediaPath('main.js')].concat(this.extraScripts.map(resource => resource.toString()));
|
||||
return scripts
|
||||
.map(source => `<script async src="${source}" nonce="${nonce}" charset="UTF-8"></script>`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
public async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
|
||||
const sourceUri = vscode.Uri.parse(uri.query);
|
||||
|
||||
let initialLine: number | undefined = undefined;
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && editor.document.uri.toString() === sourceUri.toString()) {
|
||||
initialLine = editor.selection.active.line;
|
||||
}
|
||||
|
||||
const document = await vscode.workspace.openTextDocument(sourceUri);
|
||||
const config = this.previewConfigurations.loadAndCacheConfiguration(sourceUri);
|
||||
|
||||
const initialData = {
|
||||
previewUri: uri.toString(),
|
||||
source: sourceUri.toString(),
|
||||
line: initialLine,
|
||||
scrollPreviewWithEditorSelection: config.scrollPreviewWithEditorSelection,
|
||||
scrollEditorWithPreview: config.scrollEditorWithPreview,
|
||||
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor
|
||||
};
|
||||
|
||||
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', document.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, '"')}" data-strings="${JSON.stringify(previewStrings).replace(/"/g, '"')}">
|
||||
<script src="${this.getMediaPath('csp.js')}" nonce="${nonce}"></script>
|
||||
<script src="${this.getMediaPath('loading.js')}" nonce="${nonce}"></script>
|
||||
${this.getStyles(sourceUri, nonce, config)}
|
||||
<base href="${document.uri.toString(true)}">
|
||||
</head>
|
||||
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
|
||||
${body}
|
||||
<div class="code-line" data-line="${document.lineCount}"></div>
|
||||
${this.getScripts(nonce)}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
// update all generated md documents
|
||||
for (const document of vscode.workspace.textDocuments) {
|
||||
if (document.uri.scheme === 'markdown') {
|
||||
const sourceUri = vscode.Uri.parse(document.uri.query);
|
||||
if (this.previewConfigurations.shouldUpdateConfiguration(sourceUri)) {
|
||||
this.update(document.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get onDidChange(): vscode.Event<vscode.Uri> {
|
||||
return this._onDidChange.event;
|
||||
}
|
||||
|
||||
public update(uri: vscode.Uri) {
|
||||
if (!this._waiting) {
|
||||
this._waiting = true;
|
||||
setTimeout(() => {
|
||||
this._waiting = false;
|
||||
this._onDidChange.fire(uri);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
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 'self' http: https: data:; media-src 'self' http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' 'unsafe-inline' http: https: data:; font-src 'self' http: https: data:;">`;
|
||||
|
||||
case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:
|
||||
return '';
|
||||
|
||||
case MarkdownPreviewSecurityLevel.Strict:
|
||||
default:
|
||||
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data:; media-src 'self' https: data:; script-src 'nonce-${nonce}'; style-src 'self' 'unsafe-inline' https: data:; font-src 'self' https: data:;">`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,10 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { getMarkdownUri, MDDocumentContentProvider } from './features/previewContentProvider';
|
||||
import { MarkdownPreviewManager } from './features/previewManager';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
@@ -36,8 +35,8 @@ export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPol
|
||||
private readonly should_disable_security_warning_key = 'preview_should_show_security_warning:';
|
||||
|
||||
constructor(
|
||||
private globalState: vscode.Memento,
|
||||
private workspaceState: vscode.Memento
|
||||
private readonly globalState: vscode.Memento,
|
||||
private readonly workspaceState: vscode.Memento
|
||||
) { }
|
||||
|
||||
public getSecurityLevelForResource(resource: vscode.Uri): MarkdownPreviewSecurityLevel {
|
||||
@@ -90,13 +89,13 @@ export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPol
|
||||
export class PreviewSecuritySelector {
|
||||
|
||||
public constructor(
|
||||
private cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private contentProvider: MDDocumentContentProvider
|
||||
private readonly cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private readonly webviewManager: MarkdownPreviewManager
|
||||
) { }
|
||||
|
||||
public async showSecutitySelectorForResource(resource: vscode.Uri): Promise<void> {
|
||||
interface PreviewSecurityPickItem extends vscode.QuickPickItem {
|
||||
type: 'moreinfo' | 'toggle' | MarkdownPreviewSecurityLevel;
|
||||
readonly type: 'moreinfo' | 'toggle' | MarkdownPreviewSecurityLevel;
|
||||
}
|
||||
|
||||
function markActiveWhen(when: boolean): string {
|
||||
@@ -127,7 +126,7 @@ export class PreviewSecuritySelector {
|
||||
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 effect the content security level')
|
||||
description: localize('toggleSecurityWarning.description', 'Does not affect the content security level')
|
||||
},
|
||||
], {
|
||||
placeHolder: localize(
|
||||
@@ -144,22 +143,12 @@ export class PreviewSecuritySelector {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceUri = getMarkdownUri(resource);
|
||||
if (selection.type === 'toggle') {
|
||||
this.cspArbiter.setShouldDisableSecurityWarning(!this.cspArbiter.shouldDisableSecurityWarnings());
|
||||
this.contentProvider.update(sourceUri);
|
||||
return;
|
||||
} else {
|
||||
await this.cspArbiter.setSecurityLevelForResource(resource, selection.type);
|
||||
}
|
||||
|
||||
await this.cspArbiter.setSecurityLevelForResource(resource, selection.type);
|
||||
|
||||
await vscode.commands.executeCommand('_workbench.htmlPreview.updateOptions',
|
||||
sourceUri,
|
||||
{
|
||||
allowScripts: true,
|
||||
allowSvgs: this.cspArbiter.shouldAllowSvgsForResource(resource)
|
||||
});
|
||||
|
||||
this.contentProvider.update(sourceUri);
|
||||
this.webviewManager.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,41 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
|
||||
export class Slug {
|
||||
public static fromHeading(heading: string): Slug {
|
||||
const slugifiedHeading = encodeURI(heading.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/^\-+/, '')
|
||||
.replace(/\-+$/, ''));
|
||||
|
||||
return new Slug(slugifiedHeading);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public readonly value: string
|
||||
) { }
|
||||
|
||||
public equals(other: Slug): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TocEntry {
|
||||
slug: string;
|
||||
text: string;
|
||||
level: number;
|
||||
line: number;
|
||||
location: vscode.Location;
|
||||
readonly slug: Slug;
|
||||
readonly text: string;
|
||||
readonly level: number;
|
||||
readonly line: number;
|
||||
readonly location: vscode.Location;
|
||||
}
|
||||
|
||||
export class TableOfContentsProvider {
|
||||
private toc: TocEntry[];
|
||||
private toc?: TocEntry[];
|
||||
|
||||
public constructor(
|
||||
private engine: MarkdownEngine,
|
||||
@@ -36,14 +55,10 @@ export class TableOfContentsProvider {
|
||||
return this.toc;
|
||||
}
|
||||
|
||||
public async lookup(fragment: string): Promise<number> {
|
||||
const slug = TableOfContentsProvider.slugify(fragment);
|
||||
for (const entry of await this.getToc()) {
|
||||
if (entry.slug === slug) {
|
||||
return entry.line;
|
||||
}
|
||||
}
|
||||
return NaN;
|
||||
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[]> {
|
||||
@@ -53,17 +68,13 @@ export class TableOfContentsProvider {
|
||||
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
|
||||
const lineNumber = heading.map[0];
|
||||
const line = document.lineAt(lineNumber);
|
||||
const href = TableOfContentsProvider.slugify(line.text);
|
||||
const level = TableOfContentsProvider.getHeaderLevel(heading.markup);
|
||||
if (href) {
|
||||
toc.push({
|
||||
slug: href,
|
||||
text: TableOfContentsProvider.getHeaderText(line.text),
|
||||
level: level,
|
||||
line: lineNumber,
|
||||
location: new vscode.Location(document.uri, line.range)
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -81,14 +92,4 @@ export class TableOfContentsProvider {
|
||||
private static getHeaderText(header: string): string {
|
||||
return header.replace(/^\s*#+\s*(.*?)\s*#*$/, (_, word) => word.trim());
|
||||
}
|
||||
|
||||
public static slugify(header: string): string {
|
||||
return encodeURI(header.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/^\-+/, '')
|
||||
.replace(/\-+$/, ''));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
extensions/markdown/src/test/index.ts
Normal file
28
extensions/markdown/src/test/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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;
|
||||
131
extensions/markdown/src/test/tableOfContentsProvider.test.ts
Normal file
131
extensions/markdown/src/test/tableOfContentsProvider.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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';
|
||||
|
||||
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(new MarkdownEngine(), 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(new MarkdownEngine(), 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(new MarkdownEngine(), 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(new MarkdownEngine(), 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(new MarkdownEngine(), 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);
|
||||
});
|
||||
});
|
||||
|
||||
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.');
|
||||
}
|
||||
}
|
||||
1
extensions/markdown/src/typings/ref.d.ts
vendored
1
extensions/markdown/src/typings/ref.d.ts
vendored
@@ -4,4 +4,5 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
|
||||
16
extensions/markdown/src/util/dispose.ts
Normal file
16
extensions/markdown/src/util/dispose.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
extensions/markdown/src/util/file.ts
Normal file
10
extensions/markdown/src/util/file.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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';
|
||||
}
|
||||
75
extensions/markdown/src/util/topmostLineMonitor.ts
Normal file
75
extensions/markdown/src/util/topmostLineMonitor.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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;
|
||||
}
|
||||
Reference in New Issue
Block a user