Skip to content

Commit 3f5f1e4

Browse files
authored
Merge pull request microsoft#166659 from amunger/scrollableBorder
Scrollable notebook output experiment
2 parents 775cc3e + 78831f0 commit 3f5f1e4

File tree

6 files changed

+109
-42
lines changed

6 files changed

+109
-42
lines changed

extensions/notebook-renderers/src/index.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer';
7-
import { truncatedArrayOfString } from './textHelper';
7+
import { insertOutput } from './textHelper';
88

99
interface IDisposable {
1010
dispose(): void;
@@ -28,6 +28,11 @@ interface JavaScriptRenderingHook {
2828
preEvaluate(outputItem: OutputItem, element: HTMLElement, script: string, signal: AbortSignal): string | undefined | Promise<string | undefined>;
2929
}
3030

31+
interface RenderOptions {
32+
readonly lineLimit: number;
33+
readonly outputScrolling: boolean;
34+
}
35+
3136
function clearContainer(container: HTMLElement) {
3237
while (container.firstChild) {
3338
container.removeChild(container.firstChild);
@@ -120,7 +125,7 @@ async function renderJavascript(outputInfo: OutputItem, container: HTMLElement,
120125
domEval(element);
121126
}
122127

123-
function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
128+
function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: RenderOptions }): void {
124129
const element = document.createElement('div');
125130
container.appendChild(element);
126131
type ErrorLike = Partial<Error>;
@@ -138,7 +143,7 @@ function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: Render
138143
stack.classList.add('traceback');
139144
stack.style.margin = '8px 0';
140145
const element = document.createElement('span');
141-
truncatedArrayOfString(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, element);
146+
insertOutput(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, false, element);
142147
stack.appendChild(element);
143148
container.appendChild(stack);
144149
} else {
@@ -153,7 +158,7 @@ function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: Render
153158
container.classList.add('error');
154159
}
155160

156-
function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boolean, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
161+
function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boolean, ctx: RendererContext<void> & { readonly settings: RenderOptions }): void {
157162
const outputContainer = container.parentElement;
158163
if (!outputContainer) {
159164
// should never happen
@@ -170,7 +175,7 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo
170175
const text = outputInfo.text();
171176

172177
const element = document.createElement('span');
173-
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element);
178+
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, element);
174179
outputElement.appendChild(element);
175180
return;
176181
}
@@ -180,7 +185,7 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo
180185
element.classList.add('output-stream');
181186

182187
const text = outputInfo.text();
183-
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element);
188+
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, element);
184189
while (container.firstChild) {
185190
container.removeChild(container.firstChild);
186191
}
@@ -191,12 +196,12 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo
191196
}
192197
}
193198

194-
function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
199+
function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: RenderOptions }): void {
195200
clearContainer(container);
196201
const contentNode = document.createElement('div');
197202
contentNode.classList.add('output-plaintext');
198203
const text = outputInfo.text();
199-
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, contentNode);
204+
insertOutput(outputInfo.id, [text], ctx.settings.lineLimit, ctx.settings.outputScrolling, contentNode);
200205
container.appendChild(contentNode);
201206
}
202207

@@ -205,26 +210,36 @@ export const activate: ActivationFunction<void> = (ctx) => {
205210
const htmlHooks = new Set<HtmlRenderingHook>();
206211
const jsHooks = new Set<JavaScriptRenderingHook>();
207212

208-
const latestContext = ctx as (RendererContext<void> & { readonly settings: { readonly lineLimit: number } });
213+
const latestContext = ctx as (RendererContext<void> & { readonly settings: RenderOptions });
209214

210215
const style = document.createElement('style');
211216
style.textContent = `
212217
.output-plaintext,
213218
.output-stream,
214219
.traceback {
220+
display: inline-block;
221+
width: 100%;
215222
line-height: var(--notebook-cell-output-line-height);
216223
font-family: var(--notebook-cell-output-font-family);
217-
white-space: pre-wrap;
218-
word-wrap: break-word;
219-
220224
font-size: var(--notebook-cell-output-font-size);
221225
user-select: text;
222226
-webkit-user-select: text;
223227
-ms-user-select: text;
224228
cursor: auto;
225229
}
226-
span.output-stream {
227-
display: inline-block;
230+
output-plaintext,
231+
.traceback {
232+
white-space: pre-wrap;
233+
word-wrap: break-word;
234+
}
235+
.output > span.scrollable {
236+
overflow-y: scroll;
237+
max-height: var(--notebook-cell-output-max-height);
238+
border: var(--vscode-editorWidget-border);
239+
border-style: solid;
240+
padding-left: 4px;
241+
box-sizing: border-box;
242+
border-width: 1px;
228243
}
229244
.output-plaintext .code-bold,
230245
.output-stream .code-bold,

extensions/notebook-renderers/src/textHelper.ts

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,35 @@
55

66
import { handleANSIOutput } from './ansi';
77

8-
function generateViewMoreElement(outputId: string) {
8+
function generateViewMoreElement(outputId: string, adjustableSize: boolean) {
99
const container = document.createElement('span');
1010
const first = document.createElement('span');
11-
first.textContent = 'Output exceeds the ';
12-
const second = document.createElement('a');
13-
second.textContent = 'size limit';
14-
second.href = `command:workbench.action.openSettings?%5B%22notebook.output.textLineLimit%22%5D`;
11+
12+
if (adjustableSize) {
13+
first.textContent = 'Output exceeds the ';
14+
const second = document.createElement('a');
15+
second.textContent = 'size limit';
16+
second.href = `command:workbench.action.openSettings?%5B%22notebook.output.textLineLimit%22%5D`;
17+
container.appendChild(first);
18+
container.appendChild(second);
19+
} else {
20+
first.textContent = 'Output exceeds the maximium size limit';
21+
container.appendChild(first);
22+
}
23+
1524
const third = document.createElement('span');
16-
third.textContent = '. Open the full output data';
25+
third.textContent = '. Open the full output data ';
1726
const forth = document.createElement('a');
18-
forth.textContent = ' in a text editor';
27+
forth.textContent = 'in a text editor';
1928
forth.href = `command:workbench.action.openLargeOutput?${outputId}`;
20-
container.appendChild(first);
21-
container.appendChild(second);
2229
container.appendChild(third);
2330
container.appendChild(forth);
2431
return container;
2532
}
2633

27-
export function truncatedArrayOfString(id: string, outputs: string[], linesLimit: number, container: HTMLElement) {
28-
const buffer = outputs.join('\n').split(/\r\n|\r|\n/g);
34+
function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, container: HTMLElement) {
2935
const lineCount = buffer.length;
30-
31-
if (lineCount < linesLimit) {
32-
const spanElement = handleANSIOutput(buffer.slice(0, linesLimit).join('\n'));
33-
container.appendChild(spanElement);
34-
return;
35-
}
36-
37-
container.appendChild(generateViewMoreElement(id));
36+
container.appendChild(generateViewMoreElement(id, true));
3837

3938
const div = document.createElement('div');
4039
container.appendChild(div);
@@ -49,3 +48,31 @@ export function truncatedArrayOfString(id: string, outputs: string[], linesLimit
4948
container.appendChild(div2);
5049
div2.appendChild(handleANSIOutput(buffer.slice(lineCount - 5).join('\n')));
5150
}
51+
52+
function scrollableArrayOfString(id: string, buffer: string[], container: HTMLElement) {
53+
container.classList.add('scrollable');
54+
55+
if (buffer.length > 5000) {
56+
container.appendChild(generateViewMoreElement(id, false));
57+
}
58+
const div = document.createElement('div');
59+
container.appendChild(div);
60+
div.appendChild(handleANSIOutput(buffer.slice(0, 5000).join('\n')));
61+
}
62+
63+
export function insertOutput(id: string, outputs: string[], linesLimit: number, scrollable: boolean, container: HTMLElement) {
64+
const buffer = outputs.join('\n').split(/\r\n|\r|\n/g);
65+
const lineCount = buffer.length;
66+
67+
if (lineCount < linesLimit) {
68+
const spanElement = handleANSIOutput(buffer.join('\n'));
69+
container.appendChild(spanElement);
70+
return;
71+
}
72+
73+
if (scrollable) {
74+
scrollableArrayOfString(id, buffer, container);
75+
} else {
76+
truncatedArrayOfString(id, buffer, linesLimit, container);
77+
}
78+
}

src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser
3838
import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads';
3939
import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping';
4040
import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';
41-
import { CellUri, INotebookRendererInfo, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon';
41+
import { CellUri, INotebookRendererInfo, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon';
4242
import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
4343
import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService';
4444
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
@@ -99,6 +99,8 @@ interface BacklayerWebviewOptions {
9999
readonly outputFontFamily: string;
100100
readonly markupFontSize: number;
101101
readonly outputLineHeight: number;
102+
readonly outputScrolling: boolean;
103+
readonly outputLineLimit: number;
102104
}
103105

104106
const logChannelId = 'notebook.rendering';
@@ -245,6 +247,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
245247
'notebook-markup-font-size': typeof this.options.markupFontSize === 'number' && this.options.markupFontSize > 0 ? `${this.options.markupFontSize}px` : `calc(${this.options.fontSize}px * 1.2)`,
246248
'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`,
247249
'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`,
250+
'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit}px`,
248251
'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily,
249252
'notebook-cell-markup-empty-content': nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double click or press enter to edit."),
250253
'notebook-cell-renderer-not-found-error': nls.localize({
@@ -257,13 +260,17 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
257260
private generateContent(coreDependencies: string, baseUrl: string) {
258261
const renderersData = this.getRendererData();
259262
const preloadsData = this.getStaticPreloadsData();
263+
const renderOptions = {
264+
lineLimit: this.options.outputLineLimit,
265+
outputScrolling: this.options.outputScrolling
266+
};
260267
const preloadScript = preloadsScriptStr(
261268
this.options,
262269
{ dragAndDropEnabled: this.options.dragAndDropEnabled },
270+
renderOptions,
263271
renderersData,
264272
preloadsData,
265273
this.workspaceTrustManagementService.isWorkspaceTrusted(),
266-
this.configurationService.getValue<number>(NotebookSetting.textOutputLineLimit) ?? 30,
267274
this.nonce);
268275

269276
const enableCsp = this.configurationService.getValue('notebook.experimental.enableCsp');

src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,19 @@ export interface PreloadOptions {
6464
dragAndDropEnabled: boolean;
6565
}
6666

67+
export interface RenderOptions {
68+
readonly lineLimit: number;
69+
readonly outputScrolling: boolean;
70+
}
71+
6772
interface PreloadContext {
6873
readonly nonce: string;
6974
readonly style: PreloadStyles;
7075
readonly options: PreloadOptions;
76+
readonly renderOptions: RenderOptions;
7177
readonly rendererData: readonly webviewMessages.RendererMetadata[];
7278
readonly staticPreloadsData: readonly webviewMessages.StaticPreloadMetadata[];
7379
readonly isWorkspaceTrusted: boolean;
74-
readonly lineLimit: number;
7580
}
7681

7782
declare function __import(path: string): Promise<any>;
@@ -82,7 +87,8 @@ async function webviewPreloads(ctx: PreloadContext) {
8287

8388
let currentOptions = ctx.options;
8489
let isWorkspaceTrusted = ctx.isWorkspaceTrusted;
85-
const lineLimit = ctx.lineLimit;
90+
const lineLimit = ctx.renderOptions.lineLimit;
91+
const outputScrolling = ctx.renderOptions.outputScrolling;
8692

8793
const acquireVsCodeApi = globalThis.acquireVsCodeApi;
8894
const vscode = acquireVsCodeApi();
@@ -211,7 +217,7 @@ async function webviewPreloads(ctx: PreloadContext) {
211217
}
212218

213219
interface RendererContext extends rendererApi.RendererContext<unknown> {
214-
readonly settings: { readonly lineLimit: number };
220+
readonly settings: RenderOptions;
215221
}
216222

217223
interface RendererModule {
@@ -1384,6 +1390,7 @@ async function webviewPreloads(ctx: PreloadContext) {
13841390
},
13851391
settings: {
13861392
get lineLimit() { return lineLimit; },
1393+
get outputScrolling() { return outputScrolling; },
13871394
}
13881395
};
13891396

@@ -2473,14 +2480,14 @@ async function webviewPreloads(ctx: PreloadContext) {
24732480
}();
24742481
}
24752482

2476-
export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly webviewMessages.RendererMetadata[], preloads: readonly webviewMessages.StaticPreloadMetadata[], isWorkspaceTrusted: boolean, lineLimit: number, nonce: string) {
2483+
export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderOptions: RenderOptions, renderers: readonly webviewMessages.RendererMetadata[], preloads: readonly webviewMessages.StaticPreloadMetadata[], isWorkspaceTrusted: boolean, nonce: string) {
24772484
const ctx: PreloadContext = {
24782485
style: styleValues,
24792486
options,
2487+
renderOptions,
24802488
rendererData: renderers,
24812489
staticPreloadsData: preloads,
24822490
isWorkspaceTrusted,
2483-
lineLimit,
24842491
nonce,
24852492
};
24862493
// TS will try compiling `import()` in webviewPreloads, so use a helper function instead

src/vs/workbench/contrib/notebook/common/notebookCommon.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,8 @@ export const NotebookSetting = {
926926
outputLineHeight: 'notebook.outputLineHeight',
927927
outputFontSize: 'notebook.outputFontSize',
928928
outputFontFamily: 'notebook.outputFontFamily',
929-
kernelPickerType: 'notebook.kernelPicker.type'
929+
kernelPickerType: 'notebook.kernelPicker.type',
930+
outputScrolling: 'notebook.experimental.outputScrolling',
930931
} as const;
931932

932933
export const enum CellStatusbarAlignment {

src/vs/workbench/contrib/notebook/common/notebookOptions.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export interface NotebookLayoutConfiguration {
7070
editorOptionsCustomizations: any | undefined;
7171
focusIndicatorGap: number;
7272
interactiveWindowCollapseCodeCells: InteractiveWindowCollapseCodeCells;
73+
outputScrolling: boolean;
74+
outputLineLimit: number;
7375
}
7476

7577
export interface NotebookOptionsChangeEvent {
@@ -147,6 +149,8 @@ export class NotebookOptions extends Disposable {
147149
const editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations);
148150
const interactiveWindowCollapseCodeCells: InteractiveWindowCollapseCodeCells = this.configurationService.getValue(NotebookSetting.interactiveWindowCollapseCodeCells);
149151
const outputLineHeight = this._computeOutputLineHeight();
152+
const outputScrolling = this.configurationService.getValue<boolean>(NotebookSetting.outputScrolling);
153+
const outputLineLimit = this.configurationService.getValue<number>(NotebookSetting.textOutputLineLimit) ?? 30;
150154

151155
this._layoutConfiguration = {
152156
...(compactView ? compactConfigConstants : defaultConfigConstants),
@@ -183,7 +187,9 @@ export class NotebookOptions extends Disposable {
183187
editorOptionsCustomizations,
184188
focusIndicatorGap: 3,
185189
interactiveWindowCollapseCodeCells,
186-
markdownFoldHintHeight: 22
190+
markdownFoldHintHeight: 22,
191+
outputScrolling: outputScrolling,
192+
outputLineLimit: outputLineLimit
187193
};
188194

189195
this._register(this.configurationService.onDidChangeConfiguration(e => {
@@ -566,6 +572,8 @@ export class NotebookOptions extends Disposable {
566572
outputFontFamily: this._layoutConfiguration.outputFontFamily,
567573
markupFontSize: this._layoutConfiguration.markupFontSize,
568574
outputLineHeight: this._layoutConfiguration.outputLineHeight,
575+
outputScrolling: this._layoutConfiguration.outputScrolling,
576+
outputLineLimit: this._layoutConfiguration.outputLineLimit,
569577
};
570578
}
571579

@@ -584,6 +592,8 @@ export class NotebookOptions extends Disposable {
584592
outputFontFamily: this._layoutConfiguration.outputFontFamily,
585593
markupFontSize: this._layoutConfiguration.markupFontSize,
586594
outputLineHeight: this._layoutConfiguration.outputLineHeight,
595+
outputScrolling: this._layoutConfiguration.outputScrolling,
596+
outputLineLimit: this._layoutConfiguration.outputLineLimit,
587597
};
588598
}
589599

0 commit comments

Comments
 (0)