explorerViewer.ts 56.4 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

J
João Moreno 已提交
6
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
7 8
import * as DOM from 'vs/base/browser/dom';
import * as glob from 'vs/base/common/glob';
I
isidor 已提交
9
import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
10
import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress';
I
isidor 已提交
11
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
12
import { IFileService, FileKind, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, ByteSize } from 'vs/platform/files/common/files';
13
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
I
isidor 已提交
14
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
J
Joao Moreno 已提交
15
import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
16
import { KeyCode } from 'vs/base/common/keyCodes';
I
isidor 已提交
17
import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
18
import { ITreeNode, ITreeFilter, TreeVisibility, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree';
I
isidor 已提交
19
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
B
Benjamin Pasero 已提交
20
import { IThemeService } from 'vs/platform/theme/common/themeService';
21
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
22
import { IFilesConfiguration, IExplorerService, VIEW_ID } from 'vs/workbench/contrib/files/common/files';
I
isidor 已提交
23
import { dirname, joinPath, basename, distinctParents, basenameOrAuthority } from 'vs/base/common/resources';
I
isidor 已提交
24 25 26 27 28 29
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { localize } from 'vs/nls';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { once } from 'vs/base/common/functional';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { equals, deepClone } from 'vs/base/common/objects';
30
import * as path from 'vs/base/common/path';
J
Joao Moreno 已提交
31
import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
L
Leila Pearson 已提交
32
import { compareFileNamesDefault, compareFileExtensionsDefault } from 'vs/base/common/comparers';
33
import { fillResourceDataTransfers, CodeDataTransfers, extractResources, containsDragType } from 'vs/workbench/browser/dnd';
I
isidor 已提交
34 35 36
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd';
import { Schemas } from 'vs/base/common/network';
B
Benjamin Pasero 已提交
37
import { NativeDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
I
isidor 已提交
38
import { isMacintosh, isWeb } from 'vs/base/common/platform';
39
import { IDialogService, IConfirmation, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs';
40
import { IHostService } from 'vs/workbench/services/host/browser/host';
B
Benjamin Pasero 已提交
41
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
I
isidor 已提交
42
import { URI } from 'vs/base/common/uri';
43
import { ITask, RunOnceWorker, sequence } from 'vs/base/common/async';
I
isidor 已提交
44 45
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
46
import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions';
47
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
J
Joao Moreno 已提交
48
import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event';
J
Joao Moreno 已提交
49 50 51
import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree';
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
52
import { VSBuffer, newWriteableBufferStream } from 'vs/base/common/buffer';
53
import { ILabelService } from 'vs/platform/label/common/label';
J
Joao Moreno 已提交
54
import { isNumber } from 'vs/base/common/types';
55
import { domEvent } from 'vs/base/browser/event';
56
import { IEditableData } from 'vs/workbench/common/views';
57
import { IEditorInput } from 'vs/workbench/common/editor';
58
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
59
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
I
isidor 已提交
60
import { IBulkEditService, ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService';
I
isidor 已提交
61 62 63

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

I
isidor 已提交
64
	static readonly ITEM_HEIGHT = 22;
I
isidor 已提交
65 66 67 68 69 70 71 72 73 74

	getHeight(element: ExplorerItem): number {
		return ExplorerDelegate.ITEM_HEIGHT;
	}

	getTemplateId(element: ExplorerItem): string {
		return FilesRenderer.ID;
	}
}

I
isidor 已提交
75
export const explorerRootErrorEmitter = new Emitter<URI>();
76
export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | ExplorerItem[], ExplorerItem> {
E
Erich Gamma 已提交
77 78

	constructor(
79
		@IProgressService private readonly progressService: IProgressService,
80 81 82
		@INotificationService private readonly notificationService: INotificationService,
		@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
		@IFileService private readonly fileService: IFileService,
83 84
		@IExplorerService private readonly explorerService: IExplorerService,
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
85
	) { }
E
Erich Gamma 已提交
86

87 88
	hasChildren(element: ExplorerItem | ExplorerItem[]): boolean {
		return Array.isArray(element) || element.isDirectory;
E
Erich Gamma 已提交
89 90
	}

91 92 93
	getChildren(element: ExplorerItem | ExplorerItem[]): Promise<ExplorerItem[]> {
		if (Array.isArray(element)) {
			return Promise.resolve(element);
94
		}
E
Erich Gamma 已提交
95

96 97
		const sortOrder = this.explorerService.sortOrder;
		const promise = element.fetchChildren(sortOrder).then(undefined, e => {
98 99 100 101

			if (element instanceof ExplorerItem && element.isRoot) {
				if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
					// Single folder create a dummy explorer item to show error
102
					const placeholder = new ExplorerItem(element.resource, this.fileService, undefined, false);
103 104
					placeholder.isError = true;
					return [placeholder];
I
isidor 已提交
105 106
				} else {
					explorerRootErrorEmitter.fire(element.resource);
107 108 109
				}
			} else {
				// Do not show error for roots since we already use an explorer decoration to notify user
110 111
				this.notificationService.error(e);
			}
E
Erich Gamma 已提交
112

113 114
			return []; // we could not resolve any children because of an error
		});
E
Erich Gamma 已提交
115

116 117 118 119 120
		this.progressService.withProgress({
			location: ProgressLocation.Explorer,
			delay: this.layoutService.isRestored() ? 800 : 1200 // less ugly initial startup
		}, _progress => promise);

121
		return promise;
E
Erich Gamma 已提交
122
	}
I
isidor 已提交
123
}
E
Erich Gamma 已提交
124

125 126
export interface ICompressedNavigationController {
	readonly current: ExplorerItem;
J
Joao Moreno 已提交
127
	readonly currentId: string;
128
	readonly items: ExplorerItem[];
129
	readonly labels: HTMLElement[];
130 131
	readonly index: number;
	readonly count: number;
J
Joao Moreno 已提交
132
	readonly onDidChange: Event<void>;
133 134
	previous(): void;
	next(): void;
135 136
	first(): void;
	last(): void;
J
Joao Moreno 已提交
137
	setIndex(index: number): void;
138
	updateCollapsed(collapsed: boolean): void;
139 140
}

J
Joao Moreno 已提交
141 142 143
export class CompressedNavigationController implements ICompressedNavigationController, IDisposable {

	static ID = 0;
144 145

	private _index: number;
J
jeanp413 已提交
146 147
	private _labels!: HTMLElement[];
	private _updateLabelDisposable: IDisposable;
148 149 150 151

	get index(): number { return this._index; }
	get count(): number { return this.items.length; }
	get current(): ExplorerItem { return this.items[this._index]!; }
J
Joao Moreno 已提交
152
	get currentId(): string { return `${this.id}_${this.index}`; }
J
jeanp413 已提交
153
	get labels(): HTMLElement[] { return this._labels; }
J
Joao Moreno 已提交
154 155 156

	private _onDidChange = new Emitter<void>();
	readonly onDidChange = this._onDidChange.event;
157

158
	constructor(private id: string, readonly items: ExplorerItem[], templateData: IFileTemplateData, private depth: number, private collapsed: boolean) {
159
		this._index = items.length - 1;
J
Joao Moreno 已提交
160

J
jeanp413 已提交
161 162 163 164 165 166
		this.updateLabels(templateData);
		this._updateLabelDisposable = templateData.label.onDidRender(() => this.updateLabels(templateData));
	}

	private updateLabels(templateData: IFileTemplateData): void {
		this._labels = Array.from(templateData.container.querySelectorAll('.label-name')) as HTMLElement[];
I
isidor 已提交
167
		let parents = '';
I
isidor 已提交
168
		for (let i = 0; i < this.labels.length; i++) {
I
isidor 已提交
169 170
			const ariaLabel = parents.length ? `${this.items[i].name}, compact, ${parents}` : this.items[i].name;
			this.labels[i].setAttribute('aria-label', ariaLabel);
171
			this.labels[i].setAttribute('aria-level', `${this.depth + i}`);
I
isidor 已提交
172
			parents = parents.length ? `${this.items[i].name} ${parents}` : this.items[i].name;
J
Joao Moreno 已提交
173
		}
174
		this.updateCollapsed(this.collapsed);
J
Joao Moreno 已提交
175

I
isidor 已提交
176
		if (this._index < this.labels.length) {
177
			this.labels[this._index].classList.add('active');
I
isidor 已提交
178
		}
179 180 181 182 183 184 185
	}

	previous(): void {
		if (this._index <= 0) {
			return;
		}

J
Joao Moreno 已提交
186
		this.setIndex(this._index - 1);
187 188 189 190 191 192 193
	}

	next(): void {
		if (this._index >= this.items.length - 1) {
			return;
		}

J
Joao Moreno 已提交
194
		this.setIndex(this._index + 1);
195
	}
196 197 198 199 200 201

	first(): void {
		if (this._index === 0) {
			return;
		}

J
Joao Moreno 已提交
202
		this.setIndex(0);
203 204 205 206 207 208 209
	}

	last(): void {
		if (this._index === this.items.length - 1) {
			return;
		}

J
Joao Moreno 已提交
210 211 212
		this.setIndex(this.items.length - 1);
	}

J
Joao Moreno 已提交
213 214 215 216 217
	setIndex(index: number): void {
		if (index < 0 || index >= this.items.length) {
			return;
		}

218
		this.labels[this._index].classList.remove('active');
J
Joao Moreno 已提交
219
		this._index = index;
220
		this.labels[this._index].classList.add('active');
J
Joao Moreno 已提交
221 222 223 224

		this._onDidChange.fire();
	}

225 226 227 228 229 230 231
	updateCollapsed(collapsed: boolean): void {
		this.collapsed = collapsed;
		for (let i = 0; i < this.labels.length; i++) {
			this.labels[i].setAttribute('aria-expanded', collapsed ? 'false' : 'true');
		}
	}

J
Joao Moreno 已提交
232 233
	dispose(): void {
		this._onDidChange.dispose();
J
jeanp413 已提交
234
		this._updateLabelDisposable.dispose();
235
	}
236 237
}

I
isidor 已提交
238 239
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
240
	label: IResourceLabel;
I
isidor 已提交
241
	container: HTMLElement;
E
Erich Gamma 已提交
242 243
}

J
João Moreno 已提交
244
export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, FuzzyScore, IFileTemplateData>, IListAccessibilityProvider<ExplorerItem>, IDisposable {
I
isidor 已提交
245
	static readonly ID = 'file';
246

247 248
	private config: IFilesConfiguration;
	private configListener: IDisposable;
249
	private compressedNavigationControllers = new Map<ExplorerItem, CompressedNavigationController>();
E
Erich Gamma 已提交
250

J
Joao Moreno 已提交
251 252 253
	private _onDidChangeActiveDescendant = new EventMultiplexer<void>();
	readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event;

E
Erich Gamma 已提交
254
	constructor(
B
Benjamin Pasero 已提交
255
		private labels: ResourceLabels,
J
Joao Moreno 已提交
256
		private updateWidth: (stat: ExplorerItem) => void,
257 258 259
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
260
		@IExplorerService private readonly explorerService: IExplorerService,
261 262
		@ILabelService private readonly labelService: ILabelService,
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
E
Erich Gamma 已提交
263
	) {
264
		this.config = this.configurationService.getValue<IFilesConfiguration>();
265 266
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
267
				this.config = this.configurationService.getValue();
268 269 270 271
			}
		});
	}

272 273 274 275
	getWidgetAriaLabel(): string {
		return localize('treeAriaLabel', "Files Explorer");
	}

I
isidor 已提交
276 277
	get templateId(): string {
		return FilesRenderer.ID;
278
	}
279

I
isidor 已提交
280
	renderTemplate(container: HTMLElement): IFileTemplateData {
J
Joao Moreno 已提交
281
		const elementDisposable = Disposable.None;
282
		const label = this.labels.create(container, { supportHighlights: true });
E
Erich Gamma 已提交
283

284
		return { elementDisposable, label, container };
285 286
	}

287
	renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
288
		templateData.elementDisposable.dispose();
289
		const stat = node.element;
290
		const editableData = this.explorerService.getEditableData(stat);
B
Benjamin Pasero 已提交
291

292
		templateData.label.element.classList.remove('compressed');
J
Joao Moreno 已提交
293

294 295
		// File Label
		if (!editableData) {
296
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
297
			templateData.elementDisposable = this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
298
		}
299

300 301 302
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
303
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
304
		}
305 306
	}

J
Joao Moreno 已提交
307
	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData, height: number | undefined): void {
J
Joao Moreno 已提交
308 309 310
		templateData.elementDisposable.dispose();

		const stat = node.element.elements[node.element.elements.length - 1];
I
isidor 已提交
311 312
		const editable = node.element.elements.filter(e => this.explorerService.isEditable(e));
		const editableData = editable.length === 0 ? undefined : this.explorerService.getEditableData(editable[0]);
J
Joao Moreno 已提交
313 314 315

		// File Label
		if (!editableData) {
316
			templateData.label.element.classList.add('compressed');
J
Joao Moreno 已提交
317
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
318 319

			const disposables = new DisposableStore();
J
Joao Moreno 已提交
320 321
			const id = `compressed-explorer_${CompressedNavigationController.ID++}`;

I
isidor 已提交
322
			const label = node.element.elements.map(e => e.name);
J
Joao Moreno 已提交
323
			disposables.add(this.renderStat(stat, label, id, node.filterData, templateData));
J
Joao Moreno 已提交
324

325
			const compressedNavigationController = new CompressedNavigationController(id, node.element.elements, templateData, node.depth, node.collapsed);
J
Joao Moreno 已提交
326
			disposables.add(compressedNavigationController);
327
			this.compressedNavigationControllers.set(stat, compressedNavigationController);
328

J
Joao Moreno 已提交
329 330 331
			// accessibility
			disposables.add(this._onDidChangeActiveDescendant.add(compressedNavigationController.onDidChange));

332 333 334 335 336 337 338 339
			domEvent(templateData.container, 'mousedown')(e => {
				const result = getIconLabelNameFromHTMLElement(e.target);

				if (result) {
					compressedNavigationController.setIndex(result.index);
				}
			}, undefined, disposables);

J
Joao Moreno 已提交
340
			disposables.add(toDisposable(() => this.compressedNavigationControllers.delete(stat)));
341

J
Joao Moreno 已提交
342
			templateData.elementDisposable = disposables;
J
Joao Moreno 已提交
343
		}
J
Joao Moreno 已提交
344

J
Joao Moreno 已提交
345 346
		// Input Box
		else {
347
			templateData.label.element.classList.remove('compressed');
J
Joao Moreno 已提交
348
			templateData.label.element.style.display = 'none';
I
isidor 已提交
349
			templateData.elementDisposable = this.renderInputBox(templateData.container, editable[0], editableData);
J
Joao Moreno 已提交
350
		}
J
Joao Moreno 已提交
351 352
	}

J
Joao Moreno 已提交
353
	private renderStat(stat: ExplorerItem, label: string | string[], domId: string | undefined, filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
J
Joao Moreno 已提交
354 355 356 357 358 359 360 361 362 363
		templateData.label.element.style.display = 'flex';
		const extraClasses = ['explorer-item'];
		if (this.explorerService.isCut(stat)) {
			extraClasses.push('cut');
		}

		templateData.label.setResource({ resource: stat.resource, name: label }, {
			fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
			extraClasses,
			fileDecorations: this.config.explorer.decorations,
364
			matches: createMatches(filterData),
J
Joao Moreno 已提交
365 366
			separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
			domId
J
Joao Moreno 已提交
367 368 369 370 371 372 373 374 375 376 377
		});

		return templateData.label.onDidRender(() => {
			try {
				this.updateWidth(stat);
			} catch (e) {
				// noop since the element might no longer be in the tree, no update of width necessery
			}
		});
	}

378
	private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable {
B
Benjamin Pasero 已提交
379

380
		// Use a file label only for the icon next to the input box
B
Benjamin Pasero 已提交
381
		const label = this.labels.create(container);
382
		const extraClasses = ['explorer-item', 'explorer-item-edited'];
I
isidor 已提交
383
		const fileKind = stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE;
384
		const labelOptions: IFileLabelOptions = { hidePath: true, hideLabel: true, fileKind, extraClasses };
385

I
isidor 已提交
386
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
387 388
		const value = stat.name || '';

I
isidor 已提交
389
		label.setFile(joinPath(parent, value || ' '), labelOptions); // Use icon for ' ' if name is empty.
390

J
Joao Moreno 已提交
391 392 393
		// hack: hide label
		(label.element.firstElementChild as HTMLElement).style.display = 'none';

394
		// Input field for name
395
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
396
			validationOptions: {
397
				validation: (value) => {
398 399
					const message = editableData.validationMessage(value);
					if (!message || message.severity !== Severity.Error) {
400 401 402 403
						return null;
					}

					return {
404
						content: message.content,
405 406 407 408
						formatContent: true,
						type: MessageType.ERROR
					};
				}
409
			},
I
isidor 已提交
410
			ariaLabel: localize('fileInputAriaLabel', "Type file name. Press Enter to confirm or Escape to cancel.")
J
Joao Moreno 已提交
411
		});
B
Benjamin Pasero 已提交
412
		const styler = attachInputBoxStyler(inputBox, this.themeService);
E
Erich Gamma 已提交
413

414
		const lastDot = value.lastIndexOf('.');
E
Erich Gamma 已提交
415

J
Joao Moreno 已提交
416
		inputBox.value = value;
I
isidor 已提交
417 418
		inputBox.focus();
		inputBox.select({ start: 0, end: lastDot > 0 && !stat.isDirectory ? lastDot : value.length });
E
Erich Gamma 已提交
419

J
jeanp413 已提交
420
		const done = once((success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
421
			label.element.style.display = 'none';
422
			const value = inputBox.value;
I
isidor 已提交
423
			dispose(toDispose);
O
orange4glace 已提交
424
			label.element.remove();
J
jeanp413 已提交
425 426 427
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
J
Joao Moreno 已提交
428
		});
E
Erich Gamma 已提交
429

J
jeanp413 已提交
430
		const showInputBoxNotification = () => {
431 432
			if (inputBox.isInputValid()) {
				const message = editableData.validationMessage(inputBox.value);
J
jeanp413 已提交
433 434 435 436 437 438 439 440 441 442 443 444 445
				if (message) {
					inputBox.showMessage({
						content: message.content,
						formatContent: true,
						type: message.severity === Severity.Info ? MessageType.INFO : message.severity === Severity.Warning ? MessageType.WARNING : MessageType.ERROR
					});
				} else {
					inputBox.hideMessage();
				}
			}
		};
		showInputBoxNotification();

B
Benjamin Pasero 已提交
446
		const toDispose = [
J
Joao Moreno 已提交
447
			inputBox,
J
jeanp413 已提交
448 449 450
			inputBox.onDidChange(value => {
				label.setFile(joinPath(parent, value || ' '), labelOptions); // update label icon while typing!
			}),
A
Cleanup  
Alex Dima 已提交
451
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
452
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
453
					if (inputBox.validate()) {
J
jeanp413 已提交
454
						done(true, true);
J
Joao Moreno 已提交
455
					}
A
Alexandru Dima 已提交
456
				} else if (e.equals(KeyCode.Escape)) {
J
jeanp413 已提交
457
					done(false, true);
J
Joao Moreno 已提交
458 459
				}
			}),
J
jeanp413 已提交
460 461 462
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_UP, (e: IKeyboardEvent) => {
				showInputBoxNotification();
			}),
463 464 465
			DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
				done(inputBox.isInputValid(), true);
			}),
B
Benjamin Pasero 已提交
466 467
			label,
			styler
J
Joao Moreno 已提交
468
		];
469

I
isidor 已提交
470
		return toDisposable(() => {
J
jeanp413 已提交
471
			done(false, false);
I
isidor 已提交
472
		});
E
Erich Gamma 已提交
473 474
	}

475
	disposeElement(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
476
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
477 478
	}

479
	disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
480
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
481 482
	}

I
isidor 已提交
483 484 485
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
486
	}
I
isidor 已提交
487

488 489 490 491
	getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined {
		return this.compressedNavigationControllers.get(stat);
	}

J
Joao Moreno 已提交
492
	// IAccessibilityProvider
E
Erich Gamma 已提交
493

I
isidor 已提交
494 495
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
496
	}
J
Joao Moreno 已提交
497

498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
	getAriaLevel(element: ExplorerItem): number {
		// We need to comput aria level on our own since children of compact folders will otherwise have an incorrect level	#107235
		let depth = 0;
		let parent = element.parent;
		while (parent) {
			parent = parent.parent;
			depth++;
		}

		if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
			depth = depth + 1;
		}

		return depth;
	}

J
Joao Moreno 已提交
514 515 516 517 518 519 520 521
	getActiveDescendantId(stat: ExplorerItem): string | undefined {
		const compressedNavigationController = this.compressedNavigationControllers.get(stat);
		return compressedNavigationController?.currentId;
	}

	dispose(): void {
		this.configListener.dispose();
	}
E
Erich Gamma 已提交
522 523
}

524 525 526 527 528
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

529 530 531 532
/**
 * Respectes files.exclude setting in filtering out content from the explorer.
 * Makes sure that visible editors are always shown in the explorer even if they are filtered out by settings.
 */
533
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
534
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
535
	private uriVisibilityMap = new Map<URI, boolean>();
536 537 538
	private editorsAffectingFilter = new Set<IEditorInput>();
	private _onDidChange = new Emitter<void>();
	private toDispose: IDisposable[] = [];
E
Erich Gamma 已提交
539

I
isidor 已提交
540
	constructor(
I
isidor 已提交
541 542
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
543 544
		@IExplorerService private readonly explorerService: IExplorerService,
		@IEditorService private readonly editorService: IEditorService,
545
		@IUriIdentityService private readonly uriIdentityService: IUriIdentityService
I
isidor 已提交
546
	) {
547
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
548 549 550 551 552 553 554 555 556
		this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration()));
		this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => {
			if (e.affectsConfiguration('files.exclude')) {
				this.updateConfiguration();
			}
		}));
		this.toDispose.push(this.editorService.onDidVisibleEditorsChange(() => {
			const editors = this.editorService.visibleEditors;
			let shouldFire = false;
557 558 559 560 561 562 563 564 565
			this.uriVisibilityMap.forEach((visible, uri) => {
				if (!visible) {
					editors.forEach(e => {
						if (e.resource && this.uriIdentityService.extUri.isEqualOrParent(e.resource, uri)) {
							// A filtered resource suddenly became visible since user opened an editor
							shouldFire = true;
						}
					});
				}
566 567 568
			});

			this.editorsAffectingFilter.forEach(e => {
569
				if (!editors.includes(e)) {
570 571 572 573 574 575
					// Editor that was affecting filtering is no longer visible
					shouldFire = true;
				}
			});
			if (shouldFire) {
				this.editorsAffectingFilter.clear();
576
				this.uriVisibilityMap.clear();
577 578 579 580 581 582 583 584
				this._onDidChange.fire();
			}
		}));
		this.updateConfiguration();
	}

	get onDidChange(): Event<void> {
		return this._onDidChange.event;
E
Erich Gamma 已提交
585 586
	}

587 588
	private updateConfiguration(): void {
		let shouldFire = false;
S
Sandeep Somavarapu 已提交
589
		this.contextService.getWorkspace().folders.forEach(folder => {
590
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
B
Benjamin Pasero 已提交
591
			const excludesConfig: glob.IExpression = configuration?.files?.exclude || Object.create(null);
592

593
			if (!shouldFire) {
594
				const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString());
595
				shouldFire = !cached || !equals(cached.original, excludesConfig);
596 597
			}

I
isidor 已提交
598
			const excludesConfigCopy = deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods
599

600
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
I
isidor 已提交
601
		});
E
Erich Gamma 已提交
602

603 604
		if (shouldFire) {
			this.editorsAffectingFilter.clear();
605
			this.uriVisibilityMap.clear();
606 607
			this._onDidChange.fire();
		}
E
Erich Gamma 已提交
608 609
	}

610 611 612 613
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): boolean {
		const cachedVisibility = this.uriVisibilityMap.get(stat.resource);
		if (typeof cachedVisibility === 'boolean') {
			return cachedVisibility;
614 615
		}

616 617 618
		const isVisible = this.isVisible(stat, parentVisibility);
		this.uriVisibilityMap.set(stat.resource, isVisible);

619 620 621 622
		return isVisible;
	}

	private isVisible(stat: ExplorerItem, parentVisibility: TreeVisibility): boolean {
623
		stat.isExcluded = false;
I
isidor 已提交
624
		if (parentVisibility === TreeVisibility.Hidden) {
625
			stat.isExcluded = true;
I
isidor 已提交
626 627
			return false;
		}
I
isidor 已提交
628
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
629 630 631 632
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
633
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
I
isidor 已提交
634
		if ((cached && cached.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) || stat.parent?.isExcluded) {
635
			stat.isExcluded = true;
636
			const editors = this.editorService.visibleEditors;
637
			const editor = editors.find(e => e.resource && this.uriIdentityService.extUri.isEqualOrParent(e.resource, stat.resource));
638 639 640 641 642
			if (editor) {
				this.editorsAffectingFilter.add(editor);
				return true; // Show all opened files and their parents
			}

E
Erich Gamma 已提交
643 644 645 646 647
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
648

649 650
	dispose(): void {
		dispose(this.toDispose);
B
Benjamin Pasero 已提交
651
	}
E
Erich Gamma 已提交
652 653
}

654
// Explorer Sorter
I
isidor 已提交
655
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
656

I
isidor 已提交
657
	constructor(
I
isidor 已提交
658
		@IExplorerService private readonly explorerService: IExplorerService,
659
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
660
	) { }
I
isidor 已提交
661

662
	compare(statA: ExplorerItem, statB: ExplorerItem): number {
I
isidor 已提交
663 664 665
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
666 667 668
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
669
			}
I
isidor 已提交
670

I
isidor 已提交
671 672
			return -1;
		}
I
isidor 已提交
673

I
isidor 已提交
674 675 676
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
677

I
isidor 已提交
678
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
679

I
isidor 已提交
680 681 682 683 684 685
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
686

I
isidor 已提交
687 688 689
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
690

I
isidor 已提交
691
				if (statA.isDirectory && statB.isDirectory) {
L
Leila Pearson 已提交
692
					return compareFileNamesDefault(statA.name, statB.name);
I
isidor 已提交
693
				}
I
isidor 已提交
694

I
isidor 已提交
695
				break;
I
isidor 已提交
696

I
isidor 已提交
697 698 699 700
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
701

I
isidor 已提交
702 703 704
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
705

I
isidor 已提交
706
				break;
I
isidor 已提交
707

I
isidor 已提交
708 709
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
710

I
isidor 已提交
711 712 713 714
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
715

I
isidor 已提交
716 717 718
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
719

I
isidor 已提交
720 721
				break;
		}
I
isidor 已提交
722

I
isidor 已提交
723 724 725
		// Sort Files
		switch (sortOrder) {
			case 'type':
L
Leila Pearson 已提交
726
				return compareFileExtensionsDefault(statA.name, statB.name);
I
isidor 已提交
727

I
isidor 已提交
728 729
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
730
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
731
				}
I
isidor 已提交
732

L
Leila Pearson 已提交
733
				return compareFileNamesDefault(statA.name, statB.name);
I
isidor 已提交
734

I
isidor 已提交
735
			default: /* 'default', 'mixed', 'filesFirst' */
L
Leila Pearson 已提交
736
				return compareFileNamesDefault(statA.name, statB.name);
I
isidor 已提交
737 738 739
		}
	}
}
I
isidor 已提交
740

I
isidor 已提交
741 742
function getFileOverwriteConfirm(name: string): IConfirmation {
	return {
I
isidor 已提交
743 744 745 746 747
		message: localize('confirmOverwrite', "A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", name),
		detail: localize('irreversible', "This action is irreversible!"),
		primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
		type: 'warning'
	};
I
isidor 已提交
748
}
I
isidor 已提交
749

I
isidor 已提交
750
function getMultipleFilesOverwriteConfirm(files: URI[]): IConfirmation {
751
	if (files.length > 1) {
I
isidor 已提交
752
		return {
753 754 755 756 757 758 759
			message: localize('confirmManyOverwrites', "The following {0} files and/or folders already exist in the destination folder. Do you want to replace them?", files.length),
			detail: getFileNamesMessage(files) + '\n' + localize('irreversible', "This action is irreversible!"),
			primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
			type: 'warning'
		};
	}

I
isidor 已提交
760 761
	return getFileOverwriteConfirm(basename(files[0]));
}
762

763 764 765 766 767 768 769 770 771
interface IWebkitDataTransfer {
	items: IWebkitDataTransferItem[];
}

interface IWebkitDataTransferItem {
	webkitGetAsEntry(): IWebkitDataTransferItemEntry;
}

interface IWebkitDataTransferItemEntry {
B
Benjamin Pasero 已提交
772
	name: string | undefined;
773 774 775 776 777 778 779 780 781 782 783
	isFile: boolean;
	isDirectory: boolean;

	file(resolve: (file: File) => void, reject: () => void): void;
	createReader(): IWebkitDataTransferItemEntryReader;
}

interface IWebkitDataTransferItemEntryReader {
	readEntries(resolve: (file: IWebkitDataTransferItemEntry[]) => void, reject: () => void): void
}

784
interface IUploadOperation {
785 786 787
	startTime: number;
	progressScheduler: RunOnceWorker<IProgressStep>;

788 789 790
	filesTotal: number;
	filesUploaded: number;

791
	totalBytesUploaded: number;
792 793
}

I
isidor 已提交
794 795 796
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

797 798 799
	private compressedDragOverElement: HTMLElement | undefined;
	private compressedDropTargetDisposable: IDisposable = Disposable.None;

I
isidor 已提交
800
	private toDispose: IDisposable[];
I
isidor 已提交
801
	private dropEnabled = false;
I
isidor 已提交
802 803 804 805 806 807 808 809 810 811

	constructor(
		@INotificationService private notificationService: INotificationService,
		@IExplorerService private explorerService: IExplorerService,
		@IEditorService private editorService: IEditorService,
		@IDialogService private dialogService: IDialogService,
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IFileService private fileService: IFileService,
		@IConfigurationService private configurationService: IConfigurationService,
		@IInstantiationService private instantiationService: IInstantiationService,
812
		@IHostService private hostService: IHostService,
813
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService,
814
		@IProgressService private readonly progressService: IProgressService,
I
isidor 已提交
815 816
		@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
		@IBulkEditService private readonly bulkEditService: IBulkEditService
I
isidor 已提交
817 818 819 820 821 822 823 824 825 826
	) {
		this.toDispose = [];

		const updateDropEnablement = () => {
			this.dropEnabled = this.configurationService.getValue('explorer.enableDragAndDrop');
		};
		updateDropEnablement();
		this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => updateDropEnablement()));
	}

I
isidor 已提交
827
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
828 829 830 831
		if (!this.dropEnabled) {
			return false;
		}

832 833 834 835 836 837 838 839
		// Compressed folders
		if (target) {
			const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent);

			if (compressedTarget) {
				const iconLabelName = getIconLabelNameFromHTMLElement(originalEvent.target);

				if (iconLabelName && iconLabelName.index < iconLabelName.count - 1) {
840
					const result = this.handleDragOver(data, compressedTarget, targetIndex, originalEvent);
841 842 843 844 845 846

					if (result) {
						if (iconLabelName.element !== this.compressedDragOverElement) {
							this.compressedDragOverElement = iconLabelName.element;
							this.compressedDropTargetDisposable.dispose();
							this.compressedDropTargetDisposable = toDisposable(() => {
847
								iconLabelName.element.classList.remove('drop-target');
848 849 850
								this.compressedDragOverElement = undefined;
							});

851
							iconLabelName.element.classList.add('drop-target');
852 853 854 855 856 857 858 859 860 861 862 863
						}

						return typeof result === 'boolean' ? result : { ...result, feedback: [] };
					}

					this.compressedDropTargetDisposable.dispose();
					return false;
				}
			}
		}

		this.compressedDropTargetDisposable.dispose();
864
		return this.handleDragOver(data, target, targetIndex, originalEvent);
865 866
	}

867
	private handleDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
868
		const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
B
Benjamin Pasero 已提交
869 870
		const isNative = data instanceof NativeDragAndDropData;
		const effect = (isNative || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
I
isidor 已提交
871

B
Benjamin Pasero 已提交
872 873
		// Native DND
		if (isNative) {
874
			if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) {
I
isidor 已提交
875 876 877 878 879 880 881 882 883 884 885
				return false;
			}
		}

		// Other-Tree DND
		else if (data instanceof ExternalElementsDragAndDropData) {
			return false;
		}

		// In-Explorer DND
		else {
J
Joao Moreno 已提交
886
			const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>);
I
isidor 已提交
887

I
isidor 已提交
888
			if (!target) {
H
Howard Hung 已提交
889
				// Dropping onto the empty area. Do not accept if items dragged are already
B
Benjamin Pasero 已提交
890
				// children of the root unless we are copying the file
891
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
892 893 894
					return false;
				}

I
isidor 已提交
895
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
896 897
			}

I
isidor 已提交
898
			if (!Array.isArray(items)) {
I
isidor 已提交
899 900 901
				return false;
			}

I
isidor 已提交
902
			if (items.some((source) => {
I
isidor 已提交
903 904 905 906
				if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) {
					return true; // Root folder can not be moved to a non root file stat.
				}

907
				if (this.uriIdentityService.extUri.isEqual(source.resource, target.resource)) {
I
isidor 已提交
908 909 910 911 912 913 914 915
					return true; // Can not move anything onto itself
				}

				if (source.isRoot && target instanceof ExplorerItem && target.isRoot) {
					// Disable moving workspace roots in one another
					return false;
				}

916
				if (!isCopy && this.uriIdentityService.extUri.isEqual(dirname(source.resource), target.resource)) {
I
isidor 已提交
917 918 919
					return true; // Can not move a file to the same parent unless we copy
				}

920
				if (this.uriIdentityService.extUri.isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
921 922 923 924 925 926 927 928 929 930 931
					return true; // Can not move a parent folder into one of its children
				}

				return false;
			})) {
				return false;
			}
		}

		// All (target = model)
		if (!target) {
I
isidor 已提交
932
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952
		}

		// All (target = file/folder)
		else {
			if (target.isDirectory) {
				if (target.isReadonly) {
					return false;
				}

				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: true };
			}

			if (this.contextService.getWorkspace().folders.every(folder => folder.uri.toString() !== target.resource.toString())) {
				return { accept: true, bubble: TreeDragOverBubble.Up, effect };
			}
		}

		return false;
	}

J
Joao Moreno 已提交
953
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
954
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
955 956 957
			return null;
		}

I
isidor 已提交
958 959 960
		return element.resource.toString();
	}

J
Joao Moreno 已提交
961 962 963 964
	getDragLabel(elements: ExplorerItem[], originalEvent: DragEvent): string | undefined {
		if (elements.length === 1) {
			const stat = FileDragAndDrop.getCompressedStatFromDragEvent(elements[0], originalEvent);
			return stat.name;
I
isidor 已提交
965 966
		}

J
Joao Moreno 已提交
967
		return String(elements.length);
I
isidor 已提交
968 969 970
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
J
Joao Moreno 已提交
971
		const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, originalEvent);
I
isidor 已提交
972
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
973
			// Apply some datatransfer types to allow for dragging the element outside of the application
974
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, undefined, originalEvent);
I
isidor 已提交
975 976 977

			// The only custom data transfer we set from the explorer is a file transfer
			// to be able to DND between multiple code file explorers across windows
I
isidor 已提交
978
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
979 980 981 982 983 984
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
985
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
986 987 988 989 990 991 992 993 994 995 996
		this.compressedDropTargetDisposable.dispose();

		// Find compressed target
		if (target) {
			const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent);

			if (compressedTarget) {
				target = compressedTarget;
			}
		}

997 998 999 1000
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
1001
		if (!target.isDirectory && target.parent) {
1002 1003 1004 1005 1006
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}
1007 1008 1009 1010
		const resolvedTarget = target;
		if (!resolvedTarget) {
			return;
		}
1011

I
isidor 已提交
1012
		// Desktop DND (Import file)
B
Benjamin Pasero 已提交
1013
		if (data instanceof NativeDragAndDropData) {
1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
			const cts = new CancellationTokenSource();

			// Indicate progress globally
			const dropPromise = this.progressService.withProgress({
				location: ProgressLocation.Window,
				delay: 800,
				cancellable: true,
				title: isWeb ? localize('uploadingFiles', "Uploading") : localize('copyingFiles', "Copying")
			}, async progress => {
				try {
					if (isWeb) {
						await this.handleWebExternalDrop(data, resolvedTarget, originalEvent, progress, cts.token);
					} else {
						await this.handleExternalDrop(data, resolvedTarget, originalEvent, progress, cts.token);
					}
				} catch (error) {
					this.notificationService.warn(error);
				}
			}, () => cts.dispose(true));

			// Also indicate progress in the files view
			this.progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => dropPromise);
I
isidor 已提交
1036 1037 1038
		}
		// In-Explorer DND (Move/Copy file)
		else {
1039
			this.handleExplorerDrop(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, resolvedTarget, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
1040 1041 1042
		}
	}

1043
	private async handleWebExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
1044 1045 1046 1047 1048 1049 1050 1051 1052
		const items = (originalEvent.dataTransfer as unknown as IWebkitDataTransfer).items;

		// Somehow the items thing is being modified at random, maybe as a security
		// measure since this is a DND operation. As such, we copy the items into
		// an array we own as early as possible before using it.
		const entries: IWebkitDataTransferItemEntry[] = [];
		for (const item of items) {
			entries.push(item.webkitGetAsEntry());
		}
S
Steven Hermans 已提交
1053

1054
		const results: { isFile: boolean, resource: URI }[] = [];
1055 1056 1057 1058 1059 1060 1061 1062 1063
		const operation: IUploadOperation = {
			startTime: Date.now(),
			progressScheduler: new RunOnceWorker<IProgressStep>(steps => { progress.report(steps[steps.length - 1]); }, 1000),

			filesTotal: entries.length,
			filesUploaded: 0,

			totalBytesUploaded: 0
		};
1064

1065 1066 1067 1068
		for (let entry of entries) {
			if (token.isCancellationRequested) {
				break;
			}
1069

1070 1071 1072 1073 1074
			// Confirm overwrite as needed
			if (target && entry.name && target.getChild(entry.name)) {
				const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name));
				if (!confirmed) {
					continue;
1075 1076
				}

I
isidor 已提交
1077 1078 1079 1080
				await this.bulkEditService.apply([new ResourceFileEdit(joinPath(target.resource, entry.name), undefined, { recursive: true })], {
					undoRedoSource: this.explorerService.undoRedoSource,
					label: localize('overwrite', "Overwrite {0}", entry.name)
				});
1081 1082 1083

				if (token.isCancellationRequested) {
					break;
1084
				}
S
Steven Hermans 已提交
1085
			}
1086

1087 1088 1089 1090 1091 1092
			// Upload entry
			const result = await this.doUploadWebFileEntry(entry, target.resource, target, progress, operation, token);
			if (result) {
				results.push(result);
			}
		}
1093

1094 1095
		operation.progressScheduler.dispose();

1096
		// Open uploaded file in editor only if we upload just one
1097 1098 1099
		const firstUploadedFile = results[0];
		if (!token.isCancellationRequested && firstUploadedFile?.isFile) {
			await this.editorService.openEditor({ resource: firstUploadedFile.resource, options: { pinned: true } });
1100
		}
1101 1102
	}

1103
	private async doUploadWebFileEntry(entry: IWebkitDataTransferItemEntry, parentResource: URI, target: ExplorerItem | undefined, progress: IProgress<IProgressStep>, operation: IUploadOperation, token: CancellationToken): Promise<{ isFile: boolean, resource: URI } | undefined> {
1104
		if (token.isCancellationRequested || !entry.name || (!entry.isFile && !entry.isDirectory)) {
1105 1106 1107
			return undefined;
		}

1108
		// Report progress
1109
		let fileBytesUploaded = 0;
1110
		const reportProgress = (fileSize: number, bytesUploaded: number): void => {
1111
			fileBytesUploaded += bytesUploaded;
1112
			operation.totalBytesUploaded += bytesUploaded;
1113

1114
			const bytesUploadedPerSecond = operation.totalBytesUploaded / ((Date.now() - operation.startTime) / 1000);
1115

1116
			// Small file
1117
			let message: string;
1118
			if (fileSize < ByteSize.MB) {
1119 1120 1121
				if (operation.filesTotal === 1) {
					message = `${entry.name}`;
				} else {
1122
					message = localize('uploadProgressSmallMany', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, ByteSize.formatSize(bytesUploadedPerSecond));
1123
				}
1124 1125
			}

1126 1127
			// Large file
			else {
1128
				message = localize('uploadProgressLarge', "{0} ({1} of {2}, {3}/s)", entry.name, ByteSize.formatSize(fileBytesUploaded), ByteSize.formatSize(fileSize), ByteSize.formatSize(bytesUploadedPerSecond));
1129 1130
			}

1131 1132
			// Report progress but limit to update only once per second
			operation.progressScheduler.work({ message });
1133
		};
1134
		operation.filesUploaded++;
1135
		reportProgress(0, 0);
1136

1137
		// Handle file upload
1138
		const resource = joinPath(parentResource, entry.name);
1139
		if (entry.isFile) {
S
Steven Hermans 已提交
1140
			const file = await new Promise<File>((resolve, reject) => entry.file(resolve, reject));
B
Benjamin Pasero 已提交
1141

1142 1143 1144
			if (token.isCancellationRequested) {
				return undefined;
			}
B
Benjamin Pasero 已提交
1145

1146 1147 1148
			// Chrome/Edge/Firefox support stream method, but only use it for
			// larger files to reduce the overhead of the streaming approach
			if (typeof file.stream === 'function' && file.size > ByteSize.MB) {
1149
				await this.doUploadWebFileEntryBuffered(resource, file, reportProgress, token);
1150 1151
			}

1152
			// Fallback to unbuffered upload for other browsers or small files
1153
			else {
1154
				await this.doUploadWebFileEntryUnbuffered(resource, file, reportProgress);
1155
			}
1156 1157 1158 1159 1160 1161

			return { isFile: true, resource };
		}

		// Handle folder upload
		else {
1162 1163

			// Create target folder
1164 1165
			await this.fileService.createFolder(resource);

1166 1167 1168 1169
			if (token.isCancellationRequested) {
				return undefined;
			}

1170 1171
			// Recursive upload files in this directory
			const dirReader = entry.createReader();
1172 1173 1174 1175 1176 1177 1178 1179 1180
			const childEntries: IWebkitDataTransferItemEntry[] = [];
			let done = false;
			do {
				const childEntriesChunk = await new Promise<IWebkitDataTransferItemEntry[]>((resolve, reject) => dirReader.readEntries(resolve, reject));
				if (childEntriesChunk.length > 0) {
					childEntries.push(...childEntriesChunk);
				} else {
					done = true; // an empty array is a signal that all entries have been read
				}
1181
			} while (!done && !token.isCancellationRequested);
1182

1183
			// Update operation total based on new counts
1184
			operation.filesTotal += childEntries.length;
1185 1186

			// Upload all entries as files to target
1187
			const folderTarget = target && target.getChild(entry.name) || undefined;
S
Steven Hermans 已提交
1188
			for (let childEntry of childEntries) {
1189
				await this.doUploadWebFileEntry(childEntry, resource, folderTarget, progress, operation, token);
1190
			}
1191 1192

			return { isFile: false, resource };
1193
		}
J
Joao Moreno 已提交
1194
	}
I
isidor 已提交
1195

1196 1197 1198 1199 1200 1201 1202 1203
	private async doUploadWebFileEntryBuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void, token: CancellationToken): Promise<void> {
		const writeableStream = newWriteableBufferStream({
			// Set a highWaterMark to prevent the stream
			// for file upload to produce large buffers
			// in-memory
			highWaterMark: 10
		});
		const writeFilePromise = this.fileService.writeFile(resource, writeableStream);
1204 1205

		// Read the file in chunks using File.stream() web APIs
1206 1207
		try {
			const reader: ReadableStreamDefaultReader<Uint8Array> = file.stream().getReader();
1208

1209 1210
			let res = await reader.read();
			while (!res.done) {
1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225
				if (token.isCancellationRequested) {
					return undefined;
				}

				// Write buffer into stream but make sure to wait
				// in case the highWaterMark is reached
				const buffer = VSBuffer.wrap(res.value);
				await writeableStream.write(buffer);

				if (token.isCancellationRequested) {
					return undefined;
				}

				// Report progress
				progressReporter(file.size, buffer.byteLength);
1226

1227
				res = await reader.read();
1228
			}
1229 1230 1231 1232
			writeableStream.end(res.value instanceof Uint8Array ? VSBuffer.wrap(res.value) : undefined);
		} catch (error) {
			writeableStream.end(error);
		}
1233

1234 1235 1236 1237
		if (token.isCancellationRequested) {
			return undefined;
		}

1238 1239
		// Wait for file being written to target
		await writeFilePromise;
1240 1241
	}

1242
	private doUploadWebFileEntryUnbuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void): Promise<void> {
1243 1244 1245 1246 1247
		return new Promise<void>((resolve, reject) => {
			const reader = new FileReader();
			reader.onload = async event => {
				try {
					if (event.target?.result instanceof ArrayBuffer) {
1248 1249 1250 1251 1252
						const buffer = VSBuffer.wrap(new Uint8Array(event.target.result));
						await this.fileService.writeFile(resource, buffer);

						// Report progress
						progressReporter(file.size, buffer.byteLength);
1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267
					} else {
						throw new Error('Could not read from dropped file.');
					}

					resolve();
				} catch (error) {
					reject(error);
				}
			};

			// Start reading the file to trigger `onload`
			reader.readAsArrayBuffer(file);
		});
	}

1268
	private async handleExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
1269

I
isidor 已提交
1270
		// Check for dropped external files to be folders
1271
		const droppedResources = extractResources(originalEvent, true);
1272
		const result = await this.fileService.resolveAll(droppedResources.map(droppedResource => ({ resource: droppedResource.resource })));
I
isidor 已提交
1273

1274 1275 1276 1277
		if (token.isCancellationRequested) {
			return;
		}

I
isidor 已提交
1278
		// Pass focus to window
1279
		this.hostService.focus();
I
isidor 已提交
1280

I
isidor 已提交
1281 1282 1283
		// Handle folders by adding to workspace if we are in workspace context
		const folders = result.filter(r => r.success && r.stat && r.stat.isDirectory).map(result => ({ uri: result.stat!.resource }));
		if (folders.length > 0) {
I
isidor 已提交
1284 1285 1286 1287
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
1288
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
1289 1290 1291 1292 1293 1294
			let message = folders.length > 1 ? localize('copyfolders', "Are you sure to want to copy folders?") : localize('copyfolder', "Are you sure to want to copy '{0}'?", basename(folders[0].uri));
			if (folders.some(f => workspaceFolderSchemas.indexOf(f.uri.scheme) >= 0)) {
				// We only allow to add a folder to the workspace if there is already a workspace folder with that scheme
				buttons.unshift(folders.length > 1 ? localize('addFolders', "&&Add Folders to Workspace") : localize('addFolder', "&&Add Folder to Workspace"));
				message = folders.length > 1 ? localize('dropFolders', "Do you want to copy the folders or add the folders to the workspace?")
					: localize('dropFolder', "Do you want to copy '{0}' or add '{0}' as a folder to the workspace?", basename(folders[0].uri));
I
isidor 已提交
1295
			}
I
isidor 已提交
1296

1297
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
1298
			if (choice === buttons.length - 3) {
I
isidor 已提交
1299 1300
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
1301
			if (choice === buttons.length - 2) {
1302
				return this.addResources(target, droppedResources.map(res => res.resource), progress, token);
I
isidor 已提交
1303 1304 1305
			}

			return undefined;
I
isidor 已提交
1306 1307 1308 1309
		}

		// Handle dropped files (only support FileStat as target)
		else if (target instanceof ExplorerItem) {
1310
			return this.addResources(target, droppedResources.map(res => res.resource), progress, token);
I
isidor 已提交
1311
		}
I
isidor 已提交
1312 1313
	}

1314
	private async addResources(target: ExplorerItem, resources: URI[], progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
I
isidor 已提交
1315 1316 1317
		if (resources && resources.length > 0) {

			// Resolve target to check for name collisions and ask user
I
isidor 已提交
1318 1319
			const targetStat = await this.fileService.resolve(target.resource);

1320 1321 1322 1323
			if (token.isCancellationRequested) {
				return;
			}

I
isidor 已提交
1324 1325
			// Check for name collisions
			const targetNames = new Set<string>();
1326
			const caseSensitive = this.fileService.hasCapability(target.resource, FileSystemProviderCapabilities.PathCaseSensitive);
I
isidor 已提交
1327 1328
			if (targetStat.children) {
				targetStat.children.forEach(child => {
1329
					targetNames.add(caseSensitive ? child.name : child.name.toLowerCase());
I
isidor 已提交
1330 1331
				});
			}
I
isidor 已提交
1332

I
isidor 已提交
1333 1334
			// Run add in sequence
			const addPromisesFactory: ITask<Promise<void>>[] = [];
I
isidor 已提交
1335
			await Promise.all(resources.map(async resource => {
1336
				if (targetNames.has(caseSensitive ? basename(resource) : basename(resource).toLowerCase())) {
I
isidor 已提交
1337 1338 1339 1340 1341 1342
					const confirmationResult = await this.dialogService.confirm(getFileOverwriteConfirm(basename(resource)));
					if (!confirmationResult.confirmed) {
						return;
					}
				}

I
isidor 已提交
1343
				addPromisesFactory.push(async () => {
1344 1345 1346 1347
					if (token.isCancellationRequested) {
						return;
					}

I
isidor 已提交
1348
					const sourceFile = resource;
1349 1350 1351 1352
					const sourceFileName = basename(sourceFile);
					const targetFile = joinPath(target.resource, sourceFileName);

					progress.report({ message: sourceFileName });
I
isidor 已提交
1353

I
isidor 已提交
1354 1355 1356 1357
					await this.bulkEditService.apply([new ResourceFileEdit(sourceFile, targetFile, { overwrite: true, copy: true })], {
						undoRedoSource: this.explorerService.undoRedoSource,
						label: localize('copyFile', "Copy {0}", sourceFileName)
					});
I
isidor 已提交
1358
					// if we only add one file, just open it directly
I
isidor 已提交
1359 1360 1361 1362

					const item = this.explorerService.findClosest(targetFile);
					if (resources.length === 1 && item && !item.isDirectory) {
						this.editorService.openEditor({ resource: item.resource, options: { pinned: true } });
I
isidor 已提交
1363
					}
I
isidor 已提交
1364
				});
I
isidor 已提交
1365
			}));
I
isidor 已提交
1366

I
isidor 已提交
1367 1368
			await sequence(addPromisesFactory);
		}
I
isidor 已提交
1369 1370
	}

1371 1372
	private async handleExplorerDrop(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data);
I
isidor 已提交
1373
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
1374
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
1375 1376

		// Handle confirm setting
I
isidor 已提交
1377
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
1378
		if (confirmDragAndDrop) {
1379 1380 1381 1382 1383 1384
			const message = items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?")
				: items.length > 1 ? localize('confirmMultiMove', "Are you sure you want to move the following {0} files into '{1}'?", items.length, target.name)
					: items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name)
						: localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name);
			const detail = items.length > 1 && !items.every(s => s.isRoot) ? getFileNamesMessage(items.map(i => i.resource)) : undefined;

I
isidor 已提交
1385
			const confirmation = await this.dialogService.confirm({
1386 1387
				message,
				detail,
I
isidor 已提交
1388 1389 1390 1391 1392 1393 1394
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});

I
isidor 已提交
1395 1396 1397
			if (!confirmation.confirmed) {
				return;
			}
I
isidor 已提交
1398 1399

			// Check for confirmation checkbox
I
isidor 已提交
1400
			if (confirmation.checkboxChecked === true) {
1401
				await this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false);
I
isidor 已提交
1402
			}
I
isidor 已提交
1403
		}
I
isidor 已提交
1404

I
isidor 已提交
1405 1406 1407 1408 1409 1410 1411 1412
		await this.doHandleRootDrop(items.filter(s => s.isRoot), target);

		const sources = items.filter(s => !s.isRoot);
		if (isCopy) {
			await this.doHandleExplorerDropOnCopy(sources, target);
		} else {
			return this.doHandleExplorerDropOnMove(sources, target);
		}
I
isidor 已提交
1413 1414
	}

1415
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
1416 1417 1418 1419 1420
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
1421
		let targetIndex: number | undefined;
I
isidor 已提交
1422 1423 1424 1425 1426
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
1427 1428
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
1429
			};
1430
			if (target instanceof ExplorerItem && this.uriIdentityService.extUri.isEqual(folders[index].uri, target.resource)) {
I
isidor 已提交
1431
				targetIndex = index;
I
isidor 已提交
1432 1433 1434 1435 1436 1437 1438 1439
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
1440
		if (targetIndex === undefined) {
I
isidor 已提交
1441 1442
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
1443 1444 1445 1446 1447

		workspaceCreationData.splice(targetIndex, 0, ...rootsToMove);
		return this.workspaceEditingService.updateFolders(0, workspaceCreationData.length, workspaceCreationData);
	}

1448 1449 1450
	private async doHandleExplorerDropOnCopy(sources: ExplorerItem[], target: ExplorerItem): Promise<void> {
		// Reuse duplicate action when user copies
		const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
I
isidor 已提交
1451 1452 1453 1454 1455 1456 1457 1458 1459 1460
		const resourceFileEdits = sources.map(({ resource, isDirectory }) => (new ResourceFileEdit(resource, findValidPasteFileTarget(this.explorerService, target, { resource, isDirectory, allowOverwrite: false }, incrementalNaming), { copy: true })));
		await this.bulkEditService.apply(resourceFileEdits, {
			undoRedoSource: this.explorerService.undoRedoSource,
			label: resourceFileEdits.length > 1 ? localize('copy', "Copy {0} files", resourceFileEdits.length) : localize('copyOneFile', "Copy {0}", basenameOrAuthority(resourceFileEdits[0].newResource!))
		});

		const editors = resourceFileEdits.filter(edit => {
			const item = edit.newResource ? this.explorerService.findClosest(edit.newResource) : undefined;
			return item && !item.isDirectory;
		}).map(edit => ({ resource: edit.newResource, options: { pinned: true } }));
I
isidor 已提交
1461

1462 1463
		await this.editorService.openEditors(editors);
	}
I
isidor 已提交
1464

1465
	private async doHandleExplorerDropOnMove(sources: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
1466

1467
		// Do not allow moving readonly items
I
isidor 已提交
1468 1469
		const resourceFileEdits = sources.filter(source => !source.isReadonly).map(source => new ResourceFileEdit(source.resource, joinPath(target.resource, source.name)));
		const label = sources.length > 1 ? localize('move', "Move {0} files", sources.length) : localize('moveOneFile', "Move {0}", sources[0].name);
I
isidor 已提交
1470

I
isidor 已提交
1471
		try {
I
isidor 已提交
1472
			await this.bulkEditService.apply(resourceFileEdits, { undoRedoSource: this.explorerService.undoRedoSource, label });
I
isidor 已提交
1473
		} catch (error) {
I
isidor 已提交
1474 1475
			// Conflict
			if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
1476 1477

				const overwrites: URI[] = [];
I
isidor 已提交
1478 1479 1480
				for (const edit of resourceFileEdits) {
					if (edit.newResource && await this.fileService.exists(edit.newResource)) {
						overwrites.push(edit.newResource);
1481 1482 1483 1484
					}
				}

				const confirm = getMultipleFilesOverwriteConfirm(overwrites);
I
isidor 已提交
1485
				// Move with overwrite if the user confirms
I
isidor 已提交
1486 1487 1488
				const { confirmed } = await this.dialogService.confirm(confirm);
				if (confirmed) {
					try {
I
isidor 已提交
1489 1490 1491 1492
						await this.bulkEditService.apply(resourceFileEdits.map(re => new ResourceFileEdit(re.oldResource, re.newResource, { overwrite: true })), {
							undoRedoSource: this.explorerService.undoRedoSource,
							label
						});
I
isidor 已提交
1493 1494
					} catch (error) {
						this.notificationService.error(error);
I
isidor 已提交
1495
					}
I
isidor 已提交
1496
				}
I
isidor 已提交
1497 1498 1499 1500 1501
			}
			// Any other error
			else {
				this.notificationService.error(error);
			}
I
isidor 已提交
1502
		}
I
isidor 已提交
1503
	}
J
Joao Moreno 已提交
1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518

	private static getStatsFromDragAndDropData(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, dragStartEvent?: DragEvent): ExplorerItem[] {
		if (data.context) {
			return data.context;
		}

		// Detect compressed folder dragging
		if (dragStartEvent && data.elements.length === 1) {
			data.context = [FileDragAndDrop.getCompressedStatFromDragEvent(data.elements[0], dragStartEvent)];
			return data.context;
		}

		return data.elements;
	}

1519 1520
	private static getCompressedStatFromDragEvent(stat: ExplorerItem, dragEvent: DragEvent): ExplorerItem {
		const target = document.elementFromPoint(dragEvent.clientX, dragEvent.clientY);
J
Joao Moreno 已提交
1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536
		const iconLabelName = getIconLabelNameFromHTMLElement(target);

		if (iconLabelName) {
			const { count, index } = iconLabelName;

			let i = count - 1;
			while (i > index && stat.parent) {
				stat = stat.parent;
				i--;
			}

			return stat;
		}

		return stat;
	}
1537 1538 1539 1540

	onDragEnd(): void {
		this.compressedDropTargetDisposable.dispose();
	}
J
Joao Moreno 已提交
1541 1542
}

1543
function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement, count: number, index: number } | null {
J
Joao Moreno 已提交
1544 1545 1546 1547 1548 1549
	if (!(target instanceof HTMLElement)) {
		return null;
	}

	let element: HTMLElement | null = target;

I
isidor 已提交
1550 1551
	while (element && !element.classList.contains('monaco-list-row')) {
		if (element.classList.contains('label-name') && element.hasAttribute('data-icon-label-count')) {
J
Joao Moreno 已提交
1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563
			const count = Number(element.getAttribute('data-icon-label-count'));
			const index = Number(element.getAttribute('data-icon-label-index'));

			if (isNumber(count) && isNumber(index)) {
				return { element: element, count, index };
			}
		}

		element = element.parentElement;
	}

	return null;
I
isidor 已提交
1564
}
J
Joao Moreno 已提交
1565

J
Joao Moreno 已提交
1566 1567 1568 1569
export function isCompressedFolderName(target: HTMLElement | EventTarget | Element | null): boolean {
	return !!getIconLabelNameFromHTMLElement(target);
}

J
Joao Moreno 已提交
1570 1571
export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

1572
	isIncompressible(stat: ExplorerItem): boolean {
J
Joao Moreno 已提交
1573
		return stat.isRoot || !stat.isDirectory || stat instanceof NewExplorerItem || (!stat.parent || stat.parent.isRoot);
J
Joao Moreno 已提交
1574 1575
	}
}