explorerViewer.ts 29.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';
I
isidor 已提交
12
import { IFileService, FileKind, IFileStat, 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 66

	constructor(
		@IProgressService private progressService: IProgressService,
67
		@INotificationService private notificationService: INotificationService,
68
		@IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService,
69
		@IFileService private fileService: IFileService
70
	) { }
E
Erich Gamma 已提交
71

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

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

81 82 83 84 85
		const promise = element.fetchChildren(this.fileService).then(undefined, e => {
			// Do not show error for roots since we already use an explorer decoration to notify user
			if (!(element instanceof ExplorerItem && element.isRoot)) {
				this.notificationService.error(e);
			}
E
Erich Gamma 已提交
86

87 88
			return []; // we could not resolve any children because of an error
		});
E
Erich Gamma 已提交
89

90
		this.progressService.showWhile(promise, this.layoutService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
91
		return promise;
E
Erich Gamma 已提交
92
	}
I
isidor 已提交
93
}
E
Erich Gamma 已提交
94

I
isidor 已提交
95 96
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
97
	label: IResourceLabel;
I
isidor 已提交
98
	container: HTMLElement;
E
Erich Gamma 已提交
99 100
}

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

104 105
	private config: IFilesConfiguration;
	private configListener: IDisposable;
E
Erich Gamma 已提交
106 107

	constructor(
B
Benjamin Pasero 已提交
108
		private labels: ResourceLabels,
J
Joao Moreno 已提交
109
		private updateWidth: (stat: ExplorerItem) => void,
110 111 112
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
I
isidor 已提交
113
		@IExplorerService private readonly explorerService: IExplorerService
E
Erich Gamma 已提交
114
	) {
115
		this.config = this.configurationService.getValue<IFilesConfiguration>();
116 117
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
118
				this.config = this.configurationService.getValue();
119 120 121 122
			}
		});
	}

I
isidor 已提交
123 124
	get templateId(): string {
		return FilesRenderer.ID;
125
	}
126

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

131
		return { elementDisposable, label, container };
132 133
	}

134
	renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
135
		templateData.elementDisposable.dispose();
136
		const stat = node.element;
137
		const editableData = this.explorerService.getEditableData(stat);
B
Benjamin Pasero 已提交
138

139 140
		// File Label
		if (!editableData) {
141
			templateData.label.element.style.display = 'flex';
142
			const extraClasses = ['explorer-item'];
I
isidor 已提交
143 144 145
			if (this.explorerService.isCut(stat)) {
				extraClasses.push('cut');
			}
146 147 148 149
			templateData.label.setFile(stat.resource, {
				hidePath: true,
				fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
				extraClasses,
150 151
				fileDecorations: this.config.explorer.decorations,
				matches: createMatches(node.filterData)
152
			});
153 154

			templateData.elementDisposable = templateData.label.onDidRender(() => {
J
Joao Moreno 已提交
155
				this.updateWidth(stat);
156
			});
157
		}
158

159 160 161
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
162
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
163
		}
164 165
	}

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

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

I
isidor 已提交
174
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
175 176
		const value = stat.name || '';

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

179
		// Input field for name
180
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
181
			validationOptions: {
182 183 184 185 186 187 188 189 190 191 192 193
				validation: (value) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

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

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

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

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

I
isidor 已提交
209
		const done = once(async (success: boolean) => {
T
Till Salinger 已提交
210
			label.element.style.display = 'none';
211
			const value = inputBox.value;
I
isidor 已提交
212 213
			dispose(toDispose);
			container.removeChild(label.element);
214
			editableData.onFinish(value, success);
J
Joao Moreno 已提交
215
		});
E
Erich Gamma 已提交
216

I
isidor 已提交
217 218
		let ignoreDisposeAndBlur = true;
		setTimeout(() => ignoreDisposeAndBlur = false, 0);
I
isidor 已提交
219
		const blurDisposable = DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
I
isidor 已提交
220 221 222
			if (!ignoreDisposeAndBlur) {
				done(inputBox.isInputValid());
			}
I
isidor 已提交
223
		});
224

B
Benjamin Pasero 已提交
225
		const toDispose = [
J
Joao Moreno 已提交
226
			inputBox,
A
Cleanup  
Alex Dima 已提交
227
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
228
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
229
					if (inputBox.validate()) {
I
isidor 已提交
230
						done(true);
J
Joao Moreno 已提交
231
					}
A
Alexandru Dima 已提交
232
				} else if (e.equals(KeyCode.Escape)) {
I
isidor 已提交
233
					done(false);
J
Joao Moreno 已提交
234 235
				}
			}),
I
isidor 已提交
236
			blurDisposable,
B
Benjamin Pasero 已提交
237 238
			label,
			styler
J
Joao Moreno 已提交
239
		];
240

I
isidor 已提交
241
		return toDisposable(() => {
I
isidor 已提交
242 243 244 245
			if (!ignoreDisposeAndBlur) {
				blurDisposable.dispose();
				done(inputBox.isInputValid());
			}
I
isidor 已提交
246
		});
E
Erich Gamma 已提交
247 248
	}

249
	disposeElement?(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
250
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
251 252
	}

I
isidor 已提交
253 254 255
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
256
	}
I
isidor 已提交
257

I
isidor 已提交
258 259
	dispose(): void {
		this.configListener.dispose();
I
isidor 已提交
260
	}
E
Erich Gamma 已提交
261 262
}

I
isidor 已提交
263 264 265
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
266
	}
E
Erich Gamma 已提交
267 268
}

269 270 271 272 273
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

274
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
275
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
276
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
277

I
isidor 已提交
278
	constructor(
I
isidor 已提交
279 280 281
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
282
	) {
283
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
284
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
285 286
	}

I
isidor 已提交
287
	updateConfiguration(): boolean {
I
isidor 已提交
288
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
289
		this.contextService.getWorkspace().folders.forEach(folder => {
290
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
291 292 293 294
			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 已提交
295
				needsRefresh = !cached || !equals(cached.original, excludesConfig);
296 297
			}

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

			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) } as CachedParsedExpression);
I
isidor 已提交
301
		});
E
Erich Gamma 已提交
302 303 304 305

		return needsRefresh;
	}

306
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
307 308 309
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
310
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
311 312 313 314
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
315
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
316 317
		if (cached && cached.parsed(path.normalize(path.relative(stat.root.resource.path, stat.resource.path)), stat.name, name => !!stat.parent.getChild(name))) {
			// review (isidor): is path.normalize necessary? path.relative already returns an os path
E
Erich Gamma 已提交
318 319 320 321 322
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
323 324 325 326

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

I
isidor 已提交
329
// // Explorer Sorter
I
isidor 已提交
330
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
331

I
isidor 已提交
332
	constructor(
I
isidor 已提交
333
		@IExplorerService private readonly explorerService: IExplorerService,
334
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
335
	) { }
I
isidor 已提交
336

I
isidor 已提交
337 338 339 340 341 342
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
				return this.contextService.getWorkspaceFolder(statA.resource).index - this.contextService.getWorkspaceFolder(statB.resource).index;
			}
I
isidor 已提交
343

I
isidor 已提交
344 345
			return -1;
		}
I
isidor 已提交
346

I
isidor 已提交
347 348 349
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
350

I
isidor 已提交
351
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
352

I
isidor 已提交
353 354 355 356 357 358
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
359

I
isidor 已提交
360 361 362
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
363

I
isidor 已提交
364 365 366
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
367

I
isidor 已提交
368
				break;
I
isidor 已提交
369

I
isidor 已提交
370 371 372 373
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
374

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

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

I
isidor 已提交
381 382
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
383

I
isidor 已提交
384 385 386 387
			default: /* 'default', 'modified' */
				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 394
				break;
		}
I
isidor 已提交
395

I
isidor 已提交
396 397 398 399
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
400

I
isidor 已提交
401 402 403 404
			case 'modified':
				if (statA.mtime !== statB.mtime) {
					return statA.mtime < statB.mtime ? 1 : -1;
				}
I
isidor 已提交
405

I
isidor 已提交
406
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
407

I
isidor 已提交
408 409 410 411 412
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
413

I
isidor 已提交
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448
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()));
	}

	onDragOver(data: IDragAndDropData, target: ExplorerItem, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
		if (!this.dropEnabled) {
			return false;
		}

		const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
		const fromDesktop = data instanceof DesktopDragAndDropData;
I
isidor 已提交
449
		const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
I
isidor 已提交
450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470

		// Desktop DND
		if (fromDesktop) {
			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 已提交
471 472
			const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;

I
isidor 已提交
473
			if (!target) {
B
Benjamin Pasero 已提交
474 475
				// Droping onto the empty area. Do not accept if items dragged are already
				// children of the root unless we are copying the file
476
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
477 478 479
					return false;
				}

I
isidor 已提交
480
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
481 482
			}

I
isidor 已提交
483
			if (!Array.isArray(items)) {
I
isidor 已提交
484 485 486
				return false;
			}

I
isidor 已提交
487
			if (items.some((source) => {
I
isidor 已提交
488 489 490 491 492 493 494 495 496 497 498 499 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
				}

				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 已提交
517
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
		}

		// 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 已提交
538 539 540 541 542
	getDragURI(element: ExplorerItem): string | null {
		if (this.explorerService.isEditable(element)) {
			return null;
		}

I
isidor 已提交
543 544 545 546 547 548 549 550 551 552 553 554
		return element.resource.toString();
	}

	getDragLabel(elements: ExplorerItem[]): string {
		if (elements.length > 1) {
			return String(elements.length);
		}

		return elements[0].name;
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
I
isidor 已提交
555 556
		const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
		if (items && items.length) {
I
isidor 已提交
557
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
558
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
559 560 561

			// 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 已提交
562
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
563 564 565 566 567 568 569
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

	drop(data: IDragAndDropData, target: ExplorerItem, targetIndex: number, originalEvent: DragEvent): void {
570 571 572 573 574 575 576 577 578 579 580
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
		if (!target.isDirectory) {
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
			this.handleExternalDrop(data, target, originalEvent);
		}
		// In-Explorer DND (Move/Copy file)
		else {
			this.handleExplorerDrop(data, target, originalEvent);
		}
	}


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

		// Check for dropped external files to be folders
		return this.fileService.resolveFiles(droppedResources).then(result => {

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

			// Handle folders by adding to workspace if we are in workspace context
			const folders = result.filter(r => r.success && r.stat.isDirectory).map(result => ({ uri: result.stat.resource }));
			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)
625
			else if (target instanceof ExplorerItem) {
I
isidor 已提交
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
				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
			return this.fileService.resolveFile(target.resource).then((targetStat: IFileStat) => {

				// Check for name collisions
				const targetNames = new Set<string>();
				targetStat.children.forEach((child) => {
					targetNames.add(isLinux ? child.name : child.name.toLowerCase());
				});

				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) {
						return undefined;
					}

					// 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.
							let revertPromise: Promise<ITextFileOperationResult> = Promise.resolve(null);
							if (this.textFileService.isDirty(targetFile)) {
								revertPromise = this.textFileService.revertAll([targetFile], { soft: true });
							}

							return revertPromise.then(() => {
								const copyTarget = joinPath(target.resource, basename(sourceFile));
								return this.fileService.copyFile(sourceFile, copyTarget, true).then(stat => {

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

700
	private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
701 702
		const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
703 704 705 706 707 708 709 710
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);

		let confirmPromise: Promise<IConfirmationResult>;

		// Handle confirm setting
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
		if (confirmDragAndDrop) {
			confirmPromise = this.dialogService.confirm({
I
isidor 已提交
711 712 713 714
				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 已提交
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});
		} else {
			confirmPromise = Promise.resolve({ confirmed: true } as IConfirmationResult);
		}

		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 已提交
735 736
					const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
					return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined);
I
isidor 已提交
737 738 739 740 741 742 743
				}

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

744
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
745 746 747 748 749
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
750
		let targetIndex: number | undefined;
I
isidor 已提交
751 752 753 754 755 756 757 758
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
				uri: folders[index].uri
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
759
				targetIndex = index;
I
isidor 已提交
760 761 762 763 764 765 766 767 768 769 770 771 772
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}

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

773
	private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
774 775 776
		// Reuse duplicate action if user copies
		if (isCopy) {

I
isidor 已提交
777
			return this.fileService.copyFile(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwirte: false })).then(stat => {
I
isidor 已提交
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818
				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);

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