Experiment with switching markdown extension to use native privates

Let's try this out with one extension to start
This commit is contained in:
Matt Bierner
2026-03-10 23:13:16 -07:00
parent 6597286e32
commit 7df46143a1
43 changed files with 845 additions and 690 deletions

View File

@@ -41,16 +41,28 @@ export interface ImageInfo {
}
export class MdDocumentRenderer {
readonly #engine: MarkdownItEngine;
readonly #context: vscode.ExtensionContext;
readonly #cspArbiter: ContentSecurityPolicyArbiter;
readonly #contributionProvider: MarkdownContributionProvider;
readonly #logger: ILogger;
constructor(
private readonly _engine: MarkdownItEngine,
private readonly _context: vscode.ExtensionContext,
private readonly _cspArbiter: ContentSecurityPolicyArbiter,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _logger: ILogger
engine: MarkdownItEngine,
context: vscode.ExtensionContext,
cspArbiter: ContentSecurityPolicyArbiter,
contributionProvider: MarkdownContributionProvider,
logger: ILogger
) {
this.#engine = engine;
this.#context = context;
this.#cspArbiter = cspArbiter;
this.#contributionProvider = contributionProvider;
this.#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'),
dark: vscode.Uri.joinPath(this.#context.extensionUri, 'media', 'preview-dark.svg'),
light: vscode.Uri.joinPath(this.#context.extensionUri, 'media', 'preview-light.svg'),
};
}
@@ -76,15 +88,15 @@ export class MdDocumentRenderer {
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
scrollEditorWithPreview: config.scrollEditorWithPreview,
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,
disableSecurityWarnings: this._cspArbiter.shouldDisableSecurityWarnings(),
disableSecurityWarnings: this.#cspArbiter.shouldDisableSecurityWarnings(),
webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(),
};
this._logger.trace('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData);
this.#logger.trace('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData);
// Content Security Policy
const nonce = generateUuid();
const csp = this._getCsp(resourceProvider, sourceUri, nonce);
const csp = this.#getCsp(resourceProvider, sourceUri, nonce);
const body = await this.renderBody(markdownDocument, resourceProvider);
if (token.isCancellationRequested) {
@@ -92,7 +104,7 @@ export class MdDocumentRenderer {
}
const html = `<!DOCTYPE html>
<html style="${escapeAttribute(this._getSettingsOverrideStyles(config))}">
<html style="${escapeAttribute(this.#getSettingsOverrideStyles(config))}">
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(csp)}">
@@ -101,12 +113,12 @@ export class MdDocumentRenderer {
data-strings="${escapeAttribute(JSON.stringify(previewStrings))}"
data-state="${escapeAttribute(JSON.stringify(state || {}))}"
data-initial-md-content="${escapeAttribute(body.html)}">
<script src="${this._extensionResourcePath(resourceProvider, 'pre.js')}" nonce="${nonce}"></script>
${this._getStyles(resourceProvider, sourceUri, config, imageInfo)}
<script src="${this.#extensionResourcePath(resourceProvider, 'pre.js')}" nonce="${nonce}"></script>
${this.#getStyles(resourceProvider, sourceUri, config, imageInfo)}
<base href="${resourceProvider.asWebviewUri(markdownDocument.uri)}">
</head>
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
${this._getScripts(resourceProvider, nonce)}
${this.#getScripts(resourceProvider, nonce)}
</body>
</html>`;
return {
@@ -119,7 +131,7 @@ export class MdDocumentRenderer {
markdownDocument: vscode.TextDocument,
resourceProvider: WebviewResourceProvider,
): Promise<MarkdownContentProviderOutput> {
const rendered = await this._engine.render(markdownDocument, resourceProvider);
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,
@@ -138,13 +150,13 @@ export class MdDocumentRenderer {
</html>`;
}
private _extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string {
#extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string {
const webviewResource = resourceProvider.asWebviewUri(
vscode.Uri.joinPath(this._context.extensionUri, 'media', mediaFile));
vscode.Uri.joinPath(this.#context.extensionUri, 'media', mediaFile));
return webviewResource.toString();
}
private _fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string {
#fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string {
if (!href) {
return href;
}
@@ -168,18 +180,18 @@ export class MdDocumentRenderer {
return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString();
}
private _computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
#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">`);
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 {
#getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string {
return [
config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '',
isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`,
@@ -187,7 +199,7 @@ export class MdDocumentRenderer {
].join(' ');
}
private _getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string {
#getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string {
if (!imageInfo.length) {
return '';
}
@@ -204,20 +216,20 @@ export class MdDocumentRenderer {
return ret;
}
private _getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string {
#getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string {
const baseStyles: string[] = [];
for (const resource of this._contributionProvider.contributions.previewStyles) {
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(imageInfo)}`;
${this.#computeCustomStyleSheetIncludes(resourceProvider, resource, config)}
${this.#getImageStabilizerStyles(imageInfo)}`;
}
private _getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string {
#getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string {
const out: string[] = [];
for (const resource of this._contributionProvider.contributions.previewScripts) {
for (const resource of this.#contributionProvider.contributions.previewScripts) {
out.push(`<script async
src="${escapeAttribute(resourceProvider.asWebviewUri(resource))}"
nonce="${nonce}"
@@ -226,13 +238,13 @@ export class MdDocumentRenderer {
return out.join('\n');
}
private _getCsp(
#getCsp(
provider: WebviewResourceProvider,
resource: vscode.Uri,
nonce: string
): string {
const rule = provider.cspSource.split(';')[0];
switch (this._cspArbiter.getSecurityLevelForResource(resource)) {
switch (this.#cspArbiter.getSecurityLevelForResource(resource)) {
case MarkdownPreviewSecurityLevel.AllowInsecureContent:
return `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:;`;

View File

@@ -21,16 +21,16 @@ import type { FromWebviewMessage, ToWebviewMessage } from '../../types/previewMe
export class PreviewDocumentVersion {
public readonly resource: vscode.Uri;
private readonly _version: number;
readonly #version: number;
public constructor(document: vscode.TextDocument) {
this.resource = document.uri;
this._version = document.version;
this.#version = document.version;
}
public equals(other: PreviewDocumentVersion): boolean {
return areUrisEqual(this.resource, other.resource)
&& this._version === other._version;
&& this.#version === other.#version;
}
}
@@ -42,59 +42,73 @@ interface MarkdownPreviewDelegate {
class MarkdownPreview extends Disposable implements WebviewResourceProvider {
private static readonly _unwatchedImageSchemes = new Set(['https', 'http', 'data']);
static readonly #unwatchedImageSchemes = new Set(['https', 'http', 'data']);
private _disposed: boolean = false;
#disposed: boolean = false;
private readonly _delay = 300;
private _throttleTimer: any;
readonly #delay = 300;
#throttleTimer: any;
private readonly _resource: vscode.Uri;
private readonly _webviewPanel: vscode.WebviewPanel;
readonly #resource: vscode.Uri;
readonly #webviewPanel: vscode.WebviewPanel;
private _line: number | undefined;
private readonly _scrollToFragment: string | undefined;
private _firstUpdate = true;
private _currentVersion?: PreviewDocumentVersion;
private _isScrolling = false;
#line: number | undefined;
readonly #scrollToFragment: string | undefined;
#firstUpdate = true;
#currentVersion?: PreviewDocumentVersion;
#isScrolling = false;
private _imageInfo: readonly ImageInfo[] = [];
private readonly _fileWatchersBySrc = new Map</* src: */ string, vscode.FileSystemWatcher>();
#imageInfo: readonly ImageInfo[] = [];
readonly #fileWatchersBySrc = new Map</* src: */ string, vscode.FileSystemWatcher>();
private readonly _onScrollEmitter = this._register(new vscode.EventEmitter<LastScrollLocation>());
public readonly onScroll = this._onScrollEmitter.event;
readonly #onScrollEmitter = this._register(new vscode.EventEmitter<LastScrollLocation>());
public readonly onScroll = this.#onScrollEmitter.event;
private readonly _disposeCts = this._register(new vscode.CancellationTokenSource());
readonly #disposeCts = this._register(new vscode.CancellationTokenSource());
readonly #delegate: MarkdownPreviewDelegate;
readonly #contentProvider: MdDocumentRenderer;
readonly #previewConfigurations: MarkdownPreviewConfigurationManager;
readonly #logger: ILogger;
readonly #contributionProvider: MarkdownContributionProvider;
readonly #opener: MdLinkOpener;
constructor(
webview: vscode.WebviewPanel,
resource: vscode.Uri,
startingScroll: StartingScrollLocation | undefined,
private readonly _delegate: MarkdownPreviewDelegate,
private readonly _contentProvider: MdDocumentRenderer,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _logger: ILogger,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _opener: MdLinkOpener,
delegate: MarkdownPreviewDelegate,
contentProvider: MdDocumentRenderer,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: ILogger,
contributionProvider: MarkdownContributionProvider,
opener: MdLinkOpener,
) {
super();
this._webviewPanel = webview;
this._resource = resource;
this.#delegate = delegate;
this.#contentProvider = contentProvider;
this.#previewConfigurations = previewConfigurations;
this.#logger = logger;
this.#contributionProvider = contributionProvider;
this.#opener = opener;
this.#webviewPanel = webview;
this.#resource = resource;
switch (startingScroll?.type) {
case 'line':
if (!isNaN(startingScroll.line!)) {
this._line = startingScroll.line;
this.#line = startingScroll.line;
}
break;
case 'fragment':
this._scrollToFragment = startingScroll.fragment;
this.#scrollToFragment = startingScroll.fragment;
break;
}
this._register(_contributionProvider.onContributionsChanged(() => {
this._register(contributionProvider.onContributionsChanged(() => {
setTimeout(() => this.refresh(true), 0);
}));
@@ -122,26 +136,26 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}));
}
this._register(this._webviewPanel.webview.onDidReceiveMessage((e: FromWebviewMessage.Type) => {
if (e.source !== this._resource.toString()) {
this._register(this.#webviewPanel.webview.onDidReceiveMessage((e: FromWebviewMessage.Type) => {
if (e.source !== this.#resource.toString()) {
return;
}
switch (e.type) {
case 'cacheImageSizes':
this._imageInfo = e.imageData;
this.#imageInfo = e.imageData;
break;
case 'revealLine':
this._onDidScrollPreview(e.line);
this.#onDidScrollPreview(e.line);
break;
case 'didClick':
this._onDidClickPreview(e.line);
this.#onDidClickPreview(e.line);
break;
case 'openLink':
this._onDidClickPreviewLink(e.href);
this.#onDidClickPreviewLink(e.href);
break;
case 'showPreviewSecuritySelector':
@@ -159,29 +173,29 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
override dispose() {
this._disposeCts.cancel();
this.#disposeCts.cancel();
super.dispose();
this._disposed = true;
this.#disposed = true;
clearTimeout(this._throttleTimer);
for (const entry of this._fileWatchersBySrc.values()) {
clearTimeout(this.#throttleTimer);
for (const entry of this.#fileWatchersBySrc.values()) {
entry.dispose();
}
this._fileWatchersBySrc.clear();
this.#fileWatchersBySrc.clear();
}
public get resource(): vscode.Uri {
return this._resource;
return this.#resource;
}
public get state() {
return {
resource: this._resource.toString(),
line: this._line,
fragment: this._scrollToFragment,
...this._delegate.getAdditionalState(),
resource: this.#resource.toString(),
line: this.#line,
fragment: this.#scrollToFragment,
...this.#delegate.getAdditionalState(),
};
}
@@ -191,79 +205,79 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
*/
public refresh(forceUpdate: boolean = false) {
// Schedule update if none is pending
if (!this._throttleTimer) {
if (this._firstUpdate) {
this._updatePreview(true);
if (!this.#throttleTimer) {
if (this.#firstUpdate) {
this.#updatePreview(true);
} else {
this._throttleTimer = setTimeout(() => this._updatePreview(forceUpdate), this._delay);
this.#throttleTimer = setTimeout(() => this.#updatePreview(forceUpdate), this.#delay);
}
}
this._firstUpdate = false;
this.#firstUpdate = false;
}
public isPreviewOf(resource: vscode.Uri): boolean {
return areUrisEqual(this._resource, resource);
return areUrisEqual(this.#resource, resource);
}
public postMessage(msg: ToWebviewMessage.Type) {
if (!this._disposed) {
this._webviewPanel.webview.postMessage(msg);
if (!this.#disposed) {
this.#webviewPanel.webview.postMessage(msg);
}
}
public scrollTo(topLine: number) {
if (this._disposed) {
if (this.#disposed) {
return;
}
if (this._isScrolling) {
this._isScrolling = false;
if (this.#isScrolling) {
this.#isScrolling = false;
return;
}
this._logger.trace('MarkdownPreview', 'updateForView', { markdownFile: this._resource });
this._line = topLine;
this.#logger.trace('MarkdownPreview', 'updateForView', { markdownFile: this.#resource });
this.#line = topLine;
this.postMessage({
type: 'updateView',
line: topLine,
source: this._resource.toString()
source: this.#resource.toString()
});
}
private async _updatePreview(forceUpdate?: boolean): Promise<void> {
clearTimeout(this._throttleTimer);
this._throttleTimer = undefined;
async #updatePreview(forceUpdate?: boolean): Promise<void> {
clearTimeout(this.#throttleTimer);
this.#throttleTimer = undefined;
if (this._disposed) {
if (this.#disposed) {
return;
}
let document: vscode.TextDocument;
try {
document = await vscode.workspace.openTextDocument(this._resource);
document = await vscode.workspace.openTextDocument(this.#resource);
} catch {
if (!this._disposed) {
await this._showFileNotFoundError();
if (!this.#disposed) {
await this.#showFileNotFoundError();
}
return;
}
if (this._disposed) {
if (this.#disposed) {
return;
}
const pendingVersion = new PreviewDocumentVersion(document);
if (!forceUpdate && this._currentVersion?.equals(pendingVersion)) {
if (this._line) {
this.scrollTo(this._line);
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 shouldReloadPage = forceUpdate || !this.#currentVersion || this.#currentVersion.resource.toString() !== pendingVersion.resource.toString() || !this.#webviewPanel.visible;
this.#currentVersion = pendingVersion;
let selectedLine: number | undefined = undefined;
for (const editor of vscode.window.visibleTextEditors) {
@@ -274,21 +288,21 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
const content = await (shouldReloadPage
? this._contentProvider.renderDocument(document, this, this._previewConfigurations, this._line, selectedLine, this.state, this._imageInfo, this._disposeCts.token)
: this._contentProvider.renderBody(document, this));
? this.#contentProvider.renderDocument(document, this, this.#previewConfigurations, this.#line, selectedLine, this.state, this.#imageInfo, this.#disposeCts.token)
: this.#contentProvider.renderBody(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);
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);
#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;
}
@@ -298,12 +312,12 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
continue;
}
this._isScrolling = true;
this.#isScrolling = true;
scrollEditorToLine(line, editor);
}
}
private async _onDidClickPreview(line: number): Promise<void> {
async #onDidClickPreview(line: number): Promise<void> {
// fix #82457, find currently opened but unfocused source tab
await vscode.commands.executeCommand('markdown.showSource');
@@ -322,97 +336,97 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
}
await vscode.workspace.openTextDocument(this._resource)
await vscode.workspace.openTextDocument(this.#resource)
.then(vscode.window.showTextDocument)
.then((editor) => {
revealLineInEditor(editor);
}, () => {
vscode.window.showErrorMessage(vscode.l10n.t('Could not open {0}', this._resource.toString()));
vscode.window.showErrorMessage(vscode.l10n.t('Could not open {0}', this.#resource.toString()));
});
}
private async _showFileNotFoundError() {
this._webviewPanel.webview.html = this._contentProvider.renderFileNotFoundDocument(this._resource);
async #showFileNotFoundError() {
this.#webviewPanel.webview.html = this.#contentProvider.renderFileNotFoundDocument(this.#resource);
}
private _updateWebviewContent(html: string, reloadPage: boolean): void {
if (this._disposed) {
#updateWebviewContent(html: string, reloadPage: boolean): void {
if (this.#disposed) {
return;
}
if (this._delegate.getTitle) {
this._webviewPanel.title = this._delegate.getTitle(this._resource);
if (this.#delegate.getTitle) {
this.#webviewPanel.title = this.#delegate.getTitle(this.#resource);
}
this._webviewPanel.webview.options = this._getWebviewOptions();
this.#webviewPanel.webview.options = this.#getWebviewOptions();
if (reloadPage) {
this._webviewPanel.webview.html = html;
this.#webviewPanel.webview.html = html;
} else {
this.postMessage({
type: 'updateContent',
content: html,
source: this._resource.toString(),
source: this.#resource.toString(),
});
}
}
private _updateImageWatchers(srcs: Set<string>) {
#updateImageWatchers(srcs: Set<string>) {
// Delete stale file watchers.
for (const [src, watcher] of this._fileWatchersBySrc) {
for (const [src, watcher] of this.#fileWatchersBySrc) {
if (!srcs.has(src)) {
watcher.dispose();
this._fileWatchersBySrc.delete(src);
this.#fileWatchersBySrc.delete(src);
}
}
// Create new file watchers.
const root = vscode.Uri.joinPath(this._resource, '../');
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)) {
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);
this.#fileWatchersBySrc.set(src, watcher);
}
}
}
private _getWebviewOptions(): vscode.WebviewOptions {
#getWebviewOptions(): vscode.WebviewOptions {
return {
enableScripts: true,
enableForms: false,
localResourceRoots: this._getLocalResourceRoots()
localResourceRoots: this.#getLocalResourceRoots()
};
}
private _getLocalResourceRoots(): ReadonlyArray<vscode.Uri> {
const baseRoots = Array.from(this._contributionProvider.contributions.previewResourceRoots);
#getLocalResourceRoots(): ReadonlyArray<vscode.Uri> {
const baseRoots = Array.from(this.#contributionProvider.contributions.previewResourceRoots);
const folder = vscode.workspace.getWorkspaceFolder(this._resource);
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));
baseRoots.push(uri.Utils.dirname(this.#resource));
}
return baseRoots;
}
private async _onDidClickPreviewLink(href: string) {
async #onDidClickPreviewLink(href: string) {
const config = vscode.workspace.getConfiguration('markdown', this.resource);
const openLinks = config.get<string>('preview.openMarkdownLinks', 'inPreview');
if (openLinks === 'inPreview') {
const resolved = await this._opener.resolveDocumentLink(href, this.resource);
const resolved = await this.#opener.resolveDocumentLink(href, this.resource);
if (resolved.kind === 'file') {
try {
const doc = await vscode.workspace.openTextDocument(vscode.Uri.from(resolved.uri));
if (isMarkdownFile(doc)) {
return this._delegate.openPreviewLinkToMarkdownFile(doc.uri, resolved.fragment ? decodeURIComponent(resolved.fragment) : undefined);
return this.#delegate.openPreviewLinkToMarkdownFile(doc.uri, resolved.fragment ? decodeURIComponent(resolved.fragment) : undefined);
}
} catch {
// Noop
@@ -420,21 +434,21 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
}
return this._opener.openDocumentLink(href, this.resource);
return this.#opener.openDocumentLink(href, this.resource);
}
//#region WebviewResourceProvider
asWebviewUri(resource: vscode.Uri) {
return this._webviewPanel.webview.asWebviewUri(resource);
return this.#webviewPanel.webview.asWebviewUri(resource);
}
get cspSource() {
return [
this._webviewPanel.webview.cspSource,
this.#webviewPanel.webview.cspSource,
// On web, we also need to allow loading of resources from contributed extensions
...this._contributionProvider.contributions.previewResourceRoots
...this.#contributionProvider.contributions.previewResourceRoots
.filter(root => root.scheme === 'http' || root.scheme === 'https')
.map(root => {
const dirRoot = root.path.endsWith('/') ? root : root.with({ path: root.path + '/' });
@@ -484,13 +498,16 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, opener, scrollLine);
}
private readonly _preview: MarkdownPreview;
readonly #preview: MarkdownPreview;
readonly #webviewPanel: vscode.WebviewPanel;
readonly #previewConfigurations: MarkdownPreviewConfigurationManager;
private constructor(
private readonly _webviewPanel: vscode.WebviewPanel,
webviewPanel: vscode.WebviewPanel,
resource: vscode.Uri,
contentProvider: MdDocumentRenderer,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
previewConfigurations: MarkdownPreviewConfigurationManager,
topmostLineMonitor: TopmostLineMonitor,
logger: ILogger,
contributionProvider: MarkdownContributionProvider,
@@ -498,52 +515,56 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
scrollLine?: number,
) {
super();
this.#webviewPanel = webviewPanel;
this.#previewConfigurations = previewConfigurations;
const topScrollLocation = scrollLine ? new StartingScrollLine(scrollLine) : undefined;
this._preview = this._register(new MarkdownPreview(this._webviewPanel, resource, topScrollLocation, {
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);
}), StaticMarkdownPreview.customEditorViewType, this.#webviewPanel.viewColumn);
}
}, contentProvider, _previewConfigurations, logger, contributionProvider, opener));
}, contentProvider, previewConfigurations, logger, contributionProvider, opener));
this._register(this._webviewPanel.onDidDispose(() => {
this._register(this.#webviewPanel.onDidDispose(() => {
this.dispose();
}));
this._register(this._webviewPanel.onDidChangeViewState(e => {
this._onDidChangeViewState.fire(e);
this._register(this.#webviewPanel.onDidChangeViewState(e => {
this.#onDidChangeViewState.fire(e);
}));
this._register(this._preview.onScroll((scrollInfo) => {
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);
if (this.#preview.isPreviewOf(event.resource)) {
this.#preview.scrollTo(event.line);
}
}));
}
copyImage(id: string) {
this._webviewPanel.reveal();
this._preview.postMessage({
this.#webviewPanel.reveal();
this.#preview.postMessage({
type: 'copyImage',
source: this.resource.toString(),
id: id
});
}
private readonly _onDispose = this._register(new vscode.EventEmitter<void>());
public readonly onDispose = this._onDispose.event;
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;
readonly #onDidChangeViewState = this._register(new vscode.EventEmitter<vscode.WebviewPanelOnDidChangeViewStateEvent>());
public readonly onDidChangeViewState = this.#onDidChangeViewState.event;
override dispose() {
this._onDispose.fire();
this.#onDispose.fire();
super.dispose();
}
@@ -556,21 +577,21 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
}
public refresh() {
this._preview.refresh(true);
this.#preview.refresh(true);
}
public updateConfiguration() {
if (this._previewConfigurations.hasConfigurationChanged(this._preview.resource)) {
if (this.#previewConfigurations.hasConfigurationChanged(this.#preview.resource)) {
this.refresh();
}
}
public get resource() {
return this._preview.resource;
return this.#preview.resource;
}
public get resourceColumn() {
return this._webviewPanel.viewColumn || vscode.ViewColumn.One;
return this.#webviewPanel.viewColumn || vscode.ViewColumn.One;
}
}
@@ -585,11 +606,11 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
public static readonly viewType = 'markdown.preview';
private readonly _resourceColumn: vscode.ViewColumn;
private _locked: boolean;
readonly #resourceColumn: vscode.ViewColumn;
#locked: boolean;
private readonly _webviewPanel: vscode.WebviewPanel;
private _preview: MarkdownPreview;
readonly #webviewPanel: vscode.WebviewPanel;
#preview: MarkdownPreview;
public static revive(
input: DynamicPreviewInput,
@@ -619,7 +640,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
): DynamicMarkdownPreview {
const webview = vscode.window.createWebviewPanel(
DynamicMarkdownPreview.viewType,
DynamicMarkdownPreview._getPreviewTitle(input.resource, input.locked),
DynamicMarkdownPreview.#getPreviewTitle(input.resource, input.locked),
previewColumn, { enableFindWidget: true, });
webview.iconPath = contentProvider.iconPath;
@@ -628,43 +649,57 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, opener);
}
readonly #contentProvider: MdDocumentRenderer;
readonly #previewConfigurations: MarkdownPreviewConfigurationManager;
readonly #logger: ILogger;
readonly #topmostLineMonitor: TopmostLineMonitor;
readonly #contributionProvider: MarkdownContributionProvider;
readonly #opener: MdLinkOpener;
private constructor(
webview: vscode.WebviewPanel,
input: DynamicPreviewInput,
private readonly _contentProvider: MdDocumentRenderer,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _logger: ILogger,
private readonly _topmostLineMonitor: TopmostLineMonitor,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _opener: MdLinkOpener,
contentProvider: MdDocumentRenderer,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: ILogger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider,
opener: MdLinkOpener,
) {
super();
this._webviewPanel = webview;
this.#contentProvider = contentProvider;
this.#previewConfigurations = previewConfigurations;
this.#logger = logger;
this.#topmostLineMonitor = topmostLineMonitor;
this.#contributionProvider = contributionProvider;
this.#opener = opener;
this._resourceColumn = input.resourceColumn;
this._locked = input.locked;
this.#webviewPanel = webview;
this._preview = this._createPreview(input.resource, typeof input.line === 'number' ? new StartingScrollLine(input.line) : undefined);
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.#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(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({
if (this.#preview.isPreviewOf(event.textEditor.document.uri)) {
this.#preview.postMessage({
type: 'onDidChangeTextEditorSelection',
line: event.selections[0].active.line,
source: this._preview.resource.toString()
source: this.#preview.resource.toString()
});
}
}));
@@ -675,7 +710,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
return;
}
if (isMarkdownFile(editor.document) && !this._locked && !this._preview.isPreviewOf(editor.document.uri)) {
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);
}
@@ -683,56 +718,56 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
}
copyImage(id: string) {
this._webviewPanel.reveal();
this._preview.postMessage({
this.#webviewPanel.reveal();
this.#preview.postMessage({
type: 'copyImage',
source: this.resource.toString(),
id: id
});
}
private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter<void>());
public readonly onDispose = this._onDisposeEmitter.event;
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;
readonly #onDidChangeViewStateEmitter = this._register(new vscode.EventEmitter<vscode.WebviewPanelOnDidChangeViewStateEvent>());
public readonly onDidChangeViewState = this.#onDidChangeViewStateEmitter.event;
override dispose() {
this._preview.dispose();
this._webviewPanel.dispose();
this.#preview.dispose();
this.#webviewPanel.dispose();
this._onDisposeEmitter.fire();
this._onDisposeEmitter.dispose();
this.#onDisposeEmitter.fire();
this.#onDisposeEmitter.dispose();
super.dispose();
}
public get resource() {
return this._preview.resource;
return this.#preview.resource;
}
public get resourceColumn() {
return this._resourceColumn;
return this.#resourceColumn;
}
public reveal(viewColumn: vscode.ViewColumn) {
this._webviewPanel.reveal(viewColumn);
this.#webviewPanel.reveal(viewColumn);
}
public refresh() {
this._preview.refresh(true);
this.#preview.refresh(true);
}
public updateConfiguration() {
if (this._previewConfigurations.hasConfigurationChanged(this._preview.resource)) {
if (this.#previewConfigurations.hasConfigurationChanged(this.#preview.resource)) {
this.refresh();
}
}
public update(newResource: vscode.Uri, scrollLocation?: StartingScrollLocation) {
if (this._preview.isPreviewOf(newResource)) {
if (this.#preview.isPreviewOf(newResource)) {
switch (scrollLocation?.type) {
case 'line':
this._preview.scrollTo(scrollLocation.line);
this.#preview.scrollTo(scrollLocation.line);
return;
case 'fragment':
@@ -744,16 +779,16 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
}
}
this._preview.dispose();
this._preview = this._createPreview(newResource, scrollLocation);
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);
this.#locked = !this.#locked;
this.#webviewPanel.title = DynamicMarkdownPreview.#getPreviewTitle(this.#preview.resource, this.#locked);
}
private static _getPreviewTitle(resource: vscode.Uri, locked: boolean): string {
static #getPreviewTitle(resource: vscode.Uri, locked: boolean): string {
const resourceLabel = uri.Utils.basename(resource);
return locked
? vscode.l10n.t('[Preview] {0}', resourceLabel)
@@ -761,7 +796,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
}
public get position(): vscode.ViewColumn | undefined {
return this._webviewPanel.viewColumn;
return this.#webviewPanel.viewColumn;
}
public matchesResource(
@@ -773,34 +808,34 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
return false;
}
if (this._locked) {
return otherLocked && this._preview.isPreviewOf(otherResource);
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);
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),
#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,
locked: this.#locked,
};
},
openPreviewLinkToMarkdownFile: (link: vscode.Uri, fragment?: string) => {
this.update(link, fragment ? new StartingScrollFragment(fragment) : undefined);
}
},
this._contentProvider,
this._previewConfigurations,
this._logger,
this._contributionProvider,
this._opener);
this.#contentProvider,
this.#previewConfigurations,
this.#logger,
this.#contributionProvider,
this.#opener);
}
}

View File

@@ -73,24 +73,24 @@ export class MarkdownPreviewConfiguration {
}
export class MarkdownPreviewConfigurationManager {
private readonly _previewConfigurationsForWorkspaces = new Map<string, MarkdownPreviewConfiguration>();
readonly #previewConfigurationsForWorkspaces = new Map<string, MarkdownPreviewConfiguration>();
public loadAndCacheConfiguration(
resource: vscode.Uri
): MarkdownPreviewConfiguration {
const config = MarkdownPreviewConfiguration.getForResource(resource);
this._previewConfigurationsForWorkspaces.set(this._getKey(resource), config);
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 key = this.#getKey(resource);
const currentConfig = this.#previewConfigurationsForWorkspaces.get(key);
const newConfig = MarkdownPreviewConfiguration.getForResource(resource);
return !currentConfig?.isEqualTo(newConfig);
}
private _getKey(
#getKey(
resource: vscode.Uri
): string {
const folder = vscode.workspace.getWorkspaceFolder(resource);

View File

@@ -24,23 +24,23 @@ export interface DynamicPreviewSettings {
class PreviewStore<T extends IManagedMarkdownPreview> extends Disposable {
private readonly _previews = new Set<T>();
readonly #previews = new Set<T>();
public override dispose(): void {
super.dispose();
for (const preview of this._previews) {
for (const preview of this.#previews) {
preview.dispose();
}
this._previews.clear();
this.#previews.clear();
}
[Symbol.iterator](): Iterator<T> {
return this._previews[Symbol.iterator]();
return this.#previews[Symbol.iterator]();
}
public get(resource: vscode.Uri, previewSettings: DynamicPreviewSettings): T | undefined {
const previewColumn = this._resolvePreviewColumn(previewSettings);
for (const preview of this._previews) {
const previewColumn = this.#resolvePreviewColumn(previewSettings);
for (const preview of this.#previews) {
if (preview.matchesResource(resource, previewColumn, previewSettings.locked)) {
return preview;
}
@@ -49,14 +49,14 @@ class PreviewStore<T extends IManagedMarkdownPreview> extends Disposable {
}
public add(preview: T) {
this._previews.add(preview);
this.#previews.add(preview);
}
public delete(preview: T) {
this._previews.delete(preview);
this.#previews.delete(preview);
}
private _resolvePreviewColumn(previewSettings: DynamicPreviewSettings): vscode.ViewColumn | undefined {
#resolvePreviewColumn(previewSettings: DynamicPreviewSettings): vscode.ViewColumn | undefined {
if (previewSettings.previewColumn === vscode.ViewColumn.Active) {
return vscode.window.tabGroups.activeTabGroup.viewColumn;
}
@@ -71,22 +71,32 @@ class PreviewStore<T extends IManagedMarkdownPreview> extends Disposable {
export class MarkdownPreviewManager extends Disposable implements vscode.WebviewPanelSerializer, vscode.CustomTextEditorProvider {
private readonly _topmostLineMonitor = new TopmostLineMonitor();
private readonly _previewConfigurations = new MarkdownPreviewConfigurationManager();
readonly #topmostLineMonitor = new TopmostLineMonitor();
readonly #previewConfigurations = new MarkdownPreviewConfigurationManager();
private readonly _dynamicPreviews = this._register(new PreviewStore<DynamicMarkdownPreview>());
private readonly _staticPreviews = this._register(new PreviewStore<StaticMarkdownPreview>());
readonly #dynamicPreviews = this._register(new PreviewStore<DynamicMarkdownPreview>());
readonly #staticPreviews = this._register(new PreviewStore<StaticMarkdownPreview>());
private _activePreview: IManagedMarkdownPreview | undefined = undefined;
#activePreview: IManagedMarkdownPreview | undefined = undefined;
readonly #contentProvider: MdDocumentRenderer;
readonly #logger: ILogger;
readonly #contributions: MarkdownContributionProvider;
readonly #opener: MdLinkOpener;
public constructor(
private readonly _contentProvider: MdDocumentRenderer,
private readonly _logger: ILogger,
private readonly _contributions: MarkdownContributionProvider,
private readonly _opener: MdLinkOpener,
contentProvider: MdDocumentRenderer,
logger: ILogger,
contributions: MarkdownContributionProvider,
opener: MdLinkOpener,
) {
super();
this.#contentProvider = contentProvider;
this.#logger = logger;
this.#contributions = contributions;
this.#opener = opener;
this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this));
this._register(vscode.window.registerCustomEditorProvider(StaticMarkdownPreview.customEditorViewType, this, {
@@ -96,7 +106,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
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);
const line = this.#topmostLineMonitor.getPreviousStaticEditorLineByUri(textEditor.document.uri);
if (typeof line === 'number') {
scrollEditorToLine(line, textEditor);
}
@@ -105,19 +115,19 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
}
public refresh() {
for (const preview of this._dynamicPreviews) {
for (const preview of this.#dynamicPreviews) {
preview.refresh();
}
for (const preview of this._staticPreviews) {
for (const preview of this.#staticPreviews) {
preview.refresh();
}
}
public updateConfiguration() {
for (const preview of this._dynamicPreviews) {
for (const preview of this.#dynamicPreviews) {
preview.updateConfiguration();
}
for (const preview of this._staticPreviews) {
for (const preview of this.#staticPreviews) {
preview.updateConfiguration();
}
}
@@ -126,11 +136,11 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
resource: vscode.Uri,
settings: DynamicPreviewSettings
): void {
let preview = this._dynamicPreviews.get(resource, settings);
let preview = this.#dynamicPreviews.get(resource, settings);
if (preview) {
preview.reveal(settings.previewColumn);
} else {
preview = this._createNewDynamicPreview(resource, settings);
preview = this.#createNewDynamicPreview(resource, settings);
}
preview.update(
@@ -140,15 +150,15 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
}
public get activePreviewResource() {
return this._activePreview?.resource;
return this.#activePreview?.resource;
}
public get activePreviewResourceColumn() {
return this._activePreview?.resourceColumn;
return this.#activePreview?.resourceColumn;
}
public findPreview(resource: vscode.Uri): IManagedMarkdownPreview | undefined {
for (const preview of [...this._dynamicPreviews, ...this._staticPreviews]) {
for (const preview of [...this.#dynamicPreviews, ...this.#staticPreviews]) {
if (preview.resource.fsPath === resource.fsPath) {
return preview;
}
@@ -157,12 +167,12 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
}
public toggleLock() {
const preview = this._activePreview;
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) {
for (const otherPreview of this.#dynamicPreviews) {
if (otherPreview !== preview && preview.matches(otherPreview)) {
otherPreview.dispose();
}
@@ -172,7 +182,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
public openDocumentLink(linkText: string, fromResource: vscode.Uri) {
const viewColumn = this.findPreview(fromResource)?.resourceColumn;
return this._opener.openDocumentLink(linkText, fromResource, viewColumn);
return this.#opener.openDocumentLink(linkText, fromResource, viewColumn);
}
public async deserializeWebviewPanel(
@@ -188,14 +198,14 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
const preview = DynamicMarkdownPreview.revive(
{ resource, locked, line, resourceColumn },
webview,
this._contentProvider,
this._previewConfigurations,
this._logger,
this._topmostLineMonitor,
this._contributions,
this._opener);
this.#contentProvider,
this.#previewConfigurations,
this.#logger,
this.#topmostLineMonitor,
this.#contributions,
this.#opener);
this._registerDynamicPreview(preview);
this.#registerDynamicPreview(preview);
} catch (e) {
console.error(e);
@@ -237,23 +247,23 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
document: vscode.TextDocument,
webview: vscode.WebviewPanel
): Promise<void> {
const lineNumber = this._topmostLineMonitor.getPreviousStaticTextEditorLineByUri(document.uri);
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._opener,
this.#contentProvider,
this.#previewConfigurations,
this.#topmostLineMonitor,
this.#logger,
this.#contributions,
this.#opener,
lineNumber
);
this._registerStaticPreview(preview);
this._activePreview = preview;
this.#registerStaticPreview(preview);
this.#activePreview = preview;
}
private _createNewDynamicPreview(
#createNewDynamicPreview(
resource: vscode.Uri,
previewSettings: DynamicPreviewSettings
): DynamicMarkdownPreview {
@@ -267,52 +277,52 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
line: scrollLine,
},
previewSettings.previewColumn,
this._contentProvider,
this._previewConfigurations,
this._logger,
this._topmostLineMonitor,
this._contributions,
this._opener);
this.#contentProvider,
this.#previewConfigurations,
this.#logger,
this.#topmostLineMonitor,
this.#contributions,
this.#opener);
this._activePreview = preview;
return this._registerDynamicPreview(preview);
this.#activePreview = preview;
return this.#registerDynamicPreview(preview);
}
private _registerDynamicPreview(preview: DynamicMarkdownPreview): DynamicMarkdownPreview {
this._dynamicPreviews.add(preview);
#registerDynamicPreview(preview: DynamicMarkdownPreview): DynamicMarkdownPreview {
this.#dynamicPreviews.add(preview);
preview.onDispose(() => {
this._dynamicPreviews.delete(preview);
this.#dynamicPreviews.delete(preview);
});
this._trackActive(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)));
disposeAll(Array.from(this.#dynamicPreviews).filter(otherPreview => preview !== otherPreview && preview.matches(otherPreview)));
});
return preview;
}
private _registerStaticPreview(preview: StaticMarkdownPreview): StaticMarkdownPreview {
this._staticPreviews.add(preview);
#registerStaticPreview(preview: StaticMarkdownPreview): StaticMarkdownPreview {
this.#staticPreviews.add(preview);
preview.onDispose(() => {
this._staticPreviews.delete(preview);
this.#staticPreviews.delete(preview);
});
this._trackActive(preview);
this.#trackActive(preview);
return preview;
}
private _trackActive(preview: IManagedMarkdownPreview): void {
#trackActive(preview: IManagedMarkdownPreview): void {
preview.onDidChangeViewState(({ webviewPanel }) => {
this._activePreview = webviewPanel.active ? preview : undefined;
this.#activePreview = webviewPanel.active ? preview : undefined;
});
preview.onDispose(() => {
if (this._activePreview === preview) {
this._activePreview = undefined;
if (this.#activePreview === preview) {
this.#activePreview = undefined;
}
});
}

View File

@@ -27,31 +27,37 @@ export interface ContentSecurityPolicyArbiter {
}
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:';
readonly #old_trusted_workspace_key = 'trusted_preview_workspace:';
readonly #security_level_key = 'preview_security_level:';
readonly #should_disable_security_warning_key = 'preview_should_show_security_warning:';
readonly #globalState: vscode.Memento;
readonly #workspaceState: vscode.Memento;
constructor(
private readonly _globalState: vscode.Memento,
private readonly _workspaceState: vscode.Memento
) { }
globalState: vscode.Memento,
workspaceState: vscode.Memento
) {
this.#globalState = globalState;
this.#workspaceState = workspaceState;
}
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);
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)) {
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);
return this.#globalState.update(this.#security_level_key + this.#getRoot(resource), level);
}
public shouldAllowSvgsForResource(resource: vscode.Uri) {
@@ -60,14 +66,14 @@ export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPol
}
public shouldDisableSecurityWarnings(): boolean {
return this._workspaceState.get<boolean>(this._should_disable_security_warning_key, false);
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);
return this.#workspaceState.update(this.#should_disable_security_warning_key, disabled);
}
private _getRoot(resource: vscode.Uri): vscode.Uri {
#getRoot(resource: vscode.Uri): vscode.Uri {
if (vscode.workspace.workspaceFolders) {
const folderForResource = vscode.workspace.getWorkspaceFolder(resource);
if (folderForResource) {
@@ -85,10 +91,16 @@ export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPol
export class PreviewSecuritySelector {
readonly #cspArbiter: ContentSecurityPolicyArbiter;
readonly #webviewManager: MarkdownPreviewManager;
public constructor(
private readonly _cspArbiter: ContentSecurityPolicyArbiter,
private readonly _webviewManager: MarkdownPreviewManager
) { }
cspArbiter: ContentSecurityPolicyArbiter,
webviewManager: MarkdownPreviewManager
) {
this.#cspArbiter = cspArbiter;
this.#webviewManager = webviewManager;
}
public async showSecuritySelectorForResource(resource: vscode.Uri): Promise<void> {
interface PreviewSecurityPickItem extends vscode.QuickPickItem {
@@ -99,7 +111,7 @@ export class PreviewSecuritySelector {
return when ? '• ' : '';
}
const currentSecurityLevel = this._cspArbiter.getSecurityLevelForResource(resource);
const currentSecurityLevel = this.#cspArbiter.getSecurityLevelForResource(resource);
const selection = await vscode.window.showQuickPick<PreviewSecurityPickItem>(
[
{
@@ -124,7 +136,7 @@ export class PreviewSecuritySelector {
description: ''
}, {
type: 'toggle',
label: this._cspArbiter.shouldDisableSecurityWarnings()
label: this.#cspArbiter.shouldDisableSecurityWarnings()
? vscode.l10n.t("Enable preview security warnings in this workspace")
: vscode.l10n.t("Disable preview security warning in this workspace"),
description: vscode.l10n.t("Does not affect the content security level")
@@ -142,12 +154,12 @@ export class PreviewSecuritySelector {
}
if (selection.type === 'toggle') {
this._cspArbiter.setShouldDisableSecurityWarning(!this._cspArbiter.shouldDisableSecurityWarnings());
this._webviewManager.refresh();
this.#cspArbiter.setShouldDisableSecurityWarning(!this.#cspArbiter.shouldDisableSecurityWarnings());
this.#webviewManager.refresh();
return;
} else {
await this._cspArbiter.setSecurityLevelForResource(resource, selection.type);
await this.#cspArbiter.setSecurityLevelForResource(resource, selection.type);
}
this._webviewManager.refresh();
this.#webviewManager.refresh();
}
}

View File

@@ -15,10 +15,10 @@ export interface LastScrollLocation {
export class TopmostLineMonitor extends Disposable {
private readonly _pendingUpdates = new ResourceMap<number>();
private readonly _throttle = 50;
private readonly _previousTextEditorInfo = new ResourceMap<LastScrollLocation>();
private readonly _previousStaticEditorInfo = new ResourceMap<LastScrollLocation>();
readonly #pendingUpdates = new ResourceMap<number>();
readonly #throttle = 50;
readonly #previousTextEditorInfo = new ResourceMap<LastScrollLocation>();
readonly #previousStaticEditorInfo = new ResourceMap<LastScrollLocation>();
constructor() {
super();
@@ -39,32 +39,32 @@ export class TopmostLineMonitor extends Disposable {
}));
}
private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri; readonly line: number }>());
public readonly onDidChanged = this._onChanged.event;
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, scrollLocation);
this.#previousStaticEditorInfo.set(scrollLocation.uri, scrollLocation);
}
public getPreviousStaticEditorLineByUri(resource: vscode.Uri): number | undefined {
const scrollLoc = this._previousStaticEditorInfo.get(resource);
this._previousStaticEditorInfo.delete(resource);
const scrollLoc = this.#previousStaticEditorInfo.get(resource);
this.#previousStaticEditorInfo.delete(resource);
return scrollLoc?.line;
}
public setPreviousTextEditorLine(scrollLocation: LastScrollLocation): void {
this._previousTextEditorInfo.set(scrollLocation.uri, scrollLocation);
this.#previousTextEditorInfo.set(scrollLocation.uri, scrollLocation);
}
public getPreviousTextEditorLineByUri(resource: vscode.Uri): number | undefined {
const scrollLoc = this._previousTextEditorInfo.get(resource);
this._previousTextEditorInfo.delete(resource);
const scrollLoc = this.#previousTextEditorInfo.get(resource);
this.#previousTextEditorInfo.delete(resource);
return scrollLoc?.line;
}
public getPreviousStaticTextEditorLineByUri(resource: vscode.Uri): number | undefined {
const state = this._previousStaticEditorInfo.get(resource);
const state = this.#previousStaticEditorInfo.get(resource);
return state?.line;
}
@@ -72,20 +72,20 @@ export class TopmostLineMonitor extends Disposable {
resource: vscode.Uri,
line: number
) {
if (!this._pendingUpdates.has(resource)) {
if (!this.#pendingUpdates.has(resource)) {
// schedule update
setTimeout(() => {
if (this._pendingUpdates.has(resource)) {
this._onChanged.fire({
if (this.#pendingUpdates.has(resource)) {
this.#onChanged.fire({
resource,
line: this._pendingUpdates.get(resource) as number
line: this.#pendingUpdates.get(resource) as number
});
this._pendingUpdates.delete(resource);
this.#pendingUpdates.delete(resource);
}
}, this._throttle);
}, this.#throttle);
}
this._pendingUpdates.set(resource, line);
this.#pendingUpdates.set(resource, line);
}
}