Merge pull request #166659 from amunger/scrollableBorder

Scrollable notebook output experiment
This commit is contained in:
Aaron Munger
2022-11-18 10:08:27 -08:00
committed by GitHub
6 changed files with 109 additions and 42 deletions
+29 -14
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer';
import { truncatedArrayOfString } from './textHelper';
import { insertOutput } from './textHelper';
interface IDisposable {
dispose(): void;
@@ -28,6 +28,11 @@ interface JavaScriptRenderingHook {
preEvaluate(outputItem: OutputItem, element: HTMLElement, script: string, signal: AbortSignal): string | undefined | Promise<string | undefined>;
}
interface RenderOptions {
readonly lineLimit: number;
readonly outputScrolling: boolean;
}
function clearContainer(container: HTMLElement) {
while (container.firstChild) {
container.removeChild(container.firstChild);
@@ -120,7 +125,7 @@ async function renderJavascript(outputInfo: OutputItem, container: HTMLElement,
domEval(element);
}
function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: RenderOptions }): void {
const element = document.createElement('div');
container.appendChild(element);
type ErrorLike = Partial<Error>;
@@ -138,7 +143,7 @@ function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: Render
stack.classList.add('traceback');
stack.style.margin = '8px 0';
const element = document.createElement('span');
truncatedArrayOfString(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, element);
insertOutput(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, false, element);
stack.appendChild(element);
container.appendChild(stack);
} else {
@@ -153,7 +158,7 @@ function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: Render
container.classList.add('error');
}
function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boolean, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boolean, ctx: RendererContext<void> & { readonly settings: RenderOptions }): void {
const outputContainer = container.parentElement;
if (!outputContainer) {
// should never happen
@@ -170,7 +175,7 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo
const text = outputInfo.text();
const element = document.createElement('span');
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element);
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, element);
outputElement.appendChild(element);
return;
}
@@ -180,7 +185,7 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo
element.classList.add('output-stream');
const text = outputInfo.text();
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element);
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, element);
while (container.firstChild) {
container.removeChild(container.firstChild);
}
@@ -191,12 +196,12 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo
}
}
function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: RenderOptions }): void {
clearContainer(container);
const contentNode = document.createElement('div');
contentNode.classList.add('output-plaintext');
const text = outputInfo.text();
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, contentNode);
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, contentNode);
container.appendChild(contentNode);
}
@@ -205,26 +210,36 @@ export const activate: ActivationFunction<void> = (ctx) => {
const htmlHooks = new Set<HtmlRenderingHook>();
const jsHooks = new Set<JavaScriptRenderingHook>();
const latestContext = ctx as (RendererContext<void> & { readonly settings: { readonly lineLimit: number } });
const latestContext = ctx as (RendererContext<void> & { readonly settings: RenderOptions });
const style = document.createElement('style');
style.textContent = `
.output-plaintext,
.output-stream,
.traceback {
display: inline-block;
width: 100%;
line-height: var(--notebook-cell-output-line-height);
font-family: var(--notebook-cell-output-font-family);
white-space: pre-wrap;
word-wrap: break-word;
font-size: var(--notebook-cell-output-font-size);
user-select: text;
-webkit-user-select: text;
-ms-user-select: text;
cursor: auto;
}
span.output-stream {
display: inline-block;
output-plaintext,
.traceback {
white-space: pre-wrap;
word-wrap: break-word;
}
.output > span.scrollable {
overflow-y: scroll;
max-height: var(--notebook-cell-output-max-height);
border: var(--vscode-editorWidget-border);
border-style: solid;
padding-left: 4px;
box-sizing: border-box;
border-width: 1px;
}
.output-plaintext .code-bold,
.output-stream .code-bold,
+46 -19
View File
@@ -5,36 +5,35 @@
import { handleANSIOutput } from './ansi';
function generateViewMoreElement(outputId: string) {
function generateViewMoreElement(outputId: string, adjustableSize: boolean) {
const container = document.createElement('span');
const first = document.createElement('span');
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`;
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 third = document.createElement('span');
third.textContent = '. Open the full output data';
third.textContent = '. Open the full output data ';
const forth = document.createElement('a');
forth.textContent = ' in a text editor';
forth.textContent = 'in a text editor';
forth.href = `command:workbench.action.openLargeOutput?${outputId}`;
container.appendChild(first);
container.appendChild(second);
container.appendChild(third);
container.appendChild(forth);
return container;
}
export function truncatedArrayOfString(id: string, outputs: string[], linesLimit: number, container: HTMLElement) {
const buffer = outputs.join('\n').split(/\r\n|\r|\n/g);
function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, container: HTMLElement) {
const lineCount = buffer.length;
if (lineCount < linesLimit) {
const spanElement = handleANSIOutput(buffer.slice(0, linesLimit).join('\n'));
container.appendChild(spanElement);
return;
}
container.appendChild(generateViewMoreElement(id));
container.appendChild(generateViewMoreElement(id, true));
const div = document.createElement('div');
container.appendChild(div);
@@ -49,3 +48,31 @@ export function truncatedArrayOfString(id: string, outputs: string[], linesLimit
container.appendChild(div2);
div2.appendChild(handleANSIOutput(buffer.slice(lineCount - 5).join('\n')));
}
function scrollableArrayOfString(id: string, buffer: string[], container: HTMLElement) {
container.classList.add('scrollable');
if (buffer.length > 5000) {
container.appendChild(generateViewMoreElement(id, false));
}
const div = document.createElement('div');
container.appendChild(div);
div.appendChild(handleANSIOutput(buffer.slice(0, 5000).join('\n')));
}
export function insertOutput(id: string, outputs: string[], linesLimit: number, scrollable: boolean, container: HTMLElement) {
const buffer = outputs.join('\n').split(/\r\n|\r|\n/g);
const lineCount = buffer.length;
if (lineCount < linesLimit) {
const spanElement = handleANSIOutput(buffer.join('\n'));
container.appendChild(spanElement);
return;
}
if (scrollable) {
scrollableArrayOfString(id, buffer, container);
} else {
truncatedArrayOfString(id, buffer, linesLimit, container);
}
}
@@ -38,7 +38,7 @@ import { NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser
import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads';
import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping';
import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';
import { CellUri, INotebookRendererInfo, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellUri, INotebookRendererInfo, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
@@ -99,6 +99,8 @@ interface BacklayerWebviewOptions {
readonly outputFontFamily: string;
readonly markupFontSize: number;
readonly outputLineHeight: number;
readonly outputScrolling: boolean;
readonly outputLineLimit: number;
}
const logChannelId = 'notebook.rendering';
@@ -245,6 +247,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
'notebook-markup-font-size': typeof this.options.markupFontSize === 'number' && this.options.markupFontSize > 0 ? `${this.options.markupFontSize}px` : `calc(${this.options.fontSize}px * 1.2)`,
'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`,
'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`,
'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit}px`,
'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily,
'notebook-cell-markup-empty-content': nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double click or press enter to edit."),
'notebook-cell-renderer-not-found-error': nls.localize({
@@ -257,13 +260,17 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
private generateContent(coreDependencies: string, baseUrl: string) {
const renderersData = this.getRendererData();
const preloadsData = this.getStaticPreloadsData();
const renderOptions = {
lineLimit: this.options.outputLineLimit,
outputScrolling: this.options.outputScrolling
};
const preloadScript = preloadsScriptStr(
this.options,
{ dragAndDropEnabled: this.options.dragAndDropEnabled },
renderOptions,
renderersData,
preloadsData,
this.workspaceTrustManagementService.isWorkspaceTrusted(),
this.configurationService.getValue<number>(NotebookSetting.textOutputLineLimit) ?? 30,
this.nonce);
const enableCsp = this.configurationService.getValue('notebook.experimental.enableCsp');
@@ -64,14 +64,19 @@ export interface PreloadOptions {
dragAndDropEnabled: boolean;
}
export interface RenderOptions {
readonly lineLimit: number;
readonly outputScrolling: boolean;
}
interface PreloadContext {
readonly nonce: string;
readonly style: PreloadStyles;
readonly options: PreloadOptions;
readonly renderOptions: RenderOptions;
readonly rendererData: readonly webviewMessages.RendererMetadata[];
readonly staticPreloadsData: readonly webviewMessages.StaticPreloadMetadata[];
readonly isWorkspaceTrusted: boolean;
readonly lineLimit: number;
}
declare function __import(path: string): Promise<any>;
@@ -82,7 +87,8 @@ async function webviewPreloads(ctx: PreloadContext) {
let currentOptions = ctx.options;
let isWorkspaceTrusted = ctx.isWorkspaceTrusted;
const lineLimit = ctx.lineLimit;
const lineLimit = ctx.renderOptions.lineLimit;
const outputScrolling = ctx.renderOptions.outputScrolling;
const acquireVsCodeApi = globalThis.acquireVsCodeApi;
const vscode = acquireVsCodeApi();
@@ -211,7 +217,7 @@ async function webviewPreloads(ctx: PreloadContext) {
}
interface RendererContext extends rendererApi.RendererContext<unknown> {
readonly settings: { readonly lineLimit: number };
readonly settings: RenderOptions;
}
interface RendererModule {
@@ -1384,6 +1390,7 @@ async function webviewPreloads(ctx: PreloadContext) {
},
settings: {
get lineLimit() { return lineLimit; },
get outputScrolling() { return outputScrolling; },
}
};
@@ -2473,14 +2480,14 @@ async function webviewPreloads(ctx: PreloadContext) {
}();
}
export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly webviewMessages.RendererMetadata[], preloads: readonly webviewMessages.StaticPreloadMetadata[], isWorkspaceTrusted: boolean, lineLimit: number, nonce: string) {
export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderOptions: RenderOptions, renderers: readonly webviewMessages.RendererMetadata[], preloads: readonly webviewMessages.StaticPreloadMetadata[], isWorkspaceTrusted: boolean, nonce: string) {
const ctx: PreloadContext = {
style: styleValues,
options,
renderOptions,
rendererData: renderers,
staticPreloadsData: preloads,
isWorkspaceTrusted,
lineLimit,
nonce,
};
// TS will try compiling `import()` in webviewPreloads, so use a helper function instead
@@ -926,7 +926,8 @@ export const NotebookSetting = {
outputLineHeight: 'notebook.outputLineHeight',
outputFontSize: 'notebook.outputFontSize',
outputFontFamily: 'notebook.outputFontFamily',
kernelPickerType: 'notebook.kernelPicker.type'
kernelPickerType: 'notebook.kernelPicker.type',
outputScrolling: 'notebook.experimental.outputScrolling',
} as const;
export const enum CellStatusbarAlignment {
@@ -70,6 +70,8 @@ export interface NotebookLayoutConfiguration {
editorOptionsCustomizations: any | undefined;
focusIndicatorGap: number;
interactiveWindowCollapseCodeCells: InteractiveWindowCollapseCodeCells;
outputScrolling: boolean;
outputLineLimit: number;
}
export interface NotebookOptionsChangeEvent {
@@ -147,6 +149,8 @@ export class NotebookOptions extends Disposable {
const editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations);
const interactiveWindowCollapseCodeCells: InteractiveWindowCollapseCodeCells = this.configurationService.getValue(NotebookSetting.interactiveWindowCollapseCodeCells);
const outputLineHeight = this._computeOutputLineHeight();
const outputScrolling = this.configurationService.getValue<boolean>(NotebookSetting.outputScrolling);
const outputLineLimit = this.configurationService.getValue<number>(NotebookSetting.textOutputLineLimit) ?? 30;
this._layoutConfiguration = {
...(compactView ? compactConfigConstants : defaultConfigConstants),
@@ -183,7 +187,9 @@ export class NotebookOptions extends Disposable {
editorOptionsCustomizations,
focusIndicatorGap: 3,
interactiveWindowCollapseCodeCells,
markdownFoldHintHeight: 22
markdownFoldHintHeight: 22,
outputScrolling: outputScrolling,
outputLineLimit: outputLineLimit
};
this._register(this.configurationService.onDidChangeConfiguration(e => {
@@ -566,6 +572,8 @@ export class NotebookOptions extends Disposable {
outputFontFamily: this._layoutConfiguration.outputFontFamily,
markupFontSize: this._layoutConfiguration.markupFontSize,
outputLineHeight: this._layoutConfiguration.outputLineHeight,
outputScrolling: this._layoutConfiguration.outputScrolling,
outputLineLimit: this._layoutConfiguration.outputLineLimit,
};
}
@@ -584,6 +592,8 @@ export class NotebookOptions extends Disposable {
outputFontFamily: this._layoutConfiguration.outputFontFamily,
markupFontSize: this._layoutConfiguration.markupFontSize,
outputLineHeight: this._layoutConfiguration.outputLineHeight,
outputScrolling: this._layoutConfiguration.outputScrolling,
outputLineLimit: this._layoutConfiguration.outputLineLimit,
};
}