explorerViewer.ts 40.2 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, IEditableData } from 'vs/workbench/contrib/files/common/files';
I
isidor 已提交
23
import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, 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';
I
isidor 已提交
49
import { Emitter } 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';
I
isidor 已提交
57 58 59

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

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

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

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

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

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

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

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

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

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

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

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

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

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

export class CompressedNavigationController implements ICompressedNavigationController {

	private _index: number;
136
	readonly labels: HTMLElement[];
137 138 139 140 141

	get index(): number { return this._index; }
	get count(): number { return this.items.length; }
	get current(): ExplorerItem { return this.items[this._index]!; }

142
	constructor(readonly items: ExplorerItem[], templateData: IFileTemplateData) {
143 144 145 146 147 148 149 150 151 152
		this._index = items.length - 1;
		this.labels = Array.from(templateData.container.querySelectorAll('.label-name')) as HTMLElement[];
		DOM.addClass(this.labels[this._index], 'active');
	}

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

J
Joao Moreno 已提交
153
		this.setIndex(this._index - 1);
154 155 156 157 158 159 160
	}

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

J
Joao Moreno 已提交
161
		this.setIndex(this._index + 1);
162
	}
163 164 165 166 167 168

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

J
Joao Moreno 已提交
169
		this.setIndex(0);
170 171 172 173 174 175 176
	}

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

J
Joao Moreno 已提交
177 178 179
		this.setIndex(this.items.length - 1);
	}

J
Joao Moreno 已提交
180 181 182 183 184
	setIndex(index: number): void {
		if (index < 0 || index >= this.items.length) {
			return;
		}

185
		DOM.removeClass(this.labels[this._index], 'active');
J
Joao Moreno 已提交
186
		this._index = index;
187 188
		DOM.addClass(this.labels[this._index], 'active');
	}
189 190
}

I
isidor 已提交
191 192
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
193
	label: IResourceLabel;
I
isidor 已提交
194
	container: HTMLElement;
E
Erich Gamma 已提交
195 196
}

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

200 201
	private config: IFilesConfiguration;
	private configListener: IDisposable;
202
	private compressedNavigationControllers = new Map<ExplorerItem, CompressedNavigationController>();
E
Erich Gamma 已提交
203 204

	constructor(
B
Benjamin Pasero 已提交
205
		private labels: ResourceLabels,
J
Joao Moreno 已提交
206
		private updateWidth: (stat: ExplorerItem) => void,
207 208 209
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
210 211
		@IExplorerService private readonly explorerService: IExplorerService,
		@ILabelService private readonly labelService: ILabelService
E
Erich Gamma 已提交
212
	) {
213
		this.config = this.configurationService.getValue<IFilesConfiguration>();
214 215
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
216
				this.config = this.configurationService.getValue();
217 218 219 220
			}
		});
	}

I
isidor 已提交
221 222
	get templateId(): string {
		return FilesRenderer.ID;
223
	}
224

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

229
		return { elementDisposable, label, container };
230 231
	}

232
	renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
233
		templateData.elementDisposable.dispose();
234
		const stat = node.element;
235
		const editableData = this.explorerService.getEditableData(stat);
B
Benjamin Pasero 已提交
236

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

239 240
		// File Label
		if (!editableData) {
241
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
242
			templateData.elementDisposable = this.renderStat(stat, stat.name, node.filterData, templateData);
243
		}
244

245 246 247
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
248
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
249
		}
250 251
	}

J
Joao Moreno 已提交
252
	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData, height: number | undefined): void {
J
Joao Moreno 已提交
253 254 255
		templateData.elementDisposable.dispose();

		const stat = node.element.elements[node.element.elements.length - 1];
I
isidor 已提交
256 257
		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 已提交
258 259 260

		// File Label
		if (!editableData) {
J
Joao Moreno 已提交
261
			DOM.addClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
262
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
263 264

			const disposables = new DisposableStore();
I
isidor 已提交
265
			const label = node.element.elements.map(e => e.name);
J
Joao Moreno 已提交
266 267
			disposables.add(this.renderStat(stat, label, node.filterData, templateData));

268 269
			const compressedNavigationController = new CompressedNavigationController(node.element.elements, templateData);
			this.compressedNavigationControllers.set(stat, compressedNavigationController);
270 271 272 273 274 275 276 277 278 279 280 281

			domEvent(templateData.container, 'mousedown')(e => {
				const result = getIconLabelNameFromHTMLElement(e.target);

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

			disposables.add(toDisposable(() => {
				this.compressedNavigationControllers.delete(stat);
			}));
282

J
Joao Moreno 已提交
283
			templateData.elementDisposable = disposables;
J
Joao Moreno 已提交
284
		}
J
Joao Moreno 已提交
285

J
Joao Moreno 已提交
286 287
		// Input Box
		else {
J
Joao Moreno 已提交
288
			DOM.removeClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
289
			templateData.label.element.style.display = 'none';
I
isidor 已提交
290
			templateData.elementDisposable = this.renderInputBox(templateData.container, editable[0], editableData);
J
Joao Moreno 已提交
291
		}
J
Joao Moreno 已提交
292 293
	}

294
	private renderStat(stat: ExplorerItem, label: string | string[], filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
J
Joao Moreno 已提交
295 296 297 298 299 300 301 302 303 304
		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,
305 306
			matches: createMatches(filterData),
			separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority)
J
Joao Moreno 已提交
307 308 309 310 311 312 313 314 315 316 317
		});

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

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

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

I
isidor 已提交
326
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
327 328
		const value = stat.name || '';

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

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

334
		// Input field for name
335
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
336
			validationOptions: {
337 338 339 340 341 342 343 344 345 346 347 348
				validation: (value) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

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

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

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

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

J
jeanp413 已提交
364
		const done = once((success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
365
			label.element.style.display = 'none';
366
			const value = inputBox.value;
I
isidor 已提交
367
			dispose(toDispose);
O
orange4glace 已提交
368
			label.element.remove();
J
jeanp413 已提交
369 370 371
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
J
Joao Moreno 已提交
372
		});
E
Erich Gamma 已提交
373

B
Benjamin Pasero 已提交
374
		const toDispose = [
J
Joao Moreno 已提交
375
			inputBox,
A
Cleanup  
Alex Dima 已提交
376
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
377
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
378
					if (inputBox.validate()) {
J
jeanp413 已提交
379
						done(true, true);
J
Joao Moreno 已提交
380
					}
A
Alexandru Dima 已提交
381
				} else if (e.equals(KeyCode.Escape)) {
J
jeanp413 已提交
382
					done(false, true);
J
Joao Moreno 已提交
383 384
				}
			}),
I
isidor 已提交
385
			DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
386
				done(inputBox.isInputValid(), true);
I
isidor 已提交
387
			}),
B
Benjamin Pasero 已提交
388 389
			label,
			styler
J
Joao Moreno 已提交
390
		];
391

I
isidor 已提交
392
		return toDisposable(() => {
J
jeanp413 已提交
393
			done(false, false);
I
isidor 已提交
394
		});
E
Erich Gamma 已提交
395 396
	}

397
	disposeElement(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
398
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
399 400
	}

401
	disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
402
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
403 404
	}

I
isidor 已提交
405 406 407
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
408
	}
I
isidor 已提交
409

410 411 412 413
	getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined {
		return this.compressedNavigationControllers.get(stat);
	}

I
isidor 已提交
414 415
	dispose(): void {
		this.configListener.dispose();
I
isidor 已提交
416
	}
E
Erich Gamma 已提交
417 418
}

I
isidor 已提交
419 420 421
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
422
	}
E
Erich Gamma 已提交
423 424
}

425 426 427 428 429
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

430
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
431
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
432
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
433

I
isidor 已提交
434
	constructor(
I
isidor 已提交
435 436 437
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
438
	) {
439
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
440
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
441 442
	}

I
isidor 已提交
443
	updateConfiguration(): boolean {
I
isidor 已提交
444
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
445
		this.contextService.getWorkspace().folders.forEach(folder => {
446
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
B
Benjamin Pasero 已提交
447
			const excludesConfig: glob.IExpression = configuration?.files?.exclude || Object.create(null);
448 449 450

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

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

456
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
I
isidor 已提交
457
		});
E
Erich Gamma 已提交
458 459 460 461

		return needsRefresh;
	}

462
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
463 464 465
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
466
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
467 468 469 470
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
471
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
472
		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 已提交
473 474 475 476 477
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
478 479

	public dispose(): void {
B
Benjamin Pasero 已提交
480
		dispose(this.workspaceFolderChangeListener);
B
Benjamin Pasero 已提交
481
	}
E
Erich Gamma 已提交
482 483
}

I
isidor 已提交
484
// // Explorer Sorter
I
isidor 已提交
485
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
486

I
isidor 已提交
487
	constructor(
I
isidor 已提交
488
		@IExplorerService private readonly explorerService: IExplorerService,
489
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
490
	) { }
I
isidor 已提交
491

I
isidor 已提交
492 493 494 495
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
496 497 498
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
499
			}
I
isidor 已提交
500

I
isidor 已提交
501 502
			return -1;
		}
I
isidor 已提交
503

I
isidor 已提交
504 505 506
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
507

I
isidor 已提交
508
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
509

I
isidor 已提交
510 511 512 513 514 515
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
516

I
isidor 已提交
517 518 519
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
520

I
isidor 已提交
521 522 523
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
524

I
isidor 已提交
525
				break;
I
isidor 已提交
526

I
isidor 已提交
527 528 529 530
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
531

I
isidor 已提交
532 533 534
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
535

I
isidor 已提交
536
				break;
I
isidor 已提交
537

I
isidor 已提交
538 539
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
540

I
isidor 已提交
541 542 543 544
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
545

I
isidor 已提交
546 547 548
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
549

I
isidor 已提交
550 551
				break;
		}
I
isidor 已提交
552

I
isidor 已提交
553 554 555 556
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
557

I
isidor 已提交
558 559
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
560
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
561
				}
I
isidor 已提交
562

I
isidor 已提交
563
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
564

I
isidor 已提交
565 566 567 568 569
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
570

I
isidor 已提交
571 572 573 574 575 576 577
const fileOverwriteConfirm = (name: string) => {
	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 已提交
578 579
};

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

583 584 585
	private compressedDragOverElement: HTMLElement | undefined;
	private compressedDropTargetDisposable: IDisposable = Disposable.None;

I
isidor 已提交
586
	private toDispose: IDisposable[];
I
isidor 已提交
587
	private dropEnabled = false;
I
isidor 已提交
588 589 590 591 592 593 594 595 596 597 598

	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,
599
		@IHostService private hostService: IHostService,
I
isidor 已提交
600
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
I
isidor 已提交
601 602 603 604 605 606 607 608 609 610
	) {
		this.toDispose = [];

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

I
isidor 已提交
611
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
612 613 614 615
		if (!this.dropEnabled) {
			return false;
		}

616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
		// 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 已提交
652 653
		const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
		const fromDesktop = data instanceof DesktopDragAndDropData;
I
isidor 已提交
654
		const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
I
isidor 已提交
655 656

		// Desktop DND
657 658
		if (fromDesktop) {
			if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) {
I
isidor 已提交
659 660 661 662 663 664 665 666 667 668 669
				return false;
			}
		}

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

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

I
isidor 已提交
672
			if (!target) {
H
Howard Hung 已提交
673
				// Dropping onto the empty area. Do not accept if items dragged are already
B
Benjamin Pasero 已提交
674
				// children of the root unless we are copying the file
675
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
676 677 678
					return false;
				}

I
isidor 已提交
679
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
680 681
			}

I
isidor 已提交
682
			if (!Array.isArray(items)) {
I
isidor 已提交
683 684 685
				return false;
			}

I
isidor 已提交
686
			if (items.some((source) => {
I
isidor 已提交
687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
				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
				}

704
				if (isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
705 706 707 708 709 710 711 712 713 714 715
					return true; // Can not move a parent folder into one of its children
				}

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

		// All (target = model)
		if (!target) {
I
isidor 已提交
716
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736
		}

		// 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 已提交
737
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
738
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
739 740 741
			return null;
		}

I
isidor 已提交
742 743 744
		return element.resource.toString();
	}

J
Joao Moreno 已提交
745 746 747 748
	getDragLabel(elements: ExplorerItem[], originalEvent: DragEvent): string | undefined {
		if (elements.length === 1) {
			const stat = FileDragAndDrop.getCompressedStatFromDragEvent(elements[0], originalEvent);
			return stat.name;
I
isidor 已提交
749 750
		}

J
Joao Moreno 已提交
751
		return String(elements.length);
I
isidor 已提交
752 753 754
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
J
Joao Moreno 已提交
755
		const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, originalEvent);
I
isidor 已提交
756
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
757
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
758
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
759 760 761

			// 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 已提交
762
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
763 764 765 766 767 768
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
769
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
770 771 772 773 774 775 776 777 778 779 780
		this.compressedDropTargetDisposable.dispose();

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

			if (compressedTarget) {
				target = compressedTarget;
			}
		}

781 782 783 784
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
785
		if (!target.isDirectory && target.parent) {
786 787 788 789 790 791
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
792 793
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
J
Joao Moreno 已提交
794 795 796 797 798
			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 已提交
799 800 801
		}
		// In-Explorer DND (Move/Copy file)
		else {
802
			this.handleExplorerDrop(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
803 804 805
		}
	}

J
Joao Moreno 已提交
806 807 808 809 810 811 812 813
	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)) {
814
						const { confirmed } = await this.dialogService.confirm(fileOverwriteConfirm(name));
J
Joao Moreno 已提交
815 816
						if (!confirmed) {
							return;
I
isidor 已提交
817
						}
I
isidor 已提交
818 819
					}

J
Joao Moreno 已提交
820 821 822 823 824 825 826 827 828
					const resource = joinPath(target.resource, name);
					await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target?.result)));
					if (data.files.length === 1) {
						await this.editorService.openEditor({ resource, options: { pinned: true } });
					}
				}
			};
		});
	}
I
isidor 已提交
829

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

I
isidor 已提交
835
		// Pass focus to window
836
		this.hostService.focus();
I
isidor 已提交
837

I
isidor 已提交
838 839 840
		// 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 已提交
841

I
isidor 已提交
842 843 844 845
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
846
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
847 848 849 850 851 852
			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 已提交
853
			}
I
isidor 已提交
854

855
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
856
			if (choice === buttons.length - 3) {
I
isidor 已提交
857 858
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
859
			if (choice === buttons.length - 2) {
I
isidor 已提交
860 861 862 863
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
864 865 866 867 868 869
		}

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

I
isidor 已提交
872
	private async addResources(target: ExplorerItem, resources: URI[]): Promise<void> {
I
isidor 已提交
873 874 875
		if (resources && resources.length > 0) {

			// Resolve target to check for name collisions and ask user
I
isidor 已提交
876 877 878 879 880 881 882
			const targetStat = await this.fileService.resolve(target.resource);

			// Check for name collisions
			const targetNames = new Set<string>();
			if (targetStat.children) {
				const ignoreCase = hasToIgnoreCase(target.resource);
				targetStat.children.forEach(child => {
I
isidor 已提交
883
					targetNames.add(ignoreCase ? child.name.toLowerCase() : child.name);
I
isidor 已提交
884 885
				});
			}
I
isidor 已提交
886

I
isidor 已提交
887 888
			const filtered = resources.filter(resource => targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase()));
			const resourceExists = filtered.length >= 1;
I
isidor 已提交
889
			if (resourceExists) {
I
isidor 已提交
890
				const confirmationResult = await this.dialogService.confirm(fileOverwriteConfirm(basename(filtered[0])));
I
isidor 已提交
891
				if (!confirmationResult.confirmed) {
I
isidor 已提交
892
					return;
I
isidor 已提交
893
				}
I
isidor 已提交
894
			}
I
isidor 已提交
895

I
isidor 已提交
896 897 898 899 900 901 902 903 904 905 906 907
			// Run add in sequence
			const addPromisesFactory: ITask<Promise<void>>[] = [];
			resources.forEach(resource => {
				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 已提交
908 909
					}

I
isidor 已提交
910 911 912 913 914 915
					const copyTarget = joinPath(target.resource, basename(sourceFile));
					const stat = await this.fileService.copy(sourceFile, copyTarget, true);
					// 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 已提交
916 917 918
				});
			});

I
isidor 已提交
919 920
			await sequence(addPromisesFactory);
		}
I
isidor 已提交
921 922
	}

923 924
	private async handleExplorerDrop(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data);
I
isidor 已提交
925
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
926
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
927 928

		// Handle confirm setting
I
isidor 已提交
929
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
930
		if (confirmDragAndDrop) {
I
isidor 已提交
931
			const confirmation = await this.dialogService.confirm({
I
isidor 已提交
932
				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 已提交
933
					: 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 已提交
934
						: 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 已提交
935
							: localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name),
I
isidor 已提交
936 937 938 939 940 941 942
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});

I
isidor 已提交
943 944 945
			if (!confirmation.confirmed) {
				return;
			}
I
isidor 已提交
946 947

			// Check for confirmation checkbox
I
isidor 已提交
948 949
			if (confirmation.checkboxChecked === true) {
				await this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
I
isidor 已提交
950
			}
I
isidor 已提交
951
		}
I
isidor 已提交
952

I
isidor 已提交
953 954
		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 已提交
955 956
	}

957
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
958 959 960 961 962
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
963
		let targetIndex: number | undefined;
I
isidor 已提交
964 965 966 967 968
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
969 970
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
971 972
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
973
				targetIndex = index;
I
isidor 已提交
974 975 976 977 978 979 980 981
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
982
		if (targetIndex === undefined) {
I
isidor 已提交
983 984
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
985 986 987 988 989

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

I
isidor 已提交
990
	private async doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
991 992
		// Reuse duplicate action if user copies
		if (isCopy) {
I
isidor 已提交
993
			const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
I
isidor 已提交
994 995 996 997
			const stat = await this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming));
			if (!stat.isDirectory) {
				await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
			}
I
isidor 已提交
998

I
isidor 已提交
999
			return;
I
isidor 已提交
1000 1001 1002 1003
		}

		// Otherwise move
		const targetResource = joinPath(target.resource, source.name);
I
isidor 已提交
1004 1005 1006 1007
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
1008

I
isidor 已提交
1009 1010 1011
		try {
			await this.textFileService.move(source.resource, targetResource);
		} catch (error) {
I
isidor 已提交
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021
			// Conflict
			if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
				const confirm: IConfirmation = {
					message: localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name),
					detail: localize('irreversible', "This action is irreversible!"),
					primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
					type: 'warning'
				};

				// Move with overwrite if the user confirms
I
isidor 已提交
1022 1023 1024 1025 1026 1027
				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 已提交
1028
					}
I
isidor 已提交
1029
				}
I
isidor 已提交
1030 1031 1032 1033 1034
			}
			// Any other error
			else {
				this.notificationService.error(error);
			}
I
isidor 已提交
1035
		}
I
isidor 已提交
1036
	}
J
Joao Moreno 已提交
1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051

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

1052 1053
	private static getCompressedStatFromDragEvent(stat: ExplorerItem, dragEvent: DragEvent): ExplorerItem {
		const target = document.elementFromPoint(dragEvent.clientX, dragEvent.clientY);
J
Joao Moreno 已提交
1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
		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;
	}
1070 1071 1072 1073

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

1076
function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement, count: number, index: number } | null {
J
Joao Moreno 已提交
1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096
	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 已提交
1097
}
J
Joao Moreno 已提交
1098

J
Joao Moreno 已提交
1099 1100 1101 1102
export function isCompressedFolderName(target: HTMLElement | EventTarget | Element | null): boolean {
	return !!getIconLabelNameFromHTMLElement(target);
}

J
Joao Moreno 已提交
1103 1104
export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

1105
	isIncompressible(stat: ExplorerItem): boolean {
J
Joao Moreno 已提交
1106
		return stat.isRoot || !stat.isDirectory || stat instanceof NewExplorerItem;
J
Joao Moreno 已提交
1107 1108
	}
}