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

View file

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

View file

@ -13,7 +13,7 @@ import { Disposable } from '../util/dispose';
import * as nls from 'vscode-nls';
import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContributionProvider, MarkdownContributions } from '../markdownExtensions';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { isMarkdownFile } from '../util/file';
import { resolveLinkToMarkdownFile } from '../commands/openDocumentLink';
import { WebviewResourceProvider, normalizeResource } from '../util/resources';
@ -61,10 +61,14 @@ interface PreviewStyleLoadErrorMessage extends WebviewMessage {
}
export class PreviewDocumentVersion {
public constructor(
public readonly resource: vscode.Uri,
public readonly version: number,
) { }
private readonly resource: vscode.Uri;
private readonly version: number;
public constructor(document: vscode.TextDocument) {
this.resource = document.uri;
this.version = document.version;
}
public equals(other: PreviewDocumentVersion): boolean {
return this.resource.fsPath === other.resource.fsPath
@ -72,102 +76,86 @@ export class PreviewDocumentVersion {
}
}
interface DynamicPreviewInput {
readonly resource: vscode.Uri;
readonly resourceColumn: vscode.ViewColumn;
readonly locked: boolean;
readonly line?: number;
interface MarkdownPreviewDelegate {
getTitle?(resource: vscode.Uri): string;
getAdditionalState(): {},
openPreviewLinkToMarkdownFile(markdownLink: vscode.Uri, fragment: string): void;
}
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 _resource: vscode.Uri;
private readonly _resourceColumn: vscode.ViewColumn;
private readonly _resource: vscode.Uri;
private readonly _webviewPanel: vscode.WebviewPanel;
private _locked: boolean;
private readonly editor: vscode.WebviewPanel;
private throttleTimer: any;
private line: number | undefined = undefined;
private line: number | undefined;
private scrollToFragment: string | undefined;
private firstUpdate = true;
private currentVersion?: PreviewDocumentVersion;
private isScrolling = false;
private _disposed: boolean = false;
private imageInfo: { id: string, width: number, height: number; }[] = [];
private scrollToFragment: string | undefined;
private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = [];
public static revive(
input: DynamicPreviewInput,
constructor(
webview: vscode.WebviewPanel,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
logger: Logger,
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,
resource: vscode.Uri,
startingScroll: StartingScrollLocation | undefined,
private readonly delegate: MarkdownPreviewDelegate,
private readonly _contentProvider: MarkdownContentProvider,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _logger: Logger,
topmostLineMonitor: TopmostLineMonitor,
private readonly _contributionProvider: MarkdownContributionProvider,
) {
super();
this._resource = input.resource;
this._resourceColumn = input.resourceColumn;
this._locked = input.locked;
this.editor = webview;
if (!isNaN(input.line!)) {
this.line = input.line;
this._webviewPanel = webview;
this._resource = resource;
switch (startingScroll?.type) {
case 'line':
if (!isNaN(startingScroll.line!)) {
this.line = startingScroll.line;
}
break;
case 'fragment':
this.scrollToFragment = startingScroll.fragment;
break;
}
this._register(this.editor.onDidDispose(() => {
this.dispose();
}));
this._register(this.editor.onDidChangeViewState(e => {
this._onDidChangeViewStateEmitter.fire(e);
}));
this._register(_contributionProvider.onContributionsChanged(() => {
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()) {
return;
}
@ -194,158 +182,50 @@ export class DynamicMarkdownPreview extends Disposable {
break;
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;
}
}));
this._register(vscode.workspace.onDidChangeTextDocument(event => {
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();
this.updatePreview();
}
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() {
super.dispose();
this._disposed = true;
clearTimeout(this.throttleTimer);
}
public get resource(): vscode.Uri {
return this._resource;
}
public get resourceColumn(): vscode.ViewColumn {
return this._resourceColumn;
}
public get state() {
return {
resource: this.resource.toString(),
locked: this._locked,
resource: this._resource.toString(),
line: this.line,
resourceColumn: this.resourceColumn,
imageInfo: this.imageInfo,
fragment: this.scrollToFragment
fragment: this.scrollToFragment,
...this.delegate.getAdditionalState(),
};
}
public dispose() {
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;
public refresh() {
// Schedule update if none is pending
if (!this.throttleTimer) {
if (isResourceChange || this.firstUpdate) {
this.doUpdate(isRefresh);
if (this.firstUpdate) {
this.updatePreview(true);
} else {
this.throttleTimer = setTimeout(() => this.doUpdate(isRefresh), this.delay);
this.throttleTimer = setTimeout(() => this.updatePreview(true), this.delay);
}
}
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() {
const root = path.join(this._contributionProvider.extensionPath, 'media');
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;
}
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 postMessage(msg: any) {
if (!this._disposed) {
this._webviewPanel.webview.postMessage(msg);
}
}
private updateForView(resource: vscode.Uri, topLine: number | undefined) {
if (!this.isPreviewOf(resource)) {
public scrollTo(topLine: number) {
if (this._disposed) {
return;
}
@ -374,36 +254,26 @@ export class DynamicMarkdownPreview extends Disposable {
return;
}
if (typeof topLine === 'number') {
this._logger.log('updateForView', { markdownFile: resource });
this.line = topLine;
this.postMessage({
type: 'updateView',
line: topLine,
source: resource.toString()
});
}
this._logger.log('updateForView', { markdownFile: this._resource });
this.line = topLine;
this.postMessage({
type: 'updateView',
line: topLine,
source: this._resource.toString()
});
}
private postMessage(msg: any) {
if (!this._disposed) {
this.editor.webview.postMessage(msg);
}
}
private async updatePreview(forceUpdate?: boolean): Promise<void> {
clearTimeout(this.throttleTimer);
this.throttleTimer = undefined;
private async doUpdate(forceUpdate?: boolean): Promise<void> {
if (this._disposed) {
return;
}
const markdownResource = this._resource;
clearTimeout(this.throttleTimer);
this.throttleTimer = undefined;
let document: vscode.TextDocument;
try {
document = await vscode.workspace.openTextDocument(markdownResource);
document = await vscode.workspace.openTextDocument(this._resource);
} catch {
await this.showFileNotFoundError();
return;
@ -413,61 +283,24 @@ export class DynamicMarkdownPreview extends Disposable {
return;
}
const pendingVersion = new PreviewDocumentVersion(markdownResource, document.version);
const pendingVersion = new PreviewDocumentVersion(document);
if (!forceUpdate && this.currentVersion?.equals(pendingVersion)) {
if (this.line) {
this.updateForView(markdownResource, this.line);
this.scrollTo(this.line);
}
return;
}
this.currentVersion = pendingVersion;
if (this._resource === markdownResource) {
const self = this;
const resourceProvider: WebviewResourceProvider = {
asWebviewUri: (resource) => {
return this.editor.webview.asWebviewUri(normalizeResource(markdownResource, resource));
},
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);
}
const content = await this._contentProvider.provideTextDocumentContent(document, this, 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?.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) {
this.line = line;
@ -513,16 +346,47 @@ export class DynamicMarkdownPreview extends Disposable {
}
private async showFileNotFoundError() {
this.setContent(this._contentProvider.provideFileNotFoundContent(this._resource));
this._webviewPanel.webview.html = this._contentProvider.provideFileNotFoundContent(this._resource);
}
private setContent(html: string): void {
this.editor.title = DynamicMarkdownPreview.getPreviewTitle(this._resource, this._locked);
this.editor.iconPath = this.iconPath;
this.editor.webview.options = DynamicMarkdownPreview.getWebviewOptions(this._resource, this._contributionProvider.contributions);
this.editor.webview.html = html;
if (this._disposed) {
return;
}
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) {
let [hrefPath, fragment] = decodeURIComponent(href).split('#');
@ -537,14 +401,332 @@ export class DynamicMarkdownPreview extends Disposable {
if (openLinks === 'inPreview') {
const markdownLink = await resolveLinkToMarkdownFile(hrefPath);
if (markdownLink) {
if (fragment) {
this.scrollToFragment = fragment;
}
this.update(markdownLink);
this.delegate.openPreviewLinkToMarkdownFile(markdownLink, fragment);
return;
}
}
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 { disposeAll, Disposable } from '../util/dispose';
import { TopmostLineMonitor } from '../util/topmostLineMonitor';
import { DynamicMarkdownPreview } from './preview';
import { DynamicMarkdownPreview, StaticMarkdownPreview, ManagedMarkdownPreview } from './preview';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContentProvider } from './previewContentProvider';
@ -18,9 +18,9 @@ export interface DynamicPreviewSettings {
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 {
super.dispose();
@ -30,11 +30,11 @@ class PreviewStore extends Disposable {
this._previews.clear();
}
[Symbol.iterator](): Iterator<DynamicMarkdownPreview> {
[Symbol.iterator](): Iterator<T> {
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) {
if (preview.matchesResource(resource, previewSettings.previewColumn, previewSettings.locked)) {
return preview;
@ -43,11 +43,11 @@ class PreviewStore extends Disposable {
return undefined;
}
public add(preview: DynamicMarkdownPreview) {
public add(preview: T) {
this._previews.add(preview);
}
public delete(preview: DynamicMarkdownPreview) {
public delete(preview: T) {
this._previews.delete(preview);
}
}
@ -58,10 +58,10 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
private readonly _topmostLineMonitor = new TopmostLineMonitor();
private readonly _previewConfigurations = new MarkdownPreviewConfigurationManager();
private readonly _dynamicPreviews = this._register(new PreviewStore());
private readonly _staticPreviews = this._register(new PreviewStore());
private readonly _dynamicPreviews = this._register(new PreviewStore<DynamicMarkdownPreview>());
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';
@ -117,7 +117,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
public toggleLock() {
const preview = this._activePreview;
if (preview) {
if (preview instanceof DynamicMarkdownPreview) {
preview.toggleLock();
// 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,
state: any
): Promise<void> {
console.log(state);
const resource = vscode.Uri.parse(state.resource);
const locked = state.locked;
const line = state.line;
@ -150,21 +151,16 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this.registerDynamicPreview(preview);
}
public async openCustomDocument(uri: vscode.Uri) {
return { uri, dispose: () => { } };
}
public async resolveCustomTextEditor(
document: vscode.TextDocument,
webview: vscode.WebviewPanel
): Promise<void> {
const preview = DynamicMarkdownPreview.revive(
{ resource: document.uri, locked: false, resourceColumn: vscode.ViewColumn.One },
const preview = StaticMarkdownPreview.revive(
document.uri,
webview,
this._contentProvider,
this._previewConfigurations,
this._logger,
this._topmostLineMonitor,
this._contributions);
this.registerStaticPreview(preview);
}
@ -207,7 +203,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
return preview;
}
private registerStaticPreview(preview: DynamicMarkdownPreview): DynamicMarkdownPreview {
private registerStaticPreview(preview: StaticMarkdownPreview): StaticMarkdownPreview {
this._staticPreviews.add(preview);
preview.onDispose(() => {
@ -218,7 +214,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
return preview;
}
private trackActive(preview: DynamicMarkdownPreview): void {
private trackActive(preview: ManagedMarkdownPreview): void {
preview.onDidChangeViewState(({ webviewPanel }) => {
this.setPreviewActiveContext(webviewPanel.active);
this._activePreview = webviewPanel.active ? preview : undefined;