Rework markdown preview code to better support markdown preview editors

Splits the preview part of the markdown preview from the dynamic preview management part of things. Static preview swap to preview the active markdown file and don't scroll sync with any other markdown files
This commit is contained in:
Matt Bierner 2020-04-10 22:47:40 -07:00
parent 49bcd96469
commit 9cfd597153
6 changed files with 519 additions and 321 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -18,8 +18,8 @@ const settings = getSettings();
const vscode = acquireVsCodeApi(); const vscode = acquireVsCodeApi();
// Set VS Code state const state = { ...vscode.getState(), ...getData<any>('data-state') };
let state = getData<{ line: number; fragment: string; }>('data-state'); // Make sure to sync VS Code state here
vscode.setState(state); vscode.setState(state);
const messaging = createPosterForVsCode(vscode); const messaging = createPosterForVsCode(vscode);
@ -32,23 +32,35 @@ window.onload = () => {
}; };
onceDocumentLoaded(() => { onceDocumentLoaded(() => {
const scrollProgress = state.scrollProgress;
if (typeof scrollProgress === 'number' && !settings.fragment) {
setImmediate(() => {
scrollDisabled = true;
window.scrollTo(0, scrollProgress * document.body.clientHeight);
});
return;
}
if (settings.scrollPreviewWithEditor) { if (settings.scrollPreviewWithEditor) {
setTimeout(() => { setImmediate(() => {
// Try to scroll to fragment if available // Try to scroll to fragment if available
if (state.fragment) { if (settings.fragment) {
const element = getLineElementForFragment(state.fragment); state.fragment = undefined;
vscode.setState(state);
const element = getLineElementForFragment(settings.fragment);
if (element) { if (element) {
scrollDisabled = true; scrollDisabled = true;
scrollToRevealSourceLine(element.line); scrollToRevealSourceLine(element.line);
} }
} else { } else {
const initialLine = +settings.line; if (!isNaN(settings.line!)) {
if (!isNaN(initialLine)) {
scrollDisabled = true; scrollDisabled = true;
scrollToRevealSourceLine(initialLine); scrollToRevealSourceLine(settings.line!);
} }
} }
}, 0); });
} }
}); });
@ -58,9 +70,10 @@ const onUpdateView = (() => {
scrollToRevealSourceLine(line); scrollToRevealSourceLine(line);
}, 50); }, 50);
return (line: number, settings: any) => { return (line: number) => {
if (!isNaN(line)) { if (!isNaN(line)) {
settings.line = line; state.line = line;
doScroll(line); doScroll(line);
} }
}; };
@ -91,6 +104,7 @@ let updateImageSizes = throttle(() => {
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
scrollDisabled = true; scrollDisabled = true;
updateScrollProgress();
updateImageSizes(); updateImageSizes();
}, true); }, true);
@ -105,7 +119,7 @@ window.addEventListener('message', event => {
break; break;
case 'updateView': case 'updateView':
onUpdateView(event.data.line, settings); onUpdateView(event.data.line);
break; break;
} }
}, false); }, false);
@ -165,15 +179,20 @@ document.addEventListener('click', event => {
}, true); }, true);
window.addEventListener('scroll', throttle(() => { window.addEventListener('scroll', throttle(() => {
updateScrollProgress();
if (scrollDisabled) { if (scrollDisabled) {
scrollDisabled = false; scrollDisabled = false;
} else { } else {
const line = getEditorLineNumberForPageOffset(window.scrollY); const line = getEditorLineNumberForPageOffset(window.scrollY);
if (typeof line === 'number' && !isNaN(line)) { if (typeof line === 'number' && !isNaN(line)) {
messaging.postMessage('revealLine', { line }); messaging.postMessage('revealLine', { line });
state.line = line;
vscode.setState(state);
} }
} }
}, 50)); }, 50));
function updateScrollProgress() {
state.scrollProgress = window.scrollY / document.body.clientHeight;
vscode.setState(state);
}

View file

@ -5,7 +5,8 @@
export interface PreviewSettings { export interface PreviewSettings {
readonly source: string; readonly source: string;
readonly line: number; readonly line?: number;
readonly fragment?: string
readonly lineCount: number; readonly lineCount: number;
readonly scrollPreviewWithEditor?: boolean; readonly scrollPreviewWithEditor?: boolean;
readonly scrollEditorWithPreview: boolean; readonly scrollEditorWithPreview: boolean;

View file

@ -13,7 +13,7 @@ import { Disposable } from '../util/dispose';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor'; import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor';
import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContributionProvider, MarkdownContributions } from '../markdownExtensions'; import { MarkdownContributionProvider } from '../markdownExtensions';
import { isMarkdownFile } from '../util/file'; import { isMarkdownFile } from '../util/file';
import { resolveLinkToMarkdownFile } from '../commands/openDocumentLink'; import { resolveLinkToMarkdownFile } from '../commands/openDocumentLink';
import { WebviewResourceProvider, normalizeResource } from '../util/resources'; import { WebviewResourceProvider, normalizeResource } from '../util/resources';
@ -61,10 +61,14 @@ interface PreviewStyleLoadErrorMessage extends WebviewMessage {
} }
export class PreviewDocumentVersion { export class PreviewDocumentVersion {
public constructor(
public readonly resource: vscode.Uri, private readonly resource: vscode.Uri;
public readonly version: number, private readonly version: number;
) { }
public constructor(document: vscode.TextDocument) {
this.resource = document.uri;
this.version = document.version;
}
public equals(other: PreviewDocumentVersion): boolean { public equals(other: PreviewDocumentVersion): boolean {
return this.resource.fsPath === other.resource.fsPath return this.resource.fsPath === other.resource.fsPath
@ -72,102 +76,86 @@ export class PreviewDocumentVersion {
} }
} }
interface DynamicPreviewInput { interface MarkdownPreviewDelegate {
readonly resource: vscode.Uri; getTitle?(resource: vscode.Uri): string;
readonly resourceColumn: vscode.ViewColumn; getAdditionalState(): {},
readonly locked: boolean; openPreviewLinkToMarkdownFile(markdownLink: vscode.Uri, fragment: string): void;
readonly line?: number;
} }
export class DynamicMarkdownPreview extends Disposable { class StartingScrollLine {
public readonly type = 'line';
public static readonly viewType = 'markdown.preview'; constructor(
public readonly line: number,
) { }
}
class StartingScrollFragment {
public readonly type = 'fragment';
constructor(
public readonly fragment: string,
) { }
}
type StartingScrollLocation = StartingScrollLine | StartingScrollFragment;
class MarkdownPreview extends Disposable implements WebviewResourceProvider {
private readonly delay = 300; private readonly delay = 300;
private _resource: vscode.Uri; private readonly _resource: vscode.Uri;
private readonly _resourceColumn: vscode.ViewColumn; private readonly _webviewPanel: vscode.WebviewPanel;
private _locked: boolean;
private readonly editor: vscode.WebviewPanel;
private throttleTimer: any; private throttleTimer: any;
private line: number | undefined = undefined;
private line: number | undefined;
private scrollToFragment: string | undefined;
private firstUpdate = true; private firstUpdate = true;
private currentVersion?: PreviewDocumentVersion; private currentVersion?: PreviewDocumentVersion;
private isScrolling = false; private isScrolling = false;
private _disposed: boolean = false; private _disposed: boolean = false;
private imageInfo: { id: string, width: number, height: number; }[] = []; private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = [];
private scrollToFragment: string | undefined;
public static revive( constructor(
input: DynamicPreviewInput,
webview: vscode.WebviewPanel, webview: vscode.WebviewPanel,
contentProvider: MarkdownContentProvider, resource: vscode.Uri,
previewConfigurations: MarkdownPreviewConfigurationManager, startingScroll: StartingScrollLocation | undefined,
logger: Logger, private readonly delegate: MarkdownPreviewDelegate,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider,
): DynamicMarkdownPreview {
webview.webview.options = DynamicMarkdownPreview.getWebviewOptions(input.resource, contributionProvider.contributions);
webview.title = DynamicMarkdownPreview.getPreviewTitle(input.resource, input.locked);
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider);
}
public static create(
input: DynamicPreviewInput,
previewColumn: vscode.ViewColumn,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider
): DynamicMarkdownPreview {
const webview = vscode.window.createWebviewPanel(
DynamicMarkdownPreview.viewType,
DynamicMarkdownPreview.getPreviewTitle(input.resource, input.locked),
previewColumn, {
enableFindWidget: true,
...DynamicMarkdownPreview.getWebviewOptions(input.resource, contributionProvider.contributions)
});
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider);
}
private constructor(
webview: vscode.WebviewPanel,
input: DynamicPreviewInput,
private readonly _contentProvider: MarkdownContentProvider, private readonly _contentProvider: MarkdownContentProvider,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager, private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _logger: Logger, private readonly _logger: Logger,
topmostLineMonitor: TopmostLineMonitor,
private readonly _contributionProvider: MarkdownContributionProvider, private readonly _contributionProvider: MarkdownContributionProvider,
) { ) {
super(); super();
this._resource = input.resource;
this._resourceColumn = input.resourceColumn; this._webviewPanel = webview;
this._locked = input.locked; this._resource = resource;
this.editor = webview;
if (!isNaN(input.line!)) { switch (startingScroll?.type) {
this.line = input.line; case 'line':
if (!isNaN(startingScroll.line!)) {
this.line = startingScroll.line;
}
break;
case 'fragment':
this.scrollToFragment = startingScroll.fragment;
break;
} }
this._register(this.editor.onDidDispose(() => {
this.dispose();
}));
this._register(this.editor.onDidChangeViewState(e => {
this._onDidChangeViewStateEmitter.fire(e);
}));
this._register(_contributionProvider.onContributionsChanged(() => { this._register(_contributionProvider.onContributionsChanged(() => {
setImmediate(() => this.refresh()); setImmediate(() => this.refresh());
})); }));
this._register(this.editor.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => { this._register(vscode.workspace.onDidChangeTextDocument(event => {
if (this.isPreviewOf(event.document.uri)) {
this.refresh();
}
}));
this._register(this._webviewPanel.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => {
if (e.source !== this._resource.toString()) { if (e.source !== this._resource.toString()) {
return; return;
} }
@ -194,158 +182,50 @@ export class DynamicMarkdownPreview extends Disposable {
break; break;
case 'previewStyleLoadError': case 'previewStyleLoadError':
vscode.window.showWarningMessage(localize('onPreviewStyleLoadError', "Could not load 'markdown.styles': {0}", e.body.unloadedStyles.join(', '))); vscode.window.showWarningMessage(
localize('onPreviewStyleLoadError',
"Could not load 'markdown.styles': {0}",
e.body.unloadedStyles.join(', ')));
break; break;
} }
})); }));
this._register(vscode.workspace.onDidChangeTextDocument(event => { this.updatePreview();
if (this.isPreviewOf(event.document.uri)) {
this.refresh();
}
}));
this._register(topmostLineMonitor.onDidChanged(event => {
if (this.isPreviewOf(event.resource)) {
this.updateForView(event.resource, event.line);
}
}));
this._register(vscode.window.onDidChangeTextEditorSelection(event => {
if (this.isPreviewOf(event.textEditor.document.uri)) {
this.postMessage({
type: 'onDidChangeTextEditorSelection',
line: event.selections[0].active.line,
source: this.resource.toString()
});
}
}));
this._register(vscode.window.onDidChangeActiveTextEditor(editor => {
if (editor && isMarkdownFile(editor.document) && !this._locked) {
this.update(editor.document.uri, false);
}
}));
this.doUpdate();
} }
private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter<void>()); dispose() {
public readonly onDispose = this._onDisposeEmitter.event; super.dispose();
this._disposed = true;
private readonly _onDidChangeViewStateEmitter = this._register(new vscode.EventEmitter<vscode.WebviewPanelOnDidChangeViewStateEvent>()); clearTimeout(this.throttleTimer);
public readonly onDidChangeViewState = this._onDidChangeViewStateEmitter.event; }
public get resource(): vscode.Uri { public get resource(): vscode.Uri {
return this._resource; return this._resource;
} }
public get resourceColumn(): vscode.ViewColumn {
return this._resourceColumn;
}
public get state() { public get state() {
return { return {
resource: this.resource.toString(), resource: this._resource.toString(),
locked: this._locked,
line: this.line, line: this.line,
resourceColumn: this.resourceColumn,
imageInfo: this.imageInfo, imageInfo: this.imageInfo,
fragment: this.scrollToFragment fragment: this.scrollToFragment,
...this.delegate.getAdditionalState(),
}; };
} }
public dispose() { public refresh() {
if (this._disposed) {
return;
}
this._disposed = true;
this._onDisposeEmitter.fire();
this._onDisposeEmitter.dispose();
this.editor.dispose();
super.dispose();
}
public update(resource: vscode.Uri, isRefresh = true) {
// Reposition scroll preview, position scroll to the top if active text editor
// doesn't corresponds with preview
const editor = vscode.window.activeTextEditor;
if (editor) {
if (!isRefresh || this._previewConfigurations.loadAndCacheConfiguration(this._resource).scrollEditorWithPreview) {
if (editor.document.uri.fsPath === resource.fsPath) {
this.line = getVisibleLine(editor);
} else {
this.line = 0;
}
}
}
// If we have changed resources, cancel any pending updates
const isResourceChange = resource.fsPath !== this._resource.fsPath;
if (isResourceChange) {
clearTimeout(this.throttleTimer);
this.throttleTimer = undefined;
}
this._resource = resource;
// Schedule update if none is pending // Schedule update if none is pending
if (!this.throttleTimer) { if (!this.throttleTimer) {
if (isResourceChange || this.firstUpdate) { if (this.firstUpdate) {
this.doUpdate(isRefresh); this.updatePreview(true);
} else { } else {
this.throttleTimer = setTimeout(() => this.doUpdate(isRefresh), this.delay); this.throttleTimer = setTimeout(() => this.updatePreview(true), this.delay);
} }
} }
this.firstUpdate = false; this.firstUpdate = false;
} }
public refresh() {
this.update(this._resource, true);
}
public updateConfiguration() {
if (this._previewConfigurations.hasConfigurationChanged(this._resource)) {
this.refresh();
}
}
public get position(): vscode.ViewColumn | undefined {
return this.editor.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.isPreviewOf(otherResource);
} else {
return !otherLocked;
}
}
public matches(otherPreview: DynamicMarkdownPreview): boolean {
return this.matchesResource(otherPreview._resource, otherPreview.position, otherPreview._locked);
}
public reveal(viewColumn: vscode.ViewColumn) {
this.editor.reveal(viewColumn);
}
public toggleLock() {
this._locked = !this._locked;
this.editor.title = DynamicMarkdownPreview.getPreviewTitle(this._resource, this._locked);
}
private get iconPath() { private get iconPath() {
const root = path.join(this._contributionProvider.extensionPath, 'media'); const root = path.join(this._contributionProvider.extensionPath, 'media');
return { return {
@ -354,18 +234,18 @@ export class DynamicMarkdownPreview extends Disposable {
}; };
} }
private isPreviewOf(resource: vscode.Uri): boolean { public isPreviewOf(resource: vscode.Uri): boolean {
return this._resource.fsPath === resource.fsPath; return this._resource.fsPath === resource.fsPath;
} }
private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string { public postMessage(msg: any) {
return locked if (!this._disposed) {
? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath)) this._webviewPanel.webview.postMessage(msg);
: localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)); }
} }
private updateForView(resource: vscode.Uri, topLine: number | undefined) { public scrollTo(topLine: number) {
if (!this.isPreviewOf(resource)) { if (this._disposed) {
return; return;
} }
@ -374,36 +254,26 @@ export class DynamicMarkdownPreview extends Disposable {
return; return;
} }
if (typeof topLine === 'number') { this._logger.log('updateForView', { markdownFile: this._resource });
this._logger.log('updateForView', { markdownFile: resource }); this.line = topLine;
this.line = topLine; this.postMessage({
this.postMessage({ type: 'updateView',
type: 'updateView', line: topLine,
line: topLine, source: this._resource.toString()
source: resource.toString() });
});
}
} }
private postMessage(msg: any) { private async updatePreview(forceUpdate?: boolean): Promise<void> {
if (!this._disposed) { clearTimeout(this.throttleTimer);
this.editor.webview.postMessage(msg); this.throttleTimer = undefined;
}
}
private async doUpdate(forceUpdate?: boolean): Promise<void> {
if (this._disposed) { if (this._disposed) {
return; return;
} }
const markdownResource = this._resource;
clearTimeout(this.throttleTimer);
this.throttleTimer = undefined;
let document: vscode.TextDocument; let document: vscode.TextDocument;
try { try {
document = await vscode.workspace.openTextDocument(markdownResource); document = await vscode.workspace.openTextDocument(this._resource);
} catch { } catch {
await this.showFileNotFoundError(); await this.showFileNotFoundError();
return; return;
@ -413,61 +283,24 @@ export class DynamicMarkdownPreview extends Disposable {
return; return;
} }
const pendingVersion = new PreviewDocumentVersion(markdownResource, document.version); const pendingVersion = new PreviewDocumentVersion(document);
if (!forceUpdate && this.currentVersion?.equals(pendingVersion)) { if (!forceUpdate && this.currentVersion?.equals(pendingVersion)) {
if (this.line) { if (this.line) {
this.updateForView(markdownResource, this.line); this.scrollTo(this.line);
} }
return; return;
} }
this.currentVersion = pendingVersion; this.currentVersion = pendingVersion;
if (this._resource === markdownResource) { const content = await this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state);
const self = this;
const resourceProvider: WebviewResourceProvider = { // Another call to `doUpdate` may have happened.
asWebviewUri: (resource) => { // Make sure we are still updating for the correct document
return this.editor.webview.asWebviewUri(normalizeResource(markdownResource, resource)); if (this.currentVersion?.equals(pendingVersion)) {
}, this.setContent(content);
get cspSource() { return self.editor.webview.cspSource; }
};
const content = await this._contentProvider.provideTextDocumentContent(document, resourceProvider, this._previewConfigurations, this.line, this.state);
// Another call to `doUpdate` may have happened.
// Make sure we are still updating for the correct document
if (this.currentVersion && this.currentVersion.equals(pendingVersion)) {
this.setContent(content);
}
} }
} }
private static getWebviewOptions(
resource: vscode.Uri,
contributions: MarkdownContributions
): vscode.WebviewOptions {
return {
enableScripts: true,
localResourceRoots: DynamicMarkdownPreview.getLocalResourceRoots(resource, contributions)
};
}
private static getLocalResourceRoots(
base: vscode.Uri,
contributions: MarkdownContributions
): ReadonlyArray<vscode.Uri> {
const baseRoots = Array.from(contributions.previewResourceRoots);
const folder = vscode.workspace.getWorkspaceFolder(base);
if (folder) {
const workspaceRoots = vscode.workspace.workspaceFolders?.map(folder => folder.uri);
if (workspaceRoots) {
baseRoots.push(...workspaceRoots);
}
} else if (!base.scheme || base.scheme === 'file') {
baseRoots.push(vscode.Uri.file(path.dirname(base.fsPath)));
}
return baseRoots.map(root => normalizeResource(base, root));
}
private onDidScrollPreview(line: number) { private onDidScrollPreview(line: number) {
this.line = line; this.line = line;
@ -513,16 +346,47 @@ export class DynamicMarkdownPreview extends Disposable {
} }
private async showFileNotFoundError() { private async showFileNotFoundError() {
this.setContent(this._contentProvider.provideFileNotFoundContent(this._resource)); this._webviewPanel.webview.html = this._contentProvider.provideFileNotFoundContent(this._resource);
} }
private setContent(html: string): void { private setContent(html: string): void {
this.editor.title = DynamicMarkdownPreview.getPreviewTitle(this._resource, this._locked); if (this._disposed) {
this.editor.iconPath = this.iconPath; return;
this.editor.webview.options = DynamicMarkdownPreview.getWebviewOptions(this._resource, this._contributionProvider.contributions); }
this.editor.webview.html = html;
if (this.delegate.getTitle) {
this._webviewPanel.title = this.delegate.getTitle(this._resource);
}
this._webviewPanel.iconPath = this.iconPath;
this._webviewPanel.webview.options = this.getWebviewOptions();
this._webviewPanel.webview.html = html;
} }
private getWebviewOptions(): vscode.WebviewOptions {
return {
enableScripts: true,
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 if (!this._resource.scheme || this._resource.scheme === 'file') {
baseRoots.push(vscode.Uri.file(path.dirname(this._resource.fsPath)));
}
return baseRoots.map(root => normalizeResource(this._resource, root));
}
private async onDidClickPreviewLink(href: string) { private async onDidClickPreviewLink(href: string) {
let [hrefPath, fragment] = decodeURIComponent(href).split('#'); let [hrefPath, fragment] = decodeURIComponent(href).split('#');
@ -537,14 +401,332 @@ export class DynamicMarkdownPreview extends Disposable {
if (openLinks === 'inPreview') { if (openLinks === 'inPreview') {
const markdownLink = await resolveLinkToMarkdownFile(hrefPath); const markdownLink = await resolveLinkToMarkdownFile(hrefPath);
if (markdownLink) { if (markdownLink) {
if (fragment) { this.delegate.openPreviewLinkToMarkdownFile(markdownLink, fragment);
this.scrollToFragment = fragment;
}
this.update(markdownLink);
return; return;
} }
} }
vscode.commands.executeCommand('_markdown.openDocumentLink', { path: hrefPath, fragment, fromResource: this.resource }); vscode.commands.executeCommand('_markdown.openDocumentLink', { path: hrefPath, fragment, fromResource: this.resource });
} }
//#region WebviewResourceProvider
asWebviewUri(resource: vscode.Uri) {
return this._webviewPanel.webview.asWebviewUri(normalizeResource(this._resource, 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 revive(
resource: vscode.Uri,
webview: vscode.WebviewPanel,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
contributionProvider: MarkdownContributionProvider,
): StaticMarkdownPreview {
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider);
}
private readonly preview: MarkdownPreview;
private constructor(
private readonly _webviewPanel: vscode.WebviewPanel,
resource: vscode.Uri,
contentProvider: MarkdownContentProvider,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
contributionProvider: MarkdownContributionProvider,
) {
super();
this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, undefined, {
getAdditionalState: () => { return {}; },
openPreviewLinkToMarkdownFile: () => { /* todo */ }
}, contentProvider, _previewConfigurations, logger, contributionProvider));
this._register(this._webviewPanel.onDidDispose(() => {
this.dispose();
}));
this._register(this._webviewPanel.onDidChangeViewState(e => {
this._onDidChangeViewState.fire(e);
}));
}
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;
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();
}
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;
}
/**
* A
*/
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,
): DynamicMarkdownPreview {
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider);
}
public static create(
input: DynamicPreviewInput,
previewColumn: vscode.ViewColumn,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider
): DynamicMarkdownPreview {
const webview = vscode.window.createWebviewPanel(
DynamicMarkdownPreview.viewType,
DynamicMarkdownPreview.getPreviewTitle(input.resource, input.locked),
previewColumn, { enableFindWidget: true, });
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider);
}
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,
) {
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 => {
if (editor && 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;
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();
}
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 {
return locked
? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath))
: localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath));
}
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._contentProvider,
this._previewConfigurations,
this._logger,
this._contributionProvider);
}
}

View file

@ -8,7 +8,7 @@ import { Logger } from '../logger';
import { MarkdownContributionProvider } from '../markdownExtensions'; import { MarkdownContributionProvider } from '../markdownExtensions';
import { disposeAll, Disposable } from '../util/dispose'; import { disposeAll, Disposable } from '../util/dispose';
import { TopmostLineMonitor } from '../util/topmostLineMonitor'; import { TopmostLineMonitor } from '../util/topmostLineMonitor';
import { DynamicMarkdownPreview } from './preview'; import { DynamicMarkdownPreview, StaticMarkdownPreview, ManagedMarkdownPreview } from './preview';
import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContentProvider } from './previewContentProvider'; import { MarkdownContentProvider } from './previewContentProvider';
@ -18,9 +18,9 @@ export interface DynamicPreviewSettings {
readonly locked: boolean; readonly locked: boolean;
} }
class PreviewStore extends Disposable { class PreviewStore<T extends ManagedMarkdownPreview> extends Disposable {
private readonly _previews = new Set<DynamicMarkdownPreview>(); private readonly _previews = new Set<T>();
public dispose(): void { public dispose(): void {
super.dispose(); super.dispose();
@ -30,11 +30,11 @@ class PreviewStore extends Disposable {
this._previews.clear(); this._previews.clear();
} }
[Symbol.iterator](): Iterator<DynamicMarkdownPreview> { [Symbol.iterator](): Iterator<T> {
return this._previews[Symbol.iterator](); return this._previews[Symbol.iterator]();
} }
public get(resource: vscode.Uri, previewSettings: DynamicPreviewSettings): DynamicMarkdownPreview | undefined { public get(resource: vscode.Uri, previewSettings: DynamicPreviewSettings): T | undefined {
for (const preview of this._previews) { for (const preview of this._previews) {
if (preview.matchesResource(resource, previewSettings.previewColumn, previewSettings.locked)) { if (preview.matchesResource(resource, previewSettings.previewColumn, previewSettings.locked)) {
return preview; return preview;
@ -43,11 +43,11 @@ class PreviewStore extends Disposable {
return undefined; return undefined;
} }
public add(preview: DynamicMarkdownPreview) { public add(preview: T) {
this._previews.add(preview); this._previews.add(preview);
} }
public delete(preview: DynamicMarkdownPreview) { public delete(preview: T) {
this._previews.delete(preview); this._previews.delete(preview);
} }
} }
@ -58,10 +58,10 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
private readonly _topmostLineMonitor = new TopmostLineMonitor(); private readonly _topmostLineMonitor = new TopmostLineMonitor();
private readonly _previewConfigurations = new MarkdownPreviewConfigurationManager(); private readonly _previewConfigurations = new MarkdownPreviewConfigurationManager();
private readonly _dynamicPreviews = this._register(new PreviewStore()); private readonly _dynamicPreviews = this._register(new PreviewStore<DynamicMarkdownPreview>());
private readonly _staticPreviews = this._register(new PreviewStore()); private readonly _staticPreviews = this._register(new PreviewStore<StaticMarkdownPreview>());
private _activePreview: DynamicMarkdownPreview | undefined = undefined; private _activePreview: ManagedMarkdownPreview | undefined = undefined;
private readonly customEditorViewType = 'vscode.markdown.preview.editor'; private readonly customEditorViewType = 'vscode.markdown.preview.editor';
@ -117,7 +117,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
public toggleLock() { public toggleLock() {
const preview = this._activePreview; const preview = this._activePreview;
if (preview) { if (preview instanceof DynamicMarkdownPreview) {
preview.toggleLock(); preview.toggleLock();
// Close any previews that are now redundant, such as having two dynamic previews in the same editor group // Close any previews that are now redundant, such as having two dynamic previews in the same editor group
@ -133,6 +133,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
webview: vscode.WebviewPanel, webview: vscode.WebviewPanel,
state: any state: any
): Promise<void> { ): Promise<void> {
console.log(state);
const resource = vscode.Uri.parse(state.resource); const resource = vscode.Uri.parse(state.resource);
const locked = state.locked; const locked = state.locked;
const line = state.line; const line = state.line;
@ -150,21 +151,16 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this.registerDynamicPreview(preview); this.registerDynamicPreview(preview);
} }
public async openCustomDocument(uri: vscode.Uri) {
return { uri, dispose: () => { } };
}
public async resolveCustomTextEditor( public async resolveCustomTextEditor(
document: vscode.TextDocument, document: vscode.TextDocument,
webview: vscode.WebviewPanel webview: vscode.WebviewPanel
): Promise<void> { ): Promise<void> {
const preview = DynamicMarkdownPreview.revive( const preview = StaticMarkdownPreview.revive(
{ resource: document.uri, locked: false, resourceColumn: vscode.ViewColumn.One }, document.uri,
webview, webview,
this._contentProvider, this._contentProvider,
this._previewConfigurations, this._previewConfigurations,
this._logger, this._logger,
this._topmostLineMonitor,
this._contributions); this._contributions);
this.registerStaticPreview(preview); this.registerStaticPreview(preview);
} }
@ -207,7 +203,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
return preview; return preview;
} }
private registerStaticPreview(preview: DynamicMarkdownPreview): DynamicMarkdownPreview { private registerStaticPreview(preview: StaticMarkdownPreview): StaticMarkdownPreview {
this._staticPreviews.add(preview); this._staticPreviews.add(preview);
preview.onDispose(() => { preview.onDispose(() => {
@ -218,7 +214,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
return preview; return preview;
} }
private trackActive(preview: DynamicMarkdownPreview): void { private trackActive(preview: ManagedMarkdownPreview): void {
preview.onDidChangeViewState(({ webviewPanel }) => { preview.onDidChangeViewState(({ webviewPanel }) => {
this.setPreviewActiveContext(webviewPanel.active); this.setPreviewActiveContext(webviewPanel.active);
this._activePreview = webviewPanel.active ? preview : undefined; this._activePreview = webviewPanel.active ? preview : undefined;