logic for when menu item is shown, copy text version of image

This commit is contained in:
aamunger
2023-08-09 14:46:07 -07:00
parent e2e587dfe5
commit 8d73d94ae4
4 changed files with 103 additions and 29 deletions
@@ -11,12 +11,17 @@ import { CELL_TITLE_OUTPUT_GROUP_ID, INotebookOutputActionContext, NotebookActio
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';
import { isTextStreamMime } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CLIPBOARD_COMPATIBLE_MIMETYPES } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon';
export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copyToClipboard';
registerAction2(class CopyCellOutputAction extends NotebookAction {
constructor() {
super(
{
id: 'CopyCellOutput',
id: COPY_OUTPUT_COMMAND_ID,
title: localize('notebookActions.copyOutput', "Copy Output to Clipboard"),
menu: {
id: MenuId.NotebookOutputToolbar,
@@ -28,23 +33,46 @@ registerAction2(class CopyCellOutputAction extends NotebookAction {
}
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) {
const mimeType = outputViewModel.pickedMimeType?.mimeType;
const output = mimeType && CLIPBOARD_COMPATIBLE_MIMETYPES.includes(mimeType) ?
outputTextModel.outputs.find(output => output.mime === mimeType) :
outputTextModel.outputs.find(output => CLIPBOARD_COMPATIBLE_MIMETYPES.includes(output.mime));
if (!mimeType || !output) {
return;
}
const charLimit = 100_000;
const charLimit = 100_000_000;
const decoder = new TextDecoder();
let text = decoder.decode(buffer.data.slice(0, charLimit).buffer);
let text = decoder.decode(output.data.slice(0, charLimit).buffer);
let totalLength = output.data.byteLength;
if (buffer.data.byteLength > charLimit) {
// append adjacent text streams since they are concatenated in the renderer
if (isTextStreamMime(mimeType)) {
const cellViewModel = outputViewModel.cellViewModel as ICellViewModel;
let index = cellViewModel.outputsViewModels.indexOf(outputViewModel) + 1;
while (index < cellViewModel.outputsViewModels.length && text.length < charLimit) {
const nextOutputViewModel = cellViewModel.outputsViewModels[index];
const nextMimeType = nextOutputViewModel?.pickedMimeType?.mimeType;
const nextOutputTextModel = cellViewModel.model.outputs[index];
if (!nextOutputViewModel || !nextMimeType || !isTextStreamMime(nextMimeType)) {
break;
}
const nextOutput = nextOutputTextModel.outputs.find(output => output.mime === nextMimeType);
if (nextOutput) {
text = text + decoder.decode(nextOutput.data.slice(0, charLimit).buffer);
totalLength = totalLength + nextOutput.data.byteLength;
}
index = index + 1;
}
}
if (totalLength > charLimit) {
text = text + '...(truncated)';
}
@@ -52,12 +80,13 @@ registerAction2(class CopyCellOutputAction extends NotebookAction {
text = text.replace(/\\u001b\[[0-9;]*m/gi, '').replaceAll('\\n', '\n');
}
const clipboardService = accessor.get(IClipboardService);
const logService = accessor.get(ILogService);
try {
await clipboardService.writeText(text);
} catch (e) {
logService.error(`Failed to copy content: ${e}`);
}
}
});
@@ -37,7 +37,7 @@ import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/in
const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs';
const EDIT_CELL_COMMAND_ID = 'notebook.cell.edit';
const DELETE_CELL_COMMAND_ID = 'notebook.cell.delete';
const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs';
export const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs';
registerAction2(class EditCellAction extends NotebookCellAction {
constructor() {
@@ -25,14 +25,16 @@ import { INotebookOutputActionContext } from 'vs/workbench/contrib/notebook/brow
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';
import { CodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon';
import { CLIPBOARD_COMPATIBLE_MIMETYPES, CodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { CellUri, IOrderedMimeType, NotebookCellOutputsSplice, RENDERER_NOT_AVAILABLE } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CellUri, IOrderedMimeType, NotebookCellOutputsSplice, RENDERER_NOT_AVAILABLE, isTextStreamMime } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { COPY_OUTPUT_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/controller/copyOutputAction';
import { CLEAR_CELL_OUTPUTS_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/controller/editActions';
interface IMimeTypeRenderer extends IQuickPickItem {
index: number;
@@ -253,9 +255,29 @@ class CellOutputElement extends Disposable {
return { type: RenderOutputType.Html, source: viewModel, htmlContent: el.outerHTML };
}
private shouldEnableCopy(mimeTypes: readonly IOrderedMimeType[]) {
if (!mimeTypes.find(mimeType => CLIPBOARD_COMPATIBLE_MIMETYPES.indexOf(mimeType.mimeType))) {
return false;
}
if (isTextStreamMime(mimeTypes[0].mimeType)) {
const cellViewModel = this.output.cellViewModel as ICellViewModel;
const index = cellViewModel.outputsViewModels.indexOf(this.output);
if (index > 0) {
const previousOutput = cellViewModel.model.outputs[index - 1];
// if the previous output was also a stream, the copy command will be in that output instead
return !isTextStreamMime(previousOutput.outputs[0].mime);
}
}
return true;
}
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 isCopyEnabled = this.shouldEnableCopy(mimeTypes);
if (index > 0 && !hasMultipleMimeTypes && !isCopyEnabled) {
// nothing to put in the toolbar
return;
}
@@ -284,21 +306,30 @@ class CellOutputElement extends Disposable {
// TODO: This could probably be a real registered action, but it has to talk to this output element
const pickAction = new Action('notebook.output.pickMimetype', nls.localize('pickMimeType', "Change Presentation"), ThemeIcon.asClassName(mimetypeIcon), undefined,
async _context => this._pickActiveMimeTypeRenderer(outputItemDiv, notebookTextModel, kernel, this.output));
if (index === 0 && useConsolidatedButton) {
const menu = this._renderDisposableStore.add(this.menuService.createMenu(MenuId.NotebookOutputToolbar, this.contextKeyService));
const updateMenuToolbar = () => {
const primary: IAction[] = [];
const secondary: IAction[] = [];
const result = { primary, secondary };
createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result, () => false);
toolbar.setActions([], [pickAction, ...secondary]);
};
updateMenuToolbar();
this._renderDisposableStore.add(menu.onDidChange(updateMenuToolbar));
} else {
toolbar.setActions([pickAction]);
}
const menu = this._renderDisposableStore.add(this.menuService.createMenu(MenuId.NotebookOutputToolbar, this.contextKeyService));
const updateMenuToolbar = () => {
const primary: IAction[] = [];
let secondary: IAction[] = [];
const result = { primary, secondary };
createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, result, () => false);
if (index > 0 || !useConsolidatedButton) {
// clear outputs should only appear in the first output item's menu
secondary = secondary.filter((action) => action.id !== CLEAR_CELL_OUTPUTS_COMMAND_ID);
}
if (!isCopyEnabled) {
secondary = secondary.filter((action) => action.id !== COPY_OUTPUT_COMMAND_ID);
}
if (hasMultipleMimeTypes) {
secondary = [pickAction, ...secondary];
}
toolbar.setActions([], secondary);
};
updateMenuToolbar();
this._renderDisposableStore.add(menu.onDidChange(updateMenuToolbar));
}
private async _pickActiveMimeTypeRenderer(outputItemDiv: HTMLElement, notebookTextModel: NotebookTextModel, kernel: INotebookKernel | undefined, viewModel: ICellOutputViewModel) {
@@ -756,3 +787,5 @@ const JUPYTER_RENDERER_MIMETYPES = [
'application/vnd.jupyter.widget-view+json',
'application/vnd.code.notebook.error'
];
@@ -114,3 +114,15 @@ export interface CodeCellRenderTemplate extends BaseCellRenderTemplate {
focusSinkElement: HTMLElement;
editor: ICodeEditor;
}
export const CLIPBOARD_COMPATIBLE_MIMETYPES = [
'text/latex',
'text/html',
'application/vnd.code.notebook.error',
'application/vnd.code.notebook.stdout',
'application/x.notebook.stdout',
'application/x.notebook.stream',
'application/vnd.code.notebook.stderr',
'application/x.notebook.stderr',
'text/plain'
];