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

I
isidor 已提交
6
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
7 8
import * as DOM from 'vs/base/browser/dom';
import * as glob from 'vs/base/common/glob';
I
isidor 已提交
9
import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
10
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
I
isidor 已提交
11
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
12
import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
13
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
I
isidor 已提交
14
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
15
import { IDisposable, Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
16
import { KeyCode } from 'vs/base/common/keyCodes';
I
isidor 已提交
17
import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
I
isidor 已提交
18
import { ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree';
I
isidor 已提交
19
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
B
Benjamin Pasero 已提交
20
import { IThemeService } from 'vs/platform/theme/common/themeService';
I
isidor 已提交
21
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
22
import { IFilesConfiguration, IExplorerService, IEditableData } from 'vs/workbench/contrib/files/common/files';
I
isidor 已提交
23
import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, distinctParents } from 'vs/base/common/resources';
I
isidor 已提交
24 25 26 27 28 29
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { localize } from 'vs/nls';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { once } from 'vs/base/common/functional';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { equals, deepClone } from 'vs/base/common/objects';
30
import * as path from 'vs/base/common/path';
31
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
I
isidor 已提交
32
import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers';
I
isidor 已提交
33 34 35 36
import { fillResourceDataTransfers, CodeDataTransfers, extractResources } from 'vs/workbench/browser/dnd';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd';
import { Schemas } from 'vs/base/common/network';
I
isidor 已提交
37
import { DesktopDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
38
import { isMacintosh } from 'vs/base/common/platform';
I
isidor 已提交
39 40 41 42 43 44 45 46
import { IDialogService, IConfirmationResult, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs';
import { ITextFileService, ITextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { URI } from 'vs/base/common/uri';
import { ITask, sequence } from 'vs/base/common/async';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
47
import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions';
48
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
I
isidor 已提交
49 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;
E
Erich Gamma 已提交
221

O
orange4glace 已提交
222 223 224 225 226 227 228 229 230 231
		let isFinishableDisposeEvent = false;
		setTimeout(() => {
			// Check if disposed
			if (!inputBox.inputElement) {
				return;
			}
			inputBox.focus();
			inputBox.select({ start: 0, end: lastDot > 0 && !stat.isDirectory ? lastDot : value.length });
			isFinishableDisposeEvent = true;
		}, 0);
E
Erich Gamma 已提交
232

O
orange4glace 已提交
233
		const done = once(async (success: boolean) => {
T
Till Salinger 已提交
234
			label.element.style.display = 'none';
235
			const value = inputBox.value;
I
isidor 已提交
236
			dispose(toDispose);
O
orange4glace 已提交
237
			label.element.remove();
O
orange4glace 已提交
238 239
			// Timeout: once done rendering only then re-render #70902
			setTimeout(() => editableData.onFinish(value, success), 0);
J
Joao Moreno 已提交
240
		});
E
Erich Gamma 已提交
241

I
isidor 已提交
242
		const blurDisposable = DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
O
orange4glace 已提交
243
			done(inputBox.isInputValid());
I
isidor 已提交
244
		});
245

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

I
isidor 已提交
262
		return toDisposable(() => {
O
orange4glace 已提交
263 264 265 266 267
			if (isFinishableDisposeEvent) {
				done(false);
			}
			else {
				dispose(toDispose);
O
orange4glace 已提交
268
				label.element.remove();
O
orange4glace 已提交
269
			}
I
isidor 已提交
270
		});
E
Erich Gamma 已提交
271 272
	}

273
	disposeElement?(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
274
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
275 276
	}

I
isidor 已提交
277 278 279
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
280
	}
I
isidor 已提交
281

I
isidor 已提交
282 283
	dispose(): void {
		this.configListener.dispose();
I
isidor 已提交
284
	}
E
Erich Gamma 已提交
285 286
}

I
isidor 已提交
287 288 289
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
290
	}
E
Erich Gamma 已提交
291 292
}

293 294 295 296 297
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

298
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
299
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
300
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
301

I
isidor 已提交
302
	constructor(
I
isidor 已提交
303 304 305
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
306
	) {
307
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
308
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
309 310
	}

I
isidor 已提交
311
	updateConfiguration(): boolean {
I
isidor 已提交
312
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
313
		this.contextService.getWorkspace().folders.forEach(folder => {
314
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
315 316 317 318
			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 已提交
319
				needsRefresh = !cached || !equals(cached.original, excludesConfig);
320 321
			}

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

324
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
I
isidor 已提交
325
		});
E
Erich Gamma 已提交
326 327 328 329

		return needsRefresh;
	}

330
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
331 332 333
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
334
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
335 336 337 338
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
339
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
340
		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 已提交
341 342 343 344 345
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
346 347

	public dispose(): void {
B
Benjamin Pasero 已提交
348
		dispose(this.workspaceFolderChangeListener);
B
Benjamin Pasero 已提交
349
	}
E
Erich Gamma 已提交
350 351
}

I
isidor 已提交
352
// // Explorer Sorter
I
isidor 已提交
353
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
354

I
isidor 已提交
355
	constructor(
I
isidor 已提交
356
		@IExplorerService private readonly explorerService: IExplorerService,
357
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
358
	) { }
I
isidor 已提交
359

I
isidor 已提交
360 361 362 363
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
364 365 366
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
367
			}
I
isidor 已提交
368

I
isidor 已提交
369 370
			return -1;
		}
I
isidor 已提交
371

I
isidor 已提交
372 373 374
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
375

I
isidor 已提交
376
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
377

I
isidor 已提交
378 379 380 381 382 383
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
384

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

I
isidor 已提交
389 390 391
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
392

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

I
isidor 已提交
395 396 397 398
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
399

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

I
isidor 已提交
404
				break;
I
isidor 已提交
405

I
isidor 已提交
406 407
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
408

I
isidor 已提交
409 410 411 412
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
413

I
isidor 已提交
414 415 416
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
417

I
isidor 已提交
418 419
				break;
		}
I
isidor 已提交
420

I
isidor 已提交
421 422 423 424
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
425

I
isidor 已提交
426 427
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
428
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
429
				}
I
isidor 已提交
430

I
isidor 已提交
431
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
432

I
isidor 已提交
433 434 435 436 437
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
438

I
isidor 已提交
439 440 441 442
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

	private toDispose: IDisposable[];
I
isidor 已提交
443
	private dropEnabled = false;
I
isidor 已提交
444 445 446 447 448 449 450 451 452 453 454 455

	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 已提交
456
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
I
isidor 已提交
457 458 459 460 461 462 463 464 465 466
	) {
		this.toDispose = [];

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

I
isidor 已提交
467
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
468 469 470 471 472 473
		if (!this.dropEnabled) {
			return false;
		}

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

		// Desktop DND
I
isidor 已提交
477
		if (fromDesktop && originalEvent.dataTransfer) {
I
isidor 已提交
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
			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 已提交
496 497
			const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;

I
isidor 已提交
498
			if (!target) {
H
Howard Hung 已提交
499
				// Dropping onto the empty area. Do not accept if items dragged are already
B
Benjamin Pasero 已提交
500
				// children of the root unless we are copying the file
501
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
502 503 504
					return false;
				}

I
isidor 已提交
505
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
506 507
			}

I
isidor 已提交
508
			if (!Array.isArray(items)) {
I
isidor 已提交
509 510 511
				return false;
			}

I
isidor 已提交
512
			if (items.some((source) => {
I
isidor 已提交
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
				}

530
				if (isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
531 532 533 534 535 536 537 538 539 540 541
					return true; // Can not move a parent folder into one of its children
				}

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

		// All (target = model)
		if (!target) {
I
isidor 已提交
542
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562
		}

		// 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 已提交
563
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
564
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
565 566 567
			return null;
		}

I
isidor 已提交
568 569 570
		return element.resource.toString();
	}

I
isidor 已提交
571
	getDragLabel(elements: ExplorerItem[]): string | undefined {
I
isidor 已提交
572 573 574 575 576 577 578 579
		if (elements.length > 1) {
			return String(elements.length);
		}

		return elements[0].name;
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
I
isidor 已提交
580
		const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
I
isidor 已提交
581
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
582
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
583
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
584 585 586

			// 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 已提交
587
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
588 589 590 591 592 593
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
594
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
595 596 597 598
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
599
		if (!target.isDirectory && target.parent) {
600 601 602 603 604 605
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
606 607
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
I
isidor 已提交
608
			this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
609 610 611
		}
		// In-Explorer DND (Move/Copy file)
		else {
I
isidor 已提交
612
			this.handleExplorerDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
613 614 615 616
		}
	}


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

I
isidor 已提交
622 623
		// Pass focus to window
		this.windowService.focusWindow();
I
isidor 已提交
624

I
isidor 已提交
625 626 627
		// 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 已提交
628

I
isidor 已提交
629 630 631 632
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
633
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
634 635 636 637 638 639
			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 已提交
640
			}
I
isidor 已提交
641 642 643

			const choice = await this.dialogService.show(Severity.Info, message, buttons);
			if (choice === buttons.length - 3) {
I
isidor 已提交
644 645
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
646
			if (choice === buttons.length - 2) {
I
isidor 已提交
647 648 649 650
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
651 652 653 654 655 656 657 658
		}

		// 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 已提交
659 660 661 662 663 664
	}

	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 已提交
665
			return this.fileService.resolve(target.resource).then(targetStat => {
I
isidor 已提交
666 667 668

				// Check for name collisions
				const targetNames = new Set<string>();
669
				if (targetStat.children) {
670
					const ignoreCase = hasToIgnoreCase(target.resource);
671
					targetStat.children.forEach(child => {
672
						targetNames.add(ignoreCase ? child.name : child.name.toLowerCase());
673 674
					});
				}
I
isidor 已提交
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691

				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) {
692
						return [];
I
isidor 已提交
693 694 695 696 697 698 699 700 701 702 703 704
					}

					// 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.
705
							let revertPromise: Promise<ITextFileOperationResult | null> = Promise.resolve(null);
I
isidor 已提交
706 707 708 709 710 711
							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 已提交
712
								return this.fileService.copy(sourceFile, copyTarget, true).then(stat => {
I
isidor 已提交
713 714

									// if we only add one file, just open it directly
I
isidor 已提交
715
									if (resources.length === 1 && !stat.isDirectory) {
I
isidor 已提交
716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
										this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
									}
								});
							});
						});
					});

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

		return Promise.resolve(undefined);
	}

I
isidor 已提交
731
	private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
732 733
		const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
734
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
735 736 737 738

		let confirmPromise: Promise<IConfirmationResult>;

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

		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 已提交
766
					const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
I
isidor 已提交
767
					return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined);
I
isidor 已提交
768 769 770 771 772 773 774
				}

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

775
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
776 777 778 779 780
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
781
		let targetIndex: number | undefined;
I
isidor 已提交
782 783 784 785 786
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
787 788
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
789 790
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
791
				targetIndex = index;
I
isidor 已提交
792 793 794 795 796 797 798 799
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
800
		if (targetIndex === undefined) {
I
isidor 已提交
801 802
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
803 804 805 806 807

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

808
	private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
809 810 811
		// Reuse duplicate action if user copies
		if (isCopy) {

H
Howard Hung 已提交
812
			return this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false })).then(stat => {
I
isidor 已提交
813 814 815 816 817 818 819 820 821 822
				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 已提交
823 824 825 826
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857

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