explorerViewer.ts 33.4 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';
J
Joao Moreno 已提交
15
import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } 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';
J
Joao Moreno 已提交
31
import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
I
isidor 已提交
32
import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers';
33
import { fillResourceDataTransfers, CodeDataTransfers, extractResources, containsDragType } from 'vs/workbench/browser/dnd';
I
isidor 已提交
34 35 36
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
import { isMacintosh, isWeb } from 'vs/base/common/platform';
I
isidor 已提交
39 40
import { IDialogService, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
41
import { IHostService } from 'vs/workbench/services/host/browser/host';
B
Benjamin Pasero 已提交
42
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
I
isidor 已提交
43 44 45 46
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
import { VSBuffer } from 'vs/base/common/buffer';
54
import { ILabelService } from 'vs/platform/label/common/label';
I
isidor 已提交
55 56 57

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

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

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

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

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

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

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

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

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

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

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

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

114
		return promise;
E
Erich Gamma 已提交
115
	}
I
isidor 已提交
116
}
E
Erich Gamma 已提交
117

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

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

127 128
	private config: IFilesConfiguration;
	private configListener: IDisposable;
E
Erich Gamma 已提交
129 130

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

I
isidor 已提交
147 148
	get templateId(): string {
		return FilesRenderer.ID;
149
	}
150

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

155
		return { elementDisposable, label, container };
156 157
	}

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

J
Joao Moreno 已提交
163 164
		DOM.removeClass(templateData.label.element, 'compressed');

165 166
		// File Label
		if (!editableData) {
167
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
168
			templateData.elementDisposable = this.renderStat(stat, stat.name, node.filterData, templateData);
169
		}
170

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

J
Joao Moreno 已提交
178
	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData, height: number | undefined): void {
J
Joao Moreno 已提交
179 180 181
		templateData.elementDisposable.dispose();

		const stat = node.element.elements[node.element.elements.length - 1];
182
		const label = node.element.elements.map(e => e.name);
J
Joao Moreno 已提交
183 184 185 186
		const editableData = this.explorerService.getEditableData(stat);

		// File Label
		if (!editableData) {
J
Joao Moreno 已提交
187
			DOM.addClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
188
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
189 190 191 192 193 194 195 196 197 198 199 200 201

			const disposables = new DisposableStore();
			disposables.add(this.renderStat(stat, label, node.filterData, templateData));


			// HACK IT IN
			// const nodes = templateData.label.element.querySelectorAll('.label-name');
			// nodes.forEach(labelName => {
			// 	addClass(labelName
			// });


			templateData.elementDisposable = disposables;
J
Joao Moreno 已提交
202
		}
J
Joao Moreno 已提交
203

J
Joao Moreno 已提交
204 205
		// Input Box
		else {
J
Joao Moreno 已提交
206
			DOM.removeClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
207 208 209
			templateData.label.element.style.display = 'none';
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
		}
J
Joao Moreno 已提交
210 211
	}

212
	private renderStat(stat: ExplorerItem, label: string | string[], filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
J
Joao Moreno 已提交
213 214 215 216 217 218 219 220 221 222
		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,
223 224
			matches: createMatches(filterData),
			separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority)
J
Joao Moreno 已提交
225 226 227 228 229 230 231 232 233 234 235
		});

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

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

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

I
isidor 已提交
244
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
245 246
		const value = stat.name || '';

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

249
		// Input field for name
250
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
251
			validationOptions: {
252 253 254 255 256 257 258 259 260 261 262 263
				validation: (value) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

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

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

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

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

J
jeanp413 已提交
279
		const done = once((success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
280
			label.element.style.display = 'none';
281
			const value = inputBox.value;
I
isidor 已提交
282
			dispose(toDispose);
O
orange4glace 已提交
283
			label.element.remove();
J
jeanp413 已提交
284 285 286
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
J
Joao Moreno 已提交
287
		});
E
Erich Gamma 已提交
288

B
Benjamin Pasero 已提交
289
		const toDispose = [
J
Joao Moreno 已提交
290
			inputBox,
A
Cleanup  
Alex Dima 已提交
291
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
292
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
293
					if (inputBox.validate()) {
J
jeanp413 已提交
294
						done(true, true);
J
Joao Moreno 已提交
295
					}
A
Alexandru Dima 已提交
296
				} else if (e.equals(KeyCode.Escape)) {
J
jeanp413 已提交
297
					done(false, true);
J
Joao Moreno 已提交
298 299
				}
			}),
I
isidor 已提交
300
			DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
301
				done(inputBox.isInputValid(), true);
I
isidor 已提交
302
			}),
B
Benjamin Pasero 已提交
303 304
			label,
			styler
J
Joao Moreno 已提交
305
		];
306

I
isidor 已提交
307
		return toDisposable(() => {
J
jeanp413 已提交
308
			done(false, false);
I
isidor 已提交
309
		});
E
Erich Gamma 已提交
310 311
	}

312
	disposeElement?(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
313
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
314 315
	}

I
isidor 已提交
316 317 318
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
319
	}
I
isidor 已提交
320

I
isidor 已提交
321 322
	dispose(): void {
		this.configListener.dispose();
I
isidor 已提交
323
	}
E
Erich Gamma 已提交
324 325
}

I
isidor 已提交
326 327 328
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
329
	}
E
Erich Gamma 已提交
330 331
}

332 333 334 335 336
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

337
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
338
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
339
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
340

I
isidor 已提交
341
	constructor(
I
isidor 已提交
342 343 344
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
345
	) {
346
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
347
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
348 349
	}

I
isidor 已提交
350
	updateConfiguration(): boolean {
I
isidor 已提交
351
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
352
		this.contextService.getWorkspace().folders.forEach(folder => {
353
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
B
Benjamin Pasero 已提交
354
			const excludesConfig: glob.IExpression = configuration?.files?.exclude || Object.create(null);
355 356 357

			if (!needsRefresh) {
				const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString());
I
isidor 已提交
358
				needsRefresh = !cached || !equals(cached.original, excludesConfig);
359 360
			}

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

363
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
I
isidor 已提交
364
		});
E
Erich Gamma 已提交
365 366 367 368

		return needsRefresh;
	}

369
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
370 371 372
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
373
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
374 375 376 377
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
378
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
379
		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 已提交
380 381 382 383 384
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
385 386

	public dispose(): void {
B
Benjamin Pasero 已提交
387
		dispose(this.workspaceFolderChangeListener);
B
Benjamin Pasero 已提交
388
	}
E
Erich Gamma 已提交
389 390
}

I
isidor 已提交
391
// // Explorer Sorter
I
isidor 已提交
392
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
393

I
isidor 已提交
394
	constructor(
I
isidor 已提交
395
		@IExplorerService private readonly explorerService: IExplorerService,
396
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
397
	) { }
I
isidor 已提交
398

I
isidor 已提交
399 400 401 402
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
403 404 405
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
406
			}
I
isidor 已提交
407

I
isidor 已提交
408 409
			return -1;
		}
I
isidor 已提交
410

I
isidor 已提交
411 412 413
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
414

I
isidor 已提交
415
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
416

I
isidor 已提交
417 418 419 420 421 422
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
423

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

I
isidor 已提交
428 429 430
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
431

I
isidor 已提交
432
				break;
I
isidor 已提交
433

I
isidor 已提交
434 435 436 437
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
438

I
isidor 已提交
439 440 441
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
442

I
isidor 已提交
443
				break;
I
isidor 已提交
444

I
isidor 已提交
445 446
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
447

I
isidor 已提交
448 449 450 451
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
452

I
isidor 已提交
453 454 455
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
456

I
isidor 已提交
457 458
				break;
		}
I
isidor 已提交
459

I
isidor 已提交
460 461 462 463
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
464

I
isidor 已提交
465 466
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
467
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
468
				}
I
isidor 已提交
469

I
isidor 已提交
470
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
471

I
isidor 已提交
472 473 474 475 476
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
477

I
isidor 已提交
478 479 480 481 482 483 484
const fileOverwriteConfirm: 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'
};

I
isidor 已提交
485 486 487 488
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

	private toDispose: IDisposable[];
I
isidor 已提交
489
	private dropEnabled = false;
I
isidor 已提交
490 491 492 493 494 495 496 497 498 499 500

	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,
501
		@IHostService private hostService: IHostService,
I
isidor 已提交
502
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
I
isidor 已提交
503 504 505 506 507 508 509 510 511 512
	) {
		this.toDispose = [];

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

I
isidor 已提交
513
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
514 515 516 517 518 519
		if (!this.dropEnabled) {
			return false;
		}

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

		// Desktop DND
523 524
		if (fromDesktop) {
			if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) {
I
isidor 已提交
525 526 527 528 529 530 531 532 533 534 535
				return false;
			}
		}

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

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

I
isidor 已提交
538
			if (!target) {
H
Howard Hung 已提交
539
				// Dropping onto the empty area. Do not accept if items dragged are already
B
Benjamin Pasero 已提交
540
				// children of the root unless we are copying the file
541
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
542 543 544
					return false;
				}

I
isidor 已提交
545
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
546 547
			}

I
isidor 已提交
548
			if (!Array.isArray(items)) {
I
isidor 已提交
549 550 551
				return false;
			}

I
isidor 已提交
552
			if (items.some((source) => {
I
isidor 已提交
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569
				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
				}

570
				if (isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
571 572 573 574 575 576 577 578 579 580 581
					return true; // Can not move a parent folder into one of its children
				}

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

		// All (target = model)
		if (!target) {
I
isidor 已提交
582
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602
		}

		// 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 已提交
603
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
604
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
605 606 607
			return null;
		}

I
isidor 已提交
608 609 610
		return element.resource.toString();
	}

I
isidor 已提交
611
	getDragLabel(elements: ExplorerItem[]): string | undefined {
I
isidor 已提交
612 613 614 615 616 617 618 619
		if (elements.length > 1) {
			return String(elements.length);
		}

		return elements[0].name;
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
I
isidor 已提交
620
		const items = (data as ElementsDragAndDropData<ExplorerItem>).elements;
I
isidor 已提交
621
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
622
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
623
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
624 625 626

			// 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 已提交
627
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
628 629 630 631 632 633
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
634
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
635 636 637 638
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
639
		if (!target.isDirectory && target.parent) {
640 641 642 643 644 645
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
646 647
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
I
isidor 已提交
648
			this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
649 650 651
		}
		// In-Explorer DND (Move/Copy file)
		else {
I
isidor 已提交
652
			this.handleExplorerDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
653 654 655
		}
	}

I
isidor 已提交
656
	private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672
		if (isWeb) {
			data.files.forEach(file => {
				const reader = new FileReader();
				reader.readAsArrayBuffer(file);
				reader.onload = async (event) => {
					const name = file.name;
					if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) {
						if (target.getChild(name)) {
							const { confirmed } = await this.dialogService.confirm(fileOverwriteConfirm);
							if (!confirmed) {
								return;
							}
						}

						const resource = joinPath(target.resource, name);
						await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target?.result)));
I
isidor 已提交
673 674 675
						if (data.files.length === 1) {
							await this.editorService.openEditor({ resource, options: { pinned: true } });
						}
I
isidor 已提交
676 677 678 679 680 681 682
					}
				};
			});

			return;
		}

I
isidor 已提交
683 684
		const droppedResources = extractResources(originalEvent, true);
		// Check for dropped external files to be folders
I
isidor 已提交
685
		const result = await this.fileService.resolveAll(droppedResources);
I
isidor 已提交
686

I
isidor 已提交
687
		// Pass focus to window
688
		this.hostService.focus();
I
isidor 已提交
689

I
isidor 已提交
690 691 692
		// 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 已提交
693

I
isidor 已提交
694 695 696 697
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
698
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
699 700 701 702 703 704
			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 已提交
705
			}
I
isidor 已提交
706

707
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
708
			if (choice === buttons.length - 3) {
I
isidor 已提交
709 710
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
711
			if (choice === buttons.length - 2) {
I
isidor 已提交
712 713 714 715
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
716 717 718 719 720 721
		}

		// Handle dropped files (only support FileStat as target)
		else if (target instanceof ExplorerItem) {
			return this.addResources(target, droppedResources.map(res => res.resource));
		}
I
isidor 已提交
722 723
	}

I
isidor 已提交
724
	private async addResources(target: ExplorerItem, resources: URI[]): Promise<void> {
I
isidor 已提交
725 726 727
		if (resources && resources.length > 0) {

			// Resolve target to check for name collisions and ask user
I
isidor 已提交
728 729 730 731 732 733 734
			const targetStat = await this.fileService.resolve(target.resource);

			// Check for name collisions
			const targetNames = new Set<string>();
			if (targetStat.children) {
				const ignoreCase = hasToIgnoreCase(target.resource);
				targetStat.children.forEach(child => {
I
isidor 已提交
735
					targetNames.add(ignoreCase ? child.name.toLowerCase() : child.name);
I
isidor 已提交
736 737
				});
			}
I
isidor 已提交
738

I
isidor 已提交
739 740
			const resourceExists = resources.some(resource => targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase()));
			if (resourceExists) {
I
isidor 已提交
741
				const confirmationResult = await this.dialogService.confirm(fileOverwriteConfirm);
I
isidor 已提交
742
				if (!confirmationResult.confirmed) {
I
isidor 已提交
743
					return;
I
isidor 已提交
744
				}
I
isidor 已提交
745
			}
I
isidor 已提交
746

I
isidor 已提交
747 748 749 750 751 752 753 754 755 756 757 758
			// Run add in sequence
			const addPromisesFactory: ITask<Promise<void>>[] = [];
			resources.forEach(resource => {
				addPromisesFactory.push(async () => {
					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.
					if (this.textFileService.isDirty(targetFile)) {
						await this.textFileService.revertAll([targetFile], { soft: true });
I
isidor 已提交
759 760
					}

I
isidor 已提交
761 762 763 764 765 766
					const copyTarget = joinPath(target.resource, basename(sourceFile));
					const stat = await this.fileService.copy(sourceFile, copyTarget, true);
					// if we only add one file, just open it directly
					if (resources.length === 1 && !stat.isDirectory) {
						this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
					}
I
isidor 已提交
767 768 769
				});
			});

I
isidor 已提交
770 771
			await sequence(addPromisesFactory);
		}
I
isidor 已提交
772 773
	}

I
isidor 已提交
774
	private async handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
775 776
		const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
777
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
778 779

		// Handle confirm setting
I
isidor 已提交
780
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
781
		if (confirmDragAndDrop) {
I
isidor 已提交
782
			const confirmation = await this.dialogService.confirm({
I
isidor 已提交
783
				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?")
I
isidor 已提交
784
					: items.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files into '{1}'?", items.length, target.name), items.map(s => s.resource))
I
isidor 已提交
785
						: items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name)
I
isidor 已提交
786
							: localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name),
I
isidor 已提交
787 788 789 790 791 792 793
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});

I
isidor 已提交
794 795 796
			if (!confirmation.confirmed) {
				return;
			}
I
isidor 已提交
797 798

			// Check for confirmation checkbox
I
isidor 已提交
799 800
			if (confirmation.checkboxChecked === true) {
				await this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
I
isidor 已提交
801
			}
I
isidor 已提交
802
		}
I
isidor 已提交
803

I
isidor 已提交
804 805
		const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target);
		await Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise));
I
isidor 已提交
806 807
	}

808
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
809 810 811 812 813
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
814
		let targetIndex: number | undefined;
I
isidor 已提交
815 816 817 818 819
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
820 821
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
822 823
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
824
				targetIndex = index;
I
isidor 已提交
825 826 827 828 829 830 831 832
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
833
		if (targetIndex === undefined) {
I
isidor 已提交
834 835
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
836 837 838 839 840

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

I
isidor 已提交
841
	private async doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
842 843
		// Reuse duplicate action if user copies
		if (isCopy) {
I
isidor 已提交
844
			const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
I
isidor 已提交
845 846 847 848
			const stat = await this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming));
			if (!stat.isDirectory) {
				await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
			}
I
isidor 已提交
849

I
isidor 已提交
850
			return;
I
isidor 已提交
851 852 853 854
		}

		// Otherwise move
		const targetResource = joinPath(target.resource, source.name);
I
isidor 已提交
855 856 857 858
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
859

I
isidor 已提交
860 861 862
		try {
			await this.textFileService.move(source.resource, targetResource);
		} catch (error) {
I
isidor 已提交
863 864 865 866 867 868 869 870 871 872
			// 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
I
isidor 已提交
873 874 875 876 877 878
				const { confirmed } = await this.dialogService.confirm(confirm);
				if (confirmed) {
					try {
						await this.textFileService.move(source.resource, targetResource, true /* overwrite */);
					} catch (error) {
						this.notificationService.error(error);
I
isidor 已提交
879
					}
I
isidor 已提交
880
				}
I
isidor 已提交
881 882 883 884 885
			}
			// Any other error
			else {
				this.notificationService.error(error);
			}
I
isidor 已提交
886
		}
I
isidor 已提交
887 888
	}
}
J
Joao Moreno 已提交
889 890 891

export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

892
	isIncompressible(stat: ExplorerItem): boolean {
J
Joao Moreno 已提交
893
		return stat.isRoot || !stat.isDirectory || stat instanceof NewExplorerItem;
J
Joao Moreno 已提交
894 895
	}
}