explorerViewer.ts 32.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';
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';
J
Joao Moreno 已提交
18
import { 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
import { Emitter } from 'vs/base/common/event';
J
Joao Moreno 已提交
50 51 52
import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree';
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
I
isidor 已提交
53 54 55

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

I
isidor 已提交
56
	static readonly ITEM_HEIGHT = 22;
I
isidor 已提交
57 58 59 60 61 62 63 64 65 66

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

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

I
isidor 已提交
67
export const explorerRootErrorEmitter = new Emitter<URI>();
68
export class ExplorerDataSource implements IAsyncDataSource<ExplorerItem | ExplorerItem[], ExplorerItem> {
E
Erich Gamma 已提交
69 70

	constructor(
71
		@IProgressService private readonly progressService: IProgressService,
72 73 74
		@INotificationService private readonly notificationService: INotificationService,
		@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
		@IFileService private readonly fileService: IFileService,
75 76
		@IExplorerService private readonly explorerService: IExplorerService,
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
77
	) { }
E
Erich Gamma 已提交
78

79 80
	hasChildren(element: ExplorerItem | ExplorerItem[]): boolean {
		return Array.isArray(element) || element.isDirectory;
E
Erich Gamma 已提交
81 82
	}

83 84 85
	getChildren(element: ExplorerItem | ExplorerItem[]): Promise<ExplorerItem[]> {
		if (Array.isArray(element)) {
			return Promise.resolve(element);
86
		}
E
Erich Gamma 已提交
87

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

			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];
I
isidor 已提交
96 97
				} else {
					explorerRootErrorEmitter.fire(element.resource);
98 99 100
				}
			} else {
				// Do not show error for roots since we already use an explorer decoration to notify user
101 102
				this.notificationService.error(e);
			}
E
Erich Gamma 已提交
103

104 105
			return []; // we could not resolve any children because of an error
		});
E
Erich Gamma 已提交
106

107 108 109 110 111
		this.progressService.withProgress({
			location: ProgressLocation.Explorer,
			delay: this.layoutService.isRestored() ? 800 : 1200 // less ugly initial startup
		}, _progress => promise);

112
		return promise;
E
Erich Gamma 已提交
113
	}
I
isidor 已提交
114
}
E
Erich Gamma 已提交
115

I
isidor 已提交
116 117
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
118
	label: IResourceLabel;
I
isidor 已提交
119
	container: HTMLElement;
E
Erich Gamma 已提交
120 121
}

J
Joao Moreno 已提交
122
export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, FuzzyScore, IFileTemplateData>, IDisposable {
I
isidor 已提交
123
	static readonly ID = 'file';
124

125 126
	private config: IFilesConfiguration;
	private configListener: IDisposable;
E
Erich Gamma 已提交
127 128

	constructor(
B
Benjamin Pasero 已提交
129
		private labels: ResourceLabels,
J
Joao Moreno 已提交
130
		private updateWidth: (stat: ExplorerItem) => void,
131 132 133
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
I
isidor 已提交
134
		@IExplorerService private readonly explorerService: IExplorerService
E
Erich Gamma 已提交
135
	) {
136
		this.config = this.configurationService.getValue<IFilesConfiguration>();
137 138
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
139
				this.config = this.configurationService.getValue();
140 141 142 143
			}
		});
	}

I
isidor 已提交
144 145
	get templateId(): string {
		return FilesRenderer.ID;
146
	}
147

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

152
		return { elementDisposable, label, container };
153 154
	}

155
	renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
156
		templateData.elementDisposable.dispose();
157
		const stat = node.element;
158
		const editableData = this.explorerService.getEditableData(stat);
B
Benjamin Pasero 已提交
159

160 161
		// File Label
		if (!editableData) {
162
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
163
			templateData.elementDisposable = this.renderStat(stat, stat.name, node.filterData, templateData);
164
		}
165

166 167 168
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
169
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
170
		}
171 172
	}

J
Joao Moreno 已提交
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, [number, number, number]>, index: number, templateData: IFileTemplateData, height: number | undefined): void {
		templateData.elementDisposable.dispose();

		const stat = node.element.elements[node.element.elements.length - 1];
		const label = node.element.elements.map(e => e.name).join('/');

		templateData.elementDisposable = this.renderStat(stat, label, node.filterData, templateData);
	}

	private renderStat(stat: ExplorerItem, label: string, filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
		templateData.label.element.style.display = 'flex';
		const extraClasses = ['explorer-item'];
		if (this.explorerService.isCut(stat)) {
			extraClasses.push('cut');
		}

		templateData.label.setResource({ resource: stat.resource, name: label }, {
			fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
			extraClasses,
			fileDecorations: this.config.explorer.decorations,
			matches: createMatches(filterData)
		});

		return templateData.label.onDidRender(() => {
			try {
				this.updateWidth(stat);
			} catch (e) {
				// noop since the element might no longer be in the tree, no update of width necessery
			}
		});
	}

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

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

I
isidor 已提交
213
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
214 215
		const value = stat.name || '';

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

218
		// Input field for name
219
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
220
			validationOptions: {
221 222 223 224 225 226 227 228 229 230 231 232
				validation: (value) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

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

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

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

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

J
jeanp413 已提交
248
		const done = once((success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
249
			label.element.style.display = 'none';
250
			const value = inputBox.value;
I
isidor 已提交
251
			dispose(toDispose);
O
orange4glace 已提交
252
			label.element.remove();
J
jeanp413 已提交
253 254 255
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
J
Joao Moreno 已提交
256
		});
E
Erich Gamma 已提交
257

B
Benjamin Pasero 已提交
258
		const toDispose = [
J
Joao Moreno 已提交
259
			inputBox,
A
Cleanup  
Alex Dima 已提交
260
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
261
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
262
					if (inputBox.validate()) {
J
jeanp413 已提交
263
						done(true, true);
J
Joao Moreno 已提交
264
					}
A
Alexandru Dima 已提交
265
				} else if (e.equals(KeyCode.Escape)) {
J
jeanp413 已提交
266
					done(false, true);
J
Joao Moreno 已提交
267 268
				}
			}),
I
isidor 已提交
269
			DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
270
				done(inputBox.isInputValid(), true);
I
isidor 已提交
271
			}),
B
Benjamin Pasero 已提交
272 273
			label,
			styler
J
Joao Moreno 已提交
274
		];
275

I
isidor 已提交
276
		return toDisposable(() => {
J
jeanp413 已提交
277
			done(false, false);
I
isidor 已提交
278
		});
E
Erich Gamma 已提交
279 280
	}

281
	disposeElement?(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
282
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
283 284
	}

I
isidor 已提交
285 286 287
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
288
	}
I
isidor 已提交
289

I
isidor 已提交
290 291
	dispose(): void {
		this.configListener.dispose();
I
isidor 已提交
292
	}
E
Erich Gamma 已提交
293 294
}

I
isidor 已提交
295 296 297
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
298
	}
E
Erich Gamma 已提交
299 300
}

301 302 303 304 305
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

306
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
307
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
308
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
309

I
isidor 已提交
310
	constructor(
I
isidor 已提交
311 312 313
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
314
	) {
315
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
316
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
317 318
	}

I
isidor 已提交
319
	updateConfiguration(): boolean {
I
isidor 已提交
320
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
321
		this.contextService.getWorkspace().folders.forEach(folder => {
322
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
323 324 325 326
			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 已提交
327
				needsRefresh = !cached || !equals(cached.original, excludesConfig);
328 329
			}

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

332
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
I
isidor 已提交
333
		});
E
Erich Gamma 已提交
334 335 336 337

		return needsRefresh;
	}

338
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
339 340 341
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
342
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
343 344 345 346
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
347
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
348
		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 已提交
349 350 351 352 353
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
354 355

	public dispose(): void {
B
Benjamin Pasero 已提交
356
		dispose(this.workspaceFolderChangeListener);
B
Benjamin Pasero 已提交
357
	}
E
Erich Gamma 已提交
358 359
}

I
isidor 已提交
360
// // Explorer Sorter
I
isidor 已提交
361
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
362

I
isidor 已提交
363
	constructor(
I
isidor 已提交
364
		@IExplorerService private readonly explorerService: IExplorerService,
365
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
366
	) { }
I
isidor 已提交
367

I
isidor 已提交
368 369 370 371
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
372 373 374
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
375
			}
I
isidor 已提交
376

I
isidor 已提交
377 378
			return -1;
		}
I
isidor 已提交
379

I
isidor 已提交
380 381 382
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
383

I
isidor 已提交
384
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
385

I
isidor 已提交
386 387 388 389 390 391
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
392

I
isidor 已提交
393 394 395
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
396

I
isidor 已提交
397 398 399
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
400

I
isidor 已提交
401
				break;
I
isidor 已提交
402

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

I
isidor 已提交
408 409 410
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
411

I
isidor 已提交
412
				break;
I
isidor 已提交
413

I
isidor 已提交
414 415
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
416

I
isidor 已提交
417 418 419 420
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
421

I
isidor 已提交
422 423 424
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
425

I
isidor 已提交
426 427
				break;
		}
I
isidor 已提交
428

I
isidor 已提交
429 430 431 432
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
433

I
isidor 已提交
434 435
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
436
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
437
				}
I
isidor 已提交
438

I
isidor 已提交
439
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
440

I
isidor 已提交
441 442 443 444 445
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
446

I
isidor 已提交
447 448 449 450
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

	private toDispose: IDisposable[];
I
isidor 已提交
451
	private dropEnabled = false;
I
isidor 已提交
452 453 454 455 456 457 458 459 460 461 462 463

	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 已提交
464
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
I
isidor 已提交
465 466 467 468 469 470 471 472 473 474
	) {
		this.toDispose = [];

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

I
isidor 已提交
475
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
476 477 478 479 480 481
		if (!this.dropEnabled) {
			return false;
		}

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

		// Desktop DND
I
isidor 已提交
485
		if (fromDesktop && originalEvent.dataTransfer) {
I
isidor 已提交
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
			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 已提交
504 505
			const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;

I
isidor 已提交
506
			if (!target) {
H
Howard Hung 已提交
507
				// Dropping onto the empty area. Do not accept if items dragged are already
B
Benjamin Pasero 已提交
508
				// children of the root unless we are copying the file
509
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
510 511 512
					return false;
				}

I
isidor 已提交
513
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
514 515
			}

I
isidor 已提交
516
			if (!Array.isArray(items)) {
I
isidor 已提交
517 518 519
				return false;
			}

I
isidor 已提交
520
			if (items.some((source) => {
I
isidor 已提交
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
				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
				}

538
				if (isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
539 540 541 542 543 544 545 546 547 548 549
					return true; // Can not move a parent folder into one of its children
				}

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

		// All (target = model)
		if (!target) {
I
isidor 已提交
550
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570
		}

		// 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 已提交
571
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
572
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
573 574 575
			return null;
		}

I
isidor 已提交
576 577 578
		return element.resource.toString();
	}

I
isidor 已提交
579
	getDragLabel(elements: ExplorerItem[]): string | undefined {
I
isidor 已提交
580 581 582 583 584 585 586 587
		if (elements.length > 1) {
			return String(elements.length);
		}

		return elements[0].name;
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
I
isidor 已提交
588
		const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
I
isidor 已提交
589
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
590
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
591
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
592 593 594

			// 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 已提交
595
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
596 597 598 599 600 601
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
602
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
603 604 605 606
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
607
		if (!target.isDirectory && target.parent) {
608 609 610 611 612 613
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
614 615
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
I
isidor 已提交
616
			this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
617 618 619
		}
		// In-Explorer DND (Move/Copy file)
		else {
I
isidor 已提交
620
			this.handleExplorerDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
621 622 623
		}
	}

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

I
isidor 已提交
629 630
		// Pass focus to window
		this.windowService.focusWindow();
I
isidor 已提交
631

I
isidor 已提交
632 633 634
		// 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 已提交
635

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

649
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
650
			if (choice === buttons.length - 3) {
I
isidor 已提交
651 652
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
653
			if (choice === buttons.length - 2) {
I
isidor 已提交
654 655 656 657
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
658 659 660 661 662 663 664 665
		}

		// 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 已提交
666 667 668 669 670 671
	}

	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 已提交
672
			return this.fileService.resolve(target.resource).then(targetStat => {
I
isidor 已提交
673 674 675

				// Check for name collisions
				const targetNames = new Set<string>();
676
				if (targetStat.children) {
677
					const ignoreCase = hasToIgnoreCase(target.resource);
678
					targetStat.children.forEach(child => {
679
						targetNames.add(ignoreCase ? child.name : child.name.toLowerCase());
680 681
					});
				}
I
isidor 已提交
682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698

				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) {
699
						return [];
I
isidor 已提交
700 701 702 703 704 705 706 707 708 709 710 711
					}

					// 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.
712
							let revertPromise: Promise<ITextFileOperationResult | null> = Promise.resolve(null);
I
isidor 已提交
713 714 715 716 717 718
							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 已提交
719
								return this.fileService.copy(sourceFile, copyTarget, true).then(stat => {
I
isidor 已提交
720 721

									// if we only add one file, just open it directly
I
isidor 已提交
722
									if (resources.length === 1 && !stat.isDirectory) {
I
isidor 已提交
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
										this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
									}
								});
							});
						});
					});

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

		return Promise.resolve(undefined);
	}

I
isidor 已提交
738
	private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
739 740
		const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
741
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
742 743 744 745

		let confirmPromise: Promise<IConfirmationResult>;

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

		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 已提交
773
					const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
I
isidor 已提交
774
					return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined);
I
isidor 已提交
775 776 777 778 779 780 781
				}

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

782
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
783 784 785 786 787
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
788
		let targetIndex: number | undefined;
I
isidor 已提交
789 790 791 792 793
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
794 795
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
796 797
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
798
				targetIndex = index;
I
isidor 已提交
799 800 801 802 803 804 805 806
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
807
		if (targetIndex === undefined) {
I
isidor 已提交
808 809
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
810 811 812 813 814

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

815
	private doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
816 817
		// Reuse duplicate action if user copies
		if (isCopy) {
I
isidor 已提交
818 819
			const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
			return this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming)).then(stat => {
I
isidor 已提交
820 821 822 823 824 825 826 827 828 829
				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 已提交
830 831 832 833
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864

		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;
		});
	}
}
J
Joao Moreno 已提交
865 866 867 868 869 870 871

export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

	isIncompressible(element: ExplorerItem): boolean {
		return element.isRoot || !element.isDirectory;
	}
}