explorerViewer.ts 31.5 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';
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';
38
import { isMacintosh } from 'vs/base/common/platform';
I
isidor 已提交
39 40 41 42 43 44 45 46
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 { Emitter } from 'vs/base/common/event';
I
isidor 已提交
50 51 52

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

I
isidor 已提交
53
	static readonly ITEM_HEIGHT = 22;
I
isidor 已提交
54 55 56 57 58 59 60 61 62 63

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

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

I
isidor 已提交
64
export const explorerRootErrorEmitter = new Emitter<URI>();
65
export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | ExplorerItem[], ExplorerItem> {
E
Erich Gamma 已提交
66 67

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

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

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

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

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

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

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

109
		return promise;
E
Erich Gamma 已提交
110
	}
I
isidor 已提交
111
}
E
Erich Gamma 已提交
112

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

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

122 123
	private config: IFilesConfiguration;
	private configListener: IDisposable;
E
Erich Gamma 已提交
124 125

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

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

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

149
		return { elementDisposable, label, container };
150 151
	}

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

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

			templateData.elementDisposable = templateData.label.onDidRender(() => {
I
isidor 已提交
173 174 175 176 177
				try {
					this.updateWidth(stat);
				} catch (e) {
					// noop since the element might no longer be in the tree, no update of width necessery
				}
178
			});
179
		}
180

181 182 183
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
184
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
185
		}
186 187
	}

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

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

I
isidor 已提交
196
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
197 198
		const value = stat.name || '';

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

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

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

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

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

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

J
jeanp413 已提交
231
		const done = once((success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
232
			label.element.style.display = 'none';
233
			const value = inputBox.value;
I
isidor 已提交
234
			dispose(toDispose);
O
orange4glace 已提交
235
			label.element.remove();
J
jeanp413 已提交
236 237 238
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
J
Joao Moreno 已提交
239
		});
E
Erich Gamma 已提交
240

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

I
isidor 已提交
259
		return toDisposable(() => {
J
jeanp413 已提交
260
			done(false, false);
I
isidor 已提交
261
		});
E
Erich Gamma 已提交
262 263
	}

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

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

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

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

284 285 286 287 288
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

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

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

I
isidor 已提交
302
	updateConfiguration(): boolean {
I
isidor 已提交
303
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
304
		this.contextService.getWorkspace().folders.forEach(folder => {
305
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
306 307 308 309
			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 已提交
310
				needsRefresh = !cached || !equals(cached.original, excludesConfig);
311 312
			}

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

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

		return needsRefresh;
	}

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

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

		return true;
	}
B
Benjamin Pasero 已提交
337 338

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

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

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

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

I
isidor 已提交
360 361
			return -1;
		}
I
isidor 已提交
362

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

I
isidor 已提交
367
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
368

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

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

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

I
isidor 已提交
384
				break;
I
isidor 已提交
385

I
isidor 已提交
386 387 388 389
			case 'filesFirst':
				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
				break;
I
isidor 已提交
396

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

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

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

I
isidor 已提交
409 410
				break;
		}
I
isidor 已提交
411

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

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

I
isidor 已提交
422
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
423

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

I
isidor 已提交
430 431 432 433
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

	private toDispose: IDisposable[];
I
isidor 已提交
434
	private dropEnabled = false;
I
isidor 已提交
435 436 437 438 439 440 441 442 443 444 445 446

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

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

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

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

		// Desktop DND
I
isidor 已提交
468
		if (fromDesktop && originalEvent.dataTransfer) {
I
isidor 已提交
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486
			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 已提交
487 488
			const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;

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

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

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

I
isidor 已提交
503
			if (items.some((source) => {
I
isidor 已提交
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
				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
				}

521
				if (isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
522 523 524 525 526 527 528 529 530 531 532
					return true; // Can not move a parent folder into one of its children
				}

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

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

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

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

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

		return elements[0].name;
	}

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

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

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

I
isidor 已提交
597 598
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
I
isidor 已提交
599
			this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
600 601 602
		}
		// In-Explorer DND (Move/Copy file)
		else {
I
isidor 已提交
603
			this.handleExplorerDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
604 605 606
		}
	}

I
isidor 已提交
607
	private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
608 609
		const droppedResources = extractResources(originalEvent, true);
		// Check for dropped external files to be folders
I
isidor 已提交
610
		const result = await this.fileService.resolveAll(droppedResources);
I
isidor 已提交
611

I
isidor 已提交
612 613
		// Pass focus to window
		this.windowService.focusWindow();
I
isidor 已提交
614

I
isidor 已提交
615 616 617
		// 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 已提交
618

I
isidor 已提交
619 620 621 622
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
623
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
624 625 626 627 628 629
			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 已提交
630
			}
I
isidor 已提交
631

632
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
633
			if (choice === buttons.length - 3) {
I
isidor 已提交
634 635
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
636
			if (choice === buttons.length - 2) {
I
isidor 已提交
637 638 639 640
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
641 642 643 644 645 646 647 648
		}

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

		return undefined;
I
isidor 已提交
649 650 651 652 653 654
	}

	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 已提交
655
			return this.fileService.resolve(target.resource).then(targetStat => {
I
isidor 已提交
656 657 658

				// Check for name collisions
				const targetNames = new Set<string>();
659
				if (targetStat.children) {
660
					const ignoreCase = hasToIgnoreCase(target.resource);
661
					targetStat.children.forEach(child => {
662
						targetNames.add(ignoreCase ? child.name : child.name.toLowerCase());
663 664
					});
				}
I
isidor 已提交
665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681

				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) {
682
						return [];
I
isidor 已提交
683 684 685 686 687 688 689 690 691 692 693 694
					}

					// 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.
695
							let revertPromise: Promise<ITextFileOperationResult | null> = Promise.resolve(null);
I
isidor 已提交
696 697 698 699 700 701
							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 已提交
702
								return this.fileService.copy(sourceFile, copyTarget, true).then(stat => {
I
isidor 已提交
703 704

									// if we only add one file, just open it directly
I
isidor 已提交
705
									if (resources.length === 1 && !stat.isDirectory) {
I
isidor 已提交
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720
										this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
									}
								});
							});
						});
					});

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

		return Promise.resolve(undefined);
	}

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

		let confirmPromise: Promise<IConfirmationResult>;

		// Handle confirm setting
I
isidor 已提交
729
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
730 731
		if (confirmDragAndDrop) {
			confirmPromise = this.dialogService.confirm({
I
isidor 已提交
732 733 734 735
				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 已提交
736 737 738 739 740 741 742
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});
		} else {
743
			confirmPromise = Promise.resolve({ confirmed: true });
I
isidor 已提交
744 745 746 747 748 749 750 751 752 753 754 755
		}

		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 已提交
756
					const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
I
isidor 已提交
757
					return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined);
I
isidor 已提交
758 759 760 761 762 763 764
				}

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

765
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
766 767 768 769 770
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
771
		let targetIndex: number | undefined;
I
isidor 已提交
772 773 774 775 776
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

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

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
790
		if (targetIndex === undefined) {
I
isidor 已提交
791 792
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
793 794 795 796 797

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

798
	private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
799 800
		// Reuse duplicate action if user copies
		if (isCopy) {
I
isidor 已提交
801 802
			const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
			return this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming)).then(stat => {
I
isidor 已提交
803 804 805 806 807 808 809 810 811 812
				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 已提交
813 814 815 816
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
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 843 844 845 846 847

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