explorerViewer.ts 31.2 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

I
isidor 已提交
6
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
7 8
import * as DOM from 'vs/base/browser/dom';
import * as glob from 'vs/base/common/glob';
I
isidor 已提交
9
import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
10
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
I
isidor 已提交
11
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
12
import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
13
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
I
isidor 已提交
14
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
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

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

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

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

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

			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
95 96
				this.notificationService.error(e);
			}
E
Erich Gamma 已提交
97

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return needsRefresh;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

I
isidor 已提交
500
			if (items.some((source) => {
I
isidor 已提交
501 502 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
				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 已提交
530
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
		}

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

I
isidor 已提交
556 557 558
		return element.resource.toString();
	}

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

		return elements[0].name;
	}

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

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

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

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


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

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

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

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

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

			return undefined;
I
isidor 已提交
639 640 641 642 643 644 645 646
		}

		// 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 已提交
647 648 649 650 651 652
	}

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

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

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

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

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

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

		return Promise.resolve(undefined);
	}

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

		let confirmPromise: Promise<IConfirmationResult>;

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

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

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

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

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

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

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

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

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

B
Benjamin Pasero 已提交
799
			return this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwirte: false })).then(stat => {
I
isidor 已提交
800 801 802 803 804 805 806 807 808 809
				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 已提交
810 811 812 813
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
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 843 844

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