Path: blob/main/src/vs/workbench/contrib/notebook/browser/diff/diffElementOutputs.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as DOM from '../../../../../base/browser/dom.js';6import * as nls from '../../../../../nls.js';7import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';8import { DiffElementCellViewModelBase, SideBySideDiffElementViewModel } from './diffElementViewModel.js';9import { DiffSide, INotebookTextDiffEditor } from './notebookDiffEditorBrowser.js';10import { ICellOutputViewModel, IInsetRenderOutput, RenderOutputType } from '../notebookBrowser.js';11import { NotebookTextModel } from '../../common/model/notebookTextModel.js';12import { NotebookCellOutputsSplice } from '../../common/notebookCommon.js';13import { INotebookService } from '../../common/notebookService.js';14import { DiffNestedCellViewModel } from './diffNestedCellViewModel.js';15import { ThemeIcon } from '../../../../../base/common/themables.js';16import { mimetypeIcon } from '../notebookIcons.js';17import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';18import { KeyCode } from '../../../../../base/common/keyCodes.js';19import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';2021interface IMimeTypeRenderer extends IQuickPickItem {22index: number;23}2425export class OutputElement extends Disposable {26readonly resizeListener = this._register(new DisposableStore());27domNode!: HTMLElement;28renderResult?: IInsetRenderOutput;2930constructor(31private _notebookEditor: INotebookTextDiffEditor,32private _notebookTextModel: NotebookTextModel,33private _notebookService: INotebookService,34private _quickInputService: IQuickInputService,35private _diffElementViewModel: DiffElementCellViewModelBase,36private _diffSide: DiffSide,37private _nestedCell: DiffNestedCellViewModel,38private _outputContainer: HTMLElement,39readonly output: ICellOutputViewModel40) {41super();42}4344render(index: number, beforeElement?: HTMLElement) {45const outputItemDiv = document.createElement('div');46let result: IInsetRenderOutput | undefined = undefined;4748const [mimeTypes, pick] = this.output.resolveMimeTypes(this._notebookTextModel, undefined);49const pickedMimeTypeRenderer = this.output.pickedMimeType || mimeTypes[pick];50if (mimeTypes.length > 1) {51outputItemDiv.style.position = 'relative';52const mimeTypePicker = DOM.$('.multi-mimetype-output');53mimeTypePicker.classList.add(...ThemeIcon.asClassNameArray(mimetypeIcon));54mimeTypePicker.tabIndex = 0;55mimeTypePicker.title = nls.localize('mimeTypePicker', "Choose a different output mimetype, available mimetypes: {0}", mimeTypes.map(mimeType => mimeType.mimeType).join(', '));56outputItemDiv.appendChild(mimeTypePicker);57this.resizeListener.add(DOM.addStandardDisposableListener(mimeTypePicker, 'mousedown', async e => {58if (e.leftButton) {59e.preventDefault();60e.stopPropagation();61await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output);62}63}));6465this.resizeListener.add((DOM.addDisposableListener(mimeTypePicker, DOM.EventType.KEY_DOWN, async e => {66const event = new StandardKeyboardEvent(e);67if ((event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) {68e.preventDefault();69e.stopPropagation();70await this.pickActiveMimeTypeRenderer(this._notebookTextModel, this.output);71}72})));73}7475const innerContainer = DOM.$('.output-inner-container');76DOM.append(outputItemDiv, innerContainer);777879if (mimeTypes.length !== 0) {80const renderer = this._notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId);81result = renderer82? { type: RenderOutputType.Extension, renderer, source: this.output, mimeType: pickedMimeTypeRenderer.mimeType }83: this._renderMissingRenderer(this.output, pickedMimeTypeRenderer.mimeType);8485this.output.pickedMimeType = pickedMimeTypeRenderer;86}8788this.domNode = outputItemDiv;89this.renderResult = result;9091if (!result) {92// this.viewCell.updateOutputHeight(index, 0);93return;94}9596if (beforeElement) {97this._outputContainer.insertBefore(outputItemDiv, beforeElement);98} else {99this._outputContainer.appendChild(outputItemDiv);100}101102this._notebookEditor.createOutput(103this._diffElementViewModel,104this._nestedCell,105result,106() => this.getOutputOffsetInCell(index),107this._diffElementViewModel instanceof SideBySideDiffElementViewModel108? this._diffSide109: this._diffElementViewModel.type === 'insert' ? DiffSide.Modified : DiffSide.Original110);111}112113private _renderMissingRenderer(viewModel: ICellOutputViewModel, preferredMimeType: string | undefined): IInsetRenderOutput {114if (!viewModel.model.outputs.length) {115return this._renderMessage(viewModel, nls.localize('empty', "Cell has no output"));116}117118if (!preferredMimeType) {119const mimeTypes = viewModel.model.outputs.map(op => op.mime);120const mimeTypesMessage = mimeTypes.join(', ');121return this._renderMessage(viewModel, nls.localize('noRenderer.2', "No renderer could be found for output. It has the following mimetypes: {0}", mimeTypesMessage));122}123124return this._renderSearchForMimetype(viewModel, preferredMimeType);125}126127private _renderSearchForMimetype(viewModel: ICellOutputViewModel, mimeType: string): IInsetRenderOutput {128const query = `@tag:notebookRenderer ${mimeType}`;129130const p = DOM.$('p', undefined, `No renderer could be found for mimetype "${mimeType}", but one might be available on the Marketplace.`);131const 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`);132133return {134type: RenderOutputType.Html,135source: viewModel,136htmlContent: p.outerHTML + a.outerHTML,137};138}139140private _renderMessage(viewModel: ICellOutputViewModel, message: string): IInsetRenderOutput {141const el = DOM.$('p', undefined, message);142return { type: RenderOutputType.Html, source: viewModel, htmlContent: el.outerHTML };143}144145private async pickActiveMimeTypeRenderer(notebookTextModel: NotebookTextModel, viewModel: ICellOutputViewModel) {146const [mimeTypes, currIndex] = viewModel.resolveMimeTypes(notebookTextModel, undefined);147148const items = mimeTypes.filter(mimeType => mimeType.isTrusted).map((mimeType, index): IMimeTypeRenderer => ({149label: mimeType.mimeType,150id: mimeType.mimeType,151index: index,152picked: index === currIndex,153detail: this.generateRendererInfo(mimeType.rendererId),154description: index === currIndex ? nls.localize('curruentActiveMimeType', "Currently Active") : undefined155}));156157const disposables = new DisposableStore();158const picker = disposables.add(this._quickInputService.createQuickPick());159picker.items = items;160picker.activeItems = items.filter(item => !!item.picked);161picker.placeholder = items.length !== mimeTypes.length162? nls.localize('promptChooseMimeTypeInSecure.placeHolder', "Select mimetype to render for current output. Rich mimetypes are available only when the notebook is trusted")163: nls.localize('promptChooseMimeType.placeHolder', "Select mimetype to render for current output");164165const pick = await new Promise<number | undefined>(resolve => {166disposables.add(picker.onDidAccept(() => {167resolve(picker.selectedItems.length === 1 ? (picker.selectedItems[0] as IMimeTypeRenderer).index : undefined);168disposables.dispose();169}));170picker.show();171});172173if (pick === undefined) {174return;175}176177if (pick !== currIndex) {178// user chooses another mimetype179const index = this._nestedCell.outputsViewModels.indexOf(viewModel);180const nextElement = this.domNode.nextElementSibling;181this.resizeListener.clear();182const element = this.domNode;183if (element) {184element.remove();185this._notebookEditor.removeInset(186this._diffElementViewModel,187this._nestedCell,188viewModel,189this._diffSide190);191}192193viewModel.pickedMimeType = mimeTypes[pick];194this.render(index, nextElement as HTMLElement);195}196}197198private generateRendererInfo(renderId: string): string {199const renderInfo = this._notebookService.getRendererInfo(renderId);200201if (renderInfo) {202const displayName = renderInfo.displayName !== '' ? renderInfo.displayName : renderInfo.id;203return `${displayName} (${renderInfo.extensionId.value})`;204}205206return nls.localize('builtinRenderInfo', "built-in");207}208209getCellOutputCurrentIndex() {210return this._diffElementViewModel.getNestedCellViewModel(this._diffSide).outputs.indexOf(this.output.model);211}212213updateHeight(index: number, height: number) {214this._diffElementViewModel.updateOutputHeight(this._diffSide, index, height);215}216217getOutputOffsetInContainer(index: number) {218return this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index);219}220221getOutputOffsetInCell(index: number) {222return this._diffElementViewModel.getOutputOffsetInCell(this._diffSide, index);223}224}225226export class OutputContainer extends Disposable {227private _outputEntries = new Map<ICellOutputViewModel, OutputElement>();228constructor(229private _editor: INotebookTextDiffEditor,230private _notebookTextModel: NotebookTextModel,231private _diffElementViewModel: DiffElementCellViewModelBase,232private _nestedCellViewModel: DiffNestedCellViewModel,233private _diffSide: DiffSide,234private _outputContainer: HTMLElement,235@INotebookService private _notebookService: INotebookService,236@IQuickInputService private readonly _quickInputService: IQuickInputService,237) {238super();239this._register(this._diffElementViewModel.onDidLayoutChange(() => {240this._outputEntries.forEach((value, key) => {241const index = _nestedCellViewModel.outputs.indexOf(key.model);242if (index >= 0) {243const top = this._diffElementViewModel.getOutputOffsetInContainer(this._diffSide, index);244value.domNode.style.top = `${top}px`;245}246});247}));248249this._register(this._nestedCellViewModel.textModel.onDidChangeOutputs(splice => {250this._updateOutputs(splice);251}));252}253254private _updateOutputs(splice: NotebookCellOutputsSplice) {255const removedKeys: ICellOutputViewModel[] = [];256257this._outputEntries.forEach((value, key) => {258if (this._nestedCellViewModel.outputsViewModels.indexOf(key) < 0) {259// already removed260removedKeys.push(key);261// remove element from DOM262value.domNode.remove();263this._editor.removeInset(this._diffElementViewModel, this._nestedCellViewModel, key, this._diffSide);264}265});266267removedKeys.forEach(key => {268this._outputEntries.get(key)?.dispose();269this._outputEntries.delete(key);270});271272let prevElement: HTMLElement | undefined = undefined;273const outputsToRender = this._nestedCellViewModel.outputsViewModels;274275outputsToRender.reverse().forEach(output => {276if (this._outputEntries.has(output)) {277// already exist278prevElement = this._outputEntries.get(output)!.domNode;279return;280}281282// newly added element283const currIndex = this._nestedCellViewModel.outputsViewModels.indexOf(output);284this._renderOutput(output, currIndex, prevElement);285prevElement = this._outputEntries.get(output)?.domNode;286});287}288render() {289// TODO, outputs to render (should have a limit)290for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) {291const currOutput = this._nestedCellViewModel.outputsViewModels[index];292293// always add to the end294this._renderOutput(currOutput, index, undefined);295}296}297298showOutputs() {299for (let index = 0; index < this._nestedCellViewModel.outputsViewModels.length; index++) {300const currOutput = this._nestedCellViewModel.outputsViewModels[index];301// always add to the end302this._editor.showInset(this._diffElementViewModel, currOutput.cellViewModel, currOutput, this._diffSide);303}304}305306hideOutputs() {307this._outputEntries.forEach((outputElement, cellOutputViewModel) => {308this._editor.hideInset(this._diffElementViewModel, this._nestedCellViewModel, cellOutputViewModel);309});310}311312private _renderOutput(currOutput: ICellOutputViewModel, index: number, beforeElement?: HTMLElement) {313if (!this._outputEntries.has(currOutput)) {314this._outputEntries.set(currOutput, new OutputElement(this._editor, this._notebookTextModel, this._notebookService, this._quickInputService, this._diffElementViewModel, this._diffSide, this._nestedCellViewModel, this._outputContainer, currOutput));315}316317const renderElement = this._outputEntries.get(currOutput)!;318renderElement.render(index, beforeElement);319}320}321322323