/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import * as path from 'vs/base/common/path'; import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { CELL_MARGIN, CELL_RUN_GUTTER } from 'vs/workbench/contrib/notebook/browser/constants'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; export interface IDimensionMessage { __vscode_notebook_message: boolean; type: 'dimension'; id: string; data: DOM.Dimension; } export interface IMouseEnterMessage { __vscode_notebook_message: boolean; type: 'mouseenter'; id: string; } export interface IMouseLeaveMessage { __vscode_notebook_message: boolean; type: 'mouseleave'; id: string; } export interface IWheelMessage { __vscode_notebook_message: boolean; type: 'did-scroll-wheel'; payload: any; } export interface IScrollAckMessage { __vscode_notebook_message: boolean; type: 'scroll-ack'; data: { top: number }; version: number; } export interface IBlurOutputMessage { __vscode_notebook_message: boolean; type: 'focus-editor'; id: string; focusNext?: boolean; } export interface IClearMessage { type: 'clear'; } export interface IFocusOutputMessage { type: 'focus-output'; id: string; } export interface ICreationRequestMessage { type: 'html'; content: string; id: string; outputId: string; top: number; left: number; } export interface IContentWidgetTopRequest { id: string; top: number; left: number; } export interface IViewScrollTopRequestMessage { type: 'view-scroll'; top?: number; widgets: IContentWidgetTopRequest[]; version: number; } export interface IScrollRequestMessage { type: 'scroll'; id: string; top: number; widgetTop?: number; version: number; } export interface IUpdatePreloadResourceMessage { type: 'preload'; resources: string[]; } type IMessage = IDimensionMessage | IScrollAckMessage | IWheelMessage | IMouseEnterMessage | IMouseLeaveMessage | IBlurOutputMessage; let version = 0; export class BackLayerWebView extends Disposable { element: HTMLElement; webview!: WebviewElement; insetMapping: Map = new Map(); hiddenInsetMapping: Set = new Set(); reversedInsetMapping: Map = new Map(); preloadsCache: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; private readonly _onMessage = this._register(new Emitter()); public readonly onMessage: Event = this._onMessage.event; private _initalized: Promise; private activeCellId: string | undefined; constructor( public notebookEditor: INotebookEditor, @IWebviewService readonly webviewService: IWebviewService, @IOpenerService readonly openerService: IOpenerService, @INotebookService private readonly notebookService: INotebookService, @IEnvironmentService private readonly environmentService: IEnvironmentService ) { super(); this.element = document.createElement('div'); this.element.style.width = `calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px)`; this.element.style.height = '1400px'; this.element.style.position = 'absolute'; this.element.style.margin = `0px 0 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px`; const pathsPath = getPathFromAmdModule(require, 'vs/loader.js'); const loader = URI.file(pathsPath).with({ scheme: WebviewResourceScheme }); let coreDependencies = ''; let resolveFunc: () => void; this._initalized = new Promise((resolve, reject) => { resolveFunc = resolve; }); if (!isWeb) { coreDependencies = ``; const htmlContent = this.generateContent(8, coreDependencies); this.initialize(htmlContent); resolveFunc!(); } else { fetch(pathsPath).then(async response => { if (response.status !== 200) { throw new Error(response.statusText); } const loaderJs = await response.text(); coreDependencies = ` `; const htmlContent = this.generateContent(8, coreDependencies); this.initialize(htmlContent); resolveFunc!(); }); } } generateContent(outputNodePadding: number, coreDependencies: string) { return /* html */` ${coreDependencies}
`; } private resolveOutputId(id: string): { cell: CodeCellViewModel, output: IOutput } | undefined { const output = this.reversedInsetMapping.get(id); if (!output) { return; } return { cell: this.insetMapping.get(output)!.cell, output }; } initialize(content: string) { this.webview = this._createInset(this.webviewService, content); this.webview.mountTo(this.element); this.webview.onDidFocus(() => { if (this.activeCellId) { this.webview.sendMessage({ type: 'focus-output', id: this.activeCellId }); } }); this._register(this.webview.onDidClickLink(link => { this.openerService.open(link, { fromUserGesture: true }); })); this._register(this.webview.onMessage((data: IMessage) => { if (data.__vscode_notebook_message) { if (data.type === 'dimension') { let height = data.data.height; let outputHeight = height; const info = this.resolveOutputId(data.id); if (info) { const { cell, output } = info; let outputIndex = cell.outputs.indexOf(output); cell.updateOutputHeight(outputIndex, outputHeight); this.notebookEditor.layoutNotebookCell(cell, cell.layoutInfo.totalHeight); } } else if (data.type === 'mouseenter') { const info = this.resolveOutputId(data.id); if (info) { const { cell } = info; cell.outputIsHovered = true; } } else if (data.type === 'mouseleave') { const info = this.resolveOutputId(data.id); if (info) { const { cell } = info; cell.outputIsHovered = false; } } else if (data.type === 'scroll-ack') { // const date = new Date(); // const top = data.data.top; // console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); } else if (data.type === 'did-scroll-wheel') { this.notebookEditor.triggerScroll({ ...data.payload, preventDefault: () => { }, stopPropagation: () => { } }); } else if (data.type === 'focus-editor') { const info = this.resolveOutputId(data.id); if (info) { if (data.focusNext) { const idx = this.notebookEditor.viewModel?.getCellIndex(info.cell); if (typeof idx !== 'number') { return; } const newCell = this.notebookEditor.viewModel?.viewCells[idx + 1]; if (!newCell) { return; } this.notebookEditor.focusNotebookCell(newCell, true); } else { this.notebookEditor.focusNotebookCell(info.cell, true); } } } return; } this._onMessage.fire(data); })); } async waitForInitialization() { await this._initalized; } private _createInset(webviewService: IWebviewService, content: string) { const rootPath = URI.file(path.dirname(getPathFromAmdModule(require, ''))); this.localResourceRootsCache = [...this.notebookService.getNotebookProviderResourceRoots(), rootPath]; const webview = webviewService.createWebviewElement('' + UUID.generateUuid(), { enableFindWidget: false, }, { allowMultipleAPIAcquire: true, allowScripts: true, localResourceRoots: this.localResourceRootsCache }); webview.html = content; return webview; } shouldUpdateInset(cell: CodeCellViewModel, output: IOutput, cellTop: number) { let outputCache = this.insetMapping.get(output)!; let outputIndex = cell.outputs.indexOf(output); let outputOffset = cellTop + cell.getOutputOffset(outputIndex); if (this.hiddenInsetMapping.has(output)) { return true; } if (outputOffset === outputCache.cacheOffset) { return false; } return true; } updateViewScrollTop(top: number, items: { cell: CodeCellViewModel, output: IOutput, cellTop: number }[]) { let widgets: IContentWidgetTopRequest[] = items.map(item => { let outputCache = this.insetMapping.get(item.output)!; let id = outputCache.outputId; let outputIndex = item.cell.outputs.indexOf(item.output); let outputOffset = item.cellTop + item.cell.getOutputOffset(outputIndex); outputCache.cacheOffset = outputOffset; this.hiddenInsetMapping.delete(item.output); return { id: id, top: outputOffset, left: 0 }; }); let message: IViewScrollTopRequestMessage = { top, type: 'view-scroll', version: version++, widgets: widgets }; this.webview.sendMessage(message); } createInset(cell: CodeCellViewModel, output: IOutput, cellTop: number, offset: number, shadowContent: string, preloads: Set) { this.updateRendererPreloads(preloads); let initialTop = cellTop + offset; if (this.insetMapping.has(output)) { let outputCache = this.insetMapping.get(output); if (outputCache) { this.hiddenInsetMapping.delete(output); this.webview.sendMessage({ type: 'showOutput', id: outputCache.outputId, top: initialTop }); return; } } let outputId = UUID.generateUuid(); let message: ICreationRequestMessage = { type: 'html', content: shadowContent, id: cell.id, outputId: outputId, top: initialTop, left: 0 }; this.webview.sendMessage(message); this.insetMapping.set(output, { outputId: outputId, cell: cell, cacheOffset: initialTop }); this.hiddenInsetMapping.delete(output); this.reversedInsetMapping.set(outputId, output); } removeInset(output: IOutput) { let outputCache = this.insetMapping.get(output); if (!outputCache) { return; } let id = outputCache.outputId; this.webview.sendMessage({ type: 'clearOutput', id: id }); this.insetMapping.delete(output); this.reversedInsetMapping.delete(id); } hideInset(output: IOutput) { let outputCache = this.insetMapping.get(output); if (!outputCache) { return; } let id = outputCache.outputId; this.hiddenInsetMapping.add(output); this.webview.sendMessage({ type: 'hideOutput', id: id }); } clearInsets() { this.webview.sendMessage({ type: 'clear' }); this.insetMapping = new Map(); this.reversedInsetMapping = new Map(); } focusOutput(cellId: string) { this.activeCellId = cellId; this.webview.focus(); } updateRendererPreloads(preloads: Set) { let resources: string[] = []; let extensionLocations: URI[] = []; preloads.forEach(preload => { let rendererInfo = this.notebookService.getRendererInfo(preload); if (rendererInfo) { let preloadResources = rendererInfo.preloads.map(preloadResource => { if (this.environmentService.isExtensionDevelopment && (preloadResource.scheme === 'http' || preloadResource.scheme === 'https')) { return preloadResource; } return preloadResource.with({ scheme: WebviewResourceScheme }); }); extensionLocations.push(rendererInfo.extensionLocation); preloadResources.forEach(e => { if (!this.preloadsCache.has(e.toString())) { resources.push(e.toString()); this.preloadsCache.set(e.toString(), true); } }); } }); this.rendererRootsCache = extensionLocations; const mixedResourceRoots = [...(this.localResourceRootsCache || []), ...this.rendererRootsCache]; this.webview.contentOptions = { allowMultipleAPIAcquire: true, allowScripts: true, enableCommandUris: true, localResourceRoots: mixedResourceRoots }; let message: IUpdatePreloadResourceMessage = { type: 'preload', resources: resources }; this.webview.sendMessage(message); } clearPreloadsCache() { this.preloadsCache.clear(); } }