add action to copy output text

This commit is contained in:
aamunger
2023-08-08 16:11:54 -07:00
parent 02e90ed9c6
commit e2e587dfe5
5 changed files with 75 additions and 5 deletions
@@ -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<void> {
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}`);
}
}
});
@@ -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()) {
@@ -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';
@@ -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.'));
@@ -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 = <INotebookCellActionContext>{
toolbar.context = <INotebookOutputActionContext>{
ui: true,
cell: this.output.cellViewModel as ICellViewModel,
outputViewModel: this.output,
notebookEditor: this.notebookEditor,
$mid: MarshalledId.NotebookCellActionContext
};