explorerViewer.ts 41.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.
 *--------------------------------------------------------------------------------------------*/

I
isidor 已提交
6
import { IAccessibilityProvider } 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 } from 'vs/platform/progress/common/progress';
I
isidor 已提交
11
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
12
import { IFileService, FileKind, FileOperationError, FileOperationResult } 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 } 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';
I
isidor 已提交
32
import { compareFileExtensions, compareFileNames } 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';
I
isidor 已提交
39 40
import { IDialogService, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
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';
I
isidor 已提交
53
import { VSBuffer } 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';
I
isidor 已提交
58 59 60

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

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

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

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

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

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

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

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

93
		const promise = element.fetchChildren(this.fileService, this.explorerService).then(undefined, e => {
94 95 96 97

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

109 110
			return []; // we could not resolve any children because of an error
		});
E
Erich Gamma 已提交
111

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

117
		return promise;
E
Erich Gamma 已提交
118
	}
I
isidor 已提交
119
}
E
Erich Gamma 已提交
120

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

J
Joao Moreno 已提交
136 137 138
export class CompressedNavigationController implements ICompressedNavigationController, IDisposable {

	static ID = 0;
139 140

	private _index: number;
J
jeanp413 已提交
141 142
	private _labels!: HTMLElement[];
	private _updateLabelDisposable: IDisposable;
143 144 145 146

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

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

J
Joao Moreno 已提交
153
	constructor(private id: string, readonly items: ExplorerItem[], templateData: IFileTemplateData) {
154
		this._index = items.length - 1;
J
Joao Moreno 已提交
155

J
jeanp413 已提交
156 157 158 159 160 161 162
		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 已提交
163
		for (let i = 0; i < this.labels.length; i++) {
J
jeanp413 已提交
164
			this.labels[i].setAttribute('aria-label', this.items[i].name);
J
Joao Moreno 已提交
165 166
		}

I
isidor 已提交
167 168 169
		if (this._index < this.labels.length) {
			DOM.addClass(this.labels[this._index], 'active');
		}
170 171 172 173 174 175 176
	}

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

J
Joao Moreno 已提交
177
		this.setIndex(this._index - 1);
178 179 180 181 182 183 184
	}

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

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

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

J
Joao Moreno 已提交
193
		this.setIndex(0);
194 195 196 197 198 199 200
	}

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

J
Joao Moreno 已提交
201 202 203
		this.setIndex(this.items.length - 1);
	}

J
Joao Moreno 已提交
204 205 206 207 208
	setIndex(index: number): void {
		if (index < 0 || index >= this.items.length) {
			return;
		}

209
		DOM.removeClass(this.labels[this._index], 'active');
J
Joao Moreno 已提交
210
		this._index = index;
211
		DOM.addClass(this.labels[this._index], 'active');
J
Joao Moreno 已提交
212 213 214 215 216 217

		this._onDidChange.fire();
	}

	dispose(): void {
		this._onDidChange.dispose();
J
jeanp413 已提交
218
		this._updateLabelDisposable.dispose();
219
	}
220 221
}

I
isidor 已提交
222 223
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
224
	label: IResourceLabel;
I
isidor 已提交
225
	container: HTMLElement;
E
Erich Gamma 已提交
226 227
}

J
Joao Moreno 已提交
228
export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, FuzzyScore, IFileTemplateData>, IAccessibilityProvider<ExplorerItem>, IDisposable {
I
isidor 已提交
229
	static readonly ID = 'file';
230

231 232
	private config: IFilesConfiguration;
	private configListener: IDisposable;
233
	private compressedNavigationControllers = new Map<ExplorerItem, CompressedNavigationController>();
E
Erich Gamma 已提交
234

J
Joao Moreno 已提交
235 236 237
	private _onDidChangeActiveDescendant = new EventMultiplexer<void>();
	readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event;

E
Erich Gamma 已提交
238
	constructor(
B
Benjamin Pasero 已提交
239
		private labels: ResourceLabels,
J
Joao Moreno 已提交
240
		private updateWidth: (stat: ExplorerItem) => void,
241 242 243
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
244 245
		@IExplorerService private readonly explorerService: IExplorerService,
		@ILabelService private readonly labelService: ILabelService
E
Erich Gamma 已提交
246
	) {
247
		this.config = this.configurationService.getValue<IFilesConfiguration>();
248 249
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
250
				this.config = this.configurationService.getValue();
251 252 253 254
			}
		});
	}

I
isidor 已提交
255 256
	get templateId(): string {
		return FilesRenderer.ID;
257
	}
258

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

263
		return { elementDisposable, label, container };
264 265
	}

266
	renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
267
		templateData.elementDisposable.dispose();
268
		const stat = node.element;
269
		const editableData = this.explorerService.getEditableData(stat);
B
Benjamin Pasero 已提交
270

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

273 274
		// File Label
		if (!editableData) {
275
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
276
			templateData.elementDisposable = this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
277
		}
278

279 280 281
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
282
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
283
		}
284 285
	}

J
Joao Moreno 已提交
286
	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData, height: number | undefined): void {
J
Joao Moreno 已提交
287 288 289
		templateData.elementDisposable.dispose();

		const stat = node.element.elements[node.element.elements.length - 1];
I
isidor 已提交
290 291
		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 已提交
292 293 294

		// File Label
		if (!editableData) {
J
Joao Moreno 已提交
295
			DOM.addClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
296
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
297 298

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

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

J
Joao Moreno 已提交
304 305
			const compressedNavigationController = new CompressedNavigationController(id, node.element.elements, templateData);
			disposables.add(compressedNavigationController);
306
			this.compressedNavigationControllers.set(stat, compressedNavigationController);
307

J
Joao Moreno 已提交
308 309 310
			// accessibility
			disposables.add(this._onDidChangeActiveDescendant.add(compressedNavigationController.onDidChange));

311 312 313 314 315 316 317 318
			domEvent(templateData.container, 'mousedown')(e => {
				const result = getIconLabelNameFromHTMLElement(e.target);

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

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

J
Joao Moreno 已提交
321
			templateData.elementDisposable = disposables;
J
Joao Moreno 已提交
322
		}
J
Joao Moreno 已提交
323

J
Joao Moreno 已提交
324 325
		// Input Box
		else {
J
Joao Moreno 已提交
326
			DOM.removeClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
327
			templateData.label.element.style.display = 'none';
I
isidor 已提交
328
			templateData.elementDisposable = this.renderInputBox(templateData.container, editable[0], editableData);
J
Joao Moreno 已提交
329
		}
J
Joao Moreno 已提交
330 331
	}

J
Joao Moreno 已提交
332
	private renderStat(stat: ExplorerItem, label: string | string[], domId: string | undefined, filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
J
Joao Moreno 已提交
333 334 335 336 337 338 339 340 341 342
		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,
343
			matches: createMatches(filterData),
J
Joao Moreno 已提交
344 345
			separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
			domId
J
Joao Moreno 已提交
346 347 348 349 350 351 352 353 354 355 356
		});

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

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

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

I
isidor 已提交
365
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
366 367
		const value = stat.name || '';

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

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

373
		// Input field for name
374
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
375
			validationOptions: {
376 377 378 379 380 381 382 383 384 385 386 387
				validation: (value) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

					return {
						content,
						formatContent: true,
						type: MessageType.ERROR
					};
				}
388
			},
I
isidor 已提交
389
			ariaLabel: localize('fileInputAriaLabel', "Type file name. Press Enter to confirm or Escape to cancel.")
J
Joao Moreno 已提交
390
		});
B
Benjamin Pasero 已提交
391
		const styler = attachInputBoxStyler(inputBox, this.themeService);
E
Erich Gamma 已提交
392

B
Benjamin Pasero 已提交
393
		inputBox.onDidChange(value => {
I
isidor 已提交
394
			label.setFile(joinPath(parent, value || ' '), labelOptions); // update label icon while typing!
B
Benjamin Pasero 已提交
395 396
		});

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

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

J
jeanp413 已提交
403
		const done = once((success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
404
			label.element.style.display = 'none';
405
			const value = inputBox.value;
I
isidor 已提交
406
			dispose(toDispose);
O
orange4glace 已提交
407
			label.element.remove();
J
jeanp413 已提交
408 409 410
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
J
Joao Moreno 已提交
411
		});
E
Erich Gamma 已提交
412

B
Benjamin Pasero 已提交
413
		const toDispose = [
J
Joao Moreno 已提交
414
			inputBox,
A
Cleanup  
Alex Dima 已提交
415
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
416
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
417
					if (inputBox.validate()) {
J
jeanp413 已提交
418
						done(true, true);
J
Joao Moreno 已提交
419
					}
A
Alexandru Dima 已提交
420
				} else if (e.equals(KeyCode.Escape)) {
J
jeanp413 已提交
421
					done(false, true);
J
Joao Moreno 已提交
422 423
				}
			}),
I
isidor 已提交
424
			DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
425
				done(inputBox.isInputValid(), true);
I
isidor 已提交
426
			}),
B
Benjamin Pasero 已提交
427 428
			label,
			styler
J
Joao Moreno 已提交
429
		];
430

I
isidor 已提交
431
		return toDisposable(() => {
J
jeanp413 已提交
432
			done(false, false);
I
isidor 已提交
433
		});
E
Erich Gamma 已提交
434 435
	}

436
	disposeElement(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
437
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
438 439
	}

440
	disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
441
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
442 443
	}

I
isidor 已提交
444 445 446
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
447
	}
I
isidor 已提交
448

449 450 451 452
	getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined {
		return this.compressedNavigationControllers.get(stat);
	}

J
Joao Moreno 已提交
453
	// IAccessibilityProvider
E
Erich Gamma 已提交
454

I
isidor 已提交
455 456
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
457
	}
J
Joao Moreno 已提交
458 459 460 461 462 463 464 465 466

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

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

469 470 471 472 473
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

474
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
475
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
476
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
477

I
isidor 已提交
478
	constructor(
I
isidor 已提交
479 480 481
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
482
	) {
483
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
484
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
485 486
	}

I
isidor 已提交
487
	updateConfiguration(): boolean {
I
isidor 已提交
488
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
489
		this.contextService.getWorkspace().folders.forEach(folder => {
490
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
B
Benjamin Pasero 已提交
491
			const excludesConfig: glob.IExpression = configuration?.files?.exclude || Object.create(null);
492 493 494

			if (!needsRefresh) {
				const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString());
I
isidor 已提交
495
				needsRefresh = !cached || !equals(cached.original, excludesConfig);
496 497
			}

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

500
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
I
isidor 已提交
501
		});
E
Erich Gamma 已提交
502 503 504 505

		return needsRefresh;
	}

506
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
507 508 509
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
510
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
511 512 513 514
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
515
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
516
		if (cached && cached.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) {
E
Erich Gamma 已提交
517 518 519 520 521
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
522 523

	public dispose(): void {
B
Benjamin Pasero 已提交
524
		dispose(this.workspaceFolderChangeListener);
B
Benjamin Pasero 已提交
525
	}
E
Erich Gamma 已提交
526 527
}

I
isidor 已提交
528
// // Explorer Sorter
I
isidor 已提交
529
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
530

I
isidor 已提交
531
	constructor(
I
isidor 已提交
532
		@IExplorerService private readonly explorerService: IExplorerService,
533
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
534
	) { }
I
isidor 已提交
535

I
isidor 已提交
536 537 538 539
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
540 541 542
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
543
			}
I
isidor 已提交
544

I
isidor 已提交
545 546
			return -1;
		}
I
isidor 已提交
547

I
isidor 已提交
548 549 550
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
551

I
isidor 已提交
552
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
553

I
isidor 已提交
554 555 556 557 558 559
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
560

I
isidor 已提交
561 562 563
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
564

I
isidor 已提交
565 566 567
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
568

I
isidor 已提交
569
				break;
I
isidor 已提交
570

I
isidor 已提交
571 572 573 574
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
575

I
isidor 已提交
576 577 578
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
579

I
isidor 已提交
580
				break;
I
isidor 已提交
581

I
isidor 已提交
582 583
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
584

I
isidor 已提交
585 586 587 588
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
589

I
isidor 已提交
590 591 592
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
593

I
isidor 已提交
594 595
				break;
		}
I
isidor 已提交
596

I
isidor 已提交
597 598 599 600
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
601

I
isidor 已提交
602 603
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
604
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
605
				}
I
isidor 已提交
606

I
isidor 已提交
607
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
608

I
isidor 已提交
609 610 611 612 613
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
614

I
isidor 已提交
615
const getFileOverwriteConfirm = (name: string) => {
I
isidor 已提交
616 617 618 619 620 621
	return <IConfirmation>{
		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 已提交
622 623
};

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

627 628 629
	private compressedDragOverElement: HTMLElement | undefined;
	private compressedDropTargetDisposable: IDisposable = Disposable.None;

I
isidor 已提交
630
	private toDispose: IDisposable[];
I
isidor 已提交
631
	private dropEnabled = false;
I
isidor 已提交
632 633 634 635 636 637 638 639 640 641 642

	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,
		@ITextFileService private textFileService: ITextFileService,
643
		@IHostService private hostService: IHostService,
I
isidor 已提交
644
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
I
isidor 已提交
645 646 647 648 649 650 651 652 653 654
	) {
		this.toDispose = [];

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

I
isidor 已提交
655
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
656 657 658 659
		if (!this.dropEnabled) {
			return false;
		}

660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695
		// Compressed folders
		if (target) {
			const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent);

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

				if (iconLabelName && iconLabelName.index < iconLabelName.count - 1) {
					const result = this._onDragOver(data, compressedTarget, targetIndex, originalEvent);

					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();
		return this._onDragOver(data, target, targetIndex, originalEvent);
	}

	private _onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
696 697
		const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
		const fromDesktop = data instanceof DesktopDragAndDropData;
I
isidor 已提交
698
		const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
I
isidor 已提交
699 700

		// Desktop DND
701 702
		if (fromDesktop) {
			if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) {
I
isidor 已提交
703 704 705 706 707 708 709 710 711 712 713
				return false;
			}
		}

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

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

I
isidor 已提交
716
			if (!target) {
H
Howard Hung 已提交
717
				// Dropping onto the empty area. Do not accept if items dragged are already
B
Benjamin Pasero 已提交
718
				// children of the root unless we are copying the file
719
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
720 721 722
					return false;
				}

I
isidor 已提交
723
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
724 725
			}

I
isidor 已提交
726
			if (!Array.isArray(items)) {
I
isidor 已提交
727 728 729
				return false;
			}

I
isidor 已提交
730
			if (items.some((source) => {
I
isidor 已提交
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747
				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
				}

748
				if (isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
749 750 751 752 753 754 755 756 757 758 759
					return true; // Can not move a parent folder into one of its children
				}

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

		// All (target = model)
		if (!target) {
I
isidor 已提交
760
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780
		}

		// 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 已提交
781
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
782
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
783 784 785
			return null;
		}

I
isidor 已提交
786 787 788
		return element.resource.toString();
	}

J
Joao Moreno 已提交
789 790 791 792
	getDragLabel(elements: ExplorerItem[], originalEvent: DragEvent): string | undefined {
		if (elements.length === 1) {
			const stat = FileDragAndDrop.getCompressedStatFromDragEvent(elements[0], originalEvent);
			return stat.name;
I
isidor 已提交
793 794
		}

J
Joao Moreno 已提交
795
		return String(elements.length);
I
isidor 已提交
796 797 798
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
J
Joao Moreno 已提交
799
		const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, originalEvent);
I
isidor 已提交
800
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
801
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
802
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
803 804 805

			// 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 已提交
806
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
807 808 809 810 811 812
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
813
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
814 815 816 817 818 819 820 821 822 823 824
		this.compressedDropTargetDisposable.dispose();

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

			if (compressedTarget) {
				target = compressedTarget;
			}
		}

825 826 827 828
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
829
		if (!target.isDirectory && target.parent) {
830 831 832 833 834 835
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
836 837
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
J
Joao Moreno 已提交
838 839 840 841 842
			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 已提交
843 844 845
		}
		// In-Explorer DND (Move/Copy file)
		else {
846
			this.handleExplorerDrop(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
847 848 849
		}
	}

J
Joao Moreno 已提交
850 851 852 853 854 855 856 857
	private async handleWebExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		data.files.forEach(file => {
			const reader = new FileReader();
			reader.readAsArrayBuffer(file);
			reader.onload = async (event) => {
				const name = file.name;
				if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) {
					if (target.getChild(name)) {
I
isidor 已提交
858
						const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(name));
J
Joao Moreno 已提交
859 860
						if (!confirmed) {
							return;
I
isidor 已提交
861
						}
I
isidor 已提交
862 863
					}

J
Joao Moreno 已提交
864
					const resource = joinPath(target.resource, name);
865
					await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target.result)));
J
Joao Moreno 已提交
866 867 868 869 870 871 872
					if (data.files.length === 1) {
						await this.editorService.openEditor({ resource, options: { pinned: true } });
					}
				}
			};
		});
	}
I
isidor 已提交
873

J
Joao Moreno 已提交
874
	private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
875 876
		const droppedResources = extractResources(originalEvent, true);
		// Check for dropped external files to be folders
I
isidor 已提交
877
		const result = await this.fileService.resolveAll(droppedResources);
I
isidor 已提交
878

I
isidor 已提交
879
		// Pass focus to window
880
		this.hostService.focus();
I
isidor 已提交
881

I
isidor 已提交
882 883 884
		// 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 已提交
885

I
isidor 已提交
886 887 888 889
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
890
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
891 892 893 894 895 896
			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 已提交
897
			}
I
isidor 已提交
898

899
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
900
			if (choice === buttons.length - 3) {
I
isidor 已提交
901 902
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
903
			if (choice === buttons.length - 2) {
I
isidor 已提交
904 905 906 907
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
908 909 910 911 912 913
		}

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

I
isidor 已提交
916
	private async addResources(target: ExplorerItem, resources: URI[]): Promise<void> {
I
isidor 已提交
917 918 919
		if (resources && resources.length > 0) {

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

			// Check for name collisions
			const targetNames = new Set<string>();
			if (targetStat.children) {
925
				const ignoreCase = this.explorerService.shouldIgnoreCase(target.resource);
I
isidor 已提交
926
				targetStat.children.forEach(child => {
I
isidor 已提交
927
					targetNames.add(ignoreCase ? child.name.toLowerCase() : child.name);
I
isidor 已提交
928 929
				});
			}
I
isidor 已提交
930

I
isidor 已提交
931 932
			// Run add in sequence
			const addPromisesFactory: ITask<Promise<void>>[] = [];
I
isidor 已提交
933
			await Promise.all(resources.map(async resource => {
934
				if (targetNames.has(this.explorerService.shouldIgnoreCase(resource) ? basename(resource).toLowerCase() : basename(resource))) {
I
isidor 已提交
935 936 937 938 939 940
					const confirmationResult = await this.dialogService.confirm(getFileOverwriteConfirm(basename(resource)));
					if (!confirmationResult.confirmed) {
						return;
					}
				}

I
isidor 已提交
941 942 943 944 945 946 947 948 949
				addPromisesFactory.push(async () => {
					const sourceFile = resource;
					const targetFile = joinPath(target.resource, basename(sourceFile));

					// if the target exists and is dirty, make sure to revert it. otherwise the dirty contents
					// of the target file would replace the contents of the added file. since we already
					// confirmed the overwrite before, this is OK.
					if (this.textFileService.isDirty(targetFile)) {
						await this.textFileService.revertAll([targetFile], { soft: true });
I
isidor 已提交
950 951
					}

I
isidor 已提交
952
					const copyTarget = joinPath(target.resource, basename(sourceFile));
I
isidor 已提交
953
					const stat = await this.textFileService.copy(sourceFile, copyTarget, true);
I
isidor 已提交
954 955 956 957
					// 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 已提交
958
				});
I
isidor 已提交
959
			}));
I
isidor 已提交
960

I
isidor 已提交
961 962
			await sequence(addPromisesFactory);
		}
I
isidor 已提交
963 964
	}

965 966
	private async handleExplorerDrop(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data);
I
isidor 已提交
967
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
968
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
969 970

		// Handle confirm setting
I
isidor 已提交
971
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
972
		if (confirmDragAndDrop) {
I
isidor 已提交
973
			const confirmation = await this.dialogService.confirm({
I
isidor 已提交
974
				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?")
I
isidor 已提交
975
					: items.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files into '{1}'?", items.length, target.name), items.map(s => s.resource))
I
isidor 已提交
976
						: items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name)
I
isidor 已提交
977
							: localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name),
I
isidor 已提交
978 979 980 981 982 983 984
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});

I
isidor 已提交
985 986 987
			if (!confirmation.confirmed) {
				return;
			}
I
isidor 已提交
988 989

			// Check for confirmation checkbox
I
isidor 已提交
990 991
			if (confirmation.checkboxChecked === true) {
				await this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
I
isidor 已提交
992
			}
I
isidor 已提交
993
		}
I
isidor 已提交
994

I
isidor 已提交
995 996
		const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
		await Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise));
I
isidor 已提交
997 998
	}

999
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
1000 1001 1002 1003 1004
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
1005
		let targetIndex: number | undefined;
I
isidor 已提交
1006 1007 1008 1009 1010
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
1011 1012
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
1013 1014
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
1015
				targetIndex = index;
I
isidor 已提交
1016 1017 1018 1019 1020 1021 1022 1023
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
1024
		if (targetIndex === undefined) {
I
isidor 已提交
1025 1026
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
1027 1028 1029 1030 1031

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

I
isidor 已提交
1032
	private async doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
1033 1034
		// Reuse duplicate action if user copies
		if (isCopy) {
I
isidor 已提交
1035
			const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
1036
			const stat = await this.textFileService.copy(source.resource, findValidPasteFileTarget(this.explorerService, target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming));
I
isidor 已提交
1037 1038 1039
			if (!stat.isDirectory) {
				await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
			}
I
isidor 已提交
1040

I
isidor 已提交
1041
			return;
I
isidor 已提交
1042 1043 1044 1045
		}

		// Otherwise move
		const targetResource = joinPath(target.resource, source.name);
I
isidor 已提交
1046 1047 1048 1049
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
1050

I
isidor 已提交
1051 1052 1053
		try {
			await this.textFileService.move(source.resource, targetResource);
		} catch (error) {
I
isidor 已提交
1054 1055
			// Conflict
			if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
I
isidor 已提交
1056
				const confirm = getFileOverwriteConfirm(source.name);
I
isidor 已提交
1057
				// Move with overwrite if the user confirms
I
isidor 已提交
1058 1059 1060 1061 1062 1063
				const { confirmed } = await this.dialogService.confirm(confirm);
				if (confirmed) {
					try {
						await this.textFileService.move(source.resource, targetResource, true /* overwrite */);
					} catch (error) {
						this.notificationService.error(error);
I
isidor 已提交
1064
					}
I
isidor 已提交
1065
				}
I
isidor 已提交
1066 1067 1068 1069 1070
			}
			// Any other error
			else {
				this.notificationService.error(error);
			}
I
isidor 已提交
1071
		}
I
isidor 已提交
1072
	}
J
Joao Moreno 已提交
1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087

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

1088 1089
	private static getCompressedStatFromDragEvent(stat: ExplorerItem, dragEvent: DragEvent): ExplorerItem {
		const target = document.elementFromPoint(dragEvent.clientX, dragEvent.clientY);
J
Joao Moreno 已提交
1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105
		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;
	}
1106 1107 1108 1109

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

1112
function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement, count: number, index: number } | null {
J
Joao Moreno 已提交
1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
	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 已提交
1133
}
J
Joao Moreno 已提交
1134

J
Joao Moreno 已提交
1135 1136 1137 1138
export function isCompressedFolderName(target: HTMLElement | EventTarget | Element | null): boolean {
	return !!getIconLabelNameFromHTMLElement(target);
}

J
Joao Moreno 已提交
1139 1140
export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

1141
	isIncompressible(stat: ExplorerItem): boolean {
J
Joao Moreno 已提交
1142
		return stat.isRoot || !stat.isDirectory || stat instanceof NewExplorerItem;
J
Joao Moreno 已提交
1143 1144
	}
}