Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropOrPasteResource.ts
3294 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 { IMdParser } from '../../markdownEngine';
8
import { coalesce } from '../../util/arrays';
9
import { getParentDocumentUri } from '../../util/document';
10
import { getMediaKindForMime, MediaKind, Mime, rootMediaMimesTypes } from '../../util/mimes';
11
import { Schemes } from '../../util/schemes';
12
import { UriList } from '../../util/uriList';
13
import { NewFilePathGenerator } from './newFilePathGenerator';
14
import { audioEditKind, baseLinkEditKind, createInsertUriListEdit, createUriListSnippet, DropOrPasteEdit, getSnippetLabelAndKind, imageEditKind, linkEditKind, videoEditKind } from './shared';
15
import { InsertMarkdownLink, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste';
16
17
enum CopyFilesSettings {
18
Never = 'never',
19
MediaFiles = 'mediaFiles',
20
}
21
22
/**
23
* Provides support for pasting or dropping resources into markdown documents.
24
*
25
* This includes:
26
*
27
* - `text/uri-list` data in the data transfer.
28
* - File object in the data transfer.
29
* - Media data in the data transfer, such as `image/png`.
30
*/
31
class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, vscode.DocumentDropEditProvider {
32
33
public static readonly mimeTypes = [
34
Mime.textUriList,
35
'files',
36
...Object.values(rootMediaMimesTypes).map(type => `${type}/*`),
37
];
38
39
private readonly _yieldTo = [
40
vscode.DocumentDropOrPasteEditKind.Text,
41
vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link', 'image', 'attachment'), // Prefer notebook attachments
42
];
43
44
constructor(
45
private readonly _parser: IMdParser,
46
) { }
47
48
public async provideDocumentDropEdits(
49
document: vscode.TextDocument,
50
position: vscode.Position,
51
dataTransfer: vscode.DataTransfer,
52
token: vscode.CancellationToken,
53
): Promise<vscode.DocumentDropEdit | undefined> {
54
const edit = await this._createEdit(document, [new vscode.Range(position, position)], dataTransfer, {
55
insert: this._getEnabled(document, 'editor.drop.enabled'),
56
copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get<CopyFilesSettings>('editor.drop.copyIntoWorkspace', CopyFilesSettings.MediaFiles)
57
}, undefined, token);
58
59
if (!edit || token.isCancellationRequested) {
60
return;
61
}
62
63
const dropEdit = new vscode.DocumentDropEdit(edit.snippet);
64
dropEdit.title = edit.label;
65
dropEdit.kind = edit.kind;
66
dropEdit.additionalEdit = edit.additionalEdits;
67
dropEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];
68
return dropEdit;
69
}
70
71
public async provideDocumentPasteEdits(
72
document: vscode.TextDocument,
73
ranges: readonly vscode.Range[],
74
dataTransfer: vscode.DataTransfer,
75
context: vscode.DocumentPasteEditContext,
76
token: vscode.CancellationToken,
77
): Promise<vscode.DocumentPasteEdit[] | undefined> {
78
const edit = await this._createEdit(document, ranges, dataTransfer, {
79
insert: this._getEnabled(document, 'editor.paste.enabled'),
80
copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get<CopyFilesSettings>('editor.paste.copyIntoWorkspace', CopyFilesSettings.MediaFiles)
81
}, context, token);
82
83
if (!edit || token.isCancellationRequested) {
84
return;
85
}
86
87
const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, edit.kind);
88
pasteEdit.additionalEdit = edit.additionalEdits;
89
pasteEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];
90
return [pasteEdit];
91
}
92
93
private _getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink {
94
const setting = vscode.workspace.getConfiguration('markdown', document).get<boolean | InsertMarkdownLink>(settingName, true);
95
// Convert old boolean values to new enum setting
96
if (setting === false) {
97
return InsertMarkdownLink.Never;
98
} else if (setting === true) {
99
return InsertMarkdownLink.Smart;
100
} else {
101
return setting;
102
}
103
}
104
105
private async _createEdit(
106
document: vscode.TextDocument,
107
ranges: readonly vscode.Range[],
108
dataTransfer: vscode.DataTransfer,
109
settings: Readonly<{
110
insert: InsertMarkdownLink;
111
copyIntoWorkspace: CopyFilesSettings;
112
}>,
113
context: vscode.DocumentPasteEditContext | undefined,
114
token: vscode.CancellationToken,
115
): Promise<DropOrPasteEdit | undefined> {
116
if (settings.insert === InsertMarkdownLink.Never) {
117
return;
118
}
119
120
let edit = await this._createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token);
121
if (token.isCancellationRequested) {
122
return;
123
}
124
125
if (!edit) {
126
edit = await this._createEditFromUriListData(document, ranges, dataTransfer, context, token);
127
}
128
129
if (!edit || token.isCancellationRequested) {
130
return;
131
}
132
133
if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, settings.insert, ranges, token))) {
134
edit.yieldTo.push(vscode.DocumentDropOrPasteEditKind.Empty.append('uri'));
135
}
136
137
return edit;
138
}
139
140
private async _createEditFromUriListData(
141
document: vscode.TextDocument,
142
ranges: readonly vscode.Range[],
143
dataTransfer: vscode.DataTransfer,
144
context: vscode.DocumentPasteEditContext | undefined,
145
token: vscode.CancellationToken,
146
): Promise<DropOrPasteEdit | undefined> {
147
const uriListData = await dataTransfer.get(Mime.textUriList)?.asString();
148
if (!uriListData || token.isCancellationRequested) {
149
return;
150
}
151
152
const uriList = UriList.from(uriListData);
153
if (!uriList.entries.length) {
154
return;
155
}
156
157
// In some browsers, copying from the address bar sets both text/uri-list and text/plain.
158
// Disable ourselves if there's also a text entry with the same http(s) uri as our list,
159
// unless we are explicitly requested.
160
if (
161
uriList.entries.length === 1
162
&& (uriList.entries[0].uri.scheme === Schemes.http || uriList.entries[0].uri.scheme === Schemes.https)
163
&& !context?.only?.contains(baseLinkEditKind)
164
) {
165
const text = await dataTransfer.get(Mime.textPlain)?.asString();
166
if (token.isCancellationRequested) {
167
return;
168
}
169
170
if (text && textMatchesUriList(text, uriList)) {
171
return;
172
}
173
}
174
175
const edit = createInsertUriListEdit(document, ranges, uriList, { linkKindHint: context?.only });
176
if (!edit) {
177
return;
178
}
179
180
const additionalEdits = new vscode.WorkspaceEdit();
181
additionalEdits.set(document.uri, edit.edits);
182
183
return {
184
label: edit.label,
185
kind: edit.kind,
186
snippet: new vscode.SnippetString(''),
187
additionalEdits,
188
yieldTo: []
189
};
190
}
191
192
/**
193
* Create a new edit for media files in a data transfer.
194
*
195
* This tries copying files outside of the workspace into the workspace.
196
*/
197
private async _createEditForMediaFiles(
198
document: vscode.TextDocument,
199
dataTransfer: vscode.DataTransfer,
200
copyIntoWorkspace: CopyFilesSettings,
201
token: vscode.CancellationToken,
202
): Promise<DropOrPasteEdit | undefined> {
203
if (copyIntoWorkspace !== CopyFilesSettings.MediaFiles || getParentDocumentUri(document.uri).scheme === Schemes.untitled) {
204
return;
205
}
206
207
interface FileEntry {
208
readonly uri: vscode.Uri;
209
readonly kind: MediaKind;
210
readonly newFile?: { readonly contents: vscode.DataTransferFile; readonly overwrite: boolean };
211
}
212
213
const pathGenerator = new NewFilePathGenerator();
214
const fileEntries = coalesce(await Promise.all(Array.from(dataTransfer, async ([mime, item]): Promise<FileEntry | undefined> => {
215
const mediaKind = getMediaKindForMime(mime);
216
if (!mediaKind) {
217
return;
218
}
219
220
const file = item?.asFile();
221
if (!file) {
222
return;
223
}
224
225
if (file.uri) {
226
// If the file is already in a workspace, we don't want to create a copy of it
227
const workspaceFolder = vscode.workspace.getWorkspaceFolder(file.uri);
228
if (workspaceFolder) {
229
return { uri: file.uri, kind: mediaKind };
230
}
231
}
232
233
const newFile = await pathGenerator.getNewFilePath(document, file, token);
234
if (!newFile) {
235
return;
236
}
237
return { uri: newFile.uri, kind: mediaKind, newFile: { contents: file, overwrite: newFile.overwrite } };
238
})));
239
if (!fileEntries.length) {
240
return;
241
}
242
243
const snippet = createUriListSnippet(document.uri, fileEntries);
244
if (!snippet) {
245
return;
246
}
247
248
const additionalEdits = new vscode.WorkspaceEdit();
249
for (const entry of fileEntries) {
250
if (entry.newFile) {
251
additionalEdits.createFile(entry.uri, {
252
contents: entry.newFile.contents,
253
overwrite: entry.newFile.overwrite,
254
});
255
}
256
}
257
258
const { label, kind } = getSnippetLabelAndKind(snippet);
259
return {
260
snippet: snippet.snippet,
261
label,
262
kind,
263
additionalEdits,
264
yieldTo: [],
265
};
266
}
267
}
268
269
function textMatchesUriList(text: string, uriList: UriList): boolean {
270
if (text === uriList.entries[0].str) {
271
return true;
272
}
273
274
try {
275
const uri = vscode.Uri.parse(text);
276
return uriList.entries.some(entry => entry.uri.toString() === uri.toString());
277
} catch {
278
return false;
279
}
280
}
281
282
export function registerResourceDropOrPasteSupport(selector: vscode.DocumentSelector, parser: IMdParser): vscode.Disposable {
283
const providedEditKinds = [
284
baseLinkEditKind,
285
linkEditKind,
286
imageEditKind,
287
audioEditKind,
288
videoEditKind,
289
];
290
291
return vscode.Disposable.from(
292
vscode.languages.registerDocumentPasteEditProvider(selector, new ResourcePasteOrDropProvider(parser), {
293
providedPasteEditKinds: providedEditKinds,
294
pasteMimeTypes: ResourcePasteOrDropProvider.mimeTypes,
295
}),
296
vscode.languages.registerDocumentDropEditProvider(selector, new ResourcePasteOrDropProvider(parser), {
297
providedDropEditKinds: providedEditKinds,
298
dropMimeTypes: ResourcePasteOrDropProvider.mimeTypes,
299
}),
300
);
301
}
302
303