Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts
5244 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
7
import { localize } from '../../../../../nls.js';
8
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
9
import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';
10
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
11
import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from './coreActions.js';
12
import { NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_OUTPUT_MIMETYPE } from '../../common/notebookContextKeys.js';
13
import * as icons from '../notebookIcons.js';
14
import { ILogService } from '../../../../../platform/log/common/log.js';
15
import { copyCellOutput } from '../viewModel/cellOutputTextHelper.js';
16
import { IEditorService } from '../../../../services/editor/common/editorService.js';
17
import { ICellOutputViewModel, ICellViewModel, INotebookEditor, getNotebookEditorFromEditorPane } from '../notebookBrowser.js';
18
import { CellKind, CellUri } from '../../common/notebookCommon.js';
19
import { CodeCellViewModel } from '../viewModel/codeCellViewModel.js';
20
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
21
import { INotebookEditorModelResolverService } from '../../common/notebookEditorModelResolverService.js';
22
import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
23
import { IFileService } from '../../../../../platform/files/common/files.js';
24
import { URI } from '../../../../../base/common/uri.js';
25
26
export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copy';
27
28
registerAction2(class ShowAllOutputsAction extends Action2 {
29
constructor() {
30
super({
31
id: 'notebook.cellOuput.showEmptyOutputs',
32
title: localize('notebookActions.showAllOutput', "Show Empty Outputs"),
33
menu: {
34
id: MenuId.NotebookOutputToolbar,
35
when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS)
36
},
37
f1: false,
38
category: NOTEBOOK_ACTIONS_CATEGORY
39
});
40
}
41
42
run(accessor: ServicesAccessor, context: INotebookOutputActionContext): void {
43
const cell = context.cell;
44
if (cell && cell.cellKind === CellKind.Code) {
45
46
for (let i = 1; i < cell.outputsViewModels.length; i++) {
47
if (!cell.outputsViewModels[i].visible.get()) {
48
cell.outputsViewModels[i].setVisible(true, true);
49
(cell as CodeCellViewModel).updateOutputHeight(i, 1, 'command');
50
}
51
}
52
}
53
}
54
});
55
56
registerAction2(class CopyCellOutputAction extends Action2 {
57
constructor() {
58
super({
59
id: COPY_OUTPUT_COMMAND_ID,
60
title: localize('notebookActions.copyOutput', "Copy Cell Output"),
61
menu: {
62
id: MenuId.NotebookOutputToolbar,
63
when: NOTEBOOK_CELL_HAS_OUTPUTS
64
},
65
category: NOTEBOOK_ACTIONS_CATEGORY,
66
icon: icons.copyIcon,
67
});
68
}
69
70
async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise<void> {
71
const editorService = accessor.get(IEditorService);
72
const clipboardService = accessor.get(IClipboardService);
73
const logService = accessor.get(ILogService);
74
75
const notebookEditor = getNotebookEditorFromContext(editorService, outputContext);
76
if (!notebookEditor) {
77
return;
78
}
79
80
const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor);
81
if (!outputViewModel) {
82
return;
83
}
84
85
const mimeType = outputViewModel.pickedMimeType?.mimeType;
86
87
if (mimeType?.startsWith('image/')) {
88
const focusOptions = { skipReveal: true, outputId: outputViewModel.model.outputId, altOutputId: outputViewModel.model.alternativeOutputId };
89
await notebookEditor.focusNotebookCell(outputViewModel.cellViewModel as ICellViewModel, 'output', focusOptions);
90
notebookEditor.copyOutputImage(outputViewModel);
91
} else {
92
copyCellOutput(mimeType, outputViewModel, clipboardService, logService);
93
}
94
}
95
96
});
97
98
export function getOutputViewModelFromId(outputId: string, notebookEditor: INotebookEditor): ICellOutputViewModel | undefined {
99
const notebookViewModel = notebookEditor.getViewModel();
100
if (notebookViewModel) {
101
const codeCells = notebookViewModel.viewCells.filter(cell => cell.cellKind === CellKind.Code) as CodeCellViewModel[];
102
for (const cell of codeCells) {
103
const output = cell.outputsViewModels.find(output => output.model.outputId === outputId || output.model.alternativeOutputId === outputId);
104
if (output) {
105
return output;
106
}
107
}
108
}
109
110
return undefined;
111
}
112
113
function getNotebookEditorFromContext(editorService: IEditorService, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): INotebookEditor | undefined {
114
if (outputContext && 'notebookEditor' in outputContext) {
115
return outputContext.notebookEditor;
116
}
117
return getNotebookEditorFromEditorPane(editorService.activeEditorPane);
118
}
119
120
function getOutputViewModelFromContext(outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined, notebookEditor: INotebookEditor): ICellOutputViewModel | undefined {
121
let outputViewModel: ICellOutputViewModel | undefined;
122
123
if (outputContext && 'outputId' in outputContext && typeof outputContext.outputId === 'string') {
124
outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor);
125
} else if (outputContext && 'outputViewModel' in outputContext) {
126
outputViewModel = outputContext.outputViewModel;
127
}
128
129
if (!outputViewModel) {
130
// not able to find the output from the provided context, use the active cell
131
const activeCell = notebookEditor.getActiveCell();
132
if (!activeCell) {
133
return undefined;
134
}
135
136
if (activeCell.focusedOutputId !== undefined) {
137
outputViewModel = activeCell.outputsViewModels.find(output => {
138
return output.model.outputId === activeCell.focusedOutputId;
139
});
140
} else {
141
outputViewModel = activeCell.outputsViewModels.find(output => output.pickedMimeType?.isTrusted);
142
}
143
}
144
145
return outputViewModel;
146
}
147
148
export const OPEN_OUTPUT_COMMAND_ID = 'notebook.cellOutput.openInTextEditor';
149
150
registerAction2(class OpenCellOutputInEditorAction extends Action2 {
151
constructor() {
152
super({
153
id: OPEN_OUTPUT_COMMAND_ID,
154
title: localize('notebookActions.openOutputInEditor', "Open Cell Output in Text Editor"),
155
f1: false,
156
category: NOTEBOOK_ACTIONS_CATEGORY,
157
icon: icons.copyIcon,
158
});
159
}
160
161
async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise<void> {
162
const editorService = accessor.get(IEditorService);
163
const notebookModelService = accessor.get(INotebookEditorModelResolverService);
164
const openerService = accessor.get(IOpenerService);
165
166
const notebookEditor = getNotebookEditorFromContext(editorService, outputContext);
167
if (!notebookEditor) {
168
return;
169
}
170
171
const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor);
172
173
if (outputViewModel?.model.outputId && notebookEditor.textModel?.uri) {
174
// reserve notebook document reference since the active notebook editor might not be pinned so it can be replaced by the output editor
175
const ref = await notebookModelService.resolve(notebookEditor.textModel.uri);
176
await openerService.open(CellUri.generateCellOutputUriWithId(notebookEditor.textModel.uri, outputViewModel.model.outputId));
177
ref.dispose();
178
}
179
}
180
});
181
182
export const SAVE_OUTPUT_IMAGE_COMMAND_ID = 'notebook.cellOutput.saveImage';
183
184
registerAction2(class SaveCellOutputImageAction extends Action2 {
185
constructor() {
186
super({
187
id: SAVE_OUTPUT_IMAGE_COMMAND_ID,
188
title: localize('notebookActions.saveOutputImage', "Save Image"),
189
menu: {
190
id: MenuId.NotebookOutputToolbar,
191
when: ContextKeyExpr.regex(NOTEBOOK_CELL_OUTPUT_MIMETYPE.key, /^image\//)
192
},
193
f1: false,
194
category: NOTEBOOK_ACTIONS_CATEGORY,
195
icon: icons.saveIcon,
196
});
197
}
198
199
async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise<void> {
200
const editorService = accessor.get(IEditorService);
201
const fileDialogService = accessor.get(IFileDialogService);
202
const fileService = accessor.get(IFileService);
203
const logService = accessor.get(ILogService);
204
205
const notebookEditor = getNotebookEditorFromContext(editorService, outputContext);
206
if (!notebookEditor) {
207
return;
208
}
209
210
const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor);
211
if (!outputViewModel) {
212
return;
213
}
214
215
const mimeType = outputViewModel.pickedMimeType?.mimeType;
216
217
// Only handle image mime types
218
if (!mimeType?.startsWith('image/')) {
219
return;
220
}
221
222
const outputItem = outputViewModel.model.outputs.find(output => output.mime === mimeType);
223
if (!outputItem) {
224
logService.error('Could not find output item with mime type', mimeType);
225
return;
226
}
227
228
// Determine file extension based on mime type
229
const mimeToExt: { [key: string]: string } = {
230
'image/png': 'png',
231
'image/jpeg': 'jpg',
232
'image/jpg': 'jpg',
233
'image/gif': 'gif',
234
'image/svg+xml': 'svg',
235
'image/webp': 'webp',
236
'image/bmp': 'bmp',
237
'image/tiff': 'tiff'
238
};
239
240
const extension = mimeToExt[mimeType] || 'png';
241
const defaultFileName = `image.${extension}`;
242
243
const defaultUri = notebookEditor.textModel?.uri
244
? URI.joinPath(URI.file(notebookEditor.textModel.uri.fsPath), '..', defaultFileName)
245
: undefined;
246
247
const uri = await fileDialogService.showSaveDialog({
248
defaultUri,
249
filters: [{
250
name: localize('imageFiles', "Image Files"),
251
extensions: [extension]
252
}]
253
});
254
255
if (!uri) {
256
return; // User cancelled
257
}
258
259
try {
260
const imageData = outputItem.data;
261
await fileService.writeFile(uri, imageData);
262
logService.info('Saved image output to', uri.toString());
263
} catch (error) {
264
logService.error('Failed to save image output', error);
265
}
266
}
267
});
268
269
export const OPEN_OUTPUT_IN_OUTPUT_PREVIEW_COMMAND_ID = 'notebook.cellOutput.openInOutputPreview';
270
271
registerAction2(class OpenCellOutputInNotebookOutputEditorAction extends Action2 {
272
constructor() {
273
super({
274
id: OPEN_OUTPUT_IN_OUTPUT_PREVIEW_COMMAND_ID,
275
title: localize('notebookActions.openOutputInNotebookOutputEditor', "Open in Output Preview"),
276
menu: {
277
id: MenuId.NotebookOutputToolbar,
278
when: ContextKeyExpr.and(NOTEBOOK_CELL_HAS_OUTPUTS, ContextKeyExpr.equals('config.notebook.output.openInPreviewEditor.enabled', true))
279
},
280
f1: false,
281
category: NOTEBOOK_ACTIONS_CATEGORY,
282
});
283
}
284
285
async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise<void> {
286
const editorService = accessor.get(IEditorService);
287
const openerService = accessor.get(IOpenerService);
288
289
const notebookEditor = getNotebookEditorFromContext(editorService, outputContext);
290
if (!notebookEditor) {
291
return;
292
}
293
294
const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor);
295
296
if (!outputViewModel) {
297
return;
298
}
299
300
const genericCellViewModel = outputViewModel.cellViewModel;
301
if (!genericCellViewModel) {
302
return;
303
}
304
305
// get cell index
306
const cellViewModel = notebookEditor.getCellByHandle(genericCellViewModel.handle);
307
if (!cellViewModel) {
308
return;
309
}
310
const cellIndex = notebookEditor.getCellIndex(cellViewModel);
311
if (cellIndex === undefined) {
312
return;
313
}
314
315
// get output index
316
const outputIndex = genericCellViewModel.outputsViewModels.indexOf(outputViewModel);
317
if (outputIndex === -1) {
318
return;
319
}
320
321
if (!notebookEditor.textModel) {
322
return;
323
}
324
325
// craft rich output URI to pass data to the notebook output editor/viewer
326
const outputURI = CellUri.generateOutputEditorUri(
327
notebookEditor.textModel.uri,
328
cellViewModel.id,
329
cellIndex,
330
outputViewModel.model.outputId,
331
outputIndex,
332
);
333
334
openerService.open(outputURI, { openToSide: true });
335
}
336
});
337
338