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

I
isidor 已提交
6
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
7 8
import * as DOM from 'vs/base/browser/dom';
import * as glob from 'vs/base/common/glob';
I
isidor 已提交
9
import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
10
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
I
isidor 已提交
11
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
12
import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
13
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
I
isidor 已提交
14
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
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';
J
Joao Moreno 已提交
49
import { Emitter, Event, EventMultiplexer } 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;
J
Joao Moreno 已提交
123
	readonly currentId: string;
124
	readonly items: ExplorerItem[];
125
	readonly labels: HTMLElement[];
126 127
	readonly index: number;
	readonly count: number;
J
Joao Moreno 已提交
128
	readonly onDidChange: Event<void>;
129 130
	previous(): void;
	next(): void;
131 132
	first(): void;
	last(): void;
J
Joao Moreno 已提交
133
	setIndex(index: number): void;
134 135
}

J
Joao Moreno 已提交
136 137 138
export class CompressedNavigationController implements ICompressedNavigationController, IDisposable {

	static ID = 0;
139 140

	private _index: number;
J
jeanp413 已提交
141 142
	private _labels!: HTMLElement[];
	private _updateLabelDisposable: IDisposable;
143 144 145 146

	get index(): number { return this._index; }
	get count(): number { return this.items.length; }
	get current(): ExplorerItem { return this.items[this._index]!; }
J
Joao Moreno 已提交
147
	get currentId(): string { return `${this.id}_${this.index}`; }
J
jeanp413 已提交
148
	get labels(): HTMLElement[] { return this._labels; }
J
Joao Moreno 已提交
149 150 151

	private _onDidChange = new Emitter<void>();
	readonly onDidChange = this._onDidChange.event;
152

J
Joao Moreno 已提交
153
	constructor(private id: string, readonly items: ExplorerItem[], templateData: IFileTemplateData) {
154
		this._index = items.length - 1;
J
Joao Moreno 已提交
155

J
jeanp413 已提交
156 157 158 159 160 161 162 163 164
		this.updateLabels(templateData);
		this._updateLabelDisposable = templateData.label.onDidRender(() => this.updateLabels(templateData));
	}

	private updateLabels(templateData: IFileTemplateData): void {
		this._labels = Array.from(templateData.container.querySelectorAll('.label-name')) as HTMLElement[];

		for (let i = 0; i < this.items.length; i++) {
			this.labels[i].setAttribute('aria-label', this.items[i].name);
J
Joao Moreno 已提交
165 166
		}

167 168 169 170 171 172 173 174
		DOM.addClass(this.labels[this._index], 'active');
	}

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

J
Joao Moreno 已提交
175
		this.setIndex(this._index - 1);
176 177 178 179 180 181 182
	}

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

J
Joao Moreno 已提交
183
		this.setIndex(this._index + 1);
184
	}
185 186 187 188 189 190

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

J
Joao Moreno 已提交
191
		this.setIndex(0);
192 193 194 195 196 197 198
	}

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

J
Joao Moreno 已提交
199 200 201
		this.setIndex(this.items.length - 1);
	}

J
Joao Moreno 已提交
202 203 204 205 206
	setIndex(index: number): void {
		if (index < 0 || index >= this.items.length) {
			return;
		}

207
		DOM.removeClass(this.labels[this._index], 'active');
J
Joao Moreno 已提交
208
		this._index = index;
209
		DOM.addClass(this.labels[this._index], 'active');
J
Joao Moreno 已提交
210 211 212 213 214 215

		this._onDidChange.fire();
	}

	dispose(): void {
		this._onDidChange.dispose();
J
jeanp413 已提交
216
		this._updateLabelDisposable.dispose();
217
	}
218 219
}

I
isidor 已提交
220 221
export interface IFileTemplateData {
	elementDisposable: IDisposable;
B
Benjamin Pasero 已提交
222
	label: IResourceLabel;
I
isidor 已提交
223
	container: HTMLElement;
E
Erich Gamma 已提交
224 225
}

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

229 230
	private config: IFilesConfiguration;
	private configListener: IDisposable;
231
	private compressedNavigationControllers = new Map<ExplorerItem, CompressedNavigationController>();
E
Erich Gamma 已提交
232

J
Joao Moreno 已提交
233 234 235
	private _onDidChangeActiveDescendant = new EventMultiplexer<void>();
	readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event;

E
Erich Gamma 已提交
236
	constructor(
B
Benjamin Pasero 已提交
237
		private labels: ResourceLabels,
J
Joao Moreno 已提交
238
		private updateWidth: (stat: ExplorerItem) => void,
239 240 241
		@IContextViewService private readonly contextViewService: IContextViewService,
		@IThemeService private readonly themeService: IThemeService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
242 243
		@IExplorerService private readonly explorerService: IExplorerService,
		@ILabelService private readonly labelService: ILabelService
E
Erich Gamma 已提交
244
	) {
245
		this.config = this.configurationService.getValue<IFilesConfiguration>();
246 247
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
248
				this.config = this.configurationService.getValue();
249 250 251 252
			}
		});
	}

I
isidor 已提交
253 254
	get templateId(): string {
		return FilesRenderer.ID;
255
	}
256

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

261
		return { elementDisposable, label, container };
262 263
	}

264
	renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
265
		templateData.elementDisposable.dispose();
266
		const stat = node.element;
267
		const editableData = this.explorerService.getEditableData(stat);
B
Benjamin Pasero 已提交
268

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

271 272
		// File Label
		if (!editableData) {
273
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
274
			templateData.elementDisposable = this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
275
		}
276

277 278 279
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
280
			templateData.elementDisposable = this.renderInputBox(templateData.container, stat, editableData);
281
		}
282 283
	}

J
Joao Moreno 已提交
284
	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData, height: number | undefined): void {
J
Joao Moreno 已提交
285 286 287
		templateData.elementDisposable.dispose();

		const stat = node.element.elements[node.element.elements.length - 1];
I
isidor 已提交
288 289
		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 已提交
290 291 292

		// File Label
		if (!editableData) {
J
Joao Moreno 已提交
293
			DOM.addClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
294
			templateData.label.element.style.display = 'flex';
J
Joao Moreno 已提交
295 296

			const disposables = new DisposableStore();
J
Joao Moreno 已提交
297 298
			const id = `compressed-explorer_${CompressedNavigationController.ID++}`;

I
isidor 已提交
299
			const label = node.element.elements.map(e => e.name);
J
Joao Moreno 已提交
300
			disposables.add(this.renderStat(stat, label, id, node.filterData, templateData));
J
Joao Moreno 已提交
301

J
Joao Moreno 已提交
302 303
			const compressedNavigationController = new CompressedNavigationController(id, node.element.elements, templateData);
			disposables.add(compressedNavigationController);
304
			this.compressedNavigationControllers.set(stat, compressedNavigationController);
305

J
Joao Moreno 已提交
306 307 308
			// accessibility
			disposables.add(this._onDidChangeActiveDescendant.add(compressedNavigationController.onDidChange));

309 310 311 312 313 314 315 316
			domEvent(templateData.container, 'mousedown')(e => {
				const result = getIconLabelNameFromHTMLElement(e.target);

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

J
Joao Moreno 已提交
317
			disposables.add(toDisposable(() => this.compressedNavigationControllers.delete(stat)));
318

J
Joao Moreno 已提交
319
			templateData.elementDisposable = disposables;
J
Joao Moreno 已提交
320
		}
J
Joao Moreno 已提交
321

J
Joao Moreno 已提交
322 323
		// Input Box
		else {
J
Joao Moreno 已提交
324
			DOM.removeClass(templateData.label.element, 'compressed');
J
Joao Moreno 已提交
325
			templateData.label.element.style.display = 'none';
I
isidor 已提交
326
			templateData.elementDisposable = this.renderInputBox(templateData.container, editable[0], editableData);
J
Joao Moreno 已提交
327
		}
J
Joao Moreno 已提交
328 329
	}

J
Joao Moreno 已提交
330
	private renderStat(stat: ExplorerItem, label: string | string[], domId: string | undefined, filterData: FuzzyScore | undefined, templateData: IFileTemplateData): IDisposable {
J
Joao Moreno 已提交
331 332 333 334 335 336 337 338 339 340
		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,
341
			matches: createMatches(filterData),
J
Joao Moreno 已提交
342 343
			separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
			domId
J
Joao Moreno 已提交
344 345 346 347 348 349 350 351 352 353 354
		});

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

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

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

I
isidor 已提交
363
		const parent = stat.name ? dirname(stat.resource) : stat.resource;
364 365
		const value = stat.name || '';

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

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

371
		// Input field for name
372
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
373
			validationOptions: {
374 375 376 377 378 379 380 381 382 383 384 385
				validation: (value) => {
					const content = editableData.validationMessage(value);
					if (!content) {
						return null;
					}

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

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

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

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

J
jeanp413 已提交
401
		const done = once((success: boolean, finishEditing: boolean) => {
T
Till Salinger 已提交
402
			label.element.style.display = 'none';
403
			const value = inputBox.value;
I
isidor 已提交
404
			dispose(toDispose);
O
orange4glace 已提交
405
			label.element.remove();
J
jeanp413 已提交
406 407 408
			if (finishEditing) {
				editableData.onFinish(value, success);
			}
J
Joao Moreno 已提交
409
		});
E
Erich Gamma 已提交
410

B
Benjamin Pasero 已提交
411
		const toDispose = [
J
Joao Moreno 已提交
412
			inputBox,
A
Cleanup  
Alex Dima 已提交
413
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
414
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
415
					if (inputBox.validate()) {
J
jeanp413 已提交
416
						done(true, true);
J
Joao Moreno 已提交
417
					}
A
Alexandru Dima 已提交
418
				} else if (e.equals(KeyCode.Escape)) {
J
jeanp413 已提交
419
					done(false, true);
J
Joao Moreno 已提交
420 421
				}
			}),
I
isidor 已提交
422
			DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
423
				done(inputBox.isInputValid(), true);
I
isidor 已提交
424
			}),
B
Benjamin Pasero 已提交
425 426
			label,
			styler
J
Joao Moreno 已提交
427
		];
428

I
isidor 已提交
429
		return toDisposable(() => {
J
jeanp413 已提交
430
			done(false, false);
I
isidor 已提交
431
		});
E
Erich Gamma 已提交
432 433
	}

434
	disposeElement(element: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
435
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
436 437
	}

438
	disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<ExplorerItem>, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
439
		templateData.elementDisposable.dispose();
E
Erich Gamma 已提交
440 441
	}

I
isidor 已提交
442 443 444
	disposeTemplate(templateData: IFileTemplateData): void {
		templateData.elementDisposable.dispose();
		templateData.label.dispose();
E
Erich Gamma 已提交
445
	}
I
isidor 已提交
446

447 448 449 450
	getCompressedNavigationController(stat: ExplorerItem): ICompressedNavigationController | undefined {
		return this.compressedNavigationControllers.get(stat);
	}

J
Joao Moreno 已提交
451
	// IAccessibilityProvider
E
Erich Gamma 已提交
452

I
isidor 已提交
453 454
	getAriaLabel(element: ExplorerItem): string {
		return element.name;
B
Benjamin Pasero 已提交
455
	}
J
Joao Moreno 已提交
456 457 458 459 460 461 462 463 464

	getActiveDescendantId(stat: ExplorerItem): string | undefined {
		const compressedNavigationController = this.compressedNavigationControllers.get(stat);
		return compressedNavigationController?.currentId;
	}

	dispose(): void {
		this.configListener.dispose();
	}
E
Erich Gamma 已提交
465 466
}

467 468 469 470 471
interface CachedParsedExpression {
	original: glob.IExpression;
	parsed: glob.ParsedExpression;
}

472
export class FilesFilter implements ITreeFilter<ExplorerItem, FuzzyScore> {
473
	private hiddenExpressionPerRoot: Map<string, CachedParsedExpression>;
B
Benjamin Pasero 已提交
474
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
475

I
isidor 已提交
476
	constructor(
I
isidor 已提交
477 478 479
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IExplorerService private readonly explorerService: IExplorerService
I
isidor 已提交
480
	) {
481
		this.hiddenExpressionPerRoot = new Map<string, CachedParsedExpression>();
B
Benjamin Pasero 已提交
482
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
483 484
	}

I
isidor 已提交
485
	updateConfiguration(): boolean {
I
isidor 已提交
486
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
487
		this.contextService.getWorkspace().folders.forEach(folder => {
488
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
B
Benjamin Pasero 已提交
489
			const excludesConfig: glob.IExpression = configuration?.files?.exclude || Object.create(null);
490 491 492

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

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

498
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) });
I
isidor 已提交
499
		});
E
Erich Gamma 已提交
500 501 502 503

		return needsRefresh;
	}

504
	filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
I
isidor 已提交
505 506 507
		if (parentVisibility === TreeVisibility.Hidden) {
			return false;
		}
I
isidor 已提交
508
		if (this.explorerService.getEditableData(stat) || stat.isRoot) {
E
Erich Gamma 已提交
509 510 511 512
			return true; // always visible
		}

		// Hide those that match Hidden Patterns
513
		const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString());
514
		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 已提交
515 516 517 518 519
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
520 521

	public dispose(): void {
B
Benjamin Pasero 已提交
522
		dispose(this.workspaceFolderChangeListener);
B
Benjamin Pasero 已提交
523
	}
E
Erich Gamma 已提交
524 525
}

I
isidor 已提交
526
// // Explorer Sorter
I
isidor 已提交
527
export class FileSorter implements ITreeSorter<ExplorerItem> {
I
isidor 已提交
528

I
isidor 已提交
529
	constructor(
I
isidor 已提交
530
		@IExplorerService private readonly explorerService: IExplorerService,
531
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
I
isidor 已提交
532
	) { }
I
isidor 已提交
533

I
isidor 已提交
534 535 536 537
	public compare(statA: ExplorerItem, statB: ExplorerItem): number {
		// Do not sort roots
		if (statA.isRoot) {
			if (statB.isRoot) {
I
isidor 已提交
538 539 540
				const workspaceA = this.contextService.getWorkspaceFolder(statA.resource);
				const workspaceB = this.contextService.getWorkspaceFolder(statB.resource);
				return workspaceA && workspaceB ? (workspaceA.index - workspaceB.index) : -1;
I
isidor 已提交
541
			}
I
isidor 已提交
542

I
isidor 已提交
543 544
			return -1;
		}
I
isidor 已提交
545

I
isidor 已提交
546 547 548
		if (statB.isRoot) {
			return 1;
		}
I
isidor 已提交
549

I
isidor 已提交
550
		const sortOrder = this.explorerService.sortOrder;
I
isidor 已提交
551

I
isidor 已提交
552 553 554 555 556 557
		// Sort Directories
		switch (sortOrder) {
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
558

I
isidor 已提交
559 560 561
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
562

I
isidor 已提交
563 564 565
				if (statA.isDirectory && statB.isDirectory) {
					return compareFileNames(statA.name, statB.name);
				}
I
isidor 已提交
566

I
isidor 已提交
567
				break;
I
isidor 已提交
568

I
isidor 已提交
569 570 571 572
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
I
isidor 已提交
573

I
isidor 已提交
574 575 576
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
I
isidor 已提交
577

I
isidor 已提交
578
				break;
I
isidor 已提交
579

I
isidor 已提交
580 581
			case 'mixed':
				break; // not sorting when "mixed" is on
I
isidor 已提交
582

I
isidor 已提交
583 584 585 586
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}
I
isidor 已提交
587

I
isidor 已提交
588 589 590
				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}
I
isidor 已提交
591

I
isidor 已提交
592 593
				break;
		}
I
isidor 已提交
594

I
isidor 已提交
595 596 597 598
		// Sort Files
		switch (sortOrder) {
			case 'type':
				return compareFileExtensions(statA.name, statB.name);
I
isidor 已提交
599

I
isidor 已提交
600 601
			case 'modified':
				if (statA.mtime !== statB.mtime) {
I
isidor 已提交
602
					return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1;
I
isidor 已提交
603
				}
I
isidor 已提交
604

I
isidor 已提交
605
				return compareFileNames(statA.name, statB.name);
I
isidor 已提交
606

I
isidor 已提交
607 608 609 610 611
			default: /* 'default', 'mixed', 'filesFirst' */
				return compareFileNames(statA.name, statB.name);
		}
	}
}
I
isidor 已提交
612

I
isidor 已提交
613 614 615 616 617 618 619
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 已提交
620 621
};

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

625 626 627
	private compressedDragOverElement: HTMLElement | undefined;
	private compressedDropTargetDisposable: IDisposable = Disposable.None;

I
isidor 已提交
628
	private toDispose: IDisposable[];
I
isidor 已提交
629
	private dropEnabled = false;
I
isidor 已提交
630 631 632 633 634 635 636 637 638 639 640

	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,
641
		@IHostService private hostService: IHostService,
I
isidor 已提交
642
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
I
isidor 已提交
643 644 645 646 647 648 649 650 651 652
	) {
		this.toDispose = [];

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

I
isidor 已提交
653
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
654 655 656 657
		if (!this.dropEnabled) {
			return false;
		}

658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
		// 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 已提交
694 695
		const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
		const fromDesktop = data instanceof DesktopDragAndDropData;
I
isidor 已提交
696
		const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move;
I
isidor 已提交
697 698

		// Desktop DND
699 700
		if (fromDesktop) {
			if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) {
I
isidor 已提交
701 702 703 704 705 706 707 708 709 710 711
				return false;
			}
		}

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

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

I
isidor 已提交
714
			if (!target) {
H
Howard Hung 已提交
715
				// Dropping onto the empty area. Do not accept if items dragged are already
B
Benjamin Pasero 已提交
716
				// children of the root unless we are copying the file
717
				if (!isCopy && items.every(i => !!i.parent && i.parent.isRoot)) {
I
isidor 已提交
718 719 720
					return false;
				}

I
isidor 已提交
721
				return { accept: true, bubble: TreeDragOverBubble.Down, effect, autoExpand: false };
I
isidor 已提交
722 723
			}

I
isidor 已提交
724
			if (!Array.isArray(items)) {
I
isidor 已提交
725 726 727
				return false;
			}

I
isidor 已提交
728
			if (items.some((source) => {
I
isidor 已提交
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
				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
				}

746
				if (isEqualOrParent(target.resource, source.resource)) {
I
isidor 已提交
747 748 749 750 751 752 753 754 755 756 757
					return true; // Can not move a parent folder into one of its children
				}

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

		// All (target = model)
		if (!target) {
I
isidor 已提交
758
			return { accept: true, bubble: TreeDragOverBubble.Down, effect };
I
isidor 已提交
759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778
		}

		// 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 已提交
779
	getDragURI(element: ExplorerItem): string | null {
I
isidor 已提交
780
		if (this.explorerService.isEditable(element)) {
J
Joao Moreno 已提交
781 782 783
			return null;
		}

I
isidor 已提交
784 785 786
		return element.resource.toString();
	}

J
Joao Moreno 已提交
787 788 789 790
	getDragLabel(elements: ExplorerItem[], originalEvent: DragEvent): string | undefined {
		if (elements.length === 1) {
			const stat = FileDragAndDrop.getCompressedStatFromDragEvent(elements[0], originalEvent);
			return stat.name;
I
isidor 已提交
791 792
		}

J
Joao Moreno 已提交
793
		return String(elements.length);
I
isidor 已提交
794 795 796
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
J
Joao Moreno 已提交
797
		const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, originalEvent);
I
isidor 已提交
798
		if (items && items.length && originalEvent.dataTransfer) {
I
isidor 已提交
799
			// Apply some datatransfer types to allow for dragging the element outside of the application
I
isidor 已提交
800
			this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
I
isidor 已提交
801 802 803

			// 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 已提交
804
			const fileResources = items.filter(s => !s.isDirectory && s.resource.scheme === Schemas.file).map(r => r.resource.fsPath);
I
isidor 已提交
805 806 807 808 809 810
			if (fileResources.length) {
				originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));
			}
		}
	}

I
isidor 已提交
811
	drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
812 813 814 815 816 817 818 819 820 821 822
		this.compressedDropTargetDisposable.dispose();

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

			if (compressedTarget) {
				target = compressedTarget;
			}
		}

823 824 825 826
		// Find parent to add to
		if (!target) {
			target = this.explorerService.roots[this.explorerService.roots.length - 1];
		}
I
isidor 已提交
827
		if (!target.isDirectory && target.parent) {
828 829 830 831 832 833
			target = target.parent;
		}
		if (target.isReadonly) {
			return;
		}

I
isidor 已提交
834 835
		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
J
Joao Moreno 已提交
836 837 838 839 840
			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 已提交
841 842 843
		}
		// In-Explorer DND (Move/Copy file)
		else {
844
			this.handleExplorerDrop(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target, originalEvent).then(undefined, e => this.notificationService.warn(e));
I
isidor 已提交
845 846 847
		}
	}

J
Joao Moreno 已提交
848 849 850 851 852 853 854 855
	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)) {
856
						const { confirmed } = await this.dialogService.confirm(fileOverwriteConfirm(name));
J
Joao Moreno 已提交
857 858
						if (!confirmed) {
							return;
I
isidor 已提交
859
						}
I
isidor 已提交
860 861
					}

J
Joao Moreno 已提交
862 863 864 865 866 867 868 869 870
					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 已提交
871

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

I
isidor 已提交
877
		// Pass focus to window
878
		this.hostService.focus();
I
isidor 已提交
879

I
isidor 已提交
880 881 882
		// 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 已提交
883

I
isidor 已提交
884 885 886 887
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
888
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
889 890 891 892 893 894
			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 已提交
895
			}
I
isidor 已提交
896

897
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
898
			if (choice === buttons.length - 3) {
I
isidor 已提交
899 900
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
901
			if (choice === buttons.length - 2) {
I
isidor 已提交
902 903 904 905
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
906 907 908 909 910 911
		}

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

I
isidor 已提交
914
	private async addResources(target: ExplorerItem, resources: URI[]): Promise<void> {
I
isidor 已提交
915 916 917
		if (resources && resources.length > 0) {

			// Resolve target to check for name collisions and ask user
I
isidor 已提交
918 919 920 921 922 923 924
			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 已提交
925
					targetNames.add(ignoreCase ? child.name.toLowerCase() : child.name);
I
isidor 已提交
926 927
				});
			}
I
isidor 已提交
928

I
isidor 已提交
929 930
			const filtered = resources.filter(resource => targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase()));
			const resourceExists = filtered.length >= 1;
I
isidor 已提交
931
			if (resourceExists) {
I
isidor 已提交
932
				const confirmationResult = await this.dialogService.confirm(fileOverwriteConfirm(basename(filtered[0])));
I
isidor 已提交
933
				if (!confirmationResult.confirmed) {
I
isidor 已提交
934
					return;
I
isidor 已提交
935
				}
I
isidor 已提交
936
			}
I
isidor 已提交
937

I
isidor 已提交
938 939 940 941 942 943 944 945 946 947 948 949
			// 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 已提交
950 951
					}

I
isidor 已提交
952
					const copyTarget = joinPath(target.resource, basename(sourceFile));
I
isidor 已提交
953
					const stat = await this.textFileService.copy(sourceFile, copyTarget, true);
I
isidor 已提交
954 955 956 957
					// 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 已提交
958 959 960
				});
			});

I
isidor 已提交
961 962
			await sequence(addPromisesFactory);
		}
I
isidor 已提交
963 964
	}

965 966
	private async handleExplorerDrop(data: ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
		const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data);
I
isidor 已提交
967
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
968
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
969 970

		// Handle confirm setting
I
isidor 已提交
971
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
972
		if (confirmDragAndDrop) {
I
isidor 已提交
973
			const confirmation = await this.dialogService.confirm({
I
isidor 已提交
974
				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 已提交
975
					: 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 已提交
976
						: 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 已提交
977
							: localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name),
I
isidor 已提交
978 979 980 981 982 983 984
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});

I
isidor 已提交
985 986 987
			if (!confirmation.confirmed) {
				return;
			}
I
isidor 已提交
988 989

			// Check for confirmation checkbox
I
isidor 已提交
990 991
			if (confirmation.checkboxChecked === true) {
				await this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
I
isidor 已提交
992
			}
I
isidor 已提交
993
		}
I
isidor 已提交
994

I
isidor 已提交
995 996
		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 已提交
997 998
	}

999
	private doHandleRootDrop(roots: ExplorerItem[], target: ExplorerItem): Promise<void> {
I
isidor 已提交
1000 1001 1002 1003 1004
		if (roots.length === 0) {
			return Promise.resolve(undefined);
		}

		const folders = this.contextService.getWorkspace().folders;
1005
		let targetIndex: number | undefined;
I
isidor 已提交
1006 1007 1008 1009 1010
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

		for (let index = 0; index < folders.length; index++) {
			const data = {
I
isidor 已提交
1011 1012
				uri: folders[index].uri,
				name: folders[index].name
I
isidor 已提交
1013 1014
			};
			if (target instanceof ExplorerItem && folders[index].uri.toString() === target.resource.toString()) {
I
isidor 已提交
1015
				targetIndex = index;
I
isidor 已提交
1016 1017 1018 1019 1020 1021 1022 1023
			}

			if (roots.every(r => r.resource.toString() !== folders[index].uri.toString())) {
				workspaceCreationData.push(data);
			} else {
				rootsToMove.push(data);
			}
		}
I
isidor 已提交
1024
		if (targetIndex === undefined) {
I
isidor 已提交
1025 1026
			targetIndex = workspaceCreationData.length;
		}
I
isidor 已提交
1027 1028 1029 1030 1031

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

I
isidor 已提交
1032
	private async doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
1033 1034
		// Reuse duplicate action if user copies
		if (isCopy) {
I
isidor 已提交
1035
			const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
I
isidor 已提交
1036
			const stat = await this.textFileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming));
I
isidor 已提交
1037 1038 1039
			if (!stat.isDirectory) {
				await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
			}
I
isidor 已提交
1040

I
isidor 已提交
1041
			return;
I
isidor 已提交
1042 1043 1044 1045
		}

		// Otherwise move
		const targetResource = joinPath(target.resource, source.name);
I
isidor 已提交
1046 1047 1048 1049
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
1050

I
isidor 已提交
1051 1052 1053
		try {
			await this.textFileService.move(source.resource, targetResource);
		} catch (error) {
I
isidor 已提交
1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
			// 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 已提交
1064 1065 1066 1067 1068 1069
				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 已提交
1070
					}
I
isidor 已提交
1071
				}
I
isidor 已提交
1072 1073 1074 1075 1076
			}
			// Any other error
			else {
				this.notificationService.error(error);
			}
I
isidor 已提交
1077
		}
I
isidor 已提交
1078
	}
J
Joao Moreno 已提交
1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093

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

1094 1095
	private static getCompressedStatFromDragEvent(stat: ExplorerItem, dragEvent: DragEvent): ExplorerItem {
		const target = document.elementFromPoint(dragEvent.clientX, dragEvent.clientY);
J
Joao Moreno 已提交
1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111
		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;
	}
1112 1113 1114 1115

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

1118
function getIconLabelNameFromHTMLElement(target: HTMLElement | EventTarget | Element | null): { element: HTMLElement, count: number, index: number } | null {
J
Joao Moreno 已提交
1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138
	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 已提交
1139
}
J
Joao Moreno 已提交
1140

J
Joao Moreno 已提交
1141 1142 1143 1144
export function isCompressedFolderName(target: HTMLElement | EventTarget | Element | null): boolean {
	return !!getIconLabelNameFromHTMLElement(target);
}

J
Joao Moreno 已提交
1145 1146
export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

1147
	isIncompressible(stat: ExplorerItem): boolean {
J
Joao Moreno 已提交
1148
		return stat.isRoot || !stat.isDirectory || stat instanceof NewExplorerItem;
J
Joao Moreno 已提交
1149 1150
	}
}