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

J
Johannes Rieken 已提交
7
import { TPromise } from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
8 9 10 11 12
import nls = require('vs/nls');
import lifecycle = require('vs/base/common/lifecycle');
import objects = require('vs/base/common/objects');
import DOM = require('vs/base/browser/dom');
import URI from 'vs/base/common/uri';
J
Johannes Rieken 已提交
13
import { MIME_BINARY } from 'vs/base/common/mime';
J
Joao Moreno 已提交
14
import { once } from 'vs/base/common/functional';
E
Erich Gamma 已提交
15
import paths = require('vs/base/common/paths');
I
isidor 已提交
16
import resources = require('vs/base/common/resources');
E
Erich Gamma 已提交
17
import errors = require('vs/base/common/errors');
J
Johannes Rieken 已提交
18 19
import { isString } from 'vs/base/common/types';
import { IAction, ActionRunner as BaseActionRunner, IActionRunner } from 'vs/base/common/actions';
E
Erich Gamma 已提交
20
import comparers = require('vs/base/common/comparers');
J
Johannes Rieken 已提交
21
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
22
import { isMacintosh, isLinux } from 'vs/base/common/platform';
E
Erich Gamma 已提交
23
import glob = require('vs/base/common/glob');
J
Johannes Rieken 已提交
24
import { FileLabel, IFileLabelOptions } from 'vs/workbench/browser/labels';
B
Benjamin Pasero 已提交
25
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
26
import { ContributableActionProvider } from 'vs/workbench/browser/actions';
27
import { IFilesConfiguration, SortOrder } from 'vs/workbench/parts/files/common/files';
28
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
29
import { FileOperationError, FileOperationResult, IFileService, FileKind } from 'vs/platform/files/common/files';
B
wip  
Benjamin Pasero 已提交
30
import { ResourceMap } from 'vs/base/common/map';
31
import { DuplicateFileAction, ImportFileAction, IEditableData, IFileViewletState } from 'vs/workbench/parts/files/electron-browser/fileActions';
32 33
import { IDataSource, ITree, IAccessibilityProvider, IRenderer, ContextMenuEvent, ISorter, IFilter, IDragAndDropData, IDragOverReaction, DRAG_OVER_ACCEPT_BUBBLE_DOWN, DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY, DRAG_OVER_ACCEPT_BUBBLE_UP, DRAG_OVER_ACCEPT_BUBBLE_UP_COPY, DRAG_OVER_REJECT } from 'vs/base/parts/tree/browser/tree';
import { DesktopDragAndDropData, ExternalElementsDragAndDropData, SimpleFileResourceDragAndDrop } from 'vs/base/parts/tree/browser/treeDnd';
J
Johannes Rieken 已提交
34
import { ClickBehavior, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults';
35
import { FileStat, NewStatPlaceholder, Model } from 'vs/workbench/parts/files/common/explorerModel';
J
Johannes Rieken 已提交
36 37 38
import { DragMouseEvent, IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
39
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
40
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
J
Johannes Rieken 已提交
41 42 43
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
44
import { IMessageService, IConfirmation, Severity, IConfirmationResult } from 'vs/platform/message/common/message';
J
Johannes Rieken 已提交
45 46
import { IProgressService } from 'vs/platform/progress/common/progress';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
47
import { KeyCode } from 'vs/base/common/keyCodes';
J
Johannes Rieken 已提交
48 49 50
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions';
import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem';
51
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
B
Benjamin Pasero 已提交
52 53
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
54 55
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
B
Benjamin Pasero 已提交
56
import { getPathLabel } from 'vs/base/common/labels';
57
import { extractResources } from 'vs/workbench/browser/editor';
58
import { relative } from 'path';
59
import { DataTransfers } from 'vs/base/browser/dnd';
E
Erich Gamma 已提交
60

B
Benjamin Pasero 已提交
61
export class FileDataSource implements IDataSource {
E
Erich Gamma 已提交
62 63 64 65
	constructor(
		@IProgressService private progressService: IProgressService,
		@IMessageService private messageService: IMessageService,
		@IFileService private fileService: IFileService,
B
Benjamin Pasero 已提交
66
		@IPartService private partService: IPartService
67
	) { }
E
Erich Gamma 已提交
68

69 70 71 72 73
	public getId(tree: ITree, stat: FileStat | Model): string {
		if (stat instanceof Model) {
			return 'model';
		}

I
isidor 已提交
74
		return `${stat.root.resource.toString()}:${stat.getId()}`;
E
Erich Gamma 已提交
75 76
	}

77 78
	public hasChildren(tree: ITree, stat: FileStat | Model): boolean {
		return stat instanceof Model || (stat instanceof FileStat && stat.isDirectory);
E
Erich Gamma 已提交
79 80
	}

81 82 83 84
	public getChildren(tree: ITree, stat: FileStat | Model): TPromise<FileStat[]> {
		if (stat instanceof Model) {
			return TPromise.as(stat.roots);
		}
E
Erich Gamma 已提交
85 86 87

		// Return early if stat is already resolved
		if (stat.isDirectoryResolved) {
A
Alex Dima 已提交
88
			return TPromise.as(stat.children);
E
Erich Gamma 已提交
89 90 91 92 93 94
		}

		// Resolve children and add to fileStat for future lookup
		else {

			// Resolve
95
			const promise = this.fileService.resolveFile(stat.resource, { resolveSingleChildDescendants: true }).then(dirStat => {
E
Erich Gamma 已提交
96 97

				// Convert to view model
I
isidor 已提交
98
				const modelDirStat = FileStat.create(dirStat, stat.root);
E
Erich Gamma 已提交
99

100
				// Add children to folder
E
Erich Gamma 已提交
101 102 103 104 105 106 107 108
				for (let i = 0; i < modelDirStat.children.length; i++) {
					stat.addChild(modelDirStat.children[i]);
				}

				stat.isDirectoryResolved = true;

				return stat.children;
			}, (e: any) => {
I
isidor 已提交
109
				stat.hasChildren = false;
110
				this.messageService.show(Severity.Error, e);
E
Erich Gamma 已提交
111 112 113 114

				return []; // we could not resolve any children because of an error
			});

115
			this.progressService.showWhile(promise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */);
E
Erich Gamma 已提交
116 117 118 119 120

			return promise;
		}
	}

I
isidor 已提交
121
	public getParent(tree: ITree, stat: FileStat | Model): TPromise<FileStat> {
E
Erich Gamma 已提交
122
		if (!stat) {
A
Alex Dima 已提交
123
			return TPromise.as(null); // can be null if nothing selected in the tree
E
Erich Gamma 已提交
124 125 126
		}

		// Return if root reached
I
isidor 已提交
127
		if (tree.getInput() === stat) {
A
Alex Dima 已提交
128
			return TPromise.as(null);
E
Erich Gamma 已提交
129 130 131
		}

		// Return if parent already resolved
I
isidor 已提交
132
		if (stat instanceof FileStat && stat.parent) {
A
Alex Dima 已提交
133
			return TPromise.as(stat.parent);
E
Erich Gamma 已提交
134 135 136 137 138
		}

		// We never actually resolve the parent from the disk for performance reasons. It wouldnt make
		// any sense to resolve parent by parent with requests to walk up the chain. Instead, the explorer
		// makes sure to properly resolve a deep path to a specific file and merges the result with the model.
A
Alex Dima 已提交
139
		return TPromise.as(null);
E
Erich Gamma 已提交
140 141 142 143 144 145 146 147 148 149 150 151
	}
}

export class FileActionProvider extends ContributableActionProvider {
	private state: FileViewletState;

	constructor(state: any) {
		super();

		this.state = state;
	}

B
Benjamin Pasero 已提交
152
	public hasActions(tree: ITree, stat: FileStat): boolean {
E
Erich Gamma 已提交
153 154 155 156 157 158 159
		if (stat instanceof NewStatPlaceholder) {
			return false;
		}

		return super.hasActions(tree, stat);
	}

160
	public getActions(tree: ITree, stat: FileStat): TPromise<IAction[]> {
E
Erich Gamma 已提交
161
		if (stat instanceof NewStatPlaceholder) {
A
Alex Dima 已提交
162
			return TPromise.as([]);
E
Erich Gamma 已提交
163 164 165 166 167
		}

		return super.getActions(tree, stat);
	}

168
	public hasSecondaryActions(tree: ITree, stat: FileStat | Model): boolean {
E
Erich Gamma 已提交
169 170 171 172 173 174 175
		if (stat instanceof NewStatPlaceholder) {
			return false;
		}

		return super.hasSecondaryActions(tree, stat);
	}

176
	public getSecondaryActions(tree: ITree, stat: FileStat | Model): TPromise<IAction[]> {
E
Erich Gamma 已提交
177
		if (stat instanceof NewStatPlaceholder) {
A
Alex Dima 已提交
178
			return TPromise.as([]);
E
Erich Gamma 已提交
179 180 181 182 183
		}

		return super.getSecondaryActions(tree, stat);
	}

S
Sandeep Somavarapu 已提交
184
	public runAction(tree: ITree, stat: FileStat, action: IAction, context?: any): TPromise<any>;
185 186
	public runAction(tree: ITree, stat: FileStat, actionID: string, context?: any): TPromise<any>;
	public runAction(tree: ITree, stat: FileStat, arg: any, context: any = {}): TPromise<any> {
E
Erich Gamma 已提交
187 188
		context = objects.mixin({
			viewletState: this.state,
189
			stat
E
Erich Gamma 已提交
190 191 192
		}, context);

		if (!isString(arg)) {
193
			const action = <IAction>arg;
E
Erich Gamma 已提交
194 195 196 197 198 199 200
			if (action.enabled) {
				return action.run(context);
			}

			return null;
		}

201
		const id = <string>arg;
A
Alex Dima 已提交
202
		let promise = this.hasActions(tree, stat) ? this.getActions(tree, stat) : TPromise.as([]);
E
Erich Gamma 已提交
203

204
		return promise.then((actions: IAction[]) => {
E
Erich Gamma 已提交
205 206 207 208 209 210
			for (let i = 0, len = actions.length; i < len; i++) {
				if (actions[i].id === id && actions[i].enabled) {
					return actions[i].run(context);
				}
			}

A
Alex Dima 已提交
211
			promise = this.hasSecondaryActions(tree, stat) ? this.getSecondaryActions(tree, stat) : TPromise.as([]);
E
Erich Gamma 已提交
212

213
			return promise.then((actions: IAction[]) => {
E
Erich Gamma 已提交
214 215 216 217 218 219 220 221 222 223 224 225 226 227
				for (let i = 0, len = actions.length; i < len; i++) {
					if (actions[i].id === id && actions[i].enabled) {
						return actions[i].run(context);
					}
				}

				return null;
			});
		});
	}
}

export class FileViewletState implements IFileViewletState {
	private _actionProvider: FileActionProvider;
B
wip  
Benjamin Pasero 已提交
228
	private editableStats: ResourceMap<IEditableData>;
E
Erich Gamma 已提交
229 230 231

	constructor() {
		this._actionProvider = new FileActionProvider(this);
B
wip  
Benjamin Pasero 已提交
232
		this.editableStats = new ResourceMap<IEditableData>();
E
Erich Gamma 已提交
233 234 235 236 237 238 239
	}

	public get actionProvider(): FileActionProvider {
		return this._actionProvider;
	}

	public getEditableData(stat: FileStat): IEditableData {
B
wip  
Benjamin Pasero 已提交
240
		return this.editableStats.get(stat.resource);
E
Erich Gamma 已提交
241 242 243 244
	}

	public setEditable(stat: FileStat, editableData: IEditableData): void {
		if (editableData) {
B
wip  
Benjamin Pasero 已提交
245
			this.editableStats.set(stat.resource, editableData);
E
Erich Gamma 已提交
246 247 248 249
		}
	}

	public clearEditable(stat: FileStat): void {
B
wip  
Benjamin Pasero 已提交
250
		this.editableStats.delete(stat.resource);
E
Erich Gamma 已提交
251 252 253
	}
}

254
export class ActionRunner extends BaseActionRunner implements IActionRunner {
E
Erich Gamma 已提交
255 256 257 258 259 260 261 262
	private viewletState: FileViewletState;

	constructor(state: FileViewletState) {
		super();

		this.viewletState = state;
	}

263
	public run(action: IAction, context?: any): TPromise<any> {
E
Erich Gamma 已提交
264 265 266 267
		return super.run(action, { viewletState: this.viewletState });
	}
}

268 269 270 271 272
export interface IFileTemplateData {
	label: FileLabel;
	container: HTMLElement;
}

E
Erich Gamma 已提交
273
// Explorer Renderer
274
export class FileRenderer implements IRenderer {
275

276 277
	private static readonly ITEM_HEIGHT = 22;
	private static readonly FILE_TEMPLATE_ID = 'file';
278

E
Erich Gamma 已提交
279
	private state: FileViewletState;
280 281
	private config: IFilesConfiguration;
	private configListener: IDisposable;
E
Erich Gamma 已提交
282 283 284

	constructor(
		state: FileViewletState,
285
		@IContextViewService private contextViewService: IContextViewService,
B
Benjamin Pasero 已提交
286
		@IInstantiationService private instantiationService: IInstantiationService,
287 288
		@IThemeService private themeService: IThemeService,
		@IConfigurationService private configurationService: IConfigurationService
E
Erich Gamma 已提交
289 290
	) {
		this.state = state;
291
		this.config = this.configurationService.getValue<IFilesConfiguration>();
292 293
		this.configListener = this.configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration('explorer')) {
294
				this.config = this.configurationService.getValue();
295 296 297 298 299 300
			}
		});
	}

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

303
	public getHeight(tree: ITree, element: any): number {
304
		return FileRenderer.ITEM_HEIGHT;
E
Erich Gamma 已提交
305 306
	}

307 308 309
	public getTemplateId(tree: ITree, element: any): string {
		return FileRenderer.FILE_TEMPLATE_ID;
	}
310

311 312 313 314 315 316
	public disposeTemplate(tree: ITree, templateId: string, templateData: IFileTemplateData): void {
		templateData.label.dispose();
	}

	public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): IFileTemplateData {
		const label = this.instantiationService.createInstance(FileLabel, container, void 0);
E
Erich Gamma 已提交
317

318
		return { label, container };
319 320
	}

321 322
	public renderElement(tree: ITree, stat: FileStat, templateId: string, templateData: IFileTemplateData): void {
		const editableData: IEditableData = this.state.getEditableData(stat);
B
Benjamin Pasero 已提交
323

324 325
		// File Label
		if (!editableData) {
326
			templateData.label.element.style.display = 'flex';
327
			const extraClasses = ['explorer-item'];
328 329 330 331
			templateData.label.setFile(stat.resource, {
				hidePath: true,
				fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
				extraClasses,
332
				fileDecorations: this.config.explorer.decorations
333
			});
334
		}
335

336 337 338 339 340
		// Input Box
		else {
			templateData.label.element.style.display = 'none';
			this.renderInputBox(templateData.container, tree, stat, editableData);
		}
341 342
	}

343
	private renderInputBox(container: HTMLElement, tree: ITree, stat: FileStat, editableData: IEditableData): void {
B
Benjamin Pasero 已提交
344

345 346
		// Use a file label only for the icon next to the input box
		const label = this.instantiationService.createInstance(FileLabel, container, void 0);
347
		const extraClasses = ['explorer-item', 'explorer-item-edited'];
348 349
		const fileKind = stat.isRoot ? FileKind.ROOT_FOLDER : (stat.isDirectory || (stat instanceof NewStatPlaceholder && stat.isDirectoryPlaceholder())) ? FileKind.FOLDER : FileKind.FILE;
		const labelOptions: IFileLabelOptions = { hidePath: true, hideLabel: true, fileKind, extraClasses };
B
Benjamin Pasero 已提交
350
		label.setFile(stat.resource, labelOptions);
351

352
		// Input field for name
353
		const inputBox = new InputBox(label.element, this.contextViewService, {
J
Joao Moreno 已提交
354
			validationOptions: {
B
Benjamin Pasero 已提交
355
				validation: editableData.validator
356 357
			},
			ariaLabel: nls.localize('fileInputAriaLabel', "Type file name. Press Enter to confirm or Escape to cancel.")
J
Joao Moreno 已提交
358
		});
B
Benjamin Pasero 已提交
359
		const styler = attachInputBoxStyler(inputBox, this.themeService);
E
Erich Gamma 已提交
360

I
isidor 已提交
361
		const parent = resources.dirname(stat.resource);
B
Benjamin Pasero 已提交
362
		inputBox.onDidChange(value => {
I
isidor 已提交
363
			label.setFile(parent.with({ path: paths.join(parent.path, value) }), labelOptions); // update label icon while typing!
B
Benjamin Pasero 已提交
364 365
		});

366 367
		const value = stat.name || '';
		const lastDot = value.lastIndexOf('.');
E
Erich Gamma 已提交
368

J
Joao Moreno 已提交
369 370 371
		inputBox.value = value;
		inputBox.select({ start: 0, end: lastDot > 0 && !stat.isDirectory ? lastDot : value.length });
		inputBox.focus();
E
Erich Gamma 已提交
372

373
		const done = once((commit: boolean) => {
J
Joao Moreno 已提交
374 375
			tree.clearHighlight();

J
Joao Moreno 已提交
376
			if (commit && inputBox.value) {
E
Erich Gamma 已提交
377
				this.state.actionProvider.runAction(tree, stat, editableData.action, { value: inputBox.value });
J
Joao Moreno 已提交
378
			}
E
Erich Gamma 已提交
379

380
			const restoreFocus = document.activeElement === inputBox.inputElement; // https://github.com/Microsoft/vscode/issues/20269
J
Joao Moreno 已提交
381
			setTimeout(() => {
382 383 384
				if (restoreFocus) {
					tree.DOMFocus();
				}
J
Joao Moreno 已提交
385
				lifecycle.dispose(toDispose);
386
				container.removeChild(label.element);
J
Joao Moreno 已提交
387
			}, 0);
J
Joao Moreno 已提交
388
		});
E
Erich Gamma 已提交
389

B
Benjamin Pasero 已提交
390
		const toDispose = [
J
Joao Moreno 已提交
391
			inputBox,
A
Cleanup  
Alex Dima 已提交
392
			DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
393
				if (e.equals(KeyCode.Enter)) {
J
Joao Moreno 已提交
394 395 396
					if (inputBox.validate()) {
						done(true);
					}
A
Alexandru Dima 已提交
397
				} else if (e.equals(KeyCode.Escape)) {
J
Joao Moreno 已提交
398 399 400
					done(false);
				}
			}),
401
			DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, () => {
J
Joao Moreno 已提交
402
				done(inputBox.isInputValid());
B
Benjamin Pasero 已提交
403
			}),
B
Benjamin Pasero 已提交
404 405
			label,
			styler
J
Joao Moreno 已提交
406
		];
E
Erich Gamma 已提交
407 408 409
	}
}

410
// Explorer Accessibility Provider
B
Benjamin Pasero 已提交
411
export class FileAccessibilityProvider implements IAccessibilityProvider {
412

B
Benjamin Pasero 已提交
413
	public getAriaLabel(tree: ITree, stat: FileStat): string {
414
		return nls.localize('filesExplorerViewerAriaLabel', "{0}, Files Explorer", stat.name);
415 416 417
	}
}

E
Erich Gamma 已提交
418 419 420 421
// Explorer Controller
export class FileController extends DefaultController {
	private state: FileViewletState;

422
	private contributedContextMenu: IMenu;
423

E
Erich Gamma 已提交
424 425 426 427
	constructor(state: FileViewletState,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@IContextMenuService private contextMenuService: IContextMenuService,
		@ITelemetryService private telemetryService: ITelemetryService,
428
		@IMenuService menuService: IMenuService,
429
		@IContextKeyService contextKeyService: IContextKeyService
E
Erich Gamma 已提交
430
	) {
431
		super({ clickBehavior: ClickBehavior.ON_MOUSE_UP /* do not change to not break DND */, keyboardSupport: false /* handled via IListService */ });
E
Erich Gamma 已提交
432

433
		this.contributedContextMenu = menuService.createMenu(MenuId.ExplorerContext, contextKeyService);
434

E
Erich Gamma 已提交
435 436 437
		this.state = state;
	}

438
	public onLeftClick(tree: ITree, stat: FileStat | Model, event: IMouseEvent, origin: string = 'mouse'): boolean {
439 440
		const payload = { origin: origin };
		const isDoubleClick = (origin === 'mouse' && event.detail === 2);
E
Erich Gamma 已提交
441 442 443 444 445 446 447 448 449 450 451 452 453 454

		// Handle Highlight Mode
		if (tree.getHighlight()) {

			// Cancel Event
			event.preventDefault();
			event.stopPropagation();

			tree.clearHighlight(payload);

			return false;
		}

		// Handle root
455
		if (stat instanceof Model) {
E
Erich Gamma 已提交
456 457 458 459 460 461 462
			tree.clearFocus(payload);
			tree.clearSelection(payload);

			return false;
		}

		// Cancel Event
463
		const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown';
E
Erich Gamma 已提交
464 465 466 467 468 469 470 471 472
		if (!isMouseDown) {
			event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise
		}
		event.stopPropagation();

		// Set DOM focus
		tree.DOMFocus();

		// Expand / Collapse
473
		tree.toggleExpansion(stat, event.altKey);
E
Erich Gamma 已提交
474 475 476

		// Allow to unselect
		if (event.shiftKey && !(stat instanceof NewStatPlaceholder)) {
477
			const selection = tree.getSelection();
478
			if (selection && selection.length > 0 && selection[0] === stat) {
E
Erich Gamma 已提交
479 480 481 482 483 484
				tree.clearSelection(payload);
			}
		}

		// Select, Focus and open files
		else if (!(stat instanceof NewStatPlaceholder)) {
485
			const preserveFocus = !isDoubleClick;
E
Erich Gamma 已提交
486 487 488 489 490 491
			tree.setFocus(stat, payload);

			if (isDoubleClick) {
				event.preventDefault(); // focus moves to editor, we need to prevent default
			}

492
			tree.setSelection([stat], payload);
E
Erich Gamma 已提交
493

494
			if (!stat.isDirectory) {
495
				this.openEditor(stat, { preserveFocus, sideBySide: event && (event.ctrlKey || event.metaKey), pinned: isDoubleClick });
E
Erich Gamma 已提交
496 497 498 499 500 501
			}
		}

		return true;
	}

502
	public onContextMenu(tree: ITree, stat: FileStat | Model, event: ContextMenuEvent): boolean {
E
Erich Gamma 已提交
503 504 505 506 507 508 509 510 511 512 513 514 515
		if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
			return false;
		}

		event.preventDefault();
		event.stopPropagation();

		tree.setFocus(stat);

		if (!this.state.actionProvider.hasSecondaryActions(tree, stat)) {
			return true;
		}

B
Benjamin Pasero 已提交
516
		const anchor = { x: event.posx, y: event.posy };
E
Erich Gamma 已提交
517 518
		this.contextMenuService.showContextMenu({
			getAnchor: () => anchor,
519 520
			getActions: () => {
				return this.state.actionProvider.getSecondaryActions(tree, stat).then(actions => {
521
					fillInActions(this.contributedContextMenu, stat instanceof FileStat ? { arg: stat.resource } : null, actions);
522
					return actions;
523 524
				});
			},
E
Erich Gamma 已提交
525
			getActionItem: this.state.actionProvider.getActionItem.bind(this.state.actionProvider, tree, stat),
526
			getActionsContext: (event) => {
E
Erich Gamma 已提交
527 528
				return {
					viewletState: this.state,
529 530
					stat,
					event
E
Erich Gamma 已提交
531 532 533 534 535 536 537 538 539 540 541 542
				};
			},
			onHide: (wasCancelled?: boolean) => {
				if (wasCancelled) {
					tree.DOMFocus();
				}
			}
		});

		return true;
	}

543
	public openEditor(stat: FileStat, options: { preserveFocus: boolean; sideBySide: boolean; pinned: boolean; }): void {
E
Erich Gamma 已提交
544
		if (stat && !stat.isDirectory) {
K
kieferrm 已提交
545
			/* __GDPR__
K
kieferrm 已提交
546 547 548 549 550
				"workbenchActionExecuted" : {
					"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
					"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
E
Erich Gamma 已提交
551 552
			this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'explorer' });

553
			this.editorService.openEditor({ resource: stat.resource, options }, options.sideBySide).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
554 555 556 557 558
		}
	}
}

// Explorer Sorter
B
Benjamin Pasero 已提交
559
export class FileSorter implements ISorter {
560 561 562 563
	private toDispose: IDisposable[];
	private sortOrder: SortOrder;

	constructor(
564 565
		@IConfigurationService private configurationService: IConfigurationService,
		@IWorkspaceContextService private contextService: IWorkspaceContextService
566 567 568
	) {
		this.toDispose = [];

569
		this.updateSortOrder();
570 571 572 573 574

		this.registerListeners();
	}

	private registerListeners(): void {
575
		this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.updateSortOrder()));
576 577
	}

578 579
	private updateSortOrder(): void {
		this.sortOrder = this.configurationService.getValue('explorer.sortOrder') || 'default';
580
	}
E
Erich Gamma 已提交
581

B
Benjamin Pasero 已提交
582
	public compare(tree: ITree, statA: FileStat, statB: FileStat): number {
B
Benjamin Pasero 已提交
583 584 585

		// Do not sort roots
		if (statA.isRoot) {
586
			if (statB.isRoot) {
587
				return this.contextService.getWorkspaceFolder(statA.resource).index - this.contextService.getWorkspaceFolder(statB.resource).index;
588
			}
589

B
Benjamin Pasero 已提交
590 591
			return -1;
		}
592

B
Benjamin Pasero 已提交
593 594 595 596 597
		if (statB.isRoot) {
			return 1;
		}

		// Sort Directories
598
		switch (this.sortOrder) {
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
			case 'type':
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}

				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}

				if (statA.isDirectory && statB.isDirectory) {
					return comparers.compareFileNames(statA.name, statB.name);
				}

				break;

614 615 616 617
			case 'filesFirst':
				if (statA.isDirectory && !statB.isDirectory) {
					return 1;
				}
B
Benjamin Pasero 已提交
618

619 620 621
				if (statB.isDirectory && !statA.isDirectory) {
					return -1;
				}
B
Benjamin Pasero 已提交
622

623 624
				break;

625 626 627
			case 'mixed':
				break; // not sorting when "mixed" is on

628 629 630 631 632 633 634 635 636
			default: /* 'default', 'modified' */
				if (statA.isDirectory && !statB.isDirectory) {
					return -1;
				}

				if (statB.isDirectory && !statA.isDirectory) {
					return 1;
				}

637
				break;
E
Erich Gamma 已提交
638 639
		}

B
Benjamin Pasero 已提交
640
		// Sort "New File/Folder" placeholders
E
Erich Gamma 已提交
641 642 643 644
		if (statA instanceof NewStatPlaceholder) {
			return -1;
		}

B
Benjamin Pasero 已提交
645
		if (statB instanceof NewStatPlaceholder) {
I
isidor 已提交
646 647 648
			return 1;
		}

B
Benjamin Pasero 已提交
649
		// Sort Files
650 651 652 653 654 655 656 657
		switch (this.sortOrder) {
			case 'type':
				return comparers.compareFileExtensions(statA.name, statB.name);

			case 'modified':
				if (statA.mtime !== statB.mtime) {
					return statA.mtime < statB.mtime ? 1 : -1;
				}
B
Benjamin Pasero 已提交
658 659

				return comparers.compareFileNames(statA.name, statB.name);
660 661 662

			default: /* 'default', 'mixed', 'filesFirst' */
				return comparers.compareFileNames(statA.name, statB.name);
663
		}
E
Erich Gamma 已提交
664
	}
B
Benjamin Pasero 已提交
665 666 667 668

	public dispose(): void {
		this.toDispose = dispose(this.toDispose);
	}
E
Erich Gamma 已提交
669 670 671
}

// Explorer Filter
B
Benjamin Pasero 已提交
672
export class FileFilter implements IFilter {
673

674
	private static readonly MAX_SIBLINGS_FILTER_THRESHOLD = 2000;
675

I
isidor 已提交
676
	private hiddenExpressionPerRoot: Map<string, glob.IExpression>;
B
Benjamin Pasero 已提交
677
	private workspaceFolderChangeListener: IDisposable;
E
Erich Gamma 已提交
678

I
isidor 已提交
679 680 681 682 683
	constructor(
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IConfigurationService private configurationService: IConfigurationService
	) {
		this.hiddenExpressionPerRoot = new Map<string, glob.IExpression>();
B
Benjamin Pasero 已提交
684 685 686 687 688 689

		this.registerListeners();
	}

	public registerListeners(): void {
		this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration());
E
Erich Gamma 已提交
690 691
	}

I
isidor 已提交
692 693
	public updateConfiguration(): boolean {
		let needsRefresh = false;
S
Sandeep Somavarapu 已提交
694
		this.contextService.getWorkspace().folders.forEach(folder => {
695
			const configuration = this.configurationService.getValue<IFilesConfiguration>({ resource: folder.uri });
I
isidor 已提交
696
			const excludesConfig = (configuration && configuration.files && configuration.files.exclude) || Object.create(null);
697
			needsRefresh = needsRefresh || !objects.equals(this.hiddenExpressionPerRoot.get(folder.uri.toString()), excludesConfig);
J
Johannes Rieken 已提交
698
			this.hiddenExpressionPerRoot.set(folder.uri.toString(), objects.deepClone(excludesConfig)); // do not keep the config, as it gets mutated under our hoods
I
isidor 已提交
699
		});
E
Erich Gamma 已提交
700 701 702 703

		return needsRefresh;
	}

B
Benjamin Pasero 已提交
704
	public isVisible(tree: ITree, stat: FileStat): boolean {
E
Erich Gamma 已提交
705 706 707 708
		return this.doIsVisible(stat);
	}

	private doIsVisible(stat: FileStat): boolean {
709
		if (stat instanceof NewStatPlaceholder || stat.isRoot) {
E
Erich Gamma 已提交
710 711 712
			return true; // always visible
		}

713 714 715 716 717 718
		// Workaround for O(N^2) complexity (https://github.com/Microsoft/vscode/issues/9962)
		let siblings = stat.parent && stat.parent.children && stat.parent.children;
		if (siblings && siblings.length > FileFilter.MAX_SIBLINGS_FILTER_THRESHOLD) {
			siblings = void 0;
		}

E
Erich Gamma 已提交
719
		// Hide those that match Hidden Patterns
720
		const siblingsFn = () => siblings && siblings.map(c => c.name);
I
isidor 已提交
721
		const expression = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()) || Object.create(null);
722
		if (glob.match(expression, paths.normalize(relative(stat.root.resource.fsPath, stat.resource.fsPath), true), siblingsFn)) {
E
Erich Gamma 已提交
723 724 725 726 727
			return false; // hidden through pattern
		}

		return true;
	}
B
Benjamin Pasero 已提交
728 729 730 731

	public dispose(): void {
		this.workspaceFolderChangeListener = dispose(this.workspaceFolderChangeListener);
	}
E
Erich Gamma 已提交
732 733 734
}

// Explorer Drag And Drop Controller
735
export class FileDragAndDrop extends SimpleFileResourceDragAndDrop {
736

737
	private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop';
738

739 740
	private toDispose: IDisposable[];
	private dropEnabled: boolean;
E
Erich Gamma 已提交
741 742 743 744 745

	constructor(
		@IMessageService private messageService: IMessageService,
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IFileService private fileService: IFileService,
746
		@IConfigurationService private configurationService: IConfigurationService,
E
Erich Gamma 已提交
747
		@IInstantiationService private instantiationService: IInstantiationService,
748
		@ITextFileService private textFileService: ITextFileService,
749 750
		@IBackupFileService private backupFileService: IBackupFileService,
		@IWindowService private windowService: IWindowService,
B
Benjamin Pasero 已提交
751
		@IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService
E
Erich Gamma 已提交
752
	) {
753 754
		super(stat => this.statToResource(stat));

755 756
		this.toDispose = [];

757
		this.updateDropEnablement();
758 759 760 761

		this.registerListeners();
	}

762 763
	private statToResource(stat: FileStat): URI {
		if (stat.isRoot) {
I
isidor 已提交
764 765
			return null; // Can not move root folder
		}
766

767
		if (stat.isDirectory) {
I
isidor 已提交
768
			return URI.from({ scheme: 'folder', path: stat.resource.path }); // indicates that we are dragging a folder
769 770
		}

771
		return stat.resource;
E
Erich Gamma 已提交
772 773
	}

774
	private registerListeners(): void {
775
		this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.updateDropEnablement()));
776
	}
J
Joao Moreno 已提交
777

778 779
	private updateDropEnablement(): void {
		this.dropEnabled = this.configurationService.getValue('explorer.enableDragAndDrop');
J
Joao Moreno 已提交
780 781
	}

B
Benjamin Pasero 已提交
782
	public onDragStart(tree: ITree, data: IDragAndDropData, originalEvent: DragMouseEvent): void {
783
		const sources: FileStat[] = data.getData();
E
Erich Gamma 已提交
784 785 786 787 788 789 790 791 792 793
		let source: FileStat = null;
		if (sources.length > 0) {
			source = sources[0];
		}

		// When dragging folders, make sure to collapse them to free up some space
		if (source && source.isDirectory && tree.isExpanded(source)) {
			tree.collapse(source, false);
		}

794 795 796
		// Apply some datatransfer types to allow for dragging the element outside of the application
		if (source) {
			if (!source.isDirectory) {
797
				originalEvent.dataTransfer.setData(DataTransfers.DOWNLOAD_URL, [MIME_BINARY, source.name, source.resource.toString()].join(':'));
E
Erich Gamma 已提交
798
			}
799

800
			originalEvent.dataTransfer.setData(DataTransfers.TEXT, getPathLabel(source.resource));
E
Erich Gamma 已提交
801 802 803
		}
	}

I
isidor 已提交
804
	public onDragOver(tree: ITree, data: IDragAndDropData, target: FileStat | Model, originalEvent: DragMouseEvent): IDragOverReaction {
805
		if (!this.dropEnabled) {
806 807 808
			return DRAG_OVER_REJECT;
		}

B
Benjamin Pasero 已提交
809
		const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh));
810
		const fromDesktop = data instanceof DesktopDragAndDropData;
E
Erich Gamma 已提交
811 812 813

		// Desktop DND
		if (fromDesktop) {
814
			const dragData = (<DesktopDragAndDropData>data).getData();
E
Erich Gamma 已提交
815

816 817
			const types = dragData.types;
			const typesArray: string[] = [];
E
Erich Gamma 已提交
818 819 820 821
			for (let i = 0; i < types.length; i++) {
				typesArray.push(types[i]);
			}

822
			if (typesArray.length === 0 || !typesArray.some(type => { return type === 'Files'; })) {
B
Benjamin Pasero 已提交
823
				return DRAG_OVER_REJECT;
E
Erich Gamma 已提交
824 825 826 827 828
			}
		}

		// Other-Tree DND
		else if (data instanceof ExternalElementsDragAndDropData) {
B
Benjamin Pasero 已提交
829
			return DRAG_OVER_REJECT;
E
Erich Gamma 已提交
830 831 832 833
		}

		// In-Explorer DND
		else {
834 835 836 837
			if (target instanceof Model) {
				return DRAG_OVER_REJECT;
			}

838
			const sources: FileStat[] = data.getData();
E
Erich Gamma 已提交
839
			if (!Array.isArray(sources)) {
B
Benjamin Pasero 已提交
840
				return DRAG_OVER_REJECT;
E
Erich Gamma 已提交
841 842 843 844 845 846 847
			}

			if (sources.some((source) => {
				if (source instanceof NewStatPlaceholder) {
					return true; // NewStatPlaceholders can not be moved
				}

848
				if (source.resource.toString() === target.resource.toString()) {
E
Erich Gamma 已提交
849 850 851
					return true; // Can not move anything onto itself
				}

I
isidor 已提交
852
				if (!isCopy && resources.dirname(source.resource).toString() === target.resource.toString()) {
E
Erich Gamma 已提交
853 854 855
					return true; // Can not move a file to the same parent unless we copy
				}

I
isidor 已提交
856
				if (resources.isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) {
E
Erich Gamma 已提交
857 858 859 860 861
					return true; // Can not move a parent folder into one of its children
				}

				return false;
			})) {
B
Benjamin Pasero 已提交
862
				return DRAG_OVER_REJECT;
E
Erich Gamma 已提交
863 864 865
			}
		}

866 867
		// All (target = model)
		if (target instanceof Model) {
868
			return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(false) : DRAG_OVER_REJECT; // can only drop folders to workspace
E
Erich Gamma 已提交
869 870
		}

871 872 873 874 875 876
		// All (target = file/folder)
		else {
			if (target.isDirectory) {
				return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_DOWN_COPY(true) : DRAG_OVER_ACCEPT_BUBBLE_DOWN(true);
			}

877
			if (this.contextService.getWorkspace().folders.every(folder => folder.uri.toString() !== target.resource.toString())) {
878 879
				return fromDesktop || isCopy ? DRAG_OVER_ACCEPT_BUBBLE_UP_COPY : DRAG_OVER_ACCEPT_BUBBLE_UP;
			}
E
Erich Gamma 已提交
880 881
		}

B
Benjamin Pasero 已提交
882
		return DRAG_OVER_REJECT;
E
Erich Gamma 已提交
883 884
	}

885
	public drop(tree: ITree, data: IDragAndDropData, target: FileStat | Model, originalEvent: DragMouseEvent): void {
886
		let promise: TPromise<void> = TPromise.as(null);
E
Erich Gamma 已提交
887 888 889

		// Desktop DND (Import file)
		if (data instanceof DesktopDragAndDropData) {
890
			promise = this.handleExternalDrop(tree, data, target, originalEvent);
E
Erich Gamma 已提交
891 892 893 894
		}

		// In-Explorer DND (Move/Copy file)
		else {
895 896 897 898 899 900 901 902 903
			if (target instanceof FileStat) {
				promise = this.handleExplorerDrop(tree, data, target, originalEvent);
			}
		}

		promise.done(null, errors.onUnexpectedError);
	}

	private handleExternalDrop(tree: ITree, data: DesktopDragAndDropData, target: FileStat | Model, originalEvent: DragMouseEvent): TPromise<void> {
904
		const droppedResources = extractResources(originalEvent.browserEvent as DragEvent, true);
E
Erich Gamma 已提交
905

906
		// Check for dropped external files to be folders
907
		return this.fileService.resolveFiles(droppedResources).then(result => {
E
Erich Gamma 已提交
908

909 910 911 912
			// Pass focus to window
			this.windowService.focusWindow();

			// Handle folders by adding to workspace if we are in workspace context
I
isidor 已提交
913
			const folders = result.filter(r => r.success && r.stat.isDirectory).map(result => ({ uri: result.stat.resource }));
914
			if (folders.length > 0) {
B
Benjamin Pasero 已提交
915

916 917 918
				// If we are in no-workspace context, ask for confirmation to create a workspace
				let confirmed = true;
				if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
919
					confirmed = this.messageService.confirm({
920 921 922 923 924
						message: folders.length > 1 ? nls.localize('dropFolders', "Do you want to add the folders to the workspace?") : nls.localize('dropFolder', "Do you want to add the folder to the workspace?"),
						type: 'question',
						primaryButton: folders.length > 1 ? nls.localize('addFolders', "&&Add Folders") : nls.localize('addFolder', "&&Add Folder")
					});
				}
B
Benjamin Pasero 已提交
925

926 927
				if (confirmed) {
					return this.workspaceEditingService.addFolders(folders);
E
Erich Gamma 已提交
928
				}
929
			}
E
Erich Gamma 已提交
930

931 932 933
			// Handle dropped files (only support FileStat as target)
			else if (target instanceof FileStat) {
				const importAction = this.instantiationService.createInstance(ImportFileAction, tree, target, null);
934 935

				return importAction.run(droppedResources.map(res => res.resource));
936
			}
937

938 939 940
			return void 0;
		});
	}
E
Erich Gamma 已提交
941

942 943 944
	private handleExplorerDrop(tree: ITree, data: IDragAndDropData, target: FileStat, originalEvent: DragMouseEvent): TPromise<void> {
		const source: FileStat = data.getData()[0];
		const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh);
E
Erich Gamma 已提交
945

946 947 948
		let confirmPromise: TPromise<IConfirmationResult>;

		// Handle confirm setting
S
Sandeep Somavarapu 已提交
949
		const confirmDragAndDrop = !isCopy && this.configurationService.getValue<boolean>(FileDragAndDrop.CONFIRM_DND_SETTING_KEY);
950
		if (confirmDragAndDrop) {
951
			confirmPromise = this.messageService.confirmWithCheckbox({
952 953 954 955
				message: nls.localize('confirmMove', "Are you sure you want to move '{0}'?", source.name),
				checkbox: {
					label: nls.localize('doNotAskAgain', "Do not ask me again")
				},
B
Benjamin Pasero 已提交
956 957
				type: 'question',
				primaryButton: nls.localize({ key: 'moveButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Move")
958 959 960 961 962 963 964 965 966
			});
		} else {
			confirmPromise = TPromise.as({ confirmed: true } as IConfirmationResult);
		}

		return confirmPromise.then(confirmation => {

			// Check for confirmation checkbox
			let updateConfirmSettingsPromise: TPromise<void> = TPromise.as(void 0);
967
			if (confirmation.confirmed && confirmation.checkboxChecked === true) {
968
				updateConfirmSettingsPromise = this.configurationService.updateValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY, false, ConfigurationTarget.USER);
969 970 971 972 973 974 975 976 977 978 979 980 981
			}

			return updateConfirmSettingsPromise.then(() => {
				if (confirmation.confirmed) {
					return this.doHandleExplorerDrop(tree, data, source, target, isCopy);
				}

				return TPromise.as(void 0);
			});
		});
	}

	private doHandleExplorerDrop(tree: ITree, data: IDragAndDropData, source: FileStat, target: FileStat, isCopy: boolean): TPromise<void> {
982
		return tree.expand(target).then(() => {
E
Erich Gamma 已提交
983

984 985 986 987
			// Reuse duplicate action if user copies
			if (isCopy) {
				return this.instantiationService.createInstance(DuplicateFileAction, tree, source, target).run();
			}
E
Erich Gamma 已提交
988

989 990 991 992 993 994 995 996 997
			const dirtyMoved: URI[] = [];

			// Success: load all files that are dirty again to restore their dirty contents
			// Error: discard any backups created during the process
			const onSuccess = () => TPromise.join(dirtyMoved.map(t => this.textFileService.models.loadOrCreate(t)));
			const onError = (error?: Error, showError?: boolean) => {
				if (showError) {
					this.messageService.show(Severity.Error, error);
				}
998

999 1000
				return TPromise.join(dirtyMoved.map(d => this.backupFileService.discardResourceBackup(d)));
			};
E
Erich Gamma 已提交
1001

1002
			// 1. check for dirty files that are being moved and backup to new target
I
isidor 已提交
1003
			const dirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, source.resource, !isLinux /* ignorecase */));
1004 1005
			return TPromise.join(dirty.map(d => {
				let moved: URI;
1006

1007
				// If the dirty file itself got moved, just reparent it to the target folder
I
isidor 已提交
1008 1009
				if (source.resource.toString() === d.toString()) {
					moved = target.resource.with({ path: paths.join(target.resource.path, source.name) });
1010
				}
E
Erich Gamma 已提交
1011

1012 1013
				// Otherwise, a parent of the dirty resource got moved, so we have to reparent more complicated. Example:
				else {
I
isidor 已提交
1014
					moved = target.resource.with({ path: paths.join(target.resource.path, d.path.substr(source.parent.resource.path.length + 1)) });
1015
				}
E
Erich Gamma 已提交
1016

1017
				dirtyMoved.push(moved);
E
Erich Gamma 已提交
1018

1019
				const model = this.textFileService.models.get(d);
E
Erich Gamma 已提交
1020

1021 1022
				return this.backupFileService.backupResource(moved, model.getValue(), model.getVersionId());
			}))
E
Erich Gamma 已提交
1023

1024 1025
				// 2. soft revert all dirty since we have backed up their contents
				.then(() => this.textFileService.revertAll(dirty, { soft: true /* do not attempt to load content from disk */ }))
E
Erich Gamma 已提交
1026

1027 1028
				// 3.) run the move operation
				.then(() => {
I
isidor 已提交
1029
					const targetResource = target.resource.with({ path: paths.join(target.resource.path, source.name) });
1030

1031
					return this.fileService.moveFile(source.resource, targetResource).then(null, error => {
1032

1033 1034 1035 1036 1037 1038 1039 1040
						// Conflict
						if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) {
							const confirm: IConfirmation = {
								message: nls.localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name),
								detail: nls.localize('irreversible', "This action is irreversible!"),
								primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
								type: 'warning'
							};
E
Erich Gamma 已提交
1041

1042
							// Move with overwrite if the user confirms
1043
							if (this.messageService.confirm(confirm)) {
I
isidor 已提交
1044
								const targetDirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, targetResource, !isLinux /* ignorecase */));
E
Erich Gamma 已提交
1045

1046 1047
								// Make sure to revert all dirty in target first to be able to overwrite properly
								return this.textFileService.revertAll(targetDirty, { soft: true /* do not attempt to load content from disk */ }).then(() => {
E
Erich Gamma 已提交
1048

1049 1050 1051 1052
									// Then continue to do the move operation
									return this.fileService.moveFile(source.resource, targetResource, true).then(onSuccess, error => onError(error, true));
								});
							}
E
Erich Gamma 已提交
1053

1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
							return onError();
						}

						return onError(error, true);
					});
				})

				// 4.) resolve those that were dirty to load their previous dirty contents from disk
				.then(onSuccess, onError);
		}, errors.onUnexpectedError);
E
Erich Gamma 已提交
1064
	}
1065
}