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