Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/dnd/browser/dnd.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 { DataTransfers } from '../../../base/browser/dnd.js';
7
import { mainWindow } from '../../../base/browser/window.js';
8
import { DragMouseEvent } from '../../../base/browser/mouseEvent.js';
9
import { coalesce } from '../../../base/common/arrays.js';
10
import { DeferredPromise } from '../../../base/common/async.js';
11
import { VSBuffer } from '../../../base/common/buffer.js';
12
import { ResourceMap } from '../../../base/common/map.js';
13
import { parse } from '../../../base/common/marshalling.js';
14
import { Schemas } from '../../../base/common/network.js';
15
import { isNative, isWeb } from '../../../base/common/platform.js';
16
import { URI, UriComponents } from '../../../base/common/uri.js';
17
import { localize } from '../../../nls.js';
18
import { IDialogService } from '../../dialogs/common/dialogs.js';
19
import { IBaseTextResourceEditorInput, ITextEditorSelection } from '../../editor/common/editor.js';
20
import { HTMLFileSystemProvider } from '../../files/browser/htmlFileSystemProvider.js';
21
import { WebFileSystemAccess } from '../../files/browser/webFileSystemAccess.js';
22
import { ByteSize, IFileService } from '../../files/common/files.js';
23
import { IInstantiationService, ServicesAccessor } from '../../instantiation/common/instantiation.js';
24
import { extractSelection } from '../../opener/common/opener.js';
25
import { Registry } from '../../registry/common/platform.js';
26
import { IMarker } from '../../markers/common/markers.js';
27
28
29
//#region Editor / Resources DND
30
31
export const CodeDataTransfers = {
32
EDITORS: 'CodeEditors',
33
FILES: 'CodeFiles',
34
SYMBOLS: 'application/vnd.code.symbols',
35
MARKERS: 'application/vnd.code.diagnostics',
36
NOTEBOOK_CELL_OUTPUT: 'notebook-cell-output',
37
SCM_HISTORY_ITEM: 'scm-history-item',
38
};
39
40
export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput {
41
resource: URI | undefined;
42
43
/**
44
* A hint that the source of the dragged editor input
45
* might not be the application but some external tool.
46
*/
47
isExternal?: boolean;
48
49
/**
50
* Whether we probe for the dropped editor to be a workspace
51
* (i.e. code-workspace file or even a folder), allowing to
52
* open it as workspace instead of opening as editor.
53
*/
54
allowWorkspaceOpen?: boolean;
55
}
56
57
export function extractEditorsDropData(e: DragEvent): Array<IDraggedResourceEditorInput> {
58
const editors: IDraggedResourceEditorInput[] = [];
59
if (e.dataTransfer && e.dataTransfer.types.length > 0) {
60
61
// Data Transfer: Code Editors
62
const rawEditorsData = e.dataTransfer.getData(CodeDataTransfers.EDITORS);
63
if (rawEditorsData) {
64
try {
65
editors.push(...parse(rawEditorsData));
66
} catch (error) {
67
// Invalid transfer
68
}
69
}
70
71
// Data Transfer: Resources
72
else {
73
try {
74
const rawResourcesData = e.dataTransfer.getData(DataTransfers.RESOURCES);
75
editors.push(...createDraggedEditorInputFromRawResourcesData(rawResourcesData));
76
} catch (error) {
77
// Invalid transfer
78
}
79
}
80
81
// Check for native file transfer
82
if (e.dataTransfer?.files) {
83
for (let i = 0; i < e.dataTransfer.files.length; i++) {
84
const file = e.dataTransfer.files[i];
85
if (file && getPathForFile(file)) {
86
try {
87
editors.push({ resource: URI.file(getPathForFile(file)!), isExternal: true, allowWorkspaceOpen: true });
88
} catch (error) {
89
// Invalid URI
90
}
91
}
92
}
93
}
94
95
// Check for CodeFiles transfer
96
const rawCodeFiles = e.dataTransfer.getData(CodeDataTransfers.FILES);
97
if (rawCodeFiles) {
98
try {
99
const codeFiles: string[] = JSON.parse(rawCodeFiles);
100
for (const codeFile of codeFiles) {
101
editors.push({ resource: URI.file(codeFile), isExternal: true, allowWorkspaceOpen: true });
102
}
103
} catch (error) {
104
// Invalid transfer
105
}
106
}
107
108
// Workbench contributions
109
const contributions = Registry.as<IDragAndDropContributionRegistry>(Extensions.DragAndDropContribution).getAll();
110
for (const contribution of contributions) {
111
const data = e.dataTransfer.getData(contribution.dataFormatKey);
112
if (data) {
113
try {
114
editors.push(...contribution.getEditorInputs(data));
115
} catch (error) {
116
// Invalid transfer
117
}
118
}
119
}
120
}
121
122
// Prevent duplicates: it is possible that we end up with the same
123
// dragged editor multiple times because multiple data transfers
124
// are being used (https://github.com/microsoft/vscode/issues/128925)
125
126
const coalescedEditors: IDraggedResourceEditorInput[] = [];
127
const seen = new ResourceMap<boolean>();
128
for (const editor of editors) {
129
if (!editor.resource) {
130
coalescedEditors.push(editor);
131
} else if (!seen.has(editor.resource)) {
132
coalescedEditors.push(editor);
133
seen.set(editor.resource, true);
134
}
135
}
136
137
return coalescedEditors;
138
}
139
140
export async function extractEditorsAndFilesDropData(accessor: ServicesAccessor, e: DragEvent): Promise<Array<IDraggedResourceEditorInput>> {
141
const editors = extractEditorsDropData(e);
142
143
// Web: Check for file transfer
144
if (e.dataTransfer && isWeb && containsDragType(e, DataTransfers.FILES)) {
145
const files = e.dataTransfer.items;
146
if (files) {
147
const instantiationService = accessor.get(IInstantiationService);
148
const filesData = await instantiationService.invokeFunction(accessor => extractFilesDropData(accessor, e));
149
for (const fileData of filesData) {
150
editors.push({ resource: fileData.resource, contents: fileData.contents?.toString(), isExternal: true, allowWorkspaceOpen: fileData.isDirectory });
151
}
152
}
153
}
154
155
return editors;
156
}
157
158
export function createDraggedEditorInputFromRawResourcesData(rawResourcesData: string | undefined): IDraggedResourceEditorInput[] {
159
const editors: IDraggedResourceEditorInput[] = [];
160
161
if (rawResourcesData) {
162
const resourcesRaw: string[] = JSON.parse(rawResourcesData);
163
for (const resourceRaw of resourcesRaw) {
164
if (resourceRaw.indexOf(':') > 0) { // mitigate https://github.com/microsoft/vscode/issues/124946
165
const { selection, uri } = extractSelection(URI.parse(resourceRaw));
166
editors.push({ resource: uri, options: { selection } });
167
}
168
}
169
}
170
171
return editors;
172
}
173
174
175
interface IFileTransferData {
176
resource: URI;
177
isDirectory?: boolean;
178
contents?: VSBuffer;
179
}
180
181
async function extractFilesDropData(accessor: ServicesAccessor, event: DragEvent): Promise<IFileTransferData[]> {
182
183
// Try to extract via `FileSystemHandle`
184
if (WebFileSystemAccess.supported(mainWindow)) {
185
const items = event.dataTransfer?.items;
186
if (items) {
187
return extractFileTransferData(accessor, items);
188
}
189
}
190
191
// Try to extract via `FileList`
192
const files = event.dataTransfer?.files;
193
if (!files) {
194
return [];
195
}
196
197
return extractFileListData(accessor, files);
198
}
199
200
async function extractFileTransferData(accessor: ServicesAccessor, items: DataTransferItemList): Promise<IFileTransferData[]> {
201
const fileSystemProvider = accessor.get(IFileService).getProvider(Schemas.file);
202
// eslint-disable-next-line no-restricted-syntax
203
if (!(fileSystemProvider instanceof HTMLFileSystemProvider)) {
204
return []; // only supported when running in web
205
}
206
207
const results: DeferredPromise<IFileTransferData | undefined>[] = [];
208
209
for (let i = 0; i < items.length; i++) {
210
const file = items[i];
211
if (file) {
212
const result = new DeferredPromise<IFileTransferData | undefined>();
213
results.push(result);
214
215
(async () => {
216
try {
217
const handle = await file.getAsFileSystemHandle();
218
if (!handle) {
219
result.complete(undefined);
220
return;
221
}
222
223
if (WebFileSystemAccess.isFileSystemFileHandle(handle)) {
224
result.complete({
225
resource: await fileSystemProvider.registerFileHandle(handle),
226
isDirectory: false
227
});
228
} else if (WebFileSystemAccess.isFileSystemDirectoryHandle(handle)) {
229
result.complete({
230
resource: await fileSystemProvider.registerDirectoryHandle(handle),
231
isDirectory: true
232
});
233
} else {
234
result.complete(undefined);
235
}
236
} catch (error) {
237
result.complete(undefined);
238
}
239
})();
240
}
241
}
242
243
return coalesce(await Promise.all(results.map(result => result.p)));
244
}
245
246
export async function extractFileListData(accessor: ServicesAccessor, files: FileList): Promise<IFileTransferData[]> {
247
const dialogService = accessor.get(IDialogService);
248
249
const results: DeferredPromise<IFileTransferData | undefined>[] = [];
250
251
for (let i = 0; i < files.length; i++) {
252
const file = files.item(i);
253
if (file) {
254
255
// Skip for very large files because this operation is unbuffered
256
if (file.size > 100 * ByteSize.MB) {
257
dialogService.warn(localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again."));
258
continue;
259
}
260
261
const result = new DeferredPromise<IFileTransferData | undefined>();
262
results.push(result);
263
264
const reader = new FileReader();
265
266
reader.onerror = () => result.complete(undefined);
267
reader.onabort = () => result.complete(undefined);
268
269
reader.onload = async event => {
270
const name = file.name;
271
const loadResult = event.target?.result ?? undefined;
272
if (typeof name !== 'string' || typeof loadResult === 'undefined') {
273
result.complete(undefined);
274
return;
275
}
276
277
result.complete({
278
resource: URI.from({ scheme: Schemas.untitled, path: name }),
279
contents: typeof loadResult === 'string' ? VSBuffer.fromString(loadResult) : VSBuffer.wrap(new Uint8Array(loadResult))
280
});
281
};
282
283
// Start reading
284
reader.readAsArrayBuffer(file);
285
}
286
}
287
288
return coalesce(await Promise.all(results.map(result => result.p)));
289
}
290
291
//#endregion
292
293
export function containsDragType(event: DragEvent, ...dragTypesToFind: string[]): boolean {
294
if (!event.dataTransfer) {
295
return false;
296
}
297
298
const dragTypes = event.dataTransfer.types;
299
const lowercaseDragTypes: string[] = [];
300
for (let i = 0; i < dragTypes.length; i++) {
301
lowercaseDragTypes.push(dragTypes[i].toLowerCase()); // somehow the types are lowercase
302
}
303
304
for (const dragType of dragTypesToFind) {
305
if (lowercaseDragTypes.indexOf(dragType.toLowerCase()) >= 0) {
306
return true;
307
}
308
}
309
310
return false;
311
}
312
313
//#region DND contributions
314
315
export interface IResourceStat {
316
readonly resource: URI;
317
readonly isDirectory?: boolean;
318
readonly selection?: ITextEditorSelection;
319
}
320
321
export interface IDragAndDropContributionRegistry {
322
/**
323
* Registers a drag and drop contribution.
324
*/
325
register(contribution: IDragAndDropContribution): void;
326
327
/**
328
* Returns all registered drag and drop contributions.
329
*/
330
getAll(): IterableIterator<IDragAndDropContribution>;
331
}
332
333
interface IDragAndDropContribution {
334
readonly dataFormatKey: string;
335
getEditorInputs(data: string): IDraggedResourceEditorInput[];
336
setData(resources: IResourceStat[], event: DragMouseEvent | DragEvent): void;
337
}
338
339
class DragAndDropContributionRegistry implements IDragAndDropContributionRegistry {
340
private readonly _contributions = new Map<string, IDragAndDropContribution>();
341
342
register(contribution: IDragAndDropContribution): void {
343
if (this._contributions.has(contribution.dataFormatKey)) {
344
throw new Error(`A drag and drop contributiont with key '${contribution.dataFormatKey}' was already registered.`);
345
}
346
this._contributions.set(contribution.dataFormatKey, contribution);
347
}
348
349
getAll(): IterableIterator<IDragAndDropContribution> {
350
return this._contributions.values();
351
}
352
}
353
354
export const Extensions = {
355
DragAndDropContribution: 'workbench.contributions.dragAndDrop'
356
};
357
358
Registry.add(Extensions.DragAndDropContribution, new DragAndDropContributionRegistry());
359
360
//#endregion
361
362
//#region DND Utilities
363
364
/**
365
* A singleton to store transfer data during drag & drop operations that are only valid within the application.
366
*/
367
export class LocalSelectionTransfer<T> {
368
369
private static readonly INSTANCE = new LocalSelectionTransfer();
370
371
private data?: T[];
372
private proto?: T;
373
374
private constructor() {
375
// protect against external instantiation
376
}
377
378
static getInstance<T>(): LocalSelectionTransfer<T> {
379
return LocalSelectionTransfer.INSTANCE as LocalSelectionTransfer<T>;
380
}
381
382
hasData(proto: T): boolean {
383
return proto && proto === this.proto;
384
}
385
386
clearData(proto: T): void {
387
if (this.hasData(proto)) {
388
this.proto = undefined;
389
this.data = undefined;
390
}
391
}
392
393
getData(proto: T): T[] | undefined {
394
if (this.hasData(proto)) {
395
return this.data;
396
}
397
398
return undefined;
399
}
400
401
setData(data: T[], proto: T): void {
402
if (proto) {
403
this.data = data;
404
this.proto = proto;
405
}
406
}
407
}
408
409
export interface DocumentSymbolTransferData {
410
name: string;
411
fsPath: string;
412
range: {
413
startLineNumber: number;
414
startColumn: number;
415
endLineNumber: number;
416
endColumn: number;
417
};
418
kind: number;
419
}
420
421
export interface NotebookCellOutputTransferData {
422
outputId: string;
423
}
424
425
function setDataAsJSON(e: DragEvent, kind: string, data: unknown) {
426
e.dataTransfer?.setData(kind, JSON.stringify(data));
427
}
428
429
function getDataAsJSON<T>(e: DragEvent, kind: string, defaultValue: T): T {
430
const rawSymbolsData = e.dataTransfer?.getData(kind);
431
if (rawSymbolsData) {
432
try {
433
return JSON.parse(rawSymbolsData);
434
} catch (error) {
435
// Invalid transfer
436
}
437
}
438
439
return defaultValue;
440
}
441
442
export function extractSymbolDropData(e: DragEvent): DocumentSymbolTransferData[] {
443
return getDataAsJSON(e, CodeDataTransfers.SYMBOLS, []);
444
}
445
446
export function fillInSymbolsDragData(symbolsData: readonly DocumentSymbolTransferData[], e: DragEvent): void {
447
setDataAsJSON(e, CodeDataTransfers.SYMBOLS, symbolsData);
448
}
449
450
export type MarkerTransferData = IMarker | { uri: UriComponents };
451
452
export function extractMarkerDropData(e: DragEvent): MarkerTransferData[] | undefined {
453
return getDataAsJSON(e, CodeDataTransfers.MARKERS, undefined);
454
}
455
456
export function fillInMarkersDragData(markerData: MarkerTransferData[], e: DragEvent): void {
457
setDataAsJSON(e, CodeDataTransfers.MARKERS, markerData);
458
}
459
460
export function extractNotebookCellOutputDropData(e: DragEvent): NotebookCellOutputTransferData | undefined {
461
return getDataAsJSON(e, CodeDataTransfers.NOTEBOOK_CELL_OUTPUT, undefined);
462
}
463
464
/**
465
* A helper to get access to Electrons `webUtils.getPathForFile` function
466
* in a safe way without crashing the application when running in the web.
467
*/
468
export function getPathForFile(file: File): string | undefined {
469
if (isNative && typeof (globalThis as any).vscode?.webUtils?.getPathForFile === 'function') {
470
return (globalThis as any).vscode.webUtils.getPathForFile(file);
471
}
472
473
return undefined;
474
}
475
476
//#endregion
477
478