Change:
a cell with enough output to truncate scroll the link to view as a scrollable element :bug: output height is stretched to show entire output without scrollable region

Discussion:
outputElement.classList.add('output-stream'); const text = outputInfo.text(); <del> const content = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); content.setAttribute('output-item-id', outputInfo.id); </del> <ins> const newContent = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); newContent.setAttribute('output-item-id', outputInfo.id); </ins> if (error) { <del> content.classList.add('error'); </del> <ins> newContent.classList.add('error'); </ins> } const scrollTop = outputScrolling ? findScrolledHeight(outputElement) : undefined; <ins> const existingContent = outputElement.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; const previousOutputParent = getPreviousMatchingContentGroup(outputElement); </ins> // If the previous output item for the same cell was also a stream, append this output to the previous <del> const existingContentParent = getPreviousMatchingContentGroup(outputElement) || outputElement.querySelector('div'); if (existingContentParent) { const existing = existingContentParent.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; if (existing) { existing.replaceWith(content); while (content.nextSibling) { </del> <ins> if (previousOutputParent) { if (existingContent) { existingContent.replaceWith(newContent); } else { previousOutputParent.appendChild(newContent); } previousOutputParent.classList.toggle('scrollbar-visible', previousOutputParent.scrollHeight > previousOutputParent.clientHeight); previousOutputParent.scrollTop = scrollTop !== undefined ? scrollTop : previousOutputParent.scrollHeight; } else { let contentParent = existingContent?.parentElement; if (existingContent && contentParent) { existingContent.replaceWith(newContent); while (newContent.nextSibling) { </ins> // clear out any stale content if we had previously combined streaming outputs into this one <del> content.nextSibling.remove(); </del> <ins> newContent.nextSibling.remove(); </ins> } } else { <del> existingContentParent.appendChild(content); </del> <ins> contentParent = document.createElement('div'); contentParent.appendChild(newContent); while (outputElement.firstChild) { outputElement.removeChild(outputElement.firstChild); } outputElement.appendChild(contentParent); </ins> } <del> existingContentParent.classList.toggle('scrollbar-visible', existingContentParent.scrollHeight > existingContentParent.clientHeight); existingContentParent.scrollTop = scrollTop !== undefined ? scrollTop : existingContentParent.scrollHeight; } else { const contentParent = document.createElement('div'); contentParent.appendChild(content); </del> contentParent.classList.toggle('scrollable', outputScrolling); contentParent.classList.toggle('word-wrap', ctx.settings.outputWordWrap); disposableStore.push(ctx.onDidChangeSettings(e => { <del> contentParent.classList.toggle('word-wrap', e.outputWordWrap); </del> <ins> contentParent!.classList.toggle('word-wrap', e.outputWordWrap); </ins> })); <del> while (outputElement.firstChild) { outputElement.removeChild(outputElement.firstChild); } outputElement.appendChild(contentParent); </del> initializeScroll(contentParent, disposableStore, scrollTop); }

import * as assert from 'assert'; import { activate } from '..'; import { OutputItem, RendererApi } from 'vscode-notebook-renderer'; <del> import { IRichRenderContext, RenderOptions } from '../rendererTypes'; </del> <ins> import { IDisposable, IRichRenderContext, RenderOptions } from '../rendererTypes'; </ins> import { JSDOM } from "jsdom"; const dom = new JSDOM();

type optionalRenderOptions = { [k in keyof RenderOptions]?: RenderOptions[k] }; <ins> type handler = (e: RenderOptions) => any; const settingsChangedHandlers: handler[] = []; function fireSettingsChange(options: optionalRenderOptions) { settingsChangedHandlers.forEach((handler) => handler(options as RenderOptions)); } </ins> function createContext(settings?: optionalRenderOptions): IRichRenderContext { <ins> settingsChangedHandlers.length = 0; </ins> return { setState(_value: void) { }, getState() { return undefined; },

lineLimit: 30, ...settings } as RenderOptions, <del> onDidChangeSettings<T>(_listener: (e: T) => any, _thisArgs?: any, _disposables?: any) { </del> <ins> onDidChangeSettings(listener: handler, _thisArgs?: any, disposables?: IDisposable[]) { settingsChangedHandlers.push(listener); const dispose = () => { settingsChangedHandlers.splice(settingsChangedHandlers.indexOf(listener), 1); }; disposables?.push({ dispose }); </ins> return { <del> dispose(): void { } </del> <ins> dispose </ins> }; }, workspace: {

assert.ok(inserted.innerHTML.indexOf('>second stream content</') === -1, `Content was not cleared: ${outputElement.innerHTML}`); assert.ok(inserted.innerHTML.indexOf('>third stream content</') === -1, `Content was not cleared: ${outputElement.innerHTML}`); }); <ins> test(`Rendered output will wrap on settings change event`, async () => { const context = createContext({ outputWordWrap: false, outputScrolling: true }); const renderer = await activate(context); assert.ok(renderer, 'Renderer not created'); const outputElement = new OutputHtml().getFirstOuputElement(); const outputItem = createOutputItem('content', stdoutMimeType); await renderer!.renderOutputItem(outputItem, outputElement); fireSettingsChange({ outputWordWrap: true, outputScrolling: true }); const inserted = outputElement.firstChild as HTMLElement; assert.ok(inserted.classList.contains('word-wrap') && inserted.classList.contains('scrollable'), `output content classList should contain word-wrap and scrollable ${inserted.classList}`); }); test(`Settings event change listeners should not grow if output is re-rendered`, async () => { const context = createContext({ outputWordWrap: false }); const renderer = await activate(context); assert.ok(renderer, 'Renderer not created'); const outputElement = new OutputHtml().getFirstOuputElement(); await renderer!.renderOutputItem(createOutputItem('content', stdoutMimeType), outputElement); const handlerCount = settingsChangedHandlers.length; await renderer!.renderOutputItem(createOutputItem('content', stdoutMimeType), outputElement); assert.equal(settingsChangedHandlers.length, handlerCount); }); </ins> });