diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 7c9a6a89a75c8307e2c995189184746643f7d747..b9760f9c3d5a95e8a0e59d6aba632d7140615711 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -21,6 +21,7 @@ import { equals, distinct } from 'vs/base/common/arrays'; import { DataTransfers, StaticDND, IDragAndDropData } from 'vs/base/browser/dnd'; import { disposableTimeout, Delayer } from 'vs/base/common/async'; import { isFirefox } from 'vs/base/browser/browser'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; interface IItem { readonly id: string; @@ -268,6 +269,10 @@ export class ListView implements ISpliceable, IDisposable { this.layout(); } + public triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { + this.scrollableElement.triggerScrollFromMouseWheelEvent(browserEvent); + } + updateDynamicHeight(index: number, element: T, size: number): void { this.rangeMap.splice(index, 1, [ { @@ -283,6 +288,8 @@ export class ListView implements ISpliceable, IDisposable { this.updateItemInDOM(this.items[i], i); } } + + this._onDidChangeContentHeight.fire(this.contentHeight); } splice(start: number, deleteCount: number, elements: T[] = []): T[] { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index f459a0218e9e4262aeefef38e761adaa623be9f1..8e0e0f67dd4b352ff4fe40ff44b0dfb9ca6be797 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -26,6 +26,7 @@ import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice'; import { clamp } from 'vs/base/common/numbers'; import { matchesPrefix } from 'vs/base/common/filters'; import { IDragAndDropData } from 'vs/base/browser/dnd'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; interface ITraitChangeEvent { indexes: number[]; @@ -1305,6 +1306,10 @@ export class List implements ISpliceable, IDisposable { this.eventBufferer.bufferEvents(() => this.spliceable.splice(start, deleteCount, elements)); } + public triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { + this.view.triggerScrollFromMouseWheelEvent(browserEvent); + } + updateDynamicHeight(index: number, element: T, size: number): void { this.view.updateDynamicHeight(index, element, size); } diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index c61cf6267c165dbef68ee27efcbc846ab4b8b50c..954ef2817aa79149f49e2341216dc483c23d0a7f 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -298,6 +298,10 @@ export abstract class AbstractScrollableElement extends Widget { this._revealOnScroll = value; } + public triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { + this._onMouseWheel(new StandardWheelEvent(browserEvent)); + } + // -------------------- mouse wheel scrolling -------------------- private _setListeningToMouseWheel(shouldListen: boolean): void { diff --git a/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts index 3c686886f9b36c9f394a4ad6ae79f197826847c3..bf26cf1d1096db08d748ef92594da9cf2acf5560 100644 --- a/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts @@ -32,13 +32,13 @@ import { ITextModel } from 'vs/editor/common/model'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Emitter } from 'vs/base/common/event'; import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; export class ViewCell { private _textModel: ITextModel | null = null; private _mdRenderer: marked.Renderer | null = null; private _html: string | null = null; private _dynamicHeight: number | null = null; - private _output: HTMLElement | null = null; protected readonly _onDidDispose = new Emitter(); readonly onDidDispose = this._onDidDispose.event; @@ -171,6 +171,8 @@ export interface NotebookHandler { saveNotebookCell(cell: ViewCell): void; layoutElement(cell: ViewCell, height: number): void; createContentWidget(cell: ViewCell, shadowContent: string, shadowElement: HTMLElement, offset: number): void; + disposeViewCell(cell: ViewCell): void; + triggerWheel(event: IMouseWheelEvent): void; } export interface CellRenderTemplate { @@ -618,6 +620,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.disposables.delete(templateData.outputContainer!); } } + + this.handler.disposeViewCell(element); } getSimpleCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { diff --git a/src/vs/workbench/contrib/notebook/browser/contentWidget.ts b/src/vs/workbench/contrib/notebook/browser/contentWidget.ts new file mode 100644 index 0000000000000000000000000000000000000000..cecf14eb577b2599cb14df6b6167fa44857e6bb7 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contentWidget.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookHandler, ViewCell } from 'vs/workbench/contrib/notebook/browser/cellRenderer'; +import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; +import * as DOM from 'vs/base/browser/dom'; +import * as UUID from 'vs/base/common/uuid'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; + +export class WebviewContentWidget extends Disposable { + public element: HTMLElement; + public webview: WebviewElement; + private _dimension: DOM.Dimension | null = null; + private readonly _localStore = new DisposableStore(); + private _detachedFromViewEvents: boolean = false; + + get detachedFromViewEvents() { + return this._detachedFromViewEvents; + } + + set detachedFromViewEvents(newState: boolean) { + this._detachedFromViewEvents = newState; + this._localStore.clear(); + } + + + constructor(public shadowElement: HTMLElement, public cell: ViewCell, public offset: number, public webviewService: IWebviewService, shadowContent: string, public notebookHandler: NotebookHandler) { + super(); + + this.element = document.createElement('div'); + this.element.style.width = 'calc(100% - 36px)'; + this.element.style.height = '700px'; + this.element.style.position = 'absolute'; + this.element.style.margin = '0px 24px 0px 24px'; + this.webview = this._createInset(webviewService, shadowContent); + this.webview.mountTo(this.element); + this._localStore.add(this.webview.onDidSetInitialDimension(dimension => { + this._dimension = dimension; + this.shadowElement.style.height = `${dimension.height}px`; + this.shadowElement.style.maxWidth = '100%'; + this.shadowElement.style.maxHeight = '700px'; + this.element.style.height = `${dimension.height}px`; + this.element.style.maxWidth = '100%'; + this.element.style.maxHeight = '700px'; + const lineNum = cell.lineCount; + const totalHeight = Math.max(lineNum + 1, 5) * 21; + cell.setDynamicHeight(totalHeight + 32 + dimension.height); + notebookHandler.layoutElement(cell, totalHeight + 32 + dimension.height); + })); + + this._localStore.add(this.webview.onDidWheel(e => { + this.notebookHandler.triggerWheel(e); + })); + } + + updateInitialization(shadowElement: HTMLElement, cell: ViewCell, offset: number, shadowContent: string) { + this._localStore.clear(); + this.shadowElement = shadowElement; + this.cell = cell; + this.offset = offset; + + this.element.style.height = '700px'; + this.element.style.width = 'calc(100% - 36px)'; + + this.webview.html = shadowContent; + + this._localStore.add(this.webview.onDidSetInitialDimension(dimension => { + this._dimension = dimension; + this.shadowElement.style.height = `${dimension.height}px`; + this.shadowElement.style.maxWidth = '100%'; + this.shadowElement.style.maxHeight = '700px'; + this.element.style.height = `${dimension.height}px`; + this.element.style.maxWidth = '100%'; + this.element.style.maxHeight = '700px'; + const lineNum = cell.lineCount; + const totalHeight = Math.max(lineNum + 1, 5) * 21; + cell.setDynamicHeight(totalHeight + 32 + dimension.height); + this.notebookHandler.layoutElement(cell, totalHeight + 32 + dimension.height); + })); + + this._localStore.add(this.webview.onDidWheel(e => { + this.notebookHandler.triggerWheel(e); + })); + } + + public updateShadowElement(element: HTMLElement) { + this.shadowElement = element; + if (this._dimension) { + this.shadowElement.style.minWidth = `${this._dimension.width}px`; + this.shadowElement.style.height = `${this._dimension.height}px`; + this.shadowElement.style.maxWidth = '100%'; + this.shadowElement.style.maxHeight = '700px'; + const lineNum = this.cell.lineCount; + const totalHeight = Math.max(lineNum + 1, 5) * 21; + this.cell.setDynamicHeight(totalHeight + 32 + this._dimension.height); + this.notebookHandler.layoutElement(this.cell, totalHeight + 32 + this._dimension.height); + } + } + + private _createInset(webviewService: IWebviewService, content: string) { + const webview = webviewService.createWebview('' + UUID.generateUuid(), { + enableFindWidget: false, + }, { + allowScripts: true + }); + webview.html = content; + return webview; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index de7975bf79d2e48653926fc3912479ee06dc8a94..720613fe8ba12783dba5b28c0ebd455e1187fda1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -11,7 +11,6 @@ import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/noteb import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { IEditorInput } from 'vs/workbench/common/editor'; import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { endsWith } from 'vs/base/common/strings'; @@ -30,7 +29,6 @@ Registry.as(EditorExtensions.Editors).registerEditor( ); export class NotebookContribution implements IWorkbenchContribution { - private editorOpeningListener: IDisposable | undefined; private _resourceMapping: Map = new Map(); constructor( @@ -38,7 +36,7 @@ export class NotebookContribution implements IWorkbenchContribution { @IInstantiationService private readonly instantiationService: IInstantiationService ) { - this.editorOpeningListener = this.editorService.overrideOpenEditor((editor, options, group) => this.onEditorOpening(editor, options, group)); + this.editorService.overrideOpenEditor((editor, options, group) => this.onEditorOpening(editor, options, group)); } private onEditorOpening(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 56c8d21db46712e50814075f5b5e97cbe3fc2333..561900f60d43b07b0aa6e2d762c040fbd11177d9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -20,93 +20,19 @@ import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { NotebookHandler, ViewCell, MarkdownCellRenderer, CodeCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/cellRenderer'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; -import * as UUID from 'vs/base/common/uuid'; +import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewContentWidget } from 'vs/workbench/contrib/notebook/browser/contentWidget'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; const $ = DOM.$; -export interface IContentWidget { - offset: number; - cell: ViewCell; - element: HTMLElement; - webview: WebviewElement; -} - -export class WebviewContentWidget implements IContentWidget { - public element: HTMLElement; - public webview: WebviewElement; - - private _dimension: DOM.Dimension | null = null; - - constructor( - public shadowElement: HTMLElement, - public cell: ViewCell, - public offset: number, - webviewService: IWebviewService, - shadowContent: string, - public notebookHandler: NotebookHandler - ) { - this.element = document.createElement('div'); - this.element.style.width = 'calc(100% - 36px)'; - this.element.style.height = '700px'; - this.element.style.position = 'absolute'; - this.element.style.margin = '0px 24px 0px 24px'; - - this.webview = this._createInset(webviewService, shadowContent); - this.webview.mountTo(this.element); - - this.webview.onDidSetInitialDimension(dimension => { - this._dimension = dimension; - // this.shadowElement.style.minWidth = `${dimension.width}px`; - this.shadowElement.style.height = `${dimension.height}px`; - this.shadowElement.style.maxWidth = '100%'; - this.shadowElement.style.maxHeight = '700px'; - // this.element.style.minWidth= `${dimension.width}px`; - this.element.style.height = `${dimension.height}px`; - this.element.style.maxWidth = '100%'; - this.element.style.maxHeight = '700px'; - const lineNum = cell.lineCount; - const totalHeight = Math.max(lineNum + 1, 5) * 21; - cell.setDynamicHeight(totalHeight + 32 + dimension.height); - notebookHandler.layoutElement(cell, totalHeight + 32 + dimension.height); - }); - } - - public updateShadowElement(element: HTMLElement) { - this.shadowElement = element; - if (this._dimension) { - this.shadowElement.style.minWidth = `${this._dimension.width}px`; - this.shadowElement.style.height = `${this._dimension.height}px`; - this.shadowElement.style.maxWidth = '100%'; - this.shadowElement.style.maxHeight = '700px'; - const lineNum = this.cell.lineCount; - const totalHeight = Math.max(lineNum + 1, 5) * 21; - this.cell.setDynamicHeight(totalHeight + 32 + this._dimension.height); - this.notebookHandler.layoutElement(this.cell, totalHeight + 32 + this._dimension.height); - } - } - - private _createInset(webviewService: IWebviewService, content: string) { - const webview = webviewService.createWebview('' + UUID.generateUuid(), { - enableFindWidget: false, - }, { - allowScripts: true - }); - - webview.html = content; - return webview; - } - - dispose() { - - } -} export class NotebookEditor extends BaseEditor implements NotebookHandler { static readonly ID: string = 'workbench.editor.notebook'; private rootElement!: HTMLElement; private body!: HTMLElement; private contentWidgets!: HTMLElement; private contentWidgetsMap: Map = new Map(); + private contentWidgetsPool: WebviewContentWidget[] = []; private list: WorkbenchList | undefined; private model: NotebookEditorModel | undefined; @@ -189,8 +115,12 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { } ); - this.list.onDidScroll((e) => { + this._register(this.list.onDidScroll((e) => { this.contentWidgetsMap.forEach((value, cell) => { + if (value.detachedFromViewEvents) { + return; + } + let index = this.model!.getNotebook().cells.indexOf(cell.cell); let top = this.list?.getElementTop(index); if (top !== null && top !== undefined) { @@ -199,13 +129,35 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { domElement.style.top = `${-scrollTop + top + value.offset}px`; } }); - }); + })); + + this._register(this.list); + } + + triggerWheel(event: IMouseWheelEvent) { + this.list?.triggerScrollFromMouseWheelEvent(event); } createContentWidget(cell: ViewCell, shadowContent: string, shadowElement: HTMLElement, offset: number) { let zone = this.contentWidgetsMap.get(cell); if (!zone) { + let existingContentWidget = this.contentWidgetsPool.pop(); + if (existingContentWidget) { + existingContentWidget.detachedFromViewEvents = false; + existingContentWidget.updateInitialization(shadowElement, cell, offset, shadowContent); + this.contentWidgetsMap.set(cell, existingContentWidget); + + let index = this.model!.getNotebook().cells.indexOf(cell.cell); + let top = this.list?.getElementTop(index); + if (top !== null && top !== undefined) { + let domElement = existingContentWidget.element; + let scrollTop = this.list?.scrollTop || 0; + domElement.style.top = `${-scrollTop + top + existingContentWidget.offset}px`; + } + return; + } + let contentWidget = new WebviewContentWidget( shadowElement, cell, @@ -222,6 +174,28 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { } } + disposeViewCell(cell: ViewCell) { + let zone = this.contentWidgetsMap.get(cell); + + if (zone) { + // we are going to dispose a view who has a webview + if (!zone.webview.containsScript) { + // this view can be disposed + zone.detachedFromViewEvents = true; + zone.element.style.top = '-2400px'; + zone.element.style.height = '700px'; + + if (this.contentWidgetsPool.length < 10) { + this.contentWidgetsPool.push(zone); + this.contentWidgetsMap.delete(cell); + } else { + this.contentWidgets.removeChild(zone.element); + this.contentWidgetsMap.delete(cell); + } + } + } + } + onHide() { super.onHide(); @@ -268,6 +242,7 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { // list.splice -> renderElement -> resize -> layoutElement // above flow will actually break how list view renders it self as it messes up with the internal state // instead we run the layout update in next tick + //. @TODO @rebornix, it should be batched. let index = this.model!.getNotebook().cells.indexOf(cell.cell); this.list?.updateDynamicHeight(index, cell, height); }, 0); diff --git a/src/vs/workbench/contrib/notebook/browser/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/outputRenderer.ts index 5f7ac9bf100659c98ee9a4cbdeb44ef39b99e08e..602b538cdfe2087981622f9f97be95c5ba26aeb8 100644 --- a/src/vs/workbench/contrib/notebook/browser/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/outputRenderer.ts @@ -97,12 +97,10 @@ class RichDisplayRenderer implements IMimeRenderer { outputNode.appendChild(display); hasDynamicHeight = true; } else if (output.data && output.data['text/html']) { - let data = output.data['text/html']; let str = isArray(data) ? data.join('') : data; display.style.width = '100%'; display.style.height = '100px'; - // display.style.backgroundColor = 'gray'; hasDynamicHeight = true; outputNode.appendChild(display); diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 478b03b736f454beb906a3ebb88d6547af7c59ee..a925f52163f782d4129995936e271c097a6756e9 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -13,6 +13,9 @@ import { WebviewExtensionDescription, WebviewOptions, WebviewContentOptions } fr import { areWebviewInputOptionsEqual } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/common/themeing'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { StopWatch } from 'vs/base/common/stopwatch'; export const enum WebviewMessageChannels { onmessage = 'onmessage', @@ -25,7 +28,10 @@ export const enum WebviewMessageChannels { loadResource = 'load-resource', loadLocalhost = 'load-localhost', webviewReady = 'webview-ready', - didSetInitialDimension = 'did-set-initial-dimension' + didSetInitialDimension = 'did-set-initial-dimension', + containsScripts = 'content-contains-scripts', + wheel = 'did-scroll-wheel', + ack = 'speed-test-ack' } interface IKeydownEvent { @@ -59,6 +65,8 @@ export abstract class BaseWebview extends Disposable { public extension: WebviewExtensionDescription | undefined; + private sw: StopWatch | null = null; + constructor( // TODO: matb, this should not be protected. The only reason it needs to be is that the base class ends up using it in the call to createElement protected readonly id: string, @@ -122,6 +130,19 @@ export abstract class BaseWebview extends Disposable { this.handleInitialDimension(dimension); })); + this._register(this.on(WebviewMessageChannels.containsScripts, (containsScript: boolean) => { + this.containsScript = containsScript; + })); + + this._register(this.on(WebviewMessageChannels.wheel, (event: IMouseWheelEvent) => { + this._onDidWheel.fire(event); + })); + + this._register(this.on(WebviewMessageChannels.ack, (buf) => { + this.sw!.stop(); + console.log(this._printSpeed(this._convert(2 * 10 * 1024 * 1024, this.sw!.elapsed()))); + })); + this._register(this.on(WebviewMessageChannels.didBlur, () => { this.handleFocusChange(false); })); @@ -137,6 +158,20 @@ export abstract class BaseWebview extends Disposable { this._register(webviewThemeDataProvider.onThemeDataChanged(this.style, this)); } + private _convert(byteCount: number, elapsedMillis: number): number { + return (byteCount * 1000 * 8) / elapsedMillis; + } + + private _printSpeed(n: number): string { + if (n <= 1024) { + return `${n} bps`; + } + if (n < 1024 * 1024) { + return `${(n / 1024).toFixed(1)} kbps`; + } + return `${(n / 1024 / 1024).toFixed(1)} Mbps`; + } + dispose(): void { if (this.element) { this.element.remove(); @@ -158,6 +193,9 @@ export abstract class BaseWebview extends Disposable { private readonly _onDidScroll = this._register(new Emitter<{ readonly scrollYPercentage: number; }>()); public readonly onDidScroll = this._onDidScroll.event; + private readonly _onDidWheel = this._register(new Emitter()); + public readonly onDidWheel= this._onDidWheel.event; + private readonly _onDidUpdateState = this._register(new Emitter()); public readonly onDidUpdateState = this._onDidUpdateState.event; @@ -167,6 +205,8 @@ export abstract class BaseWebview extends Disposable { private readonly _onDidSetInitialDimension = this._register(new Emitter()); public readonly onDidSetInitialDimension = this._onDidSetInitialDimension.event; + public containsScript: boolean = false; + public sendMessage(data: any): void { this._send('message', data); } @@ -221,6 +261,9 @@ export abstract class BaseWebview extends Disposable { state: this.content.state, }; this.doUpdateContent(); + setTimeout(() => { + this.speedTest(); + }, 3000); } public set contentOptions(options: WebviewContentOptions) { @@ -248,6 +291,18 @@ export abstract class BaseWebview extends Disposable { this._send('initial-scroll-position', value); } + public speedTest() { + const SIZE = 10 * 1024 * 1024; // 10MB + let buff = VSBuffer.alloc(SIZE); + let value = Math.random() % 256; + for (let i = 0; i < SIZE; i++) { + buff.writeUInt8(value, i); + } + + this.sw = StopWatch.create(true); + this._send('speedTest', buff.buffer); + } + private doUpdateContent() { this._send('content', { contents: this.content.html, diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index 7ace00968fbeb1d8fbefd669e667cefe85b70bb2..abe5acf3e1ca40dca6089d4b5429965dea65eeee 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -10,11 +10,15 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IWebviewService, Webview, WebviewContentOptions, WebviewEditorOverlay, WebviewElement, WebviewOptions, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { Dimension } from 'vs/base/browser/dom'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; /** * Webview editor overlay that creates and destroys the underlying webview as needed. */ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewEditorOverlay { + private readonly _onDidWheel= this._register(new Emitter()); + public readonly onDidWheel = this._onDidWheel.event; + private readonly _onDidSetInitialDimension = this._register(new Emitter()); public readonly onDidSetInitialDimension = this._onDidSetInitialDimension.event; @@ -31,6 +35,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewEd private _options: WebviewOptions; private _owner: any = undefined; + containsScript: boolean = false; public constructor( private readonly id: string, diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 675d893f438e319315ea843d3f34a9db6248a772..6b87a144a187b8ca6269691652d118b6c91b5ab2 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -60,7 +60,6 @@ font-weight: var(--vscode-font-weight); font-size: var(--vscode-font-size); margin: 0; - padding: 0 20px; height: 100%; width: 100%; } @@ -271,6 +270,21 @@ }; let isHandlingScroll = false; + const handleWheel = (event) => { + if (isHandlingScroll) { + return; + } + + host.postMessage('did-scroll-wheel', { + deltaMode: event.deltaMode, + deltaX: event.deltaX, + deltaY: event.deltaY, + deltaZ: event.deltaZ, + detail: event.detail, + type: event.type + }); + }; + const handleInnerScroll = (event) => { if (!event.target || !event.target.body) { return; @@ -312,6 +326,7 @@ // apply default script if (options.allowScripts) { const defaultScript = newDocument.createElement('script'); + defaultScript.id = '_vscodeApiScript'; defaultScript.textContent = getVsCodeApiScript(data.state); newDocument.head.prepend(defaultScript); } @@ -453,6 +468,23 @@ }, 0); }); + const checkScripts = (document) => { + let scripts = document.getElementsByTagName('script'); + + if (scripts.length > 1) { + return true; + } + + if (scripts[0].id !== '_vscodeApiScript') { + return true; + } + + // no scripts + + let iframes = document.getElementsByTagName('iframe'); + return iframes.length > 0; + }; + const onLoad = (contentDocument, contentWindow) => { if (contentDocument && contentDocument.body) { // Workaround for https://github.com/Microsoft/vscode/issues/12865 @@ -475,8 +507,10 @@ } host.postMessage('did-set-initial-dimension', { width: newFrame.contentWindow.document.body.scrollWidth, height: newFrame.contentWindow.document.body.scrollHeight }); + host.postMessage('content-contains-scripts', checkScripts(newFrame.contentWindow.document)); contentWindow.addEventListener('scroll', handleInnerScroll); + contentWindow.addEventListener('wheel', handleWheel); pendingMessages.forEach((data) => { contentWindow.postMessage(data, '*'); @@ -545,6 +579,9 @@ initData.initialScrollProgress = progress; }); + host.onMessage('speedTest', (_event, buf) => { + host.postMessage('speed-test-ack', buf); + }); trackFocus({ onFocus: () => host.postMessage('did-focus'), diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 83a01cf26d23768e1fe1bd55ddf03695e6fd74f1..7ff03627a5633e31b5880e58f5cab48d22664b0a 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -12,6 +12,7 @@ import * as nls from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; /** * Set when the find widget in a webview is visible. @@ -69,11 +70,13 @@ export interface Webview extends IDisposable { extension: WebviewExtensionDescription | undefined; initialScrollProgress: number; state: string | undefined; + containsScript: boolean; readonly onDidFocus: Event; readonly onDidSetInitialDimension: Event; readonly onDidClickLink: Event; readonly onDidScroll: Event<{ scrollYPercentage: number }>; + readonly onDidWheel: Event; readonly onDidUpdateState: Event; readonly onMessage: Event; readonly onMissingCsp: Event;