diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index c542f6dff..052f579c1 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -44,6 +44,11 @@ const KERNEL_STATUS_INFO_CLASS = 'jp-RetroKernelStatus-info'; */ const KERNEL_STATUS_FADE_OUT_CLASS = 'jp-RetroKernelStatus-fade'; +/** + * The class for scrolled outputs + */ +const SCROLLED_OUTPUTS_CLASS = 'jp-mod-outputsScrolled'; + /** * A plugin for the checkpoint indicator */ @@ -217,13 +222,35 @@ const kernelStatus: JupyterFrontEndPlugin = { }; /** - * A plugin to enable scrolling for outputs by default + * A plugin to enable scrolling for outputs by default. + * Mimic the logic from the classic notebook, as found here: + * https://github.com/jupyter/notebook/blob/a9a31c096eeffe1bff4e9164c6a0442e0e13cdb3/notebook/static/notebook/js/outputarea.js#L96-L120 */ const scrollOutput: JupyterFrontEndPlugin = { id: '@retrolab/notebook-extension:scroll-output', autoStart: true, requires: [INotebookTracker], activate: async (app: JupyterFrontEnd, tracker: INotebookTracker) => { + const autoScrollThreshold = 100; + + // decide whether to scroll the output of the cell based on some heuristics + const autoScroll = (cell: CodeCell) => { + const { outputArea } = cell; + // respect cells with an explicit scrolled state + const scrolled = cell.model.metadata.get('scrolled'); + if (scrolled !== undefined) { + return; + } + const { node } = outputArea; + const height = node.getBoundingClientRect().height; + const fontSize = parseFloat(node.style.fontSize.replace('px', '')); + const lineHeight = (fontSize || 14) * 1.3; + const scroll = height > lineHeight * autoScrollThreshold; + // do not set via cell.outputScrolled = true, as this would + // otherwise synchronize the scrolled state to the notebook metadata + cell.toggleClass(SCROLLED_OUTPUTS_CLASS, scroll); + }; + tracker.widgetAdded.connect((sender, notebook) => { notebook.model?.cells.changed.connect((sender, changed) => { // process new cells only @@ -233,7 +260,20 @@ const scrollOutput: JupyterFrontEndPlugin = { const [cellModel] = changed.newValues; notebook.content.widgets.forEach(cell => { if (cell.model.id === cellModel.id && cell.model.type === 'code') { - (cell as CodeCell).outputsScrolled = true; + const codeCell = cell as CodeCell; + codeCell.outputArea.model.changed.connect(() => + autoScroll(codeCell) + ); + } + }); + }); + + // when the notebook widget is created, process all the cells + // TODO: investigate why notebook.content.fullyRendered is not enough + notebook.sessionContext.ready.then(() => { + notebook.content.widgets.forEach(cell => { + if (cell.model.type === 'code') { + autoScroll(cell as CodeCell); } }); });