From e2e587dfe5d22c072a8151bd64fb54e9f0c3e6cb Mon Sep 17 00:00:00 2001 From: aamunger Date: Tue, 8 Aug 2023 16:11:54 -0700 Subject: [PATCH] add action to copy output text --- .../browser/controller/copyOutputAction.ts | 63 +++++++++++++++++++ .../browser/controller/coreActions.ts | 6 +- .../notebook/browser/notebook.contribution.ts | 1 + .../contrib/notebook/browser/notebookIcons.ts | 1 + .../browser/view/cellParts/cellOutput.ts | 9 +-- 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 src/vs/workbench/contrib/notebook/browser/controller/copyOutputAction.ts diff --git a/src/vs/workbench/contrib/notebook/browser/controller/copyOutputAction.ts b/src/vs/workbench/contrib/notebook/browser/controller/copyOutputAction.ts new file mode 100644 index 00000000000..a1029e05c67 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/controller/copyOutputAction.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { localize } from 'vs/nls'; +import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { CELL_TITLE_OUTPUT_GROUP_ID, INotebookOutputActionContext, NotebookAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { NOTEBOOK_CELL_HAS_OUTPUTS } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { ILogService } from 'vs/platform/log/common/log'; + +registerAction2(class CopyCellOutputAction extends NotebookAction { + constructor() { + super( + { + id: 'CopyCellOutput', + title: localize('notebookActions.copyOutput', "Copy Output to Clipboard"), + menu: { + id: MenuId.NotebookOutputToolbar, + when: NOTEBOOK_CELL_HAS_OUTPUTS, + group: CELL_TITLE_OUTPUT_GROUP_ID + }, + icon: icons.copyIcon, + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookOutputActionContext): Promise { + const clipboardService = accessor.get(IClipboardService); + const logService = accessor.get(ILogService); + + const outputViewModel = context.outputViewModel; + const outputTextModel = outputViewModel.model; + const mimeType = outputViewModel.pickedMimeType?.mimeType; + const buffer = outputTextModel.outputs.find(output => output.mime === mimeType); + + if (!buffer || !mimeType) { + return; + } + + const charLimit = 100_000; + const decoder = new TextDecoder(); + let text = decoder.decode(buffer.data.slice(0, charLimit).buffer); + + if (buffer.data.byteLength > charLimit) { + text = text + '...(truncated)'; + } + + if (mimeType.endsWith('error')) { + text = text.replace(/\\u001b\[[0-9;]*m/gi, '').replaceAll('\\n', '\n'); + } + + try { + await clipboardService.writeText(text); + + } catch (e) { + logService.error(`Failed to copy content: ${e}`); + } + + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index 34031dc51c3..aca17332c77 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -9,7 +9,7 @@ import { Action2, IAction2Options, MenuId, MenuRegistry } from 'vs/platform/acti import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, cellRangeToViewCells } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, cellRangeToViewCells, ICellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SOURCE_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { ICellRange, isICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -70,6 +70,10 @@ export interface INotebookCellActionContext extends INotebookActionContext { cell: ICellViewModel; } +export interface INotebookOutputActionContext extends INotebookCellActionContext { + outputViewModel: ICellOutputViewModel; +} + export function getContextFromActiveEditor(editorService: IEditorService): INotebookActionContext | undefined { const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); if (!editor || !editor.hasModel()) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 7df3d82cc89..a4f1aa4dd7f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -64,6 +64,7 @@ import 'vs/workbench/contrib/notebook/browser/controller/insertCellActions'; import 'vs/workbench/contrib/notebook/browser/controller/executeActions'; import 'vs/workbench/contrib/notebook/browser/controller/layoutActions'; import 'vs/workbench/contrib/notebook/browser/controller/editActions'; +import 'vs/workbench/contrib/notebook/browser/controller/copyOutputAction'; import 'vs/workbench/contrib/notebook/browser/controller/apiActions'; import 'vs/workbench/contrib/notebook/browser/controller/foldingController'; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index dc87541717f..69c932935eb 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -31,6 +31,7 @@ export const openAsTextIcon = registerIcon('notebook-open-as-text', Codicon.file export const revertIcon = registerIcon('notebook-revert', Codicon.discard, localize('revertIcon', 'Icon to revert in notebook editors.')); export const renderOutputIcon = registerIcon('notebook-render-output', Codicon.preview, localize('renderOutputIcon', 'Icon to render output in diff editor.')); export const mimetypeIcon = registerIcon('notebook-mimetype', Codicon.code, localize('mimetypeIcon', 'Icon for a mime type in notebook editors.')); +export const copyIcon = registerIcon('notebook-copy', Codicon.copy, localize('copyIcon', 'Icon to copy content to clipboard')); export const previousChangeIcon = registerIcon('notebook-diff-editor-previous-change', Codicon.arrowUp, localize('previousChangeIcon', 'Icon for the previous change action in the diff editor.')); export const nextChangeIcon = registerIcon('notebook-diff-editor-next-change', Codicon.arrowDown, localize('nextChangeIcon', 'Icon for the next change action in the diff editor.')); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index c45ec2487c9..4dca2df7df3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -21,7 +21,7 @@ import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/commo import { ThemeIcon } from 'vs/base/common/themables'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSION_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { INotebookOutputActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { ICellOutputViewModel, ICellViewModel, IInsetRenderOutput, INotebookEditorDelegate, JUPYTER_EXTENSION_ID, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { mimetypeIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; @@ -254,8 +254,8 @@ class CellOutputElement extends Disposable { } private async _attachToolbar(outputItemDiv: HTMLElement, notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, index: number, mimeTypes: readonly IOrderedMimeType[]) { - const hasMultipleMimeTypes = mimeTypes.filter(mimeType => mimeType.isTrusted).length <= 1; - if (index > 0 && hasMultipleMimeTypes) { + const hasMultipleMimeTypes = mimeTypes.filter(mimeType => mimeType.isTrusted).length > 1; + if (index > 0 && !hasMultipleMimeTypes) { return; } @@ -273,9 +273,10 @@ class CellOutputElement extends Disposable { const toolbar = this._renderDisposableStore.add(this.instantiationService.createInstance(WorkbenchToolBar, mimeTypePicker, { renderDropdownAsChildElement: false })); - toolbar.context = { + toolbar.context = { ui: true, cell: this.output.cellViewModel as ICellViewModel, + outputViewModel: this.output, notebookEditor: this.notebookEditor, $mid: MarshalledId.NotebookCellActionContext };