diff --git a/src/vs/workbench/contrib/positronNotebook/browser/NotebookVisibilityContext.tsx b/src/vs/workbench/contrib/positronNotebook/browser/NotebookVisibilityContext.tsx new file mode 100644 index 00000000000..7c728d4fccd --- /dev/null +++ b/src/vs/workbench/contrib/positronNotebook/browser/NotebookVisibilityContext.tsx @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +// React. +import React, { PropsWithChildren, createContext, useContext } from 'react'; + +// Other dependencies. +import { ISettableObservable } from '../../../../base/common/observableInternal/base.js'; + +/** + * Create the notebook visibility context. + */ +const NotebookVisibilityContext = createContext>(undefined!); + +/** + * Provider component for notebook visibility state + */ +export const NotebookVisibilityProvider = ({ + isVisible, + children +}: PropsWithChildren<{ isVisible: ISettableObservable }>) => { + return ( + + {children} + + ); +}; + +/** + * Hook to access the notebook visibility state + * @returns The current visibility state as a boolean + */ +export const useNotebookVisibility = () => { + return useContext(NotebookVisibilityContext) +}; diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx index b1c71cdd422..08a238f7787 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx @@ -30,11 +30,16 @@ export function PositronNotebookComponent() { const notebookInstance = useNotebookInstance(); const notebookCells = useObservedValue(notebookInstance.cells); const fontStyles = useFontStyles(); + const containerRef = React.useRef(null); + + React.useEffect(() => { + notebookInstance.setCellsContainer(containerRef.current); + }, [notebookInstance]); return (
-
+
{notebookCells?.length ? notebookCells?.map((cell, index) => <> diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx index 4e9d16b7a3f..db2f87011fa 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx @@ -48,6 +48,8 @@ import { NotebookInstanceProvider } from './NotebookInstanceProvider.js'; import { PositronNotebookComponent } from './PositronNotebookComponent.js'; import { ServicesProvider } from './ServicesProvider.js'; import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { PositronNotebookEditorInput } from './PositronNotebookEditorInput.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IWebviewService } from '../../webview/browser/webview.js'; @@ -56,6 +58,7 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IPositronNotebookOutputWebviewService } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; import { IPositronWebviewPreloadService } from '../../../services/positronWebviewPreloads/browser/positronWebviewPreloadService.js'; +import { NotebookVisibilityProvider } from './NotebookVisibilityContext.js'; interface NotebookLayoutInfo { @@ -124,6 +127,8 @@ export class PositronNotebookEditor extends EditorPane { @ICommandService private readonly _commandService: ICommandService, @IOpenerService private readonly _openerService: IOpenerService, @INotificationService private readonly _notificationService: INotificationService, + @IEditorService private readonly _editorService: IEditorService, + @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, ) { // Call the base class's constructor. super( @@ -219,11 +224,22 @@ export class PositronNotebookEditor extends EditorPane { */ private _size = observableValue('size', { width: 0, height: 0 }); + /** + * Observable tracking if the editor is currently visible + */ + private readonly _isVisible = observableValue('isVisible', false); + + // Getter for notebook instance to avoid having to cast the input every time. get notebookInstance() { return (this.input as PositronNotebookEditorInput)?.notebookInstance; } + protected override setEditorVisible(visible: boolean): void { + this._isVisible.set(visible, undefined); + super.setEditorVisible(visible); + } + protected override createEditor(parent: HTMLElement): void { this._logService.info(this._identifier, 'createEditor'); @@ -315,14 +331,19 @@ export class PositronNotebookEditor extends EditorPane { override clearInput(): void { this._logService.info(this._identifier, 'clearInput'); - - // Clear the input observable. - this._input = undefined; + if (this.notebookInstance && this._parentDiv) { + this.notebookInstance.detachView(); + console.log('isVisible', this._isVisible.get()); + } if (this.notebookInstance) { this._saveEditorViewState(); this.notebookInstance.detachView(); } + + // Clear the input observable. + this._input = undefined; + this._disposeReactRenderer(); // Call the base class's method. @@ -444,24 +465,28 @@ export class PositronNotebookEditor extends EditorPane { const reactRenderer: PositronReactRenderer = this._positronReactRenderer ?? new PositronReactRenderer(this._parentDiv); reactRenderer.render( - - scopedContextKeyService.createScoped(container) - }}> - - - + + + scopedContextKeyService.createScoped(container), + editorService: this._editorService, + layoutService: this._layoutService + }}> + + + + ); } diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts index 4052e3044c3..10b6727ba11 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { ISettableObservable, observableValue } from '../../../../base/common/observableInternal/base.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; @@ -109,6 +109,16 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot */ private _container: HTMLElement | undefined = undefined; + /** + * The DOM element that contains the cells for the notebook. + */ + private _cellsContainer: HTMLElement | undefined = undefined; + + /** + * Disposables for the current cells container event listeners + */ + private readonly _cellsContainerListeners = this._register(new DisposableStore()); + /** * Callback to clear the keyboard navigation listeners. Set when listeners are attached. */ @@ -159,6 +169,12 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; + /** + * Event emitter for when the cells container is scrolled + */ + private readonly _onDidScrollCellsContainer = this._register(new Emitter()); + readonly onDidScrollCellsContainer = this._onDidScrollCellsContainer.event; + // ============================================================================================= // #region Public Properties @@ -167,6 +183,59 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot */ private _id: string; + /** + * The DOM element that contains the cells for the notebook. + */ + get cellsContainer(): HTMLElement | undefined { + return this._cellsContainer; + } + + /** + * Sets the DOM element that contains the cells for the notebook. + * @param container The container element to set, or undefined to clear + */ + setCellsContainer(container: HTMLElement | undefined | null): void { + // Clean up any existing listeners + this._cellsContainerListeners.clear(); + + if (!container) { return; } + + this._cellsContainer = container; + + // Fire initial scroll event after a small delay to ensure layout has settled + const initialScrollTimeout = setTimeout(() => { + this._onDidScrollCellsContainer.fire(); + }, 50); + + // Set up scroll listener + const scrollListener = DOM.addDisposableListener(container, 'scroll', () => { + this._onDidScrollCellsContainer.fire(); + }); + + // Set up mutation observer to watch for DOM changes + const observer = new MutationObserver(() => { + // Small delay to let the DOM changes settle + setTimeout(() => { + this._onDidScrollCellsContainer.fire(); + }, 0); + }); + + observer.observe(container, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['style', 'class'] + }); + + // Add all the disposables to our store + this._cellsContainerListeners.add(toDisposable(() => clearTimeout(initialScrollTimeout))); + this._cellsContainerListeners.add(scrollListener); + this._cellsContainerListeners.add(toDisposable(() => observer.disconnect())); + + // Fire initial scroll event + this._onDidScrollCellsContainer.fire(); + } + /** * User facing cells wrapped in an observerable for the UI to react to changes */ diff --git a/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx b/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx index 537dfe4bc94..493dd5c100f 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx @@ -17,6 +17,8 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { IPositronNotebookOutputWebviewService } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; import { IWebviewService } from '../../webview/browser/webview.js'; import { IPositronWebviewPreloadService } from '../../../services/positronWebviewPreloads/browser/positronWebviewPreloadService.js'; @@ -83,6 +85,16 @@ interface ServiceBundle { */ scopedContextKeyProviderCallback: (container: HTMLElement) => IScopedContextKeyService; + /** + * Service for managing active editors and editor state + */ + editorService: IEditorService; + + /** + * Service for managing workbench layout and panel positions + */ + layoutService: IWorkbenchLayoutService; + } /** @@ -110,5 +122,3 @@ export function useServices() { } return serviceBundle; } - - diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts index ce5226300a9..be041c85c4f 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -4,66 +4,268 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; -import { getWindow } from '../../../../../../base/browser/dom.js'; +import { getWindow, addDisposableListener } from '../../../../../../base/browser/dom.js'; import { INotebookOutputWebview } from '../../../../positronOutputWebview/browser/notebookOutputWebviewService.js'; -import { IWebviewElement } from '../../../../webview/browser/webview.js'; -import { assertIsStandardPositronWebview } from '../../../../positronOutputWebview/browser/notebookOutputWebviewServiceImpl.js'; import { isHTMLOutputWebviewMessage } from '../../../../positronWebviewPreloads/browser/notebookOutputUtils.js'; +import { useNotebookInstance } from '../../NotebookInstanceProvider.js'; +import { useServices } from '../../ServicesProvider.js'; +import { IOverlayWebview } from '../../../../webview/browser/webview.js'; +import { DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { useNotebookVisibility } from '../../NotebookVisibilityContext.js'; +import { Event } from '../../../../../../base/common/event.js'; +// Constants +const MAX_OUTPUT_HEIGHT = 1000; +const EMPTY_OUTPUT_HEIGHT = 150; +/** + * Custom error class for webview-specific errors + */ +export class WebviewMountError extends Error { + constructor(message: string, public override readonly cause?: Error) { + super(message); + this.name = 'WebviewMountError'; + } +} + +/** + * A custom React hook that mounts and manages a notebook output webview. It: + * 1. Claims and releases the webview on visibility changes + * 2. Sets up layout, scroll, and blur listeners to position the webview + * 3. Cleans up listeners and disposables on unmount + * + * @param webview A promise resolving to an INotebookOutputWebview + * @returns An object with a containerRef for rendering, a loading state, and an error + * + * @example + * const { containerRef, isLoading, error } = useWebviewMount(myWebview); + * + * @throws {WebviewMountError} When the webview fails to mount or during layout operations + */ export function useWebviewMount(webview: Promise) { - const [isLoading, setIsLoading] = React.useState(true); - const [error, setError] = React.useState(null); + // State tracking: loading or error + const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(null); + + // References to the container DOM element const containerRef = React.useRef(null); + // Retrieve relevant context + const notebookInstance = useNotebookInstance(); + const visibilityObservable = useNotebookVisibility(); + const { editorService, layoutService } = useServices(); + + // Memoize the webview message handler + const handleWebviewMessage = React.useCallback(({ message }: { message: unknown }) => { + if (!isHTMLOutputWebviewMessage(message) || !containerRef.current) { + return; + } + let boundedHeight = Math.min(message.bodyScrollHeight, MAX_OUTPUT_HEIGHT); + // Avoid undesired default 150px "empty output" height + if (boundedHeight === EMPTY_OUTPUT_HEIGHT) { + boundedHeight = 0; + } + containerRef.current.style.height = `${boundedHeight}px`; + }, []); + React.useEffect(() => { + // Abort controller for canceling ongoing tasks if needed const controller = new AbortController(); - let webviewElement: IWebviewElement | undefined; + // Webview references + let webviewElement: IOverlayWebview | undefined; + + // Create a disposable store to manage all disposables + const disposables = new DisposableStore(); + let resizeObserver: ResizeObserver | undefined; + + /** + * Manages layout calls for the webview using requestAnimationFrame for better performance + * + * @param immediate If true, layout occurs in the current frame + */ + let layoutFrame: number | undefined; + function updateWebviewLayout(immediate = false): void { + if (!webviewElement || !containerRef.current) { + return; + } + + // Clear any pending layout update + if (layoutFrame !== undefined) { + window.cancelAnimationFrame(layoutFrame); + layoutFrame = undefined; + } + + const doLayout = () => { + try { + if (!containerRef.current || !notebookInstance.cellsContainer) { + return; + } + webviewElement?.layoutWebviewOverElement( + containerRef.current, + undefined, + notebookInstance.cellsContainer + ); + } catch (err) { + setError(new WebviewMountError('Failed to layout webview', err instanceof Error ? err : undefined)); + } + }; + + if (immediate) { + doLayout(); + } else { + layoutFrame = window.requestAnimationFrame(doLayout); + disposables.add(toDisposable(() => { + if (layoutFrame !== undefined) { + window.cancelAnimationFrame(layoutFrame); + layoutFrame = undefined; + } + })); + } + } + + /** + * Claims the webview, instructing it to position itself over our container. + */ + function claimWebview(): void { + if (!webviewElement || !containerRef.current) { + return; + } + try { + // We're using the base ref here because it's a constant reference and thus + // will avoid unnecessary mismatches for claiming and releasing the webview + // across multiple renders. + webviewElement.claim(containerRef, getWindow(containerRef.current), undefined); + } catch (err) { + setError(new WebviewMountError('Failed to claim webview', err instanceof Error ? err : undefined)); + } + } + + /** + * Releases the webview, e.g., on hidden state or unmount. + */ + function releaseWebview(): void { + try { + webviewElement?.release(containerRef); + } catch (err) { + setError(new WebviewMountError('Failed to release webview', err instanceof Error ? err : undefined)); + } + } + + /** + * Asynchronously mounts the webview if visible. + * Sets up listeners for resizing, scrolling, focus changes, etc. + */ async function mountWebview() { + const emptyDisposable = toDisposable(() => { /* no-op */ }); + try { - const resolvedWebview = await webview; + // If not visible, don't mount the webview + if (!visibilityObservable) { + return emptyDisposable; + } + // Wait for the INotebookOutputWebview instance + const resolvedWebview = await webview; if (controller.signal.aborted || !containerRef.current) { - return; + return emptyDisposable; } setIsLoading(false); - assertIsStandardPositronWebview(resolvedWebview); webviewElement = resolvedWebview.webview; - webviewElement.mountTo( - containerRef.current, - getWindow(containerRef.current) - ); - webviewElement.onMessage((x) => { - const { message } = x; - if (!isHTMLOutputWebviewMessage(message) || !containerRef.current) { return; } - // Set the height of the webview to the height of the content - // Don't allow the webview to be taller than 1000px - const maxHeight = 1000; - let boundedHeight = Math.min(message.bodyScrollHeight, maxHeight); - if (boundedHeight === 150) { - // 150 is a default size that we want to avoid, otherwise we'll get - // empty outputs that are 150px tall - boundedHeight = 0; - } - containerRef.current.style.height = `${boundedHeight}px`; + // Position it initially + claimWebview(); + updateWebviewLayout(); + + // Scroll listener: reposition the webview if the notebook container scrolls + disposables.add(notebookInstance.onDidScrollCellsContainer(() => + updateWebviewLayout(true) + )); + + // When focus leaves the notebook container, update layout to ensure correct size + if (notebookInstance.cellsContainer) { + disposables.add(addDisposableListener( + notebookInstance.cellsContainer, + 'focusout', + (e) => { + if ( + notebookInstance.cellsContainer && + !notebookInstance.cellsContainer.contains(e.relatedTarget as Node) + ) { + updateWebviewLayout(true); + } + } + )); + } + + // Listen for messages from the webview; adjust container height if needed + disposables.add(toDisposable(() => webviewElement!.onMessage(handleWebviewMessage).dispose())); + + // React to editor or layout changes + const handleLayoutChange = () => updateWebviewLayout(false); + disposables.add(Event.any( + editorService.onDidActiveEditorChange, + editorService.onDidVisibleEditorsChange, + layoutService.onDidLayoutMainContainer, + layoutService.onDidLayoutContainer, + layoutService.onDidLayoutActiveContainer, + layoutService.onDidChangePartVisibility, + layoutService.onDidChangeWindowMaximized, + layoutService.onDidChangePanelAlignment, + layoutService.onDidChangePanelPosition, + layoutService.onDidChangeMainEditorCenteredLayout + )(handleLayoutChange)); + + // Watch for container resize + resizeObserver = new ResizeObserver(() => { + updateWebviewLayout(true); }); + if (notebookInstance.cellsContainer) { + resizeObserver.observe(notebookInstance.cellsContainer); + disposables.add(toDisposable(() => resizeObserver?.disconnect())); + } + return emptyDisposable; } catch (err) { - setError(err instanceof Error ? err : new Error('Failed to mount webview')); + const mountError = new WebviewMountError( + 'Failed to mount webview', + err instanceof Error ? err : undefined + ); + setError(mountError); setIsLoading(false); + return emptyDisposable; } } + // Listen for changes in visibility, claiming or releasing the webview + if (visibilityObservable) { + disposables.add( + Event.fromObservable(visibilityObservable)((isVisible) => { + if (isVisible) { + claimWebview(); + } else { + releaseWebview(); + } + }) + ); + } + + // Actually start the mounting process mountWebview(); + // Cleanup callback: abort tasks, release the webview, and dispose of all listeners return () => { controller.abort(); - webviewElement?.dispose(); + releaseWebview(); + disposables.dispose(); }; - }, [webview]); + }, [webview, notebookInstance, visibilityObservable, handleWebviewMessage]); - return { containerRef, isLoading, error }; + // Return the container reference plus loading/error states + return { + containerRef, + isLoading, + error + }; } diff --git a/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebview.ts b/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebview.ts index f7de1550488..cc6f170b184 100644 --- a/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebview.ts +++ b/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebview.ts @@ -16,15 +16,14 @@ import { INotificationService } from '../../../../platform/notification/common/n import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { FromWebviewMessage, IClickedDataUrlMessage } from '../../notebook/browser/view/renderers/webviewMessages.js'; import { IScopedRendererMessaging } from '../../notebook/common/notebookRendererMessagingService.js'; -import { INotebookOutputWebview, WebviewType } from './notebookOutputWebviewService.js'; -import { IOverlayWebview, IWebviewElement } from '../../webview/browser/webview.js'; +import { INotebookOutputWebview } from './notebookOutputWebviewService.js'; +import { IOverlayWebview } from '../../webview/browser/webview.js'; import { INotebookLoggingService } from '../../notebook/common/notebookLoggingService.js'; interface NotebookOutputWebviewOptions { readonly id: string; readonly sessionId: string; - readonly webview: IOverlayWebview | IWebviewElement; - webviewType: WebviewType; + readonly webview: IOverlayWebview; rendererMessaging?: IScopedRendererMessaging; } @@ -40,8 +39,7 @@ export class NotebookOutputWebview extends Disposable implements INotebookOutput readonly id: string; readonly sessionId: string; - readonly webview: IOverlayWebview | IWebviewElement; - readonly webviewType: WebviewType; + readonly webview: IOverlayWebview; /** * Fired when the webviewPreloads script is loaded. @@ -68,7 +66,6 @@ export class NotebookOutputWebview extends Disposable implements INotebookOutput sessionId, webview, rendererMessaging, - webviewType }: NotebookOutputWebviewOptions, @IFileDialogService private _fileDialogService: IFileDialogService, @IFileService private _fileService: IFileService, @@ -85,7 +82,6 @@ export class NotebookOutputWebview extends Disposable implements INotebookOutput this.id = id; this.sessionId = sessionId; this.webview = webview; - this.webviewType = webviewType; if (rendererMessaging) { this._register(rendererMessaging); diff --git a/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebviewService.ts b/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebviewService.ts index d5b0d706912..519dcc48ee1 100644 --- a/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebviewService.ts +++ b/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebviewService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IOverlayWebview, IWebviewElement } from '../../webview/browser/webview.js'; +import { IOverlayWebview } from '../../webview/browser/webview.js'; import { ILanguageRuntimeMessageOutput, ILanguageRuntimeMessageWebOutput } from '../../../services/languageRuntime/common/languageRuntimeService.js'; import { ILanguageRuntimeSession } from '../../../services/runtimeSession/common/runtimeSessionService.js'; import { Event } from '../../../../base/common/event.js'; @@ -24,30 +24,12 @@ export interface INotebookOutputWebview extends IDisposable { sessionId: string; /** The webview containing the output's content */ - webview: IOverlayWebview | IWebviewElement; - - /** The type of webview, used for later type assertions */ - webviewType: WebviewType; + webview: IOverlayWebview; /** Fired when the content completes rendering */ onDidRender: Event; } -export interface INotebookOutputOverlayWebview extends INotebookOutputWebview { - webview: IOverlayWebview; - webviewType: WebviewType.Overlay; -} - -export interface INotebookOutputStandardWebview extends INotebookOutputWebview { - webview: IWebviewElement; - webviewType: WebviewType.Standard; -} - -export enum WebviewType { - Overlay = 'overlay', - Standard = 'standard' -} - export interface IPositronNotebookOutputWebviewService { // Required for dependency injection @@ -71,7 +53,6 @@ export interface IPositronNotebookOutputWebviewService { runtime: ILanguageRuntimeSession; output: ILanguageRuntimeMessageOutput; viewType?: string; - webviewType: WebviewType; } ): Promise; @@ -94,7 +75,6 @@ export interface IPositronNotebookOutputWebviewService { preReqMessages: ILanguageRuntimeMessageWebOutput[]; displayMessage: ILanguageRuntimeMessageWebOutput; viewType?: string; - webviewType: WebviewType; }): Promise; /** @@ -111,7 +91,6 @@ export interface IPositronNotebookOutputWebviewService { id: string; html: string; runtimeOrSessionId: ILanguageRuntimeSession | string; - webviewType: WebviewType; }): Promise; } diff --git a/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebviewServiceImpl.ts b/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebviewServiceImpl.ts index f6f425cce4e..07eade98be3 100644 --- a/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebviewServiceImpl.ts +++ b/src/vs/workbench/contrib/positronOutputWebview/browser/notebookOutputWebviewServiceImpl.ts @@ -13,7 +13,7 @@ import { preloadsScriptStr } from '../../notebook/browser/view/renderers/webview import { INotebookRendererInfo, RENDERER_NOT_AVAILABLE, RendererMessagingSpec } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; import { NotebookOutputWebview } from './notebookOutputWebview.js'; -import { INotebookOutputOverlayWebview, INotebookOutputStandardWebview, INotebookOutputWebview, IPositronNotebookOutputWebviewService, WebviewType } from './notebookOutputWebviewService.js'; +import { INotebookOutputWebview, IPositronNotebookOutputWebviewService } from './notebookOutputWebviewService.js'; import { IWebviewService, WebviewInitInfo } from '../../webview/browser/webview.js'; import { asWebviewUri } from '../../webview/common/webview.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; @@ -24,8 +24,6 @@ import { INotebookRendererMessagingService } from '../../notebook/common/noteboo import { ILogService } from '../../../../platform/log/common/log.js'; import { handleWebviewLinkClicksInjection } from './downloadUtils.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { OverlayWebview } from '../../webview/browser/overlayWebview.js'; -import { WebviewElement } from '../../webview/browser/webviewElement.js'; import { webviewMessageCodeString } from '../../positronWebviewPreloads/browser/notebookOutputUtils.js'; /** @@ -37,41 +35,6 @@ type MessageRenderInfo = { output: ILanguageRuntimeMessageWebOutput; }; -/** - * Type assertion function to verify that a webview is an OverlayWebview. - * @param webview The webview to check - * @throws {Error} If the webview is not an OverlayWebview - */ -function assertIsOverlayWebview(webview: unknown): asserts webview is OverlayWebview { - if (!(webview instanceof OverlayWebview)) { - throw new Error('Expected webview to be an overlay webview'); - } -} - -/** - * Type assertion function to verify that a webview is a standard WebviewElement. - * @param webview The webview to check - * @throws {Error} If the webview is not a WebviewElement - */ -function assertIsStandardWebview(webview: unknown): asserts webview is WebviewElement { - if (!(webview instanceof WebviewElement)) { - throw new Error('Expected webview to be a standard webview'); - } -} - -/** - * Assert that a webview is an overlay webview. Relies on the webviewType property. - */ -export function assertIsOverlayPositronWebview(notebookWebview: INotebookOutputWebview): asserts notebookWebview is INotebookOutputOverlayWebview { - assertIsOverlayWebview(notebookWebview.webview); -} - -/** - * Assert that a webview is a standard webview. Relies on the webviewType property. - */ -export function assertIsStandardPositronWebview(notebookWebview: INotebookOutputWebview): asserts notebookWebview is INotebookOutputStandardWebview { - assertIsStandardWebview(notebookWebview.webview); -} export class PositronNotebookOutputWebviewService implements IPositronNotebookOutputWebviewService { @@ -144,14 +107,12 @@ export class PositronNotebookOutputWebviewService implements IPositronNotebookOu runtimeId, preReqMessages, displayMessage, - viewType, - webviewType + viewType }: { runtimeId: string; preReqMessages: ILanguageRuntimeMessageWebOutput[]; displayMessage: ILanguageRuntimeMessageWebOutput; viewType?: string; - webviewType: WebviewType; }): Promise { const displayInfo = this._findRendererForOutput(displayMessage); @@ -169,17 +130,15 @@ export class PositronNotebookOutputWebviewService implements IPositronNotebookOu displayMessageInfo: displayInfo, preReqMessagesInfo: this._findRenderersForOutputs(preReqMessages), viewType, - webviewType, }); } async createNotebookOutputWebview( - { id, runtime, output, viewType, webviewType }: + { id, runtime, output, viewType }: { id: string; runtime: ILanguageRuntimeSession; output: ILanguageRuntimeMessageWebOutput; - webviewType: WebviewType; viewType?: string; } ): Promise { @@ -200,7 +159,6 @@ export class PositronNotebookOutputWebviewService implements IPositronNotebookOu runtimeId: runtime.sessionId, displayMessageInfo: { mimeType, renderer, output }, viewType, - webviewType, }); } } @@ -213,7 +171,6 @@ export class PositronNotebookOutputWebviewService implements IPositronNotebookOu id, runtimeOrSessionId: runtime, html: output.data[mimeType], - webviewType }); } } @@ -311,15 +268,13 @@ export class PositronNotebookOutputWebviewService implements IPositronNotebookOu runtimeId, displayMessageInfo, preReqMessagesInfo, - viewType, - webviewType + viewType }: { id: string; runtimeId: string; displayMessageInfo: MessageRenderInfo; preReqMessagesInfo?: MessageRenderInfo[]; viewType?: string; - webviewType: WebviewType; }): Promise { // Make message info into an array if it isn't already @@ -372,9 +327,7 @@ export class PositronNotebookOutputWebviewService implements IPositronNotebookOu }; // Create the webview itself - const webview = webviewType === WebviewType.Overlay - ? this._webviewService.createWebviewOverlay(webviewInitInfo) - : this._webviewService.createWebviewElement(webviewInitInfo); + const webview = this._webviewService.createWebviewOverlay(webviewInitInfo); // Form the HTML to send to the webview. Currently, this is a very simplified version // of the HTML that the notebook renderer API creates, but it works for many renderers. @@ -409,7 +362,6 @@ export class PositronNotebookOutputWebviewService implements IPositronNotebookOu id, sessionId: runtimeId, webview, - webviewType, rendererMessaging: scopedRendererMessaging }, ); @@ -442,11 +394,10 @@ export class PositronNotebookOutputWebviewService implements IPositronNotebookOu return notebookOutputWebview; } - async createRawHtmlOutput({ id, html, runtimeOrSessionId, webviewType }: { + async createRawHtmlOutput({ id, html, runtimeOrSessionId }: { id: string; html: string; runtimeOrSessionId: ILanguageRuntimeSession | string; - webviewType: WebviewType; }): Promise { // Load the Jupyter extension. Many notebook HTML outputs have a dependency on jQuery, @@ -472,9 +423,7 @@ export class PositronNotebookOutputWebviewService implements IPositronNotebookOu extension: typeof runtimeOrSessionId === 'string' ? undefined : { id: runtimeOrSessionId.runtimeMetadata.extensionId } }; - const webview = webviewType === WebviewType.Overlay - ? this._webviewService.createWebviewOverlay(webviewInitInfo) - : this._webviewService.createWebviewElement(webviewInitInfo); + const webview = this._webviewService.createWebviewOverlay(webviewInitInfo); // Form the path to the jQuery library and inject it into the HTML const jQueryPath = asWebviewUri( @@ -504,8 +453,7 @@ window.onload = function() { { id, sessionId: typeof runtimeOrSessionId === 'string' ? runtimeOrSessionId : runtimeOrSessionId.sessionId, - webview, - webviewType + webview } ); } diff --git a/src/vs/workbench/contrib/positronPlots/browser/notebookMultiMessagePlotClient.ts b/src/vs/workbench/contrib/positronPlots/browser/notebookMultiMessagePlotClient.ts index c257139918b..334f17e4ac2 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/notebookMultiMessagePlotClient.ts +++ b/src/vs/workbench/contrib/positronPlots/browser/notebookMultiMessagePlotClient.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { INotebookOutputWebview, IPositronNotebookOutputWebviewService, WebviewType } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; -import { assertIsOverlayPositronWebview } from '../../positronOutputWebview/browser/notebookOutputWebviewServiceImpl.js'; +import { INotebookOutputWebview, IPositronNotebookOutputWebviewService } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; import { WebviewPlotClient } from './webviewPlotClient.js'; import { ILanguageRuntimeMessageWebOutput } from '../../../services/languageRuntime/common/languageRuntimeService.js'; import { ILanguageRuntimeSession } from '../../../services/runtimeSession/common/runtimeSessionService.js'; @@ -56,13 +55,11 @@ export class NotebookMultiMessagePlotClient extends WebviewPlotClient { runtimeId: this._session.sessionId, preReqMessages: this._preReqMessages, displayMessage: this._displayMessage, - viewType: 'jupyter-notebook', - webviewType: WebviewType.Overlay + viewType: 'jupyter-notebook' }); if (!output) { throw new Error('Failed to create notebook output webview'); } - assertIsOverlayPositronWebview(output); this._output.value = output; // Wait for the webview to finish rendering. When it does, nudge the diff --git a/src/vs/workbench/contrib/positronPlots/browser/notebookOutputPlotClient.ts b/src/vs/workbench/contrib/positronPlots/browser/notebookOutputPlotClient.ts index 7ec92f52843..8453811d1e5 100644 --- a/src/vs/workbench/contrib/positronPlots/browser/notebookOutputPlotClient.ts +++ b/src/vs/workbench/contrib/positronPlots/browser/notebookOutputPlotClient.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { INotebookOutputWebview, IPositronNotebookOutputWebviewService, WebviewType } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; -import { assertIsOverlayPositronWebview } from '../../positronOutputWebview/browser/notebookOutputWebviewServiceImpl.js'; +import { INotebookOutputWebview, IPositronNotebookOutputWebviewService } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; import { WebviewPlotClient } from './webviewPlotClient.js'; import { ILanguageRuntimeMessageOutput } from '../../../services/languageRuntime/common/languageRuntimeService.js'; import { ILanguageRuntimeSession } from '../../../services/runtimeSession/common/runtimeSessionService.js'; @@ -53,14 +52,12 @@ export class NotebookOutputPlotClient extends WebviewPlotClient { id: this.id, runtime: this._session, output: this._message, - viewType: 'jupyter-notebook', - webviewType: WebviewType.Overlay, + viewType: 'jupyter-notebook' }); if (!output) { throw new Error('Failed to create notebook output webview'); } - assertIsOverlayPositronWebview(output); this._output.value = output; // Wait for the webview to finish rendering. When it does, nudge the // timer that renders the thumbnail. diff --git a/src/vs/workbench/contrib/positronPreview/browser/positronPreviewServiceImpl.ts b/src/vs/workbench/contrib/positronPreview/browser/positronPreviewServiceImpl.ts index 30533f8cc2c..cd3de60486a 100644 --- a/src/vs/workbench/contrib/positronPreview/browser/positronPreviewServiceImpl.ts +++ b/src/vs/workbench/contrib/positronPreview/browser/positronPreviewServiceImpl.ts @@ -13,7 +13,7 @@ import { IViewsService } from '../../../services/views/common/viewsService.js'; import { POSITRON_PREVIEW_HTML_VIEW_TYPE, POSITRON_PREVIEW_URL_VIEW_TYPE, POSITRON_PREVIEW_VIEW_ID } from './positronPreviewSevice.js'; import { ILanguageRuntimeMessageOutput, LanguageRuntimeSessionMode, RuntimeOutputKind } from '../../../services/languageRuntime/common/languageRuntimeService.js'; import { ILanguageRuntimeSession, IRuntimeSessionService } from '../../../services/runtimeSession/common/runtimeSessionService.js'; -import { IPositronNotebookOutputWebviewService, WebviewType } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; +import { IPositronNotebookOutputWebviewService } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; import { URI } from '../../../../base/common/uri.js'; import { PreviewUrl } from './previewUrl.js'; import { ShowHtmlFileEvent, ShowUrlEvent, UiFrontendEvent } from '../../../services/languageRuntime/common/positronUiComm.js'; @@ -28,7 +28,6 @@ import { basename } from '../../../../base/common/path.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { Schemas } from '../../../../base/common/network.js'; -import { assertIsOverlayPositronWebview } from '../../positronOutputWebview/browser/notebookOutputWebviewServiceImpl.js'; /** * Positron preview service; keeps track of the set of active previews and @@ -404,11 +403,9 @@ export class PositronPreviewService extends Disposable implements IPositronPrevi this._notebookOutputWebviewService.createNotebookOutputWebview({ id: e.id, runtime: session, - output: e, - webviewType: WebviewType.Overlay + output: e }); if (webview) { - assertIsOverlayPositronWebview(webview); const overlay = this.createOverlayWebview(webview.webview); const preview = new PreviewWebview( 'notebookRenderer', diff --git a/src/vs/workbench/contrib/positronWebviewPreloads/browser/positronWebviewPreloadsService.ts b/src/vs/workbench/contrib/positronWebviewPreloads/browser/positronWebviewPreloadsService.ts index 86ddd7af522..8398611a221 100644 --- a/src/vs/workbench/contrib/positronWebviewPreloads/browser/positronWebviewPreloadsService.ts +++ b/src/vs/workbench/contrib/positronWebviewPreloads/browser/positronWebviewPreloadsService.ts @@ -9,7 +9,7 @@ import { ILanguageRuntimeMessageOutput, ILanguageRuntimeMessageWebOutput, Langua import { IPositronWebviewPreloadService, NotebookPreloadOutputResults } from '../../../services/positronWebviewPreloads/browser/positronWebviewPreloadService.js'; import { ILanguageRuntimeSession, IRuntimeSessionService } from '../../../services/runtimeSession/common/runtimeSessionService.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IPositronNotebookOutputWebviewService, WebviewType, INotebookOutputWebview } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; +import { IPositronNotebookOutputWebviewService, INotebookOutputWebview } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; import { NotebookMultiMessagePlotClient } from '../../positronPlots/browser/notebookMultiMessagePlotClient.js'; import { UiFrontendEvent } from '../../../services/languageRuntime/common/positronUiComm.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; @@ -240,8 +240,7 @@ export class PositronWebviewPreloadService extends Disposable implements IPositr html: buildWebviewHTML({ content: htmlOutput.data.toString(), script: webviewMessageCodeString, - }), - webviewType: WebviewType.Standard + }) }); return notebookWebview; @@ -268,8 +267,7 @@ export class PositronWebviewPreloadService extends Disposable implements IPositr runtimeId: instance.id, preReqMessages: storedMessages, displayMessage: displayMessage, - viewType: 'jupyter-notebook', - webviewType: WebviewType.Standard + viewType: 'jupyter-notebook' }); // Assert that we have a webview diff --git a/src/vs/workbench/services/positronNotebook/browser/IPositronNotebookInstance.ts b/src/vs/workbench/services/positronNotebook/browser/IPositronNotebookInstance.ts index 12fd52cc00d..0c611fe6233 100644 --- a/src/vs/workbench/services/positronNotebook/browser/IPositronNotebookInstance.ts +++ b/src/vs/workbench/services/positronNotebook/browser/IPositronNotebookInstance.ts @@ -8,7 +8,7 @@ import { URI } from '../../../../base/common/uri.js'; import { CellKind, IPositronNotebookCell } from './IPositronNotebookCell.js'; import { SelectionStateMachine } from './selectionMachine.js'; import { ILanguageRuntimeSession } from '../../runtimeSession/common/runtimeSessionService.js'; - +import { Event } from '../../../../base/common/event.js'; /** * Represents the possible states of a notebook's kernel connection */ @@ -56,6 +56,18 @@ export interface IPositronNotebookInstance { */ readonly connectedToEditor: boolean; + /** + * The DOM element that contains the cells for the notebook. + * This is set when the cells container is mounted in the React component. + */ + readonly cellsContainer: HTMLElement | undefined; + + /** + * Sets the DOM element that contains the cells for the notebook. + * @param container The container element to set, or undefined to clear + */ + setCellsContainer(container: HTMLElement | undefined): void; + /** * Observable array of cells that make up the notebook. Changes to this array * will trigger UI updates in connected views. @@ -86,6 +98,11 @@ export interface IPositronNotebookInstance { */ isDisposed: boolean; + /** + * Event that fires when the cells container is scrolled + */ + readonly onDidScrollCellsContainer: Event; + // ===== Methods ===== /** * Executes the specified cells in order.