explorerViewer.ts 53.0 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, BinarySize } 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';
J
Joao Moreno 已提交
18
import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, 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';
I
isidor 已提交
21
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
22
import { IFilesConfiguration, IExplorerService, VIEW_ID } from 'vs/workbench/contrib/files/common/files';
23
import { dirname, joinPath, isEqualOrParent, basename, distinctParents } 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';
32
import { compareFileExtensionsNumeric, compareFileNamesNumeric } 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';
I
isidor 已提交
37
import { DesktopDragAndDropData, 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 { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
41
import { IHostService } from 'vs/workbench/services/host/browser/host';
B
Benjamin Pasero 已提交
42
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
I
isidor 已提交
43 44 45 46
import { URI } from 'vs/base/common/uri';
import { ITask, sequence } from 'vs/base/common/async';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
47
import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions';
48
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
J
Joao Moreno 已提交
49
import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event';
J
Joao Moreno 已提交
50 51 52
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';
53
import { VSBuffer, newWriteableBufferStream } from 'vs/base/common/buffer';
54
import { ILabelService } from 'vs/platform/label/common/label';
J
Joao Moreno 已提交
55
import { isNumber } from 'vs/base/common/types';
56
import { domEvent } from 'vs/base/browser/event';
57
import { IEditableData } from 'vs/workbench/common/views';
58
import { IEditorInput } from 'vs/workbench/common/editor';
59
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
I
isidor 已提交
60 61 62

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	static ID = 0;
143 144

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

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

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

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

J
jeanp413 已提交
160 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
		for (let i = 0; i < this.labels.length; i++) {
J
jeanp413 已提交
168
			this.labels[i].setAttribute('aria-label', this.items[i].name);
169
			this.labels[i].setAttribute('aria-level', `${this.depth + i}`);
J
Joao Moreno 已提交
170
		}
171
		this.updateCollapsed(this.collapsed);
J
Joao Moreno 已提交
172

I
isidor 已提交
173 174 175
		if (this._index < this.labels.length) {
			DOM.addClass(this.labels[this._index], 'active');
		}
176 177 178 179 180 181 182
	}

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

J
Joao Moreno 已提交
183
		this.setIndex(this._index - 1);
184 185 186 187 188 189 190
	}

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

J
Joao Moreno 已提交
191
		this.setIndex(this._index + 1);
192
	}
193 194 195 196 197 198

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

J
Joao Moreno 已提交
199
		this.setIndex(0);
200 201 202 203 204 205 206
	}

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

J
Joao Moreno 已提交
207 208 209
		this.setIndex(this.items.length - 1);
	}

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

215
		DOM.removeClass(this.labels[this._index], 'active');
J
Joao Moreno 已提交
216
		this._index = index;
217
		DOM.addClass(this.labels[this._index], 'active');
J
Joao Moreno 已提交
218 219 220 221

		this._onDidChange.fire();
	}

222 223 224 225 226 227 228
	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 已提交
229 230
	dispose(): void {
		this._onDidChange.dispose();
J
jeanp413 已提交
231
		this._updateLabelDisposable.dispose();
232
	}
233 234
}

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

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

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

J
Joao Moreno 已提交
248 249 250
	private _onDidChangeActiveDescendant = new EventMultiplexer<void>();
	readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event;

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

268 269 270 271
	getWidgetAriaLabel(): string {
		return localize('treeAriaLabel', "Files Explorer");
	}

I
isidor 已提交
272 273
	get templateId(): string {
		return FilesRenderer.ID;
274
	}
275

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

280
		return { elementDisposable, label, container };
281 282
	}

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

J
Joao Moreno 已提交
288 289
		DOM.removeClass(templateData.label.element, 'compressed');

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

296 297 298
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
299
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
300
		}
301 302
	}

J
Joao Moreno 已提交
303
	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData, height: number | undefined): void {
J
Joao Moreno 已提交
304 305 306
		templateData.elementDisposable.dispose();

		const stat = node.element.elements[node.element.elements.length - 1];
I
isidor 已提交
307 308
		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 已提交
309 310 311

		// File Label
		if (!editableData) {
J
Joao Moreno 已提交
312
			DOM.addClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
313
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
314 315

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

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

321
			const compressedNavigationController = new CompressedNavigationController(id, node.element.elements, templateData, node.depth, node.collapsed);
J
Joao Moreno 已提交
322
			disposables.add(compressedNavigationController);
323
			this.compressedNavigationControllers.set(stat, compressedNavigationController);
324

J
Joao Moreno 已提交
325 326 327
			// accessibility
			disposables.add(this._onDidChangeActiveDescendant.add(compressedNavigationController.onDidChange));

328 329 330 331 332 333 334 335
			domEvent(templateData.container, 'mousedown')(e => {
				const result = getIconLabelNameFromHTMLElement(e.target);

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

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

J
Joao Moreno 已提交
338
			templateData.elementDisposable = disposables;
J
Joao Moreno 已提交
339
		}
J
Joao Moreno 已提交
340

J
Joao Moreno 已提交
341 342
		// Input Box
		else {
J
Joao Moreno 已提交
343
			DOM.removeClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
344
			templateData.label.element.style.display = 'none';
I
isidor 已提交
345
			templateData.elementDisposable = this.renderInputBox(templateData.container, editable[0], editableData);
J
Joao Moreno 已提交
346
		}
J
Joao Moreno 已提交
347 348
	}

J
Joao Moreno 已提交
349
	private renderStat(stat: ExplorerItem, label: string | string[], domId: string | undefined, filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
J
Joao Moreno 已提交
350 351 352 353 354 355 356 357 358 359
		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,
360
			matches: createMatches(filterData),
J
Joao Moreno 已提交
361 362
			separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
			domId
J
Joao Moreno 已提交
363 364 365 366 367 368 369 370 371 372 373
		});

		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
			}
		});
	}

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

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

I
isidor 已提交
382
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
383 384
		const value = stat.name || '';

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

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

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

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

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

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

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

J
jeanp413 已提交
426
		const showInputBoxNotification = () => {
427 428
			if (inputBox.isInputValid()) {
				const message = editableData.validationMessage(inputBox.value);
J
jeanp413 已提交
429 430 431 432 433 434 435 436 437 438 439 440 441
				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 已提交
442
		const toDispose = [
J
Joao Moreno 已提交
443
			inputBox,
J
jeanp413 已提交
444 445 446
			inputBox.onDidChange(value => {
				label.setFile(joinPath(parent, value || ' '), labelOptions); // update label icon while typing!
			}),
A
Cleanup  
Alex Dima 已提交
447
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
448
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
449
					if (inputBox.validate()) {
J
jeanp413 已提交
450
						done(true, true);
J
Joao Moreno 已提交
451
					}
A
Alexandru Dima 已提交
452
				} else if (e.equals(KeyCode.Escape)) {
J
jeanp413 已提交
453
					done(false, true);
J
Joao Moreno 已提交
454 455
				}
			}),
J
jeanp413 已提交
456 457 458
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_UP, (e: IKeyboardEvent) => {
				showInputBoxNotification();
			}),
459 460 461
			DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
				done(inputBox.isInputValid(), true);
			}),
B
Benjamin Pasero 已提交
462 463
			label,
			styler
J
Joao Moreno 已提交
464
		];
465

I
isidor 已提交
466
		return toDisposable(() => {
J
jeanp413 已提交
467
			done(false, false);
I
isidor 已提交
468
		});
E
Erich Gamma 已提交
469 470
	}

471
	disposeElement(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
472
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
473 474
	}

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

I
isidor 已提交
479 480 481
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
482
	}
I
isidor 已提交
483

484 485 486 487
	getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined {
		return this.compressedNavigationControllers.get(stat);
	}

J
Joao Moreno 已提交
488
	// IAccessibilityProvider
E
Erich Gamma 已提交
489

I
isidor 已提交
490 491
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
492
	}
J
Joao Moreno 已提交
493 494 495 496 497 498 499 500 501

	getActiveDescendantId(stat: ExplorerItem): string | undefined {
		const compressedNavigationController = this.compressedNavigationControllers.get(stat);
		return compressedNavigationController?.currentId;
	}

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

504 505 506 507 508
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

509 510 511 512
/**
 * 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.
 */
513
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
514
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
515 516 517 518
	private hiddenUris = new Set<URI>();
	private editorsAffectingFilter = new Set<IEditorInput>();
	private _onDidChange = new Emitter<void>();
	private toDispose: IDisposable[] = [];
E
Erich Gamma 已提交
519

I
isidor 已提交
520
	constructor(
I
isidor 已提交
521 522
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
523 524
		@IExplorerService private readonly explorerService: IExplorerService,
		@IEditorService private readonly editorService: IEditorService,
I
isidor 已提交
525
	) {
526
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561
		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;
			this.hiddenUris.forEach(u => {
				editors.forEach(e => {
					if (e.resource && isEqualOrParent(e.resource, u)) {
						// A filtered resource suddenly became visible since user opened an editor
						shouldFire = true;
					}
				});
			});

			this.editorsAffectingFilter.forEach(e => {
				if (editors.indexOf(e) === -1) {
					// Editor that was affecting filtering is no longer visible
					shouldFire = true;
				}
			});
			if (shouldFire) {
				this.editorsAffectingFilter.clear();
				this.hiddenUris.clear();
				this._onDidChange.fire();
			}
		}));
		this.updateConfiguration();
	}

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

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

570
			if (!shouldFire) {
571
				const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString());
572
				shouldFire = !cached || !equals(cached.original, excludesConfig);
573 574
			}

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

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

580 581 582 583 584
		if (shouldFire) {
			this.editorsAffectingFilter.clear();
			this.hiddenUris.clear();
			this._onDidChange.fire();
		}
E
Erich Gamma 已提交
585 586
	}

587
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
588 589 590 591 592 593 594 595 596 597 598
		const isVisible = this.isVisible(stat, parentVisibility);
		if (isVisible) {
			this.hiddenUris.delete(stat.resource);
		} else {
			this.hiddenUris.add(stat.resource);
		}

		return isVisible;
	}

	private isVisible(stat: ExplorerItem, parentVisibility: TreeVisibility): boolean {
599
		stat.isExcluded = false;
I
isidor 已提交
600
		if (parentVisibility === TreeVisibility.Hidden) {
601
			stat.isExcluded = true;
I
isidor 已提交
602 603
			return false;
		}
I
isidor 已提交
604
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
605 606 607 608
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
609
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
I
isidor 已提交
610
		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) {
611
			stat.isExcluded = true;
612
			const editors = this.editorService.visibleEditors;
I
isidor 已提交
613
			const editor = editors.find(e => e.resource && isEqualOrParent(e.resource, stat.resource));
614 615 616 617 618
			if (editor) {
				this.editorsAffectingFilter.add(editor);
				return true; // Show all opened files and their parents
			}

E
Erich Gamma 已提交
619 620 621 622 623
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
624

625 626
	dispose(): void {
		dispose(this.toDispose);
B
Benjamin Pasero 已提交
627
	}
E
Erich Gamma 已提交
628 629
}

630
// Explorer Sorter
I
isidor 已提交
631
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
632

I
isidor 已提交
633
	constructor(
I
isidor 已提交
634
		@IExplorerService private readonly explorerService: IExplorerService,
635
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
636
	) { }
I
isidor 已提交
637

638
	compare(statA: ExplorerItem, statB: ExplorerItem): number {
I
isidor 已提交
639 640 641
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
642 643 644
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
645
			}
I
isidor 已提交
646

I
isidor 已提交
647 648
			return -1;
		}
I
isidor 已提交
649

I
isidor 已提交
650 651 652
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
653

I
isidor 已提交
654
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
655

I
isidor 已提交
656 657 658 659 660 661
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
662

I
isidor 已提交
663 664 665
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
666

I
isidor 已提交
667
				if (statA.isDirectory && statB.isDirectory) {
668
					return compareFileNamesNumeric(statA.name, statB.name);
I
isidor 已提交
669
				}
I
isidor 已提交
670

I
isidor 已提交
671
				break;
I
isidor 已提交
672

I
isidor 已提交
673 674 675 676
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
677

I
isidor 已提交
678 679 680
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
681

I
isidor 已提交
682
				break;
I
isidor 已提交
683

I
isidor 已提交
684 685
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
686

I
isidor 已提交
687 688 689 690
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
691

I
isidor 已提交
692 693 694
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
695

I
isidor 已提交
696 697
				break;
		}
I
isidor 已提交
698

I
isidor 已提交
699 700 701
		// Sort Files
		switch (sortOrder) {
			case 'type':
702
				return compareFileExtensionsNumeric(statA.name, statB.name);
I
isidor 已提交
703

I
isidor 已提交
704 705
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
706
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
707
				}
I
isidor 已提交
708

709
				return compareFileNamesNumeric(statA.name, statB.name);
I
isidor 已提交
710

I
isidor 已提交
711
			default: /* 'default', 'mixed', 'filesFirst' */
712
				return compareFileNamesNumeric(statA.name, statB.name);
I
isidor 已提交
713 714 715
		}
	}
}
I
isidor 已提交
716

I
isidor 已提交
717 718
function getFileOverwriteConfirm(name: string): IConfirmation {
	return {
I
isidor 已提交
719 720 721 722 723
		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 已提交
724
}
I
isidor 已提交
725

I
isidor 已提交
726
function getMultipleFilesOverwriteConfirm(files: URI[]): IConfirmation {
727
	if (files.length > 1) {
I
isidor 已提交
728
		return {
729 730 731 732 733 734 735
			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 已提交
736 737
	return getFileOverwriteConfirm(basename(files[0]));
}
738

739 740 741 742 743 744 745 746 747
interface IWebkitDataTransfer {
	items: IWebkitDataTransferItem[];
}

interface IWebkitDataTransferItem {
	webkitGetAsEntry(): IWebkitDataTransferItemEntry;
}

interface IWebkitDataTransferItemEntry {
B
Benjamin Pasero 已提交
748
	name: string | undefined;
749 750 751 752 753 754 755 756 757 758 759
	isFile: boolean;
	isDirectory: boolean;

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

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

760 761 762 763 764 765 766 767
interface IUploadOperation {
	filesTotal: number;
	filesUploaded: number;

	startTime: number;
	bytesUploaded: number;
}

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

771 772 773
	private compressedDragOverElement: HTMLElement | undefined;
	private compressedDropTargetDisposable: IDisposable = Disposable.None;

I
isidor 已提交
774
	private toDispose: IDisposable[];
I
isidor 已提交
775
	private dropEnabled = false;
I
isidor 已提交
776 777 778 779 780 781 782 783 784 785

	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,
786
		@IWorkingCopyFileService private workingCopyFileService: IWorkingCopyFileService,
787
		@IHostService private hostService: IHostService,
788 789
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService,
		@IProgressService private readonly progressService: IProgressService
I
isidor 已提交
790 791 792 793 794 795 796 797 798 799
	) {
		this.toDispose = [];

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

I
isidor 已提交
800
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
801 802 803 804
		if (!this.dropEnabled) {
			return false;
		}

805 806 807 808 809 810 811 812
		// Compressed folders
		if (target) {
			const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent);

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

				if (iconLabelName && iconLabelName.index < iconLabelName.count - 1) {
813
					const result = this.handleDragOver(data, compressedTarget, targetIndex, originalEvent);
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836

					if (result) {
						if (iconLabelName.element !== this.compressedDragOverElement) {
							this.compressedDragOverElement = iconLabelName.element;
							this.compressedDropTargetDisposable.dispose();
							this.compressedDropTargetDisposable = toDisposable(() => {
								DOM.removeClass(iconLabelName.element, 'drop-target');
								this.compressedDragOverElement = undefined;
							});

							DOM.addClass(iconLabelName.element, 'drop-target');
						}

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

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

		this.compressedDropTargetDisposable.dispose();
837
		return this.handleDragOver(data, target, targetIndex, originalEvent);
838 839
	}

840
	private handleDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
841 842
		const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
		const fromDesktop = data instanceof DesktopDragAndDropData;
I
isidor 已提交
843
		const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
I
isidor 已提交
844 845

		// Desktop DND
846 847
		if (fromDesktop) {
			if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) {
I
isidor 已提交
848 849 850 851 852 853 854 855 856 857 858
				return false;
			}
		}

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

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

I
isidor 已提交
861
			if (!target) {
H
Howard Hung 已提交
862
				// Dropping onto the empty area. Do not accept if items dragged are already
B
Benjamin Pasero 已提交
863
				// children of the root unless we are copying the file
864
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
865 866 867
					return false;
				}

I
isidor 已提交
868
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
869 870
			}

I
isidor 已提交
871
			if (!Array.isArray(items)) {
I
isidor 已提交
872 873 874
				return false;
			}

I
isidor 已提交
875
			if (items.some((source) => {
I
isidor 已提交
876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892
				if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) {
					return true; // Root folder can not be moved to a non root file stat.
				}

				if (source.resource.toString() === target.resource.toString()) {
					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;
				}

				if (!isCopy && dirname(source.resource).toString() === target.resource.toString()) {
					return true; // Can not move a file to the same parent unless we copy
				}

893
				if (isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
894 895 896 897 898 899 900 901 902 903 904
					return true; // Can not move a parent folder into one of its children
				}

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

		// All (target = model)
		if (!target) {
I
isidor 已提交
905
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925
		}

		// 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 已提交
926
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
927
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
928 929 930
			return null;
		}

I
isidor 已提交
931 932 933
		return element.resource.toString();
	}

J
Joao Moreno 已提交
934 935 936 937
	getDragLabel(elements: ExplorerItem[], originalEvent: DragEvent): string | undefined {
		if (elements.length === 1) {
			const stat = FileDragAndDrop.getCompressedStatFromDragEvent(elements[0], originalEvent);
			return stat.name;
I
isidor 已提交
938 939
		}

J
Joao Moreno 已提交
940
		return String(elements.length);
I
isidor 已提交
941 942 943
	}

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

			// 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 已提交
951
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
952 953 954 955 956 957
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
958
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
959 960 961 962 963 964 965 966 967 968 969
		this.compressedDropTargetDisposable.dispose();

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

			if (compressedTarget) {
				target = compressedTarget;
			}
		}

970 971 972 973
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
974
		if (!target.isDirectory && target.parent) {
975 976 977 978 979 980
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
981 982
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
J
Joao Moreno 已提交
983 984 985 986 987
			if (isWeb) {
				this.handleWebExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
			} else {
				this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
			}
I
isidor 已提交
988 989 990
		}
		// In-Explorer DND (Move/Copy file)
		else {
991
			this.handleExplorerDrop(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
992 993 994
		}
	}

J
Joao Moreno 已提交
995
	private async handleWebExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
996 997 998 999 1000 1001 1002 1003 1004
		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 已提交
1005

1006
		const results: { isFile: boolean, resource: URI }[] = [];
1007
		const cts = new CancellationTokenSource();
1008
		const operation: IUploadOperation = { filesTotal: entries.length, filesUploaded: 0, startTime: Date.now(), bytesUploaded: 0 };
1009 1010 1011 1012 1013 1014 1015 1016 1017

		// Start upload and report progress globally
		const uploadPromise = this.progressService.withProgress({
			location: ProgressLocation.Window,
			delay: 800,
			cancellable: true,
			title: localize('uploadingFiles', "Uploading")
		}, async progress => {
			for (let entry of entries) {
1018 1019 1020 1021 1022 1023 1024 1025

				// Confirm overwrite as needed
				if (target && entry.name && target.getChild(entry.name)) {
					const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(entry.name));
					if (!confirmed) {
						continue;
					}

1026
					await this.workingCopyFileService.delete([joinPath(target.resource, entry.name)], { recursive: true });
1027 1028 1029
				}

				// Upload entry
1030
				const result = await this.doUploadWebFileEntry(entry, target.resource, target, progress, operation, cts.token);
1031 1032 1033
				if (result) {
					results.push(result);
				}
S
Steven Hermans 已提交
1034
			}
1035 1036 1037 1038 1039 1040 1041
		}, () => cts.dispose(true));

		// Also indicate progress in the files view
		this.progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => uploadPromise);

		// Wait until upload is done
		await uploadPromise;
1042 1043

		// Open uploaded file in editor only if we upload just one
1044
		if (!cts.token.isCancellationRequested && results.length === 1 && results[0].isFile) {
1045 1046
			await this.editorService.openEditor({ resource: results[0].resource, options: { pinned: true } });
		}
1047 1048
	}

1049
	private async doUploadWebFileEntry(entry: IWebkitDataTransferItemEntry, parentResource: URI, target: ExplorerItem | undefined, progress: IProgress<IProgressStep>, operation: IUploadOperation, token: CancellationToken): Promise<{ isFile: boolean, resource: URI } | undefined> {
1050
		if (token.isCancellationRequested || !entry.name || (!entry.isFile && !entry.isDirectory)) {
1051 1052 1053
			return undefined;
		}

1054
		// Report progress
1055
		let fileBytesUploaded = 0;
1056
		const reportProgress = (fileSize: number, bytesUploaded: number): void => {
1057 1058 1059 1060
			fileBytesUploaded += bytesUploaded;
			operation.bytesUploaded += bytesUploaded;

			const bytesUploadedPerSecond = operation.bytesUploaded / ((Date.now() - operation.startTime) / 1000);
1061 1062

			let message: string;
1063
			if (operation.filesTotal === 1 && entry.name) {
1064 1065
				message = entry.name;
			} else {
1066
				message = localize('uploadProgress', "{0} of {1} files ({2}/s)", operation.filesUploaded, operation.filesTotal, BinarySize.formatSize(bytesUploadedPerSecond));
1067 1068 1069
			}

			if (fileSize > BinarySize.MB) {
1070
				message = localize('uploadProgressDetail', "{0} ({1} of {2}, {3}/s)", message, BinarySize.formatSize(fileBytesUploaded), BinarySize.formatSize(fileSize), BinarySize.formatSize(bytesUploadedPerSecond));
1071 1072 1073 1074
			}

			progress.report({ message });
		};
1075
		operation.filesUploaded++;
1076
		reportProgress(0, 0);
1077

1078
		// Handle file upload
1079
		const resource = joinPath(parentResource, entry.name);
1080
		if (entry.isFile) {
S
Steven Hermans 已提交
1081
			const file = await new Promise<File>((resolve, reject) => entry.file(resolve, reject));
B
Benjamin Pasero 已提交
1082

1083 1084 1085
			if (token.isCancellationRequested) {
				return undefined;
			}
B
Benjamin Pasero 已提交
1086

1087 1088
			// Chrome/Edge/Firefox support stream method
			if (typeof file.stream === 'function') {
1089
				await this.doUploadWebFileEntryBuffered(resource, file, reportProgress, token);
1090 1091 1092 1093
			}

			// Fallback to unbuffered upload for other browsers
			else {
1094
				await this.doUploadWebFileEntryUnbuffered(resource, file, reportProgress);
1095
			}
1096 1097 1098 1099 1100 1101

			return { isFile: true, resource };
		}

		// Handle folder upload
		else {
1102 1103

			// Create target folder
1104 1105
			await this.fileService.createFolder(resource);

1106 1107 1108 1109
			if (token.isCancellationRequested) {
				return undefined;
			}

1110 1111
			// Recursive upload files in this directory
			const dirReader = entry.createReader();
1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
			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
				}
			} while (!done);
1122

1123
			// Update operation total based on new counts
1124
			operation.filesTotal += childEntries.length;
1125 1126

			// Upload all entries as files to target
1127
			const folderTarget = target && target.getChild(entry.name) || undefined;
S
Steven Hermans 已提交
1128
			for (let childEntry of childEntries) {
1129
				await this.doUploadWebFileEntry(childEntry, resource, folderTarget, progress, operation, token);
1130
			}
1131 1132

			return { isFile: false, resource };
1133
		}
J
Joao Moreno 已提交
1134
	}
I
isidor 已提交
1135

1136 1137 1138 1139 1140 1141 1142 1143
	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);
1144 1145

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

1149 1150
			let res = await reader.read();
			while (!res.done) {
1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165
				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);
1166

1167
				res = await reader.read();
1168
			}
1169 1170 1171 1172
			writeableStream.end(res.value instanceof Uint8Array ? VSBuffer.wrap(res.value) : undefined);
		} catch (error) {
			writeableStream.end(error);
		}
1173

1174 1175 1176 1177
		if (token.isCancellationRequested) {
			return undefined;
		}

1178 1179
		// Wait for file being written to target
		await writeFilePromise;
1180 1181
	}

1182
	private doUploadWebFileEntryUnbuffered(resource: URI, file: File, progressReporter: (fileSize: number, bytesUploaded: number) => void): Promise<void> {
1183 1184 1185 1186 1187
		return new Promise<void>((resolve, reject) => {
			const reader = new FileReader();
			reader.onload = async event => {
				try {
					if (event.target?.result instanceof ArrayBuffer) {
1188 1189 1190 1191 1192
						const buffer = VSBuffer.wrap(new Uint8Array(event.target.result));
						await this.fileService.writeFile(resource, buffer);

						// Report progress
						progressReporter(file.size, buffer.byteLength);
1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207
					} 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);
		});
	}

J
Joao Moreno 已提交
1208
	private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
1209

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

I
isidor 已提交
1214
		// Pass focus to window
1215
		this.hostService.focus();
I
isidor 已提交
1216

I
isidor 已提交
1217 1218 1219
		// 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 已提交
1220 1221 1222 1223
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
1224
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
1225 1226 1227 1228 1229 1230
			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 已提交
1231
			}
I
isidor 已提交
1232

1233
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
1234
			if (choice === buttons.length - 3) {
I
isidor 已提交
1235 1236
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
1237
			if (choice === buttons.length - 2) {
I
isidor 已提交
1238 1239 1240 1241
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
1242 1243 1244 1245 1246 1247
		}

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

I
isidor 已提交
1250
	private async addResources(target: ExplorerItem, resources: URI[]): Promise<void> {
I
isidor 已提交
1251 1252 1253
		if (resources && resources.length > 0) {

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

			// Check for name collisions
			const targetNames = new Set<string>();
1258
			const caseSensitive = this.fileService.hasCapability(target.resource, FileSystemProviderCapabilities.PathCaseSensitive);
I
isidor 已提交
1259 1260
			if (targetStat.children) {
				targetStat.children.forEach(child => {
1261
					targetNames.add(caseSensitive ? child.name : child.name.toLowerCase());
I
isidor 已提交
1262 1263
				});
			}
I
isidor 已提交
1264

I
isidor 已提交
1265 1266
			// Run add in sequence
			const addPromisesFactory: ITask<Promise<void>>[] = [];
I
isidor 已提交
1267
			await Promise.all(resources.map(async resource => {
1268
				if (targetNames.has(caseSensitive ? basename(resource) : basename(resource).toLowerCase())) {
I
isidor 已提交
1269 1270 1271 1272 1273 1274
					const confirmationResult = await this.dialogService.confirm(getFileOverwriteConfirm(basename(resource)));
					if (!confirmationResult.confirmed) {
						return;
					}
				}

I
isidor 已提交
1275 1276 1277 1278
				addPromisesFactory.push(async () => {
					const sourceFile = resource;
					const targetFile = joinPath(target.resource, basename(sourceFile));

1279
					const stat = (await this.workingCopyFileService.copy([{ source: sourceFile, target: targetFile }], { overwrite: true }))[0];
I
isidor 已提交
1280 1281 1282 1283
					// if we only add one file, just open it directly
					if (resources.length === 1 && !stat.isDirectory) {
						this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
					}
I
isidor 已提交
1284
				});
I
isidor 已提交
1285
			}));
I
isidor 已提交
1286

I
isidor 已提交
1287 1288
			await sequence(addPromisesFactory);
		}
I
isidor 已提交
1289 1290
	}

1291 1292
	private async handleExplorerDrop(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data);
I
isidor 已提交
1293
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
1294
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
1295 1296

		// Handle confirm setting
I
isidor 已提交
1297
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
1298
		if (confirmDragAndDrop) {
1299 1300 1301 1302 1303 1304
			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 已提交
1305
			const confirmation = await this.dialogService.confirm({
1306 1307
				message,
				detail,
I
isidor 已提交
1308 1309 1310 1311 1312 1313 1314
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});

I
isidor 已提交
1315 1316 1317
			if (!confirmation.confirmed) {
				return;
			}
I
isidor 已提交
1318 1319

			// Check for confirmation checkbox
I
isidor 已提交
1320 1321
			if (confirmation.checkboxChecked === true) {
				await this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
I
isidor 已提交
1322
			}
I
isidor 已提交
1323
		}
I
isidor 已提交
1324

I
isidor 已提交
1325 1326 1327 1328 1329 1330 1331 1332
		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 已提交
1333 1334
	}

1335
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
1336 1337 1338 1339 1340
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
1341
		let targetIndex: number | undefined;
I
isidor 已提交
1342 1343 1344 1345 1346
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
1347 1348
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
1349 1350
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
1351
				targetIndex = index;
I
isidor 已提交
1352 1353 1354 1355 1356 1357 1358 1359
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
1360
		if (targetIndex === undefined) {
I
isidor 已提交
1361 1362
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
1363 1364 1365 1366 1367

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

1368 1369 1370 1371
	private async doHandleExplorerDropOnCopy(sources: ExplorerItem[], target: ExplorerItem): Promise<void> {
		// Reuse duplicate action when user copies
		const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
		const sourceTargetPairs = sources.map(({ resource, isDirectory }) => ({ source: resource, target: findValidPasteFileTarget(this.explorerService, target, { resource, isDirectory, allowOverwrite: false }, incrementalNaming) }));
I
isidor 已提交
1372 1373 1374
		const stats = await this.workingCopyFileService.copy(sourceTargetPairs);
		const editors = stats.filter(stat => !stat.isDirectory).map(({ resource }) => ({ resource, options: { pinned: true } }));

1375 1376
		await this.editorService.openEditors(editors);
	}
I
isidor 已提交
1377

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

1380 1381
		// Do not allow moving readonly items
		const sourceTargetPairs = sources.filter(source => !source.isReadonly).map(source => ({ source: source.resource, target: joinPath(target.resource, source.name) }));
I
isidor 已提交
1382

I
isidor 已提交
1383
		try {
1384
			await this.workingCopyFileService.move(sourceTargetPairs);
I
isidor 已提交
1385
		} catch (error) {
I
isidor 已提交
1386 1387
			// Conflict
			if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
1388 1389 1390 1391 1392 1393 1394 1395 1396

				const overwrites: URI[] = [];
				for (const { target } of sourceTargetPairs) {
					if (await this.fileService.exists(target)) {
						overwrites.push(target);
					}
				}

				const confirm = getMultipleFilesOverwriteConfirm(overwrites);
I
isidor 已提交
1397
				// Move with overwrite if the user confirms
I
isidor 已提交
1398 1399 1400
				const { confirmed } = await this.dialogService.confirm(confirm);
				if (confirmed) {
					try {
1401
						await this.workingCopyFileService.move(sourceTargetPairs, { overwrite: true });
I
isidor 已提交
1402 1403
					} catch (error) {
						this.notificationService.error(error);
I
isidor 已提交
1404
					}
I
isidor 已提交
1405
				}
I
isidor 已提交
1406 1407 1408 1409 1410
			}
			// Any other error
			else {
				this.notificationService.error(error);
			}
I
isidor 已提交
1411
		}
I
isidor 已提交
1412
	}
J
Joao Moreno 已提交
1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427

	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;
	}

1428 1429
	private static getCompressedStatFromDragEvent(stat: ExplorerItem, dragEvent: DragEvent): ExplorerItem {
		const target = document.elementFromPoint(dragEvent.clientX, dragEvent.clientY);
J
Joao Moreno 已提交
1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445
		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;
	}
1446 1447 1448 1449

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

1452
function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement, count: number, index: number } | null {
J
Joao Moreno 已提交
1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472
	if (!(target instanceof HTMLElement)) {
		return null;
	}

	let element: HTMLElement | null = target;

	while (element && !DOM.hasClass(element, 'monaco-list-row')) {
		if (DOM.hasClass(element, 'label-name') && element.hasAttribute('data-icon-label-count')) {
			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 已提交
1473
}
J
Joao Moreno 已提交
1474

J
Joao Moreno 已提交
1475 1476 1477 1478
export function isCompressedFolderName(target: HTMLElement | EventTarget | Element | null): boolean {
	return !!getIconLabelNameFromHTMLElement(target);
}

J
Joao Moreno 已提交
1479 1480
export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

1481
	isIncompressible(stat: ExplorerItem): boolean {
J
Joao Moreno 已提交
1482
		return stat.isRoot || !stat.isDirectory || stat instanceof NewExplorerItem || (!stat.parent || stat.parent.isRoot);
J
Joao Moreno 已提交
1483 1484
	}
}