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

import * as nls from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
B
Benjamin Pasero 已提交
8
import { IStateService } from 'vs/platform/state/common/state';
9 10
import { app } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
I
isidor 已提交
11
import { getBaseLabel, getPathLabel } from 'vs/base/common/labels';
12
import { IPath } from 'vs/platform/windows/common/windows';
M
Matt Bierner 已提交
13
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
M
Martin Aeschlimann 已提交
14 15
import { isWindows, isMacintosh } from 'vs/base/common/platform';
import { IWorkspaceIdentifier, IWorkspacesMainService, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
M
Martin Aeschlimann 已提交
16
import { IHistoryMainService, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile } from 'vs/platform/history/common/history';
17
import { RunOnceScheduler } from 'vs/base/common/async';
M
Martin Aeschlimann 已提交
18
import { isEqual as areResourcesEqual, dirname, originalFSPath } from 'vs/base/common/resources';
M
Martin Aeschlimann 已提交
19
import { URI } from 'vs/base/common/uri';
20
import { Schemas } from 'vs/base/common/network';
I
isidor 已提交
21 22
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { getSimpleWorkspaceLabel } from 'vs/platform/label/common/label';
M
Martin Aeschlimann 已提交
23
import { toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData } from 'vs/platform/history/electron-main/historyStorage';
24

25 26
export class HistoryMainService implements IHistoryMainService {

27
	private static readonly MAX_TOTAL_RECENT_ENTRIES = 100;
28 29
	private static readonly MAX_MACOS_DOCK_RECENT_FOLDERS = 10;
	private static readonly MAX_MACOS_DOCK_RECENT_FILES = 5;
30

31
	private static readonly recentlyOpenedStorageKey = 'openedPathsList';
32 33 34

	_serviceBrand: any;

B
Benjamin Pasero 已提交
35 36
	private _onRecentlyOpenedChange = new Emitter<void>();
	onRecentlyOpenedChange: CommonEvent<void> = this._onRecentlyOpenedChange.event;
37

38 39
	private macOSRecentDocumentsUpdater: RunOnceScheduler;

40
	constructor(
41 42 43 44
		@IStateService private readonly stateService: IStateService,
		@ILogService private readonly logService: ILogService,
		@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
		@IEnvironmentService private readonly environmentService: IEnvironmentService
45
	) {
46
		this.macOSRecentDocumentsUpdater = new RunOnceScheduler(() => this.updateMacOSRecentDocuments(), 800);
47 48
	}

M
Martin Aeschlimann 已提交
49 50
	addRecentlyOpened(newlyAdded: IRecent[]): void {
		const mru = this.getRecentlyOpened();
B
Benjamin Pasero 已提交
51

M
Martin Aeschlimann 已提交
52 53
		for (let curr of newlyAdded) {
			if (isRecentWorkspace(curr)) {
M
Martin Aeschlimann 已提交
54
				if (!this.workspacesMainService.isUntitledWorkspace(curr.workspace) && indexOfWorkspace(mru.workspaces, curr.workspace) === -1) {
M
Martin Aeschlimann 已提交
55 56 57
					mru.workspaces.unshift(curr);
				}
			} else if (isRecentFolder(curr)) {
M
Martin Aeschlimann 已提交
58
				if (indexOfFolder(mru.workspaces, curr.folderUri) === -1) {
M
Martin Aeschlimann 已提交
59 60 61
					mru.workspaces.unshift(curr);
				}
			} else {
M
Martin Aeschlimann 已提交
62
				if (indexOfFile(mru.files, curr.fileUri) === -1) {
M
Martin Aeschlimann 已提交
63
					mru.files.unshift(curr);
B
Benjamin Pasero 已提交
64
					// Add to recent documents (Windows only, macOS later)
M
Martin Aeschlimann 已提交
65 66
					if (isWindows && curr.fileUri.scheme === Schemas.file) {
						app.addRecentDocument(curr.fileUri.fsPath);
B
Benjamin Pasero 已提交
67
					}
M
Martin Aeschlimann 已提交
68
				}
B
Benjamin Pasero 已提交
69
			}
70 71

			// Make sure its bounded
72
			mru.workspaces = mru.workspaces.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES);
73 74
			mru.files = mru.files.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES);

75
			this.saveRecentlyOpened(mru);
76
			this._onRecentlyOpenedChange.fire();
77 78 79 80 81

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

M
Martin Aeschlimann 已提交
85 86 87 88 89 90
	removeFromRecentlyOpened(toRemove: URI[]): void {
		const keep = (recent: IRecent) => {
			const uri = location(recent);
			for (const r of toRemove) {
				if (areResourcesEqual(r, uri)) {
					return false;
91
				}
92
			}
M
Martin Aeschlimann 已提交
93 94
			return true;
		};
95

M
Martin Aeschlimann 已提交
96 97 98
		const mru = this.getRecentlyOpened();
		const workspaces = mru.workspaces.filter(keep);
		const files = mru.files.filter(keep);
99

M
Martin Aeschlimann 已提交
100 101
		if (workspaces.length !== mru.workspaces.length || files.length !== mru.files.length) {
			this.saveRecentlyOpened({ files, workspaces });
B
Benjamin Pasero 已提交
102
			this._onRecentlyOpenedChange.fire();
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123

			// 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();

124
		// Fill in workspaces
M
Martin Aeschlimann 已提交
125 126 127 128 129
		for (let i = 0, entries = 0; i < mru.workspaces.length && entries < HistoryMainService.MAX_MACOS_DOCK_RECENT_FOLDERS; i++) {
			const loc = location(mru.workspaces[i]);
			if (loc.scheme === Schemas.file) {
				app.addRecentDocument(originalFSPath(loc));
				entries++;
130
			}
131 132
		}

133
		// Fill in files
M
Martin Aeschlimann 已提交
134 135 136 137
		for (let i = 0, entries = 0; i < mru.files.length && entries < HistoryMainService.MAX_MACOS_DOCK_RECENT_FILES; i++) {
			const loc = location(mru.files[i]);
			if (loc.scheme === Schemas.file) {
				app.addRecentDocument(originalFSPath(loc));
138
				entries++;
139
			}
140 141 142
		}
	}

B
Benjamin Pasero 已提交
143
	clearRecentlyOpened(): void {
144
		this.saveRecentlyOpened({ workspaces: [], files: [] });
145 146 147
		app.clearRecentDocuments();

		// Event
B
Benjamin Pasero 已提交
148
		this._onRecentlyOpenedChange.fire();
149 150
	}

M
Martin Aeschlimann 已提交
151
	getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened {
152

M
Martin Aeschlimann 已提交
153 154
		const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
		const files: IRecentFile[] = [];
155

156
		// Add current workspace to beginning if set
M
Martin Aeschlimann 已提交
157 158
		if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) {
			workspaces.push({ workspace: currentWorkspace });
M
Martin Aeschlimann 已提交
159
		}
M
Martin Aeschlimann 已提交
160 161
		if (currentFolder) {
			workspaces.push({ folderUri: currentFolder });
162 163
		}

164
		// Add currently files to open to the beginning if any
165
		if (currentFiles) {
M
Martin Aeschlimann 已提交
166 167
			for (let currentFile of currentFiles) {
				const fileUri = currentFile.fileUri;
M
Martin Aeschlimann 已提交
168 169
				if (fileUri && indexOfFile(files, fileUri) === -1) {
					files.push({ fileUri });
M
Martin Aeschlimann 已提交
170 171
				}
			}
172 173
		}

M
Martin Aeschlimann 已提交
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
		// Get from storage
		let recents = this.getRecentlyOpenedFromStorage();
		for (let recent of recents.workspaces) {
			let index = isRecentFolder(recent) ? indexOfFolder(workspaces, recent.folderUri) : indexOfWorkspace(workspaces, recent.workspace);
			if (index >= 0) {
				workspaces[index].label = workspaces[index].label || recent.label;
			} else {
				workspaces.push(recent);
			}
		}
		for (let recent of recents.files) {
			let index = indexOfFile(files, recent.fileUri);
			if (index >= 0) {
				files[index].label = files[index].label || recent.label;
			} else {
				files.push(recent);
			}
		}
192
		return { workspaces, files };
193 194
	}

195
	private getRecentlyOpenedFromStorage(): IRecentlyOpened {
M
Martin Aeschlimann 已提交
196 197
		const storedRecents = this.stateService.getItem<RecentlyOpenedStorageData>(HistoryMainService.recentlyOpenedStorageKey);
		return restoreRecentlyOpened(storedRecents);
198 199
	}

200
	private saveRecentlyOpened(recent: IRecentlyOpened): void {
M
Martin Aeschlimann 已提交
201
		const serialized = toStoreData(recent);
202
		this.stateService.setItem(HistoryMainService.recentlyOpenedStorageKey, serialized);
203 204
	}

B
Benjamin Pasero 已提交
205
	updateWindowsJumpList(): void {
B
Benjamin Pasero 已提交
206
		if (!isWindows) {
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
			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
				}
			]
		});

228 229
		// Recent Workspaces
		if (this.getRecentlyOpened().workspaces.length > 0) {
230 231 232 233 234

			// 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
M
Martin Aeschlimann 已提交
235
			let toRemove: URI[] = [];
236 237 238
			for (let item of app.getJumpListSettings().removedItems) {
				const args = item.args;
				if (args) {
M
Martin Aeschlimann 已提交
239
					const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args);
240
					if (match) {
M
Martin Aeschlimann 已提交
241
						toRemove.push(URI.parse(match[2]));
242 243 244 245
					}
				}
			}
			this.removeFromRecentlyOpened(toRemove);
246 247 248 249

			// Add entries
			jumpList.push({
				type: 'custom',
250
				name: nls.localize('recentFolders', "Recent Workspaces"),
M
Martin Aeschlimann 已提交
251 252 253
				items: arrays.coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => {
					const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri;
					const title = recent.label || getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome);
254
					let description;
255
					let args;
256
					if (isSingleFolderWorkspaceIdentifier(workspace)) {
M
Martin Aeschlimann 已提交
257
						const parentFolder = dirname(workspace);
M
Martin Aeschlimann 已提交
258
						description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(parentFolder, this.environmentService));
M
Martin Aeschlimann 已提交
259
						args = `--folder-uri "${workspace.toString()}"`;
260
					} else {
261
						description = nls.localize('codeWorkspace', "Code Workspace");
M
Martin Aeschlimann 已提交
262
						args = `--file-uri "${workspace.configPath.toString()}"`;
263
					}
264 265
					return <Electron.JumpListItem>{
						type: 'task',
266 267
						title,
						description,
268
						program: process.execPath,
269
						args,
270 271 272
						iconPath: 'explorer.exe', // simulate folder icon
						iconIndex: 0
					};
M
Matt Bierner 已提交
273
				}))
274 275 276 277 278 279 280 281 282 283 284
			});
		}

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

		try {
			app.setJumpList(jumpList);
		} catch (error) {
J
Joao Moreno 已提交
285
			this.logService.warn('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors
286 287
		}
	}
288
}
M
Martin Aeschlimann 已提交
289 290 291 292 293 294 295 296 297 298 299

function location(recent: IRecent): URI {
	if (isRecentFolder(recent)) {
		return recent.folderUri;
	}
	if (isRecentFile(recent)) {
		return recent.fileUri;
	}
	return recent.workspace.configPath;
}

M
Martin Aeschlimann 已提交
300
function indexOfWorkspace(arr: IRecent[], workspace: IWorkspaceIdentifier): number {
M
Martin Aeschlimann 已提交
301 302 303
	return arrays.firstIndex(arr, w => isRecentWorkspace(w) && w.workspace.id === workspace.id);
}

M
Martin Aeschlimann 已提交
304
function indexOfFolder(arr: IRecent[], folderURI: ISingleFolderWorkspaceIdentifier): number {
M
Martin Aeschlimann 已提交
305 306 307
	return arrays.firstIndex(arr, f => isRecentFolder(f) && areResourcesEqual(f.folderUri, folderURI));
}

M
Martin Aeschlimann 已提交
308 309
function indexOfFile(arr: IRecentFile[], fileURI: URI): number {
	return arrays.firstIndex(arr, f => areResourcesEqual(f.fileUri, fileURI));
M
Martin Aeschlimann 已提交
310
}