From bcb3e9e76cc45ecf0d05394fc7a5c13b0dcfcd39 Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Fri, 13 Dec 2024 11:35:42 -0500 Subject: [PATCH 01/12] Switch to overlay webviews for outputs --- .../browser/PositronNotebookComponent.tsx | 52 ++++++++++++++++++- .../browser/PositronNotebookInstance.ts | 14 +++++ .../notebookCells/PreloadMessageOutput.tsx | 10 +++- .../notebookCells/hooks/useWebviewMount.ts | 47 +++++++++++++---- .../browser/positronWebviewPreloadsService.ts | 2 +- .../browser/IPositronNotebookInstance.ts | 7 ++- 6 files changed, 118 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx index b1c71cdd422..4fd509a42bc 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx @@ -30,11 +30,14 @@ export function PositronNotebookComponent() { const notebookInstance = useNotebookInstance(); const notebookCells = useObservedValue(notebookInstance.cells); const fontStyles = useFontStyles(); + const containerRef = React.useRef(null); + + useScrollContainerEvents(containerRef); return (
-
+
{notebookCells?.length ? notebookCells?.map((cell, index) => <> @@ -62,6 +65,53 @@ function useFontStyles(): React.CSSProperties { } as React.CSSProperties; } +/** + * Hook to manage scroll and DOM mutation events for the notebook cells container + */ +function useScrollContainerEvents( + containerRef: React.RefObject, +) { + const notebookInstance = useNotebookInstance(); + + React.useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + + // Fire initial scroll event after a small delay to ensure layout has settled + const initialScrollTimeout = setTimeout(() => { + notebookInstance.fireOnDidScrollCellsContainer(); + }, 50); + + // Set up scroll listener + const scrollListener = DOM.addDisposableListener(container, 'scroll', () => { + notebookInstance.fireOnDidScrollCellsContainer(); + }); + + // Set up mutation observer to watch for DOM changes + const observer = new MutationObserver(() => { + // Small delay to let the DOM changes settle + setTimeout(() => { + notebookInstance.fireOnDidScrollCellsContainer(); + }, 0); + }); + + observer.observe(container, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['style', 'class'] + }); + + return () => { + clearTimeout(initialScrollTimeout); + scrollListener.dispose(); + observer.disconnect(); + }; + }, [notebookInstance]); +} + function NotebookCell({ cell }: { cell: PositronNotebookCellGeneral; }) { diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts index 4052e3044c3..27f449d37e9 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts @@ -159,6 +159,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 @@ -491,6 +497,14 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot this._logService.info(this.id, 'attachView'); } + /** + * Manually fires the scroll event for the cells container. + * This can be called from React components after the container is mounted. + */ + fireOnDidScrollCellsContainer(): void { + this._onDidScrollCellsContainer.fire(); + } + /** * Gets the base cell editor options for the given language. * If they don't exist yet, they will be created. diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/PreloadMessageOutput.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/PreloadMessageOutput.tsx index c5d649c31f7..bbbd21dc3a0 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/PreloadMessageOutput.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/PreloadMessageOutput.tsx @@ -42,7 +42,7 @@ export function PreloadMessageOutput({ preloadMessageResult }: PreloadMessageOut } function DisplayedPreloadMessage({ webview }: DisplayedPreloadMessageProps) { - const { containerRef, isLoading, error } = useWebviewMount(webview); + const { containerRef, clipContainerRef, isLoading, error } = useWebviewMount(webview); if (error) { return
{error.message}
; @@ -51,7 +51,13 @@ function DisplayedPreloadMessage({ webview }: DisplayedPreloadMessageProps) { return ( <> {isLoading &&
{MESSAGES.LOADING}
} -
+
+
+
); } 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..5364f658957 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -15,27 +15,50 @@ export function useWebviewMount(webview: Promise) { const [isLoading, setIsLoading] = React.useState(true); const [error, setError] = React.useState(null); const containerRef = React.useRef(null); + const clipContainerRef = React.useRef(null); + const notebookInstance = useNotebookInstance(); React.useEffect(() => { const controller = new AbortController(); - let webviewElement: IWebviewElement | undefined; + let webviewElement: IOverlayWebview | undefined; async function mountWebview() { + const emptyDisposable = toDisposable(() => { }); try { const resolvedWebview = await webview; if (controller.signal.aborted || !containerRef.current) { - return; + return emptyDisposable; } setIsLoading(false); - assertIsStandardPositronWebview(resolvedWebview); + assertIsOverlayPositronWebview(resolvedWebview); webviewElement = resolvedWebview.webview; - webviewElement.mountTo( - containerRef.current, - getWindow(containerRef.current) + + webviewElement.claim( + containerRef, + getWindow(containerRef.current), + undefined ); + // Initial layout + if (containerRef.current) { + webviewElement.layoutWebviewOverElement( + containerRef.current, + new Dimension(400, 400) + ); + } + + // Update layout on scroll + const scrollDisposable = notebookInstance.onDidScrollCellsContainer(() => { + if (webviewElement && containerRef.current) { + webviewElement.layoutWebviewOverElement( + containerRef.current, + // new Dimension(400, 400) + ); + } + }); + webviewElement.onMessage((x) => { const { message } = x; if (!isHTMLOutputWebviewMessage(message) || !containerRef.current) { return; } @@ -48,12 +71,18 @@ export function useWebviewMount(webview: Promise) { // empty outputs that are 150px tall boundedHeight = 0; } - containerRef.current.style.height = `${boundedHeight}px`; + if (clipContainerRef.current) { + console.log("ClipContainerRef", clipContainerRef.current); + clipContainerRef.current.style.height = `${boundedHeight}px`; + } }); + return scrollDisposable; + } catch (err) { setError(err instanceof Error ? err : new Error('Failed to mount webview')); setIsLoading(false); + return emptyDisposable; } } @@ -63,7 +92,7 @@ export function useWebviewMount(webview: Promise) { controller.abort(); webviewElement?.dispose(); }; - }, [webview]); + }, [webview, notebookInstance]); - return { containerRef, isLoading, error }; + return { containerRef, clipContainerRef, isLoading, error }; } diff --git a/src/vs/workbench/contrib/positronWebviewPreloads/browser/positronWebviewPreloadsService.ts b/src/vs/workbench/contrib/positronWebviewPreloads/browser/positronWebviewPreloadsService.ts index 86ddd7af522..d1f59893cbe 100644 --- a/src/vs/workbench/contrib/positronWebviewPreloads/browser/positronWebviewPreloadsService.ts +++ b/src/vs/workbench/contrib/positronWebviewPreloads/browser/positronWebviewPreloadsService.ts @@ -269,7 +269,7 @@ export class PositronWebviewPreloadService extends Disposable implements IPositr preReqMessages: storedMessages, displayMessage: displayMessage, viewType: 'jupyter-notebook', - webviewType: WebviewType.Standard + webviewType: WebviewType.Overlay }); // 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..025c9277726 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 */ @@ -86,6 +86,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. From bfc6db3e8d28cc63a98f09418f0af790c91ebf89 Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Fri, 13 Dec 2024 14:30:36 -0500 Subject: [PATCH 02/12] Add back in some removed imports from rebase --- .../browser/notebookCells/hooks/useWebviewMount.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 5364f658957..7bba9a9dd37 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; -import { getWindow } from '../../../../../../base/browser/dom.js'; +import { Dimension, getWindow } 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 { IOverlayWebview } from '../../../../webview/browser/webview.js'; +import { toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { assertIsOverlayPositronWebview } from '../../../../positronOutputWebview/browser/notebookOutputWebviewServiceImpl.js'; export function useWebviewMount(webview: Promise) { From 12d2e95b01b3c4a62b0200d03490263734546e4d Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Fri, 13 Dec 2024 14:57:51 -0500 Subject: [PATCH 03/12] Move layout function into helper for DRY-ness --- .../notebookCells/hooks/useWebviewMount.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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 7bba9a9dd37..a82539a1a1b 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; -import { Dimension, getWindow } from '../../../../../../base/browser/dom.js'; +import { getWindow } from '../../../../../../base/browser/dom.js'; import { INotebookOutputWebview } from '../../../../positronOutputWebview/browser/notebookOutputWebviewService.js'; import { isHTMLOutputWebviewMessage } from '../../../../positronWebviewPreloads/browser/notebookOutputUtils.js'; import { useNotebookInstance } from '../../NotebookInstanceProvider.js'; @@ -24,6 +24,18 @@ export function useWebviewMount(webview: Promise) { const controller = new AbortController(); let webviewElement: IOverlayWebview | undefined; + /** + * Updates the layout of the webview element if both the webview and container are available + */ + function updateWebviewLayout() { + if (!webviewElement || !containerRef.current) { return; } + webviewElement.layoutWebviewOverElement( + containerRef.current, + undefined, + clipContainerRef.current || undefined + ); + } + async function mountWebview() { const emptyDisposable = toDisposable(() => { }); try { @@ -44,22 +56,10 @@ export function useWebviewMount(webview: Promise) { ); // Initial layout - if (containerRef.current) { - webviewElement.layoutWebviewOverElement( - containerRef.current, - new Dimension(400, 400) - ); - } + updateWebviewLayout(); - // Update layout on scroll - const scrollDisposable = notebookInstance.onDidScrollCellsContainer(() => { - if (webviewElement && containerRef.current) { - webviewElement.layoutWebviewOverElement( - containerRef.current, - // new Dimension(400, 400) - ); - } - }); + // Update layout on scroll and visibility changes + const scrollDisposable = notebookInstance.onDidScrollCellsContainer(updateWebviewLayout); webviewElement.onMessage((x) => { const { message } = x; From 0c6e920c3f0cabc64ee1eafcddf4cf063684f921 Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Fri, 13 Dec 2024 15:38:47 -0500 Subject: [PATCH 04/12] Fix problem with webviews spilling out beyond notebook and consolidate logic a bit better. --- .../browser/PositronNotebookComponent.tsx | 51 +------------ .../browser/PositronNotebookInstance.ts | 73 ++++++++++++++++--- .../notebookCells/PreloadMessageOutput.tsx | 10 +-- .../notebookCells/hooks/useWebviewMount.ts | 10 +-- .../browser/IPositronNotebookInstance.ts | 12 +++ 5 files changed, 84 insertions(+), 72 deletions(-) diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx index 4fd509a42bc..08a238f7787 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookComponent.tsx @@ -32,7 +32,9 @@ export function PositronNotebookComponent() { const fontStyles = useFontStyles(); const containerRef = React.useRef(null); - useScrollContainerEvents(containerRef); + React.useEffect(() => { + notebookInstance.setCellsContainer(containerRef.current); + }, [notebookInstance]); return (
@@ -65,53 +67,6 @@ function useFontStyles(): React.CSSProperties { } as React.CSSProperties; } -/** - * Hook to manage scroll and DOM mutation events for the notebook cells container - */ -function useScrollContainerEvents( - containerRef: React.RefObject, -) { - const notebookInstance = useNotebookInstance(); - - React.useEffect(() => { - const container = containerRef.current; - if (!container) { - return; - } - - // Fire initial scroll event after a small delay to ensure layout has settled - const initialScrollTimeout = setTimeout(() => { - notebookInstance.fireOnDidScrollCellsContainer(); - }, 50); - - // Set up scroll listener - const scrollListener = DOM.addDisposableListener(container, 'scroll', () => { - notebookInstance.fireOnDidScrollCellsContainer(); - }); - - // Set up mutation observer to watch for DOM changes - const observer = new MutationObserver(() => { - // Small delay to let the DOM changes settle - setTimeout(() => { - notebookInstance.fireOnDidScrollCellsContainer(); - }, 0); - }); - - observer.observe(container, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['style', 'class'] - }); - - return () => { - clearTimeout(initialScrollTimeout); - scrollListener.dispose(); - observer.disconnect(); - }; - }, [notebookInstance]); -} - function NotebookCell({ cell }: { cell: PositronNotebookCellGeneral; }) { diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookInstance.ts index 27f449d37e9..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. */ @@ -173,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 */ @@ -497,14 +560,6 @@ export class PositronNotebookInstance extends Disposable implements IPositronNot this._logService.info(this.id, 'attachView'); } - /** - * Manually fires the scroll event for the cells container. - * This can be called from React components after the container is mounted. - */ - fireOnDidScrollCellsContainer(): void { - this._onDidScrollCellsContainer.fire(); - } - /** * Gets the base cell editor options for the given language. * If they don't exist yet, they will be created. diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/PreloadMessageOutput.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/PreloadMessageOutput.tsx index bbbd21dc3a0..c5d649c31f7 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/PreloadMessageOutput.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/PreloadMessageOutput.tsx @@ -42,7 +42,7 @@ export function PreloadMessageOutput({ preloadMessageResult }: PreloadMessageOut } function DisplayedPreloadMessage({ webview }: DisplayedPreloadMessageProps) { - const { containerRef, clipContainerRef, isLoading, error } = useWebviewMount(webview); + const { containerRef, isLoading, error } = useWebviewMount(webview); if (error) { return
{error.message}
; @@ -51,13 +51,7 @@ function DisplayedPreloadMessage({ webview }: DisplayedPreloadMessageProps) { return ( <> {isLoading &&
{MESSAGES.LOADING}
} -
-
-
+
); } 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 a82539a1a1b..461cd80789e 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -17,7 +17,6 @@ export function useWebviewMount(webview: Promise) { const [isLoading, setIsLoading] = React.useState(true); const [error, setError] = React.useState(null); const containerRef = React.useRef(null); - const clipContainerRef = React.useRef(null); const notebookInstance = useNotebookInstance(); React.useEffect(() => { @@ -32,7 +31,7 @@ export function useWebviewMount(webview: Promise) { webviewElement.layoutWebviewOverElement( containerRef.current, undefined, - clipContainerRef.current || undefined + notebookInstance.cellsContainer ); } @@ -73,10 +72,7 @@ export function useWebviewMount(webview: Promise) { // empty outputs that are 150px tall boundedHeight = 0; } - if (clipContainerRef.current) { - console.log("ClipContainerRef", clipContainerRef.current); - clipContainerRef.current.style.height = `${boundedHeight}px`; - } + containerRef.current.style.height = `${boundedHeight}px`; }); return scrollDisposable; @@ -96,5 +92,5 @@ export function useWebviewMount(webview: Promise) { }; }, [webview, notebookInstance]); - return { containerRef, clipContainerRef, isLoading, error }; + return { containerRef, isLoading, error }; } diff --git a/src/vs/workbench/services/positronNotebook/browser/IPositronNotebookInstance.ts b/src/vs/workbench/services/positronNotebook/browser/IPositronNotebookInstance.ts index 025c9277726..0c611fe6233 100644 --- a/src/vs/workbench/services/positronNotebook/browser/IPositronNotebookInstance.ts +++ b/src/vs/workbench/services/positronNotebook/browser/IPositronNotebookInstance.ts @@ -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. From 5e138c1c7ff5011013890c657bdf89d44e79003a Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Fri, 13 Dec 2024 15:57:30 -0500 Subject: [PATCH 05/12] Remove logic for making standard webviews in various webview creation services --- .../notebookCells/hooks/useWebviewMount.ts | 2 - .../browser/notebookOutputWebview.ts | 12 ++-- .../browser/notebookOutputWebviewService.ts | 25 +------ .../notebookOutputWebviewServiceImpl.ts | 68 +++---------------- .../browser/notebookMultiMessagePlotClient.ts | 7 +- .../browser/notebookOutputPlotClient.ts | 7 +- .../browser/positronPreviewServiceImpl.ts | 7 +- .../browser/positronWebviewPreloadsService.ts | 8 +-- 8 files changed, 23 insertions(+), 113 deletions(-) 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 461cd80789e..80113027738 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -10,7 +10,6 @@ import { isHTMLOutputWebviewMessage } from '../../../../positronWebviewPreloads/ import { useNotebookInstance } from '../../NotebookInstanceProvider.js'; import { IOverlayWebview } from '../../../../webview/browser/webview.js'; import { toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { assertIsOverlayPositronWebview } from '../../../../positronOutputWebview/browser/notebookOutputWebviewServiceImpl.js'; export function useWebviewMount(webview: Promise) { @@ -45,7 +44,6 @@ export function useWebviewMount(webview: Promise) { } setIsLoading(false); - assertIsOverlayPositronWebview(resolvedWebview); webviewElement = resolvedWebview.webview; webviewElement.claim( 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 d1f59893cbe..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.Overlay + viewType: 'jupyter-notebook' }); // Assert that we have a webview From 1a9d3a00719de191304b70e7dda3b4919fbd08ee Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Wed, 18 Dec 2024 17:47:59 -0500 Subject: [PATCH 06/12] Hide webviews when they arent actually visible. - Added NotebookVisibilityProvider to manage editor visibility state. - Introduced observable for tracking editor visibility in PositronNotebookEditor. - Updated setEditorVisible method to set visibility state. - Modified webview mounting logic to conditionally mount based on visibility. --- .../browser/NotebookVisibilityContext.tsx | 37 +++++++++++ .../browser/PositronNotebookEditor.tsx | 61 ++++++++++++------- .../notebookCells/hooks/useWebviewMount.ts | 48 ++++++++++++--- 3 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 src/vs/workbench/contrib/positronNotebook/browser/NotebookVisibilityContext.tsx 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/PositronNotebookEditor.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx index 4e9d16b7a3f..90b12bae415 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx @@ -56,6 +56,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 { @@ -219,11 +220,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 +327,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 +461,26 @@ export class PositronNotebookEditor extends EditorPane { const reactRenderer: PositronReactRenderer = this._positronReactRenderer ?? new PositronReactRenderer(this._parentDiv); reactRenderer.render( - - scopedContextKeyService.createScoped(container) - }}> - - - + + + scopedContextKeyService.createScoped(container) + }}> + + + + ); } 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 80113027738..4392ccfcc24 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -9,7 +9,9 @@ import { INotebookOutputWebview } from '../../../../positronOutputWebview/browse import { isHTMLOutputWebviewMessage } from '../../../../positronWebviewPreloads/browser/notebookOutputUtils.js'; import { useNotebookInstance } from '../../NotebookInstanceProvider.js'; import { IOverlayWebview } from '../../../../webview/browser/webview.js'; -import { toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { useNotebookVisibility } from '../../NotebookVisibilityContext.js'; +import { Event } from '../../../../../../base/common/event.js'; export function useWebviewMount(webview: Promise) { @@ -17,10 +19,13 @@ export function useWebviewMount(webview: Promise) { const [error, setError] = React.useState(null); const containerRef = React.useRef(null); const notebookInstance = useNotebookInstance(); + const visibilityObservable = useNotebookVisibility(); React.useEffect(() => { const controller = new AbortController(); let webviewElement: IOverlayWebview | undefined; + let scrollDisposable: IDisposable | undefined; + let visibilityObserver: IDisposable | undefined; /** * Updates the layout of the webview element if both the webview and container are available @@ -34,9 +39,27 @@ export function useWebviewMount(webview: Promise) { ); } + function claimWebview() { + if (!webviewElement || !containerRef.current) { return; } + webviewElement.claim( + containerRef, + getWindow(containerRef.current), + undefined + ); + } + + function releaseWebview() { + webviewElement?.release(containerRef) + } + async function mountWebview() { const emptyDisposable = toDisposable(() => { }); try { + // If not visible, don't mount the webview + if (!visibilityObservable) { + return emptyDisposable; + } + const resolvedWebview = await webview; if (controller.signal.aborted || !containerRef.current) { @@ -46,17 +69,13 @@ export function useWebviewMount(webview: Promise) { setIsLoading(false); webviewElement = resolvedWebview.webview; - webviewElement.claim( - containerRef, - getWindow(containerRef.current), - undefined - ); + claimWebview(); // Initial layout updateWebviewLayout(); // Update layout on scroll and visibility changes - const scrollDisposable = notebookInstance.onDidScrollCellsContainer(updateWebviewLayout); + scrollDisposable = notebookInstance.onDidScrollCellsContainer(updateWebviewLayout); webviewElement.onMessage((x) => { const { message } = x; @@ -82,13 +101,24 @@ export function useWebviewMount(webview: Promise) { } } + + Event.fromObservable(visibilityObservable)((isVisible) => { + if (isVisible) { + claimWebview(); + } else { + releaseWebview(); + } + }); + mountWebview(); return () => { controller.abort(); - webviewElement?.dispose(); + releaseWebview(); + scrollDisposable?.dispose(); + visibilityObserver?.dispose(); }; - }, [webview, notebookInstance]); + }, [webview, notebookInstance, visibilityObservable]); return { containerRef, isLoading, error }; } From bbb99c5a402ca2579f5519688db355e641a0f12b Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Thu, 19 Dec 2024 17:21:29 -0500 Subject: [PATCH 07/12] Fix bug with plots staying put when switching to another notebook --- .../browser/PositronNotebookEditor.tsx | 5 +++- .../browser/ServicesProvider.tsx | 8 +++++-- .../notebookCells/hooks/useWebviewMount.ts | 23 ++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx index 90b12bae415..87ba350402d 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx @@ -48,6 +48,7 @@ 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 { PositronNotebookEditorInput } from './PositronNotebookEditorInput.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IWebviewService } from '../../webview/browser/webview.js'; @@ -125,6 +126,7 @@ 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, ) { // Call the base class's constructor. super( @@ -475,7 +477,8 @@ export class PositronNotebookEditor extends EditorPane { openerService: this._openerService, notificationService: this._notificationService, sizeObservable: this._size, - scopedContextKeyProviderCallback: container => scopedContextKeyService.createScoped(container) + scopedContextKeyProviderCallback: container => scopedContextKeyService.createScoped(container), + editorService: this._editorService }}> diff --git a/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx b/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx index 537dfe4bc94..2057c4e2650 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx @@ -17,6 +17,7 @@ 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 { IPositronNotebookOutputWebviewService } from '../../positronOutputWebview/browser/notebookOutputWebviewService.js'; import { IWebviewService } from '../../webview/browser/webview.js'; import { IPositronWebviewPreloadService } from '../../../services/positronWebviewPreloads/browser/positronWebviewPreloadService.js'; @@ -83,6 +84,11 @@ interface ServiceBundle { */ scopedContextKeyProviderCallback: (container: HTMLElement) => IScopedContextKeyService; + /** + * Service for managing active editors and editor state + */ + editorService: IEditorService; + } /** @@ -110,5 +116,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 4392ccfcc24..0e13291048a 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ 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 { isHTMLOutputWebviewMessage } from '../../../../positronWebviewPreloads/browser/notebookOutputUtils.js'; import { useNotebookInstance } from '../../NotebookInstanceProvider.js'; +import { useServices } from '../../ServicesProvider.js'; import { IOverlayWebview } from '../../../../webview/browser/webview.js'; import { IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { useNotebookVisibility } from '../../NotebookVisibilityContext.js'; @@ -20,12 +21,15 @@ export function useWebviewMount(webview: Promise) { const containerRef = React.useRef(null); const notebookInstance = useNotebookInstance(); const visibilityObservable = useNotebookVisibility(); + const { editorService } = useServices(); React.useEffect(() => { const controller = new AbortController(); let webviewElement: IOverlayWebview | undefined; let scrollDisposable: IDisposable | undefined; let visibilityObserver: IDisposable | undefined; + let containerBlurDisposable: IDisposable | undefined; + let editorChangeDisposable: IDisposable | undefined; /** * Updates the layout of the webview element if both the webview and container are available @@ -77,6 +81,16 @@ export function useWebviewMount(webview: Promise) { // Update layout on scroll and visibility changes scrollDisposable = notebookInstance.onDidScrollCellsContainer(updateWebviewLayout); + // Update layout when focus leaves the notebook container + if (notebookInstance.cellsContainer) { + containerBlurDisposable = addDisposableListener(notebookInstance.cellsContainer, 'focusout', (e) => { + // Only update if focus is moving outside the notebook container + if (!notebookInstance.cellsContainer?.contains(e.relatedTarget as Node)) { + updateWebviewLayout(); + } + }); + } + webviewElement.onMessage((x) => { const { message } = x; if (!isHTMLOutputWebviewMessage(message) || !containerRef.current) { return; } @@ -92,6 +106,11 @@ export function useWebviewMount(webview: Promise) { containerRef.current.style.height = `${boundedHeight}px`; }); + // Listen for editor group changes and update layout + editorChangeDisposable = editorService.onDidActiveEditorChange(() => { + updateWebviewLayout(); + }); + return scrollDisposable; } catch (err) { @@ -116,7 +135,9 @@ export function useWebviewMount(webview: Promise) { controller.abort(); releaseWebview(); scrollDisposable?.dispose(); + containerBlurDisposable?.dispose(); visibilityObserver?.dispose(); + editorChangeDisposable?.dispose(); }; }, [webview, notebookInstance, visibilityObservable]); From 60ca8976419e345355a015b0d49b0e1895738528 Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Thu, 19 Dec 2024 18:13:59 -0500 Subject: [PATCH 08/12] Fix bug with toggling panes causing plots to spill over --- .../browser/PositronNotebookEditor.tsx | 5 +- .../browser/ServicesProvider.tsx | 6 ++ .../notebookCells/hooks/useWebviewMount.ts | 78 ++++++++++++++++--- 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx index 87ba350402d..db2f87011fa 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/PositronNotebookEditor.tsx @@ -49,6 +49,7 @@ 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'; @@ -127,6 +128,7 @@ export class PositronNotebookEditor extends EditorPane { @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( @@ -478,7 +480,8 @@ export class PositronNotebookEditor extends EditorPane { notificationService: this._notificationService, sizeObservable: this._size, scopedContextKeyProviderCallback: container => scopedContextKeyService.createScoped(container), - editorService: this._editorService + editorService: this._editorService, + layoutService: this._layoutService }}> diff --git a/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx b/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx index 2057c4e2650..493dd5c100f 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/ServicesProvider.tsx @@ -18,6 +18,7 @@ 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'; @@ -89,6 +90,11 @@ interface ServiceBundle { */ editorService: IEditorService; + /** + * Service for managing workbench layout and panel positions + */ + layoutService: IWorkbenchLayoutService; + } /** 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 0e13291048a..244ecd84bfa 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -21,7 +21,7 @@ export function useWebviewMount(webview: Promise) { const containerRef = React.useRef(null); const notebookInstance = useNotebookInstance(); const visibilityObservable = useNotebookVisibility(); - const { editorService } = useServices(); + const { editorService, layoutService } = useServices(); React.useEffect(() => { const controller = new AbortController(); @@ -30,17 +30,40 @@ export function useWebviewMount(webview: Promise) { let visibilityObserver: IDisposable | undefined; let containerBlurDisposable: IDisposable | undefined; let editorChangeDisposable: IDisposable | undefined; + let resizeObserver: ResizeObserver | undefined; /** * Updates the layout of the webview element if both the webview and container are available */ - function updateWebviewLayout() { + // Track if there's a pending layout update + let layoutTimeout: number | undefined; + function updateWebviewLayout(immediate = false) { if (!webviewElement || !containerRef.current) { return; } - webviewElement.layoutWebviewOverElement( - containerRef.current, - undefined, - notebookInstance.cellsContainer - ); + + // Clear any pending layout update + if (layoutTimeout !== undefined) { + window.clearTimeout(layoutTimeout); + layoutTimeout = undefined; + } + + const doLayout = () => { + if (!containerRef.current || !notebookInstance.cellsContainer) { + return; + } + + webviewElement?.layoutWebviewOverElement( + containerRef.current, + undefined, + notebookInstance.cellsContainer + ); + }; + + if (immediate) { + doLayout(); + } else { + // Add a small delay to ensure the layout has settled + layoutTimeout = window.setTimeout(doLayout, 50); + } } function claimWebview() { @@ -79,14 +102,14 @@ export function useWebviewMount(webview: Promise) { updateWebviewLayout(); // Update layout on scroll and visibility changes - scrollDisposable = notebookInstance.onDidScrollCellsContainer(updateWebviewLayout); + scrollDisposable = notebookInstance.onDidScrollCellsContainer(() => updateWebviewLayout(true)); // Update layout when focus leaves the notebook container if (notebookInstance.cellsContainer) { containerBlurDisposable = addDisposableListener(notebookInstance.cellsContainer, 'focusout', (e) => { // Only update if focus is moving outside the notebook container if (!notebookInstance.cellsContainer?.contains(e.relatedTarget as Node)) { - updateWebviewLayout(); + updateWebviewLayout(true); } }); } @@ -106,11 +129,37 @@ export function useWebviewMount(webview: Promise) { containerRef.current.style.height = `${boundedHeight}px`; }); - // Listen for editor group changes and update layout - editorChangeDisposable = editorService.onDidActiveEditorChange(() => { - updateWebviewLayout(); + // Listen for all editor and layout changes that might affect the webview + const handleLayoutChange = () => updateWebviewLayout(false); + editorChangeDisposable = Event.any( + editorService.onDidActiveEditorChange, + editorService.onDidVisibleEditorsChange, // Catches editor group changes + layoutService.onDidChangePartVisibility, + layoutService.onDidChangeZenMode, + layoutService.onDidChangeWindowMaximized, + layoutService.onDidChangePanelAlignment, + layoutService.onDidChangePanelPosition + )(handleLayoutChange); + + // Create and setup resize observer for layout changes + resizeObserver = new ResizeObserver(() => { + updateWebviewLayout(true); }); + if (notebookInstance.cellsContainer) { + resizeObserver.observe(notebookInstance.cellsContainer); + } + + // Update layout when focus leaves the notebook container + if (notebookInstance.cellsContainer) { + containerBlurDisposable = addDisposableListener(notebookInstance.cellsContainer, 'focusout', (e) => { + // Only update if focus is moving outside the notebook container + if (!notebookInstance.cellsContainer?.contains(e.relatedTarget as Node)) { + updateWebviewLayout(true); + } + }); + } + return scrollDisposable; } catch (err) { @@ -133,11 +182,16 @@ export function useWebviewMount(webview: Promise) { return () => { controller.abort(); + if (layoutTimeout !== undefined) { + window.clearTimeout(layoutTimeout); + layoutTimeout = undefined; + } releaseWebview(); scrollDisposable?.dispose(); containerBlurDisposable?.dispose(); visibilityObserver?.dispose(); editorChangeDisposable?.dispose(); + resizeObserver?.disconnect(); }; }, [webview, notebookInstance, visibilityObservable]); From 444f8d15b6c46a1c0df7f8ed9a52073c51c7baaa Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Fri, 20 Dec 2024 09:48:41 -0500 Subject: [PATCH 09/12] Fix bug where resizing with a drag causes plot to overlap --- .../browser/notebookCells/hooks/useWebviewMount.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 244ecd84bfa..747ac551add 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -134,11 +134,15 @@ export function useWebviewMount(webview: Promise) { editorChangeDisposable = Event.any( editorService.onDidActiveEditorChange, editorService.onDidVisibleEditorsChange, // Catches editor group changes + layoutService.onDidLayoutMainContainer, // Listen for main container layout changes + layoutService.onDidLayoutContainer, // Listen for any container layout changes + layoutService.onDidLayoutActiveContainer, // Listen for active container layout changes layoutService.onDidChangePartVisibility, layoutService.onDidChangeZenMode, layoutService.onDidChangeWindowMaximized, layoutService.onDidChangePanelAlignment, - layoutService.onDidChangePanelPosition + layoutService.onDidChangePanelPosition, + layoutService.onDidChangeMainEditorCenteredLayout // Listen for main editor centered layout changes )(handleLayoutChange); // Create and setup resize observer for layout changes From f049f3c52ee42c7410dffb999dc5a04ebb505391 Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Fri, 20 Dec 2024 09:50:52 -0500 Subject: [PATCH 10/12] Clean up and describe a bit better how we're reacting to changes. --- .../notebookCells/hooks/useWebviewMount.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 747ac551add..8755bba6536 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -132,17 +132,16 @@ export function useWebviewMount(webview: Promise) { // Listen for all editor and layout changes that might affect the webview const handleLayoutChange = () => updateWebviewLayout(false); editorChangeDisposable = Event.any( - editorService.onDidActiveEditorChange, - editorService.onDidVisibleEditorsChange, // Catches editor group changes - layoutService.onDidLayoutMainContainer, // Listen for main container layout changes - layoutService.onDidLayoutContainer, // Listen for any container layout changes - layoutService.onDidLayoutActiveContainer, // Listen for active container layout changes - layoutService.onDidChangePartVisibility, - layoutService.onDidChangeZenMode, - layoutService.onDidChangeWindowMaximized, - layoutService.onDidChangePanelAlignment, - layoutService.onDidChangePanelPosition, - layoutService.onDidChangeMainEditorCenteredLayout // Listen for main editor centered layout changes + editorService.onDidActiveEditorChange, // Active editor switched + editorService.onDidVisibleEditorsChange, // Editor groups/layout changed + layoutService.onDidLayoutMainContainer, // Main container resized/moved + layoutService.onDidLayoutContainer, // Any container resized/moved + layoutService.onDidLayoutActiveContainer, // Active container resized/moved + layoutService.onDidChangePartVisibility, // UI part shown/hidden + layoutService.onDidChangeWindowMaximized, // Window maximized/restored + layoutService.onDidChangePanelAlignment, // Panel alignment changed + layoutService.onDidChangePanelPosition, // Panel position changed + layoutService.onDidChangeMainEditorCenteredLayout // Editor centered mode toggled )(handleLayoutChange); // Create and setup resize observer for layout changes From bdec72acf606abee6a8f41da7e78beba380da4f5 Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Fri, 20 Dec 2024 10:06:09 -0500 Subject: [PATCH 11/12] More general refactoring and cleanup --- .../notebookCells/hooks/useWebviewMount.ts | 282 +++++++++++------- 1 file changed, 174 insertions(+), 108 deletions(-) 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 8755bba6536..146281e3a6f 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -10,85 +10,160 @@ import { isHTMLOutputWebviewMessage } from '../../../../positronWebviewPreloads/ import { useNotebookInstance } from '../../NotebookInstanceProvider.js'; import { useServices } from '../../ServicesProvider.js'; import { IOverlayWebview } from '../../../../webview/browser/webview.js'; -import { IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.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(); + + // Webview references let webviewElement: IOverlayWebview | undefined; - let scrollDisposable: IDisposable | undefined; - let visibilityObserver: IDisposable | undefined; - let containerBlurDisposable: IDisposable | undefined; - let editorChangeDisposable: IDisposable | undefined; + + // Create a disposable store to manage all disposables + const disposables = new DisposableStore(); let resizeObserver: ResizeObserver | undefined; /** - * Updates the layout of the webview element if both the webview and container are available + * Manages layout calls for the webview using requestAnimationFrame for better performance + * + * @param immediate If true, layout occurs in the current frame */ - // Track if there's a pending layout update - let layoutTimeout: number | undefined; - function updateWebviewLayout(immediate = false) { - if (!webviewElement || !containerRef.current) { return; } + let layoutFrame: number | undefined; + function updateWebviewLayout(immediate = false): void { + if (!webviewElement || !containerRef.current) { + return; + } // Clear any pending layout update - if (layoutTimeout !== undefined) { - window.clearTimeout(layoutTimeout); - layoutTimeout = undefined; + if (layoutFrame !== undefined) { + window.cancelAnimationFrame(layoutFrame); + layoutFrame = undefined; } const doLayout = () => { - if (!containerRef.current || !notebookInstance.cellsContainer) { - return; + 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)); } - - webviewElement?.layoutWebviewOverElement( - containerRef.current, - undefined, - notebookInstance.cellsContainer - ); }; if (immediate) { doLayout(); } else { - // Add a small delay to ensure the layout has settled - layoutTimeout = window.setTimeout(doLayout, 50); + layoutFrame = window.requestAnimationFrame(doLayout); + disposables.add(toDisposable(() => { + if (layoutFrame !== undefined) { + window.cancelAnimationFrame(layoutFrame); + layoutFrame = undefined; + } + })); } } - function claimWebview() { - if (!webviewElement || !containerRef.current) { return; } - webviewElement.claim( - containerRef, - getWindow(containerRef.current), - undefined - ); + /** + * Claims the webview, instructing it to position itself over our container. + */ + function claimWebview(): void { + if (!webviewElement || !containerRef.current) { + return; + } + try { + webviewElement.claim(containerRef, getWindow(containerRef.current), undefined); + } catch (err) { + setError(new WebviewMountError('Failed to claim webview', err instanceof Error ? err : undefined)); + } } - function releaseWebview() { - webviewElement?.release(containerRef) + /** + * 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(() => { }); + const emptyDisposable = toDisposable(() => { /* no-op */ }); + try { // 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 emptyDisposable; } @@ -96,107 +171,98 @@ export function useWebviewMount(webview: Promise) { setIsLoading(false); webviewElement = resolvedWebview.webview; + // Position it initially claimWebview(); - - // Initial layout updateWebviewLayout(); - // Update layout on scroll and visibility changes - scrollDisposable = notebookInstance.onDidScrollCellsContainer(() => updateWebviewLayout(true)); + // Scroll listener: reposition the webview if the notebook container scrolls + disposables.add(notebookInstance.onDidScrollCellsContainer(() => + updateWebviewLayout(true) + )); - // Update layout when focus leaves the notebook container + // When focus leaves the notebook container, update layout to ensure correct size if (notebookInstance.cellsContainer) { - containerBlurDisposable = addDisposableListener(notebookInstance.cellsContainer, 'focusout', (e) => { - // Only update if focus is moving outside the notebook container - if (!notebookInstance.cellsContainer?.contains(e.relatedTarget as Node)) { - updateWebviewLayout(true); + disposables.add(addDisposableListener( + notebookInstance.cellsContainer, + 'focusout', + (e) => { + if ( + notebookInstance.cellsContainer && + !notebookInstance.cellsContainer.contains(e.relatedTarget as Node) + ) { + updateWebviewLayout(true); + } } - }); + )); } - 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`; - }); + // Listen for messages from the webview; adjust container height if needed + disposables.add(toDisposable(() => webviewElement!.onMessage(handleWebviewMessage).dispose())); - // Listen for all editor and layout changes that might affect the webview + // React to editor or layout changes const handleLayoutChange = () => updateWebviewLayout(false); - editorChangeDisposable = Event.any( - editorService.onDidActiveEditorChange, // Active editor switched - editorService.onDidVisibleEditorsChange, // Editor groups/layout changed - layoutService.onDidLayoutMainContainer, // Main container resized/moved - layoutService.onDidLayoutContainer, // Any container resized/moved - layoutService.onDidLayoutActiveContainer, // Active container resized/moved - layoutService.onDidChangePartVisibility, // UI part shown/hidden - layoutService.onDidChangeWindowMaximized, // Window maximized/restored - layoutService.onDidChangePanelAlignment, // Panel alignment changed - layoutService.onDidChangePanelPosition, // Panel position changed - layoutService.onDidChangeMainEditorCenteredLayout // Editor centered mode toggled - )(handleLayoutChange); - - // Create and setup resize observer for layout changes + 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())); } - // Update layout when focus leaves the notebook container - if (notebookInstance.cellsContainer) { - containerBlurDisposable = addDisposableListener(notebookInstance.cellsContainer, 'focusout', (e) => { - // Only update if focus is moving outside the notebook container - if (!notebookInstance.cellsContainer?.contains(e.relatedTarget as Node)) { - updateWebviewLayout(true); - } - }); - } - - return scrollDisposable; - + 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(); + } + }) + ); + } - 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(); - if (layoutTimeout !== undefined) { - window.clearTimeout(layoutTimeout); - layoutTimeout = undefined; - } releaseWebview(); - scrollDisposable?.dispose(); - containerBlurDisposable?.dispose(); - visibilityObserver?.dispose(); - editorChangeDisposable?.dispose(); - resizeObserver?.disconnect(); + disposables.dispose(); }; - }, [webview, notebookInstance, visibilityObservable]); + }, [webview, notebookInstance, visibilityObservable, handleWebviewMessage]); - return { containerRef, isLoading, error }; + // Return the container reference plus loading/error states + return { + containerRef, + isLoading, + error + }; } From 2cbdd9e646185086c35e1cb8c4929c6c5b4c0dae Mon Sep 17 00:00:00 2001 From: Nick Strayer Date: Fri, 20 Dec 2024 11:10:31 -0500 Subject: [PATCH 12/12] Add comment explaining why we use just the root of a ref in claiming and releasing the webview --- .../browser/notebookCells/hooks/useWebviewMount.ts | 3 +++ 1 file changed, 3 insertions(+) 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 146281e3a6f..be041c85c4f 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/hooks/useWebviewMount.ts @@ -132,6 +132,9 @@ export function useWebviewMount(webview: Promise) { 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));