explorerView.ts 34.5 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';
I
isidor 已提交
11
import { ThrottledDelayer, sequence } from 'vs/base/common/async';
E
Erich Gamma 已提交
12 13
import errors = require('vs/base/common/errors');
import paths = require('vs/base/common/paths');
I
isidor 已提交
14
import resources = require('vs/base/common/resources');
15
import glob = require('vs/base/common/glob');
16
import { Action, IAction } from 'vs/base/common/actions';
17
import { prepareActions } from 'vs/workbench/browser/actions';
18
import { memoize } from 'vs/base/common/decorators';
J
Johannes Rieken 已提交
19 20
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
21
import { IFilesConfiguration, ExplorerFolderContext, FilesExplorerFocusedContext, ExplorerFocusedContext, SortOrderConfiguration, SortOrder } from 'vs/workbench/parts/files/common/files';
22
import { FileOperation, FileOperationEvent, IResolveFileOptions, FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files';
23
import { RefreshViewExplorerAction, NewFolderAction, NewFileAction } from 'vs/workbench/parts/files/browser/fileActions';
J
Johannes Rieken 已提交
24
import { FileDragAndDrop, FileFilter, FileSorter, FileController, FileRenderer, FileDataSource, FileViewletState, FileAccessibilityProvider } from 'vs/workbench/parts/files/browser/views/explorerViewer';
25
import { toResource } from 'vs/workbench/common/editor';
26
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
J
Johannes Rieken 已提交
27
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
M
Maxime Quandalle 已提交
28
import * as DOM from 'vs/base/browser/dom';
29
import { CollapseAction } from 'vs/workbench/browser/viewlet';
J
Joao Moreno 已提交
30
import { ViewsViewletPanel, IViewletViewOptions, IViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
31
import { FileStat, Model } from 'vs/workbench/parts/files/common/explorerModel';
32
import { IListService } from 'vs/platform/list/browser/listService';
J
Johannes Rieken 已提交
33 34
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
S
Sandeep Somavarapu 已提交
35
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
J
Johannes Rieken 已提交
36 37 38 39 40 41
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';
42
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
43
import { ResourceContextKey, ResourceGlobMatcher } from 'vs/workbench/common/resources';
44
import { IWorkbenchThemeService, IFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
45
import { isLinux } from 'vs/base/common/platform';
46 47
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { attachListStyler } from 'vs/platform/theme/common/styler';
48
import { IFileDecorationsService } from 'vs/workbench/services/fileDecorations/browser/fileDecorations';
E
Erich Gamma 已提交
49

50
export interface IExplorerViewOptions extends IViewletViewOptions {
51 52 53
	viewletState: FileViewletState;
}

54
export class ExplorerView extends ViewsViewletPanel {
E
Erich Gamma 已提交
55

56
	public static ID: string = 'workbench.explorer.fileView';
E
Erich Gamma 已提交
57 58 59 60 61 62 63
	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';

64 65
	public readonly id: string = ExplorerView.ID;

E
Erich Gamma 已提交
66 67 68 69
	private explorerViewer: ITree;
	private filter: FileFilter;
	private viewletState: FileViewletState;

J
Joao Moreno 已提交
70 71
	private explorerRefreshDelayer: ThrottledDelayer<void>;
	private explorerImportDelayer: ThrottledDelayer<void>;
E
Erich Gamma 已提交
72

73
	private resourceContext: ResourceContextKey;
74
	private folderContext: IContextKey<boolean>;
75

76 77
	private filesExplorerFocusedContext: IContextKey<boolean>;
	private explorerFocusedContext: IContextKey<boolean>;
78

79 80
	private fileEventsFilter: ResourceGlobMatcher;

E
Erich Gamma 已提交
81
	private shouldRefresh: boolean;
82
	private autoReveal: boolean;
83
	private sortOrder: SortOrder;
B
Benjamin Pasero 已提交
84
	private settings: object;
E
Erich Gamma 已提交
85 86

	constructor(
87
		options: IExplorerViewOptions,
S
#27823  
Sandeep Somavarapu 已提交
88
		@IMessageService private messageService: IMessageService,
E
Erich Gamma 已提交
89 90
		@IContextMenuService contextMenuService: IContextMenuService,
		@IInstantiationService private instantiationService: IInstantiationService,
91
		@IEditorGroupService private editorGroupService: IEditorGroupService,
E
Erich Gamma 已提交
92 93
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IProgressService private progressService: IProgressService,
94
		@IListService private listService: IListService,
E
Erich Gamma 已提交
95 96 97
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@IFileService private fileService: IFileService,
		@IPartService private partService: IPartService,
B
Benjamin Pasero 已提交
98
		@IKeybindingService keybindingService: IKeybindingService,
99
		@IContextKeyService contextKeyService: IContextKeyService,
100
		@IConfigurationService private configurationService: IConfigurationService,
101
		@IWorkbenchThemeService private themeService: IWorkbenchThemeService,
102 103
		@IEnvironmentService private environmentService: IEnvironmentService,
		@IFileDecorationsService private fileDecorationsService: IFileDecorationsService
E
Erich Gamma 已提交
104
	) {
105
		super({ ...(options as IViewOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService);
E
Erich Gamma 已提交
106

107
		this.settings = options.viewletSettings;
108
		this.viewletState = options.viewletState;
109
		this.autoReveal = true;
E
Erich Gamma 已提交
110

J
Joao Moreno 已提交
111 112
		this.explorerRefreshDelayer = new ThrottledDelayer<void>(ExplorerView.EXPLORER_FILE_CHANGES_REFRESH_DELAY);
		this.explorerImportDelayer = new ThrottledDelayer<void>(ExplorerView.EXPLORER_IMPORT_REFRESH_DELAY);
113 114

		this.resourceContext = instantiationService.createInstance(ResourceContextKey);
115
		this.folderContext = ExplorerFolderContext.bindTo(contextKeyService);
116

117 118
		this.filesExplorerFocusedContext = FilesExplorerFocusedContext.bindTo(contextKeyService);
		this.explorerFocusedContext = ExplorerFocusedContext.bindTo(contextKeyService);
119

120
		this.fileEventsFilter = instantiationService.createInstance(ResourceGlobMatcher, (root: URI) => this.getFileEventsExcludes(root), (expression: glob.IExpression) => glob.parse(expression));
121 122 123 124 125 126 127
	}

	private getFileEventsExcludes(root?: URI): glob.IExpression {
		const scope = root ? { resource: root } : void 0;
		const configuration = this.configurationService.getConfiguration<IFilesConfiguration>(undefined, scope);

		return (configuration && configuration.files && configuration.files.exclude) || Object.create(null);
E
Erich Gamma 已提交
128 129
	}

130
	protected renderHeader(container: HTMLElement): void {
131 132 133
		super.renderHeader(container);

		const titleElement = container.querySelector('.title') as HTMLElement;
134
		const setHeader = () => {
B
Benjamin Pasero 已提交
135
			const workspace = this.contextService.getWorkspace();
I
isidor 已提交
136
			const title = workspace.folders.map(folder => folder.name).join();
137 138
			titleElement.textContent = this.name;
			titleElement.title = title;
139
		};
I
isidor 已提交
140

J
Joao Moreno 已提交
141
		this.disposables.push(this.contextService.onDidChangeWorkspaceName(setHeader));
142
		setHeader();
E
Erich Gamma 已提交
143 144
	}

I
isidor 已提交
145
	public get name(): string {
146
		return this.contextService.getWorkspace().name;
I
isidor 已提交
147 148 149 150 151 152
	}

	public set name(value) {
		// noop
	}

E
Erich Gamma 已提交
153 154 155
	public renderBody(container: HTMLElement): void {
		this.treeContainer = super.renderViewTree(container);
		DOM.addClass(this.treeContainer, 'explorer-folders-view');
156
		DOM.addClass(this.treeContainer, 'show-file-icons');
E
Erich Gamma 已提交
157 158 159

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

160 161
		if (this.toolbar) {
			this.toolbar.setActions(prepareActions(this.getActions()), this.getSecondaryActions())();
I
isidor 已提交
162
		}
163 164 165 166 167

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

J
Joao Moreno 已提交
168
		this.disposables.push(this.themeService.onDidFileIconThemeChange(onFileIconThemeChange));
169
		this.disposables.push(this.contextService.onDidChangeWorkspaceFolders(e => this.refreshFromEvent(e.added)));
I
isidor 已提交
170
		this.disposables.push(this.contextService.onDidChangeWorkbenchState(e => this.refreshFromEvent()));
171
		this.disposables.push(this.fileDecorationsService.onDidChangeFileDecoration(this.onDidChangeFileDecorations, this));
172
		onFileIconThemeChange(this.themeService.getFileIconTheme());
E
Erich Gamma 已提交
173 174
	}

I
isidor 已提交
175 176
	public getActions(): IAction[] {
		const actions: Action[] = [];
E
Erich Gamma 已提交
177

B
polish  
Benjamin Pasero 已提交
178
		actions.push(this.instantiationService.createInstance(NewFileAction, this.getViewer(), null));
E
Erich Gamma 已提交
179 180 181 182 183 184
		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++) {
185
			const action = actions[i];
E
Erich Gamma 已提交
186 187 188
			action.order = 10 * (i + 1);
		}

I
isidor 已提交
189
		return actions;
E
Erich Gamma 已提交
190 191 192
	}

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

194 195 196
		// Update configuration
		const configuration = this.configurationService.getConfiguration<IFilesConfiguration>();
		this.onConfigurationUpdated(configuration);
E
Erich Gamma 已提交
197

198
		// Load and Fill Viewer
I
isidor 已提交
199 200 201 202 203
		let targetsToExpand = [];
		if (this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES]) {
			targetsToExpand = this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES].map((e: string) => URI.parse(e));
		}
		return this.doRefresh(targetsToExpand).then(() => {
E
Erich Gamma 已提交
204

205
			// When the explorer viewer is loaded, listen to changes to the editor input
J
Joao Moreno 已提交
206
			this.disposables.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
E
Erich Gamma 已提交
207

208
			// Also handle configuration updates
J
Joao Moreno 已提交
209
			this.disposables.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationUpdated(this.configurationService.getConfiguration<IFilesConfiguration>(), true)));
E
Erich Gamma 已提交
210 211 212
		});
	}

213
	private onEditorsChanged(): void {
214 215 216 217
		if (!this.autoReveal) {
			return; // do not touch selection or focus if autoReveal === false
		}

218
		let clearSelection = true;
219
		let clearFocus = false;
E
Erich Gamma 已提交
220

221 222 223
		// Handle files
		const activeFile = this.getActiveFile();
		if (activeFile) {
E
Erich Gamma 已提交
224 225

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

228
			// Select file if input is inside workspace
229
			if (this.isVisible() && this.contextService.isInsideWorkspace(activeFile)) {
230
				const selection = this.hasSelection(activeFile);
231
				if (!selection) {
232
					this.select(activeFile).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
233
				}
234 235

				clearSelection = false;
E
Erich Gamma 已提交
236 237 238
			}
		}

239
		// Handle closed or untitled file (convince explorer to not reopen any file when getting visible)
240
		const activeInput = this.editorService.getActiveEditorInput();
B
Benjamin Pasero 已提交
241
		if (!activeInput || toResource(activeInput, { supportSideBySide: true, filter: 'untitled' })) {
242
			this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE] = void 0;
243
			clearFocus = true;
244 245
		}

E
Erich Gamma 已提交
246
		// Otherwise clear
247
		if (clearSelection) {
248
			this.explorerViewer.clearSelection();
E
Erich Gamma 已提交
249
		}
250 251 252 253

		if (clearFocus) {
			this.explorerViewer.clearFocus();
		}
E
Erich Gamma 已提交
254 255 256
	}

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

261
		this.autoReveal = configuration && configuration.explorer && configuration.explorer.autoReveal;
E
Erich Gamma 已提交
262 263 264 265

		// Push down config updates to components of viewer
		let needsRefresh = false;
		if (this.filter) {
I
isidor 已提交
266
			needsRefresh = this.filter.updateConfiguration();
E
Erich Gamma 已提交
267 268
		}

269 270 271 272 273 274
		const configSortOrder = configuration && configuration.explorer && configuration.explorer.sortOrder || 'default';
		if (this.sortOrder !== configSortOrder) {
			this.sortOrder = configSortOrder;
			needsRefresh = true;
		}

E
Erich Gamma 已提交
275 276
		// Refresh viewer as needed
		if (refresh && needsRefresh) {
277
			this.doRefresh().done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
278 279 280
		}
	}

281 282 283
	public focus(): void {
		super.focus();

284
		let keepFocus = false;
285 286 287 288

		// Make sure the current selected element is revealed
		if (this.explorerViewer) {
			if (this.autoReveal) {
289
				const selection = this.explorerViewer.getSelection();
290 291 292 293 294 295 296
				if (selection.length > 0) {
					this.reveal(selection[0], 0.5).done(null, errors.onUnexpectedError);
				}
			}

			// Pass Focus to Viewer
			this.explorerViewer.DOMFocus();
297
			keepFocus = true;
298
		}
E
Erich Gamma 已提交
299

300
		// Open the focused element in the editor if there is currently no file opened
301 302
		const activeFile = this.getActiveFile();
		if (!activeFile) {
303
			this.openFocusedElement(keepFocus);
E
Erich Gamma 已提交
304 305 306 307 308 309 310 311 312 313
		}
	}

	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
314
				let refreshPromise = TPromise.as<void>(null);
E
Erich Gamma 已提交
315
				if (this.shouldRefresh) {
316
					refreshPromise = this.doRefresh();
E
Erich Gamma 已提交
317 318 319
					this.shouldRefresh = false; // Reset flag
				}

320 321 322 323
				if (!this.autoReveal) {
					return refreshPromise; // do not react to setVisible call if autoReveal === false
				}

324
				// Always select the current navigated file in explorer if input is file editor input
325
				// unless autoReveal is set to false
326 327
				const activeFile = this.getActiveFile();
				if (activeFile) {
E
Erich Gamma 已提交
328
					return refreshPromise.then(() => {
329
						return this.select(activeFile);
E
Erich Gamma 已提交
330 331 332 333 334
					});
				}

				// 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 已提交
335
					return TPromise.as(null);
E
Erich Gamma 已提交
336 337 338 339 340 341 342 343
				}

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

I
isidor 已提交
344
				if (lastActiveFileResource && this.isCreated && this.model.findClosest(lastActiveFileResource)) {
345
					this.editorService.openEditor({ resource: lastActiveFileResource, options: { revealIfVisible: true } }).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
346 347 348 349 350 351

					return refreshPromise;
				}

				// Otherwise restore last used file: By Explorer selection
				return refreshPromise.then(() => {
352
					this.openFocusedElement();
E
Erich Gamma 已提交
353 354
				});
			}
355 356

			return void 0;
E
Erich Gamma 已提交
357 358 359
		});
	}

360
	private openFocusedElement(preserveFocus?: boolean): void {
361
		const stat: FileStat = this.explorerViewer.getFocus();
E
Erich Gamma 已提交
362
		if (stat && !stat.isDirectory) {
363
			this.editorService.openEditor({ resource: stat.resource, options: { preserveFocus, revealIfVisible: true } }).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
364 365 366
		}
	}

367
	private getActiveFile(): URI {
368
		const input = this.editorService.getActiveEditorInput();
369 370 371 372

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

375
		// check for files
I
isidor 已提交
376
		return toResource(input, { supportSideBySide: true });
E
Erich Gamma 已提交
377 378
	}

379
	private get isCreated(): boolean {
380
		return !!(this.explorerViewer && this.explorerViewer.getInput());
381 382 383 384
	}

	@memoize
	private get model(): Model {
B
Benjamin Pasero 已提交
385
		const model = this.instantiationService.createInstance(Model);
J
Joao Moreno 已提交
386
		this.disposables.push(model);
B
Benjamin Pasero 已提交
387 388

		return model;
389 390
	}

E
Erich Gamma 已提交
391
	public createViewer(container: Builder): ITree {
B
Benjamin Pasero 已提交
392
		const dataSource = this.instantiationService.createInstance(FileDataSource);
393
		const renderer = this.instantiationService.createInstance(FileRenderer, this.viewletState);
B
Benjamin Pasero 已提交
394
		const controller = this.instantiationService.createInstance(FileController, this.viewletState);
395
		const sorter = this.instantiationService.createInstance(FileSorter);
J
Joao Moreno 已提交
396
		this.disposables.push(sorter);
E
Erich Gamma 已提交
397
		this.filter = this.instantiationService.createInstance(FileFilter);
J
Joao Moreno 已提交
398
		this.disposables.push(this.filter);
B
Benjamin Pasero 已提交
399 400
		const dnd = this.instantiationService.createInstance(FileDragAndDrop);
		const accessibilityProvider = this.instantiationService.createInstance(FileAccessibilityProvider);
E
Erich Gamma 已提交
401 402

		this.explorerViewer = new Tree(container.getHTMLElement(), {
B
Benjamin Pasero 已提交
403 404 405 406
			dataSource,
			renderer,
			controller,
			sorter,
E
Erich Gamma 已提交
407
			filter: this.filter,
B
Benjamin Pasero 已提交
408 409
			dnd,
			accessibilityProvider
E
Erich Gamma 已提交
410
		}, {
411
				autoExpandSingleChildren: true,
412
				ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"),
413
				twistiePixels: 12,
414 415
				showTwistie: false,
				keyboardSupport: false
416
			});
E
Erich Gamma 已提交
417

B
Benjamin Pasero 已提交
418
		// Theme styler
J
Joao Moreno 已提交
419
		this.disposables.push(attachListStyler(this.explorerViewer, this.themeService));
B
Benjamin Pasero 已提交
420

421
		// Register to list service
J
Joao Moreno 已提交
422
		this.disposables.push(this.listService.register(this.explorerViewer, [this.explorerFocusedContext, this.filesExplorerFocusedContext]));
423

E
Erich Gamma 已提交
424
		// Update Viewer based on File Change Events
J
Joao Moreno 已提交
425 426
		this.disposables.push(this.fileService.onAfterOperation(e => this.onFileOperation(e)));
		this.disposables.push(this.fileService.onFileChanges(e => this.onFileChanges(e)));
E
Erich Gamma 已提交
427

428
		// Update resource context based on focused element
J
Joao Moreno 已提交
429
		this.disposables.push(this.explorerViewer.addListener('focus', (e: { focus: FileStat }) => {
430 431 432
			this.resourceContext.set(e.focus && e.focus.resource);
			this.folderContext.set(e.focus && e.focus.isDirectory);
		}));
433

434
		// Open when selecting via keyboard
J
Joao Moreno 已提交
435
		this.disposables.push(this.explorerViewer.addListener('selection', event => {
436
			if (event && event.payload && event.payload.origin === 'keyboard') {
437
				const element = this.tree.getSelection();
B
Benjamin Pasero 已提交
438

439 440 441
				if (Array.isArray(element) && element[0] instanceof FileStat) {
					if (element[0].isDirectory) {
						this.explorerViewer.toggleExpansion(element[0]);
B
Benjamin Pasero 已提交
442 443
					}

444
					controller.openEditor(element[0], { pinned: false, sideBySide: false, preserveFocus: false });
B
Benjamin Pasero 已提交
445
				}
446 447 448
			}
		}));

E
Erich Gamma 已提交
449 450 451
		return this.explorerViewer;
	}

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

M
Maxime Quandalle 已提交
456 457 458
		return DOM.getLargestChildWidth(parentNode, childNodes);
	}

459
	private onFileOperation(e: FileOperationEvent): void {
460
		if (!this.isCreated) {
B
Benjamin Pasero 已提交
461 462 463
			return; // ignore if not yet created
		}

E
Erich Gamma 已提交
464
		// Add
465 466
		if (e.operation === FileOperation.CREATE || e.operation === FileOperation.IMPORT || e.operation === FileOperation.COPY) {
			const addedElement = e.target;
I
isidor 已提交
467
			const parentResource = resources.dirname(addedElement.resource);
468
			const parents = this.model.findAll(parentResource);
E
Erich Gamma 已提交
469

I
isidor 已提交
470
			if (parents.length) {
E
Erich Gamma 已提交
471 472

				// Add the new file to its parent (Model)
473
				parents.forEach(p => {
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
					// We have to check if the parent is resolved #29177
					(p.isDirectoryResolved ? TPromise.as(null) : this.fileService.resolveFile(p.resource)).then(stat => {
						if (stat) {
							const modelStat = FileStat.create(stat, p.root);
							FileStat.mergeLocalWithDisk(modelStat, p);
						}

						const childElement = FileStat.create(addedElement, p.root);
						p.removeChild(childElement); // make sure to remove any previous version of the file if any
						p.addChild(childElement);
						// Refresh the Parent (View)
						this.explorerViewer.refresh(p).then(() => {
							return this.reveal(childElement, 0.5).then(() => {

								// Focus new element
								this.explorerViewer.setFocus(childElement);
							});
						}).done(null, errors.onUnexpectedError);
					});
493
				});
E
Erich Gamma 已提交
494 495 496 497
			}
		}

		// Move (including Rename)
498 499 500
		else if (e.operation === FileOperation.MOVE) {
			const oldResource = e.resource;
			const newElement = e.target;
E
Erich Gamma 已提交
501

I
isidor 已提交
502 503
			const oldParentResource = resources.dirname(oldResource);
			const newParentResource = resources.dirname(newElement.resource);
E
Erich Gamma 已提交
504 505

			// Only update focus if renamed/moved element is selected
506
			let restoreFocus = false;
507
			const focus: FileStat = this.explorerViewer.getFocus();
508
			if (focus && focus.resource && focus.resource.toString() === oldResource.toString()) {
509
				restoreFocus = true;
E
Erich Gamma 已提交
510 511 512
			}

			// Handle Rename
513
			if (oldParentResource && newParentResource && oldParentResource.toString() === newParentResource.toString()) {
514 515
				const modelElements = this.model.findAll(oldResource);
				modelElements.forEach(modelElement => {
E
Erich Gamma 已提交
516 517 518 519
					// Rename File (Model)
					modelElement.rename(newElement);

					// Update Parent (View)
520 521 522 523 524 525 526 527
					this.explorerViewer.refresh(modelElement.parent).done(() => {

						// Select in Viewer if set
						if (restoreFocus) {
							this.explorerViewer.setFocus(modelElement);
						}
					}, errors.onUnexpectedError);
				});
E
Erich Gamma 已提交
528 529 530 531
			}

			// Handle Move
			else if (oldParentResource && newParentResource) {
I
isidor 已提交
532
				const newParents = this.model.findAll(newParentResource);
533
				const modelElements = this.model.findAll(oldResource);
E
Erich Gamma 已提交
534

535
				if (newParents.length && modelElements.length) {
E
Erich Gamma 已提交
536 537

					// Move in Model
538 539 540 541 542 543 544 545 546
					modelElements.forEach((modelElement, index) => {
						const oldParent = modelElement.parent;
						modelElement.move(newParents[index], (callback: () => void) => {
							// Update old parent
							this.explorerViewer.refresh(oldParent).done(callback, errors.onUnexpectedError);
						}, () => {
							// Update new parent
							this.explorerViewer.refresh(newParents[index], true).done(() => this.explorerViewer.expand(newParents[index]), errors.onUnexpectedError);
						});
E
Erich Gamma 已提交
547 548 549 550 551 552
					});
				}
			}
		}

		// Delete
553
		else if (e.operation === FileOperation.DELETE) {
554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571
			const modelElements = this.model.findAll(e.resource);
			modelElements.forEach(element => {
				if (element.parent) {
					const parent = element.parent;
					// Remove Element from Parent (Model)
					parent.removeChild(element);

					// Refresh Parent (View)
					const restoreFocus = this.explorerViewer.isDOMFocused();
					this.explorerViewer.refresh(parent).done(() => {

						// Ensure viewer has keyboard focus if event originates from viewer
						if (restoreFocus) {
							this.explorerViewer.DOMFocus();
						}
					}, errors.onUnexpectedError);
				}
			});
E
Erich Gamma 已提交
572 573 574 575 576
		}
	}

	private onFileChanges(e: FileChangesEvent): void {

577 578 579 580
		// 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(() => {
581
			const lastActiveResource: string = this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE];
582 583 584 585
			if (lastActiveResource && e.contains(URI.parse(lastActiveResource), FileChangeType.DELETED)) {
				this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE] = null;
			}
		});
E
Erich Gamma 已提交
586 587 588 589

		// 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.
590
		setTimeout(() => {
E
Erich Gamma 已提交
591 592 593
			if (!this.shouldRefresh && this.shouldRefreshFromEvent(e)) {
				this.refreshFromEvent();
			}
594
		}, ExplorerView.EXPLORER_FILE_CHANGES_REACT_DELAY);
E
Erich Gamma 已提交
595 596 597 598 599
	}

	private shouldRefreshFromEvent(e: FileChangesEvent): boolean {

		// Filter to the ones we care
I
isidor 已提交
600
		e = this.filterFileEvents(e);
E
Erich Gamma 已提交
601

602 603 604
		if (!this.isCreated) {
			return false;
		}
E
Erich Gamma 已提交
605

606 607
		if (e.gotAdded()) {
			const added = e.getAdded();
E
Erich Gamma 已提交
608 609

			// Check added: Refresh if added file/folder is not part of resolved root and parent is part of it
610
			const ignoredPaths: { [fsPath: string]: boolean } = <{ [fsPath: string]: boolean }>{};
E
Erich Gamma 已提交
611
			for (let i = 0; i < added.length; i++) {
612
				const change = added[i];
E
Erich Gamma 已提交
613 614 615 616 617
				if (!this.contextService.isInsideWorkspace(change.resource)) {
					continue; // out of workspace file
				}

				// Find parent
618
				const parent = paths.dirname(change.resource.fsPath);
E
Erich Gamma 已提交
619 620 621 622 623 624 625

				// 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
I
isidor 已提交
626 627
				const parentStat = this.model.findClosest(URI.file(parent));
				if (parentStat && parentStat.isDirectoryResolved && !this.model.findClosest(change.resource)) {
E
Erich Gamma 已提交
628 629 630 631 632 633 634 635
					return true;
				}

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

		if (e.gotDeleted()) {
			const deleted = e.getDeleted();
E
Erich Gamma 已提交
640 641 642

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

I
isidor 已提交
648
				if (this.model.findClosest(del.resource)) {
E
Erich Gamma 已提交
649 650 651 652 653
					return true;
				}
			}
		}

654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669
		if (this.sortOrder === SortOrderConfiguration.MODIFIED && e.gotUpdated()) {
			const updated = e.getUpdated();

			// Check updated: Refresh if updated file/folder part of resolved root
			for (let j = 0; j < updated.length; j++) {
				const upd = updated[j];
				if (!this.contextService.isInsideWorkspace(upd.resource)) {
					continue; // out of workspace file
				}

				if (this.model.findClosest(upd.resource)) {
					return true;
				}
			}
		}

E
Erich Gamma 已提交
670 671 672
		return false;
	}

I
isidor 已提交
673
	private filterFileEvents(e: FileChangesEvent): FileChangesEvent {
B
polish  
Benjamin Pasero 已提交
674
		return new FileChangesEvent(e.changes.filter(change => {
675 676
			if (!this.contextService.isInsideWorkspace(change.resource)) {
				return false; // exclude changes for resources outside of workspace
I
isidor 已提交
677 678
			}

679 680
			if (this.fileEventsFilter.matches(change.resource)) {
				return false; // excluded via files.exclude setting
E
Erich Gamma 已提交
681 682
			}

683
			return true;
E
Erich Gamma 已提交
684 685 686
		}));
	}

687 688 689 690 691 692 693 694 695 696 697 698 699
	private onDidChangeFileDecorations(uris: URI[]): void {
		let seen = new Set<FileStat>();
		let stack = uris.map(uri => this.model.findClosest(uri));
		while (stack.length > 0) {
			let stat = stack.shift();
			if (stat && !seen.has(stat)) {
				this.explorerViewer.refresh(stat, false);
				stack.push(stat.parent);
				seen.add(stat);
			}
		}
	}

S
Sandeep Somavarapu 已提交
700
	private refreshFromEvent(newRoots: IWorkspaceFolder[] = []): void {
701
		if (this.isVisible()) {
E
Erich Gamma 已提交
702 703
			this.explorerRefreshDelayer.trigger(() => {
				if (!this.explorerViewer.getHighlight()) {
I
isidor 已提交
704 705 706 707 708 709 710
					return this.doRefresh(newRoots.map(r => r.uri)).then(() => {
						if (newRoots.length === 1) {
							return this.reveal(this.model.findClosest(newRoots[0].uri), 0.5);
						}

						return undefined;
					});
E
Erich Gamma 已提交
711 712
				}

A
Alex Dima 已提交
713
				return TPromise.as(null);
E
Erich Gamma 已提交
714 715 716 717 718 719 720 721 722
			}).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.
	 */
723 724 725 726 727 728 729 730 731 732 733
	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) {
734
			resourceToFocus = this.getActiveFile();
735
			if (!resourceToFocus) {
736
				const selection = this.explorerViewer.getSelection();
737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
				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);
		});
	}

I
isidor 已提交
752
	private doRefresh(targetsToExpand: URI[] = []): TPromise<any> {
I
isidor 已提交
753
		const targetsToResolve = this.model.roots.map(root => ({ root, resource: root.resource, options: { resolveTo: [] } }));
E
Erich Gamma 已提交
754 755

		// First time refresh: Receive target through active editor input or selection and also include settings from previous session
756
		if (!this.isCreated) {
757 758
			const activeFile = this.getActiveFile();
			if (activeFile) {
S
Sandeep Somavarapu 已提交
759 760
				const workspaceFolder = this.contextService.getWorkspaceFolder(activeFile);
				if (workspaceFolder) {
761
					const found = targetsToResolve.filter(t => t.root.resource.toString() === workspaceFolder.uri.toString()).pop();
I
isidor 已提交
762 763
					found.options.resolveTo.push(activeFile);
				}
E
Erich Gamma 已提交
764 765
			}

I
isidor 已提交
766
			targetsToExpand.forEach(toExpand => {
S
Sandeep Somavarapu 已提交
767 768
				const workspaceFolder = this.contextService.getWorkspaceFolder(toExpand);
				if (workspaceFolder) {
769
					const found = targetsToResolve.filter(ttr => ttr.resource.toString() === workspaceFolder.uri.toString()).pop();
I
isidor 已提交
770 771 772
					found.options.resolveTo.push(toExpand);
				}
			});
E
Erich Gamma 已提交
773 774 775 776
		}

		// Subsequent refresh: Receive targets through expanded folders in tree
		else {
I
isidor 已提交
777 778 779
			targetsToResolve.forEach(t => {
				this.getResolvedDirectories(t.root, t.options.resolveTo);
			});
E
Erich Gamma 已提交
780 781 782
		}

		// Load Root Stat with given target path configured
I
isidor 已提交
783
		const promise = TPromise.join(targetsToResolve.map((target, index) => this.fileService.resolveFile(target.resource, target.options).then(result => {
E
Erich Gamma 已提交
784
			// Convert to model
I
isidor 已提交
785
			const modelStat = FileStat.create(result, target.root, target.options.resolveTo);
I
isidor 已提交
786
			// Subsequent refresh: Merge stat into our local model and refresh tree
I
isidor 已提交
787
			FileStat.mergeLocalWithDisk(modelStat, this.model.roots[index]);
E
Erich Gamma 已提交
788

789
			const input = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? this.model.roots[0] : this.model;
I
isidor 已提交
790
			let statsToExpand: FileStat[] = this.explorerViewer.getExpandedElements().concat(targetsToExpand.map(target => this.model.findClosest(target)));
791
			if (input === this.explorerViewer.getInput()) {
I
isidor 已提交
792
				return this.explorerViewer.refresh().then(() => sequence(statsToExpand.map(e => () => this.explorerViewer.expand(e))));
E
Erich Gamma 已提交
793 794
			}

795
			// Display roots only when multi folder workspace
I
isidor 已提交
796
			// Make sure to expand all folders that where expanded in the previous session
I
isidor 已提交
797 798 799 800 801
			if (input === this.model) {
				// We have transitioned into workspace view -> expand all roots
				statsToExpand = this.model.roots.concat(statsToExpand);
			}
			return this.explorerViewer.setInput(input).then(() => sequence(statsToExpand.map(e => () => this.explorerViewer.expand(e))));
I
isidor 已提交
802 803 804 805 806 807 808 809
		}, e => FileStat.create({
			resource: target.resource,
			name: resources.basenameOrAuthority(target.resource),
			mtime: 0,
			etag: undefined,
			isDirectory: true,
			hasChildren: false
		}, target.root))));
E
Erich Gamma 已提交
810

811
		this.progressService.showWhile(promise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */);
E
Erich Gamma 已提交
812 813 814 815 816 817 818 819 820

		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) {
821
			if (!stat.isRoot) {
E
Erich Gamma 已提交
822 823 824

				// Drop those path which are parents of the current one
				for (let i = resolvedDirectories.length - 1; i >= 0; i--) {
825
					const resource = resolvedDirectories[i];
I
isidor 已提交
826
					if (resources.isEqualOrParent(stat.resource, resource, !isLinux /* ignorecase */)) {
E
Erich Gamma 已提交
827 828 829 830 831 832 833 834 835 836
						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++) {
837
				const child = stat.children[i];
E
Erich Gamma 已提交
838 839 840 841 842 843 844 845 846
				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.
	 */
847
	public select(resource: URI, reveal: boolean = this.autoReveal): TPromise<void> {
E
Erich Gamma 已提交
848 849

		// Require valid path
850
		if (!resource) {
A
Alex Dima 已提交
851
			return TPromise.as(null);
E
Erich Gamma 已提交
852 853 854
		}

		// If path already selected, just reveal and return
855
		const selection = this.hasSelection(resource);
856 857
		if (selection) {
			return reveal ? this.reveal(selection, 0.5) : TPromise.as(null);
E
Erich Gamma 已提交
858 859 860
		}

		// First try to get the stat object from the input to avoid a roundtrip
861
		if (!this.isCreated) {
A
Alex Dima 已提交
862
			return TPromise.as(null);
E
Erich Gamma 已提交
863 864
		}

I
isidor 已提交
865
		const fileStat = this.model.findClosest(resource);
E
Erich Gamma 已提交
866
		if (fileStat) {
867
			return this.doSelect(fileStat, reveal);
E
Erich Gamma 已提交
868 869 870
		}

		// Stat needs to be resolved first and then revealed
871
		const options: IResolveFileOptions = { resolveTo: [resource] };
872 873
		const workspaceFolder = this.contextService.getWorkspaceFolder(resource);
		const rootUri = workspaceFolder ? workspaceFolder.uri : this.model.roots[0].resource;
I
isidor 已提交
874
		return this.fileService.resolveFile(rootUri, options).then(stat => {
E
Erich Gamma 已提交
875 876

			// Convert to model
I
isidor 已提交
877
			const root = this.model.roots.filter(r => r.resource.toString() === rootUri.toString()).pop();
I
isidor 已提交
878
			const modelStat = FileStat.create(stat, root, options.resolveTo);
E
Erich Gamma 已提交
879
			// Update Input with disk Stat
I
isidor 已提交
880
			FileStat.mergeLocalWithDisk(modelStat, root);
E
Erich Gamma 已提交
881 882

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

R
Ron Buckton 已提交
885
		}, e => { this.messageService.show(Severity.Error, e); });
E
Erich Gamma 已提交
886 887
	}

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

891
		for (let i = 0; i < currentSelection.length; i++) {
892
			if (currentSelection[i].resource.toString() === resource.toString()) {
893 894 895 896 897 898 899 900
				return currentSelection[i];
			}
		}

		return null;
	}

	private doSelect(fileStat: FileStat, reveal: boolean): TPromise<void> {
E
Erich Gamma 已提交
901
		if (!fileStat) {
A
Alex Dima 已提交
902
			return TPromise.as(null);
E
Erich Gamma 已提交
903 904 905 906 907 908 909
		}

		// 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 已提交
910
				return TPromise.as(null);
E
Erich Gamma 已提交
911 912 913
			}
		}

914 915 916 917 918 919 920 921 922
		// 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 已提交
923 924 925 926 927 928 929 930 931 932 933
			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
934
		if (this.isCreated) {
935
			const expanded = this.explorerViewer.getExpandedElements()
936
				.filter(e => e instanceof FileStat)
E
Erich Gamma 已提交
937 938
				.map((e: FileStat) => e.resource.toString());

939 940 941 942 943 944 945
			if (expanded.length) {
				this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES] = expanded;
			} else {
				delete this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES];
			}
		}

946
		// Clean up last focused if not set
947 948
		if (!this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE]) {
			delete this.settings[ExplorerView.MEMENTO_LAST_ACTIVE_FILE_RESOURCE];
E
Erich Gamma 已提交
949 950 951 952
		}

		super.shutdown();
	}
J
Johannes Rieken 已提交
953
}