mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 20:26:08 +00:00
scroll to the bottom of scrollable outputs
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer';
|
||||
import { insertOutput } from './textHelper';
|
||||
import { insertOutput, scrollableClass } from './textHelper';
|
||||
|
||||
interface IDisposable {
|
||||
dispose(): void;
|
||||
@@ -176,7 +176,7 @@ function renderError(
|
||||
}
|
||||
|
||||
if (err.stack) {
|
||||
const stack = document.createElement('pre');
|
||||
const stack = document.createElement('span');
|
||||
stack.classList.add('traceback');
|
||||
if (ctx.settings.outputWordWrap) {
|
||||
stack.classList.add('wordWrap');
|
||||
@@ -185,11 +185,8 @@ function renderError(
|
||||
stack.classList.toggle('wordWrap', e.outputWordWrap);
|
||||
}));
|
||||
|
||||
stack.style.margin = '8px 0';
|
||||
const element = document.createElement('span');
|
||||
insertOutput(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, ctx.settings.outputScrolling, element, true);
|
||||
stack.appendChild(element);
|
||||
container.appendChild(stack);
|
||||
insertOutput(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, ctx.settings.outputScrolling, stack, true);
|
||||
appendChildAndScroll(container, stack);
|
||||
} else {
|
||||
const header = document.createElement('div');
|
||||
const headerMessage = err.name && err.message ? `${err.name}: ${err.message}` : err.name || err.message;
|
||||
@@ -203,41 +200,68 @@ function renderError(
|
||||
return disposableStore;
|
||||
}
|
||||
|
||||
function getPreviousOutputWithMatchingMimeType(container: HTMLElement, mimeType: string) {
|
||||
const outputContainer = container.parentElement;
|
||||
|
||||
const previous = outputContainer?.previousSibling;
|
||||
if (previous) {
|
||||
const outputElement = (previous.firstChild as HTMLElement | null);
|
||||
if (outputElement && outputElement.getAttribute('output-mime-type') === mimeType) {
|
||||
return outputElement;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 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, scrollTop?: number) {
|
||||
container.appendChild(child);
|
||||
child.childNodes.forEach((node) => {
|
||||
if (node instanceof HTMLElement && node.classList.contains(scrollableClass)) {
|
||||
node.scrollTop = scrollTop !== undefined ? scrollTop : node.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Find the scrollTop of the existing scrollable output, return undefined if at the bottom
|
||||
function findScrolledHeight(outputContainer: HTMLElement): number | undefined {
|
||||
let result: number | undefined;
|
||||
outputContainer.childNodes.forEach((output) => {
|
||||
output.childNodes.forEach((node) => {
|
||||
if (node instanceof HTMLElement && node.classList.contains(scrollableClass)) {
|
||||
if (node.scrollHeight - node.scrollTop - node.clientHeight > 2) {
|
||||
// not scrolled to the bottom
|
||||
result = node.scrollTop;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boolean, ctx: IRichRenderContext): IDisposable {
|
||||
const disposableStore = createDisposableStore();
|
||||
|
||||
const outputContainer = container.parentElement;
|
||||
if (!outputContainer) {
|
||||
// should never happen
|
||||
return disposableStore;
|
||||
}
|
||||
|
||||
const prev = outputContainer.previousSibling;
|
||||
if (prev) {
|
||||
// OutputItem in the same cell
|
||||
// check if the previous item is a stream
|
||||
const outputElement = (prev.firstChild as HTMLElement | null);
|
||||
if (outputElement && outputElement.getAttribute('output-mime-type') === outputInfo.mime) {
|
||||
// same stream
|
||||
|
||||
// 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);
|
||||
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, element, false);
|
||||
outputElement.appendChild(element);
|
||||
return disposableStore;
|
||||
// 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);
|
||||
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, element, false);
|
||||
appendChildAndScroll(outputElement, element);
|
||||
return disposableStore;
|
||||
}
|
||||
|
||||
const element = document.createElement('span');
|
||||
@@ -250,10 +274,11 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo
|
||||
|
||||
const text = outputInfo.text();
|
||||
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, element, false);
|
||||
const scrollTop = findScrolledHeight(container);
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
container.appendChild(element);
|
||||
appendChildAndScroll(container, element, scrollTop);
|
||||
container.setAttribute('output-mime-type', outputInfo.mime);
|
||||
if (error) {
|
||||
container.classList.add('error');
|
||||
@@ -273,7 +298,7 @@ function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: IRichRe
|
||||
}
|
||||
const text = outputInfo.text();
|
||||
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, contentNode, false);
|
||||
container.appendChild(contentNode);
|
||||
appendChildAndScroll(container, contentNode);
|
||||
return disposableStore;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { handleANSIOutput } from './ansi';
|
||||
|
||||
export const scrollableClass = 'scrollable';
|
||||
|
||||
function generateViewMoreElement(outputId: string, adjustableSize: boolean) {
|
||||
const container = document.createElement('span');
|
||||
const first = document.createElement('span');
|
||||
@@ -51,7 +53,7 @@ function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number
|
||||
|
||||
function scrollableArrayOfString(id: string, buffer: string[], container: HTMLElement, trustHtml: boolean) {
|
||||
const scrollableDiv = document.createElement('div');
|
||||
scrollableDiv.classList.add('scrollable');
|
||||
scrollableDiv.classList.add(scrollableClass);
|
||||
|
||||
if (buffer.length > 5000) {
|
||||
container.appendChild(generateViewMoreElement(id, false));
|
||||
|
||||
Reference in New Issue
Block a user