explorerViewer.ts 30.7 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

	constructor(
66 67 68 69
		@IProgressService private readonly progressService: IProgressService,
		@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
		this.progressService.showWhile(promise, this.layoutService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
102
		return promise;
E
Erich Gamma 已提交
103
	}
I
isidor 已提交
104
}
E
Erich Gamma 已提交
105

I
isidor 已提交
106 107
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
108
	label: IResourceLabel;
I
isidor 已提交
109
	container: HTMLElement;
E
Erich Gamma 已提交
110 111
}

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

115 116
	private config: IFilesConfiguration;
	private configListener: IDisposable;
E
Erich Gamma 已提交
117 118

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

I
isidor 已提交
134 135
	get templateId(): string {
		return FilesRenderer.ID;
136
	}
137

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

142
		return { elementDisposable, label, container };
143 144
	}

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

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

			templateData.elementDisposable = templateData.label.onDidRender(() => {
J
Joao Moreno 已提交
166
				this.updateWidth(stat);
167
			});
168
		}
169

170 171 172
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
173
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
174
		}
175 176
	}

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

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

I
isidor 已提交
185
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
186 187
		const value = stat.name || '';

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

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

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

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

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

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

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

I
isidor 已提交
229
		let ignoreDisposeAndBlur = true;
I
isidor 已提交
230
		setTimeout(() => ignoreDisposeAndBlur = false, 100);
I
isidor 已提交
231
		const blurDisposable = DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
I
isidor 已提交
232 233 234
			if (!ignoreDisposeAndBlur) {
				done(inputBox.isInputValid());
			}
I
isidor 已提交
235
		});
236

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

I
isidor 已提交
253
		return toDisposable(() => {
I
isidor 已提交
254 255
			if (!ignoreDisposeAndBlur) {
				blurDisposable.dispose();
I
isidor 已提交
256
				done(false);
I
isidor 已提交
257
			}
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 329
		const relativeTo = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? path.dirname(stat.root.resource.path) : stat.root.resource.path;
		if (cached && cached.parsed(path.relative(relativeTo, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) {
E
Erich Gamma 已提交
330 331 332 333 334
			return false; // hidden through pattern
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

I
isidor 已提交
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
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 已提交
456
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
457 458 459 460 461 462
		if (!this.dropEnabled) {
			return false;
		}

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

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

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

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

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

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

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

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

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

		return elements[0].name;
	}

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

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

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

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


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

		// Check for dropped external files to be folders
B
Benjamin Pasero 已提交
610
		return this.fileService.resolveAll(droppedResources).then(result => {
I
isidor 已提交
611 612 613 614 615

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

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

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

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

					return undefined;
				});
			}

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

			return undefined;
		});
	}

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

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

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

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

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

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

					// Run add in sequence
					const addPromisesFactory: ITask<Promise<void>>[] = [];
					resources.forEach(resource => {
						addPromisesFactory.push(() => {
							const sourceFile = resource;
							const targetFile = joinPath(target.resource, basename(sourceFile));

							// if the target exists and is dirty, make sure to revert it. otherwise the dirty contents
							// of the target file would replace the contents of the added file. since we already
							// confirmed the overwrite before, this is OK.
690
							let revertPromise: Promise<ITextFileOperationResult | null> = Promise.resolve(null);
I
isidor 已提交
691 692 693 694 695 696
							if (this.textFileService.isDirty(targetFile)) {
								revertPromise = this.textFileService.revertAll([targetFile], { soft: true });
							}

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

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

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

		return Promise.resolve(undefined);
	}

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

		let confirmPromise: Promise<IConfirmationResult>;

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

		return confirmPromise.then(res => {

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

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

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

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

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

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

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

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

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

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

				return undefined;
			});
		}

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

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

			// Conflict
			if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
				const confirm: IConfirmation = {
					message: localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name),
					detail: localize('irreversible', "This action is irreversible!"),
					primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
					type: 'warning'
				};

				// Move with overwrite if the user confirms
				return this.dialogService.confirm(confirm).then(res => {
					if (res.confirmed) {
						return this.textFileService.move(source.resource, targetResource, true /* overwrite */).then(undefined, error => this.notificationService.error(error));
					}

					return undefined;
				});
			}

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

			return undefined;
		});
	}
}