Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as DOM from '../../../../../base/browser/dom.js';
7
import * as nls from '../../../../../nls.js';
8
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
9
import { DiffElementCellViewModelBase, SideBySideDiffElementViewModel } from './diffElementViewModel.js';
10
import { DiffSide, INotebookTextDiffEditor } from './notebookDiffEditorBrowser.js';
11
import { ICellOutputViewModel, IInsetRenderOutput, RenderOutputType } from '../notebookBrowser.js';
12
import { NotebookTextModel } from '../../common/model/notebookTextModel.js';
13
import { NotebookCellOutputsSplice } from '../../common/notebookCommon.js';
14
import { INotebookService } from '../../common/notebookService.js';
15
import { DiffNestedCellViewModel } from './diffNestedCellViewModel.js';
16
import { ThemeIcon } from '../../../../../base/common/themables.js';
17
import { mimetypeIcon } from '../notebookIcons.js';
18
import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
19
import { KeyCode } from '../../../../../base/common/keyCodes.js';
20
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
21
22
interface IMimeTypeRenderer extends IQuickPickItem {
23
index: number;
24
}
25
26
export class OutputElement extends Disposable {
27
readonly resizeListener = this._register(new DisposableStore());
28
domNode!: HTMLElement;
29
renderResult?: IInsetRenderOutput;
30
31
constructor(
32
private _notebookEditor: INotebookTextDiffEditor,
33
private _notebookTextModel: NotebookTextModel,
34
private _notebookService: INotebookService,
35
private _quickInputService: IQuickInputService,
36
private _diffElementViewModel: DiffElementCellViewModelBase,
37
private _diffSide: DiffSide,
38
private _nestedCell: DiffNestedCellViewModel,
39
private _outputContainer: HTMLElement,
40
readonly output: ICellOutputViewModel
41
) {
42
super();
43
}
44
45
render(index: number, beforeElement?: HTMLElement) {
46
const outputItemDiv = document.createElement('div');
47
let result: IInsetRenderOutput | undefined = undefined;
48
49
const [mimeTypes, pick] = this.output.resolveMimeTypes(this._notebookTextModel, undefined);
50
const pickedMimeTypeRenderer = this.output.pickedMimeType || mimeTypes[pick];
51
if (mimeTypes.length > 1) {
52
outputItemDiv.style.position = 'relative';
53
const mimeTypePicker = DOM.$('.multi-mimetype-output');
54
mimeTypePicker.classList.add(...ThemeIcon.asClassNameArray(mimetypeIcon));
55
mimeTypePicker.tabIndex = 0;
56
mimeTypePicker.title = nls.localize('mimeTypePicker', "Choose a different output mimetype, available mimetypes: {0}", mimeTypes.map(mimeType => mimeType.mimeType).join(', '));
57
outputItemDiv.appendChild(mimeTypePicker);
58
this.resizeListener.add(DOM.addStandardDisposableListener(mimeTypePicker, 'mousedown', async e => {
59
if (e.leftButton) {
60
e.preventDefault();
61
e.stopPropagation();
62
await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output);
63
}
64
}));
65
66
this.resizeListener.add((DOM.addDisposableListener(mimeTypePicker, DOM.EventType.KEY_DOWN, async e => {
67
const event = new StandardKeyboardEvent(e);
68
if ((event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) {
69
e.preventDefault();
70
e.stopPropagation();
71
await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output);
72
}
73
})));
74
}
75
76
const innerContainer = DOM.$('.output-inner-container');
77
DOM.append(outputItemDiv, innerContainer);
78
79
80
if (mimeTypes.length !== 0) {
81
const renderer = this._notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId);
82
result = renderer
83
? { type: RenderOutputType.Extension, renderer, source: this.output, mimeType: pickedMimeTypeRenderer.mimeType }
84
: this._renderMissingRenderer(this.output, pickedMimeTypeRenderer.mimeType);
85
86
this.output.pickedMimeType = pickedMimeTypeRenderer;
87
}
88
89
this.domNode = outputItemDiv;
90
this.renderResult = result;
91
92
if (!result) {
93
// this.viewCell.updateOutputHeight(index, 0);
94
return;
95
}
96
97
if (beforeElement) {
98
this._outputContainer.insertBefore(outputItemDiv, beforeElement);
99
} else {
100
this._outputContainer.appendChild(outputItemDiv);
101
}
102
103
this._notebookEditor.createOutput(
104
this._diffElementViewModel,
105
this._nestedCell,
106
result,
107
() => this.getOutputOffsetInCell(index),
108
this._diffElementViewModel instanceof SideBySideDiffElementViewModel
109
? this._diffSide
110
: this._diffElementViewModel.type === 'insert' ? DiffSide.Modified : DiffSide.Original
111
);
112
}
113
114
private _renderMissingRenderer(viewModel: ICellOutputViewModel, preferredMimeType: string | undefined): IInsetRenderOutput {
115
if (!viewModel.model.outputs.length) {
116
return this._renderMessage(viewModel, nls.localize('empty', "Cell has no output"));
117
}
118
119
if (!preferredMimeType) {
120
const mimeTypes = viewModel.model.outputs.map(op => op.mime);
121
const mimeTypesMessage = mimeTypes.join(', ');
122
return this._renderMessage(viewModel, nls.localize('noRenderer.2', "No renderer could be found for output. It has the following mimetypes: {0}", mimeTypesMessage));
123
}
124
125
return this._renderSearchForMimetype(viewModel, preferredMimeType);
126
}
127
128
private _renderSearchForMimetype(viewModel: ICellOutputViewModel, mimeType: string): IInsetRenderOutput {
129
const query = `@tag:notebookRenderer ${mimeType}`;
130
131
const p = DOM.$('p', undefined, `No renderer could be found for mimetype "${mimeType}", but one might be available on the Marketplace.`);
132
const a = DOM.$('a', { href: `command:workbench.extensions.search?%22${query}%22`, class: 'monaco-button monaco-text-button', tabindex: 0, role: 'button', style: 'padding: 8px; text-decoration: none; color: rgb(255, 255, 255); background-color: rgb(14, 99, 156); max-width: 200px;' }, `Search Marketplace`);
133
134
return {
135
type: RenderOutputType.Html,
136
source: viewModel,
137
htmlContent: p.outerHTML + a.outerHTML,
138
};
139
}
140
141
private _renderMessage(viewModel: ICellOutputViewModel, message: string): IInsetRenderOutput {
142
const el = DOM.$('p', undefined, message);
143
return { type: RenderOutputType.Html, source: viewModel, htmlContent: el.outerHTML };
144
}
145
146
private async pickActiveMimeTypeRenderer(notebookTextModel: NotebookTextModel, viewModel: ICellOutputViewModel) {
147
const [mimeTypes, currIndex] = viewModel.resolveMimeTypes(notebookTextModel, undefined);
148
149
const items = mimeTypes.filter(mimeType => mimeType.isTrusted).map((mimeType, index): IMimeTypeRenderer => ({
150
label: mimeType.mimeType,
151
id: mimeType.mimeType,
152
index: index,
153
picked: index === currIndex,
154
detail: this.generateRendererInfo(mimeType.rendererId),
155
description: index === currIndex ? nls.localize('curruentActiveMimeType', "Currently Active") : undefined
156
}));
157
158
const disposables = new DisposableStore();
159
const picker = disposables.add(this._quickInputService.createQuickPick());
160
picker.items = items;
161
picker.activeItems = items.filter(item => !!item.picked);
162
picker.placeholder = items.length !== mimeTypes.length
163
? nls.localize('promptChooseMimeTypeInSecure.placeHolder', "Select mimetype to render for current output. Rich mimetypes are available only when the notebook is trusted")
164
: nls.localize('promptChooseMimeType.placeHolder', "Select mimetype to render for current output");
165
166
const pick = await new Promise<number | undefined>(resolve => {
167
disposables.add(picker.onDidAccept(() => {
168
resolve(picker.selectedItems.length === 1 ? (picker.selectedItems[0] as IMimeTypeRenderer).index : undefined);
169
disposables.dispose();
170
}));
171
picker.show();
172
});
173
174
if (pick === undefined) {
175
return;
176
}
177
178
if (pick !== currIndex) {
179
// user chooses another mimetype
180
const index = this._nestedCell.outputsViewModels.indexOf(viewModel);
181
const nextElement = this.domNode.nextElementSibling;
182
this.resizeListener.clear();
183
const element = this.domNode;
184
if (element) {
185
element.remove();
186
this._notebookEditor.removeInset(
187
this._diffElementViewModel,
188
this._nestedCell,
189
viewModel,
190
this._diffSide
191
);
192
}
193
194
viewModel.pickedMimeType = mimeTypes[pick];
195
this.render(index, nextElement as HTMLElement);
196
}
197
}
198
199
private generateRendererInfo(renderId: string): string {
200
const renderInfo = this._notebookService.getRendererInfo(renderId);
201
202
if (renderInfo) {
203
const displayName = renderInfo.displayName !== '' ? renderInfo.displayName : renderInfo.id;
204
return `${displayName} (${renderInfo.extensionId.value})`;
205
}
206
207
return nls.localize('builtinRenderInfo', "built-in");
208
}
209
210
getCellOutputCurrentIndex() {
211
return this._diffElementViewModel.getNestedCellViewModel(this._diffSide).outputs.indexOf(this.output.model);
212
}
213
214
updateHeight(index: number, height: number) {
215
this._diffElementViewModel.updateOutputHeight(this._diffSide, index, height);
216
}
217
218
getOutputOffsetInContainer(index: number) {
219
return this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index);
220
}
221
222
getOutputOffsetInCell(index: number) {
223
return this._diffElementViewModel.getOutputOffsetInCell(this._diffSide, index);
224
}
225
}
226
227
export class OutputContainer extends Disposable {
228
private _outputEntries = new Map<ICellOutputViewModel, OutputElement>();
229
constructor(
230
private _editor: INotebookTextDiffEditor,
231
private _notebookTextModel: NotebookTextModel,
232
private _diffElementViewModel: DiffElementCellViewModelBase,
233
private _nestedCellViewModel: DiffNestedCellViewModel,
234
private _diffSide: DiffSide,
235
private _outputContainer: HTMLElement,
236
@INotebookService private _notebookService: INotebookService,
237
@IQuickInputService private readonly _quickInputService: IQuickInputService,
238
) {
239
super();
240
this._register(this._diffElementViewModel.onDidLayoutChange(() => {
241
this._outputEntries.forEach((value, key) => {
242
const index = _nestedCellViewModel.outputs.indexOf(key.model);
243
if (index >= 0) {
244
const top = this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index);
245
value.domNode.style.top = `${top}px`;
246
}
247
});
248
}));
249
250
this._register(this._nestedCellViewModel.textModel.onDidChangeOutputs(splice => {
251
this._updateOutputs(splice);
252
}));
253
}
254
255
private _updateOutputs(splice: NotebookCellOutputsSplice) {
256
const removedKeys: ICellOutputViewModel[] = [];
257
258
this._outputEntries.forEach((value, key) => {
259
if (this._nestedCellViewModel.outputsViewModels.indexOf(key) < 0) {
260
// already removed
261
removedKeys.push(key);
262
// remove element from DOM
263
value.domNode.remove();
264
this._editor.removeInset(this._diffElementViewModel, this._nestedCellViewModel, key, this._diffSide);
265
}
266
});
267
268
removedKeys.forEach(key => {
269
this._outputEntries.get(key)?.dispose();
270
this._outputEntries.delete(key);
271
});
272
273
let prevElement: HTMLElement | undefined = undefined;
274
const outputsToRender = this._nestedCellViewModel.outputsViewModels;
275
276
outputsToRender.reverse().forEach(output => {
277
if (this._outputEntries.has(output)) {
278
// already exist
279
prevElement = this._outputEntries.get(output)!.domNode;
280
return;
281
}
282
283
// newly added element
284
const currIndex = this._nestedCellViewModel.outputsViewModels.indexOf(output);
285
this._renderOutput(output, currIndex, prevElement);
286
prevElement = this._outputEntries.get(output)?.domNode;
287
});
288
}
289
render() {
290
// TODO, outputs to render (should have a limit)
291
for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) {
292
const currOutput = this._nestedCellViewModel.outputsViewModels[index];
293
294
// always add to the end
295
this._renderOutput(currOutput, index, undefined);
296
}
297
}
298
299
showOutputs() {
300
for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) {
301
const currOutput = this._nestedCellViewModel.outputsViewModels[index];
302
// always add to the end
303
this._editor.showInset(this._diffElementViewModel, currOutput.cellViewModel, currOutput, this._diffSide);
304
}
305
}
306
307
hideOutputs() {
308
this._outputEntries.forEach((outputElement, cellOutputViewModel) => {
309
this._editor.hideInset(this._diffElementViewModel, this._nestedCellViewModel, cellOutputViewModel);
310
});
311
}
312
313
private _renderOutput(currOutput: ICellOutputViewModel, index: number, beforeElement?: HTMLElement) {
314
if (!this._outputEntries.has(currOutput)) {
315
this._outputEntries.set(currOutput, new OutputElement(this._editor, this._notebookTextModel, this._notebookService, this._quickInputService, this._diffElementViewModel, this._diffSide, this._nestedCellViewModel, this._outputContainer, currOutput));
316
}
317
318
const renderElement = this._outputEntries.get(currOutput)!;
319
renderElement.render(index, beforeElement);
320
}
321
}
322
323