Path: blob/main/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts
5244 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 { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';6import { localize } from '../../../../../nls.js';7import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';8import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';9import { IOpenerService } from '../../../../../platform/opener/common/opener.js';10import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from './coreActions.js';11import { NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_OUTPUT_MIMETYPE } from '../../common/notebookContextKeys.js';12import * as icons from '../notebookIcons.js';13import { ILogService } from '../../../../../platform/log/common/log.js';14import { copyCellOutput } from '../viewModel/cellOutputTextHelper.js';15import { IEditorService } from '../../../../services/editor/common/editorService.js';16import { ICellOutputViewModel, ICellViewModel, INotebookEditor, getNotebookEditorFromEditorPane } from '../notebookBrowser.js';17import { CellKind, CellUri } from '../../common/notebookCommon.js';18import { CodeCellViewModel } from '../viewModel/codeCellViewModel.js';19import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';20import { INotebookEditorModelResolverService } from '../../common/notebookEditorModelResolverService.js';21import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js';22import { IFileService } from '../../../../../platform/files/common/files.js';23import { URI } from '../../../../../base/common/uri.js';2425export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copy';2627registerAction2(class ShowAllOutputsAction extends Action2 {28constructor() {29super({30id: 'notebook.cellOuput.showEmptyOutputs',31title: localize('notebookActions.showAllOutput', "Show Empty Outputs"),32menu: {33id: MenuId.NotebookOutputToolbar,34when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS)35},36f1: false,37category: NOTEBOOK_ACTIONS_CATEGORY38});39}4041run(accessor: ServicesAccessor, context: INotebookOutputActionContext): void {42const cell = context.cell;43if (cell && cell.cellKind === CellKind.Code) {4445for (let i = 1; i < cell.outputsViewModels.length; i++) {46if (!cell.outputsViewModels[i].visible.get()) {47cell.outputsViewModels[i].setVisible(true, true);48(cell as CodeCellViewModel).updateOutputHeight(i, 1, 'command');49}50}51}52}53});5455registerAction2(class CopyCellOutputAction extends Action2 {56constructor() {57super({58id: COPY_OUTPUT_COMMAND_ID,59title: localize('notebookActions.copyOutput', "Copy Cell Output"),60menu: {61id: MenuId.NotebookOutputToolbar,62when: NOTEBOOK_CELL_HAS_OUTPUTS63},64category: NOTEBOOK_ACTIONS_CATEGORY,65icon: icons.copyIcon,66});67}6869async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise<void> {70const editorService = accessor.get(IEditorService);71const clipboardService = accessor.get(IClipboardService);72const logService = accessor.get(ILogService);7374const notebookEditor = getNotebookEditorFromContext(editorService, outputContext);75if (!notebookEditor) {76return;77}7879const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor);80if (!outputViewModel) {81return;82}8384const mimeType = outputViewModel.pickedMimeType?.mimeType;8586if (mimeType?.startsWith('image/')) {87const focusOptions = { skipReveal: true, outputId: outputViewModel.model.outputId, altOutputId: outputViewModel.model.alternativeOutputId };88await notebookEditor.focusNotebookCell(outputViewModel.cellViewModel as ICellViewModel, 'output', focusOptions);89notebookEditor.copyOutputImage(outputViewModel);90} else {91copyCellOutput(mimeType, outputViewModel, clipboardService, logService);92}93}9495});9697export function getOutputViewModelFromId(outputId: string, notebookEditor: INotebookEditor): ICellOutputViewModel | undefined {98const notebookViewModel = notebookEditor.getViewModel();99if (notebookViewModel) {100const codeCells = notebookViewModel.viewCells.filter(cell => cell.cellKind === CellKind.Code) as CodeCellViewModel[];101for (const cell of codeCells) {102const output = cell.outputsViewModels.find(output => output.model.outputId === outputId || output.model.alternativeOutputId === outputId);103if (output) {104return output;105}106}107}108109return undefined;110}111112function getNotebookEditorFromContext(editorService: IEditorService, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): INotebookEditor | undefined {113if (outputContext && 'notebookEditor' in outputContext) {114return outputContext.notebookEditor;115}116return getNotebookEditorFromEditorPane(editorService.activeEditorPane);117}118119function getOutputViewModelFromContext(outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined, notebookEditor: INotebookEditor): ICellOutputViewModel | undefined {120let outputViewModel: ICellOutputViewModel | undefined;121122if (outputContext && 'outputId' in outputContext && typeof outputContext.outputId === 'string') {123outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor);124} else if (outputContext && 'outputViewModel' in outputContext) {125outputViewModel = outputContext.outputViewModel;126}127128if (!outputViewModel) {129// not able to find the output from the provided context, use the active cell130const activeCell = notebookEditor.getActiveCell();131if (!activeCell) {132return undefined;133}134135if (activeCell.focusedOutputId !== undefined) {136outputViewModel = activeCell.outputsViewModels.find(output => {137return output.model.outputId === activeCell.focusedOutputId;138});139} else {140outputViewModel = activeCell.outputsViewModels.find(output => output.pickedMimeType?.isTrusted);141}142}143144return outputViewModel;145}146147export const OPEN_OUTPUT_COMMAND_ID = 'notebook.cellOutput.openInTextEditor';148149registerAction2(class OpenCellOutputInEditorAction extends Action2 {150constructor() {151super({152id: OPEN_OUTPUT_COMMAND_ID,153title: localize('notebookActions.openOutputInEditor', "Open Cell Output in Text Editor"),154f1: false,155category: NOTEBOOK_ACTIONS_CATEGORY,156icon: icons.copyIcon,157});158}159160async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise<void> {161const editorService = accessor.get(IEditorService);162const notebookModelService = accessor.get(INotebookEditorModelResolverService);163const openerService = accessor.get(IOpenerService);164165const notebookEditor = getNotebookEditorFromContext(editorService, outputContext);166if (!notebookEditor) {167return;168}169170const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor);171172if (outputViewModel?.model.outputId && notebookEditor.textModel?.uri) {173// reserve notebook document reference since the active notebook editor might not be pinned so it can be replaced by the output editor174const ref = await notebookModelService.resolve(notebookEditor.textModel.uri);175await openerService.open(CellUri.generateCellOutputUriWithId(notebookEditor.textModel.uri, outputViewModel.model.outputId));176ref.dispose();177}178}179});180181export const SAVE_OUTPUT_IMAGE_COMMAND_ID = 'notebook.cellOutput.saveImage';182183registerAction2(class SaveCellOutputImageAction extends Action2 {184constructor() {185super({186id: SAVE_OUTPUT_IMAGE_COMMAND_ID,187title: localize('notebookActions.saveOutputImage', "Save Image"),188menu: {189id: MenuId.NotebookOutputToolbar,190when: ContextKeyExpr.regex(NOTEBOOK_CELL_OUTPUT_MIMETYPE.key, /^image\//)191},192f1: false,193category: NOTEBOOK_ACTIONS_CATEGORY,194icon: icons.saveIcon,195});196}197198async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise<void> {199const editorService = accessor.get(IEditorService);200const fileDialogService = accessor.get(IFileDialogService);201const fileService = accessor.get(IFileService);202const logService = accessor.get(ILogService);203204const notebookEditor = getNotebookEditorFromContext(editorService, outputContext);205if (!notebookEditor) {206return;207}208209const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor);210if (!outputViewModel) {211return;212}213214const mimeType = outputViewModel.pickedMimeType?.mimeType;215216// Only handle image mime types217if (!mimeType?.startsWith('image/')) {218return;219}220221const outputItem = outputViewModel.model.outputs.find(output => output.mime === mimeType);222if (!outputItem) {223logService.error('Could not find output item with mime type', mimeType);224return;225}226227// Determine file extension based on mime type228const mimeToExt: { [key: string]: string } = {229'image/png': 'png',230'image/jpeg': 'jpg',231'image/jpg': 'jpg',232'image/gif': 'gif',233'image/svg+xml': 'svg',234'image/webp': 'webp',235'image/bmp': 'bmp',236'image/tiff': 'tiff'237};238239const extension = mimeToExt[mimeType] || 'png';240const defaultFileName = `image.${extension}`;241242const defaultUri = notebookEditor.textModel?.uri243? URI.joinPath(URI.file(notebookEditor.textModel.uri.fsPath), '..', defaultFileName)244: undefined;245246const uri = await fileDialogService.showSaveDialog({247defaultUri,248filters: [{249name: localize('imageFiles', "Image Files"),250extensions: [extension]251}]252});253254if (!uri) {255return; // User cancelled256}257258try {259const imageData = outputItem.data;260await fileService.writeFile(uri, imageData);261logService.info('Saved image output to', uri.toString());262} catch (error) {263logService.error('Failed to save image output', error);264}265}266});267268export const OPEN_OUTPUT_IN_OUTPUT_PREVIEW_COMMAND_ID = 'notebook.cellOutput.openInOutputPreview';269270registerAction2(class OpenCellOutputInNotebookOutputEditorAction extends Action2 {271constructor() {272super({273id: OPEN_OUTPUT_IN_OUTPUT_PREVIEW_COMMAND_ID,274title: localize('notebookActions.openOutputInNotebookOutputEditor', "Open in Output Preview"),275menu: {276id: MenuId.NotebookOutputToolbar,277when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, ContextKeyExpr.equals('config.notebook.output.openInPreviewEditor.enabled', true))278},279f1: false,280category: NOTEBOOK_ACTIONS_CATEGORY,281});282}283284async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise<void> {285const editorService = accessor.get(IEditorService);286const openerService = accessor.get(IOpenerService);287288const notebookEditor = getNotebookEditorFromContext(editorService, outputContext);289if (!notebookEditor) {290return;291}292293const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor);294295if (!outputViewModel) {296return;297}298299const genericCellViewModel = outputViewModel.cellViewModel;300if (!genericCellViewModel) {301return;302}303304// get cell index305const cellViewModel = notebookEditor.getCellByHandle(genericCellViewModel.handle);306if (!cellViewModel) {307return;308}309const cellIndex = notebookEditor.getCellIndex(cellViewModel);310if (cellIndex === undefined) {311return;312}313314// get output index315const outputIndex = genericCellViewModel.outputsViewModels.indexOf(outputViewModel);316if (outputIndex === -1) {317return;318}319320if (!notebookEditor.textModel) {321return;322}323324// craft rich output URI to pass data to the notebook output editor/viewer325const outputURI = CellUri.generateOutputEditorUri(326notebookEditor.textModel.uri,327cellViewModel.id,328cellIndex,329outputViewModel.model.outputId,330outputIndex,331);332333openerService.open(outputURI, { openToSide: true });334}335});336337338