Skip to content

Commit

Permalink
Only initialize notebook cell editor when in viewport (#13476)
Browse files Browse the repository at this point in the history
* only initialize notebbok cell editor when in viewport

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* using actual line height for editor height estimation

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* fix editors sometimes not correctly filling

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

* rebase build fix and removed console-logs

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>

---------

Signed-off-by: Jonah Iden <jonah.iden@typefox.io>
  • Loading branch information
jonah-iden authored Mar 19, 2024
1 parent 3e16157 commit 79a3244
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 19 deletions.
24 changes: 16 additions & 8 deletions packages/notebook/src/browser/notebook-editor-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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();

Expand Down Expand Up @@ -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<void>();
readonly onDidChangeModel = this.onDidChangeModelEmitter.event;

Expand Down Expand Up @@ -198,13 +202,16 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa
if (this._model) {
return <div className='theia-notebook-main-container'>
{this.notebookMainToolbarRenderer.render(this._model, this.node)}
<PerfectScrollbar className='theia-notebook-scroll-container'>
<NotebookCellListView renderers={this.renderers}
notebookModel={this._model}
toolbarRenderer={this.cellToolbarFactory}
commandRegistry={this.commandRegistry} />
</PerfectScrollbar>
</div>;
<div className='theia-notebook-viewport' ref={(ref: HTMLDivElement) => this.viewportService.viewportElement = ref}>
<PerfectScrollbar className='theia-notebook-scroll-container'
onScrollY={(e: HTMLDivElement) => this.viewportService.onScroll(e)}>
<NotebookCellListView renderers={this.renderers}
notebookModel={this._model}
toolbarRenderer={this.cellToolbarFactory}
commandRegistry={this.commandRegistry} />
</PerfectScrollbar>
</div>
</div >;
} else {
return <div></div>;
}
Expand Down Expand Up @@ -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();
}
}
12 changes: 9 additions & 3 deletions packages/notebook/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 23 additions & 4 deletions packages/notebook/src/browser/view/notebook-cell-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -49,7 +53,17 @@ export class CellEditor extends React.Component<CellEditorProps, {}> {

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 {
Expand All @@ -65,6 +79,7 @@ export class CellEditor extends React.Component<CellEditorProps, {}> {
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,
Expand Down Expand Up @@ -93,10 +108,14 @@ export class CellEditor extends React.Component<CellEditorProps, {}> {
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 <div className='theia-notebook-cell-editor' onResize={this.handleResize} id={this.props.cell.uri.toString()}
ref={container => this.setContainer(container)}>

ref={container => this.setContainer(container)} style={{ height: this.editor ? undefined : this.estimateHeight() }}>
</div>;
}

Expand Down
41 changes: 38 additions & 3 deletions packages/notebook/src/browser/view/notebook-code-cell-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 <div>
<div className='theia-notebook-cell-with-sidebar'>
Expand All @@ -60,17 +72,40 @@ export class NotebookCodeCellRenderer implements CellRenderer {
<p className='theia-notebook-code-cell-execution-order'>{`[${cell.exec ?? ' '}]`}</p> */}
</div>
<div className='theia-notebook-cell-editor-container'>
<CellEditor notebookModel={notebookModel} cell={cell} monacoServices={this.monacoServices} notebookContextManager={this.notebookContextManager} />
<CellEditor notebookModel={notebookModel} cell={cell}
monacoServices={this.monacoServices}
notebookContextManager={this.notebookContextManager}
notebookViewportService={this.notebookViewportService}
fontInfo={this.getOrCreateMonacoFontInfo()} />
<NotebookCodeCellStatus cell={cell} executionStateService={this.executionStateService}></NotebookCodeCellStatus>
</div>
</div>
</div >
</div >
<div className='theia-notebook-cell-with-sidebar'>
<NotebookCodeCellOutputs cell={cell} notebook={notebookModel} outputWebviewFactory={this.cellOutputWebviewFactory}
renderSidebar={() =>
this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.OUTPUT_SIDEBAR_MENU, notebookModel, cell, cell.outputs[0])} />
</div>
</div >;
}

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 {
Expand Down
61 changes: 61 additions & 0 deletions packages/notebook/src/browser/view/notebook-viewport-service.ts
Original file line number Diff line number Diff line change
@@ -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<void>();
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 79a3244

Please sign in to comment.