mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-28 13:56:50 +01:00
logic for when menu item is shown, copy text version of image
This commit is contained in:
@@ -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'
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user