Merge vscode 1.67 (#20883)

* Fix initial build breaks from 1.67 merge (#2514)

* Update yarn lock files

* Update build scripts

* Fix tsconfig

* Build breaks

* WIP

* Update yarn lock files

* Misc breaks

* Updates to package.json

* Breaks

* Update yarn

* Fix breaks

* Breaks

* Build breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Missing file

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Fix several runtime breaks (#2515)

* Missing files

* Runtime breaks

* Fix proxy ordering issue

* Remove commented code

* Fix breaks with opening query editor

* Fix post merge break

* Updates related to setup build and other breaks (#2516)

* Fix bundle build issues

* Update distro

* Fix distro merge and update build JS files

* Disable pipeline steps

* Remove stats call

* Update license name

* Make new RPM dependencies a warning

* Fix extension manager version checks

* Update JS file

* Fix a few runtime breaks

* Fixes

* Fix runtime issues

* Fix build breaks

* Update notebook tests (part 1)

* Fix broken tests

* Linting errors

* Fix hygiene

* Disable lint rules

* Bump distro

* Turn off smoke tests

* Disable integration tests

* Remove failing "activate" test

* Remove failed test assertion

* Disable other broken test

* Disable query history tests

* Disable extension unit tests

* Disable failing tasks
This commit is contained in:
Karl Burtram
2022-10-19 19:13:18 -07:00
committed by GitHub
parent 33c6daaea1
commit 8a3d08f0de
3738 changed files with 192313 additions and 107208 deletions

View File

@@ -0,0 +1,808 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import * as uri from 'vscode-uri';
import { Logger } from '../logger';
import { MarkdownEngine } from '../markdownEngine';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { openDocumentLink, resolveDocumentLink, resolveUriToMarkdownFile } from '../util/openDocumentLink';
import { WebviewResourceProvider } from '../util/resources';
import { urlToUri } from '../util/url';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContentProvider } from './previewContentProvider';
import { scrollEditorToLine, StartingScrollFragment, StartingScrollLine, StartingScrollLocation } from './scrolling';
import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from './topmostLineMonitor';
const localize = nls.loadMessageBundle();
interface WebviewMessage {
readonly source: string;
}
interface CacheImageSizesMessage extends WebviewMessage {
readonly type: 'cacheImageSizes';
readonly body: { id: string; width: number; height: number }[];
}
interface RevealLineMessage extends WebviewMessage {
readonly type: 'revealLine';
readonly body: {
readonly line: number;
};
}
interface DidClickMessage extends WebviewMessage {
readonly type: 'didClick';
readonly body: {
readonly line: number;
};
}
interface ClickLinkMessage extends WebviewMessage {
readonly type: 'openLink';
readonly body: {
readonly href: string;
};
}
interface ShowPreviewSecuritySelectorMessage extends WebviewMessage {
readonly type: 'showPreviewSecuritySelector';
}
interface PreviewStyleLoadErrorMessage extends WebviewMessage {
readonly type: 'previewStyleLoadError';
readonly body: {
readonly unloadedStyles: string[];
};
}
export class PreviewDocumentVersion {
public readonly resource: vscode.Uri;
private readonly version: number;
public constructor(document: vscode.TextDocument) {
this.resource = document.uri;
this.version = document.version;
}
public equals(other: PreviewDocumentVersion): boolean {
return this.resource.fsPath === other.resource.fsPath
&& this.version === other.version;
}
}
interface MarkdownPreviewDelegate {
getTitle?(resource: vscode.Uri): string;
getAdditionalState(): {};
openPreviewLinkToMarkdownFile(markdownLink: vscode.Uri, fragment: string): void;
}
class MarkdownPreview extends Disposable implements WebviewResourceProvider {
private static readonly unwatchedImageSchemes = new Set(['https', 'http', 'data']);
private _disposed: boolean = false;
private readonly delay = 300;
private throttleTimer: any;
private readonly _resource: vscode.Uri;
private readonly _webviewPanel: vscode.WebviewPanel;
private line: number | undefined;
private scrollToFragment: string | undefined;
private firstUpdate = true;
private currentVersion?: PreviewDocumentVersion;
private isScrolling = false;
private imageInfo: { readonly id: string; readonly width: number; readonly height: number }[] = [];
private readonly _fileWatchersBySrc = new Map</* src: */ string, vscode.FileSystemWatcher>();
private readonly _onScrollEmitter = this._register(new vscode.EventEmitter<LastScrollLocation>());
public readonly onScroll = this._onScrollEmitter.event;
constructor(
webview: vscode.WebviewPanel,
resource: vscode.Uri,
startingScroll: StartingScrollLocation | undefined,
private readonly delegate: MarkdownPreviewDelegate,
private readonly engine: MarkdownEngine,
private readonly _contentProvider: MarkdownContentProvider,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _logger: Logger,
private readonly _contributionProvider: MarkdownContributionProvider,
) {
super();
this._webviewPanel = webview;
this._resource = resource;
switch (startingScroll?.type) {
case 'line':
if (!isNaN(startingScroll.line!)) {
this.line = startingScroll.line;
}
break;
case 'fragment':
this.scrollToFragment = startingScroll.fragment;
break;
}
this._register(_contributionProvider.onContributionsChanged(() => {
setTimeout(() => this.refresh(), 0);
}));
this._register(vscode.workspace.onDidChangeTextDocument(event => {
if (this.isPreviewOf(event.document.uri)) {
this.refresh();
}
}));
this._register(vscode.workspace.onDidOpenTextDocument(document => {
if (this.isPreviewOf(document.uri)) {
this.refresh();
}
}));
const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(resource, '*')));
this._register(watcher.onDidChange(uri => {
if (this.isPreviewOf(uri)) {
// Only use the file system event when VS Code does not already know about the file
if (!vscode.workspace.textDocuments.some(doc => doc.uri.toString() !== uri.toString())) {
this.refresh();
}
}
}));
this._register(this._webviewPanel.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => {
if (e.source !== this._resource.toString()) {
return;
}
switch (e.type) {
case 'cacheImageSizes':
this.imageInfo = e.body;
break;
case 'revealLine':
this.onDidScrollPreview(e.body.line);
break;
case 'didClick':
this.onDidClickPreview(e.body.line);
break;
case 'openLink':
this.onDidClickPreviewLink(e.body.href);
break;
case 'showPreviewSecuritySelector':
vscode.commands.executeCommand('markdown.showPreviewSecuritySelector', e.source);
break;
case 'previewStyleLoadError':
vscode.window.showWarningMessage(
localize('onPreviewStyleLoadError',
"Could not load 'markdown.styles': {0}",
e.body.unloadedStyles.join(', ')));
break;
}
}));
this.updatePreview();
}
override dispose() {
super.dispose();
this._disposed = true;
clearTimeout(this.throttleTimer);
for (const entry of this._fileWatchersBySrc.values()) {
entry.dispose();
}
this._fileWatchersBySrc.clear();
}
public get resource(): vscode.Uri {
return this._resource;
}
public get state() {
return {
resource: this._resource.toString(),
line: this.line,
imageInfo: this.imageInfo,
fragment: this.scrollToFragment,
...this.delegate.getAdditionalState(),
};
}
/**
* The first call immediately refreshes the preview,
* calls happening shortly thereafter are debounced.
*/
public refresh(forceUpdate: boolean = false) {
// Schedule update if none is pending
if (!this.throttleTimer) {
if (this.firstUpdate) {
this.updatePreview(true);
} else {
this.throttleTimer = setTimeout(() => this.updatePreview(forceUpdate), this.delay);
}
}
this.firstUpdate = false;
}
public isPreviewOf(resource: vscode.Uri): boolean {
return this._resource.fsPath === resource.fsPath;
}
public postMessage(msg: any) {
if (!this._disposed) {
this._webviewPanel.webview.postMessage(msg);
}
}
public scrollTo(topLine: number) {
if (this._disposed) {
return;
}
if (this.isScrolling) {
this.isScrolling = false;
return;
}
this._logger.log('updateForView', { markdownFile: this._resource });
this.line = topLine;
this.postMessage({
type: 'updateView',
line: topLine,
source: this._resource.toString()
});
}
private async updatePreview(forceUpdate?: boolean): Promise<void> {
clearTimeout(this.throttleTimer);
this.throttleTimer = undefined;
if (this._disposed) {
return;
}
let document: vscode.TextDocument;
try {
document = await vscode.workspace.openTextDocument(this._resource);
} catch {
await this.showFileNotFoundError();
return;
}
if (this._disposed) {
return;
}
const pendingVersion = new PreviewDocumentVersion(document);
if (!forceUpdate && this.currentVersion?.equals(pendingVersion)) {
if (this.line) {
this.scrollTo(this.line);
}
return;
}
const shouldReloadPage = forceUpdate || !this.currentVersion || this.currentVersion.resource.toString() !== pendingVersion.resource.toString() || !this._webviewPanel.visible;
this.currentVersion = pendingVersion;
const content = await (shouldReloadPage
? this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state)
: this._contentProvider.markdownBody(document, this));
// Another call to `doUpdate` may have happened.
// Make sure we are still updating for the correct document
if (this.currentVersion?.equals(pendingVersion)) {
this.updateWebviewContent(content.html, shouldReloadPage);
this.updateImageWatchers(content.containingImages);
}
}
private onDidScrollPreview(line: number) {
this.line = line;
this._onScrollEmitter.fire({ line: this.line, uri: this._resource });
const config = this._previewConfigurations.loadAndCacheConfiguration(this._resource);
if (!config.scrollEditorWithPreview) {
return;
}
for (const editor of vscode.window.visibleTextEditors) {
if (!this.isPreviewOf(editor.document.uri)) {
continue;
}
this.isScrolling = true;
scrollEditorToLine(line, editor);
}
}
private async onDidClickPreview(line: number): Promise<void> {
// fix #82457, find currently opened but unfocused source tab
await vscode.commands.executeCommand('markdown.showSource');
const revealLineInEditor = (editor: vscode.TextEditor) => {
const position = new vscode.Position(line, 0);
const newSelection = new vscode.Selection(position, position);
editor.selection = newSelection;
editor.revealRange(newSelection, vscode.TextEditorRevealType.InCenterIfOutsideViewport);
};
for (const visibleEditor of vscode.window.visibleTextEditors) {
if (this.isPreviewOf(visibleEditor.document.uri)) {
const editor = await vscode.window.showTextDocument(visibleEditor.document, visibleEditor.viewColumn);
revealLineInEditor(editor);
return;
}
}
await vscode.workspace.openTextDocument(this._resource)
.then(vscode.window.showTextDocument)
.then((editor) => {
revealLineInEditor(editor);
}, () => {
vscode.window.showErrorMessage(localize('preview.clickOpenFailed', 'Could not open {0}', this._resource.toString()));
});
}
private async showFileNotFoundError() {
this._webviewPanel.webview.html = this._contentProvider.provideFileNotFoundContent(this._resource);
}
private updateWebviewContent(html: string, reloadPage: boolean): void {
if (this._disposed) {
return;
}
if (this.delegate.getTitle) {
this._webviewPanel.title = this.delegate.getTitle(this._resource);
}
this._webviewPanel.webview.options = this.getWebviewOptions();
if (reloadPage) {
this._webviewPanel.webview.html = html;
} else {
this._webviewPanel.webview.postMessage({
type: 'updateContent',
content: html,
source: this._resource.toString(),
});
}
}
private updateImageWatchers(containingImages: { src: string }[]) {
const srcs = new Set(containingImages.map(img => img.src));
// Delete stale file watchers.
for (const [src, watcher] of this._fileWatchersBySrc) {
if (!srcs.has(src)) {
watcher.dispose();
this._fileWatchersBySrc.delete(src);
}
}
// Create new file watchers.
const root = vscode.Uri.joinPath(this._resource, '../');
for (const src of srcs) {
const uri = urlToUri(src, root);
if (uri && !MarkdownPreview.unwatchedImageSchemes.has(uri.scheme) && !this._fileWatchersBySrc.has(src)) {
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, '*'));
watcher.onDidChange(() => {
this.refresh(true);
});
this._fileWatchersBySrc.set(src, watcher);
}
}
}
private getWebviewOptions(): vscode.WebviewOptions {
return {
enableScripts: true,
enableForms: false,
localResourceRoots: this.getLocalResourceRoots()
};
}
private getLocalResourceRoots(): ReadonlyArray<vscode.Uri> {
const baseRoots = Array.from(this._contributionProvider.contributions.previewResourceRoots);
const folder = vscode.workspace.getWorkspaceFolder(this._resource);
if (folder) {
const workspaceRoots = vscode.workspace.workspaceFolders?.map(folder => folder.uri);
if (workspaceRoots) {
baseRoots.push(...workspaceRoots);
}
} else {
baseRoots.push(uri.Utils.dirname(this._resource));
}
return baseRoots;
}
private async onDidClickPreviewLink(href: string) {
const targetResource = resolveDocumentLink(href, this.resource);
const config = vscode.workspace.getConfiguration('markdown', this.resource);
const openLinks = config.get<string>('preview.openMarkdownLinks', 'inPreview');
if (openLinks === 'inPreview') {
const linkedDoc = await resolveUriToMarkdownFile(targetResource);
if (linkedDoc) {
this.delegate.openPreviewLinkToMarkdownFile(linkedDoc.uri, targetResource.fragment);
return;
}
}
return openDocumentLink(this.engine, targetResource, this.resource);
}
//#region WebviewResourceProvider
asWebviewUri(resource: vscode.Uri) {
return this._webviewPanel.webview.asWebviewUri(resource);
}
get cspSource() {
return this._webviewPanel.webview.cspSource;
}
//#endregion
}
export interface ManagedMarkdownPreview {
readonly resource: vscode.Uri;
readonly resourceColumn: vscode.ViewColumn;
readonly onDispose: vscode.Event<void>;
readonly onDidChangeViewState: vscode.Event<vscode.WebviewPanelOnDidChangeViewStateEvent>;
dispose(): void;
refresh(): void;
updateConfiguration(): void;
matchesResource(
otherResource: vscode.Uri,
otherPosition: vscode.ViewColumn | undefined,
otherLocked: boolean
): boolean;
}
export class StaticMarkdownPreview extends Disposable implements ManagedMarkdownPreview {
public static readonly customEditorViewType = 'vscode.markdown.preview.editor';
public static revive(
resource: vscode.Uri,
webview: vscode.WebviewPanel,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
topmostLineMonitor: TopmostLineMonitor,
logger: Logger,
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
scrollLine?: number,
): StaticMarkdownPreview {
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, engine, scrollLine);
}
private readonly preview: MarkdownPreview;
private constructor(
private readonly _webviewPanel: vscode.WebviewPanel,
resource: vscode.Uri,
contentProvider: MarkdownContentProvider,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
topmostLineMonitor: TopmostLineMonitor,
logger: Logger,
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
scrollLine?: number,
) {
super();
const topScrollLocation = scrollLine ? new StartingScrollLine(scrollLine) : undefined;
this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, topScrollLocation, {
getAdditionalState: () => { return {}; },
openPreviewLinkToMarkdownFile: (markdownLink, fragment) => {
return vscode.commands.executeCommand('vscode.openWith', markdownLink.with({
fragment
}), StaticMarkdownPreview.customEditorViewType, this._webviewPanel.viewColumn);
}
}, engine, contentProvider, _previewConfigurations, logger, contributionProvider));
this._register(this._webviewPanel.onDidDispose(() => {
this.dispose();
}));
this._register(this._webviewPanel.onDidChangeViewState(e => {
this._onDidChangeViewState.fire(e);
}));
this._register(this.preview.onScroll((scrollInfo) => {
topmostLineMonitor.setPreviousStaticEditorLine(scrollInfo);
}));
this._register(topmostLineMonitor.onDidChanged(event => {
if (this.preview.isPreviewOf(event.resource)) {
this.preview.scrollTo(event.line);
}
}));
}
private readonly _onDispose = this._register(new vscode.EventEmitter<void>());
public readonly onDispose = this._onDispose.event;
private readonly _onDidChangeViewState = this._register(new vscode.EventEmitter<vscode.WebviewPanelOnDidChangeViewStateEvent>());
public readonly onDidChangeViewState = this._onDidChangeViewState.event;
override dispose() {
this._onDispose.fire();
super.dispose();
}
public matchesResource(
_otherResource: vscode.Uri,
_otherPosition: vscode.ViewColumn | undefined,
_otherLocked: boolean
): boolean {
return false;
}
public refresh() {
this.preview.refresh(true);
}
public updateConfiguration() {
if (this._previewConfigurations.hasConfigurationChanged(this.preview.resource)) {
this.refresh();
}
}
public get resource() {
return this.preview.resource;
}
public get resourceColumn() {
return this._webviewPanel.viewColumn || vscode.ViewColumn.One;
}
}
interface DynamicPreviewInput {
readonly resource: vscode.Uri;
readonly resourceColumn: vscode.ViewColumn;
readonly locked: boolean;
readonly line?: number;
}
export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdownPreview {
public static readonly viewType = 'markdown.preview';
private readonly _resourceColumn: vscode.ViewColumn;
private _locked: boolean;
private readonly _webviewPanel: vscode.WebviewPanel;
private _preview: MarkdownPreview;
public static revive(
input: DynamicPreviewInput,
webview: vscode.WebviewPanel,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
): DynamicMarkdownPreview {
webview.iconPath = contentProvider.iconPath;
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
}
public static create(
input: DynamicPreviewInput,
previewColumn: vscode.ViewColumn,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
): DynamicMarkdownPreview {
const webview = vscode.window.createWebviewPanel(
DynamicMarkdownPreview.viewType,
DynamicMarkdownPreview.getPreviewTitle(input.resource, input.locked),
previewColumn, { enableFindWidget: true, });
webview.iconPath = contentProvider.iconPath;
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
}
private constructor(
webview: vscode.WebviewPanel,
input: DynamicPreviewInput,
private readonly _contentProvider: MarkdownContentProvider,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _logger: Logger,
private readonly _topmostLineMonitor: TopmostLineMonitor,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _engine: MarkdownEngine,
) {
super();
this._webviewPanel = webview;
this._resourceColumn = input.resourceColumn;
this._locked = input.locked;
this._preview = this.createPreview(input.resource, typeof input.line === 'number' ? new StartingScrollLine(input.line) : undefined);
this._register(webview.onDidDispose(() => { this.dispose(); }));
this._register(this._webviewPanel.onDidChangeViewState(e => {
this._onDidChangeViewStateEmitter.fire(e);
}));
this._register(this._topmostLineMonitor.onDidChanged(event => {
if (this._preview.isPreviewOf(event.resource)) {
this._preview.scrollTo(event.line);
}
}));
this._register(vscode.window.onDidChangeTextEditorSelection(event => {
if (this._preview.isPreviewOf(event.textEditor.document.uri)) {
this._preview.postMessage({
type: 'onDidChangeTextEditorSelection',
line: event.selections[0].active.line,
source: this._preview.resource.toString()
});
}
}));
this._register(vscode.window.onDidChangeActiveTextEditor(editor => {
// Only allow previewing normal text editors which have a viewColumn: See #101514
if (typeof editor?.viewColumn === 'undefined') {
return;
}
if (isMarkdownFile(editor.document) && !this._locked && !this._preview.isPreviewOf(editor.document.uri)) {
const line = getVisibleLine(editor);
this.update(editor.document.uri, line ? new StartingScrollLine(line) : undefined);
}
}));
}
private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter<void>());
public readonly onDispose = this._onDisposeEmitter.event;
private readonly _onDidChangeViewStateEmitter = this._register(new vscode.EventEmitter<vscode.WebviewPanelOnDidChangeViewStateEvent>());
public readonly onDidChangeViewState = this._onDidChangeViewStateEmitter.event;
override dispose() {
this._preview.dispose();
this._webviewPanel.dispose();
this._onDisposeEmitter.fire();
this._onDisposeEmitter.dispose();
super.dispose();
}
public get resource() {
return this._preview.resource;
}
public get resourceColumn() {
return this._resourceColumn;
}
public reveal(viewColumn: vscode.ViewColumn) {
this._webviewPanel.reveal(viewColumn);
}
public refresh() {
this._preview.refresh(true);
}
public updateConfiguration() {
if (this._previewConfigurations.hasConfigurationChanged(this._preview.resource)) {
this.refresh();
}
}
public update(newResource: vscode.Uri, scrollLocation?: StartingScrollLocation) {
if (this._preview.isPreviewOf(newResource)) {
switch (scrollLocation?.type) {
case 'line':
this._preview.scrollTo(scrollLocation.line);
return;
case 'fragment':
// Workaround. For fragments, just reload the entire preview
break;
default:
return;
}
}
this._preview.dispose();
this._preview = this.createPreview(newResource, scrollLocation);
}
public toggleLock() {
this._locked = !this._locked;
this._webviewPanel.title = DynamicMarkdownPreview.getPreviewTitle(this._preview.resource, this._locked);
}
private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string {
const resourceLabel = uri.Utils.basename(resource);
return locked
? localize('lockedPreviewTitle', '[Preview] {0}', resourceLabel)
: localize('previewTitle', 'Preview {0}', resourceLabel);
}
public get position(): vscode.ViewColumn | undefined {
return this._webviewPanel.viewColumn;
}
public matchesResource(
otherResource: vscode.Uri,
otherPosition: vscode.ViewColumn | undefined,
otherLocked: boolean
): boolean {
if (this.position !== otherPosition) {
return false;
}
if (this._locked) {
return otherLocked && this._preview.isPreviewOf(otherResource);
} else {
return !otherLocked;
}
}
public matches(otherPreview: DynamicMarkdownPreview): boolean {
return this.matchesResource(otherPreview._preview.resource, otherPreview.position, otherPreview._locked);
}
private createPreview(resource: vscode.Uri, startingScroll?: StartingScrollLocation): MarkdownPreview {
return new MarkdownPreview(this._webviewPanel, resource, startingScroll, {
getTitle: (resource) => DynamicMarkdownPreview.getPreviewTitle(resource, this._locked),
getAdditionalState: () => {
return {
resourceColumn: this.resourceColumn,
locked: this._locked,
};
},
openPreviewLinkToMarkdownFile: (link: vscode.Uri, fragment?: string) => {
this.update(link, fragment ? new StartingScrollFragment(fragment) : undefined);
}
},
this._engine,
this._contentProvider,
this._previewConfigurations,
this._logger,
this._contributionProvider);
}
}

View File

@@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* 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 { equals } from '../util/arrays';
export class MarkdownPreviewConfiguration {
public static getForResource(resource: vscode.Uri) {
return new MarkdownPreviewConfiguration(resource);
}
public readonly scrollBeyondLastLine: boolean;
public readonly wordWrap: boolean;
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: readonly 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]', resource);
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.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 (const key in this) {
if (this.hasOwnProperty(key) && key !== 'styles') {
if (this[key] !== otherConfig[key]) {
return false;
}
}
}
return equals(this.styles, otherConfig.styles);
}
[key: string]: any;
}
export class MarkdownPreviewConfigurationManager {
private readonly previewConfigurationsForWorkspaces = new Map<string, MarkdownPreviewConfiguration>();
public loadAndCacheConfiguration(
resource: vscode.Uri
): MarkdownPreviewConfiguration {
const config = MarkdownPreviewConfiguration.getForResource(resource);
this.previewConfigurationsForWorkspaces.set(this.getKey(resource), config);
return config;
}
public hasConfigurationChanged(
resource: vscode.Uri
): boolean {
const key = this.getKey(resource);
const currentConfig = this.previewConfigurationsForWorkspaces.get(key);
const newConfig = MarkdownPreviewConfiguration.getForResource(resource);
return (!currentConfig || !currentConfig.isEqualTo(newConfig));
}
private getKey(
resource: vscode.Uri
): string {
const folder = vscode.workspace.getWorkspaceFolder(resource);
return folder ? folder.uri.toString() : '';
}
}

View File

@@ -0,0 +1,257 @@
/*---------------------------------------------------------------------------------------------
* 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 nls from 'vscode-nls';
import * as uri from 'vscode-uri';
import { Logger } from '../logger';
import { MarkdownEngine } from '../markdownEngine';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { WebviewResourceProvider } from '../util/resources';
import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig';
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security';
const localize = nls.loadMessageBundle();
/**
* 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')
};
function escapeAttribute(value: string | vscode.Uri): string {
return value.toString().replace(/"/g, '&quot;');
}
export interface MarkdownContentProviderOutput {
html: string;
containingImages: { src: string }[];
}
export class MarkdownContentProvider {
constructor(
private readonly engine: MarkdownEngine,
private readonly context: vscode.ExtensionContext,
private readonly cspArbiter: ContentSecurityPolicyArbiter,
private readonly contributionProvider: MarkdownContributionProvider,
private readonly logger: Logger
) {
this.iconPath = {
dark: vscode.Uri.joinPath(this.context.extensionUri, 'media', 'preview-dark.svg'),
light: vscode.Uri.joinPath(this.context.extensionUri, 'media', 'preview-light.svg'),
};
}
public readonly iconPath: { light: vscode.Uri; dark: vscode.Uri };
public async provideTextDocumentContent(
markdownDocument: vscode.TextDocument,
resourceProvider: WebviewResourceProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
initialLine: number | undefined = undefined,
state?: any
): Promise<MarkdownContentProviderOutput> {
const sourceUri = markdownDocument.uri;
const config = previewConfigurations.loadAndCacheConfiguration(sourceUri);
const initialData = {
source: sourceUri.toString(),
fragment: state?.fragment || markdownDocument.uri.fragment || undefined,
line: initialLine,
lineCount: markdownDocument.lineCount,
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
scrollEditorWithPreview: config.scrollEditorWithPreview,
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,
disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings(),
webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(),
};
this.logger.log('provideTextDocumentContent', initialData);
// Content Security Policy
const nonce = getNonce();
const csp = this.getCsp(resourceProvider, sourceUri, nonce);
const body = await this.markdownBody(markdownDocument, resourceProvider);
const html = `<!DOCTYPE html>
<html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}">
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
${csp}
<meta id="vscode-markdown-preview-data"
data-settings="${escapeAttribute(JSON.stringify(initialData))}"
data-strings="${escapeAttribute(JSON.stringify(previewStrings))}"
data-state="${escapeAttribute(JSON.stringify(state || {}))}">
<script src="${this.extensionResourcePath(resourceProvider, 'pre.js')}" nonce="${nonce}"></script>
${this.getStyles(resourceProvider, sourceUri, config, state)}
<base href="${resourceProvider.asWebviewUri(markdownDocument.uri)}">
</head>
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
${body.html}
${this.getScripts(resourceProvider, nonce)}
</body>
</html>`;
return {
html,
containingImages: body.containingImages,
};
}
public async markdownBody(
markdownDocument: vscode.TextDocument,
resourceProvider: WebviewResourceProvider,
): Promise<MarkdownContentProviderOutput> {
const rendered = await this.engine.render(markdownDocument, resourceProvider);
const html = `<div class="markdown-body" dir="auto">${rendered.html}<div class="code-line" data-line="${markdownDocument.lineCount}"></div></div>`;
return {
html,
containingImages: rendered.containingImages
};
}
public provideFileNotFoundContent(
resource: vscode.Uri,
): string {
const resourcePath = uri.Utils.basename(resource);
const body = localize('preview.notFound', '{0} cannot be found', resourcePath);
return `<!DOCTYPE html>
<html>
<body class="vscode-body">
${body}
</body>
</html>`;
}
private extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string {
const webviewResource = resourceProvider.asWebviewUri(
vscode.Uri.joinPath(this.context.extensionUri, 'media', mediaFile));
return webviewResource.toString();
}
private fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string {
if (!href) {
return href;
}
if (href.startsWith('http:') || href.startsWith('https:') || href.startsWith('file:')) {
return href;
}
// Assume it must be a local file
if (href.startsWith('/') || /^[a-z]:\\/i.test(href)) {
return resourceProvider.asWebviewUri(vscode.Uri.file(href)).toString();
}
// Use a workspace relative path if there is a workspace
const root = vscode.workspace.getWorkspaceFolder(resource);
if (root) {
return resourceProvider.asWebviewUri(vscode.Uri.joinPath(root.uri, href)).toString();
}
// Otherwise look relative to the markdown file
return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString();
}
private computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
if (!Array.isArray(config.styles)) {
return '';
}
const out: string[] = [];
for (const style of config.styles) {
out.push(`<link rel="stylesheet" class="code-user-style" data-source="${escapeAttribute(style)}" href="${escapeAttribute(this.fixHref(resourceProvider, resource, style))}" type="text/css" media="screen">`);
}
return out.join('\n');
}
private getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string {
return [
config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '',
isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`,
isNaN(config.lineHeight) ? '' : `--markdown-line-height: ${config.lineHeight};`,
].join(' ');
}
private getImageStabilizerStyles(state?: any) {
let ret = '<style>\n';
if (state && state.imageInfo) {
state.imageInfo.forEach((imgInfo: any) => {
ret += `#${imgInfo.id}.loading {
height: ${imgInfo.height}px;
width: ${imgInfo.width}px;
}\n`;
});
}
ret += '</style>\n';
return ret;
}
private getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, state?: any): string {
const baseStyles: string[] = [];
for (const resource of this.contributionProvider.contributions.previewStyles) {
baseStyles.push(`<link rel="stylesheet" type="text/css" href="${escapeAttribute(resourceProvider.asWebviewUri(resource))}">`);
}
return `${baseStyles.join('\n')}
${this.computeCustomStyleSheetIncludes(resourceProvider, resource, config)}
${this.getImageStabilizerStyles(state)}`;
}
private getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string {
const out: string[] = [];
for (const resource of this.contributionProvider.contributions.previewScripts) {
out.push(`<script async
src="${escapeAttribute(resourceProvider.asWebviewUri(resource))}"
nonce="${nonce}"
charset="UTF-8"></script>`);
}
return out.join('\n');
}
private getCsp(
provider: WebviewResourceProvider,
resource: vscode.Uri,
nonce: string
): string {
const rule = provider.cspSource;
switch (this.cspArbiter.getSecurityLevelForResource(resource)) {
case MarkdownPreviewSecurityLevel.AllowInsecureContent:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${rule} http: https: data:; media-src 'self' ${rule} http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' http: https: data:; font-src 'self' ${rule} http: https: data:;">`;
case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${rule} https: data: http://localhost:* http://127.0.0.1:*; media-src 'self' ${rule} https: data: http://localhost:* http://127.0.0.1:*; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src 'self' ${rule} https: data: http://localhost:* http://127.0.0.1:*;">`;
case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:
return '<meta http-equiv="Content-Security-Policy" content="">';
case MarkdownPreviewSecurityLevel.Strict:
default:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${rule} https: data:; media-src 'self' ${rule} https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' https: data:; font-src 'self' ${rule} https: data:;">`;
}
}
}
function getNonce() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 64; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

View File

@@ -0,0 +1,263 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownEngine } from '../markdownEngine';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { Disposable, disposeAll } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { DynamicMarkdownPreview, ManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContentProvider } from './previewContentProvider';
import { scrollEditorToLine, StartingScrollFragment } from './scrolling';
import { TopmostLineMonitor } from './topmostLineMonitor';
export interface DynamicPreviewSettings {
readonly resourceColumn: vscode.ViewColumn;
readonly previewColumn: vscode.ViewColumn;
readonly locked: boolean;
}
class PreviewStore<T extends ManagedMarkdownPreview> extends Disposable {
private readonly _previews = new Set<T>();
public override dispose(): void {
super.dispose();
for (const preview of this._previews) {
preview.dispose();
}
this._previews.clear();
}
[Symbol.iterator](): Iterator<T> {
return this._previews[Symbol.iterator]();
}
public get(resource: vscode.Uri, previewSettings: DynamicPreviewSettings): T | undefined {
for (const preview of this._previews) {
if (preview.matchesResource(resource, previewSettings.previewColumn, previewSettings.locked)) {
return preview;
}
}
return undefined;
}
public add(preview: T) {
this._previews.add(preview);
}
public delete(preview: T) {
this._previews.delete(preview);
}
}
export class MarkdownPreviewManager extends Disposable implements vscode.WebviewPanelSerializer, vscode.CustomTextEditorProvider {
private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus';
private readonly _topmostLineMonitor = new TopmostLineMonitor();
private readonly _previewConfigurations = new MarkdownPreviewConfigurationManager();
private readonly _dynamicPreviews = this._register(new PreviewStore<DynamicMarkdownPreview>());
private readonly _staticPreviews = this._register(new PreviewStore<StaticMarkdownPreview>());
private _activePreview: ManagedMarkdownPreview | undefined = undefined;
public constructor(
private readonly _contentProvider: MarkdownContentProvider,
private readonly _logger: Logger,
private readonly _contributions: MarkdownContributionProvider,
private readonly _engine: MarkdownEngine,
) {
super();
this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this));
this._register(vscode.window.registerCustomEditorProvider(StaticMarkdownPreview.customEditorViewType, this, {
webviewOptions: { enableFindWidget: true }
}));
this._register(vscode.window.onDidChangeActiveTextEditor(textEditor => {
// When at a markdown file, apply existing scroll settings
if (textEditor?.document && isMarkdownFile(textEditor.document)) {
const line = this._topmostLineMonitor.getPreviousStaticEditorLineByUri(textEditor.document.uri);
if (typeof line === 'number') {
scrollEditorToLine(line, textEditor);
}
}
}));
}
public refresh() {
for (const preview of this._dynamicPreviews) {
preview.refresh();
}
for (const preview of this._staticPreviews) {
preview.refresh();
}
}
public updateConfiguration() {
for (const preview of this._dynamicPreviews) {
preview.updateConfiguration();
}
for (const preview of this._staticPreviews) {
preview.updateConfiguration();
}
}
public openDynamicPreview(
resource: vscode.Uri,
settings: DynamicPreviewSettings
): void {
let preview = this._dynamicPreviews.get(resource, settings);
if (preview) {
preview.reveal(settings.previewColumn);
} else {
preview = this.createNewDynamicPreview(resource, settings);
}
preview.update(
resource,
resource.fragment ? new StartingScrollFragment(resource.fragment) : undefined
);
}
public get activePreviewResource() {
return this._activePreview?.resource;
}
public get activePreviewResourceColumn() {
return this._activePreview?.resourceColumn;
}
public toggleLock() {
const preview = this._activePreview;
if (preview instanceof DynamicMarkdownPreview) {
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._dynamicPreviews) {
if (otherPreview !== preview && preview.matches(otherPreview)) {
otherPreview.dispose();
}
}
}
}
public async deserializeWebviewPanel(
webview: vscode.WebviewPanel,
state: any
): Promise<void> {
const resource = vscode.Uri.parse(state.resource);
const locked = state.locked;
const line = state.line;
const resourceColumn = state.resourceColumn;
const preview = await DynamicMarkdownPreview.revive(
{ resource, locked, line, resourceColumn },
webview,
this._contentProvider,
this._previewConfigurations,
this._logger,
this._topmostLineMonitor,
this._contributions,
this._engine);
this.registerDynamicPreview(preview);
}
public async resolveCustomTextEditor(
document: vscode.TextDocument,
webview: vscode.WebviewPanel
): Promise<void> {
const lineNumber = this._topmostLineMonitor.getPreviousStaticTextEditorLineByUri(document.uri);
const preview = StaticMarkdownPreview.revive(
document.uri,
webview,
this._contentProvider,
this._previewConfigurations,
this._topmostLineMonitor,
this._logger,
this._contributions,
this._engine,
lineNumber
);
this.registerStaticPreview(preview);
}
private createNewDynamicPreview(
resource: vscode.Uri,
previewSettings: DynamicPreviewSettings
): DynamicMarkdownPreview {
const activeTextEditorURI = vscode.window.activeTextEditor?.document.uri;
const scrollLine = (activeTextEditorURI?.toString() === resource.toString()) ? vscode.window.activeTextEditor?.visibleRanges[0].start.line : undefined;
const preview = DynamicMarkdownPreview.create(
{
resource,
resourceColumn: previewSettings.resourceColumn,
locked: previewSettings.locked,
line: scrollLine,
},
previewSettings.previewColumn,
this._contentProvider,
this._previewConfigurations,
this._logger,
this._topmostLineMonitor,
this._contributions,
this._engine);
this.setPreviewActiveContext(true);
this._activePreview = preview;
return this.registerDynamicPreview(preview);
}
private registerDynamicPreview(preview: DynamicMarkdownPreview): DynamicMarkdownPreview {
this._dynamicPreviews.add(preview);
preview.onDispose(() => {
this._dynamicPreviews.delete(preview);
});
this.trackActive(preview);
preview.onDidChangeViewState(() => {
// Remove other dynamic previews in our column
disposeAll(Array.from(this._dynamicPreviews).filter(otherPreview => preview !== otherPreview && preview.matches(otherPreview)));
});
return preview;
}
private registerStaticPreview(preview: StaticMarkdownPreview): StaticMarkdownPreview {
this._staticPreviews.add(preview);
preview.onDispose(() => {
this._staticPreviews.delete(preview);
});
this.trackActive(preview);
return preview;
}
private trackActive(preview: ManagedMarkdownPreview): void {
preview.onDidChangeViewState(({ webviewPanel }) => {
this.setPreviewActiveContext(webviewPanel.active);
this._activePreview = webviewPanel.active ? preview : undefined;
});
preview.onDispose(() => {
if (this._activePreview === preview) {
this.setPreviewActiveContext(false);
this._activePreview = undefined;
}
});
}
private setPreviewActiveContext(value: boolean) {
vscode.commands.executeCommand('setContext', MarkdownPreviewManager.markdownPreviewActiveContextKey, value);
}
}

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* 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';
/**
* Change the top-most visible line of `editor` to be at `line`
*/
export function scrollEditorToLine(
line: number,
editor: vscode.TextEditor
) {
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 StartingScrollFragment {
public readonly type = 'fragment';
constructor(
public readonly fragment: string,
) { }
}
export class StartingScrollLine {
public readonly type = 'line';
constructor(
public readonly line: number,
) { }
}
export type StartingScrollLocation = StartingScrollLine | StartingScrollFragment;

View File

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

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* 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 { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
export interface LastScrollLocation {
readonly line: number;
readonly uri: vscode.Uri;
}
export class TopmostLineMonitor extends Disposable {
private readonly pendingUpdates = new Map<string, number>();
private readonly throttle = 50;
private previousTextEditorInfo = new Map<string, LastScrollLocation>();
private previousStaticEditorInfo = new Map<string, LastScrollLocation>();
constructor() {
super();
if (vscode.window.activeTextEditor) {
const line = getVisibleLine(vscode.window.activeTextEditor);
this.setPreviousTextEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 });
}
this._register(vscode.window.onDidChangeTextEditorVisibleRanges(event => {
if (isMarkdownFile(event.textEditor.document)) {
const line = getVisibleLine(event.textEditor);
if (typeof line === 'number') {
this.updateLine(event.textEditor.document.uri, line);
this.setPreviousTextEditorLine({ uri: event.textEditor.document.uri, line: line });
}
}
}));
}
private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri; readonly line: number }>());
public readonly onDidChanged = this._onChanged.event;
public setPreviousStaticEditorLine(scrollLocation: LastScrollLocation): void {
this.previousStaticEditorInfo.set(scrollLocation.uri.toString(), scrollLocation);
}
public getPreviousStaticEditorLineByUri(resource: vscode.Uri): number | undefined {
const scrollLoc = this.previousStaticEditorInfo.get(resource.toString());
this.previousStaticEditorInfo.delete(resource.toString());
return scrollLoc?.line;
}
public setPreviousTextEditorLine(scrollLocation: LastScrollLocation): void {
this.previousTextEditorInfo.set(scrollLocation.uri.toString(), scrollLocation);
}
public getPreviousTextEditorLineByUri(resource: vscode.Uri): number | undefined {
const scrollLoc = this.previousTextEditorInfo.get(resource.toString());
this.previousTextEditorInfo.delete(resource.toString());
return scrollLoc?.line;
}
public getPreviousStaticTextEditorLineByUri(resource: vscode.Uri): number | undefined {
const state = this.previousStaticEditorInfo.get(resource.toString());
return state?.line;
}
public updateLine(
resource: vscode.Uri,
line: number
) {
const key = resource.toString();
if (!this.pendingUpdates.has(key)) {
// schedule update
setTimeout(() => {
if (this.pendingUpdates.has(key)) {
this._onChanged.fire({
resource,
line: this.pendingUpdates.get(key) as number
});
this.pendingUpdates.delete(key);
}
}, this.throttle);
}
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;
}