allow copying cell output images from context menu

This commit is contained in:
Aaron Munger 2023-08-18 10:43:43 -07:00
parent e40344eba7
commit bd60cc529c
5 changed files with 182 additions and 140 deletions

View File

@ -1,124 +1,134 @@
{
"name": "ipynb",
"displayName": "%displayName%",
"description": "%description%",
"publisher": "vscode",
"version": "1.0.0",
"license": "MIT",
"engines": {
"vscode": "^1.57.0"
},
"enabledApiProposals": [
"documentPaste",
"diffContentOptions",
"dropMetadata"
],
"activationEvents": [
"onNotebook:jupyter-notebook",
"onNotebookSerializer:interactive"
],
"extensionKind": [
"workspace",
"ui"
],
"main": "./out/ipynbMain.js",
"browser": "./dist/browser/ipynbMain.js",
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
}
},
"contributes": {
"configuration": [
{
"properties": {
"ipynb.pasteImagesAsAttachments.enabled": {
"type": "boolean",
"scope": "resource",
"markdownDescription": "%ipynb.pasteImagesAsAttachments.enabled%",
"default": true
}
}
}
],
"commands": [
{
"command": "ipynb.newUntitledIpynb",
"title": "%newUntitledIpynb.title%",
"shortTitle": "%newUntitledIpynb.shortTitle%",
"category": "Create"
},
{
"command": "ipynb.openIpynbInNotebookEditor",
"title": "%openIpynbInNotebookEditor.title%"
},
{
"command": "ipynb.cleanInvalidImageAttachment",
"title": "%cleanInvalidImageAttachment.title%"
}
],
"notebooks": [
{
"type": "jupyter-notebook",
"displayName": "Jupyter Notebook",
"selector": [
{
"filenamePattern": "*.ipynb"
}
],
"priority": "default"
}
],
"notebookRenderer": [
{
"id": "vscode.markdown-it-cell-attachment-renderer",
"displayName": "%markdownAttachmentRenderer.displayName%",
"entrypoint": {
"extends": "vscode.markdown-it-renderer",
"path": "./notebook-out/cellAttachmentRenderer.js"
}
}
],
"menus": {
"file/newFile": [
{
"command": "ipynb.newUntitledIpynb",
"group": "notebook"
}
],
"commandPalette": [
{
"command": "ipynb.newUntitledIpynb"
},
{
"command": "ipynb.openIpynbInNotebookEditor",
"when": "false"
},
{
"command": "ipynb.cleanInvalidImageAttachment",
"when": "false"
}
]
}
},
"scripts": {
"compile": "npx gulp compile-extension:ipynb && npm run build-notebook",
"watch": "npx gulp watch-extension:ipynb",
"build-notebook": "node ./esbuild"
},
"dependencies": {
"@enonic/fnv-plus": "^1.3.0",
"detect-indent": "^6.0.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@jupyterlab/nbformat": "^3.2.9",
"@types/markdown-it": "12.2.3",
"@types/uuid": "^8.3.1"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
"name": "ipynb",
"displayName": "%displayName%",
"description": "%description%",
"publisher": "vscode",
"version": "1.0.0",
"license": "MIT",
"engines": {
"vscode": "^1.57.0"
},
"enabledApiProposals": [
"documentPaste",
"diffContentOptions",
"dropMetadata"
],
"activationEvents": [
"onNotebook:jupyter-notebook",
"onNotebookSerializer:interactive"
],
"extensionKind": [
"workspace",
"ui"
],
"main": "./out/ipynbMain.js",
"browser": "./dist/browser/ipynbMain.js",
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
}
},
"contributes": {
"configuration": [
{
"properties": {
"ipynb.pasteImagesAsAttachments.enabled": {
"type": "boolean",
"scope": "resource",
"markdownDescription": "%ipynb.pasteImagesAsAttachments.enabled%",
"default": true
}
}
}
],
"commands": [
{
"command": "ipynb.newUntitledIpynb",
"title": "%newUntitledIpynb.title%",
"shortTitle": "%newUntitledIpynb.shortTitle%",
"category": "Create"
},
{
"command": "ipynb.openIpynbInNotebookEditor",
"title": "%openIpynbInNotebookEditor.title%"
},
{
"command": "ipynb.cleanInvalidImageAttachment",
"title": "%cleanInvalidImageAttachment.title%"
},
{
"command": "notebook.cellOutput.copyToClipboard",
"title": "%copyOutputToClipboard.title%"
}
],
"notebooks": [
{
"type": "jupyter-notebook",
"displayName": "Jupyter Notebook",
"selector": [
{
"filenamePattern": "*.ipynb"
}
],
"priority": "default"
}
],
"notebookRenderer": [
{
"id": "vscode.markdown-it-cell-attachment-renderer",
"displayName": "%markdownAttachmentRenderer.displayName%",
"entrypoint": {
"extends": "vscode.markdown-it-renderer",
"path": "./notebook-out/cellAttachmentRenderer.js"
}
}
],
"menus": {
"file/newFile": [
{
"command": "ipynb.newUntitledIpynb",
"group": "notebook"
}
],
"commandPalette": [
{
"command": "ipynb.newUntitledIpynb"
},
{
"command": "ipynb.openIpynbInNotebookEditor",
"when": "false"
},
{
"command": "ipynb.cleanInvalidImageAttachment",
"when": "false"
}
],
"webview/context": [
{
"command": "notebook.cellOutput.copyToClipboard",
"when": "webviewId == 'notebook.output' && webviewSection == 'image'"
}
]
}
},
"scripts": {
"compile": "npx gulp compile-extension:ipynb && npm run build-notebook",
"watch": "npx gulp watch-extension:ipynb",
"build-notebook": "node ./esbuild"
},
"dependencies": {
"@enonic/fnv-plus": "^1.3.0",
"detect-indent": "^6.0.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@jupyterlab/nbformat": "^3.2.9",
"@types/markdown-it": "12.2.3",
"@types/uuid": "^8.3.1"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}

View File

@ -6,6 +6,7 @@
"newUntitledIpynb.shortTitle": "Jupyter Notebook",
"openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor",
"cleanInvalidImageAttachment.title": "Clean Invalid Image Attachment Reference",
"copyOutputToClipboard.title": "Copy Output to Clipboard",
"markdownAttachmentRenderer.displayName": {
"message": "Markdown-It ipynb Cell Attachment renderer",
"comment": [

View File

@ -37,6 +37,7 @@ function renderImage(outputInfo: OutputItem, element: HTMLElement): IDisposable
if (alt) {
image.alt = alt;
}
image.setAttribute('data-vscode-context', JSON.stringify({ webviewSection: 'image', outputId: outputInfo.id, 'preventDefaultContextMenuItems': true }));
const display = document.createElement('div');
display.classList.add('display');
display.appendChild(image);

View File

@ -5,39 +5,51 @@
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { INotebookOutputActionContext, NotebookAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } 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';
import { copyCellOutput } from 'vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { ICellOutputViewModel, ICellViewModel, INotebookEditor, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copyToClipboard';
registerAction2(class CopyCellOutputAction extends NotebookAction {
registerAction2(class CopyCellOutputAction extends Action2 {
constructor() {
super(
{
id: COPY_OUTPUT_COMMAND_ID,
title: localize('notebookActions.copyOutput', "Copy Output to Clipboard"),
menu: {
id: MenuId.NotebookOutputToolbar,
when: NOTEBOOK_CELL_HAS_OUTPUTS
},
icon: icons.copyIcon,
});
super({
id: COPY_OUTPUT_COMMAND_ID,
title: localize('notebookActions.copyOutput', "Copy Output to Clipboard"),
menu: {
id: MenuId.NotebookOutputToolbar,
when: NOTEBOOK_CELL_HAS_OUTPUTS
},
category: NOTEBOOK_ACTIONS_CATEGORY,
icon: icons.copyIcon,
});
}
async runWithContext(accessor: ServicesAccessor, context: INotebookOutputActionContext): Promise<void> {
const outputViewModel = context.outputViewModel;
async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel }): Promise<void> {
const editorService = accessor.get(IEditorService);
let outputViewModel: ICellOutputViewModel | undefined;
if ('outputId' in outputContext && typeof outputContext.outputId === 'string') {
outputViewModel = getOutputViewModelFromId(outputContext.outputId, editorService);
} else {
outputViewModel = outputContext.outputViewModel;
}
if (!outputViewModel) {
return;
}
const mimeType = outputViewModel.pickedMimeType?.mimeType;
if (mimeType?.startsWith('image/')) {
const editorService = accessor.get(IEditorService);
const editor = editorService.activeEditorPane?.getControl() as INotebookEditor;
await editor.focusNotebookCell(outputViewModel.cellViewModel as ICellViewModel, 'output', { skipReveal: true, outputId: outputViewModel.model.outputId });
editor.copyOutputImage(outputViewModel);
@ -48,4 +60,20 @@ registerAction2(class CopyCellOutputAction extends NotebookAction {
copyCellOutput(mimeType, outputViewModel, clipboardService, logService);
}
}
});
function getOutputViewModelFromId(outputId: string, editorService: IEditorService): ICellOutputViewModel | undefined {
const notebookViewModel = getNotebookEditorFromEditorPane(editorService.activeEditorPane)?.getViewModel();
if (notebookViewModel) {
const codeCells = notebookViewModel.viewCells.filter(cell => cell.cellKind === CellKind.Code) as CodeCellViewModel[];
for (const cell of codeCells) {
const output = cell.outputsViewModels.find(output => output.model.outputId === outputId);
if (output) {
return output;
}
}
}
return undefined;
}

View File

@ -1074,10 +1074,12 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
allowScripts: true,
localResourceRoots: this.localResourceRootsCache,
},
extension: undefined
extension: undefined,
providedViewType: 'notebook.output'
});
webview.setHtml(content);
webview.setContextKeyService(this.contextKeyService);
return webview;
}