explorerView.ts 30.0 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';

7
import nls = require('vs/nls');
J
Johannes Rieken 已提交
8 9
import { TPromise } from 'vs/base/common/winjs.base';
import { Builder, $ } from 'vs/base/browser/builder';
E
Erich Gamma 已提交
10
import URI from 'vs/base/common/uri';
J
Johannes Rieken 已提交
11
import { ThrottledDelayer } from 'vs/base/common/async';
E
Erich Gamma 已提交
12
import errors = require('vs/base/common/errors');
13
import labels = require('vs/base/common/labels');
E
Erich Gamma 已提交
14
import paths = require('vs/base/common/paths');
J
Johannes Rieken 已提交
15 16 17 18
import { Action, IActionRunner, IAction } from 'vs/base/common/actions';
import { prepareActions } from 'vs/workbench/browser/actionBarRegistry';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
19
import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocussedContext, ExplorerFocussedContext } from 'vs/workbench/parts/files/common/files';
B
Benjamin Pasero 已提交
20
import { FileOperation, FileOperationEvent, IResolveFileOptions, FileChangeType, FileChangesEvent, IFileChange, IFileService, isEqual, isEqualOrParent } from 'vs/platform/files/common/files';
21
import { RefreshViewExplorerAction, NewFolderAction, NewFileAction } from 'vs/workbench/parts/files/browser/fileActions';
J
Johannes Rieken 已提交
22
import { FileDragAndDrop, FileFilter, FileSorter, FileController, FileRenderer, FileDataSource, FileViewletState, FileAccessibilityProvider } from 'vs/workbench/parts/files/browser/views/explorerViewer';
E
Erich Gamma 已提交
23
import lifecycle = require('vs/base/common/lifecycle');
24
import { toResource } from 'vs/workbench/common/editor';
25
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
J
Johannes Rieken 已提交
26 27
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
M
Maxime Quandalle 已提交
28
import * as DOM from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
29 30
import { CollapseAction, CollapsibleViewletView } from 'vs/workbench/browser/viewlet';
import { FileStat } from 'vs/workbench/parts/files/common/explorerViewModel';
31
import { IListService } from 'vs/platform/list/browser/listService';
J
Johannes Rieken 已提交
32 33
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
34
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
J
Johannes Rieken 已提交
35 36 37 38 39 40
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
41
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
J
Johannes Rieken 已提交
42
import { ResourceContextKey } from 'vs/workbench/common/resourceContextKey';
43
import { IWorkbenchThemeService, IFileIconTheme } from 'vs/workbench/services/themes/common/themeService';
44
import { isLinux } from 'vs/base/common/platform';
E
Erich Gamma 已提交
45 46 47 48 49 50 51 52 53 54

export class ExplorerView extends CollapsibleViewletView {

	private static EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first
	private static EXPLORER_FILE_CHANGES_REFRESH_DELAY = 100; // delay in ms to refresh the explorer from disk file changes
	private static EXPLORER_IMPORT_REFRESH_DELAY = 300; // delay in ms to refresh the explorer from imports

	private static MEMENTO_LAST_ACTIVE_FILE_RESOURCE = 'explorer.memento.lastActiveFileResource';
	private static MEMENTO_EXPANDED_FOLDER_RESOURCES = 'explorer.memento.expandedFolderResources';

B
Benjamin Pasero 已提交
55 56
	private static COMMON_SCM_FOLDERS = ['.git', '.svn', '.hg'];

E
Erich Gamma 已提交
57 58 59 60
	private explorerViewer: ITree;
	private filter: FileFilter;
	private viewletState: FileViewletState;

J
Joao Moreno 已提交
61 62
	private explorerRefreshDelayer: ThrottledDelayer<void>;
	private explorerImportDelayer: ThrottledDelayer<void>;
E
Erich Gamma 已提交
63

64
	private resourceContext: ResourceContextKey;
65
	private folderContext: IContextKey<boolean>;
66

67
	private filesExplorerFocussedContext: IContextKey<boolean>;
68 69
	private explorerFocussedContext: IContextKey<boolean>;

E
Erich Gamma 已提交
70 71
	private shouldRefresh: boolean;

72 73
	private autoReveal: boolean;

E
Erich Gamma 已提交
74 75 76 77 78 79
	private settings: any;

	constructor(
		viewletState: FileViewletState,
		actionRunner: IActionRunner,
		settings: any,
I
isidor 已提交
80
		headerSize: number,
E
Erich Gamma 已提交
81 82 83
		@IMessageService messageService: IMessageService,
		@IContextMenuService contextMenuService: IContextMenuService,
		@IInstantiationService private instantiationService: IInstantiationService,
84
		@IEditorGroupService private editorGroupService: IEditorGroupService,
E
Erich Gamma 已提交
85 86
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IProgressService private progressService: IProgressService,
87
		@IListService private listService: IListService,
E
Erich Gamma 已提交
88 89 90
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@IFileService private fileService: IFileService,
		@IPartService private partService: IPartService,
B
Benjamin Pasero 已提交
91
		@IKeybindingService keybindingService: IKeybindingService,
92
		@IContextKeyService contextKeyService: IContextKeyService,
93
		@IConfigurationService private configurationService: IConfigurationService,
94
		@IWorkbenchThemeService private themeService: IWorkbenchThemeService
E
Erich Gamma 已提交
95
	) {
I
isidor 已提交
96
		super(actionRunner, false, nls.localize('explorerSection', "Files Explorer Section"), messageService, keybindingService, contextMenuService, headerSize);
E
Erich Gamma 已提交
97 98 99 100

		this.settings = settings;
		this.viewletState = viewletState;
		this.actionRunner = actionRunner;
101
		this.autoReveal = true;
E
Erich Gamma 已提交
102

J
Joao Moreno 已提交
103 104
		this.explorerRefreshDelayer = new ThrottledDelayer<void>(ExplorerView.EXPLORER_FILE_CHANGES_REFRESH_DELAY);
		this.explorerImportDelayer = new ThrottledDelayer<void>(ExplorerView.EXPLORER_IMPORT_REFRESH_DELAY);
105 106

		this.resourceContext = instantiationService.createInstance(ResourceContextKey);
107
		this.folderContext = ExplorerFolderContext.bindTo(contextKeyService);
108 109

		this.filesExplorerFocussedContext = FilesExplorerFocussedContext.bindTo(contextKeyService);
110
		this.explorerFocussedContext = ExplorerFocussedContext.bindTo(contextKeyService);
E
Erich Gamma 已提交
111 112 113
	}

	public renderHeader(container: HTMLElement): void {
114
		const titleDiv = $('div.title').appendTo(container);
115
		$('span').text(this.contextService.getWorkspace().name).title(labels.getPathLabel(this.contextService.getWorkspace().resource.fsPath)).appendTo(titleDiv);
I
isidor 已提交
116 117

		super.renderHeader(container);
E
Erich Gamma 已提交
118 119 120 121 122
	}

	public renderBody(container: HTMLElement): void {
		this.treeContainer = super.renderViewTree(container);
		DOM.addClass(this.treeContainer, 'explorer-folders-view');
123
		DOM.addClass(this.treeContainer, 'show-file-icons');
E
Erich Gamma 已提交
124 125 126

		this.tree = this.createViewer($(this.treeContainer));

I
isidor 已提交
127 128 129
		if (this.toolBar) {
			this.toolBar.setActions(prepareActions(this.getActions()), [])();
		}
130 131 132 133 134 135 136

		const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => {
			DOM.toggleClass(this.treeContainer, 'align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons);
		};

		this.themeService.onDidFileIconThemeChange(onFileIconThemeChange);
		onFileIconThemeChange(this.themeService.getFileIconTheme());
E
Erich Gamma 已提交
137 138
	}

I
isidor 已提交
139 140
	public getActions(): IAction[] {
		const actions: Action[] = [];
E
Erich Gamma 已提交
141

B
polish  
Benjamin Pasero 已提交
142
		actions.push(this.instantiationService.createInstance(NewFileAction, this.getViewer(), null));
E
Erich Gamma 已提交
143 144 145 146 147 148
		actions.push(this.instantiationService.createInstance(NewFolderAction, this.getViewer(), null));
		actions.push(this.instantiationService.createInstance(RefreshViewExplorerAction, this, 'explorer-action refresh-explorer'));
		actions.push(this.instantiationService.createInstance(CollapseAction, this.getViewer(), true, 'explorer-action collapse-explorer'));

		// Set Order
		for (let i = 0; i < actions.length; i++) {
149
			const action = actions[i];
E
Erich Gamma 已提交
150 151 152
			action.order = 10 * (i + 1);
		}

I
isidor 已提交
153
		return actions;
E
Erich Gamma 已提交
154 155 156
	}

	public create(): TPromise<void> {
B
Benjamin Pasero 已提交
157

158 159 160
		// Update configuration
		const configuration = this.configurationService.getConfiguration<IFilesConfiguration>();
		this.onConfigurationUpdated(configuration);
E
Erich Gamma 已提交
161

162
		// Load and Fill Viewer
163
		return this.doRefresh().then(() => {
E
Erich Gamma 已提交
164

165
			// When the explorer viewer is loaded, listen to changes to the editor input
166
			this.toDispose.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
E
Erich Gamma 已提交
167

168
			// Also handle configuration updates
169
			this.toDispose.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationUpdated(e.config, true)));
E
Erich Gamma 已提交
170 171 172
		});
	}

173
	private onEditorsChanged(): void {
174 175 176 177
		if (!this.autoReveal) {
			return; // do not touch selection or focus if autoReveal === false
		}

178
		let clearSelection = true;
179
		let clearFocus = false;
E
Erich Gamma 已提交
180

181 182 183
		// Handle files
		const activeFile = this.getActiveFile();
		if (activeFile) {
E
Erich Gamma 已提交
184 185

			// Always remember last opened file
186
			this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE] = activeFile.toString();
E
Erich Gamma 已提交
187

188
			// Select file if input is inside workspace
189 190
			if (this.isVisible && this.contextService.isInsideWorkspace(activeFile)) {
				const selection = this.hasSelection(activeFile);
191
				if (!selection) {
192
					this.select(activeFile).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
193
				}
194 195

				clearSelection = false;
E
Erich Gamma 已提交
196 197 198
			}
		}

199
		// Handle closed or untitled file (convince explorer to not reopen any file when getting visible)
200
		const activeInput = this.editorService.getActiveEditorInput();
201
		if (activeInput instanceof UntitledEditorInput || !activeInput) {
202
			this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE] = void 0;
203
			clearFocus = true;
204 205
		}

E
Erich Gamma 已提交
206
		// Otherwise clear
207
		if (clearSelection) {
208
			this.explorerViewer.clearSelection();
E
Erich Gamma 已提交
209
		}
210 211 212 213

		if (clearFocus) {
			this.explorerViewer.clearFocus();
		}
E
Erich Gamma 已提交
214 215 216
	}

	private onConfigurationUpdated(configuration: IFilesConfiguration, refresh?: boolean): void {
B
Benjamin Pasero 已提交
217 218 219 220
		if (this.isDisposed) {
			return; // guard against possible race condition when config change causes recreate of views
		}

221
		this.autoReveal = configuration && configuration.explorer && configuration.explorer.autoReveal;
E
Erich Gamma 已提交
222 223 224 225 226 227 228 229 230

		// Push down config updates to components of viewer
		let needsRefresh = false;
		if (this.filter) {
			needsRefresh = this.filter.updateConfiguration(configuration);
		}

		// Refresh viewer as needed
		if (refresh && needsRefresh) {
231
			this.doRefresh().done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
232 233 234
		}
	}

235
	public focusBody(): void {
236
		let keepFocus = false;
237 238 239 240

		// Make sure the current selected element is revealed
		if (this.explorerViewer) {
			if (this.autoReveal) {
241
				const selection = this.explorerViewer.getSelection();
242 243 244 245 246 247 248
				if (selection.length > 0) {
					this.reveal(selection[0], 0.5).done(null, errors.onUnexpectedError);
				}
			}

			// Pass Focus to Viewer
			this.explorerViewer.DOMFocus();
249
			keepFocus = true;
250
		}
E
Erich Gamma 已提交
251

252
		// Open the focused element in the editor if there is currently no file opened
253 254
		const activeFile = this.getActiveFile();
		if (!activeFile) {
255
			this.openFocusedElement(keepFocus);
E
Erich Gamma 已提交
256 257 258 259 260 261 262 263 264 265
		}
	}

	public setVisible(visible: boolean): TPromise<void> {
		return super.setVisible(visible).then(() => {

			// Show
			if (visible) {

				// If a refresh was requested and we are now visible, run it
A
Alex Dima 已提交
266
				let refreshPromise = TPromise.as(null);
E
Erich Gamma 已提交
267
				if (this.shouldRefresh) {
268
					refreshPromise = this.doRefresh();
E
Erich Gamma 已提交
269 270 271
					this.shouldRefresh = false; // Reset flag
				}

272 273 274 275
				if (!this.autoReveal) {
					return refreshPromise; // do not react to setVisible call if autoReveal === false
				}

276
				// Always select the current navigated file in explorer if input is file editor input
277
				// unless autoReveal is set to false
278 279
				const activeFile = this.getActiveFile();
				if (activeFile) {
E
Erich Gamma 已提交
280
					return refreshPromise.then(() => {
281
						return this.select(activeFile);
E
Erich Gamma 已提交
282 283 284 285 286
					});
				}

				// Return now if the workbench has not yet been created - in this case the workbench takes care of restoring last used editors
				if (!this.partService.isCreated()) {
A
Alex Dima 已提交
287
					return TPromise.as(null);
E
Erich Gamma 已提交
288 289 290 291 292 293 294 295
				}

				// Otherwise restore last used file: By lastActiveFileResource
				let lastActiveFileResource: URI;
				if (this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE]) {
					lastActiveFileResource = URI.parse(this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE]);
				}

296
				if (lastActiveFileResource && this.root && this.root.find(lastActiveFileResource)) {
297
					this.editorService.openEditor({ resource: lastActiveFileResource, options: { revealIfVisible: true } }).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
298 299 300 301 302 303

					return refreshPromise;
				}

				// Otherwise restore last used file: By Explorer selection
				return refreshPromise.then(() => {
304
					this.openFocusedElement();
E
Erich Gamma 已提交
305 306
				});
			}
307
			return undefined;
E
Erich Gamma 已提交
308 309 310
		});
	}

311
	private openFocusedElement(preserveFocus?: boolean): void {
312
		const stat: FileStat = this.explorerViewer.getFocus();
E
Erich Gamma 已提交
313
		if (stat && !stat.isDirectory) {
314
			this.editorService.openEditor({ resource: stat.resource, options: { preserveFocus, revealIfVisible: true } }).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
315 316 317
		}
	}

318
	private getActiveFile(): URI {
319
		const input = this.editorService.getActiveEditorInput();
320 321 322 323

		// ignore diff editor inputs (helps to get out of diffing when returning to explorer)
		if (input instanceof DiffEditorInput) {
			return null;
E
Erich Gamma 已提交
324 325
		}

326
		// check for files
327
		return toResource(input, { supportSideBySide: true, filter: 'file' });
E
Erich Gamma 已提交
328 329
	}

330
	private get root(): FileStat {
E
Erich Gamma 已提交
331 332 333 334
		return this.explorerViewer ? (<FileStat>this.explorerViewer.getInput()) : null;
	}

	public createViewer(container: Builder): ITree {
B
Benjamin Pasero 已提交
335
		const dataSource = this.instantiationService.createInstance(FileDataSource);
336
		const renderer = this.instantiationService.createInstance(FileRenderer, this.viewletState, this.actionRunner);
B
Benjamin Pasero 已提交
337 338
		const controller = this.instantiationService.createInstance(FileController, this.viewletState);
		const sorter = new FileSorter();
E
Erich Gamma 已提交
339
		this.filter = this.instantiationService.createInstance(FileFilter);
B
Benjamin Pasero 已提交
340 341
		const dnd = this.instantiationService.createInstance(FileDragAndDrop);
		const accessibilityProvider = this.instantiationService.createInstance(FileAccessibilityProvider);
E
Erich Gamma 已提交
342 343

		this.explorerViewer = new Tree(container.getHTMLElement(), {
B
Benjamin Pasero 已提交
344 345 346 347
			dataSource,
			renderer,
			controller,
			sorter,
E
Erich Gamma 已提交
348
			filter: this.filter,
B
Benjamin Pasero 已提交
349 350
			dnd,
			accessibilityProvider
E
Erich Gamma 已提交
351
		}, {
352
				autoExpandSingleChildren: true,
353
				ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"),
354
				twistiePixels: 12,
355 356
				showTwistie: false,
				keyboardSupport: false
357
			});
E
Erich Gamma 已提交
358 359 360

		this.toDispose.push(lifecycle.toDisposable(() => renderer.dispose()));

361
		// Register to list service
362
		this.toDispose.push(this.listService.register(this.explorerViewer, [this.explorerFocussedContext, this.filesExplorerFocussedContext]));
363

E
Erich Gamma 已提交
364
		// Update Viewer based on File Change Events
365 366
		this.toDispose.push(this.fileService.onAfterOperation(e => this.onFileOperation(e)));
		this.toDispose.push(this.fileService.onFileChanges(e => this.onFileChanges(e)));
E
Erich Gamma 已提交
367

368
		// Update resource context based on focused element
369 370 371 372
		this.toDispose.push(this.explorerViewer.addListener2('focus', (e: { focus: FileStat }) => {
			this.resourceContext.set(e.focus && e.focus.resource);
			this.folderContext.set(e.focus && e.focus.isDirectory);
		}));
373

374 375 376
		// Open when selecting via keyboard
		this.toDispose.push(this.explorerViewer.addListener2('selection', event => {
			if (event && event.payload && event.payload.origin === 'keyboard') {
B
Benjamin Pasero 已提交
377 378 379 380 381 382 383 384 385
				const element = this.tree.getFocus();

				if (element instanceof FileStat) {
					if (element.isDirectory) {
						this.explorerViewer.toggleExpansion(element);
					}

					controller.openEditor(element, { pinned: false, sideBySide: false, preserveFocus: false });
				}
386 387 388
			}
		}));

E
Erich Gamma 已提交
389 390 391
		return this.explorerViewer;
	}

M
Maxime Quandalle 已提交
392
	public getOptimalWidth(): number {
393
		const parentNode = this.explorerViewer.getHTMLElement();
394
		const childNodes = [].slice.call(parentNode.querySelectorAll('.explorer-item > a'));
B
polish  
Benjamin Pasero 已提交
395

M
Maxime Quandalle 已提交
396 397 398
		return DOM.getLargestChildWidth(parentNode, childNodes);
	}

399
	private onFileOperation(e: FileOperationEvent): void {
B
Benjamin Pasero 已提交
400 401 402 403
		if (!this.root) {
			return; // ignore if not yet created
		}

E
Erich Gamma 已提交
404 405 406 407 408 409
		let modelElement: FileStat;
		let parent: FileStat;
		let parentResource: URI;
		let parentElement: FileStat;

		// Add
410 411
		if (e.operation === FileOperation.CREATE || e.operation === FileOperation.IMPORT || e.operation === FileOperation.COPY) {
			const addedElement = e.target;
E
Erich Gamma 已提交
412
			parentResource = URI.file(paths.dirname(addedElement.resource.fsPath));
413
			parentElement = this.root.find(parentResource);
E
Erich Gamma 已提交
414 415 416 417

			if (parentElement) {

				// Add the new file to its parent (Model)
418
				const childElement = FileStat.create(addedElement);
419
				parentElement.removeChild(childElement); // make sure to remove any previous version of the file if any
E
Erich Gamma 已提交
420 421
				parentElement.addChild(childElement);

422 423 424
				// Refresh the Parent (View)
				this.explorerViewer.refresh(parentElement).then(() => {
					return this.reveal(childElement, 0.5).then(() => {
E
Erich Gamma 已提交
425

426 427
						// Focus new element
						this.explorerViewer.setFocus(childElement);
E
Erich Gamma 已提交
428
					});
429
				}).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
430 431 432 433
			}
		}

		// Move (including Rename)
434 435 436
		else if (e.operation === FileOperation.MOVE) {
			const oldResource = e.resource;
			const newElement = e.target;
E
Erich Gamma 已提交
437

438
			const oldParentResource = URI.file(paths.dirname(oldResource.fsPath));
439
			const newParentResource = URI.file(paths.dirname(newElement.resource.fsPath));
E
Erich Gamma 已提交
440 441

			// Only update focus if renamed/moved element is selected
442
			let restoreFocus = false;
443
			const focus: FileStat = this.explorerViewer.getFocus();
B
wip  
Benjamin Pasero 已提交
444
			if (focus && focus.resource && isEqual(focus.resource.fsPath, oldResource.fsPath)) {
445
				restoreFocus = true;
E
Erich Gamma 已提交
446 447 448
			}

			// Handle Rename
B
wip  
Benjamin Pasero 已提交
449
			if (oldParentResource && newParentResource && isEqual(oldParentResource.fsPath, newParentResource.fsPath)) {
450
				modelElement = this.root.find(oldResource);
E
Erich Gamma 已提交
451 452 453 454 455 456 457 458 459 460 461
				if (modelElement) {

					// Rename File (Model)
					modelElement.rename(newElement);

					// Update Parent (View)
					parent = modelElement.parent;
					if (parent) {
						this.explorerViewer.refresh(parent).done(() => {

							// Select in Viewer if set
462
							if (restoreFocus) {
E
Erich Gamma 已提交
463 464 465 466 467 468 469 470 471
								this.explorerViewer.setFocus(modelElement);
							}
						}, errors.onUnexpectedError);
					}
				}
			}

			// Handle Move
			else if (oldParentResource && newParentResource) {
472 473 474
				const oldParent = this.root.find(oldParentResource);
				const newParent = this.root.find(newParentResource);
				modelElement = this.root.find(oldResource);
E
Erich Gamma 已提交
475 476 477 478 479 480 481 482 483 484 485

				if (oldParent && newParent && modelElement) {

					// Move in Model
					modelElement.move(newParent, (callback: () => void) => {

						// Update old parent
						this.explorerViewer.refresh(oldParent, true).done(callback, errors.onUnexpectedError);
					}, () => {

						// Update new parent
486
						this.explorerViewer.refresh(newParent, true).done(() => this.explorerViewer.expand(newParent), errors.onUnexpectedError);
E
Erich Gamma 已提交
487 488 489 490 491 492
					});
				}
			}
		}

		// Delete
493
		else if (e.operation === FileOperation.DELETE) {
494
			modelElement = this.root.find(e.resource);
E
Erich Gamma 已提交
495 496 497 498 499 500 501
			if (modelElement && modelElement.parent) {
				parent = modelElement.parent;

				// Remove Element from Parent (Model)
				parent.removeChild(modelElement);

				// Refresh Parent (View)
502
				const restoreFocus = this.explorerViewer.isDOMFocused();
E
Erich Gamma 已提交
503 504 505
				this.explorerViewer.refresh(parent).done(() => {

					// Ensure viewer has keyboard focus if event originates from viewer
506 507 508
					if (restoreFocus) {
						this.explorerViewer.DOMFocus();
					}
E
Erich Gamma 已提交
509 510 511 512 513 514 515
				}, errors.onUnexpectedError);
			}
		}
	}

	private onFileChanges(e: FileChangesEvent): void {

516 517 518 519
		// Ensure memento state does not capture a deleted file (we run this from a timeout because
		// delete events can result in UI activity that will fill the memento again when multiple
		// editors are closing)
		setTimeout(() => {
520
			const lastActiveResource: string = this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE];
521 522 523 524
			if (lastActiveResource && e.contains(URI.parse(lastActiveResource), FileChangeType.DELETED)) {
				this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE] = null;
			}
		});
E
Erich Gamma 已提交
525 526 527 528

		// Check if an explorer refresh is necessary (delayed to give internal events a chance to react first)
		// Note: there is no guarantee when the internal events are fired vs real ones. Code has to deal with the fact that one might
		// be fired first over the other or not at all.
529
		setTimeout(() => {
E
Erich Gamma 已提交
530 531 532
			if (!this.shouldRefresh && this.shouldRefreshFromEvent(e)) {
				this.refreshFromEvent();
			}
533
		}, ExplorerView.EXPLORER_FILE_CHANGES_REACT_DELAY);
E
Erich Gamma 已提交
534 535 536 537 538 539
	}

	private shouldRefreshFromEvent(e: FileChangesEvent): boolean {

		// Filter to the ones we care
		e = this.filterToAddRemovedOnWorkspacePath(e, (event, segments) => {
B
Benjamin Pasero 已提交
540 541 542 543 544 545
			if (
				segments[0] !== ExplorerView.COMMON_SCM_FOLDERS[0] &&
				segments[0] !== ExplorerView.COMMON_SCM_FOLDERS[1] &&
				segments[0] !== ExplorerView.COMMON_SCM_FOLDERS[2]
			) {
				return true; // we like all things outside common SCM folders
E
Erich Gamma 已提交
546 547
			}

B
Benjamin Pasero 已提交
548
			return segments.length === 1; // otherwise we only care about the SCM folder itself
E
Erich Gamma 已提交
549 550 551 552
		});

		// We only ever refresh from files/folders that got added or deleted
		if (e.gotAdded() || e.gotDeleted()) {
553 554
			const added = e.getAdded();
			const deleted = e.getDeleted();
E
Erich Gamma 已提交
555

556
			if (!this.root) {
E
Erich Gamma 已提交
557 558 559 560
				return false;
			}

			// Check added: Refresh if added file/folder is not part of resolved root and parent is part of it
561
			const ignoredPaths: { [fsPath: string]: boolean } = <{ [fsPath: string]: boolean }>{};
E
Erich Gamma 已提交
562
			for (let i = 0; i < added.length; i++) {
563
				const change = added[i];
E
Erich Gamma 已提交
564 565 566 567 568
				if (!this.contextService.isInsideWorkspace(change.resource)) {
					continue; // out of workspace file
				}

				// Find parent
569
				const parent = paths.dirname(change.resource.fsPath);
E
Erich Gamma 已提交
570 571 572 573 574 575 576

				// Continue if parent was already determined as to be ignored
				if (ignoredPaths[parent]) {
					continue;
				}

				// Compute if parent is visible and added file not yet part of it
577 578
				const parentStat = this.root.find(URI.file(parent));
				if (parentStat && parentStat.isDirectoryResolved && !this.root.find(change.resource)) {
E
Erich Gamma 已提交
579 580 581 582 583 584 585 586 587 588 589
					return true;
				}

				// Keep track of path that can be ignored for faster lookup
				if (!parentStat || !parentStat.isDirectoryResolved) {
					ignoredPaths[parent] = true;
				}
			}

			// Check deleted: Refresh if deleted file/folder part of resolved root
			for (let j = 0; j < deleted.length; j++) {
590
				const del = deleted[j];
E
Erich Gamma 已提交
591 592 593 594
				if (!this.contextService.isInsideWorkspace(del.resource)) {
					continue; // out of workspace file
				}

595
				if (this.root.find(del.resource)) {
E
Erich Gamma 已提交
596 597 598 599 600 601 602 603 604
					return true;
				}
			}
		}

		return false;
	}

	private filterToAddRemovedOnWorkspacePath(e: FileChangesEvent, fn: (change: IFileChange, workspacePathSegments: string[]) => boolean): FileChangesEvent {
B
polish  
Benjamin Pasero 已提交
605
		return new FileChangesEvent(e.changes.filter(change => {
E
Erich Gamma 已提交
606 607 608 609
			if (change.type === FileChangeType.UPDATED) {
				return false; // we only want added / removed
			}

610
			const workspacePath = this.contextService.toWorkspaceRelativePath(change.resource);
E
Erich Gamma 已提交
611 612 613 614
			if (!workspacePath) {
				return false; // not inside workspace
			}

615
			const segments = workspacePath.split(/\//);
E
Erich Gamma 已提交
616 617 618 619 620 621 622 623 624

			return fn(change, segments);
		}));
	}

	private refreshFromEvent(): void {
		if (this.isVisible) {
			this.explorerRefreshDelayer.trigger(() => {
				if (!this.explorerViewer.getHighlight()) {
625
					return this.doRefresh();
E
Erich Gamma 已提交
626 627
				}

A
Alex Dima 已提交
628
				return TPromise.as(null);
E
Erich Gamma 已提交
629 630 631 632 633 634 635 636 637
			}).done(null, errors.onUnexpectedError);
		} else {
			this.shouldRefresh = true;
		}
	}

	/**
	 * Refresh the contents of the explorer to get up to date data from the disk about the file structure.
	 */
638 639 640 641 642 643 644 645 646 647 648
	public refresh(): TPromise<void> {
		if (!this.explorerViewer || this.explorerViewer.getHighlight()) {
			return TPromise.as(null);
		}

		// Focus
		this.explorerViewer.DOMFocus();

		// Find resource to focus from active editor input if set
		let resourceToFocus: URI;
		if (this.autoReveal) {
649
			resourceToFocus = this.getActiveFile();
650
			if (!resourceToFocus) {
651
				const selection = this.explorerViewer.getSelection();
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667
				if (selection && selection.length === 1) {
					resourceToFocus = (<FileStat>selection[0]).resource;
				}
			}
		}

		return this.doRefresh().then(() => {
			if (resourceToFocus) {
				return this.select(resourceToFocus, true);
			}

			return TPromise.as(null);
		});
	}

	private doRefresh(): TPromise<void> {
668
		const targetsToResolve: URI[] = [];
E
Erich Gamma 已提交
669 670 671
		let targetsToExpand: URI[] = [];

		if (this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES]) {
B
Benjamin Pasero 已提交
672
			targetsToExpand = this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES].map((e: string) => URI.parse(e));
E
Erich Gamma 已提交
673 674 675
		}

		// First time refresh: Receive target through active editor input or selection and also include settings from previous session
676
		if (!this.root) {
677 678
			const activeFile = this.getActiveFile();
			if (activeFile) {
679
				targetsToResolve.push(activeFile);
E
Erich Gamma 已提交
680 681 682 683 684 685 686 687 688
			}

			if (targetsToExpand.length) {
				targetsToResolve.push(...targetsToExpand);
			}
		}

		// Subsequent refresh: Receive targets through expanded folders in tree
		else {
689
			this.getResolvedDirectories(this.root, targetsToResolve);
E
Erich Gamma 已提交
690 691 692
		}

		// Load Root Stat with given target path configured
693
		const options: IResolveFileOptions = { resolveTo: targetsToResolve };
694
		const promise = this.fileService.resolveFile(this.contextService.getWorkspace().resource, options).then(stat => {
695
			let explorerPromise: TPromise<void>;
E
Erich Gamma 已提交
696 697

			// Convert to model
698
			const modelStat = FileStat.create(stat, options.resolveTo);
E
Erich Gamma 已提交
699 700

			// First time refresh: The stat becomes the input of the viewer
701
			if (!this.root) {
E
Erich Gamma 已提交
702 703 704 705
				explorerPromise = this.explorerViewer.setInput(modelStat).then(() => {

					// Make sure to expand all folders that where expanded in the previous session
					if (targetsToExpand) {
706
						return this.explorerViewer.expandAll(targetsToExpand.map(expand => this.root.find(expand)));
E
Erich Gamma 已提交
707 708
					}

A
Alex Dima 已提交
709
					return TPromise.as(null);
E
Erich Gamma 已提交
710 711 712 713 714
				});
			}

			// Subsequent refresh: Merge stat into our local model and refresh tree
			else {
715
				FileStat.mergeLocalWithDisk(modelStat, this.root);
E
Erich Gamma 已提交
716

717
				explorerPromise = this.explorerViewer.refresh(this.root);
E
Erich Gamma 已提交
718 719
			}

720
			return explorerPromise;
721
		}, (e: any) => TPromise.wrapError(e));
E
Erich Gamma 已提交
722

723
		this.progressService.showWhile(promise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */);
E
Erich Gamma 已提交
724 725 726 727 728 729 730 731 732

		return promise;
	}

	/**
	 * Given a stat, fills an array of path that make all folders below the stat that are resolved directories.
	 */
	private getResolvedDirectories(stat: FileStat, resolvedDirectories: URI[]): void {
		if (stat.isDirectoryResolved) {
B
wip  
Benjamin Pasero 已提交
733
			if (!isEqual(stat.resource.fsPath, this.contextService.getWorkspace().resource.fsPath)) {
E
Erich Gamma 已提交
734 735 736

				// Drop those path which are parents of the current one
				for (let i = resolvedDirectories.length - 1; i >= 0; i--) {
737
					const resource = resolvedDirectories[i];
738
					if (isEqualOrParent(stat.resource.fsPath, resource.fsPath, !isLinux /* ignorecase */)) {
E
Erich Gamma 已提交
739 740 741 742 743 744 745 746 747 748
						resolvedDirectories.splice(i);
					}
				}

				// Add to the list of path to resolve
				resolvedDirectories.push(stat.resource);
			}

			// Recurse into children
			for (let i = 0; i < stat.children.length; i++) {
749
				const child = stat.children[i];
E
Erich Gamma 已提交
750 751 752 753 754 755 756 757 758
				this.getResolvedDirectories(child, resolvedDirectories);
			}
		}
	}

	/**
	 * Selects and reveal the file element provided by the given resource if its found in the explorer. Will try to
	 * resolve the path from the disk in case the explorer is not yet expanded to the file yet.
	 */
759
	public select(resource: URI, reveal: boolean = this.autoReveal): TPromise<void> {
E
Erich Gamma 已提交
760 761

		// Require valid path
B
wip  
Benjamin Pasero 已提交
762
		if (!resource || isEqual(resource.fsPath, this.contextService.getWorkspace().resource.fsPath)) {
A
Alex Dima 已提交
763
			return TPromise.as(null);
E
Erich Gamma 已提交
764 765 766
		}

		// If path already selected, just reveal and return
767
		const selection = this.hasSelection(resource);
768 769
		if (selection) {
			return reveal ? this.reveal(selection, 0.5) : TPromise.as(null);
E
Erich Gamma 已提交
770 771 772
		}

		// First try to get the stat object from the input to avoid a roundtrip
773
		if (!this.root) {
A
Alex Dima 已提交
774
			return TPromise.as(null);
E
Erich Gamma 已提交
775 776
		}

777
		const fileStat = this.root.find(resource);
E
Erich Gamma 已提交
778
		if (fileStat) {
779
			return this.doSelect(fileStat, reveal);
E
Erich Gamma 已提交
780 781 782
		}

		// Stat needs to be resolved first and then revealed
783
		const options: IResolveFileOptions = { resolveTo: [resource] };
784
		return this.fileService.resolveFile(this.contextService.getWorkspace().resource, options).then(stat => {
E
Erich Gamma 已提交
785 786

			// Convert to model
787
			const modelStat = FileStat.create(stat, options.resolveTo);
E
Erich Gamma 已提交
788 789

			// Update Input with disk Stat
790
			FileStat.mergeLocalWithDisk(modelStat, this.root);
E
Erich Gamma 已提交
791 792

			// Select and Reveal
793
			return this.explorerViewer.refresh(this.root).then(() => this.doSelect(this.root.find(resource), reveal));
B
polish  
Benjamin Pasero 已提交
794

E
Erich Gamma 已提交
795 796 797
		}, (e: any) => this.messageService.show(Severity.Error, e));
	}

798
	private hasSelection(resource: URI): FileStat {
799
		const currentSelection: FileStat[] = this.explorerViewer.getSelection();
B
polish  
Benjamin Pasero 已提交
800

801
		for (let i = 0; i < currentSelection.length; i++) {
B
wip  
Benjamin Pasero 已提交
802
			if (isEqual(currentSelection[i].resource.fsPath, resource.fsPath)) {
803 804 805 806 807 808 809 810
				return currentSelection[i];
			}
		}

		return null;
	}

	private doSelect(fileStat: FileStat, reveal: boolean): TPromise<void> {
E
Erich Gamma 已提交
811
		if (!fileStat) {
A
Alex Dima 已提交
812
			return TPromise.as(null);
E
Erich Gamma 已提交
813 814 815 816 817 818 819
		}

		// Special case: we are asked to reveal and select an element that is not visible
		// In this case we take the parent element so that we are at least close to it.
		if (!this.filter.isVisible(this.tree, fileStat)) {
			fileStat = fileStat.parent;
			if (!fileStat) {
A
Alex Dima 已提交
820
				return TPromise.as(null);
E
Erich Gamma 已提交
821 822 823
			}
		}

824 825 826 827 828 829 830 831 832
		// Reveal depending on flag
		let revealPromise: TPromise<void>;
		if (reveal) {
			revealPromise = this.reveal(fileStat, 0.5);
		} else {
			revealPromise = TPromise.as(null);
		}

		return revealPromise.then(() => {
E
Erich Gamma 已提交
833 834 835 836 837 838 839 840 841 842 843
			if (!fileStat.isDirectory) {
				this.explorerViewer.setSelection([fileStat]); // Since folders can not be opened, only select files
			}

			this.explorerViewer.setFocus(fileStat);
		});
	}

	public shutdown(): void {

		// Keep list of expanded folders to restore on next load
844
		if (this.root) {
845
			const expanded = this.explorerViewer.getExpandedElements()
B
wip  
Benjamin Pasero 已提交
846
				.filter((e: FileStat) => !isEqual(e.resource.fsPath, this.contextService.getWorkspace().resource.fsPath))
E
Erich Gamma 已提交
847 848 849 850 851 852 853 854 855 856 857 858 859 860 861
				.map((e: FileStat) => e.resource.toString());

			this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES] = expanded;
		}

		super.shutdown();
	}

	public dispose(): void {
		if (this.toolBar) {
			this.toolBar.dispose();
		}

		super.dispose();
	}
J
Johannes Rieken 已提交
862
}