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';
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 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;
I
isidor 已提交
144
		const label = this.labels.create(container, { supportHighlights: true, donotSupportOcticons: 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
		inputBox.value = value;
J
jeanp413 已提交
221 222
		inputBox.focus();
		inputBox.select({ start: 0, end: lastDot > 0 && !stat.isDirectory ? lastDot : value.length });
E
Erich Gamma 已提交
223

J
jeanp413 已提交
224
		const done = once((success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
225
			label.element.style.display = 'none';
226
			const value = inputBox.value;
I
isidor 已提交
227
			dispose(toDispose);
O
orange4glace 已提交
228
			label.element.remove();
J
jeanp413 已提交
229 230 231
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
J
Joao Moreno 已提交
232
		});
E
Erich Gamma 已提交
233

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

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

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

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

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

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

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

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

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

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

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

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

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

		return needsRefresh;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

I
isidor 已提交
426 427 428 429
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

	private toDispose: IDisposable[];
I
isidor 已提交
430
	private dropEnabled = false;
I
isidor 已提交
431 432 433 434 435 436 437 438 439 440 441 442

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return elements[0].name;
	}

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

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

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

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


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

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

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

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

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

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

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

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

				// Check for name collisions
				const targetNames = new Set<string>();
656
				if (targetStat.children) {
657
					const ignoreCase = hasToIgnoreCase(target.resource);
658
					targetStat.children.forEach(child => {
659
						targetNames.add(ignoreCase ? child.name : child.name.toLowerCase());
660 661
					});
				}
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) {

H
Howard Hung 已提交
799
			return this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: 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;
		});
	}
}