Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/ipynb/src/notebookImagePaste.ts
3292 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 * as vscode from 'vscode';
7
import { JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR } from './constants';
8
import { basename, extname } from 'path';
9
10
enum MimeType {
11
bmp = 'image/bmp',
12
gif = 'image/gif',
13
ico = 'image/ico',
14
jpeg = 'image/jpeg',
15
png = 'image/png',
16
tiff = 'image/tiff',
17
webp = 'image/webp',
18
plain = 'text/plain',
19
uriList = 'text/uri-list',
20
}
21
22
const imageMimeTypes: ReadonlySet<string> = new Set<string>([
23
MimeType.bmp,
24
MimeType.gif,
25
MimeType.ico,
26
MimeType.jpeg,
27
MimeType.png,
28
MimeType.tiff,
29
MimeType.webp,
30
]);
31
32
const imageExtToMime: ReadonlyMap<string, string> = new Map<string, string>([
33
['.bmp', MimeType.bmp],
34
['.gif', MimeType.gif],
35
['.ico', MimeType.ico],
36
['.jpe', MimeType.jpeg],
37
['.jpeg', MimeType.jpeg],
38
['.jpg', MimeType.jpeg],
39
['.png', MimeType.png],
40
['.tif', MimeType.tiff],
41
['.tiff', MimeType.tiff],
42
['.webp', MimeType.webp],
43
]);
44
45
function getImageMimeType(uri: vscode.Uri): string | undefined {
46
return imageExtToMime.get(extname(uri.fsPath).toLowerCase());
47
}
48
49
class DropOrPasteEditProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider {
50
51
public static readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link', 'image', 'attachment');
52
53
async provideDocumentPasteEdits(
54
document: vscode.TextDocument,
55
_ranges: readonly vscode.Range[],
56
dataTransfer: vscode.DataTransfer,
57
_context: vscode.DocumentPasteEditContext,
58
token: vscode.CancellationToken,
59
): Promise<vscode.DocumentPasteEdit[] | undefined> {
60
const enabled = vscode.workspace.getConfiguration('ipynb', document).get('pasteImagesAsAttachments.enabled', true);
61
if (!enabled) {
62
return;
63
}
64
65
const insert = await this.createInsertImageAttachmentEdit(document, dataTransfer, token);
66
if (!insert) {
67
return;
68
}
69
70
const pasteEdit = new vscode.DocumentPasteEdit(insert.insertText, vscode.l10n.t('Insert Image as Attachment'), DropOrPasteEditProvider.kind);
71
pasteEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Text];
72
pasteEdit.additionalEdit = insert.additionalEdit;
73
return [pasteEdit];
74
}
75
76
async provideDocumentDropEdits(
77
document: vscode.TextDocument,
78
_position: vscode.Position,
79
dataTransfer: vscode.DataTransfer,
80
token: vscode.CancellationToken,
81
): Promise<vscode.DocumentDropEdit | undefined> {
82
const insert = await this.createInsertImageAttachmentEdit(document, dataTransfer, token);
83
if (!insert) {
84
return;
85
}
86
87
const dropEdit = new vscode.DocumentDropEdit(insert.insertText);
88
dropEdit.yieldTo = [vscode.DocumentDropOrPasteEditKind.Text];
89
dropEdit.additionalEdit = insert.additionalEdit;
90
dropEdit.title = vscode.l10n.t('Insert Image as Attachment');
91
return dropEdit;
92
}
93
94
private async createInsertImageAttachmentEdit(
95
document: vscode.TextDocument,
96
dataTransfer: vscode.DataTransfer,
97
token: vscode.CancellationToken,
98
): Promise<{ insertText: vscode.SnippetString; additionalEdit: vscode.WorkspaceEdit } | undefined> {
99
const imageData = await getDroppedImageData(dataTransfer, token);
100
if (!imageData.length || token.isCancellationRequested) {
101
return;
102
}
103
104
const currentCell = getCellFromCellDocument(document);
105
if (!currentCell) {
106
return undefined;
107
}
108
109
// create updated metadata for cell (prep for WorkspaceEdit)
110
const newAttachment = buildAttachment(currentCell, imageData);
111
if (!newAttachment) {
112
return;
113
}
114
115
// build edits
116
const additionalEdit = new vscode.WorkspaceEdit();
117
const nbEdit = vscode.NotebookEdit.updateCellMetadata(currentCell.index, newAttachment.metadata);
118
const notebookUri = currentCell.notebook.uri;
119
additionalEdit.set(notebookUri, [nbEdit]);
120
121
// create a snippet for paste
122
const insertText = new vscode.SnippetString();
123
newAttachment.filenames.forEach((filename, i) => {
124
insertText.appendText('![');
125
insertText.appendPlaceholder(`${filename}`);
126
insertText.appendText(`](${/\s/.test(filename) ? `<attachment:${filename}>` : `attachment:${filename}`})`);
127
if (i !== newAttachment.filenames.length - 1) {
128
insertText.appendText(' ');
129
}
130
});
131
132
return { insertText, additionalEdit };
133
}
134
}
135
136
async function getDroppedImageData(
137
dataTransfer: vscode.DataTransfer,
138
token: vscode.CancellationToken,
139
): Promise<readonly ImageAttachmentData[]> {
140
141
// Prefer using image data in the clipboard
142
const files = coalesce(await Promise.all(Array.from(dataTransfer, async ([mimeType, item]): Promise<ImageAttachmentData | undefined> => {
143
if (!imageMimeTypes.has(mimeType)) {
144
return;
145
}
146
147
const file = item.asFile();
148
if (!file) {
149
return;
150
}
151
152
const data = await file.data();
153
return { fileName: file.name, mimeType, data };
154
})));
155
if (files.length) {
156
return files;
157
}
158
159
// Then fallback to image files in the uri-list
160
const urlList = await dataTransfer.get('text/uri-list')?.asString();
161
if (token.isCancellationRequested) {
162
return [];
163
}
164
165
if (urlList) {
166
const uris: vscode.Uri[] = [];
167
for (const resource of urlList.split(/\r?\n/g)) {
168
try {
169
uris.push(vscode.Uri.parse(resource));
170
} catch {
171
// noop
172
}
173
}
174
175
const entries = await Promise.all(uris.map(async (uri) => {
176
const mimeType = getImageMimeType(uri);
177
if (!mimeType) {
178
return;
179
}
180
181
const data = await vscode.workspace.fs.readFile(uri);
182
return { fileName: basename(uri.fsPath), mimeType, data };
183
}));
184
185
return coalesce(entries);
186
}
187
188
return [];
189
}
190
191
function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
192
return <T[]>array.filter(e => !!e);
193
}
194
195
function getCellFromCellDocument(cellDocument: vscode.TextDocument): vscode.NotebookCell | undefined {
196
for (const notebook of vscode.workspace.notebookDocuments) {
197
if (notebook.uri.path === cellDocument.uri.path) {
198
for (const cell of notebook.getCells()) {
199
if (cell.document === cellDocument) {
200
return cell;
201
}
202
}
203
}
204
}
205
return undefined;
206
}
207
208
/**
209
* Taken from https://github.com/microsoft/vscode/blob/743b016722db90df977feecde0a4b3b4f58c2a4c/src/vs/base/common/buffer.ts#L350-L387
210
*/
211
function encodeBase64(buffer: Uint8Array, padded = true, urlSafe = false) {
212
const base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
213
const base64UrlSafeAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
214
215
const dictionary = urlSafe ? base64UrlSafeAlphabet : base64Alphabet;
216
let output = '';
217
218
const remainder = buffer.byteLength % 3;
219
220
let i = 0;
221
for (; i < buffer.byteLength - remainder; i += 3) {
222
const a = buffer[i + 0];
223
const b = buffer[i + 1];
224
const c = buffer[i + 2];
225
226
output += dictionary[a >>> 2];
227
output += dictionary[(a << 4 | b >>> 4) & 0b111111];
228
output += dictionary[(b << 2 | c >>> 6) & 0b111111];
229
output += dictionary[c & 0b111111];
230
}
231
232
if (remainder === 1) {
233
const a = buffer[i + 0];
234
output += dictionary[a >>> 2];
235
output += dictionary[(a << 4) & 0b111111];
236
if (padded) { output += '=='; }
237
} else if (remainder === 2) {
238
const a = buffer[i + 0];
239
const b = buffer[i + 1];
240
output += dictionary[a >>> 2];
241
output += dictionary[(a << 4 | b >>> 4) & 0b111111];
242
output += dictionary[(b << 2) & 0b111111];
243
if (padded) { output += '='; }
244
}
245
246
return output;
247
}
248
249
250
interface ImageAttachmentData {
251
readonly fileName: string;
252
readonly data: Uint8Array;
253
readonly mimeType: string;
254
}
255
256
function buildAttachment(
257
cell: vscode.NotebookCell,
258
attachments: readonly ImageAttachmentData[],
259
): { metadata: { [key: string]: any }; filenames: string[] } | undefined {
260
const cellMetadata = { ...cell.metadata };
261
const tempFilenames: string[] = [];
262
if (!attachments.length) {
263
return undefined;
264
}
265
266
if (!cellMetadata.attachments) {
267
cellMetadata.attachments = {};
268
}
269
270
for (const attachment of attachments) {
271
const b64 = encodeBase64(attachment.data);
272
273
const fileExt = extname(attachment.fileName);
274
const filenameWithoutExt = basename(attachment.fileName, fileExt);
275
276
let tempFilename = filenameWithoutExt + fileExt;
277
for (let appendValue = 2; tempFilename in cellMetadata.attachments; appendValue++) {
278
const objEntries = Object.entries(cellMetadata.attachments[tempFilename]);
279
if (objEntries.length) { // check that mime:b64 are present
280
const [mime, attachmentb64] = objEntries[0];
281
if (mime === attachment.mimeType && attachmentb64 === b64) { // checking if filename can be reused, based on comparison of image data
282
break;
283
} else {
284
tempFilename = filenameWithoutExt.concat(`-${appendValue}`) + fileExt;
285
}
286
}
287
}
288
289
tempFilenames.push(tempFilename);
290
cellMetadata.attachments[tempFilename] = { [attachment.mimeType]: b64 };
291
}
292
293
return {
294
metadata: cellMetadata,
295
filenames: tempFilenames,
296
};
297
}
298
299
export function notebookImagePasteSetup(): vscode.Disposable {
300
const provider = new DropOrPasteEditProvider();
301
return vscode.Disposable.from(
302
vscode.languages.registerDocumentPasteEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, {
303
providedPasteEditKinds: [DropOrPasteEditProvider.kind],
304
pasteMimeTypes: [
305
MimeType.png,
306
MimeType.uriList,
307
],
308
}),
309
vscode.languages.registerDocumentDropEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, provider, {
310
providedDropEditKinds: [DropOrPasteEditProvider.kind],
311
dropMimeTypes: [
312
...Object.values(imageExtToMime),
313
MimeType.uriList,
314
],
315
})
316
);
317
}
318
319