未验证 提交 2038b8fc 编写于 作者: M Matt Bierner 提交者: GitHub

Webview API prototype 3 (#44307)

* Webview API prototype 3

Part of #43713

Third try at refining the webview api. This pass reworks  #44165.  Major changes:

- Adds an `id` field to webviews. The id is provided by the extension and identifies the webview. It is used with the new event handling apis.

- Adds a new `onDidChangeActiveEditor` api. This is similar to `onDidChangeActiveTextEditor` but is also fired when you change webviews. It replaces the old `onFocus` and `onBlur` events on the webview itself

- Adds an `onDispose` event ot webviews. This is fired when a webview is closed by the user

- Perist webview state when the editor group changes. This is enabled for all webviews, not just those with keep alive.

* Throw error when trying to access disposed webview

* Improving webview documentation

* Clean up dispose management

* Throw if we receive a bad handle

* Move more event handling to input

* Simplify input updating

* Remove extra container property

* Fixing md security alert button

* Remove extra update container call

* Restore syncing of preview to active editor

* Fixing posting to webview

* Debounce preview updates

* Remove previewUri

* Enable direct window.postMessage instead of window.parent.postMessage

* Fixing scroll position not preserved when updating previews

* Revert parent.postMessage change.

Old behavior was correct

* Properly hide webview container on tab switch

* Make sure we only handle scroll events for the correct document

* Don't try setting negative scroll

* Revert vs code whitespace change
上级 de87042d
......@@ -16,7 +16,6 @@
return;
}
didShow = true;
const args = [settings.previewUri];
const notification = document.createElement('a');
notification.innerText = strings.cspAlertMessageText;
......@@ -25,8 +24,12 @@
notification.setAttribute('role', 'button');
notification.setAttribute('aria-label', strings.cspAlertMessageLabel);
notification.setAttribute('href', `command:markdown.showPreviewSecuritySelector?${encodeURIComponent(JSON.stringify(args))}`);
notification.onclick = () => {
window.parent.postMessage({
command: 'markdown.showPreviewSecuritySelector',
args: [settings.source]
}, '*');
};
document.body.appendChild(notification);
};
......
......@@ -146,7 +146,7 @@
} else {
scrollTo = previous.element.getBoundingClientRect().top;
}
window.scroll(0, window.scrollY + scrollTo + getSourceRevealAddedOffset());
window.scroll(0, Math.max(1, window.scrollY + scrollTo + getSourceRevealAddedOffset()));
}
}
......@@ -193,13 +193,13 @@
function onLoad() {
if (settings.scrollPreviewWithEditorSelection) {
const initialLine = +settings.line;
if (!isNaN(initialLine)) {
setTimeout(() => {
setTimeout(() => {
const initialLine = +settings.line;
if (!isNaN(initialLine)) {
scrollDisabled = true;
scrollToRevealSourceLine(initialLine);
}, 0);
}
}
}, 0);
}
}
......@@ -220,8 +220,13 @@
scrollToRevealSourceLine(line);
}, 50);
return event => {
if (event.data.source !== settings.source) {
return;
}
const line = +event.data.line;
if (!isNaN(line)) {
settings.line = line;
doScroll(line);
}
};
......
......@@ -3,25 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Command } from '../commandManager';
import { isMarkdownFile, getMarkdownUri, MarkdownPreviewWebviewManager } from '../features/previewContentProvider';
import { MarkdownPreviewManager } from '../features/previewContentProvider';
export class RefreshPreviewCommand implements Command {
public readonly id = 'markdown.refreshPreview';
public constructor(
private readonly webviewManager: MarkdownPreviewWebviewManager
private readonly webviewManager: MarkdownPreviewManager
) { }
public execute(resource: string | undefined) {
if (resource) {
const source = vscode.Uri.parse(resource);
this.webviewManager.update(source);
} else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
this.webviewManager.update(getMarkdownUri(vscode.window.activeTextEditor.document.uri));
} else {
this.webviewManager.updateAll();
}
public execute() {
this.webviewManager.refresh();
}
}
\ No newline at end of file
......@@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import { Command } from '../commandManager';
import { MarkdownPreviewWebviewManager, } from '../features/previewContentProvider';
import { MarkdownPreviewManager, } from '../features/previewContentProvider';
import { TelemetryReporter } from '../telemetryReporter';
......@@ -30,12 +30,12 @@ function getViewColumn(sideBySide: boolean): vscode.ViewColumn | undefined {
return active.viewColumn;
}
function showPreview(
webviewManager: MarkdownPreviewWebviewManager,
async function showPreview(
webviewManager: MarkdownPreviewManager,
telemetryReporter: TelemetryReporter,
uri?: vscode.Uri,
sideBySide: boolean = false,
) {
): Promise<any> {
let resource = uri;
if (!(resource instanceof vscode.Uri)) {
if (vscode.window.activeTextEditor) {
......@@ -53,23 +53,22 @@ function showPreview(
return;
}
const view = webviewManager.create(
webviewManager.preview(
resource,
(vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One,
getViewColumn(sideBySide) || vscode.ViewColumn.Active);
telemetryReporter.sendTelemetryEvent('openPreview', {
where: sideBySide ? 'sideBySide' : 'inPlace',
how: (uri instanceof vscode.Uri) ? 'action' : 'pallete'
});
return view;
}
export class ShowPreviewCommand implements Command {
public readonly id = 'markdown.showPreview';
public constructor(
private readonly webviewManager: MarkdownPreviewWebviewManager,
private readonly webviewManager: MarkdownPreviewManager,
private readonly telemetryReporter: TelemetryReporter
) { }
......@@ -84,7 +83,7 @@ export class ShowPreviewToSideCommand implements Command {
public readonly id = 'markdown.showPreviewToSide';
public constructor(
private readonly webviewManager: MarkdownPreviewWebviewManager,
private readonly webviewManager: MarkdownPreviewManager,
private readonly telemetryReporter: TelemetryReporter
) { }
......
......@@ -14,7 +14,7 @@ import { loadDefaultTelemetryReporter } from './telemetryReporter';
import { loadMarkdownExtensions } from './markdownExtensions';
import LinkProvider from './features/documentLinkProvider';
import MDDocumentSymbolProvider from './features/documentSymbolProvider';
import { MarkdownContentProvider, getMarkdownUri, isMarkdownFile, MarkdownPreviewWebviewManager } from './features/previewContentProvider';
import { MarkdownContentProvider, MarkdownPreviewManager } from './features/previewContentProvider';
export function activate(context: vscode.ExtensionContext) {
......@@ -30,20 +30,20 @@ export function activate(context: vscode.ExtensionContext) {
const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, logger);
loadMarkdownExtensions(contentProvider, engine);
const webviewManager = new MarkdownPreviewWebviewManager(contentProvider);
context.subscriptions.push(webviewManager);
const previewManager = new MarkdownPreviewManager(contentProvider, logger);
context.subscriptions.push(previewManager);
context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(selector, new MDDocumentSymbolProvider(engine)));
context.subscriptions.push(vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider()));
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, webviewManager);
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
const commandManager = new CommandManager();
context.subscriptions.push(commandManager);
commandManager.register(new commands.ShowPreviewCommand(webviewManager, telemetryReporter));
commandManager.register(new commands.ShowPreviewToSideCommand(webviewManager, telemetryReporter));
commandManager.register(new commands.ShowPreviewCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowSourceCommand());
commandManager.register(new commands.RefreshPreviewCommand(webviewManager));
commandManager.register(new commands.RefreshPreviewCommand(previewManager));
commandManager.register(new commands.RevealLineCommand(logger));
commandManager.register(new commands.MoveCursorToPositionCommand());
commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector));
......@@ -53,19 +53,6 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
logger.updateConfiguration();
webviewManager.updateConfiguration();
}));
context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(event => {
if (isMarkdownFile(event.textEditor.document)) {
const markdownFile = getMarkdownUri(event.textEditor.document.uri);
logger.log('updatePreviewForSelection', { markdownFile: markdownFile.toString() });
vscode.commands.executeCommand('_workbench.htmlPreview.postMessage',
markdownFile,
{
line: event.selections[0].active.line
});
}
previewManager.updateConfiguration();
}));
}
......@@ -19,20 +19,7 @@ const previewStrings = {
};
export function isMarkdownFile(document: vscode.TextDocument) {
return document.languageId === 'markdown'
&& document.uri.scheme !== MarkdownContentProvider.scheme; // prevent processing of own documents
}
export function getMarkdownUri(uri: vscode.Uri) {
if (uri.scheme === MarkdownContentProvider.scheme) {
return uri;
}
return uri.with({
scheme: MarkdownContentProvider.scheme,
path: uri.path + '.rendered',
query: uri.toString()
});
return document.languageId === 'markdown';
}
export class MarkdownPreviewConfig {
......@@ -137,8 +124,6 @@ export class PreviewConfigManager {
}
export class MarkdownContentProvider {
public static readonly scheme = 'markdown';
private extraStyles: Array<vscode.Uri> = [];
private extraScripts: Array<vscode.Uri> = [];
......@@ -234,19 +219,13 @@ export class MarkdownContentProvider {
public async provideTextDocumentContent(
sourceUri: vscode.Uri,
previewConfigurations: PreviewConfigManager
previewConfigurations: PreviewConfigManager,
initialLine: number | undefined = undefined
): Promise<string> {
let initialLine: number | undefined = undefined;
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.uri.toString() === sourceUri.toString()) {
initialLine = editor.selection.active.line;
}
const document = await vscode.workspace.openTextDocument(sourceUri);
const config = previewConfigurations.loadAndCacheConfiguration(sourceUri);
const initialData = {
previewUri: sourceUri.toString(), // TODO
source: sourceUri.toString(),
line: initialLine,
scrollPreviewWithEditorSelection: config.scrollPreviewWithEditorSelection,
......@@ -296,21 +275,99 @@ export class MarkdownContentProvider {
}
}
export class MarkdownPreviewWebviewManager {
private readonly webviews = new Map<string, vscode.Webview>();
class MarkdownPreview {
private throttleTimer: any;
private initialLine: number | undefined = undefined;
constructor(
public resource: vscode.Uri,
public webview: vscode.Webview,
public ofColumn: vscode.ViewColumn,
private readonly contentProvider: MarkdownContentProvider,
private readonly previewConfigurations: PreviewConfigManager
) { }
public update(resource: vscode.Uri) {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.uri.fsPath === resource.fsPath) {
this.initialLine = editor.selection.active.line;
} else {
this.initialLine = undefined;
}
// Schedule update
if (!this.throttleTimer) {
this.throttleTimer = setTimeout(() => this.doUpdate(), resource.fsPath === this.resource.fsPath ? 300 : 0);
}
this.resource = resource;
}
public updateForSelection(resource: vscode.Uri, line: number) {
if (this.resource.fsPath !== resource.fsPath) {
return;
}
this.initialLine = line;
this.webview.postMessage({ line, source: resource.toString() });
}
private getPreviewTitle(resource: vscode.Uri): string {
return localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath));
}
private doUpdate() {
const resource = this.resource;
this.throttleTimer = undefined;
this.contentProvider.provideTextDocumentContent(resource, this.previewConfigurations, this.initialLine)
.then(content => {
if (this.resource === resource) {
this.webview.title = this.getPreviewTitle(this.resource);
this.webview.html = content;
}
});
}
}
export class MarkdownPreviewManager {
private static webviewId = vscode.Uri.parse('vscode-markdown-preview://preview');
private previews: MarkdownPreview[] = [];
private readonly previewConfigurations = new PreviewConfigManager();
private readonly disposables: vscode.Disposable[] = [];
public constructor(
private readonly contentProvider: MarkdownContentProvider
private readonly contentProvider: MarkdownContentProvider,
private readonly logger: Logger
) {
vscode.workspace.onDidSaveTextDocument(document => {
this.update(document.uri);
vscode.workspace.onDidChangeTextDocument(event => {
this.update(event.document, undefined);
}, null, this.disposables);
vscode.workspace.onDidChangeTextDocument(event => {
this.update(event.document.uri);
vscode.window.onDidChangeActiveEditor(editor => {
vscode.commands.executeCommand('setContext', 'markdownPreview', editor && editor.editorType === 'webview' && editor.uri.fsPath === MarkdownPreviewManager.webviewId.fsPath);
if (editor && editor.editorType === 'texteditor') {
if (isMarkdownFile(editor.document)) {
for (const preview of this.previews.filter(preview => preview.ofColumn === editor.viewColumn)) {
preview.update(editor.document.uri);
}
}
}
}, null, this.disposables);
vscode.window.onDidChangeTextEditorSelection(event => {
if (!isMarkdownFile(event.textEditor.document)) {
return;
}
const resource = event.textEditor.document.uri;
for (const previewForResource of this.previews.filter(preview => preview.resource.fsPath === resource.fsPath)) {
this.logger.log('updatePreviewForSelection', { markdownFile: resource });
previewForResource.updateForSelection(resource, event.selections[0].active.line);
}
}, null, this.disposables);
}
......@@ -321,65 +378,73 @@ export class MarkdownPreviewWebviewManager {
item.dispose();
}
}
this.webviews.clear();
this.previews = [];
}
public update(uri: vscode.Uri) {
const webview = this.webviews.get(uri.fsPath);
if (webview) {
this.contentProvider.provideTextDocumentContent(uri, this.previewConfigurations).then(x => webview.html = x);
public refresh() {
for (const preview of this.previews) {
preview.update(preview.resource);
}
}
public updateAll() {
for (const resource of this.webviews.keys()) {
const sourceUri = vscode.Uri.parse(resource);
this.update(sourceUri);
public updateConfiguration() {
for (const preview of this.previews) {
if (this.previewConfigurations.shouldUpdateConfiguration(preview.resource)) {
preview.update(preview.resource);
}
}
}
public updateConfiguration() {
for (const resource of this.webviews.keys()) {
const sourceUri = vscode.Uri.parse(resource);
if (this.previewConfigurations.shouldUpdateConfiguration(sourceUri)) {
this.update(sourceUri);
private update(document: vscode.TextDocument, viewColumn: vscode.ViewColumn | undefined) {
if (!isMarkdownFile(document)) {
return;
}
for (const preview of this.previews) {
if (preview.resource.fsPath === document.uri.fsPath || viewColumn && preview.ofColumn === viewColumn) {
preview.update(document.uri);
}
}
}
public create(
public preview(
resource: vscode.Uri,
viewColumn: vscode.ViewColumn
resourceColumn: vscode.ViewColumn,
previewColumn: vscode.ViewColumn
) {
const view = vscode.window.createWebview(
localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)),
viewColumn,
{
enableScripts: true,
localResourceRoots: this.getLocalResourceRoots(resource)
// Only allow a single markdown preview per column
let preview = this.previews.find(preview => preview.webview.viewColumn === previewColumn);
if (preview) {
preview.resource = resource;
preview.ofColumn = resourceColumn;
} else {
const webview = vscode.window.createWebview(
MarkdownPreviewManager.webviewId,
previewColumn, {
enableScripts: true,
localResourceRoots: this.getLocalResourceRoots(resource)
});
webview.onDispose(() => {
const existing = this.previews.findIndex(preview => preview.webview === webview);
if (existing >= 0) {
this.previews.splice(existing, 1);
}
});
this.contentProvider.provideTextDocumentContent(resource, this.previewConfigurations).then(x => view.html = x);
view.onMessage(e => {
vscode.commands.executeCommand(e.command, ...e.args);
});
view.onBecameActive(() => {
vscode.commands.executeCommand('setContext', 'markdownPreview', true);
});
webview.onMessage(e => {
vscode.commands.executeCommand(e.command, ...e.args);
});
view.onBecameInactive(() => {
vscode.commands.executeCommand('setContext', 'markdownPreview', false);
});
preview = new MarkdownPreview(resource, webview, resourceColumn, this.contentProvider, this.previewConfigurations);
this.previews.push(preview);
}
this.webviews.set(resource.fsPath, view);
return view;
preview.update(preview.resource);
return preview.webview;
}
private getLocalResourceRoots(
resource: vscode.Uri
): vscode.Uri[] {
private getLocalResourceRoots(resource: vscode.Uri): vscode.Uri[] {
const folder = vscode.workspace.getWorkspaceFolder(resource);
if (folder) {
return [folder.uri];
......
......@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import { getMarkdownUri, MarkdownPreviewWebviewManager } from './features/previewContentProvider';
import { MarkdownPreviewManager } from './features/previewContentProvider';
import * as nls from 'vscode-nls';
......@@ -90,7 +90,7 @@ export class PreviewSecuritySelector {
public constructor(
private cspArbiter: ContentSecurityPolicyArbiter,
private webviewManager: MarkdownPreviewWebviewManager
private webviewManager: MarkdownPreviewManager
) { }
public async showSecutitySelectorForResource(resource: vscode.Uri): Promise<void> {
......@@ -143,15 +143,12 @@ export class PreviewSecuritySelector {
return;
}
const sourceUri = getMarkdownUri(resource);
if (selection.type === 'toggle') {
this.cspArbiter.setShouldDisableSecurityWarning(!this.cspArbiter.shouldDisableSecurityWarnings());
this.webviewManager.update(sourceUri);
return;
} else {
await this.cspArbiter.setSecurityLevelForResource(resource, selection.type);
}
await this.cspArbiter.setSecurityLevelForResource(resource, selection.type);
this.webviewManager.update(sourceUri);
this.webviewManager.refresh();
}
}
......@@ -513,19 +513,31 @@ declare module 'vscode' {
*/
export interface Webview {
/**
* Title of the webview.
* Type identifying the editor as a webview editor.
*/
title: string;
readonly editorType: 'webview';
/**
* Contents of the webview.
* Unique identifer of the webview.
*/
html: string;
readonly uri: Uri;
/**
* Content settings for the webview.
*/
options: WebviewOptions;
readonly options: WebviewOptions;
/**
* Title of the webview shown in UI.
*/
title: string;
/**
* Contents of the webview.
*
* Should be a complete html document.
*/
html: string;
/**
* The column in which the webview is showing.
......@@ -538,14 +550,14 @@ declare module 'vscode' {
readonly onMessage: Event<any>;
/**
* Fired when the webview becomes the active editor.
* Fired when the webview is disposed.
*/
readonly onBecameActive: Event<void>;
readonly onDispose: Event<void>;
/**
* Fired when the webview stops being the active editor
* Fired when the webview's view column changes.
*/
readonly onBecameInactive: Event<void>;
readonly onDidChangeViewColumn: Event<ViewColumn>;
/**
* Post a message to the webview content.
......@@ -554,23 +566,39 @@ declare module 'vscode' {
*
* @param message Body of the message.
*/
postMessage(message: any): Thenable<any>;
postMessage(message: any): Thenable<boolean>;
/**
* Dispose the webview.
* Dispose of the the webview.
*
* This closes the webview if it showing and disposes of the resources owned by the webview.
* Webview are also disposed when the user closes the webview editor. Both cases fire `onDispose`
* event. Trying to use the webview after it has been disposed throws an exception.
*/
dispose(): any;
}
export interface TextEditor {
/**
* Type identifying the editor as a text editor.
*/
readonly editorType: 'texteditor';
}
namespace window {
/**
* Create and show a new webview.
*
* @param title Title of the webview.
* @param uri Unique identifier for the webview.
* @param column Editor column to show the new webview in.
* @param options Webview content options.
* @param options Content settings for the webview.
*/
export function createWebview(uri: Uri, column: ViewColumn, options: WebviewOptions): Webview;
/**
* Event fired when the active editor changes.
*/
export function createWebview(title: string, column: ViewColumn, options: WebviewOptions): Webview;
export const onDidChangeActiveEditor: Event<TextEditor | Webview | undefined>;
}
export namespace window {
......
......@@ -97,7 +97,8 @@ export function createApiFactory(
rpcProtocol.set(ExtHostContext.ExtHostLogService, extHostLogService);
const extHostHeapService = rpcProtocol.set(ExtHostContext.ExtHostHeapService, new ExtHostHeapService());
const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, new ExtHostDecorations(rpcProtocol));
const extHostDocumentsAndEditors = rpcProtocol.set(ExtHostContext.ExtHostDocumentsAndEditors, new ExtHostDocumentsAndEditors(rpcProtocol));
const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol));
const extHostDocumentsAndEditors = rpcProtocol.set(ExtHostContext.ExtHostDocumentsAndEditors, new ExtHostDocumentsAndEditors(rpcProtocol, extHostWebviews));
const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors));
const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors));
const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadTextEditors)));
......@@ -117,7 +118,6 @@ export function createApiFactory(
const extHostTask = rpcProtocol.set(ExtHostContext.ExtHostTask, new ExtHostTask(rpcProtocol, extHostWorkspace));
const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol));
rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService);
const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol));
// Check that no named customers are missing
const expected: ProxyIdentifier<any>[] = Object.keys(ExtHostContext).map((key) => ExtHostContext[key]);
......@@ -402,8 +402,11 @@ export function createApiFactory(
registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider) => {
return extHostDecorations.registerDecorationProvider(provider, extension.id);
}),
createWebview: proposedApiFunction(extension, (name: string, column: vscode.ViewColumn, options: vscode.WebviewOptions) => {
return extHostWebviews.createWebview(name, column, options);
createWebview: proposedApiFunction(extension, (uri: vscode.Uri, column: vscode.ViewColumn, options: vscode.WebviewOptions) => {
return extHostWebviews.createWebview(uri, column, options);
}),
onDidChangeActiveEditor: proposedApiFunction(extension, (listener, thisArg?, disposables?) => {
return extHostDocumentsAndEditors.onDidChangeActiveEditor(listener, thisArg, disposables);
})
};
......
......@@ -342,19 +342,21 @@ export interface MainThreadTelemetryShape extends IDisposable {
$publicLog(eventName: string, data?: any): void;
}
export interface MainThreadWebviewShape extends IDisposable {
$createWebview(handle: number): void;
$disposeWebview(handle: number): void;
$show(handle: number, column: EditorPosition): void;
$setTitle(handle: number, value: string): void;
$setHtml(handle: number, value: string): void;
$setOptions(handle: number, value: vscode.WebviewOptions): void;
$sendMessage(handle: number, value: any): Thenable<boolean>;
export type WebviewHandle = number;
export interface MainThreadWebviewsShape extends IDisposable {
$createWebview(handle: WebviewHandle, uri: URI, options: vscode.WebviewOptions): void;
$disposeWebview(handle: WebviewHandle): void;
$show(handle: WebviewHandle, column: EditorPosition): void;
$setTitle(handle: WebviewHandle, value: string): void;
$setHtml(handle: WebviewHandle, value: string): void;
$sendMessage(handle: WebviewHandle, value: any): Thenable<boolean>;
}
export interface ExtHostWebviewsShape {
$onMessage(handle: number, message: any): void;
$onBecameActive(handle: number): void;
$onBecameInactive(handle: number): void;
$onMessage(handle: WebviewHandle, message: any): void;
$onDidChangeActiveWeview(handle: WebviewHandle | undefined): void;
$onDidDisposeWeview(handle: WebviewHandle): void;
$onDidChangePosition(handle: WebviewHandle, newPosition: EditorPosition): void;
}
export interface MainThreadWorkspaceShape extends IDisposable {
......@@ -817,7 +819,7 @@ export const MainContext = {
MainThreadStorage: createMainId<MainThreadStorageShape>('MainThreadStorage'),
MainThreadTelemetry: createMainId<MainThreadTelemetryShape>('MainThreadTelemetry'),
MainThreadTerminalService: createMainId<MainThreadTerminalServiceShape>('MainThreadTerminalService'),
MainThreadWebview: createMainId<MainThreadWebviewShape>('MainThreadWebview'),
MainThreadWebviews: createMainId<MainThreadWebviewsShape>('MainThreadWebviews'),
MainThreadWorkspace: createMainId<MainThreadWorkspaceShape>('MainThreadWorkspace'),
MainThreadFileSystem: createMainId<MainThreadFileSystemShape>('MainThreadFileSystem'),
MainThreadExtensionService: createMainId<MainThreadExtensionServiceShape>('MainThreadExtensionService'),
......
......@@ -12,10 +12,16 @@ import { ExtHostTextEditor } from './extHostTextEditor';
import * as assert from 'assert';
import * as typeConverters from './extHostTypeConverters';
import URI from 'vs/base/common/uri';
import { ExtHostWebview, ExtHostWebviews } from './extHostWebview';
import { Disposable } from './extHostTypes';
export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsShape {
private _disposables: Disposable[] = [];
private _activeEditorId: string;
private _activeWebview: ExtHostWebview;
private readonly _editors = new Map<string, ExtHostTextEditor>();
private readonly _documents = new Map<string, ExtHostDocumentData>();
......@@ -23,15 +29,34 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha
private readonly _onDidRemoveDocuments = new Emitter<ExtHostDocumentData[]>();
private readonly _onDidChangeVisibleTextEditors = new Emitter<ExtHostTextEditor[]>();
private readonly _onDidChangeActiveTextEditor = new Emitter<ExtHostTextEditor>();
private readonly _onDidChangeActiveEditor = new Emitter<ExtHostTextEditor | ExtHostWebview>();
readonly onDidAddDocuments: Event<ExtHostDocumentData[]> = this._onDidAddDocuments.event;
readonly onDidRemoveDocuments: Event<ExtHostDocumentData[]> = this._onDidRemoveDocuments.event;
readonly onDidChangeVisibleTextEditors: Event<ExtHostTextEditor[]> = this._onDidChangeVisibleTextEditors.event;
readonly onDidChangeActiveTextEditor: Event<ExtHostTextEditor> = this._onDidChangeActiveTextEditor.event;
readonly onDidChangeActiveEditor: Event<ExtHostTextEditor | ExtHostWebview> = this._onDidChangeActiveEditor.event;
constructor(
private readonly _mainContext: IMainContext
private readonly _mainContext: IMainContext,
_extHostWebviews?: ExtHostWebviews
) {
if (_extHostWebviews) {
_extHostWebviews.onDidChangeActiveWebview(webview => {
if (webview) {
if (webview !== this._activeWebview) {
this._onDidChangeActiveEditor.fire(webview);
this._activeWebview = webview;
}
} else {
this._activeWebview = webview;
}
}, this, this._disposables);
}
}
dispose() {
this._disposables = dispose(this._disposables);
}
$acceptDocumentsAndEditorsDelta(delta: IDocumentsAndEditorsDelta): void {
......@@ -117,6 +142,9 @@ export class ExtHostDocumentsAndEditors implements ExtHostDocumentsAndEditorsSha
}
if (delta.newActiveEditor !== undefined) {
this._onDidChangeActiveTextEditor.fire(this.activeEditor());
const activeEditor = this.activeEditor();
this._onDidChangeActiveEditor.fire(activeEditor || this._activeWebview);
}
}
......
......@@ -313,7 +313,7 @@ export class ExtHostTextEditorOptions implements vscode.TextEditorOptions {
export class ExtHostTextEditor implements vscode.TextEditor {
public readonly type = 'texteditor';
public readonly editorType = 'texteditor';
private readonly _proxy: MainThreadTextEditorsShape;
private readonly _id: string;
......
......@@ -3,49 +3,66 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MainContext, MainThreadWebviewShape, IMainContext, ExtHostWebviewsShape } from './extHost.protocol';
import { MainContext, MainThreadWebviewsShape, IMainContext, ExtHostWebviewsShape, WebviewHandle } from './extHost.protocol';
import * as vscode from 'vscode';
import { Emitter } from 'vs/base/common/event';
import Event, { Emitter } from 'vs/base/common/event';
import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters';
import { Position } from 'vs/platform/editor/common/editor';
export class ExtHostWebview implements vscode.Webview {
public readonly editorType = 'webview';
class ExtHostWebview implements vscode.Webview {
private _title: string;
private _html: string;
private _options: vscode.WebviewOptions;
private _isDisposed: boolean = false;
private _viewColumn: vscode.ViewColumn;
public readonly onMessageEmitter = new Emitter<any>();
public readonly onMessage = this.onMessageEmitter.event;
public readonly onMessage: Event<any> = this.onMessageEmitter.event;
public readonly onBecameActiveEmitter = new Emitter<void>();
public readonly onBecameActive = this.onBecameActiveEmitter.event;
public readonly onDisposeEmitter = new Emitter<void>();
public readonly onDispose: Event<void> = this.onDisposeEmitter.event;
public readonly onBecameInactiveEmitter = new Emitter<void>();
public readonly onBecameInactive = this.onBecameInactiveEmitter.event;
public readonly onDidChangeViewColumnEmitter = new Emitter<vscode.ViewColumn>();
public readonly onDidChangeViewColumn: Event<vscode.ViewColumn> = this.onDidChangeViewColumnEmitter.event;
constructor(
private readonly _proxy: MainThreadWebviewShape,
private readonly _handle: number,
viewColumn: vscode.ViewColumn
private readonly _handle: WebviewHandle,
private readonly _proxy: MainThreadWebviewsShape,
private readonly _uri: vscode.Uri,
viewColumn: vscode.ViewColumn,
options: vscode.WebviewOptions
) {
this._viewColumn = viewColumn;
this._options = options;
}
public dispose() {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this._proxy.$disposeWebview(this._handle);
this.onDisposeEmitter.dispose();
this.onMessageEmitter.dispose();
this.onDidChangeViewColumnEmitter.dispose();
}
get uri(): vscode.Uri {
this.assertNotDisposed();
return this._uri;
}
get title(): string {
this.assertNotDisposed();
return this._title;
}
set title(value: string) {
this.assertNotDisposed();
if (this._title !== value) {
this._title = value;
this._proxy.$setTitle(this._handle, value);
......@@ -53,10 +70,12 @@ class ExtHostWebview implements vscode.Webview {
}
get html(): string {
this.assertNotDisposed();
return this._html;
}
set html(value: string) {
this.assertNotDisposed();
if (this._html !== value) {
this._html = value;
this._proxy.$setHtml(this._handle, value);
......@@ -64,63 +83,88 @@ class ExtHostWebview implements vscode.Webview {
}
get options(): vscode.WebviewOptions {
this.assertNotDisposed();
return this._options;
}
set options(value: vscode.WebviewOptions) {
this._proxy.$setOptions(this._handle, value);
}
get viewColumn(): vscode.ViewColumn {
this.assertNotDisposed();
return this._viewColumn;
}
set viewColumn(value: vscode.ViewColumn) {
this.assertNotDisposed();
this._viewColumn = value;
}
public postMessage(message: any): Thenable<any> {
return this._proxy.$sendMessage(this._handle, message);
}
private assertNotDisposed() {
if (this._isDisposed) {
throw new Error('Webview is disposed');
}
}
}
export class ExtHostWebviews implements ExtHostWebviewsShape {
private static _handlePool = 0;
private static handlePool = 0;
private readonly _proxy: MainThreadWebviewShape;
private readonly _proxy: MainThreadWebviewsShape;
private readonly _webviews = new Map<number, ExtHostWebview>();
private readonly _webviews = new Map<WebviewHandle, ExtHostWebview>();
constructor(
mainContext: IMainContext
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadWebview);
this._proxy = mainContext.getProxy(MainContext.MainThreadWebviews);
}
createWebview(
title: string,
uri: vscode.Uri,
viewColumn: vscode.ViewColumn,
options: vscode.WebviewOptions
): vscode.Webview {
const handle = ExtHostWebviews._handlePool++;
this._proxy.$createWebview(handle);
const handle = ExtHostWebviews.handlePool++;
if (!this._webviews.has(handle)) {
this._proxy.$createWebview(handle, uri, options);
const webview = new ExtHostWebview(handle, this._proxy, uri, viewColumn, options);
this._webviews.set(handle, webview);
}
const webview = new ExtHostWebview(this._proxy, handle, viewColumn);
this._webviews.set(handle, webview);
webview.title = title;
webview.options = options;
this._proxy.$show(handle, typeConverters.fromViewColumn(viewColumn));
return webview;
return this._webviews.get(handle);
}
$onMessage(handle: number, message: any): void {
$onMessage(handle: WebviewHandle, message: any): void {
const webview = this._webviews.get(handle);
webview.onMessageEmitter.fire(message);
}
$onBecameActive(handle: number): void {
$onDidChangeActiveWeview(handle: WebviewHandle | undefined): void {
const webview = this._webviews.get(handle);
webview.onBecameActiveEmitter.fire();
this._onDidChangeActiveWebview.fire(webview);
}
$onBecameInactive(handle: number): void {
$onDidDisposeWeview(handle: WebviewHandle): void {
const webview = this._webviews.get(handle);
webview.onBecameInactiveEmitter.fire();
if (webview) {
webview.onDisposeEmitter.fire();
}
}
$onDidChangePosition(handle: WebviewHandle, newPosition: Position): void {
const webview = this._webviews.get(handle);
if (webview) {
const newViewColumn = typeConverters.toViewColumn(newPosition);
webview.viewColumn = newViewColumn;
webview.onDidChangeViewColumnEmitter.fire(newViewColumn);
}
}
private readonly _onDidChangeActiveWebview = new Emitter<ExtHostWebview | undefined>();
public readonly onDidChangeActiveWebview = this._onDidChangeActiveWebview.event;
}
\ No newline at end of file
......@@ -14,6 +14,8 @@ import { IEditorControl } from 'vs/platform/editor/common/editor';
import Event, { Emitter } from 'vs/base/common/event';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IConstructorSignature0, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import DOM = require('vs/base/browser/dom');
import { IDisposable } from 'vs/base/common/lifecycle';
/**
* Composites are layed out in the sidebar and panel part of the workbench. At a time only one composite
......@@ -27,6 +29,10 @@ import { IConstructorSignature0, IInstantiationService } from 'vs/platform/insta
*/
export abstract class Composite extends Component implements IComposite {
private _onTitleAreaUpdate: Emitter<void>;
private _onDidFocus: Emitter<void>;
private _focusTracker?: DOM.IFocusTracker;
private _focusListenerDisposable?: IDisposable;
private visible: boolean;
private parent: Builder;
......@@ -45,6 +51,7 @@ export abstract class Composite extends Component implements IComposite {
this.visible = false;
this._onTitleAreaUpdate = new Emitter<void>();
this._onDidFocus = new Emitter<void>();
}
public getTitle(): string {
......@@ -85,8 +92,12 @@ export abstract class Composite extends Component implements IComposite {
return this.parent;
}
public getFocusContainer(): Builder {
return this.getContainer();
public get onDidFocus(): Event<any> {
this._focusTracker = DOM.trackFocus(this.getContainer().getHTMLElement());
this._focusListenerDisposable = this._focusTracker.onDidFocus(() => {
this._onDidFocus.fire();
});
return this._onDidFocus.event;
}
/**
......@@ -189,6 +200,15 @@ export abstract class Composite extends Component implements IComposite {
public dispose(): void {
this._onTitleAreaUpdate.dispose();
this._onDidFocus.dispose();
if (this._focusTracker) {
this._focusTracker.dispose();
}
if (this._focusListenerDisposable) {
this._focusListenerDisposable.dispose();
}
super.dispose();
}
......
......@@ -35,7 +35,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { editorBackground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { Themable, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_GROUP_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BORDER } from 'vs/workbench/common/theme';
import { attachProgressBarStyler } from 'vs/platform/theme/common/styler';
import { IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ResourcesDropHandler, LocalSelectionTransfer, DraggedEditorIdentifier } from 'vs/workbench/browser/dnd';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IPartService } from 'vs/workbench/services/part/common/partService';
......@@ -454,13 +454,9 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro
this.visibleEditorFocusTrackerDisposable[position].dispose();
}
// Track focus on editor container
const focusTracker = DOM.trackFocus(editor.getFocusContainer().getHTMLElement());
const listenerDispose = focusTracker.onDidFocus(() => {
this.visibleEditorFocusTrackerDisposable[position] = editor.onDidFocus(() => {
this.onFocusGained(editor);
});
this.visibleEditorFocusTrackerDisposable[position] = combinedDisposable([focusTracker, listenerDispose]);
}
private onFocusGained(editor: BaseEditor): void {
......
......@@ -70,6 +70,16 @@
}
}
function onMessage(message) {
if (enableWrappedPostMessage) {
// Modern webview. Forward wrapped message
ipcRenderer.sendToHost('onmessage', message.data);
} else {
// Old school webview. Forward exact message
ipcRenderer.sendToHost(message.data.command, message.data.data);
}
}
var isHandlingScroll = false;
function handleInnerScroll(event) {
if (isHandlingScroll) {
......@@ -337,15 +347,7 @@
});
// Forward messages from the embedded iframe
window.onmessage = function (message) {
if (enableWrappedPostMessage) {
// Modern webview. Forward wrapped message
ipcRenderer.sendToHost('onmessage', message.data);
} else {
// Old school webview. Forward exact message
ipcRenderer.sendToHost(message.data.command, message.data.data);
}
};
window.onmessage = onMessage;
// signal ready
ipcRenderer.sendToHost('webview-ready', process.pid);
......
......@@ -234,6 +234,10 @@ export class Webview {
}
public set contents(value: string) {
if (this._contents === value) {
return;
}
this._contents = value;
this._send('content', {
contents: value,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册