/
notebookImagePaste.ts
157 lines (134 loc) · 5.34 KB
/
notebookImagePaste.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR } from './constants';
class CopyPasteEditProvider implements vscode.DocumentPasteEditProvider {
async provideDocumentPasteEdits(
document: vscode.TextDocument,
_ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
_token: vscode.CancellationToken
): Promise<vscode.DocumentPasteEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('ipynb', document).get('pasteImagesAsAttachments.enabled', false);
if (!enabled) {
return undefined;
}
// get b64 data from paste
// TODO: dataTransfer.get() limits to one image pasted
const dataItem = dataTransfer.get('image/png');
if (!dataItem) {
return undefined;
}
const fileDataAsUint8 = await dataItem.asFile()?.data();
if (!fileDataAsUint8) {
return undefined;
}
// get filename data from paste
const clipboardFilename = dataItem.asFile()?.name;
if (!clipboardFilename) {
return undefined;
}
const separatorIndex = clipboardFilename?.lastIndexOf('.');
const filename = clipboardFilename?.slice(0, separatorIndex);
const filetype = clipboardFilename?.slice(separatorIndex);
if (!filename || !filetype) {
return undefined;
}
const currentCell = this.getCellFromCellDocument(document);
if (!currentCell) {
return undefined;
}
const notebookUri = currentCell.notebook.uri;
// create updated metadata for cell (prep for WorkspaceEdit)
const b64string = encodeBase64(fileDataAsUint8);
const startingAttachments = currentCell.metadata.attachments;
const newAttachment = buildAttachment(b64string, currentCell, filename, filetype, startingAttachments);
// build edits
const nbEdit = vscode.NotebookEdit.updateCellMetadata(currentCell.index, newAttachment.metadata);
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.set(notebookUri, [nbEdit]);
// create a snippet for paste
const pasteSnippet = new vscode.SnippetString();
pasteSnippet.appendText('![');
pasteSnippet.appendPlaceholder(`${clipboardFilename}`);
pasteSnippet.appendText(`](attachment:${newAttachment.filename})`);
return { insertText: pasteSnippet, additionalEdit: workspaceEdit };
}
private getCellFromCellDocument(cellDocument: vscode.TextDocument): vscode.NotebookCell | undefined {
for (const notebook of vscode.workspace.notebookDocuments) {
if (notebook.uri.path === cellDocument.uri.path) {
for (const cell of notebook.getCells()) {
if (cell.document === cellDocument) {
return cell;
}
}
}
}
return undefined;
}
}
/**
* Taken from https://github.com/microsoft/vscode/blob/743b016722db90df977feecde0a4b3b4f58c2a4c/src/vs/base/common/buffer.ts#L350-L387
*/
function encodeBase64(buffer: Uint8Array, padded = true, urlSafe = false) {
const base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const base64UrlSafeAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
const dictionary = urlSafe ? base64UrlSafeAlphabet : base64Alphabet;
let output = '';
const remainder = buffer.byteLength % 3;
let i = 0;
for (; i < buffer.byteLength - remainder; i += 3) {
const a = buffer[i + 0];
const b = buffer[i + 1];
const c = buffer[i + 2];
output += dictionary[a >>> 2];
output += dictionary[(a << 4 | b >>> 4) & 0b111111];
output += dictionary[(b << 2 | c >>> 6) & 0b111111];
output += dictionary[c & 0b111111];
}
if (remainder === 1) {
const a = buffer[i + 0];
output += dictionary[a >>> 2];
output += dictionary[(a << 4) & 0b111111];
if (padded) { output += '=='; }
} else if (remainder === 2) {
const a = buffer[i + 0];
const b = buffer[i + 1];
output += dictionary[a >>> 2];
output += dictionary[(a << 4 | b >>> 4) & 0b111111];
output += dictionary[(b << 2) & 0b111111];
if (padded) { output += '='; }
}
return output;
}
function buildAttachment(b64: string, cell: vscode.NotebookCell, filename: string, filetype: string, startingAttachments: any): { metadata: { [key: string]: any }; filename: string } {
const cellMetadata = { ...cell.metadata };
let tempFilename = filename + filetype;
if (!cellMetadata.attachments) {
cellMetadata['attachments'] = { [tempFilename]: { 'image/png': b64 } };
} else {
for (let appendValue = 2; tempFilename in startingAttachments; appendValue++) {
const objEntries = Object.entries(startingAttachments[tempFilename]);
if (objEntries.length) { // check that mime:b64 are present
const [, attachmentb64] = objEntries[0];
if (attachmentb64 === b64) { // checking if filename can be reused, based on camparison of image data
break;
} else {
tempFilename = filename.concat(`-${appendValue}`) + filetype;
}
}
}
cellMetadata.attachments[tempFilename] = { 'image/png': b64 };
}
return {
metadata: cellMetadata,
filename: tempFilename
};
}
export function notebookImagePasteSetup() {
return vscode.languages.registerDocumentPasteEditProvider(JUPYTER_NOTEBOOK_MARKDOWN_SELECTOR, new CopyPasteEditProvider(), {
pasteMimeTypes: ['image/png'],
});
}