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

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

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

	private static readonly ITEM_HEIGHT = 22;

	getHeight(element: ExplorerItem): number {
		return ExplorerDelegate.ITEM_HEIGHT;
	}

	getTemplateId(element: ExplorerItem): string {
		return FilesRenderer.ID;
	}
}

63
export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | ExplorerItem[], ExplorerItem> {
E
Erich Gamma 已提交
64 65

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

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

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

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

			if (element instanceof ExplorerItem && element.isRoot) {
				if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
					// Single folder create a dummy explorer item to show error
					const placeholder = new ExplorerItem(element.resource, undefined, false);
					placeholder.isError = true;

					return [placeholder];
				}
			} else {
				// Do not show error for roots since we already use an explorer decoration to notify user
95 96
				this.notificationService.error(e);
			}
E
Erich Gamma 已提交
97

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

101
		this.progressService.showWhile(promise, this.layoutService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
102
		return promise;
E
Erich Gamma 已提交
103
	}
I
isidor 已提交
104
}
E
Erich Gamma 已提交
105

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

I
isidor 已提交
252
		return toDisposable(() => {
I
isidor 已提交
253 254 255 256
			if (!ignoreDisposeAndBlur) {
				blurDisposable.dispose();
				done(inputBox.isInputValid());
			}
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) } as CachedParsedExpression);
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());
I
isidor 已提交
327
		if (cached && cached.parsed(path.normalize(path.relative(stat.root.resource.path, stat.resource.path)), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) {
328
			// review (isidor): is path.normalize necessary? path.relative already returns an os path
E
Erich Gamma 已提交
329 330 331 332 333
			return false; // hidden through pattern
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

I
isidor 已提交
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

	private toDispose: IDisposable[];
	private dropEnabled: boolean;

	constructor(
		@INotificationService private notificationService: INotificationService,
		@IExplorerService private explorerService: IExplorerService,
		@IEditorService private editorService: IEditorService,
		@IDialogService private dialogService: IDialogService,
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IFileService private fileService: IFileService,
		@IConfigurationService private configurationService: IConfigurationService,
		@IInstantiationService private instantiationService: IInstantiationService,
		@ITextFileService private textFileService: ITextFileService,
		@IWindowService private windowService: IWindowService,
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
	) {
		this.toDispose = [];

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

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

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

		// Desktop DND
I
isidor 已提交
465
		if (fromDesktop && originalEvent.dataTransfer) {
I
isidor 已提交
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
			const types = originalEvent.dataTransfer.types;
			const typesArray: string[] = [];
			for (let i = 0; i < types.length; i++) {
				typesArray.push(types[i].toLowerCase()); // somehow the types are lowercase
			}

			if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) {
				return false;
			}
		}

		// Other-Tree DND
		else if (data instanceof ExternalElementsDragAndDropData) {
			return false;
		}

		// In-Explorer DND
		else {
I
isidor 已提交
484 485
			const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;

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

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

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

I
isidor 已提交
500
			if (items.some((source) => {
I
isidor 已提交
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
				if (source.isRoot && target instanceof ExplorerItem && !target.isRoot) {
					return true; // Root folder can not be moved to a non root file stat.
				}

				if (source.resource.toString() === target.resource.toString()) {
					return true; // Can not move anything onto itself
				}

				if (source.isRoot && target instanceof ExplorerItem && target.isRoot) {
					// Disable moving workspace roots in one another
					return false;
				}

				if (!isCopy && dirname(source.resource).toString() === target.resource.toString()) {
					return true; // Can not move a file to the same parent unless we copy
				}

				if (isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) {
					return true; // Can not move a parent folder into one of its children
				}

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

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

		// All (target = file/folder)
		else {
			if (target.isDirectory) {
				if (target.isReadonly) {
					return false;
				}

				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: true };
			}

			if (this.contextService.getWorkspace().folders.every(folder => folder.uri.toString() !== target.resource.toString())) {
				return { accept: true, bubble: TreeDragOverBubble.Up, effect };
			}
		}

		return false;
	}

J
Joao Moreno 已提交
551
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
552
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
553 554 555
			return null;
		}

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

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

		return elements[0].name;
	}

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

			// The only custom data transfer we set from the explorer is a file transfer
			// to be able to DND between multiple code file explorers across windows
I
isidor 已提交
575
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
576 577 578 579 580 581
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

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

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


	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
I
isidor 已提交
615
			const folders = result.filter(r => r.success && r.stat && r.stat.isDirectory).map(result => ({ uri: result.stat!.resource }));
I
isidor 已提交
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
			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)
638
			else if (target instanceof ExplorerItem) {
I
isidor 已提交
639 640 641 642 643 644 645 646 647 648 649
				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
650
			return this.fileService.resolveFile(target.resource).then(targetStat => {
I
isidor 已提交
651 652 653

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

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

					// 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.
689
							let revertPromise: Promise<ITextFileOperationResult | null> = Promise.resolve(null);
I
isidor 已提交
690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714
							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);
	}

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

		let confirmPromise: Promise<IConfirmationResult>;

		// Handle confirm setting
I
isidor 已提交
723
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
724 725
		if (confirmDragAndDrop) {
			confirmPromise = this.dialogService.confirm({
I
isidor 已提交
726 727 728 729
				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 已提交
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
				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 已提交
750
					const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
I
isidor 已提交
751
					return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined);
I
isidor 已提交
752 753 754 755 756 757 758
				}

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

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

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

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

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

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

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

I
isidor 已提交
796
			return this.fileService.copyFile(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwirte: false })).then(stat => {
I
isidor 已提交
797 798 799 800 801 802 803 804 805 806
				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 已提交
807 808 809 810
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841

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