From 455e0be3c68a03c16d01730549ad0c50b1ecf2e9 Mon Sep 17 00:00:00 2001 From: seem Date: Tue, 14 Apr 2026 16:13:08 +0200 Subject: [PATCH 01/20] feat: add drag handle to vertically resize scrollable cell output Adds a HorizontalSplitter below scrollable notebook cell outputs that lets users drag to resize the output height. Double-clicking resets to the default height, and re-running the cell clears the override. Improves HorizontalSplitter with hover delay, pointer capture on the sizer element, didDrag guard, user-select during drag, and the workbench.sash.hoverBorder color token. --- .../splitters/horizontalSplitter.css | 9 +- .../splitters/horizontalSplitter.tsx | 122 ++++++++++---- .../notebookCells/NotebookCodeCell.css | 6 + .../notebookCells/NotebookCodeCell.tsx | 46 +++++- .../notebook-output-resize.test.ts | 149 ++++++++++++++++++ 5 files changed, 301 insertions(+), 31 deletions(-) create mode 100644 test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.css b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.css index db15c2779dcb..3d72a9626868 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.css +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.css @@ -22,9 +22,16 @@ z-index: 27; width: 100%; position: relative; + touch-action: none; +} + +.horizontal-splitter +.sizer.hovering { + transition: background-color 0.1s ease-out; + background-color: var(--vscode-sash-hoverBorder); } .horizontal-splitter .sizer.resizing { - background-color: var(--vscode-focusBorder); + background-color: var(--vscode-sash-hoverBorder); } diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index ca87ed22cb39..3baf85bbc68c 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -7,13 +7,16 @@ import './horizontalSplitter.css'; // React. -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; // Other dependencies. import * as DOM from '../../../dom.js'; +import { Delayer } from '../../../../common/async.js'; import { isMacintosh } from '../../../../common/platform.js'; +import { DisposableStore } from '../../../../common/lifecycle.js'; import { positronClassNames } from '../../../../common/positronUtilities.js'; import { createStyleSheet } from '../../../domStylesheets.js'; +import { usePositronReactServicesContext } from '../../../positronReactRendererContext.js'; /** * HorizontalSplitterResizeParams interface. This defines the parameters of a resize operation. When @@ -36,9 +39,52 @@ export const HorizontalSplitter = (props: { showResizeIndicator?: boolean; onBeginResize: () => HorizontalSplitterResizeParams; onResize: (height: number) => void; + onDoubleClick?: () => void; }) => { + // Context hooks. + const services = usePositronReactServicesContext(); + // State hooks. const [resizing, setResizing] = useState(false); + const [hovering, setHovering] = useState(false); + const [hoverDelay, setHoverDelay] = useState( + services.configurationService.getValue('workbench.sash.hoverDelay') + ); + + // Ref hooks. + const hoverDelayerRef = useRef>(undefined!); + + // Setup the hover delayer and listen for config changes. + useEffect(() => { + const disposables = new DisposableStore(); + hoverDelayerRef.current = disposables.add(new Delayer(0)); + + disposables.add( + services.configurationService.onDidChangeConfiguration(e => { + if (e.affectedKeys.has('workbench.sash.hoverDelay')) { + setHoverDelay(services.configurationService.getValue('workbench.sash.hoverDelay')); + } + }) + ); + + return () => disposables.dispose(); + }, [services.configurationService]); + + /** + * onPointerEnter handler. + */ + const pointerEnterHandler = () => { + hoverDelayerRef.current?.trigger(() => setHovering(true), hoverDelay); + }; + + /** + * onPointerLeave handler. + */ + const pointerLeaveHandler = () => { + if (!resizing) { + hoverDelayerRef.current?.trigger(() => setHovering(false), hoverDelay); + } + }; /** * onPointerDown handler. @@ -56,15 +102,21 @@ export const HorizontalSplitter = (props: { // Setup the resize state. const resizeParams = props.onBeginResize(); - const target = DOM.getWindow(e.currentTarget).document.body; + const sizer = e.currentTarget; + const body = DOM.getWindow(sizer).document.body; const clientY = e.clientY; - const styleSheet = createStyleSheet(target); + const styleSheet = createStyleSheet(body); + + // Track whether any meaningful drag occurred, so we can distinguish + // a click (or double-click) from a drag on pointer release. + let didDrag = false; /** * pointermove event handler. * @param e A PointerEvent that describes a user interaction with the pointer. */ const pointerMoveHandler = (e: PointerEvent) => { + didDrag = true; // Consume the event. e.preventDefault(); e.stopPropagation(); @@ -84,9 +136,9 @@ export const HorizontalSplitter = (props: { cursor = isMacintosh ? 'row-resize' : 'ns-resize'; } - // Update the style sheet's text content with the desired cursor. This is a clever - // technique adopted from src/vs/base/browser/ui/sash/sash.ts. - styleSheet.textContent = `* { cursor: ${cursor} !important; }`; + // Update the style sheet's text content with the desired cursor and + // disable text selection during the resize operation. + styleSheet.textContent = `* { cursor: ${cursor} !important; user-select: none !important; }`; // Call the onResize callback. props.onResize(newHeight); @@ -101,24 +153,32 @@ export const HorizontalSplitter = (props: { setResizing(false); // Remove our pointer event handlers. - target.removeEventListener('pointermove', pointerMoveHandler); - target.removeEventListener('lostpointercapture', lostPointerCaptureHandler); - - // Calculate the new height. - let newHeight = calculateNewHeight(e); - - // Adjust the new height to be within limits. - if (newHeight < resizeParams.minimumHeight) { - newHeight = resizeParams.minimumHeight; - } else if (newHeight > resizeParams.maximumHeight) { - newHeight = resizeParams.maximumHeight; - } + sizer.removeEventListener('pointermove', pointerMoveHandler); + sizer.removeEventListener('lostpointercapture', lostPointerCaptureHandler); // Remove the style sheet. - target.removeChild(styleSheet); + body.removeChild(styleSheet); + + // Only commit the final height if the user actually dragged. + // This avoids interfering with click and double-click interactions. + if (didDrag) { + // Calculate the new height. + let newHeight = calculateNewHeight(e); + + // Adjust the new height to be within limits. + if (newHeight < resizeParams.minimumHeight) { + newHeight = resizeParams.minimumHeight; + } else if (newHeight > resizeParams.maximumHeight) { + newHeight = resizeParams.maximumHeight; + } + + // Call the onEndResize callback. + props.onResize(newHeight); + } - // Call the onEndResize callback. - props.onResize(newHeight); + // Reset hover state based on pointer position. + hoverDelayerRef.current?.cancel(); + setHovering(false); }; /** @@ -128,7 +188,7 @@ export const HorizontalSplitter = (props: { */ const calculateNewHeight = (e: PointerEvent) => { // Calculate the delta. - const delta = Math.trunc(e.clientY - clientY); + const delta = e.clientY - clientY; // Calculate the new height. return !resizeParams.invert ? @@ -136,25 +196,31 @@ export const HorizontalSplitter = (props: { resizeParams.startingHeight - delta; }; - // Set the dragging flag. + // Set the dragging flag and show hover indicator immediately. setResizing(true); + hoverDelayerRef.current?.cancel(); + setHovering(true); - // Set the capture target of future pointer events to be the current target and add our - // pointer event handlers. - target.setPointerCapture(e.pointerId); - target.addEventListener('pointermove', pointerMoveHandler); - target.addEventListener('lostpointercapture', lostPointerCaptureHandler); + // Set pointer capture on the sizer element and add our pointer event handlers. + sizer.setPointerCapture(e.pointerId); + sizer.addEventListener('pointermove', pointerMoveHandler); + sizer.addEventListener('lostpointercapture', lostPointerCaptureHandler); }; // Render. return (
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
); diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.css b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.css index 731bcfcdfbed..e32c1018d390 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.css +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.css @@ -43,6 +43,7 @@ .positron-notebook-cell-outputs { overflow: hidden; + position: relative; } .positron-notebook-code-cell-outputs-inner { @@ -54,6 +55,11 @@ max-height: var(--vscode-positronNotebook-output-max-height); overflow-y: auto; } + + /* When user has manually resized, the inline style controls height */ + &.height-override { + overflow-y: auto; + } overflow-x: auto; & .notebook-error, diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx index eac0d6b4d62a..b83d2e77c41b 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx @@ -7,7 +7,7 @@ import './NotebookCodeCell.css'; // React. -import React, { useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; // Other dependencies. import { NotebookCellOutputs, ParsedTextOutput } from '../PositronNotebookCells/IPositronNotebookCell.js'; @@ -38,6 +38,7 @@ import { useCellScopedContextKeyService } from './CellContextKeyServiceProvider. import { useScrollingIndicator } from './useScrollingIndicator.js'; import { CellOutputActionBar } from './CellOutputActionBar.js'; import { Button } from '../../../../../base/browser/ui/positronComponents/button/button.js'; +import { HorizontalSplitter, HorizontalSplitterResizeParams } from '../../../../../base/browser/ui/positronComponents/splitters/horizontalSplitter.js'; const copyOutputTextLabel = localize('positron.notebook.copyOutputText', "Copy Output Text"); const expandOutputTooltip = localize('positron.notebook.expandOutput', "Click to Expand Output"); @@ -69,6 +70,38 @@ const CellOutputsSection = React.memo(function CellOutputsSection({ cell, output // Per-cell scrolling override takes precedence over global setting. const outputScrolling = perCellScrolling ?? layout.outputScrolling; + const clearHeightOverride = useCallback(() => { + const el = outputsInnerRef.current; + if (el) { + el.style.height = ''; + el.style.maxHeight = ''; + el.classList.remove('height-override'); + } + }, []); + + // Reset height override when outputs change (new execution) or scrolling mode toggles. + useEffect(() => { + clearHeightOverride(); + }, [outputs, outputScrolling, clearHeightOverride]); + + const onBeginResize = useCallback((): HorizontalSplitterResizeParams => { + const el = outputsInnerRef.current; + return { + startingHeight: el?.offsetHeight ?? 0, + minimumHeight: 50, + maximumHeight: 2000, + }; + }, []); + + const onResize = useCallback((height: number) => { + const el = outputsInnerRef.current; + if (el) { + el.style.height = height + 'px'; + el.style.maxHeight = height + 'px'; + el.classList.add('height-override'); + } + }, []); + // Update the output overflow context key. React.useEffect(() => { if (!outputOverflowsKey) { return; } @@ -181,8 +214,9 @@ const CellOutputsSection = React.memo(function CellOutputsSection({ cell, output 'positron-notebook-code-cell-outputs-inner', 'positron-notebook-scrollable', 'positron-notebook-scrollable-fade', - { 'output-scrolling': outputScrolling } + { 'output-scrolling': outputScrolling }, )}> + {isCollapsed ? : outputs?.map((output) => ( @@ -201,6 +235,14 @@ const CellOutputsSection = React.memo(function CellOutputsSection({ cell, output )) }
+ {outputScrolling && !isCollapsed && outputs.length > 0 && + + } ); diff --git a/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts b/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts new file mode 100644 index 000000000000..f14da14c5b2c --- /dev/null +++ b/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2026 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect, tags } from '../_test.setup'; +import { test } from './_test.setup.js'; + +test.use({ + suiteId: __filename +}); + +test.describe('Positron Notebooks: Output Resize Handle', { + tag: [tags.WIN, tags.WEB, tags.POSITRON_NOTEBOOKS] +}, () => { + + test('Drag handle resizes scrollable cell output', async function ({ app, page, settings }) { + const { notebooks, notebooksPositron } = app.workbench; + + await test.step('Setup: Create notebook with scrollable output', async () => { + // Enable output scrolling explicitly -- the default depends on + // product.quality and may be false in local dev builds. + await settings.set({ 'notebook.output.scrolling': true }); + + await notebooks.createNewNotebook(); + await notebooksPositron.expectCellCountToBe(1); + await notebooksPositron.kernel.select('Python'); + + // Generate output that exceeds the scrollable area so the resize + // handle appears. + await notebooksPositron.addCodeToCell(0, 'for i in range(100): print(f"line {i}")', { run: true }); + await notebooksPositron.expectOutputAtIndex(0, ['line 0']); + }); + + const cellOutput = notebooksPositron.cellOutput(0); + const splitter = cellOutput.locator('.horizontal-splitter'); + const resizeHandle = splitter.locator('.sizer'); + const outputInner = cellOutput.locator('.positron-notebook-code-cell-outputs-inner'); + + await test.step('Resize handle is visible for scrollable output', async () => { + await expect(splitter).toBeVisible({ timeout: 10000 }); + }); + + await test.step('Dragging the handle changes the output height', async () => { + const initialBox = await outputInner.boundingBox(); + expect(initialBox).toBeTruthy(); + const initialHeight = initialBox!.height; + + // Drag the resize handle downward to grow the output area. + // The HorizontalSplitter uses setPointerCapture on document.body, + // so we dispatch native PointerEvent objects to ensure capture + // and event routing work correctly. + const handleBox = await resizeHandle.boundingBox(); + expect(handleBox).toBeTruthy(); + const startX = handleBox!.x + handleBox!.width / 2; + const startY = handleBox!.y + handleBox!.height / 2; + const dragDistance = 150; + + await resizeHandle.evaluate((el, { startX, startY, dragDistance }) => { + const body = document.body; + + el.dispatchEvent(new PointerEvent('pointerdown', { + clientX: startX, clientY: startY, + pointerId: 1, pointerType: 'mouse', + buttons: 1, bubbles: true, cancelable: true, + })); + + for (let i = 1; i <= 5; i++) { + body.dispatchEvent(new PointerEvent('pointermove', { + clientX: startX, + clientY: startY + (dragDistance * i) / 5, + pointerId: 1, pointerType: 'mouse', + buttons: 1, bubbles: true, cancelable: true, + })); + } + + // Include final position -- the splitter calls pointerMoveHandler + // with this event for one last update before cleanup. + body.dispatchEvent(new PointerEvent('lostpointercapture', { + clientX: startX, + clientY: startY + dragDistance, + pointerId: 1, pointerType: 'mouse', + bubbles: true, + })); + }, { startX, startY, dragDistance }); + + // The output container should now be taller + await expect(async () => { + const newBox = await outputInner.boundingBox(); + expect(newBox).toBeTruthy(); + expect(newBox!.height).toBeGreaterThan(initialHeight + 50); + }).toPass({ timeout: 5000 }); + }); + + await test.step('Double-clicking the handle resets to default height', async () => { + // The output inner should have a height-override class after resizing + await expect(outputInner).toHaveClass(/height-override/); + + // Dispatch dblclick directly to avoid triggering pointerdown + // drag handlers that would re-set the height override. + await resizeHandle.dispatchEvent('dblclick', {}); + + // After reset, the height-override class should be removed + await expect(outputInner).not.toHaveClass(/height-override/, { timeout: 5000 }); + }); + + await test.step('Resize handle is hidden when output is collapsed', async () => { + await notebooksPositron.outputCollapseToggle(0).click(); + await expect(notebooksPositron.outputCollapsedLabel(0)).toBeVisible(); + await expect(splitter).toBeHidden(); + + // Expand again + await notebooksPositron.outputCollapseToggle(0).click(); + await expect(splitter).toBeVisible(); + }); + + await test.step('Re-running cell resets the resize override', async () => { + // Resize first + const handleBox = await resizeHandle.boundingBox(); + expect(handleBox).toBeTruthy(); + const startX = handleBox!.x + handleBox!.width / 2; + const startY = handleBox!.y + handleBox!.height / 2; + + await resizeHandle.evaluate((el, { startX, startY }) => { + const body = document.body; + el.dispatchEvent(new PointerEvent('pointerdown', { + clientX: startX, clientY: startY, + pointerId: 1, pointerType: 'mouse', + buttons: 1, bubbles: true, cancelable: true, + })); + body.dispatchEvent(new PointerEvent('pointermove', { + clientX: startX, clientY: startY + 100, + pointerId: 1, pointerType: 'mouse', + buttons: 1, bubbles: true, cancelable: true, + })); + body.dispatchEvent(new PointerEvent('lostpointercapture', { + clientX: startX, clientY: startY + 100, + pointerId: 1, pointerType: 'mouse', bubbles: true, + })); + }, { startX, startY }); + + await expect(outputInner).toHaveClass(/height-override/); + + // Re-run the cell - should reset the height override + await notebooksPositron.runCodeAtIndex(0); + await expect(outputInner).not.toHaveClass(/height-override/, { timeout: 15000 }); + }); + }); +}); From 86f34432975180faf8bec95dec7c545f69091abb Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 15:46:19 +0200 Subject: [PATCH 02/20] use a ref for the hover delayer --- .../splitters/verticalSplitter.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx index a91133f0f3f1..8c5d641d83a3 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx @@ -162,15 +162,20 @@ export const VerticalSplitter = ({ const [hoverDelay, setHoverDelay] = useState(getHoverDelay(services.configurationService)); const [hovering, setHovering] = useState(false); const [highlightExpandCollapse, setHighlightExpandCollapse] = useState(false); - const [hoveringDelayer, setHoveringDelayer] = useState>(undefined!); - const [collapsed, setCollapsed, collapsedRef] = useStateRef(isCollapsed); + const [collapsed, setCollapsed, collapsedRef] = useStateRef(isCollapsed); const [resizing, setResizing] = useState(false); + // Ref hooks. + const hoverDelayerRef = useRef>(undefined); + // Main useEffect. useEffect(() => { // Create the disposable store for cleanup. const disposableStore = new DisposableStore(); + // Set the hover delayer. + hoverDelayerRef.current = disposableStore.add(new Delayer(0)); + // Add the onDidChangeConfiguration event handler. disposableStore.add( services.configurationService.onDidChangeConfiguration(configurationChangeEvent => { @@ -191,9 +196,6 @@ export const VerticalSplitter = ({ }) ); - // Set the hover delayer. - setHoveringDelayer(disposableStore.add(new Delayer(0))); - // Return the cleanup function that will dispose of the disposables. return () => disposableStore.dispose(); }, [collapsible, services.configurationService]); @@ -208,7 +210,7 @@ export const VerticalSplitter = ({ * @param e A PointerEvent that describes a user interaction with the pointer. */ const sashPointerEnterHandler = (e: React.PointerEvent) => { - hoveringDelayer.trigger(() => { + hoverDelayerRef.current?.trigger(() => { setHovering(true); const rect = sashRef.current.getBoundingClientRect(); if (e.clientY >= rect.top + EXPAND_COLLAPSE_BUTTON_TOP && @@ -225,7 +227,7 @@ export const VerticalSplitter = ({ const sashPointerLeaveHandler = (e: React.PointerEvent) => { // When not resizing, trigger the delayer. if (!resizing) { - hoveringDelayer.trigger(() => setHovering(false), hoverDelay); + hoverDelayerRef.current?.trigger(() => setHovering(false), hoverDelay); } }; @@ -234,7 +236,7 @@ export const VerticalSplitter = ({ * @param e A PointerEvent that describes a user interaction with the pointer. */ const expandCollapseButtonPointerEnterHandler = (e: React.PointerEvent) => { - hoveringDelayer.cancel(); + hoverDelayerRef.current?.cancel(); setHovering(true); setHighlightExpandCollapse(true); }; @@ -244,7 +246,7 @@ export const VerticalSplitter = ({ * @param e A PointerEvent that describes a user interaction with the pointer. */ const expandCollapseButtonPointerLeaveHandler = (e: React.PointerEvent) => { - hoveringDelayer.trigger(() => setHovering(false), hoverDelay); + hoverDelayerRef.current?.trigger(() => setHovering(false), hoverDelay); setHighlightExpandCollapse(false); }; @@ -261,7 +263,7 @@ export const VerticalSplitter = ({ onCollapsedChanged?.(false); } - hoveringDelayer.cancel(); + hoverDelayerRef.current?.cancel(); setHovering(false); setHighlightExpandCollapse(false); }; @@ -365,7 +367,7 @@ export const VerticalSplitter = ({ // Clear the resizing flag. setResizing(false); - hoveringDelayer.cancel(); + hoverDelayerRef.current?.cancel(); setHovering(isPointInsideElement(e.clientX, e.clientY, sashRef.current)); }; From 07bea5f1b592c30f49190d72acc309e0d8747ace Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 16:26:21 +0200 Subject: [PATCH 03/20] unhover immediately --- .../positronComponents/splitters/horizontalSplitter.tsx | 3 ++- .../ui/positronComponents/splitters/verticalSplitter.tsx | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index 3baf85bbc68c..bb2aded028b8 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -82,7 +82,8 @@ export const HorizontalSplitter = (props: { */ const pointerLeaveHandler = () => { if (!resizing) { - hoverDelayerRef.current?.trigger(() => setHovering(false), hoverDelay); + hoverDelayerRef.current?.cancel(); +setHovering(false); } }; diff --git a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx index 8c5d641d83a3..2588c97fdef0 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx @@ -225,9 +225,10 @@ export const VerticalSplitter = ({ * @param e A PointerEvent that describes a user interaction with the pointer. */ const sashPointerLeaveHandler = (e: React.PointerEvent) => { - // When not resizing, trigger the delayer. + // When not resizing, unset hover. if (!resizing) { - hoverDelayerRef.current?.trigger(() => setHovering(false), hoverDelay); + hoverDelayerRef.current?.cancel(); +setHovering(false); } }; @@ -246,7 +247,8 @@ export const VerticalSplitter = ({ * @param e A PointerEvent that describes a user interaction with the pointer. */ const expandCollapseButtonPointerLeaveHandler = (e: React.PointerEvent) => { - hoverDelayerRef.current?.trigger(() => setHovering(false), hoverDelay); + hoverDelayerRef.current?.cancel(); +setHovering(false); setHighlightExpandCollapse(false); }; From 419ce526e74ba79b97bdab932e91840221cec700 Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 16:29:59 +0200 Subject: [PATCH 04/20] use the correct theme color: sash hover border --- .../ui/positronComponents/splitters/verticalSplitter.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.css b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.css index 6ea6b3e9bb4c..63274e3c8dc7 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.css +++ b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.css @@ -29,11 +29,11 @@ .vertical-splitter .sash-hovering { transition: background-color 0.1s ease-out; - background-color: var(--vscode-focusBorder); + background-color: var(--vscode-sash-hoverBorder); } .vertical-splitter .sash-resizing { - background-color: var(--vscode-focusBorder); + background-color: var(--vscode-sash-hoverBorder); } .vertical-splitter .sash .sash-indicator { @@ -43,11 +43,11 @@ .vertical-splitter .sash .sash-indicator.hovering { transition: background-color 0.1s ease-out; - background-color: var(--vscode-focusBorder); + background-color: var(--vscode-sash-hoverBorder); } .vertical-splitter .sash .sash-indicator.resizing { - background-color: var(--vscode-focusBorder); + background-color: var(--vscode-sash-hoverBorder); } .vertical-splitter .expand-collapse-button { From a993fae8718637eb315072e9ac9cd90b94672e84 Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 16:45:21 +0200 Subject: [PATCH 05/20] set pointer capture on the sizer, not the whole body --- .../splitters/verticalSplitter.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx index 2588c97fdef0..ab79eba3ce92 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx @@ -7,7 +7,7 @@ import './verticalSplitter.css'; // React. -import React, { PointerEvent, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; // Other dependencies. import * as DOM from '../../../dom.js'; @@ -294,9 +294,10 @@ setHovering(false); // Setup the resize state. const resizeParams = onBeginResize(); const startingWidth = collapsed ? sashWidth : resizeParams.startingWidth; - const target = DOM.getWindow(e.currentTarget).document.body; + const sizer = e.currentTarget; + const body = DOM.getWindow(e.currentTarget).document.body; const clientX = e.clientX; - const styleSheet = createStyleSheet(target); + const styleSheet = createStyleSheet(body); /** * pointermove event handler. @@ -359,18 +360,16 @@ setHovering(false); pointerMoveHandler(e); // Remove our pointer event handlers. - // @ts-ignore - target.removeEventListener('pointermove', pointerMoveHandler); - // @ts-ignore - target.removeEventListener('lostpointercapture', lostPointerCaptureHandler); + sizer.removeEventListener('pointermove', pointerMoveHandler); + sizer.removeEventListener('lostpointercapture', lostPointerCaptureHandler); // Remove the style sheet. - target.removeChild(styleSheet); + sizer.removeChild(styleSheet); // Clear the resizing flag. setResizing(false); hoverDelayerRef.current?.cancel(); - setHovering(isPointInsideElement(e.clientX, e.clientY, sashRef.current)); + setHovering(false); }; // Set the dragging flag @@ -378,11 +377,9 @@ setHovering(false); // Set the capture target of future pointer events to be the current target and add our // pointer event handlers. - target.setPointerCapture(e.pointerId); - // @ts-ignore - target.addEventListener('pointermove', pointerMoveHandler); - // @ts-ignore - target.addEventListener('lostpointercapture', lostPointerCaptureHandler); + sizer.setPointerCapture(e.pointerId); + sizer.addEventListener('pointermove', pointerMoveHandler); + sizer.addEventListener('lostpointercapture', lostPointerCaptureHandler); }; // Render. From ae1c7ae640fa592f0e1f5a3c05260652b4cf6a32 Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 16:56:26 +0200 Subject: [PATCH 06/20] set hovering based on final pointer position --- .../splitters/horizontalSplitter.tsx | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index bb2aded028b8..0cad247cb408 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -150,8 +150,12 @@ setHovering(false); * @param e A PointerEvent that describes a user interaction with the pointer. */ const lostPointerCaptureHandler = (e: PointerEvent) => { - // Clear the dragging flag. - setResizing(false); + // Only commit the final height if the user actually dragged. + // This avoids interfering with click and double-click interactions. + if (didDrag) { + // Handle the last possible move change. + pointerMoveHandler(e); + } // Remove our pointer event handlers. sizer.removeEventListener('pointermove', pointerMoveHandler); @@ -160,26 +164,10 @@ setHovering(false); // Remove the style sheet. body.removeChild(styleSheet); - // Only commit the final height if the user actually dragged. - // This avoids interfering with click and double-click interactions. - if (didDrag) { - // Calculate the new height. - let newHeight = calculateNewHeight(e); - - // Adjust the new height to be within limits. - if (newHeight < resizeParams.minimumHeight) { - newHeight = resizeParams.minimumHeight; - } else if (newHeight > resizeParams.maximumHeight) { - newHeight = resizeParams.maximumHeight; - } - - // Call the onEndResize callback. - props.onResize(newHeight); - } - - // Reset hover state based on pointer position. + // Clear the resizing flag. + setResizing(false); hoverDelayerRef.current?.cancel(); - setHovering(false); + setHovering(isPointInsideElement(e.clientX, e.clientY, sizer)); }; /** From e1ff34753260458d3a7e5e0b1bf3d0047fd072e3 Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 16:58:14 +0200 Subject: [PATCH 07/20] clear hovering state after double click --- .../splitters/horizontalSplitter.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index 0cad247cb408..83727e7f8726 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -196,6 +196,15 @@ setHovering(false); sizer.addEventListener('lostpointercapture', lostPointerCaptureHandler); }; + /** + * onDoubleClick handler. + */ + const doubleClickHandler = () => { + props.onDoubleClick?.(); + hoverDelayerRef.current?.cancel(); + setHovering(false); + }; + // Render. return (
@@ -206,7 +215,7 @@ setHovering(false); { 'hovering': hovering && props.showResizeIndicator }, { 'resizing': resizing && props.showResizeIndicator } )} - onDoubleClick={props.onDoubleClick} + onDoubleClick={doubleClickHandler} onPointerDown={pointerDownHandler} onPointerEnter={pointerEnterHandler} onPointerLeave={pointerLeaveHandler} From c2d907cde4b91aa2cce2043e151e579df2006980 Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 17:00:03 +0200 Subject: [PATCH 08/20] reuse sash helpers --- .../splitters/horizontalSplitter.tsx | 19 +++++++++++++------ .../splitters/verticalSplitter.tsx | 8 ++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index 83727e7f8726..2a1207d3f846 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -17,6 +17,7 @@ import { DisposableStore } from '../../../../common/lifecycle.js'; import { positronClassNames } from '../../../../common/positronUtilities.js'; import { createStyleSheet } from '../../../domStylesheets.js'; import { usePositronReactServicesContext } from '../../../positronReactRendererContext.js'; +import { getHoverDelay, isPointInsideElement } from './verticalSplitter.js'; /** * HorizontalSplitterResizeParams interface. This defines the parameters of a resize operation. When @@ -47,26 +48,30 @@ export const HorizontalSplitter = (props: { // State hooks. const [resizing, setResizing] = useState(false); const [hovering, setHovering] = useState(false); - const [hoverDelay, setHoverDelay] = useState( - services.configurationService.getValue('workbench.sash.hoverDelay') - ); + const [hoverDelay, setHoverDelay] = useState(getHoverDelay(services.configurationService)); // Ref hooks. const hoverDelayerRef = useRef>(undefined!); - // Setup the hover delayer and listen for config changes. + // Main useEffect. useEffect(() => { + // Create the disposable store for cleanup. const disposables = new DisposableStore(); + + // Set the hover delayer. hoverDelayerRef.current = disposables.add(new Delayer(0)); + // Add the onDidChangeConfiguration event handler. disposables.add( services.configurationService.onDidChangeConfiguration(e => { + // Track changes to workbench.sash.hoverDelay. if (e.affectedKeys.has('workbench.sash.hoverDelay')) { - setHoverDelay(services.configurationService.getValue('workbench.sash.hoverDelay')); + setHoverDelay(getHoverDelay(services.configurationService)); } }) ); + // Return the cleanup function that will dispose of the disposables. return () => disposables.dispose(); }, [services.configurationService]); @@ -83,7 +88,7 @@ export const HorizontalSplitter = (props: { const pointerLeaveHandler = () => { if (!resizing) { hoverDelayerRef.current?.cancel(); -setHovering(false); + setHovering(false); } }; @@ -117,7 +122,9 @@ setHovering(false); * @param e A PointerEvent that describes a user interaction with the pointer. */ const pointerMoveHandler = (e: PointerEvent) => { + // The pointer moved, mark as dragging. didDrag = true; + // Consume the event. e.preventDefault(); e.stopPropagation(); diff --git a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx index ab79eba3ce92..bbbe1f348f68 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx @@ -71,7 +71,7 @@ export interface VerticalSplitterResizeParams { * @param configurationService The configuration service. * @returns The sash size. */ -const getSashSize = (configurationService: IConfigurationService) => +export const getSashSize = (configurationService: IConfigurationService) => configurationService.getValue('workbench.sash.size'); /** @@ -79,7 +79,7 @@ const getSashSize = (configurationService: IConfigurationService) => * @param configurationService The configuration service. * @returns The hover delay. */ -const getHoverDelay = (configurationService: IConfigurationService) => +export const getHoverDelay = (configurationService: IConfigurationService) => configurationService.getValue('workbench.sash.hoverDelay'); @@ -90,7 +90,7 @@ const getHoverDelay = (configurationService: IConfigurationService) => * @param element The element. * @returns true, if the point is inside the specified element; otherwise, false. */ -const isPointInsideElement = (x: number, y: number, element?: HTMLElement) => { +export const isPointInsideElement = (x: number, y: number, element?: HTMLElement) => { if (!element) { return false; } @@ -369,7 +369,7 @@ setHovering(false); // Clear the resizing flag. setResizing(false); hoverDelayerRef.current?.cancel(); - setHovering(false); + setHovering(isPointInsideElement(e.clientX, e.clientY, sizer)); }; // Set the dragging flag From 2a965d99d1766591a96d5f1ba1bbe36826bf6d2b Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 17:00:22 +0200 Subject: [PATCH 09/20] fix: remove style sheet from body --- .../ui/positronComponents/splitters/verticalSplitter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx index bbbe1f348f68..9dcc2c2f4afe 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx @@ -364,7 +364,7 @@ setHovering(false); sizer.removeEventListener('lostpointercapture', lostPointerCaptureHandler); // Remove the style sheet. - sizer.removeChild(styleSheet); + body.removeChild(styleSheet); // Clear the resizing flag. setResizing(false); From d9e9227ded16d9b3acc13219b3b653c582497a85 Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 17:09:02 +0200 Subject: [PATCH 10/20] remove hover delay state (just set the delayer value); remove sashRef and use event targets --- .../splitters/horizontalSplitter.tsx | 16 ++++---- .../splitters/verticalSplitter.tsx | 39 +++++++++---------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index 2a1207d3f846..4fa54dfedd56 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -45,13 +45,12 @@ export const HorizontalSplitter = (props: { // Context hooks. const services = usePositronReactServicesContext(); + // Reference hooks. + const hoverDelayerRef = useRef>(undefined); + // State hooks. const [resizing, setResizing] = useState(false); const [hovering, setHovering] = useState(false); - const [hoverDelay, setHoverDelay] = useState(getHoverDelay(services.configurationService)); - - // Ref hooks. - const hoverDelayerRef = useRef>(undefined!); // Main useEffect. useEffect(() => { @@ -59,14 +58,15 @@ export const HorizontalSplitter = (props: { const disposables = new DisposableStore(); // Set the hover delayer. - hoverDelayerRef.current = disposables.add(new Delayer(0)); + const hoverDelay = getHoverDelay(services.configurationService); + hoverDelayerRef.current = disposables.add(new Delayer(hoverDelay)); // Add the onDidChangeConfiguration event handler. disposables.add( services.configurationService.onDidChangeConfiguration(e => { // Track changes to workbench.sash.hoverDelay. - if (e.affectedKeys.has('workbench.sash.hoverDelay')) { - setHoverDelay(getHoverDelay(services.configurationService)); + if (e.affectedKeys.has('workbench.sash.hoverDelay') && hoverDelayerRef.current) { + hoverDelayerRef.current.defaultDelay = getHoverDelay(services.configurationService); } }) ); @@ -79,7 +79,7 @@ export const HorizontalSplitter = (props: { * onPointerEnter handler. */ const pointerEnterHandler = () => { - hoverDelayerRef.current?.trigger(() => setHovering(true), hoverDelay); + hoverDelayerRef.current?.trigger(() => setHovering(true)); }; /** diff --git a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx index 9dcc2c2f4afe..2d62a2ccc319 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx @@ -148,8 +148,8 @@ export const VerticalSplitter = ({ const services = usePositronReactServicesContext(); // Reference hooks. - const sashRef = useRef(undefined!); const expandCollapseButtonRef = useRef(undefined!); + const hoverDelayerRef = useRef>(undefined); // State hooks. const [splitterWidth, setSplitterWidth] = useState( @@ -159,22 +159,19 @@ export const VerticalSplitter = ({ calculateSashWidth(services.configurationService, collapsible) ); const [sashIndicatorWidth, setSashIndicatorWidth] = useState(getSashSize(services.configurationService)); - const [hoverDelay, setHoverDelay] = useState(getHoverDelay(services.configurationService)); const [hovering, setHovering] = useState(false); const [highlightExpandCollapse, setHighlightExpandCollapse] = useState(false); - const [collapsed, setCollapsed, collapsedRef] = useStateRef(isCollapsed); + const [collapsed, setCollapsed, collapsedRef] = useStateRef(isCollapsed); const [resizing, setResizing] = useState(false); - // Ref hooks. - const hoverDelayerRef = useRef>(undefined); - // Main useEffect. useEffect(() => { // Create the disposable store for cleanup. const disposableStore = new DisposableStore(); // Set the hover delayer. - hoverDelayerRef.current = disposableStore.add(new Delayer(0)); + const hoverDelay = getHoverDelay(services.configurationService); + hoverDelayerRef.current = disposableStore.add(new Delayer(hoverDelay)); // Add the onDidChangeConfiguration event handler. disposableStore.add( @@ -189,8 +186,8 @@ export const VerticalSplitter = ({ } // Track changes to workbench.sash.hoverDelay. - if (configurationChangeEvent.affectedKeys.has('workbench.sash.hoverDelay')) { - setHoverDelay(getHoverDelay(services.configurationService)); + if (configurationChangeEvent.affectedKeys.has('workbench.sash.hoverDelay') && hoverDelayerRef.current) { + hoverDelayerRef.current.defaultDelay = getHoverDelay(services.configurationService); } } }) @@ -210,14 +207,15 @@ export const VerticalSplitter = ({ * @param e A PointerEvent that describes a user interaction with the pointer. */ const sashPointerEnterHandler = (e: React.PointerEvent) => { + const sash = e.currentTarget; hoverDelayerRef.current?.trigger(() => { setHovering(true); - const rect = sashRef.current.getBoundingClientRect(); + const rect = sash.getBoundingClientRect(); if (e.clientY >= rect.top + EXPAND_COLLAPSE_BUTTON_TOP && e.clientY <= rect.top + EXPAND_COLLAPSE_BUTTON_TOP + EXPAND_COLLAPSE_BUTTON_SIZE) { setHighlightExpandCollapse(true); } - }, hoverDelay); + }); }; /** @@ -228,7 +226,7 @@ export const VerticalSplitter = ({ // When not resizing, unset hover. if (!resizing) { hoverDelayerRef.current?.cancel(); -setHovering(false); + setHovering(false); } }; @@ -248,7 +246,7 @@ setHovering(false); */ const expandCollapseButtonPointerLeaveHandler = (e: React.PointerEvent) => { hoverDelayerRef.current?.cancel(); -setHovering(false); + setHovering(false); setHighlightExpandCollapse(false); }; @@ -294,7 +292,7 @@ setHovering(false); // Setup the resize state. const resizeParams = onBeginResize(); const startingWidth = collapsed ? sashWidth : resizeParams.startingWidth; - const sizer = e.currentTarget; + const sash = e.currentTarget; const body = DOM.getWindow(e.currentTarget).document.body; const clientX = e.clientX; const styleSheet = createStyleSheet(body); @@ -360,8 +358,8 @@ setHovering(false); pointerMoveHandler(e); // Remove our pointer event handlers. - sizer.removeEventListener('pointermove', pointerMoveHandler); - sizer.removeEventListener('lostpointercapture', lostPointerCaptureHandler); + sash.removeEventListener('pointermove', pointerMoveHandler); + sash.removeEventListener('lostpointercapture', lostPointerCaptureHandler); // Remove the style sheet. body.removeChild(styleSheet); @@ -369,7 +367,7 @@ setHovering(false); // Clear the resizing flag. setResizing(false); hoverDelayerRef.current?.cancel(); - setHovering(isPointInsideElement(e.clientX, e.clientY, sizer)); + setHovering(isPointInsideElement(e.clientX, e.clientY, sash)); }; // Set the dragging flag @@ -377,9 +375,9 @@ setHovering(false); // Set the capture target of future pointer events to be the current target and add our // pointer event handlers. - sizer.setPointerCapture(e.pointerId); - sizer.addEventListener('pointermove', pointerMoveHandler); - sizer.addEventListener('lostpointercapture', lostPointerCaptureHandler); + sash.setPointerCapture(e.pointerId); + sash.addEventListener('pointermove', pointerMoveHandler); + sash.addEventListener('lostpointercapture', lostPointerCaptureHandler); }; // Render. @@ -394,7 +392,6 @@ setHovering(false); }} >
Date: Mon, 20 Apr 2026 17:15:20 +0200 Subject: [PATCH 11/20] rename sizer to sash --- .../splitters/horizontalSplitter.css | 6 +++--- .../splitters/horizontalSplitter.tsx | 21 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.css b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.css index 3d72a9626868..b69456e1f9ac 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.css +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.css @@ -16,7 +16,7 @@ } .horizontal-splitter -.sizer { +.sash { top: -2px; height: 5px; z-index: 27; @@ -26,12 +26,12 @@ } .horizontal-splitter -.sizer.hovering { +.sash.hovering { transition: background-color 0.1s ease-out; background-color: var(--vscode-sash-hoverBorder); } .horizontal-splitter -.sizer.resizing { +.sash.resizing { background-color: var(--vscode-sash-hoverBorder); } diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index 4fa54dfedd56..f1a77cd83614 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -108,8 +108,8 @@ export const HorizontalSplitter = (props: { // Setup the resize state. const resizeParams = props.onBeginResize(); - const sizer = e.currentTarget; - const body = DOM.getWindow(sizer).document.body; + const sash = e.currentTarget; + const body = DOM.getWindow(sash).document.body; const clientY = e.clientY; const styleSheet = createStyleSheet(body); @@ -145,7 +145,8 @@ export const HorizontalSplitter = (props: { } // Update the style sheet's text content with the desired cursor and - // disable text selection during the resize operation. + // disable text selection during the resize operation. This is a clever + // technique adopted from src/vs/base/browser/ui/sash/sash.ts. styleSheet.textContent = `* { cursor: ${cursor} !important; user-select: none !important; }`; // Call the onResize callback. @@ -165,8 +166,8 @@ export const HorizontalSplitter = (props: { } // Remove our pointer event handlers. - sizer.removeEventListener('pointermove', pointerMoveHandler); - sizer.removeEventListener('lostpointercapture', lostPointerCaptureHandler); + sash.removeEventListener('pointermove', pointerMoveHandler); + sash.removeEventListener('lostpointercapture', lostPointerCaptureHandler); // Remove the style sheet. body.removeChild(styleSheet); @@ -174,7 +175,7 @@ export const HorizontalSplitter = (props: { // Clear the resizing flag. setResizing(false); hoverDelayerRef.current?.cancel(); - setHovering(isPointInsideElement(e.clientX, e.clientY, sizer)); + setHovering(isPointInsideElement(e.clientX, e.clientY, sash)); }; /** @@ -198,9 +199,9 @@ export const HorizontalSplitter = (props: { setHovering(true); // Set pointer capture on the sizer element and add our pointer event handlers. - sizer.setPointerCapture(e.pointerId); - sizer.addEventListener('pointermove', pointerMoveHandler); - sizer.addEventListener('lostpointercapture', lostPointerCaptureHandler); + sash.setPointerCapture(e.pointerId); + sash.addEventListener('pointermove', pointerMoveHandler); + sash.addEventListener('lostpointercapture', lostPointerCaptureHandler); }; /** @@ -218,7 +219,7 @@ export const HorizontalSplitter = (props: { {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
Date: Mon, 20 Apr 2026 17:15:39 +0200 Subject: [PATCH 12/20] set role and orientation --- .../ui/positronComponents/splitters/horizontalSplitter.tsx | 3 ++- .../ui/positronComponents/splitters/verticalSplitter.tsx | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index f1a77cd83614..cb5db69b339a 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -216,13 +216,14 @@ export const HorizontalSplitter = (props: { // Render. return (
- {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
Date: Mon, 20 Apr 2026 17:31:17 +0200 Subject: [PATCH 13/20] fix height offset while scrolling --- .../browser/notebookCells/NotebookCodeCell.css | 8 +++++++- .../browser/notebookCells/NotebookCodeCell.tsx | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.css b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.css index e32c1018d390..aed539c3ac87 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.css +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.css @@ -56,8 +56,14 @@ overflow-y: auto; } - /* When user has manually resized, the inline style controls height */ + /* + When user has manually resized, the inline style controls height. + border-box ensures offsetHeight (used to seed the drag) and + style.height (written during drag) are in the same coordinate space, + preventing a visual jump at the start of a resize. + */ &.height-override { + box-sizing: border-box; overflow-y: auto; } overflow-x: auto; diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx index b83d2e77c41b..ef0ebfb2ce2b 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx @@ -235,7 +235,7 @@ const CellOutputsSection = React.memo(function CellOutputsSection({ cell, output )) }
- {outputScrolling && !isCollapsed && outputs.length > 0 && + {outputScrolling && !isCollapsed && hasOutputs && Date: Mon, 20 Apr 2026 17:35:04 +0200 Subject: [PATCH 14/20] remove math.trunc - will double check --- .../ui/positronComponents/splitters/verticalSplitter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx index 1fd7892cc08a..93ea02fee5ab 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx @@ -307,7 +307,7 @@ export const VerticalSplitter = ({ e.stopPropagation(); // Calculate the delta. - const delta = Math.trunc(e.clientX - clientX); + const delta = e.clientX - clientX; // Calculate the new width. let newWidth = !invert ? From d0310bfd4a393bdc15fbe899d6abf1299ef48c4c Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 18:50:17 +0200 Subject: [PATCH 15/20] clean up tests --- test/e2e/pages/notebooksPositron.ts | 55 ++++++++ .../notebook-output-resize.test.ts | 122 +++--------------- 2 files changed, 76 insertions(+), 101 deletions(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index 3c7c778603a9..ac08239628ae 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -68,6 +68,7 @@ export class PositronNotebooks extends Notebooks { // Cell outputs cellOutput = (index: number) => this.cell.nth(index).getByTestId('cell-output'); + cellOutputSash = (index: number) => this.cellOutput(index).locator('.horizontal-splitter .sash'); private outputActionBar = (index: number) => this.cell.nth(index).locator('.cell-output-action-bar'); outputCollapsedLabel = (index: number) => this.cellOutput(index).getByText('Output collapsed'); outputTruncationMessage = (index: number) => this.cellOutput(index).getByText(/\.\.\. Show [\d,.\s\u00A0]+ more lines/); @@ -637,6 +638,49 @@ export class PositronNotebooks extends Notebooks { }); } + /** + * Action: Drag the output resize sash for a cell by a given distance. + * @param cellIndex - The index of the cell whose output sash to drag. + * @param distance - The vertical distance in pixels to drag (positive = down). + */ + async dragCellOutputSash(cellIndex: number, distance: number) { + const page = this.code.driver.page; + const sash = this.cellOutputSash(cellIndex); + + // Reveal the sash for debugging. + await sash.scrollIntoViewIfNeeded(); + + // Get the sash's starting position. + const box = await sash.boundingBox(); + expect(box).toBeTruthy(); + const startX = box!.x + box!.width / 2; + const startY = box!.y + box!.height / 2; + + // Drag the sash down to grow the output area. + // await sash.hover(); + await page.mouse.move(startX, startY); + await page.mouse.down(); + await page.mouse.move(startX, startY + distance, { steps: 10 }); + await page.mouse.up(); + + // Reveal the final sash position for debugging. + await sash.scrollIntoViewIfNeeded(); + } + + /** + * Get the height of a cell's output area. + * @param cellIndex - The index of the cell to measure + * @returns The height of the cell's output area in pixels + */ + async getCellOutputHeight(cellIndex: number): Promise { + const output = this.cellOutput(cellIndex); + const box = await output.boundingBox(); + if (!box) { + throw new Error(`Could not get bounding box for cell output at index ${cellIndex}`); + } + return box.height; + } + /** * Action: Create a new code cell at the END of the notebook. */ @@ -1394,6 +1438,17 @@ export class PositronNotebooks extends Notebooks { }); } + /** + * Verify: the height of the cell's output area matches expected height. + * @param cellIndex - The index of the cell to check. + * @param height - The expected height of the cell's output area in pixels. + */ + async expectCellOutputHeight(cellIndex: number, height: number): Promise { + await test.step(`Verify cell output height at index ${cellIndex} is ${height}px`, async () => { + expect(await this.getCellOutputHeight(cellIndex)).toBe(height); + }); + } + /** * Verify: the cell at the specified index is fully visible within the * notebook scroll container. For cells taller than the viewport, checks diff --git a/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts b/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts index f14da14c5b2c..9e1a9cc33b84 100644 --- a/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts +++ b/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts @@ -10,16 +10,15 @@ test.use({ suiteId: __filename }); -test.describe('Positron Notebooks: Output Resize Handle', { +test.describe('Positron Notebooks: Output Resize', { tag: [tags.WIN, tags.WEB, tags.POSITRON_NOTEBOOKS] }, () => { - test('Drag handle resizes scrollable cell output', async function ({ app, page, settings }) { + test('Drag sash resizes scrollable cell output', async function ({ app, page, settings }) { const { notebooks, notebooksPositron } = app.workbench; await test.step('Setup: Create notebook with scrollable output', async () => { - // Enable output scrolling explicitly -- the default depends on - // product.quality and may be false in local dev builds. + // Enable output scrolling explicitly - the default differs in dev vs release builds. await settings.set({ 'notebook.output.scrolling': true }); await notebooks.createNewNotebook(); @@ -27,123 +26,44 @@ test.describe('Positron Notebooks: Output Resize Handle', { await notebooksPositron.kernel.select('Python'); // Generate output that exceeds the scrollable area so the resize - // handle appears. + // sash appears. await notebooksPositron.addCodeToCell(0, 'for i in range(100): print(f"line {i}")', { run: true }); await notebooksPositron.expectOutputAtIndex(0, ['line 0']); }); - const cellOutput = notebooksPositron.cellOutput(0); - const splitter = cellOutput.locator('.horizontal-splitter'); - const resizeHandle = splitter.locator('.sizer'); - const outputInner = cellOutput.locator('.positron-notebook-code-cell-outputs-inner'); + const sash = notebooksPositron.cellOutputSash(0); - await test.step('Resize handle is visible for scrollable output', async () => { - await expect(splitter).toBeVisible({ timeout: 10000 }); + await test.step('Resize sash is visible for scrollable output', async () => { + await expect(sash).toBeVisible(); }); - await test.step('Dragging the handle changes the output height', async () => { - const initialBox = await outputInner.boundingBox(); - expect(initialBox).toBeTruthy(); - const initialHeight = initialBox!.height; + const initialHeight = await notebooksPositron.getCellOutputHeight(0); - // Drag the resize handle downward to grow the output area. - // The HorizontalSplitter uses setPointerCapture on document.body, - // so we dispatch native PointerEvent objects to ensure capture - // and event routing work correctly. - const handleBox = await resizeHandle.boundingBox(); - expect(handleBox).toBeTruthy(); - const startX = handleBox!.x + handleBox!.width / 2; - const startY = handleBox!.y + handleBox!.height / 2; + await test.step('Dragging the sash changes the output height', async () => { const dragDistance = 150; - - await resizeHandle.evaluate((el, { startX, startY, dragDistance }) => { - const body = document.body; - - el.dispatchEvent(new PointerEvent('pointerdown', { - clientX: startX, clientY: startY, - pointerId: 1, pointerType: 'mouse', - buttons: 1, bubbles: true, cancelable: true, - })); - - for (let i = 1; i <= 5; i++) { - body.dispatchEvent(new PointerEvent('pointermove', { - clientX: startX, - clientY: startY + (dragDistance * i) / 5, - pointerId: 1, pointerType: 'mouse', - buttons: 1, bubbles: true, cancelable: true, - })); - } - - // Include final position -- the splitter calls pointerMoveHandler - // with this event for one last update before cleanup. - body.dispatchEvent(new PointerEvent('lostpointercapture', { - clientX: startX, - clientY: startY + dragDistance, - pointerId: 1, pointerType: 'mouse', - bubbles: true, - })); - }, { startX, startY, dragDistance }); - - // The output container should now be taller - await expect(async () => { - const newBox = await outputInner.boundingBox(); - expect(newBox).toBeTruthy(); - expect(newBox!.height).toBeGreaterThan(initialHeight + 50); - }).toPass({ timeout: 5000 }); + await notebooksPositron.dragCellOutputSash(0, dragDistance); + await notebooksPositron.expectCellOutputHeight(0, initialHeight + dragDistance); }); - await test.step('Double-clicking the handle resets to default height', async () => { - // The output inner should have a height-override class after resizing - await expect(outputInner).toHaveClass(/height-override/); - - // Dispatch dblclick directly to avoid triggering pointerdown - // drag handlers that would re-set the height override. - await resizeHandle.dispatchEvent('dblclick', {}); - - // After reset, the height-override class should be removed - await expect(outputInner).not.toHaveClass(/height-override/, { timeout: 5000 }); + await test.step('Double-clicking the sash resets to default height', async () => { + await sash.click({ clickCount: 2 }); + await notebooksPositron.expectCellOutputHeight(0, initialHeight); }); - await test.step('Resize handle is hidden when output is collapsed', async () => { + await test.step('Sash is hidden when output is collapsed', async () => { + await notebooksPositron.outputCollapseToggle(0).scrollIntoViewIfNeeded(); await notebooksPositron.outputCollapseToggle(0).click(); - await expect(notebooksPositron.outputCollapsedLabel(0)).toBeVisible(); - await expect(splitter).toBeHidden(); + await expect(sash).toBeHidden(); // Expand again await notebooksPositron.outputCollapseToggle(0).click(); - await expect(splitter).toBeVisible(); + await expect(sash).toBeVisible(); }); - await test.step('Re-running cell resets the resize override', async () => { - // Resize first - const handleBox = await resizeHandle.boundingBox(); - expect(handleBox).toBeTruthy(); - const startX = handleBox!.x + handleBox!.width / 2; - const startY = handleBox!.y + handleBox!.height / 2; - - await resizeHandle.evaluate((el, { startX, startY }) => { - const body = document.body; - el.dispatchEvent(new PointerEvent('pointerdown', { - clientX: startX, clientY: startY, - pointerId: 1, pointerType: 'mouse', - buttons: 1, bubbles: true, cancelable: true, - })); - body.dispatchEvent(new PointerEvent('pointermove', { - clientX: startX, clientY: startY + 100, - pointerId: 1, pointerType: 'mouse', - buttons: 1, bubbles: true, cancelable: true, - })); - body.dispatchEvent(new PointerEvent('lostpointercapture', { - clientX: startX, clientY: startY + 100, - pointerId: 1, pointerType: 'mouse', bubbles: true, - })); - }, { startX, startY }); - - await expect(outputInner).toHaveClass(/height-override/); - - // Re-run the cell - should reset the height override + await test.step('Re-running cell resets height', async () => { + await notebooksPositron.dragCellOutputSash(0, 100); await notebooksPositron.runCodeAtIndex(0); - await expect(outputInner).not.toHaveClass(/height-override/, { timeout: 15000 }); + await notebooksPositron.expectCellOutputHeight(0, initialHeight); }); }); }); From 2f2d4511b17d725d0b83df80a5ac577f930d5c02 Mon Sep 17 00:00:00 2001 From: seem Date: Mon, 20 Apr 2026 20:04:49 +0200 Subject: [PATCH 16/20] reload on web --- .../e2e/tests/notebooks-positron/notebook-output-resize.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts b/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts index 9e1a9cc33b84..193e4dd5c3e7 100644 --- a/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts +++ b/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts @@ -19,7 +19,7 @@ test.describe('Positron Notebooks: Output Resize', { await test.step('Setup: Create notebook with scrollable output', async () => { // Enable output scrolling explicitly - the default differs in dev vs release builds. - await settings.set({ 'notebook.output.scrolling': true }); + await settings.set({ 'notebook.output.scrolling': true }, { reload: 'web' }); await notebooks.createNewNotebook(); await notebooksPositron.expectCellCountToBe(1); From 22e5658f2c9814258dd39e2a915456bb45072a69 Mon Sep 17 00:00:00 2001 From: Wasim Lorgat Date: Wed, 22 Apr 2026 18:27:31 +0200 Subject: [PATCH 17/20] Update test/e2e/pages/notebooksPositron.ts Co-authored-by: Nick Strayer Signed-off-by: Wasim Lorgat --- test/e2e/pages/notebooksPositron.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/pages/notebooksPositron.ts b/test/e2e/pages/notebooksPositron.ts index ac08239628ae..15ca063c6402 100644 --- a/test/e2e/pages/notebooksPositron.ts +++ b/test/e2e/pages/notebooksPositron.ts @@ -657,7 +657,6 @@ export class PositronNotebooks extends Notebooks { const startY = box!.y + box!.height / 2; // Drag the sash down to grow the output area. - // await sash.hover(); await page.mouse.move(startX, startY); await page.mouse.down(); await page.mouse.move(startX, startY + distance, { steps: 10 }); From 33959fbf732d04c40adf83c07f388638fc9935a7 Mon Sep 17 00:00:00 2001 From: seem Date: Wed, 22 Apr 2026 18:38:06 +0200 Subject: [PATCH 18/20] address reviews --- .../splitters/horizontalSplitter.tsx | 3 +-- .../splitters/verticalSplitter.tsx | 2 -- .../browser/notebookCells/NotebookCodeCell.tsx | 9 +++++++-- test/e2e/pages/notebooksPositron.ts | 18 +++++++++++++++--- .../notebook-output-resize.test.ts | 8 ++++---- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index cb5db69b339a..f1a77cd83614 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -216,14 +216,13 @@ export const HorizontalSplitter = (props: { // Render. return (
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
{ - await test.step(`Verify cell output height at index ${cellIndex} is ${height}px`, async () => { - expect(await this.getCellOutputHeight(cellIndex)).toBe(height); + async expectCellOutputHeight( + cellIndex: number, + height: number, + { tolerance = 0 }: { tolerance?: number } = {} + ): Promise { + await test.step(`Verify cell output height at index ${cellIndex} is ${height}px (±${tolerance}px)`, async () => { + const actual = await this.getCellOutputHeight(cellIndex); + if (tolerance === 0) { + expect(actual).toBe(height); + } else { + expect(actual).toBeGreaterThanOrEqual(height - tolerance); + expect(actual).toBeLessThanOrEqual(height + tolerance); + } }); } diff --git a/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts b/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts index 193e4dd5c3e7..fd73d9f1eb88 100644 --- a/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts +++ b/test/e2e/tests/notebooks-positron/notebook-output-resize.test.ts @@ -14,7 +14,7 @@ test.describe('Positron Notebooks: Output Resize', { tag: [tags.WIN, tags.WEB, tags.POSITRON_NOTEBOOKS] }, () => { - test('Drag sash resizes scrollable cell output', async function ({ app, page, settings }) { + test('Drag sash resizes scrollable cell output', async function ({ app, settings }) { const { notebooks, notebooksPositron } = app.workbench; await test.step('Setup: Create notebook with scrollable output', async () => { @@ -42,12 +42,12 @@ test.describe('Positron Notebooks: Output Resize', { await test.step('Dragging the sash changes the output height', async () => { const dragDistance = 150; await notebooksPositron.dragCellOutputSash(0, dragDistance); - await notebooksPositron.expectCellOutputHeight(0, initialHeight + dragDistance); + await notebooksPositron.expectCellOutputHeight(0, initialHeight + dragDistance, { tolerance: 5 }); }); await test.step('Double-clicking the sash resets to default height', async () => { await sash.click({ clickCount: 2 }); - await notebooksPositron.expectCellOutputHeight(0, initialHeight); + await notebooksPositron.expectCellOutputHeight(0, initialHeight, { tolerance: 5 }); }); await test.step('Sash is hidden when output is collapsed', async () => { @@ -63,7 +63,7 @@ test.describe('Positron Notebooks: Output Resize', { await test.step('Re-running cell resets height', async () => { await notebooksPositron.dragCellOutputSash(0, 100); await notebooksPositron.runCodeAtIndex(0); - await notebooksPositron.expectCellOutputHeight(0, initialHeight); + await notebooksPositron.expectCellOutputHeight(0, initialHeight, { tolerance: 5 }); }); }); }); From 5ef2371cf14a7d4638186956711860969d679a54 Mon Sep 17 00:00:00 2001 From: seem Date: Wed, 22 Apr 2026 19:28:12 +0200 Subject: [PATCH 19/20] re-add truncation --- .../ui/positronComponents/splitters/horizontalSplitter.tsx | 2 +- .../ui/positronComponents/splitters/verticalSplitter.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx index f1a77cd83614..24501e95a93c 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/horizontalSplitter.tsx @@ -185,7 +185,7 @@ export const HorizontalSplitter = (props: { */ const calculateNewHeight = (e: PointerEvent) => { // Calculate the delta. - const delta = e.clientY - clientY; + const delta = Math.trunc(e.clientY - clientY); // Calculate the new height. return !resizeParams.invert ? diff --git a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx index 153932aad699..2d62a2ccc319 100644 --- a/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx +++ b/src/vs/base/browser/ui/positronComponents/splitters/verticalSplitter.tsx @@ -307,7 +307,7 @@ export const VerticalSplitter = ({ e.stopPropagation(); // Calculate the delta. - const delta = e.clientX - clientX; + const delta = Math.trunc(e.clientX - clientX); // Calculate the new width. let newWidth = !invert ? From b056b8ad27aa2db97ff8b5ca727e210518f433ee Mon Sep 17 00:00:00 2001 From: seem Date: Thu, 23 Apr 2026 17:34:37 +0200 Subject: [PATCH 20/20] cap the max height to content height --- .../browser/notebookCells/NotebookCodeCell.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx index 0c4c7d536ba1..033f356b839a 100644 --- a/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx +++ b/src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCodeCell.tsx @@ -42,8 +42,6 @@ import { HorizontalSplitter, HorizontalSplitterResizeParams } from '../../../../ /** The minimum height (pixels) that scrollable outputs can be resized to. */ const MINIMUM_SCROLLABLE_OUTPUT_HEIGHT = 50; -/** The maximum height (pixels) that scrollable outputs can be resized to. */ -const MAXIMUM_SCROLLABLE_OUTPUT_HEIGHT = 2000; const copyOutputTextLabel = localize('positron.notebook.copyOutputText', "Copy Output Text"); const expandOutputTooltip = localize('positron.notebook.expandOutput', "Click to Expand Output"); @@ -94,7 +92,8 @@ const CellOutputsSection = React.memo(function CellOutputsSection({ cell, output return { startingHeight: el?.offsetHeight ?? 0, minimumHeight: MINIMUM_SCROLLABLE_OUTPUT_HEIGHT, - maximumHeight: MAXIMUM_SCROLLABLE_OUTPUT_HEIGHT, + // Cap the max height to the output content. + maximumHeight: Math.max(el?.scrollHeight ?? 0, MINIMUM_SCROLLABLE_OUTPUT_HEIGHT), }; }, []);