explorerViewer.ts 30.8 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 } 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';
15
import { IDisposable, Disposable, dispose, toDisposable } 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';
I
isidor 已提交
18
import { ITreeRenderer, 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';
31
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
I
isidor 已提交
32
import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers';
I
isidor 已提交
33 34 35 36
import { fillResourceDataTransfers, CodeDataTransfers, extractResources } from 'vs/workbench/browser/dnd';
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 39 40 41 42 43 44 45 46
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs';
import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
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 { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
I
isidor 已提交
50 51 52 53 54 55 56 57 58 59 60 61 62 63

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

	private static readonly ITEM_HEIGHT = 22;

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

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

64
export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | ExplorerItem[], ExplorerItem> {
E
Erich Gamma 已提交
65 66

	constructor(
67
		@IProgressService private readonly progressService: IProgressService,
68 69 70
		@INotificationService private readonly notificationService: INotificationService,
		@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
		@IFileService private readonly fileService: IFileService,
71 72
		@IExplorerService private readonly explorerService: IExplorerService,
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
73
	) { }
E
Erich Gamma 已提交
74

75 76
	hasChildren(element: ExplorerItem | ExplorerItem[]): boolean {
		return Array.isArray(element) || element.isDirectory;
E
Erich Gamma 已提交
77 78
	}

79 80 81
	getChildren(element: ExplorerItem | ExplorerItem[]): Promise<ExplorerItem[]> {
		if (Array.isArray(element)) {
			return Promise.resolve(element);
82
		}
E
Erich Gamma 已提交
83

84
		const promise = element.fetchChildren(this.fileService, this.explorerService).then(undefined, e => {
85 86 87 88 89 90 91 92 93 94 95

			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];
				}
			} else {
				// Do not show error for roots since we already use an explorer decoration to notify user
96 97
				this.notificationService.error(e);
			}
E
Erich Gamma 已提交
98

99 100
			return []; // we could not resolve any children because of an error
		});
E
Erich Gamma 已提交
101

102 103 104 105 106
		this.progressService.withProgress({
			location: ProgressLocation.Explorer,
			delay: this.layoutService.isRestored() ? 800 : 1200 // less ugly initial startup
		}, _progress => promise);

107
		return promise;
E
Erich Gamma 已提交
108
	}
I
isidor 已提交
109
}
E
Erich Gamma 已提交
110

I
isidor 已提交
111 112
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
113
	label: IResourceLabel;
I
isidor 已提交
114
	container: HTMLElement;
E
Erich Gamma 已提交
115 116
}

117
export class FilesRenderer implements ITreeRenderer<ExplorerItem, FuzzyScore, IFileTemplateData>, IDisposable {
I
isidor 已提交
118
	static readonly ID = 'file';
119

120 121
	private config: IFilesConfiguration;
	private configListener: IDisposable;
E
Erich Gamma 已提交
122 123

	constructor(
B
Benjamin Pasero 已提交
124
		private labels: ResourceLabels,
J
Joao Moreno 已提交
125
		private updateWidth: (stat: ExplorerItem) => void,
126 127 128
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
I
isidor 已提交
129
		@IExplorerService private readonly explorerService: IExplorerService
E
Erich Gamma 已提交
130
	) {
131
		this.config = this.configurationService.getValue<IFilesConfiguration>();
132 133
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
134
				this.config = this.configurationService.getValue();
135 136 137 138
			}
		});
	}

I
isidor 已提交
139 140
	get templateId(): string {
		return FilesRenderer.ID;
141
	}
142

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

147
		return { elementDisposable, label, container };
148 149
	}

150
	renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
151
		templateData.elementDisposable.dispose();
152
		const stat = node.element;
153
		const editableData = this.explorerService.getEditableData(stat);
B
Benjamin Pasero 已提交
154

155 156
		// File Label
		if (!editableData) {
157
			templateData.label.element.style.display = 'flex';
158
			const extraClasses = ['explorer-item'];
I
isidor 已提交
159 160 161
			if (this.explorerService.isCut(stat)) {
				extraClasses.push('cut');
			}
162 163 164 165
			templateData.label.setFile(stat.resource, {
				hidePath: true,
				fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
				extraClasses,
166 167
				fileDecorations: this.config.explorer.decorations,
				matches: createMatches(node.filterData)
168
			});
169 170

			templateData.elementDisposable = templateData.label.onDidRender(() => {
J
Joao Moreno 已提交
171
				this.updateWidth(stat);
172
			});
173
		}
174

175 176 177
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
178
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
179
		}
180 181
	}

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

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

I
isidor 已提交
190
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
191 192
		const value = stat.name || '';

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

195
		// Input field for name
196
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
197
			validationOptions: {
198 199 200 201 202 203 204 205 206 207 208 209
				validation: (value) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

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

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

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

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

J
jeanp413 已提交
225
		const done = once(async (success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
226
			label.element.style.display = 'none';
227
			const value = inputBox.value;
I
isidor 已提交
228 229
			dispose(toDispose);
			container.removeChild(label.element);
J
jeanp413 已提交
230
			if (finishEditing) {
231 232 233
				// Timeout: once done rendering only then re-render #70902
				setTimeout(() => editableData.onFinish(value, success), 0);
			}
J
Joao Moreno 已提交
234
		});
E
Erich Gamma 已提交
235

I
isidor 已提交
236
		const blurDisposable = DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
I
isidor 已提交
237
			done(inputBox.isInputValid(), true);
I
isidor 已提交
238
		});
239

B
Benjamin Pasero 已提交
240
		const toDispose = [
J
Joao Moreno 已提交
241
			inputBox,
A
Cleanup  
Alex Dima 已提交
242
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
243
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
244
					if (inputBox.validate()) {
245
						done(true, true);
J
Joao Moreno 已提交
246
					}
A
Alexandru Dima 已提交
247
				} else if (e.equals(KeyCode.Escape)) {
248
					done(false, true);
J
Joao Moreno 已提交
249 250
				}
			}),
I
isidor 已提交
251
			blurDisposable,
B
Benjamin Pasero 已提交
252 253
			label,
			styler
J
Joao Moreno 已提交
254
		];
255

I
isidor 已提交
256
		return toDisposable(() => {
257 258
			blurDisposable.dispose();
			done(false, false);
I
isidor 已提交
259
		});
E
Erich Gamma 已提交
260 261
	}

262
	disposeElement?(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
263
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
264 265
	}

I
isidor 已提交
266 267 268
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
269
	}
I
isidor 已提交
270

I
isidor 已提交
271 272
	dispose(): void {
		this.configListener.dispose();
I
isidor 已提交
273
	}
E
Erich Gamma 已提交
274 275
}

I
isidor 已提交
276 277 278
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
279
	}
E
Erich Gamma 已提交
280 281
}

282 283 284 285 286
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

287
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
288
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
289
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
290

I
isidor 已提交
291
	constructor(
I
isidor 已提交
292 293 294
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
295
	) {
296
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
297
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
298 299
	}

I
isidor 已提交
300
	updateConfiguration(): boolean {
I
isidor 已提交
301
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
302
		this.contextService.getWorkspace().folders.forEach(folder => {
303
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
304 305 306 307
			const excludesConfig: glob.IExpression = (configuration && configuration.files && configuration.files.exclude) || Object.create(null);

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

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

313
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
I
isidor 已提交
314
		});
E
Erich Gamma 已提交
315 316 317 318

		return needsRefresh;
	}

319
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
320 321 322
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
323
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
324 325 326 327
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
328
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
329
		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 已提交
330 331 332 333 334
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
335 336

	public dispose(): void {
B
Benjamin Pasero 已提交
337
		dispose(this.workspaceFolderChangeListener);
B
Benjamin Pasero 已提交
338
	}
E
Erich Gamma 已提交
339 340
}

I
isidor 已提交
341
// // Explorer Sorter
I
isidor 已提交
342
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
343

I
isidor 已提交
344
	constructor(
I
isidor 已提交
345
		@IExplorerService private readonly explorerService: IExplorerService,
346
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
347
	) { }
I
isidor 已提交
348

I
isidor 已提交
349 350 351 352
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
353 354 355
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
356
			}
I
isidor 已提交
357

I
isidor 已提交
358 359
			return -1;
		}
I
isidor 已提交
360

I
isidor 已提交
361 362 363
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
364

I
isidor 已提交
365
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
366

I
isidor 已提交
367 368 369 370 371 372
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
373

I
isidor 已提交
374 375 376
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
377

I
isidor 已提交
378 379 380
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
381

I
isidor 已提交
382
				break;
I
isidor 已提交
383

I
isidor 已提交
384 385 386 387
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
388

I
isidor 已提交
389 390 391
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
392

I
isidor 已提交
393
				break;
I
isidor 已提交
394

I
isidor 已提交
395 396
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
397

I
isidor 已提交
398 399 400 401
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
402

I
isidor 已提交
403 404 405
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
406

I
isidor 已提交
407 408
				break;
		}
I
isidor 已提交
409

I
isidor 已提交
410 411 412 413
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
414

I
isidor 已提交
415 416
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
417
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
418
				}
I
isidor 已提交
419

I
isidor 已提交
420
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
421

I
isidor 已提交
422 423 424 425 426
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
427

I
isidor 已提交
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

	private toDispose: IDisposable[];
	private dropEnabled: boolean;

	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,
		@IWindowService private windowService: IWindowService,
I
isidor 已提交
445 446
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService,
		@IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService
I
isidor 已提交
447 448 449 450 451 452 453 454 455 456
	) {
		this.toDispose = [];

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

I
isidor 已提交
457
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
458 459 460 461 462 463
		if (!this.dropEnabled) {
			return false;
		}

		const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
		const fromDesktop = data instanceof DesktopDragAndDropData;
I
isidor 已提交
464
		const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
I
isidor 已提交
465 466

		// Desktop DND
I
isidor 已提交
467
		if (fromDesktop && originalEvent.dataTransfer) {
I
isidor 已提交
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
			const types = originalEvent.dataTransfer.types;
			const typesArray: string[] = [];
			for (let i = 0; i < types.length; i++) {
				typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase
			}

			if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) {
				return false;
			}
		}

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

		// In-Explorer DND
		else {
I
isidor 已提交
486 487
			const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;

I
isidor 已提交
488
			if (!target) {
B
Benjamin Pasero 已提交
489 490
				// Droping onto the empty area. Do not accept if items dragged are already
				// children of the root unless we are copying the file
491
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
492 493 494
					return false;
				}

I
isidor 已提交
495
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
496 497
			}

I
isidor 已提交
498
			if (!Array.isArray(items)) {
I
isidor 已提交
499 500 501
				return false;
			}

I
isidor 已提交
502
			if (items.some((source) => {
I
isidor 已提交
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531
				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
				}

				if (isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) {
					return true; // Can not move a parent folder into one of its children
				}

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

		// All (target = model)
		if (!target) {
I
isidor 已提交
532
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
		}

		// 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 已提交
553
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
554
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
555 556 557
			return null;
		}

I
isidor 已提交
558 559 560
		return element.resource.toString();
	}

I
isidor 已提交
561
	getDragLabel(elements: ExplorerItem[]): string | undefined {
I
isidor 已提交
562 563 564 565 566 567 568 569
		if (elements.length > 1) {
			return String(elements.length);
		}

		return elements[0].name;
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
I
isidor 已提交
570
		const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
I
isidor 已提交
571
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
572
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
573
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
574 575 576

			// 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 已提交
577
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
578 579 580 581 582 583
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
584
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
585 586 587 588
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
589
		if (!target.isDirectory && target.parent) {
590 591 592 593 594 595
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
596 597 598 599 600 601
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
			this.handleExternalDrop(data, target, originalEvent);
		}
		// In-Explorer DND (Move/Copy file)
		else {
I
isidor 已提交
602
			this.handleExplorerDrop(data, target, originalEvent);
I
isidor 已提交
603 604 605 606 607 608 609
		}
	}


	private handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		const droppedResources = extractResources(originalEvent, true);
		// Check for dropped external files to be folders
B
Benjamin Pasero 已提交
610
		return this.fileService.resolveAll(droppedResources).then(result => {
I
isidor 已提交
611 612 613 614 615

			// Pass focus to window
			this.windowService.focusWindow();

			// Handle folders by adding to workspace if we are in workspace context
I
isidor 已提交
616
			const folders = result.filter(r => r.success && r.stat && r.stat.isDirectory).map(result => ({ uri: result.stat!.resource }));
I
isidor 已提交
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638
			if (folders.length > 0) {

				// If we are in no-workspace context, ask for confirmation to create a workspace
				let confirmedPromise: Promise<IConfirmationResult> = Promise.resolve({ confirmed: true });
				if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
					confirmedPromise = this.dialogService.confirm({
						message: folders.length > 1 ? localize('dropFolders', "Do you want to add the folders to the workspace?") : localize('dropFolder', "Do you want to add the folder to the workspace?"),
						type: 'question',
						primaryButton: folders.length > 1 ? localize('addFolders', "&&Add Folders") : localize('addFolder', "&&Add Folder")
					});
				}

				return confirmedPromise.then(res => {
					if (res.confirmed) {
						return this.workspaceEditingService.addFolders(folders);
					}

					return undefined;
				});
			}

			// Handle dropped files (only support FileStat as target)
639
			else if (target instanceof ExplorerItem) {
I
isidor 已提交
640 641 642 643 644 645 646 647 648 649 650
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
		});
	}

	private addResources(target: ExplorerItem, resources: URI[]): Promise<any> {
		if (resources && resources.length > 0) {

			// Resolve target to check for name collisions and ask user
B
Benjamin Pasero 已提交
651
			return this.fileService.resolve(target.resource).then(targetStat => {
I
isidor 已提交
652 653 654

				// Check for name collisions
				const targetNames = new Set<string>();
655
				if (targetStat.children) {
656
					targetStat.children.forEach(child => {
657 658 659
						targetNames.add(isLinux ? child.name : child.name.toLowerCase());
					});
				}
I
isidor 已提交
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676

				let overwritePromise: Promise<IConfirmationResult> = Promise.resolve({ confirmed: true });
				if (resources.some(resource => {
					return targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase());
				})) {
					const confirm: IConfirmation = {
						message: localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"),
						detail: localize('irreversible', "This action is irreversible!"),
						primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
						type: 'warning'
					};

					overwritePromise = this.dialogService.confirm(confirm);
				}

				return overwritePromise.then(res => {
					if (!res.confirmed) {
677
						return [];
I
isidor 已提交
678 679 680 681 682 683 684 685 686 687 688 689
					}

					// Run add in sequence
					const addPromisesFactory: ITask<Promise<void>>[] = [];
					resources.forEach(resource => {
						addPromisesFactory.push(() => {
							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.
690
							let revertPromise: Promise<ITextFileOperationResult | null> = Promise.resolve(null);
I
isidor 已提交
691 692 693 694 695 696
							if (this.textFileService.isDirty(targetFile)) {
								revertPromise = this.textFileService.revertAll([targetFile], { soft: true });
							}

							return revertPromise.then(() => {
								const copyTarget = joinPath(target.resource, basename(sourceFile));
B
Benjamin Pasero 已提交
697
								return this.fileService.copy(sourceFile, copyTarget, true).then(stat => {
I
isidor 已提交
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715

									// if we only add one file, just open it directly
									if (resources.length === 1) {
										this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
									}
								});
							});
						});
					});

					return sequence(addPromisesFactory);
				});
			});
		}

		return Promise.resolve(undefined);
	}

I
isidor 已提交
716
	private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
717 718
		const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
719
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
720 721 722 723

		let confirmPromise: Promise<IConfirmationResult>;

		// Handle confirm setting
I
isidor 已提交
724
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
725 726
		if (confirmDragAndDrop) {
			confirmPromise = this.dialogService.confirm({
I
isidor 已提交
727 728 729 730
				message: items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?")
					: items.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files?", items.length), items.map(s => s.resource))
						: items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name)
							: localize('confirmMove', "Are you sure you want to move '{0}'?", items[0].name),
I
isidor 已提交
731 732 733 734 735 736 737
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});
		} else {
738
			confirmPromise = Promise.resolve({ confirmed: true });
I
isidor 已提交
739 740 741 742 743 744 745 746 747 748 749 750
		}

		return confirmPromise.then(res => {

			// Check for confirmation checkbox
			let updateConfirmSettingsPromise: Promise<void> = Promise.resolve(undefined);
			if (res.confirmed && res.checkboxChecked === true) {
				updateConfirmSettingsPromise = this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
			}

			return updateConfirmSettingsPromise.then(() => {
				if (res.confirmed) {
I
isidor 已提交
751
					const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
I
isidor 已提交
752
					return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined);
I
isidor 已提交
753 754 755 756 757 758 759
				}

				return Promise.resolve(undefined);
			});
		});
	}

760
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
761 762 763 764 765
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
766
		let targetIndex: number | undefined;
I
isidor 已提交
767 768 769 770 771
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
772 773
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
774 775
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
776
				targetIndex = index;
I
isidor 已提交
777 778 779 780 781 782 783 784
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
785
		if (targetIndex === undefined) {
I
isidor 已提交
786 787
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
788 789 790 791 792

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

793
	private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
794 795 796
		// Reuse duplicate action if user copies
		if (isCopy) {

B
Benjamin Pasero 已提交
797
			return this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwirte: false })).then(stat => {
I
isidor 已提交
798 799 800 801 802 803 804 805 806 807
				if (!stat.isDirectory) {
					return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }).then(() => undefined);
				}

				return undefined;
			});
		}

		// Otherwise move
		const targetResource = joinPath(target.resource, source.name);
I
isidor 已提交
808 809 810 811
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842

		return this.textFileService.move(source.resource, targetResource).then(undefined, error => {

			// 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
				return this.dialogService.confirm(confirm).then(res => {
					if (res.confirmed) {
						return this.textFileService.move(source.resource, targetResource, true /* overwrite */).then(undefined, error => this.notificationService.error(error));
					}

					return undefined;
				});
			}

			// Any other error
			else {
				this.notificationService.error(error);
			}

			return undefined;
		});
	}
}