historyMainService.ts 12.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

import * as path from 'path';
import * as nls from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
import { trim } from 'vs/base/common/strings';
B
Benjamin Pasero 已提交
12
import { IStateService } from 'vs/platform/state/common/state';
13 14
import { app } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
B
Benjamin Pasero 已提交
15
import { getPathLabel, getBaseLabel } from 'vs/base/common/labels';
16
import { IPath } from 'vs/platform/windows/common/windows';
M
Matt Bierner 已提交
17
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
B
Benjamin Pasero 已提交
18
import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform';
19
import { IWorkspaceIdentifier, IWorkspacesMainService, getWorkspaceLabel, IWorkspaceSavedEvent, ISingleFolderWorkspaceIdentifier2, isSingleFolderWorkspaceIdentifier2, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
B
Benjamin Pasero 已提交
20 21 22
import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { isEqual } from 'vs/base/common/paths';
23
import { RunOnceScheduler } from 'vs/base/common/async';
24 25 26 27 28 29 30 31
import { getComparisonKey, isEqual as areResourcesEqual, hasToIgnoreCase } from 'vs/base/common/resources';
import URI, { UriComponents } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';

interface ISerializedRecentlyOpened {
	workspaces: (IWorkspaceIdentifier | string | UriComponents)[];
	files: string[];
}
32

33 34
export class HistoryMainService implements IHistoryMainService {

35 36
	private static readonly MAX_TOTAL_RECENT_ENTRIES = 100;
	private static readonly MAX_MACOS_DOCK_RECENT_ENTRIES = 10;
37

38
	private static readonly recentlyOpenedStorageKey = 'openedPathsList';
39 40 41

	_serviceBrand: any;

B
Benjamin Pasero 已提交
42 43
	private _onRecentlyOpenedChange = new Emitter<void>();
	onRecentlyOpenedChange: CommonEvent<void> = this._onRecentlyOpenedChange.event;
44

45 46
	private macOSRecentDocumentsUpdater: RunOnceScheduler;

47
	constructor(
B
Benjamin Pasero 已提交
48
		@IStateService private stateService: IStateService,
B
Benjamin Pasero 已提交
49
		@ILogService private logService: ILogService,
B
Benjamin Pasero 已提交
50
		@IWorkspacesMainService private workspacesMainService: IWorkspacesMainService,
I
isidor 已提交
51
		@IEnvironmentService private environmentService: IEnvironmentService
52
	) {
53 54
		this.macOSRecentDocumentsUpdater = new RunOnceScheduler(() => this.updateMacOSRecentDocuments(), 800);

55 56 57 58
		this.registerListeners();
	}

	private registerListeners(): void {
B
Benjamin Pasero 已提交
59
		this.workspacesMainService.onWorkspaceSaved(e => this.onWorkspaceSaved(e));
60 61 62 63
	}

	private onWorkspaceSaved(e: IWorkspaceSavedEvent): void {

64 65
		// Make sure to add newly saved workspaces to the list of recent workspaces
		this.addRecentlyOpened([e.workspace], []);
66 67
	}

68
	addRecentlyOpened(workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier2)[], files: string[]): void {
69 70
		if ((workspaces && workspaces.length > 0) || (files && files.length > 0)) {
			const mru = this.getRecentlyOpened();
71

72
			// Workspaces
B
Benjamin Pasero 已提交
73 74
			if (Array.isArray(workspaces)) {
				workspaces.forEach(workspace => {
75
					const isUntitledWorkspace = !isSingleFolderWorkspaceIdentifier2(workspace) && this.workspacesMainService.isUntitledWorkspace(workspace);
B
Benjamin Pasero 已提交
76 77 78
					if (isUntitledWorkspace) {
						return; // only store saved workspaces
					}
79

B
Benjamin Pasero 已提交
80 81
					mru.workspaces.unshift(workspace);
					mru.workspaces = arrays.distinct(mru.workspaces, workspace => this.distinctFn(workspace));
82

B
Benjamin Pasero 已提交
83 84 85 86
					// We do not add to recent documents here because on Windows we do this from a custom
					// JumpList and on macOS we fill the recent documents in one go from all our data later.
				});
			}
87

88
			// Files
B
Benjamin Pasero 已提交
89 90 91 92 93 94 95 96 97 98 99
			if (Array.isArray(files)) {
				files.forEach((path) => {
					mru.files.unshift(path);
					mru.files = arrays.distinct(mru.files, file => this.distinctFn(file));

					// Add to recent documents (Windows only, macOS later)
					if (isWindows) {
						app.addRecentDocument(path);
					}
				});
			}
100 101

			// Make sure its bounded
102
			mru.workspaces = mru.workspaces.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES);
103 104
			mru.files = mru.files.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES);

105
			this.saveRecentlyOpened(mru);
106
			this._onRecentlyOpenedChange.fire();
107 108 109 110 111

			// Schedule update to recent documents on macOS dock
			if (isMacintosh) {
				this.macOSRecentDocumentsUpdater.schedule();
			}
112
		}
113 114
	}

115
	removeFromRecentlyOpened(pathsToRemove: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier2 | string)[]): void {
B
Benjamin Pasero 已提交
116
		const mru = this.getRecentlyOpened();
117 118
		let update = false;

119
		pathsToRemove.forEach((pathToRemove => {
120 121

			// Remove workspace
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
			let index = arrays.firstIndex(mru.workspaces, workspace => {
				if (isWorkspaceIdentifier(pathToRemove)) {
					return isWorkspaceIdentifier(workspace) && isEqual(pathToRemove.configPath, workspace.configPath, !isLinux /* ignorecase */);
				}
				if (isSingleFolderWorkspaceIdentifier2(pathToRemove)) {
					return isSingleFolderWorkspaceIdentifier2(workspace) && areResourcesEqual(pathToRemove, workspace, hasToIgnoreCase(pathToRemove));
				}
				if (typeof pathsToRemove === 'string') {
					if (isSingleFolderWorkspaceIdentifier2(workspace)) {
						return workspace.scheme === Schemas.file && areResourcesEqual(URI.file(pathToRemove), workspace, hasToIgnoreCase(workspace));
					}
					if (isWorkspaceIdentifier(workspace)) {
						return isEqual(pathToRemove, workspace.configPath, !isLinux /* ignorecase */);
					}
				}
				return false;
			});
139 140 141
			if (index >= 0) {
				mru.workspaces.splice(index, 1);
				update = true;
142 143
			}

144
			// Remove file
145
			index = arrays.firstIndex(mru.files, file => typeof pathToRemove === 'string' && isEqual(file, pathToRemove, !isLinux /* ignorecase */));
146 147 148
			if (index >= 0) {
				mru.files.splice(index, 1);
				update = true;
149
			}
150
		}));
151 152

		if (update) {
153
			this.saveRecentlyOpened(mru);
B
Benjamin Pasero 已提交
154
			this._onRecentlyOpenedChange.fire();
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181

			// Schedule update to recent documents on macOS dock
			if (isMacintosh) {
				this.macOSRecentDocumentsUpdater.schedule();
			}
		}
	}

	private updateMacOSRecentDocuments(): void {
		if (!isMacintosh) {
			return;
		}

		// macOS recent documents in the dock are behaving strangely. the entries seem to get
		// out of sync quickly over time. the attempted fix is to always set the list fresh
		// from our MRU history data. So we clear the documents first and then set the documents
		// again.

		app.clearRecentDocuments();

		const mru = this.getRecentlyOpened();

		let maxEntries = HistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES;

		// Take up to maxEntries/2 workspaces
		for (let i = 0; i < mru.workspaces.length && i < HistoryMainService.MAX_MACOS_DOCK_RECENT_ENTRIES / 2; i++) {
			const workspace = mru.workspaces[i];
S
Sandeep Somavarapu 已提交
182
			app.addRecentDocument(isSingleFolderWorkspaceIdentifier2(workspace) ? workspace.scheme === Schemas.file ? workspace.fsPath : workspace.toString() : workspace.configPath);
183 184 185 186 187 188 189
			maxEntries--;
		}

		// Take up to maxEntries files
		for (let i = 0; i < mru.files.length && i < maxEntries; i++) {
			const file = mru.files[i];
			app.addRecentDocument(file);
190 191 192
		}
	}

B
Benjamin Pasero 已提交
193
	clearRecentlyOpened(): void {
194
		this.saveRecentlyOpened({ workspaces: [], files: [] });
195 196 197
		app.clearRecentDocuments();

		// Event
B
Benjamin Pasero 已提交
198
		this._onRecentlyOpenedChange.fire();
199 200
	}

201 202
	getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier2, currentFiles?: IPath[]): IRecentlyOpened {
		let workspaces: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier2)[];
203 204 205
		let files: string[];

		// Get from storage
206
		const storedRecents = this.getRecentlyOpenedFromStorage();
207
		if (storedRecents) {
208
			workspaces = storedRecents.workspaces || [];
209 210
			files = storedRecents.files || [];
		} else {
211
			workspaces = [];
212 213 214
			files = [];
		}

215 216 217 218 219
		// Add current workspace to beginning if set
		if (currentWorkspace) {
			workspaces.unshift(currentWorkspace);
		}

220
		// Add currently files to open to the beginning if any
221 222
		if (currentFiles) {
			files.unshift(...currentFiles.map(f => f.filePath));
223 224 225
		}

		// Clear those dupes
226
		workspaces = arrays.distinct(workspaces, workspace => this.distinctFn(workspace));
B
Benjamin Pasero 已提交
227
		files = arrays.distinct(files, file => this.distinctFn(file));
228

229
		// Hide untitled workspaces
230
		workspaces = workspaces.filter(workspace => isSingleFolderWorkspaceIdentifier2(workspace) || !this.workspacesMainService.isUntitledWorkspace(workspace));
231

232
		return { workspaces, files };
233 234
	}

235 236 237 238 239
	private distinctFn(workspaceOrFile: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier2 | string): string {
		if (isSingleFolderWorkspaceIdentifier2(workspaceOrFile)) {
			return getComparisonKey(workspaceOrFile);
		}
		if (typeof workspaceOrFile === 'string') {
B
Benjamin Pasero 已提交
240
			return isLinux ? workspaceOrFile : workspaceOrFile.toLowerCase();
241 242
		}

243
		return workspaceOrFile.id;
244 245
	}

246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
	private getRecentlyOpenedFromStorage(): IRecentlyOpened {
		const storedRecents: ISerializedRecentlyOpened = this.stateService.getItem<ISerializedRecentlyOpened>(HistoryMainService.recentlyOpenedStorageKey);
		const result: IRecentlyOpened = { workspaces: [], files: storedRecents.files };
		for (const workspace of storedRecents.workspaces) {
			if (typeof workspace === 'string') {
				result.workspaces.push(URI.file(workspace));
			} else if (isWorkspaceIdentifier(workspace)) {
				result.workspaces.push(workspace);
			} else {
				result.workspaces.push(URI.revive(workspace));
			}
		}
		return result;
	}

261
	private saveRecentlyOpened(recent: IRecentlyOpened): void {
262 263 264 265 266 267 268 269
		const serialized: ISerializedRecentlyOpened = { workspaces: [], files: recent.files };
		for (const workspace of recent.workspaces) {
			if (isSingleFolderWorkspaceIdentifier2(workspace)) {
				serialized.workspaces.push(<UriComponents>workspace.toJSON());
			} else {
				serialized.workspaces.push(workspace);
			}
		}
B
Benjamin Pasero 已提交
270
		this.stateService.setItem(HistoryMainService.recentlyOpenedStorageKey, recent);
271 272
	}

B
Benjamin Pasero 已提交
273
	updateWindowsJumpList(): void {
B
Benjamin Pasero 已提交
274
		if (!isWindows) {
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
			return; // only on windows
		}

		const jumpList: Electron.JumpListCategory[] = [];

		// Tasks
		jumpList.push({
			type: 'tasks',
			items: [
				{
					type: 'task',
					title: nls.localize('newWindow', "New Window"),
					description: nls.localize('newWindowDesc', "Opens a new window"),
					program: process.execPath,
					args: '-n', // force new window
					iconPath: process.execPath,
					iconIndex: 0
				}
			]
		});

296 297
		// Recent Workspaces
		if (this.getRecentlyOpened().workspaces.length > 0) {
298 299 300 301 302

			// The user might have meanwhile removed items from the jump list and we have to respect that
			// so we need to update our list of recent paths with the choice of the user to not add them again
			// Also: Windows will not show our custom category at all if there is any entry which was removed
			// by the user! See https://github.com/Microsoft/vscode/issues/15052
B
Benjamin Pasero 已提交
303
			this.removeFromRecentlyOpened(app.getJumpListSettings().removedItems.filter(r => !!r.args).map(r => trim(r.args, '"')));
304 305 306 307

			// Add entries
			jumpList.push({
				type: 'custom',
308 309
				name: nls.localize('recentFolders', "Recent Workspaces"),
				items: this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(workspace => {
S
Sandeep Somavarapu 已提交
310
					const title = getWorkspaceLabel(workspace, this.environmentService);
311 312 313 314 315 316 317 318 319 320 321 322
					const description = isSingleFolderWorkspaceIdentifier2(workspace) ? nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(path.dirname(workspace.path), this.environmentService)) : nls.localize('codeWorkspace', "Code Workspace");
					let args;
					// use quotes to support paths with whitespaces
					if (isSingleFolderWorkspaceIdentifier2(workspace)) {
						if (workspace.scheme === Schemas.file) {
							args = `"${workspace.fsPath}"`;
						} else {
							args = `--folderUri "${workspace.path}"`;
						}
					} else {
						args = `"${workspace.configPath}"`;
					}
323

324 325
					return <Electron.JumpListItem>{
						type: 'task',
326 327
						title,
						description,
328
						program: process.execPath,
329
						args,
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
						iconPath: 'explorer.exe', // simulate folder icon
						iconIndex: 0
					};
				}).filter(i => !!i)
			});
		}

		// Recent
		jumpList.push({
			type: 'recent' // this enables to show files in the "recent" category
		});

		try {
			app.setJumpList(jumpList);
		} catch (error) {
J
Joao Moreno 已提交
345
			this.logService.warn('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors
346 347
		}
	}
348
}