explorerViewer.ts 30.1 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';
I
isidor 已提交
10 11
import { IProgressService } from 'vs/platform/progress/common/progress';
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 50 51 52 53 54 55 56 57 58 59 60 61 62

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

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

	constructor(
		@IProgressService private progressService: IProgressService,
67
		@INotificationService private notificationService: INotificationService,
68
		@IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService,
69
		@IFileService private fileService: IFileService
70
	) { }
E
Erich Gamma 已提交
71

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

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

81 82 83 84 85
		const promise = element.fetchChildren(this.fileService).then(undefined, e => {
			// Do not show error for roots since we already use an explorer decoration to notify user
			if (!(element instanceof ExplorerItem && element.isRoot)) {
				this.notificationService.error(e);
			}
E
Erich Gamma 已提交
86

87 88
			return []; // we could not resolve any children because of an error
		});
E
Erich Gamma 已提交
89

90
		this.progressService.showWhile(promise, this.layoutService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
91
		return promise;
E
Erich Gamma 已提交
92
	}
I
isidor 已提交
93
}
E
Erich Gamma 已提交
94

I
isidor 已提交
95 96
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
97
	label: IResourceLabel;
I
isidor 已提交
98
	container: HTMLElement;
E
Erich Gamma 已提交
99 100
}

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

104 105
	private config: IFilesConfiguration;
	private configListener: IDisposable;
E
Erich Gamma 已提交
106 107

	constructor(
B
Benjamin Pasero 已提交
108
		private labels: ResourceLabels,
J
Joao Moreno 已提交
109
		private updateWidth: (stat: ExplorerItem) => void,
110 111 112
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
I
isidor 已提交
113
		@IExplorerService private readonly explorerService: IExplorerService
E
Erich Gamma 已提交
114
	) {
115
		this.config = this.configurationService.getValue<IFilesConfiguration>();
116 117
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
118
				this.config = this.configurationService.getValue();
119 120 121 122
			}
		});
	}

I
isidor 已提交
123 124
	get templateId(): string {
		return FilesRenderer.ID;
125
	}
126

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

131
		return { elementDisposable, label, container };
132 133
	}

134
	renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
135
		templateData.elementDisposable.dispose();
136
		const stat = node.element;
137
		const editableData = this.explorerService.getEditableData(stat);
B
Benjamin Pasero 已提交
138

139 140
		// File Label
		if (!editableData) {
141
			templateData.label.element.style.display = 'flex';
142
			const extraClasses = ['explorer-item'];
I
isidor 已提交
143 144 145
			if (this.explorerService.isCut(stat)) {
				extraClasses.push('cut');
			}
146 147 148 149
			templateData.label.setFile(stat.resource, {
				hidePath: true,
				fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
				extraClasses,
150 151
				fileDecorations: this.config.explorer.decorations,
				matches: createMatches(node.filterData)
152
			});
153 154

			templateData.elementDisposable = templateData.label.onDidRender(() => {
J
Joao Moreno 已提交
155
				this.updateWidth(stat);
156
			});
157
		}
158

159 160 161
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
162
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
163
		}
164 165
	}

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

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

I
isidor 已提交
174
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
175 176
		const value = stat.name || '';

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

179
		// Input field for name
180
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
181
			validationOptions: {
182 183 184 185 186 187 188 189 190 191 192 193
				validation: (value) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

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

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

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

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

I
isidor 已提交
209
		const done = once(async (success: boolean) => {
T
Till Salinger 已提交
210
			label.element.style.display = 'none';
211
			const value = inputBox.value;
I
isidor 已提交
212 213
			dispose(toDispose);
			container.removeChild(label.element);
214
			editableData.onFinish(value, success);
J
Joao Moreno 已提交
215
		});
E
Erich Gamma 已提交
216

I
isidor 已提交
217
		let ignoreDisposeAndBlur = true;
I
isidor 已提交
218
		setTimeout(() => ignoreDisposeAndBlur = false, 100);
I
isidor 已提交
219
		const blurDisposable = DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
I
isidor 已提交
220 221 222
			if (!ignoreDisposeAndBlur) {
				done(inputBox.isInputValid());
			}
I
isidor 已提交
223
		});
224

B
Benjamin Pasero 已提交
225
		const toDispose = [
J
Joao Moreno 已提交
226
			inputBox,
A
Cleanup  
Alex Dima 已提交
227
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
228
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
229
					if (inputBox.validate()) {
I
isidor 已提交
230
						done(true);
J
Joao Moreno 已提交
231
					}
A
Alexandru Dima 已提交
232
				} else if (e.equals(KeyCode.Escape)) {
I
isidor 已提交
233
					done(false);
J
Joao Moreno 已提交
234 235
				}
			}),
I
isidor 已提交
236
			blurDisposable,
B
Benjamin Pasero 已提交
237 238
			label,
			styler
J
Joao Moreno 已提交
239
		];
240

I
isidor 已提交
241
		return toDisposable(() => {
I
isidor 已提交
242 243 244 245
			if (!ignoreDisposeAndBlur) {
				blurDisposable.dispose();
				done(inputBox.isInputValid());
			}
I
isidor 已提交
246
		});
E
Erich Gamma 已提交
247 248
	}

249
	disposeElement?(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
250
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
251 252
	}

I
isidor 已提交
253 254 255
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
256
	}
I
isidor 已提交
257

I
isidor 已提交
258 259
	dispose(): void {
		this.configListener.dispose();
I
isidor 已提交
260
	}
E
Erich Gamma 已提交
261 262
}

I
isidor 已提交
263 264 265
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
266
	}
E
Erich Gamma 已提交
267 268
}

269 270 271 272 273
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

274
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
275
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
276
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
277

I
isidor 已提交
278
	constructor(
I
isidor 已提交
279 280 281
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
282
	) {
283
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
284
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
285 286
	}

I
isidor 已提交
287
	updateConfiguration(): boolean {
I
isidor 已提交
288
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
289
		this.contextService.getWorkspace().folders.forEach(folder => {
290
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
291 292 293 294
			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 已提交
295
				needsRefresh = !cached || !equals(cached.original, excludesConfig);
296 297
			}

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

			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression);
I
isidor 已提交
301
		});
E
Erich Gamma 已提交
302 303 304 305

		return needsRefresh;
	}

306
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
307 308 309
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
310
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
311 312 313 314
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
315
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
I
isidor 已提交
316
		if (cached && cached.parsed(path.normalize(path.relative(stat.root.resource.path, stat.resource.path)), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) {
317
			// review (isidor): is path.normalize necessary? path.relative already returns an os path
E
Erich Gamma 已提交
318 319 320 321 322
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
323 324 325 326

	public dispose(): void {
		this.workspaceFolderChangeListener = dispose(this.workspaceFolderChangeListener);
	}
E
Erich Gamma 已提交
327 328
}

I
isidor 已提交
329
// // Explorer Sorter
I
isidor 已提交
330
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
331

I
isidor 已提交
332
	constructor(
I
isidor 已提交
333
		@IExplorerService private readonly explorerService: IExplorerService,
334
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
335
	) { }
I
isidor 已提交
336

I
isidor 已提交
337 338 339 340
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
341 342 343
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
344
			}
I
isidor 已提交
345

I
isidor 已提交
346 347
			return -1;
		}
I
isidor 已提交
348

I
isidor 已提交
349 350 351
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
352

I
isidor 已提交
353
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
354

I
isidor 已提交
355 356 357 358 359 360
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
361

I
isidor 已提交
362 363 364
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
365

I
isidor 已提交
366 367 368
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
369

I
isidor 已提交
370
				break;
I
isidor 已提交
371

I
isidor 已提交
372 373 374 375
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
376

I
isidor 已提交
377 378 379
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
380

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

I
isidor 已提交
383 384
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
385

I
isidor 已提交
386 387 388 389
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
390

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

I
isidor 已提交
395 396
				break;
		}
I
isidor 已提交
397

I
isidor 已提交
398 399 400 401
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
402

I
isidor 已提交
403 404
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
405
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
406
				}
I
isidor 已提交
407

I
isidor 已提交
408
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
409

I
isidor 已提交
410 411 412 413 414
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
415

I
isidor 已提交
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
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,
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
	) {
		this.toDispose = [];

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

I
isidor 已提交
444
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
445 446 447 448 449 450
		if (!this.dropEnabled) {
			return false;
		}

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

		// Desktop DND
I
isidor 已提交
454
		if (fromDesktop && originalEvent.dataTransfer) {
I
isidor 已提交
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
			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 已提交
473 474
			const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;

I
isidor 已提交
475
			if (!target) {
B
Benjamin Pasero 已提交
476 477
				// Droping onto the empty area. Do not accept if items dragged are already
				// children of the root unless we are copying the file
478
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
479 480 481
					return false;
				}

I
isidor 已提交
482
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
483 484
			}

I
isidor 已提交
485
			if (!Array.isArray(items)) {
I
isidor 已提交
486 487 488
				return false;
			}

I
isidor 已提交
489
			if (items.some((source) => {
I
isidor 已提交
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
				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 已提交
519
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
		}

		// 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 已提交
540 541 542 543 544
	getDragURI(element: ExplorerItem): string | null {
		if (this.explorerService.isEditable(element)) {
			return null;
		}

I
isidor 已提交
545 546 547
		return element.resource.toString();
	}

I
isidor 已提交
548
	getDragLabel(elements: ExplorerItem[]): string | undefined {
I
isidor 已提交
549 550 551 552 553 554 555 556
		if (elements.length > 1) {
			return String(elements.length);
		}

		return elements[0].name;
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
I
isidor 已提交
557
		const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
I
isidor 已提交
558
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
559
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
560
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
561 562 563

			// 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 已提交
564
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
565 566 567 568 569 570
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
571
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
572 573 574 575
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
576
		if (!target.isDirectory && target.parent) {
577 578 579 580 581 582
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
			this.handleExternalDrop(data, target, originalEvent);
		}
		// In-Explorer DND (Move/Copy file)
		else {
			this.handleExplorerDrop(data, target, originalEvent);
		}
	}


	private handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		const droppedResources = extractResources(originalEvent, true);

		// Check for dropped external files to be folders
		return this.fileService.resolveFiles(droppedResources).then(result => {

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

			// Handle folders by adding to workspace if we are in workspace context
I
isidor 已提交
604
			const folders = result.filter(r => r.success && r.stat && r.stat.isDirectory).map(result => ({ uri: result.stat!.resource }));
I
isidor 已提交
605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626
			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)
627
			else if (target instanceof ExplorerItem) {
I
isidor 已提交
628 629 630 631 632 633 634 635 636 637 638
				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
639
			return this.fileService.resolveFile(target.resource).then(targetStat => {
I
isidor 已提交
640 641 642

				// Check for name collisions
				const targetNames = new Set<string>();
643
				if (targetStat.children) {
644
					targetStat.children.forEach(child => {
645 646 647
						targetNames.add(isLinux ? child.name : child.name.toLowerCase());
					});
				}
I
isidor 已提交
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664

				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) {
665
						return [];
I
isidor 已提交
666 667 668 669 670 671 672 673 674 675 676 677
					}

					// 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.
678
							let revertPromise: Promise<ITextFileOperationResult | null> = Promise.resolve(null);
I
isidor 已提交
679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703
							if (this.textFileService.isDirty(targetFile)) {
								revertPromise = this.textFileService.revertAll([targetFile], { soft: true });
							}

							return revertPromise.then(() => {
								const copyTarget = joinPath(target.resource, basename(sourceFile));
								return this.fileService.copyFile(sourceFile, copyTarget, true).then(stat => {

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

704
	private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
705 706
		const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
707 708 709 710 711 712 713 714
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);

		let confirmPromise: Promise<IConfirmationResult>;

		// Handle confirm setting
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
		if (confirmDragAndDrop) {
			confirmPromise = this.dialogService.confirm({
I
isidor 已提交
715 716 717 718
				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 已提交
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});
		} else {
			confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult);
		}

		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 已提交
739 740
					const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
					return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined);
I
isidor 已提交
741 742 743 744 745 746 747
				}

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

748
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
749 750 751 752 753
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
754
		let targetIndex: number | undefined;
I
isidor 已提交
755 756 757 758 759
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
760 761
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
762 763
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
764
				targetIndex = index;
I
isidor 已提交
765 766 767 768 769 770 771 772
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
773 774 775
		if (!targetIndex) {
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
776 777 778 779 780

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

781
	private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
782 783 784
		// Reuse duplicate action if user copies
		if (isCopy) {

I
isidor 已提交
785
			return this.fileService.copyFile(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwirte: false })).then(stat => {
I
isidor 已提交
786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826
				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);

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