diff --git a/packages/notebook/src/browser/notebook-editor-widget.tsx b/packages/notebook/src/browser/notebook-editor-widget.tsx index ca894b04aae98..533f7c6c850ef 100644 --- a/packages/notebook/src/browser/notebook-editor-widget.tsx +++ b/packages/notebook/src/browser/notebook-editor-widget.tsx @@ -31,7 +31,7 @@ import { NotebookMainToolbarRenderer } from './view/notebook-main-toolbar'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; import { NotebookContextManager } from './service/notebook-context-manager'; - +import { NotebookViewportService } from './view/notebook-viewport-service'; const PerfectScrollbar = require('react-perfect-scrollbar'); export const NotebookEditorWidgetContainerFactory = Symbol('NotebookEditorWidgetContainerFactory'); @@ -45,6 +45,7 @@ export function createNotebookEditorWidgetContainer(parent: interfaces.Container child.bind(NotebookMainToolbarRenderer).toSelf().inSingletonScope(); child.bind(NotebookCodeCellRenderer).toSelf().inSingletonScope(); child.bind(NotebookMarkdownCellRenderer).toSelf().inSingletonScope(); + child.bind(NotebookViewportService).toSelf().inSingletonScope(); child.bind(NotebookEditorWidget).toSelf(); @@ -96,6 +97,9 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa @inject(NotebookEditorProps) protected readonly props: NotebookEditorProps; + @inject(NotebookViewportService) + protected readonly viewportService: NotebookViewportService; + protected readonly onDidChangeModelEmitter = new Emitter(); readonly onDidChangeModel = this.onDidChangeModelEmitter.event; @@ -198,13 +202,16 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa if (this._model) { return
{this.notebookMainToolbarRenderer.render(this._model, this.node)} - - - -
; +
this.viewportService.viewportElement = ref}> + this.viewportService.onScroll(e)}> + + +
+ ; } else { return
; } @@ -233,6 +240,7 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa this.onDidPostKernelMessageEmitter.dispose(); this.onDidReceiveKernelMessageEmitter.dispose(); this.onPostRendererMessageEmitter.dispose(); + this.viewportService.dispose(); super.dispose(); } } diff --git a/packages/notebook/src/browser/style/index.css b/packages/notebook/src/browser/style/index.css index eabf9c15d15e7..e17335df5fbdc 100644 --- a/packages/notebook/src/browser/style/index.css +++ b/packages/notebook/src/browser/style/index.css @@ -171,10 +171,16 @@ overflow: hidden; } +.theia-notebook-viewport { + display: flex; + overflow: hidden; + height: 100%; +} + .theia-notebook-scroll-container { - flex: 1; - overflow: hidden; - position: relative; + flex: 1; + overflow: hidden; + position: relative; } .theia-notebook-main-toolbar { diff --git a/packages/notebook/src/browser/view/notebook-cell-editor.tsx b/packages/notebook/src/browser/view/notebook-cell-editor.tsx index 31cdc04c35f2b..1ad807a14cfb3 100644 --- a/packages/notebook/src/browser/view/notebook-cell-editor.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-editor.tsx @@ -20,15 +20,19 @@ import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor'; import { MonacoEditor, MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; -import { DisposableCollection } from '@theia/core'; import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; import { NotebookContextManager } from '../service/notebook-context-manager'; +import { DisposableCollection, OS } from '@theia/core'; +import { NotebookViewportService } from './notebook-viewport-service'; +import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; interface CellEditorProps { notebookModel: NotebookModel, cell: NotebookCellModel, monacoServices: MonacoEditorServices, notebookContextManager: NotebookContextManager; + notebookViewportService?: NotebookViewportService, + fontInfo?: BareFontInfo; } const DEFAULT_EDITOR_OPTIONS: MonacoEditor.IOptions = { @@ -49,7 +53,17 @@ export class CellEditor extends React.Component { override componentDidMount(): void { this.disposeEditor(); - this.initEditor(); + if (!this.props.notebookViewportService || (this.container && this.props.notebookViewportService.isElementInViewport(this.container))) { + this.initEditor(); + } else { + const disposable = this.props.notebookViewportService?.onDidChangeViewport(() => { + if (!this.editor && this.container && this.props.notebookViewportService!.isElementInViewport(this.container)) { + this.initEditor(); + disposable.dispose(); + } + }); + this.toDispose.push(disposable); + } } override componentWillUnmount(): void { @@ -65,6 +79,7 @@ export class CellEditor extends React.Component { const { cell, notebookModel, monacoServices } = this.props; if (this.container) { const editorNode = this.container; + editorNode.style.height = ''; const editorModel = await cell.resolveTextModel(); const uri = cell.uri; this.editor = new SimpleMonacoEditor(uri, @@ -93,10 +108,14 @@ export class CellEditor extends React.Component { this.editor?.refresh(); }; + protected estimateHeight(): string { + const lineHeight = this.props.fontInfo?.lineHeight ?? 20; + return this.props.cell.text.split(OS.backend.EOL).length * lineHeight + 10 + 7 + 'px'; + } + override render(): React.ReactNode { return
this.setContainer(container)}> - + ref={container => this.setContainer(container)} style={{ height: this.editor ? undefined : this.estimateHeight() }}>
; } diff --git a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx index 6d32dbd4e413d..8c8b9f026f88d 100644 --- a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx @@ -30,6 +30,10 @@ import { codicon } from '@theia/core/lib/browser'; import { NotebookCellExecutionState } from '../../common'; import { DisposableCollection } from '@theia/core'; import { NotebookContextManager } from '../service/notebook-context-manager'; +import { NotebookViewportService } from './notebook-viewport-service'; +import { EditorPreferences } from '@theia/editor/lib/browser'; +import { BareFontInfo } from '@theia/monaco-editor-core/esm/vs/editor/common/config/fontInfo'; +import { PixelRatio } from '@theia/monaco-editor-core/esm/vs/base/browser/browser'; @injectable() export class NotebookCodeCellRenderer implements CellRenderer { @@ -51,6 +55,14 @@ export class NotebookCodeCellRenderer implements CellRenderer { @inject(NotebookContextManager) protected readonly notebookContextManager: NotebookContextManager; + @inject(NotebookViewportService) + protected readonly notebookViewportService: NotebookViewportService; + + @inject(EditorPreferences) + protected readonly editorPreferences: EditorPreferences; + + protected fontInfo: BareFontInfo | undefined; + render(notebookModel: NotebookModel, cell: NotebookCellModel, handle: number): React.ReactNode { return
@@ -60,10 +72,14 @@ export class NotebookCodeCellRenderer implements CellRenderer {

{`[${cell.exec ?? ' '}]`}

*/}
- + -
-
+ +
@@ -71,6 +87,25 @@ export class NotebookCodeCellRenderer implements CellRenderer {
; } + + protected getOrCreateMonacoFontInfo(): BareFontInfo { + if (!this.fontInfo) { + this.fontInfo = this.createFontInfo(); + this.editorPreferences.onPreferenceChanged(e => this.fontInfo = this.createFontInfo()); + } + return this.fontInfo; + } + + protected createFontInfo(): BareFontInfo { + return BareFontInfo.createFromRawSettings({ + fontFamily: this.editorPreferences['editor.fontFamily'], + fontWeight: String(this.editorPreferences['editor.fontWeight']), + fontSize: this.editorPreferences['editor.fontSize'], + fontLigatures: this.editorPreferences['editor.fontLigatures'], + lineHeight: this.editorPreferences['editor.lineHeight'], + letterSpacing: this.editorPreferences['editor.letterSpacing'], + }, PixelRatio.value); + } } export interface NotebookCodeCellStatusProps { diff --git a/packages/notebook/src/browser/view/notebook-viewport-service.ts b/packages/notebook/src/browser/view/notebook-viewport-service.ts new file mode 100644 index 0000000000000..93dc2a2ccf859 --- /dev/null +++ b/packages/notebook/src/browser/view/notebook-viewport-service.ts @@ -0,0 +1,61 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { Disposable } from '@theia/core'; +import { injectable } from '@theia/core/shared/inversify'; +import { Emitter } from '@theia/core/shared/vscode-languageserver-protocol'; + +/** + * this service is for managing the viewport and scroll state of a notebook editor. + * its used both for restoring scroll state after reopening an editor and for cell to check if they are in the viewport. + */ +@injectable() +export class NotebookViewportService implements Disposable { + + protected onDidChangeViewportEmitter = new Emitter(); + readonly onDidChangeViewport = this.onDidChangeViewportEmitter.event; + + protected _viewportElement: HTMLDivElement | undefined; + + protected resizeObserver: ResizeObserver; + + set viewportElement(element: HTMLDivElement | undefined) { + this._viewportElement = element; + if (element) { + this.onDidChangeViewportEmitter.fire(); + this.resizeObserver?.disconnect(); + this.resizeObserver = new ResizeObserver(() => this.onDidChangeViewportEmitter.fire()); + this.resizeObserver.observe(element); + } + } + + isElementInViewport(element: HTMLElement): boolean { + if (this._viewportElement) { + const rect = element.getBoundingClientRect(); + const viewRect = this._viewportElement.getBoundingClientRect(); + return rect.top < viewRect.top ? rect.bottom > viewRect.top : rect.top < viewRect.bottom; + } + return false; + } + + onScroll(e: HTMLDivElement): void { + this.onDidChangeViewportEmitter.fire(); + } + + dispose(): void { + this.resizeObserver.disconnect(); + } +} diff --git a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx index 662d39c11137c..934cc61308f38 100644 --- a/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx +++ b/packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx @@ -194,7 +194,7 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable { this.webviewWidget.setIframeHeight(message.contentHeight + 5); break; case 'did-scroll-wheel': - this.editor.node.children[0].children[1].scrollBy(message.deltaX, message.deltaY); + this.editor.node.getElementsByClassName('theia-notebook-viewport')[0].children[0].scrollBy(message.deltaX, message.deltaY); break; case 'customKernelMessage': this.editor.recieveKernelMessage(message.message);