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

I
isidor 已提交
6
import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
7 8
import * as DOM from 'vs/base/browser/dom';
import * as glob from 'vs/base/common/glob';
I
isidor 已提交
9
import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list';
10
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
I
isidor 已提交
11
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
12
import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
13
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
I
isidor 已提交
14
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
15
import { IDisposable, Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
16
import { KeyCode } from 'vs/base/common/keyCodes';
I
isidor 已提交
17
import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels';
J
Joao Moreno 已提交
18
import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree';
I
isidor 已提交
19
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
B
Benjamin Pasero 已提交
20
import { IThemeService } from 'vs/platform/theme/common/themeService';
I
isidor 已提交
21
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
22
import { IFilesConfiguration, IExplorerService, IEditableData } from 'vs/workbench/contrib/files/common/files';
I
isidor 已提交
23
import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, distinctParents } from 'vs/base/common/resources';
I
isidor 已提交
24 25 26 27 28 29
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { localize } from 'vs/nls';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { once } from 'vs/base/common/functional';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { equals, deepClone } from 'vs/base/common/objects';
30
import * as path from 'vs/base/common/path';
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';
I
isidor 已提交
54 55 56

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

I
isidor 已提交
320
	updateConfiguration(): boolean {
I
isidor 已提交
321
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
322
		this.contextService.getWorkspace().folders.forEach(folder => {
323
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
B
Benjamin Pasero 已提交
324
			const excludesConfig: glob.IExpression = configuration?.files?.exclude || Object.create(null);
325 326 327

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

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

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

		return needsRefresh;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

I
isidor 已提交
448 449 450 451 452 453 454
const fileOverwriteConfirm: IConfirmation = {
	message: localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"),
	detail: localize('irreversible', "This action is irreversible!"),
	primaryButton: localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
	type: 'warning'
};

I
isidor 已提交
455 456 457 458
export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';

	private toDispose: IDisposable[];
I
isidor 已提交
459
	private dropEnabled = false;
I
isidor 已提交
460 461 462 463 464 465 466 467 468 469 470

	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,
471
		@IHostService private hostService: IHostService,
I
isidor 已提交
472
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
I
isidor 已提交
473 474 475 476 477 478 479 480 481 482
	) {
		this.toDispose = [];

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

I
isidor 已提交
483
	onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
I
isidor 已提交
484 485 486 487 488 489
		if (!this.dropEnabled) {
			return false;
		}

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

		// Desktop DND
493 494
		if (fromDesktop) {
			if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) {
I
isidor 已提交
495 496 497 498 499 500 501 502 503 504 505
				return false;
			}
		}

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

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

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

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

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

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

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

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

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

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

I
isidor 已提交
578 579 580
		return element.resource.toString();
	}

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

		return elements[0].name;
	}

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

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

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

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

I
isidor 已提交
626
	private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642
		if (isWeb) {
			data.files.forEach(file => {
				const reader = new FileReader();
				reader.readAsArrayBuffer(file);
				reader.onload = async (event) => {
					const name = file.name;
					if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) {
						if (target.getChild(name)) {
							const { confirmed } = await this.dialogService.confirm(fileOverwriteConfirm);
							if (!confirmed) {
								return;
							}
						}

						const resource = joinPath(target.resource, name);
						await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target?.result)));
I
isidor 已提交
643 644 645
						if (data.files.length === 1) {
							await this.editorService.openEditor({ resource, options: { pinned: true } });
						}
I
isidor 已提交
646 647 648 649 650 651 652
					}
				};
			});

			return;
		}

I
isidor 已提交
653 654
		const droppedResources = extractResources(originalEvent, true);
		// Check for dropped external files to be folders
I
isidor 已提交
655
		const result = await this.fileService.resolveAll(droppedResources);
I
isidor 已提交
656

I
isidor 已提交
657
		// Pass focus to window
658
		this.hostService.focus();
I
isidor 已提交
659

I
isidor 已提交
660 661 662
		// 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 已提交
663

I
isidor 已提交
664 665 666 667
			const buttons = [
				folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"),
				localize('cancel', "Cancel")
			];
I
isidor 已提交
668
			const workspaceFolderSchemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
I
isidor 已提交
669 670 671 672 673 674
			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 已提交
675
			}
I
isidor 已提交
676

677
			const { choice } = await this.dialogService.show(Severity.Info, message, buttons);
I
isidor 已提交
678
			if (choice === buttons.length - 3) {
I
isidor 已提交
679 680
				return this.workspaceEditingService.addFolders(folders);
			}
I
isidor 已提交
681
			if (choice === buttons.length - 2) {
I
isidor 已提交
682 683 684 685
				return this.addResources(target, droppedResources.map(res => res.resource));
			}

			return undefined;
I
isidor 已提交
686 687 688 689 690 691
		}

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

I
isidor 已提交
694
	private async addResources(target: ExplorerItem, resources: URI[]): Promise<void> {
I
isidor 已提交
695 696 697
		if (resources && resources.length > 0) {

			// Resolve target to check for name collisions and ask user
I
isidor 已提交
698 699 700 701 702 703 704
			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 已提交
705
					targetNames.add(ignoreCase ? child.name.toLowerCase() : child.name);
I
isidor 已提交
706 707
				});
			}
I
isidor 已提交
708

I
isidor 已提交
709 710
			const resourceExists = resources.some(resource => targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase()));
			if (resourceExists) {
I
isidor 已提交
711
				const confirmationResult = await this.dialogService.confirm(fileOverwriteConfirm);
I
isidor 已提交
712
				if (!confirmationResult.confirmed) {
I
isidor 已提交
713
					return;
I
isidor 已提交
714
				}
I
isidor 已提交
715
			}
I
isidor 已提交
716

I
isidor 已提交
717 718 719 720 721 722 723 724 725 726 727 728
			// 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 已提交
729 730
					}

I
isidor 已提交
731 732 733 734 735 736
					const copyTarget = joinPath(target.resource, basename(sourceFile));
					const stat = await this.fileService.copy(sourceFile, copyTarget, true);
					// if we only add one file, just open it directly
					if (resources.length === 1 && !stat.isDirectory) {
						this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
					}
I
isidor 已提交
737 738 739
				});
			});

I
isidor 已提交
740 741
			await sequence(addPromisesFactory);
		}
I
isidor 已提交
742 743
	}

I
isidor 已提交
744
	private async handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
I
isidor 已提交
745 746
		const elementsData = (data as ElementsDragAndDropData<ExplorerItem>).elements;
		const items = distinctParents(elementsData, s => s.resource);
I
isidor 已提交
747
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
I
isidor 已提交
748 749

		// Handle confirm setting
I
isidor 已提交
750
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
I
isidor 已提交
751
		if (confirmDragAndDrop) {
I
isidor 已提交
752
			const confirmation = await this.dialogService.confirm({
I
isidor 已提交
753
				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 已提交
754
					: 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 已提交
755
						: 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 已提交
756
							: localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name),
I
isidor 已提交
757 758 759 760 761 762 763
				checkbox: {
					label: localize('doNotAskAgain', "Do not ask me again")
				},
				type: 'question',
				primaryButton: localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
			});

I
isidor 已提交
764 765 766
			if (!confirmation.confirmed) {
				return;
			}
I
isidor 已提交
767 768

			// Check for confirmation checkbox
I
isidor 已提交
769 770
			if (confirmation.checkboxChecked === true) {
				await this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
I
isidor 已提交
771
			}
I
isidor 已提交
772
		}
I
isidor 已提交
773

I
isidor 已提交
774 775
		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 已提交
776 777
	}

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

		const folders = this.contextService.getWorkspace().folders;
784
		let targetIndex: number | undefined;
I
isidor 已提交
785 786 787 788 789
		const workspaceCreationData: IWorkspaceFolderCreationData[] = [];
		const rootsToMove: IWorkspaceFolderCreationData[] = [];

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

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

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

I
isidor 已提交
811
	private async doHandleExplorerDrop(source: ExplorerItem, target: ExplorerItem, isCopy: boolean): Promise<void> {
I
isidor 已提交
812 813
		// Reuse duplicate action if user copies
		if (isCopy) {
I
isidor 已提交
814
			const incrementalNaming = this.configurationService.getValue<IFilesConfiguration>().explorer.incrementalNaming;
I
isidor 已提交
815 816 817 818
			const stat = await this.fileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming));
			if (!stat.isDirectory) {
				await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } });
			}
I
isidor 已提交
819

I
isidor 已提交
820
			return;
I
isidor 已提交
821 822 823 824
		}

		// Otherwise move
		const targetResource = joinPath(target.resource, source.name);
I
isidor 已提交
825 826 827 828
		if (source.isReadonly) {
			// Do not allow moving readonly items
			return Promise.resolve();
		}
I
isidor 已提交
829

I
isidor 已提交
830 831 832
		try {
			await this.textFileService.move(source.resource, targetResource);
		} catch (error) {
I
isidor 已提交
833 834 835 836 837 838 839 840 841 842
			// 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 已提交
843 844 845 846 847 848
				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 已提交
849
					}
I
isidor 已提交
850
				}
I
isidor 已提交
851 852 853 854 855
			}
			// Any other error
			else {
				this.notificationService.error(error);
			}
I
isidor 已提交
856
		}
I
isidor 已提交
857 858
	}
}
J
Joao Moreno 已提交
859 860 861

export class ExplorerCompressionDelegate implements ITreeCompressionDelegate<ExplorerItem> {

862
	isIncompressible(stat: ExplorerItem): boolean {
J
Joao Moreno 已提交
863
		return stat.isRoot || !stat.isDirectory || stat instanceof NewExplorerItem;
J
Joao Moreno 已提交
864 865
	}
}