explorerViewer.ts 40.2 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 } 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';
J
Joao Moreno 已提交
55
import { isNumber } from 'vs/base/common/types';
56
import { domEvent } from 'vs/base/browser/event';
57
import { IEditableData } from 'vs/workbench/common/views';
I
isidor 已提交
58 59 60

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

I
isidor 已提交
61
	static readonly ITEM_HEIGHT = 22;
I
isidor 已提交
62 63 64 65 66 67 68 69 70 71

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

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

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

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

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

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

93
		const promise = element.fetchChildren(this.fileService, this.explorerService).then(undefined, e => {
94 95 96 97 98 99 100

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

109 110
			return []; // we could not resolve any children because of an error
		});
E
Erich Gamma 已提交
111

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

117
		return promise;
E
Erich Gamma 已提交
118
	}
I
isidor 已提交
119
}
E
Erich Gamma 已提交
120

121 122
export interface ICompressedNavigationController {
	readonly current: ExplorerItem;
123
	readonly items: ExplorerItem[];
124
	readonly labels: HTMLElement[];
125 126 127 128
	readonly index: number;
	readonly count: number;
	previous(): void;
	next(): void;
129 130
	first(): void;
	last(): void;
J
Joao Moreno 已提交
131
	setIndex(index: number): void;
132 133 134 135 136
}

export class CompressedNavigationController implements ICompressedNavigationController {

	private _index: number;
137
	readonly labels: HTMLElement[];
138 139 140 141 142

	get index(): number { return this._index; }
	get count(): number { return this.items.length; }
	get current(): ExplorerItem { return this.items[this._index]!; }

143
	constructor(readonly items: ExplorerItem[], templateData: IFileTemplateData) {
144 145 146 147 148 149 150 151 152 153
		this._index = items.length - 1;
		this.labels = Array.from(templateData.container.querySelectorAll('.label-name')) as HTMLElement[];
		DOM.addClass(this.labels[this._index], 'active');
	}

	previous(): void {
		if (this._index <= 0) {
			return;
		}

J
Joao Moreno 已提交
154
		this.setIndex(this._index - 1);
155 156 157 158 159 160 161
	}

	next(): void {
		if (this._index >= this.items.length - 1) {
			return;
		}

J
Joao Moreno 已提交
162
		this.setIndex(this._index + 1);
163
	}
164 165 166 167 168 169

	first(): void {
		if (this._index === 0) {
			return;
		}

J
Joao Moreno 已提交
170
		this.setIndex(0);
171 172 173 174 175 176 177
	}

	last(): void {
		if (this._index === this.items.length - 1) {
			return;
		}

J
Joao Moreno 已提交
178 179 180
		this.setIndex(this.items.length - 1);
	}

J
Joao Moreno 已提交
181 182 183 184 185
	setIndex(index: number): void {
		if (index < 0 || index >= this.items.length) {
			return;
		}

186
		DOM.removeClass(this.labels[this._index], 'active');
J
Joao Moreno 已提交
187
		this._index = index;
188 189
		DOM.addClass(this.labels[this._index], 'active');
	}
190 191
}

I
isidor 已提交
192 193
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
194
	label: IResourceLabel;
I
isidor 已提交
195
	container: HTMLElement;
E
Erich Gamma 已提交
196 197
}

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

201 202
	private config: IFilesConfiguration;
	private configListener: IDisposable;
203
	private compressedNavigationControllers = new Map<ExplorerItem, CompressedNavigationController>();
E
Erich Gamma 已提交
204 205

	constructor(
B
Benjamin Pasero 已提交
206
		private labels: ResourceLabels,
J
Joao Moreno 已提交
207
		private updateWidth: (stat: ExplorerItem) => void,
208 209 210
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
211 212
		@IExplorerService private readonly explorerService: IExplorerService,
		@ILabelService private readonly labelService: ILabelService
E
Erich Gamma 已提交
213
	) {
214
		this.config = this.configurationService.getValue<IFilesConfiguration>();
215 216
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
217
				this.config = this.configurationService.getValue();
218 219 220 221
			}
		});
	}

I
isidor 已提交
222 223
	get templateId(): string {
		return FilesRenderer.ID;
224
	}
225

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

230
		return { elementDisposable, label, container };
231 232
	}

233
	renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
234
		templateData.elementDisposable.dispose();
235
		const stat = node.element;
236
		const editableData = this.explorerService.getEditableData(stat);
B
Benjamin Pasero 已提交
237

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

240 241
		// File Label
		if (!editableData) {
242
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
243
			templateData.elementDisposable = this.renderStat(stat, stat.name, node.filterData, templateData);
244
		}
245

246 247 248
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
249
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
250
		}
251 252
	}

J
Joao Moreno 已提交
253
	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData, height: number | undefined): void {
J
Joao Moreno 已提交
254 255 256
		templateData.elementDisposable.dispose();

		const stat = node.element.elements[node.element.elements.length - 1];
I
isidor 已提交
257 258
		const editable = node.element.elements.filter(e => this.explorerService.isEditable(e));
		const editableData = editable.length === 0 ? undefined : this.explorerService.getEditableData(editable[0]);
J
Joao Moreno 已提交
259 260 261

		// File Label
		if (!editableData) {
J
Joao Moreno 已提交
262
			DOM.addClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
263
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
264 265

			const disposables = new DisposableStore();
I
isidor 已提交
266
			const label = node.element.elements.map(e => e.name);
J
Joao Moreno 已提交
267 268
			disposables.add(this.renderStat(stat, label, node.filterData, templateData));

269 270
			const compressedNavigationController = new CompressedNavigationController(node.element.elements, templateData);
			this.compressedNavigationControllers.set(stat, compressedNavigationController);
271 272 273 274 275 276 277 278 279 280 281 282

			domEvent(templateData.container, 'mousedown')(e => {
				const result = getIconLabelNameFromHTMLElement(e.target);

				if (result) {
					compressedNavigationController.setIndex(result.index);
				}
			}, undefined, disposables);

			disposables.add(toDisposable(() => {
				this.compressedNavigationControllers.delete(stat);
			}));
283

J
Joao Moreno 已提交
284
			templateData.elementDisposable = disposables;
J
Joao Moreno 已提交
285
		}
J
Joao Moreno 已提交
286

J
Joao Moreno 已提交
287 288
		// Input Box
		else {
J
Joao Moreno 已提交
289
			DOM.removeClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
290
			templateData.label.element.style.display = 'none';
I
isidor 已提交
291
			templateData.elementDisposable = this.renderInputBox(templateData.container, editable[0], editableData);
J
Joao Moreno 已提交
292
		}
J
Joao Moreno 已提交
293 294
	}

295
	private renderStat(stat: ExplorerItem, label: string | string[], filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
J
Joao Moreno 已提交
296 297 298 299 300 301 302 303 304 305
		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,
306 307
			matches: createMatches(filterData),
			separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority)
J
Joao Moreno 已提交
308 309 310 311 312 313 314 315 316 317 318
		});

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

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

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

I
isidor 已提交
327
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
328 329
		const value = stat.name || '';

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

J
Joao Moreno 已提交
332 333 334
		// hack: hide label
		(label.element.firstElementChild as HTMLElement).style.display = 'none';

335
		// Input field for name
336
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
337
			validationOptions: {
338 339 340 341 342 343 344 345 346 347 348 349
				validation: (value) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

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

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

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

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

J
jeanp413 已提交
365
		const done = once((success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
366
			label.element.style.display = 'none';
367
			const value = inputBox.value;
I
isidor 已提交
368
			dispose(toDispose);
O
orange4glace 已提交
369
			label.element.remove();
J
jeanp413 已提交
370 371 372
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
J
Joao Moreno 已提交
373
		});
E
Erich Gamma 已提交
374

B
Benjamin Pasero 已提交
375
		const toDispose = [
J
Joao Moreno 已提交
376
			inputBox,
A
Cleanup  
Alex Dima 已提交
377
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
378
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
379
					if (inputBox.validate()) {
J
jeanp413 已提交
380
						done(true, true);
J
Joao Moreno 已提交
381
					}
A
Alexandru Dima 已提交
382
				} else if (e.equals(KeyCode.Escape)) {
J
jeanp413 已提交
383
					done(false, true);
J
Joao Moreno 已提交
384 385
				}
			}),
I
isidor 已提交
386
			DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
387
				done(inputBox.isInputValid(), true);
I
isidor 已提交
388
			}),
B
Benjamin Pasero 已提交
389 390
			label,
			styler
J
Joao Moreno 已提交
391
		];
392

I
isidor 已提交
393
		return toDisposable(() => {
J
jeanp413 已提交
394
			done(false, false);
I
isidor 已提交
395
		});
E
Erich Gamma 已提交
396 397
	}

398
	disposeElement(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
399
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
400 401
	}

402
	disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
403
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
404 405
	}

I
isidor 已提交
406 407 408
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
409
	}
I
isidor 已提交
410

411 412 413 414
	getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined {
		return this.compressedNavigationControllers.get(stat);
	}

I
isidor 已提交
415 416
	dispose(): void {
		this.configListener.dispose();
I
isidor 已提交
417
	}
E
Erich Gamma 已提交
418 419
}

I
isidor 已提交
420 421 422
export class ExplorerAccessibilityProvider implements IAccessibilityProvider<ExplorerItem> {
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
423
	}
E
Erich Gamma 已提交
424 425
}

426 427 428 429 430
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

431
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
432
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
433
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
434

I
isidor 已提交
435
	constructor(
I
isidor 已提交
436 437 438
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
439
	) {
440
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
441
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
442 443
	}

I
isidor 已提交
444
	updateConfiguration(): boolean {
I
isidor 已提交
445
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
446
		this.contextService.getWorkspace().folders.forEach(folder => {
447
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
B
Benjamin Pasero 已提交
448
			const excludesConfig: glob.IExpression = configuration?.files?.exclude || Object.create(null);
449 450 451

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

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

457
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
I
isidor 已提交
458
		});
E
Erich Gamma 已提交
459 460 461 462

		return needsRefresh;
	}

463
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
464 465 466
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
467
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
468 469 470 471
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
472
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
473
		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 已提交
474 475 476 477 478
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
479 480

	public dispose(): void {
B
Benjamin Pasero 已提交
481
		dispose(this.workspaceFolderChangeListener);
B
Benjamin Pasero 已提交
482
	}
E
Erich Gamma 已提交
483 484
}

I
isidor 已提交
485
// // Explorer Sorter
I
isidor 已提交
486
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
487

I
isidor 已提交
488
	constructor(
I
isidor 已提交
489
		@IExplorerService private readonly explorerService: IExplorerService,
490
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
491
	) { }
I
isidor 已提交
492

I
isidor 已提交
493 494 495 496
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
497 498 499
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
500
			}
I
isidor 已提交
501

I
isidor 已提交
502 503
			return -1;
		}
I
isidor 已提交
504

I
isidor 已提交
505 506 507
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
508

I
isidor 已提交
509
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
510

I
isidor 已提交
511 512 513 514 515 516
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
517

I
isidor 已提交
518 519 520
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
521

I
isidor 已提交
522 523 524
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
525

I
isidor 已提交
526
				break;
I
isidor 已提交
527

I
isidor 已提交
528 529 530 531
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
532

I
isidor 已提交
533 534 535
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
536

I
isidor 已提交
537
				break;
I
isidor 已提交
538

I
isidor 已提交
539 540
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
541

I
isidor 已提交
542 543 544 545
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
546

I
isidor 已提交
547 548 549
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
550

I
isidor 已提交
551 552
				break;
		}
I
isidor 已提交
553

I
isidor 已提交
554 555 556 557
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
558

I
isidor 已提交
559 560
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
561
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
562
				}
I
isidor 已提交
563

I
isidor 已提交
564
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
565

I
isidor 已提交
566 567 568 569 570
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
571

I
isidor 已提交
572 573 574 575 576 577 578
const fileOverwriteConfirm = (name: string) => {
	return <IConfirmation>{
		message: localize('confirmOverwrite', "A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", name),
		detail: localize('irreversible', "This action is irreversible!"),
		primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
		type: 'warning'
	};
I
isidor 已提交
579 580
};

I
isidor 已提交
581 582 583
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

584 585 586
	private compressedDragOverElement: HTMLElement | undefined;
	private compressedDropTargetDisposable: IDisposable = Disposable.None;

I
isidor 已提交
587
	private toDispose: IDisposable[];
I
isidor 已提交
588
	private dropEnabled = false;
I
isidor 已提交
589 590 591 592 593 594 595 596 597 598 599

	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,
600
		@IHostService private hostService: IHostService,
I
isidor 已提交
601
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
I
isidor 已提交
602 603 604 605 606 607 608 609 610 611
	) {
		this.toDispose = [];

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

I
isidor 已提交
612
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
613 614 615 616
		if (!this.dropEnabled) {
			return false;
		}

617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
		// Compressed folders
		if (target) {
			const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent);

			if (compressedTarget) {
				const iconLabelName = getIconLabelNameFromHTMLElement(originalEvent.target);

				if (iconLabelName && iconLabelName.index < iconLabelName.count - 1) {
					const result = this._onDragOver(data, compressedTarget, targetIndex, originalEvent);

					if (result) {
						if (iconLabelName.element !== this.compressedDragOverElement) {
							this.compressedDragOverElement = iconLabelName.element;
							this.compressedDropTargetDisposable.dispose();
							this.compressedDropTargetDisposable = toDisposable(() => {
								DOM.removeClass(iconLabelName.element, 'drop-target');
								this.compressedDragOverElement = undefined;
							});

							DOM.addClass(iconLabelName.element, 'drop-target');
						}

						return typeof result === 'boolean' ? result : { ...result, feedback: [] };
					}

					this.compressedDropTargetDisposable.dispose();
					return false;
				}
			}
		}

		this.compressedDropTargetDisposable.dispose();
		return this._onDragOver(data, target, targetIndex, originalEvent);
	}

	private _onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
653 654
		const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
		const fromDesktop = data instanceof DesktopDragAndDropData;
I
isidor 已提交
655
		const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
I
isidor 已提交
656 657

		// Desktop DND
658 659
		if (fromDesktop) {
			if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) {
I
isidor 已提交
660 661 662 663 664 665 666 667 668 669 670
				return false;
			}
		}

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

		// In-Explorer DND
		else {
J
Joao Moreno 已提交
671
			const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>);
I
isidor 已提交
672

I
isidor 已提交
673
			if (!target) {
H
Howard Hung 已提交
674
				// Dropping onto the empty area. Do not accept if items dragged are already
B
Benjamin Pasero 已提交
675
				// children of the root unless we are copying the file
676
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
677 678 679
					return false;
				}

I
isidor 已提交
680
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
681 682
			}

I
isidor 已提交
683
			if (!Array.isArray(items)) {
I
isidor 已提交
684 685 686
				return false;
			}

I
isidor 已提交
687
			if (items.some((source) => {
I
isidor 已提交
688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
				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
				}

705
				if (isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
706 707 708 709 710 711 712 713 714 715 716
					return true; // Can not move a parent folder into one of its children
				}

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

		// All (target = model)
		if (!target) {
I
isidor 已提交
717
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
		}

		// 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 已提交
738
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
739
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
740 741 742
			return null;
		}

I
isidor 已提交
743 744 745
		return element.resource.toString();
	}

J
Joao Moreno 已提交
746 747 748 749
	getDragLabel(elements: ExplorerItem[], originalEvent: DragEvent): string | undefined {
		if (elements.length === 1) {
			const stat = FileDragAndDrop.getCompressedStatFromDragEvent(elements[0], originalEvent);
			return stat.name;
I
isidor 已提交
750 751
		}

J
Joao Moreno 已提交
752
		return String(elements.length);
I
isidor 已提交
753 754 755
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
J
Joao Moreno 已提交
756
		const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, originalEvent);
I
isidor 已提交
757
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
758
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
759
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
760 761 762

			// 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 已提交
763
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
764 765 766 767 768 769
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
770
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
771 772 773 774 775 776 777 778 779 780 781
		this.compressedDropTargetDisposable.dispose();

		// Find compressed target
		if (target) {
			const compressedTarget = FileDragAndDrop.getCompressedStatFromDragEvent(target, originalEvent);

			if (compressedTarget) {
				target = compressedTarget;
			}
		}

782 783 784 785
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
786
		if (!target.isDirectory && target.parent) {
787 788 789 790 791 792
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
793 794
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
J
Joao Moreno 已提交
795 796 797 798 799
			if (isWeb) {
				this.handleWebExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
			} else {
				this.handleExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
			}
I
isidor 已提交
800 801 802
		}
		// In-Explorer DND (Move/Copy file)
		else {
803
			this.handleExplorerDrop(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
804 805 806
		}
	}

J
Joao Moreno 已提交
807 808 809 810 811 812 813 814
	private async handleWebExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		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)) {
815
						const { confirmed } = await this.dialogService.confirm(fileOverwriteConfirm(name));
J
Joao Moreno 已提交
816 817
						if (!confirmed) {
							return;
I
isidor 已提交
818
						}
I
isidor 已提交
819 820
					}

J
Joao Moreno 已提交
821 822 823 824 825 826 827 828 829
					const resource = joinPath(target.resource, name);
					await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target?.result)));
					if (data.files.length === 1) {
						await this.editorService.openEditor({ resource, options: { pinned: true } });
					}
				}
			};
		});
	}
I
isidor 已提交
830

J
Joao Moreno 已提交
831
	private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
832 833
		const droppedResources = extractResources(originalEvent, true);
		// Check for dropped external files to be folders
I
isidor 已提交
834
		const result = await this.fileService.resolveAll(droppedResources);
I
isidor 已提交
835

I
isidor 已提交
836
		// Pass focus to window
837
		this.hostService.focus();
I
isidor 已提交
838

I
isidor 已提交
839 840 841
		// 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 已提交
842

I
isidor 已提交
843 844 845 846
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
847
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
848 849 850 851 852 853
			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 已提交
854
			}
I
isidor 已提交
855

856
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
857
			if (choice === buttons.length - 3) {
I
isidor 已提交
858 859
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
860
			if (choice === buttons.length - 2) {
I
isidor 已提交
861 862 863 864
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
865 866 867 868 869 870
		}

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

I
isidor 已提交
873
	private async addResources(target: ExplorerItem, resources: URI[]): Promise<void> {
I
isidor 已提交
874 875 876
		if (resources && resources.length > 0) {

			// Resolve target to check for name collisions and ask user
I
isidor 已提交
877 878 879 880 881 882 883
			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 已提交
884
					targetNames.add(ignoreCase ? child.name.toLowerCase() : child.name);
I
isidor 已提交
885 886
				});
			}
I
isidor 已提交
887

I
isidor 已提交
888 889
			const filtered = resources.filter(resource => targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase()));
			const resourceExists = filtered.length >= 1;
I
isidor 已提交
890
			if (resourceExists) {
I
isidor 已提交
891
				const confirmationResult = await this.dialogService.confirm(fileOverwriteConfirm(basename(filtered[0])));
I
isidor 已提交
892
				if (!confirmationResult.confirmed) {
I
isidor 已提交
893
					return;
I
isidor 已提交
894
				}
I
isidor 已提交
895
			}
I
isidor 已提交
896

I
isidor 已提交
897 898 899 900 901 902 903 904 905 906 907 908
			// 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 已提交
909 910
					}

I
isidor 已提交
911
					const copyTarget = joinPath(target.resource, basename(sourceFile));
I
isidor 已提交
912
					const stat = await this.textFileService.copy(sourceFile, copyTarget, true);
I
isidor 已提交
913 914 915 916
					// 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 已提交
917 918 919
				});
			});

I
isidor 已提交
920 921
			await sequence(addPromisesFactory);
		}
I
isidor 已提交
922 923
	}

924 925
	private async handleExplorerDrop(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data);
I
isidor 已提交
926
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
927
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
928 929

		// Handle confirm setting
I
isidor 已提交
930
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
931
		if (confirmDragAndDrop) {
I
isidor 已提交
932
			const confirmation = await this.dialogService.confirm({
I
isidor 已提交
933
				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 已提交
934
					: 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 已提交
935
						: 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 已提交
936
							: localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name),
I
isidor 已提交
937 938 939 940 941 942 943
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});

I
isidor 已提交
944 945 946
			if (!confirmation.confirmed) {
				return;
			}
I
isidor 已提交
947 948

			// Check for confirmation checkbox
I
isidor 已提交
949 950
			if (confirmation.checkboxChecked === true) {
				await this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
I
isidor 已提交
951
			}
I
isidor 已提交
952
		}
I
isidor 已提交
953

I
isidor 已提交
954 955
		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 已提交
956 957
	}

958
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
959 960 961 962 963
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
964
		let targetIndex: number | undefined;
I
isidor 已提交
965 966 967 968 969
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
970 971
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
972 973
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
974
				targetIndex = index;
I
isidor 已提交
975 976 977 978 979 980 981 982
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
983
		if (targetIndex === undefined) {
I
isidor 已提交
984 985
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
986 987 988 989 990

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

I
isidor 已提交
991
	private async doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
992 993
		// Reuse duplicate action if user copies
		if (isCopy) {
I
isidor 已提交
994
			const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
I
isidor 已提交
995
			const stat = await this.textFileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming));
I
isidor 已提交
996 997 998
			if (!stat.isDirectory) {
				await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
			}
I
isidor 已提交
999

I
isidor 已提交
1000
			return;
I
isidor 已提交
1001 1002 1003 1004
		}

		// Otherwise move
		const targetResource = joinPath(target.resource, source.name);
I
isidor 已提交
1005 1006 1007 1008
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
1009

I
isidor 已提交
1010 1011 1012
		try {
			await this.textFileService.move(source.resource, targetResource);
		} catch (error) {
I
isidor 已提交
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
			// 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 已提交
1023 1024 1025 1026 1027 1028
				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 已提交
1029
					}
I
isidor 已提交
1030
				}
I
isidor 已提交
1031 1032 1033 1034 1035
			}
			// Any other error
			else {
				this.notificationService.error(error);
			}
I
isidor 已提交
1036
		}
I
isidor 已提交
1037
	}
J
Joao Moreno 已提交
1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052

	private static getStatsFromDragAndDropData(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, dragStartEvent?: DragEvent): ExplorerItem[] {
		if (data.context) {
			return data.context;
		}

		// Detect compressed folder dragging
		if (dragStartEvent && data.elements.length === 1) {
			data.context = [FileDragAndDrop.getCompressedStatFromDragEvent(data.elements[0], dragStartEvent)];
			return data.context;
		}

		return data.elements;
	}

1053 1054
	private static getCompressedStatFromDragEvent(stat: ExplorerItem, dragEvent: DragEvent): ExplorerItem {
		const target = document.elementFromPoint(dragEvent.clientX, dragEvent.clientY);
J
Joao Moreno 已提交
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
		const iconLabelName = getIconLabelNameFromHTMLElement(target);

		if (iconLabelName) {
			const { count, index } = iconLabelName;

			let i = count - 1;
			while (i > index && stat.parent) {
				stat = stat.parent;
				i--;
			}

			return stat;
		}

		return stat;
	}
1071 1072 1073 1074

	onDragEnd(): void {
		this.compressedDropTargetDisposable.dispose();
	}
J
Joao Moreno 已提交
1075 1076
}

1077
function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement, count: number, index: number } | null {
J
Joao Moreno 已提交
1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097
	if (!(target instanceof HTMLElement)) {
		return null;
	}

	let element: HTMLElement | null = target;

	while (element && !DOM.hasClass(element, 'monaco-list-row')) {
		if (DOM.hasClass(element, 'label-name') && element.hasAttribute('data-icon-label-count')) {
			const count = Number(element.getAttribute('data-icon-label-count'));
			const index = Number(element.getAttribute('data-icon-label-index'));

			if (isNumber(count) && isNumber(index)) {
				return { element: element, count, index };
			}
		}

		element = element.parentElement;
	}

	return null;
I
isidor 已提交
1098
}
J
Joao Moreno 已提交
1099

J
Joao Moreno 已提交
1100 1101 1102 1103
export function isCompressedFolderName(target: HTMLElement | EventTarget | Element | null): boolean {
	return !!getIconLabelNameFromHTMLElement(target);
}

J
Joao Moreno 已提交
1104 1105
export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

1106
	isIncompressible(stat: ExplorerItem): boolean {
J
Joao Moreno 已提交
1107
		return stat.isRoot || !stat.isDirectory || stat instanceof NewExplorerItem;
J
Joao Moreno 已提交
1108 1109
	}
}