From db07a2b60ae7eaea0e36b647cc313b27cce715c6 Mon Sep 17 00:00:00 2001 From: aamunger Date: Mon, 13 Mar 2023 15:55:13 -0700 Subject: [PATCH] updated stream output to consolidate correctly --- extensions/notebook-renderers/src/index.ts | 93 +++++++++---------- .../notebook-renderers/src/textHelper.ts | 60 +++++++----- .../view/renderers/backLayerWebView.ts | 9 +- 3 files changed, 87 insertions(+), 75 deletions(-) diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 2bd2732ec3a..59f5d10c762 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -201,15 +201,20 @@ function renderError( function getPreviousOutputWithMatchingMimeType(container: HTMLElement, mimeType: string) { const outputContainer = container.parentElement; + let match: HTMLElement | undefined = undefined; - const previous = outputContainer?.previousSibling; - if (previous) { + let previous = outputContainer?.previousSibling; + while (previous) { const outputElement = (previous.firstChild as HTMLElement | null); - if (outputElement && outputElement.getAttribute('output-mime-type') === mimeType) { - return outputElement; + if (!outputElement || outputElement.getAttribute('output-mime-type') !== mimeType) { + break; } + + match = outputElement; + previous = outputContainer?.previousSibling; } - return undefined; + + return match; } function onScrollHandler(e: globalThis.Event) { @@ -224,16 +229,15 @@ function onScrollHandler(e: globalThis.Event) { // if there is a scrollable output, it will be scrolled to the given value if provided or the bottom of the element function appendChildAndScroll(container: HTMLElement, child: HTMLElement, disposables: DisposableStore, scrollTop?: number) { container.appendChild(child); - if (child.classList.contains(scrollableClass)) { - child.scrollTop = scrollTop !== undefined ? scrollTop : child.scrollHeight; - child.addEventListener('scroll', onScrollHandler); - disposables.push({ dispose: () => child.removeEventListener('scroll', onScrollHandler) }); + if (container.classList.contains(scrollableClass)) { + container.scrollTop = scrollTop !== undefined ? scrollTop : container.scrollHeight; + container.addEventListener('scroll', onScrollHandler); + disposables.push({ dispose: () => container.removeEventListener('scroll', onScrollHandler) }); } } // Find the scrollTop of the existing scrollable output, return undefined if at the bottom or element doesn't exist -function findScrolledHeight(outputContainer: HTMLElement, outputId: string): number | undefined { - const scrollableElement = outputContainer.querySelector(`[output-item-id="${outputId}"].${scrollableClass}`); +function findScrolledHeight(scrollableElement: HTMLElement): number | undefined { if (scrollableElement && scrollableElement.scrollHeight - scrollableElement.scrollTop - scrollableElement.clientHeight > 2) { // not scrolled to the bottom return scrollableElement.scrollTop; @@ -245,46 +249,37 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo const disposableStore = createDisposableStore(); const outputScrolling = ctx.settings.outputScrolling; - // If the previous output item for the same cell was also a stream, append this output to the previous - const outputElement = getPreviousOutputWithMatchingMimeType(container, outputInfo.mime); - if (outputElement) { - // find child with same id - const existing = outputElement.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; - if (existing) { - clearContainer(existing); - } - - const text = outputInfo.text(); - const element = existing ?? document.createElement('span'); - element.classList.add('output-stream'); - element.classList.toggle('wordWrap', ctx.settings.outputWordWrap); - disposableStore.push(ctx.onDidChangeSettings(e => { - element.classList.toggle('wordWrap', e.outputWordWrap); - })); - element.setAttribute('output-item-id', outputInfo.id); - const content = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); - appendChildAndScroll(outputElement, content, disposableStore); - return disposableStore; - } - + container.setAttribute('output-mime-type', outputInfo.mime); const text = outputInfo.text(); const content = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); - - content.classList.add('output-stream'); - content.classList.toggle('wordWrap', ctx.settings.outputWordWrap); - disposableStore.push(ctx.onDidChangeSettings(e => { - content.classList.toggle('wordWrap', e.outputWordWrap); - })); content.setAttribute('output-item-id', outputInfo.id); - - const scrollTop = outputScrolling ? findScrolledHeight(container, outputInfo.id) : undefined; - while (container.firstChild) { - container.removeChild(container.firstChild); - } - appendChildAndScroll(container, content, disposableStore, scrollTop); - content.setAttribute('output-mime-type', outputInfo.mime); if (error) { - container.classList.add('error'); + content.classList.add('error'); + } + const scrollTop = outputScrolling ? findScrolledHeight(container) : undefined; + + // If the previous output item for the same cell was also a stream, append this output to the previous + const existingContainer = getPreviousOutputWithMatchingMimeType(container, outputInfo.mime); + if (existingContainer) { + const existing = existingContainer.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; + if (existing) { + existing.replaceWith(content); + } else { + existingContainer.appendChild(content); + } + } else { + container.classList.toggle('scrollable', outputScrolling); + container.classList.add('output-stream'); + container.classList.toggle('wordWrap', ctx.settings.outputWordWrap); + disposableStore.push(ctx.onDidChangeSettings(e => { + container.classList.toggle('wordWrap', e.outputWordWrap); + })); + + + while (container.firstChild) { + container.removeChild(container.firstChild); + } + appendChildAndScroll(container, content, disposableStore, scrollTop); } return disposableStore; @@ -336,7 +331,7 @@ export const activate: ActivationFunction = (ctx) => { .traceback.wordWrap span { white-space: pre-wrap; } - .output .scrollable { + .output.scrollable { overflow-y: scroll; max-height: var(--notebook-cell-output-max-height); border: var(--vscode-editorWidget-border); @@ -345,7 +340,7 @@ export const activate: ActivationFunction = (ctx) => { box-sizing: border-box; border-width: 1px; } - .output .scrollable.more-above { + .output.scrollable.more-above { box-shadow: var(--vscode-scrollbar-shadow) 0 6px 6px -6px inset } .output-plaintext .code-bold, diff --git a/extensions/notebook-renderers/src/textHelper.ts b/extensions/notebook-renderers/src/textHelper.ts index 14bdf40e6de..46567effe3c 100644 --- a/extensions/notebook-renderers/src/textHelper.ts +++ b/extensions/notebook-renderers/src/textHelper.ts @@ -7,35 +7,50 @@ import { handleANSIOutput } from './ansi'; export const scrollableClass = 'scrollable'; -function generateViewMoreElement(outputId: string, adjustableSize: boolean) { +function generateViewMoreElement(outputId: string) { const container = document.createElement('span'); const first = document.createElement('span'); + first.textContent = 'Output exceeds the '; - if (adjustableSize) { - first.textContent = 'Output exceeds the '; - const second = document.createElement('a'); - second.textContent = 'size limit'; - second.href = `command:workbench.action.openSettings?%5B%22notebook.output.textLineLimit%22%5D`; - container.appendChild(first); - container.appendChild(second); - } else { - first.textContent = 'Output exceeds the maximium size limit'; - container.appendChild(first); - } + const second = document.createElement('a'); + second.textContent = 'size limit'; + second.href = `command:workbench.action.openSettings?%5B%22notebook.output.textLineLimit%22%5D`; + container.appendChild(first); + container.appendChild(second); const third = document.createElement('span'); third.textContent = '. Open the full output data '; + const forth = document.createElement('a'); forth.textContent = 'in a text editor'; forth.href = `command:workbench.action.openLargeOutput?${outputId}`; container.appendChild(third); container.appendChild(forth); + + return container; +} + +function generateNestedViewAllElement(outputId: string) { + const container = document.createElement('span'); + const prefix = document.createElement('span'); + prefix.innerText = '... (View full content in a '; + container.append(prefix); + + const link = document.createElement('a'); + link.textContent = 'text editor'; + link.href = `command:workbench.action.openLargeOutput?${outputId}`; + container.append(link); + + const postfix = document.createElement('span'); + prefix.innerText = ')\n'; + container.append(postfix); + return container; } function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, container: HTMLElement, trustHtml: boolean) { const lineCount = buffer.length; - container.parentNode!.append(generateViewMoreElement(id, true)); + container.appendChild(generateViewMoreElement(id)); container.appendChild(handleANSIOutput(buffer.slice(0, linesLimit - 5).join('\n'), trustHtml)); @@ -48,29 +63,28 @@ function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number } function scrollableArrayOfString(id: string, buffer: string[], container: HTMLElement, trustHtml: boolean) { - container.classList.add(scrollableClass); - if (buffer.length > 5000) { - container.parentNode!.append(generateViewMoreElement(id, false)); + container.parentNode!.append(generateNestedViewAllElement(id)); } - container.appendChild(handleANSIOutput(buffer.slice(0, 5000).join('\n'), trustHtml)); + container.appendChild(handleANSIOutput(buffer.slice(-5000).join('\n'), trustHtml)); } export function insertOutput(id: string, outputs: string[], linesLimit: number, scrollable: boolean, trustHtml: boolean) { const element = document.createElement('div'); const buffer = outputs.join('\n').split(/\r\n|\r|\n/g); + + if (scrollable) { + scrollableArrayOfString(id, buffer, element, trustHtml); + } + const lineCount = buffer.length; - if (lineCount < linesLimit) { + if (lineCount <= linesLimit) { const spanElement = handleANSIOutput(buffer.join('\n'), trustHtml); element.appendChild(spanElement); } else { - if (scrollable) { - scrollableArrayOfString(id, buffer, element, trustHtml); - } else { - truncatedArrayOfString(id, buffer, linesLimit, element, trustHtml); - } + truncatedArrayOfString(id, buffer, linesLimit, element, trustHtml); } return element; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 4b367a71c77..6b0ea8ddb54 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -323,18 +323,21 @@ export class BackLayerWebView extends Themable { width: 100%; } - #container > div > div > div.output { + #container > div > div > div.output:not(.scrollable) { font-size: var(--notebook-cell-output-font-size); width: var(--notebook-output-width); margin-left: var(--notebook-output-left-margin); + background-color: var(--theme-notebook-output-background); + } + + #container > div > div > div.output:not(.scrollable) { padding-top: var(--notebook-output-node-padding); padding-right: var(--notebook-output-node-padding); padding-bottom: var(--notebook-output-node-padding); padding-left: var(--notebook-output-node-left-padding); box-sizing: border-box; - border-top: none !important; + border-top: none; border: 1px solid var(--theme-notebook-output-border); - background-color: var(--theme-notebook-output-background); } /* markdown */