main - extract service for handling recent paths and jumplist

This commit is contained in:
Benjamin Pasero 2017-06-09 08:10:09 +02:00
parent 4541e70733
commit 2a580b4d68
8 changed files with 249 additions and 197 deletions

View file

@ -45,6 +45,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ConfigurationService } from 'vs/platform/configuration/node/configurationService';
import { TPromise } from "vs/base/common/winjs.base";
import { IWindowsMainService } from "vs/platform/windows/electron-main/windows";
import { IHistoryMainService } from "vs/platform/history/electron-main/historyMainService";
export class CodeApplication {
private toDispose: IDisposable[];
@ -63,7 +64,8 @@ export class CodeApplication {
@IEnvironmentService private environmentService: IEnvironmentService,
@ILifecycleService private lifecycleService: ILifecycleService,
@IConfigurationService private configurationService: ConfigurationService<any>,
@IStorageService private storageService: IStorageService
@IStorageService private storageService: IStorageService,
@IHistoryMainService private historyService: IHistoryMainService
) {
this.toDispose = [mainIpcServer, configurationService];
@ -258,8 +260,8 @@ export class CodeApplication {
appInstantiationService.createInstance(CodeMenu);
// Jump List
this.windowsMainService.updateWindowsJumpList();
this.windowsMainService.onRecentPathsChange(() => this.windowsMainService.updateWindowsJumpList());
this.historyService.updateWindowsJumpList();
this.historyService.onRecentPathsChange(() => this.historyService.updateWindowsJumpList());
// Start shared process here
this.sharedProcess.spawn();

View file

@ -19,7 +19,7 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ILogService, MainLogService } from 'vs/platform/log/common/log';
import { ILogService, LogMainService } from 'vs/platform/log/common/log';
import { IStorageService, StorageService } from 'vs/platform/storage/node/storage';
import { IBackupMainService } from 'vs/platform/backup/common/backup';
import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService';
@ -33,12 +33,14 @@ import { IURLService } from 'vs/platform/url/common/url';
import { URLService } from 'vs/platform/url/electron-main/urlService';
import * as fs from 'original-fs';
import { CodeApplication } from "vs/code/electron-main/app";
import { HistoryMainService, IHistoryMainService } from "vs/platform/history/electron-main/historyMainService";
function createServices(args: ParsedArgs): IInstantiationService {
const services = new ServiceCollection();
services.set(IEnvironmentService, new SyncDescriptor(EnvironmentService, args, process.execPath));
services.set(ILogService, new SyncDescriptor(MainLogService));
services.set(ILogService, new SyncDescriptor(LogMainService));
services.set(IHistoryMainService, new SyncDescriptor(HistoryMainService));
services.set(ILifecycleService, new SyncDescriptor(LifecycleService));
services.set(IStorageService, new SyncDescriptor(StorageService));
services.set(IConfigurationService, new SyncDescriptor(ConfigurationService));

View file

@ -21,6 +21,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { tildify } from 'vs/base/common/labels';
import { KeybindingsResolver } from "vs/code/electron-main/keyboard";
import { IWindowsMainService } from "vs/platform/windows/electron-main/windows";
import { IHistoryMainService } from "vs/platform/history/electron-main/historyMainService";
interface IExtensionViewlet {
id: string;
@ -75,7 +76,8 @@ export class CodeMenu {
@IConfigurationService private configurationService: IConfigurationService,
@IWindowsMainService private windowsService: IWindowsMainService,
@IEnvironmentService private environmentService: IEnvironmentService,
@ITelemetryService private telemetryService: ITelemetryService
@ITelemetryService private telemetryService: ITelemetryService,
@IHistoryMainService private historyService: IHistoryMainService
) {
this.extensionViewlets = [];
@ -98,7 +100,7 @@ export class CodeMenu {
// Listen to some events from window service
this.windowsService.onPathsOpen(paths => this.updateMenu());
this.windowsService.onRecentPathsChange(paths => this.updateMenu());
this.historyService.onRecentPathsChange(paths => this.updateMenu());
this.windowsService.onWindowClose(_ => this.onClose(this.windowsService.getWindowCount()));
// Listen to extension viewlets
@ -414,7 +416,7 @@ export class CodeMenu {
private setOpenRecentMenu(openRecentMenu: Electron.Menu): void {
openRecentMenu.append(this.createMenuItem(nls.localize({ key: 'miReopenClosedEditor', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed Editor"), 'workbench.action.reopenClosedEditor'));
const { folders, files } = this.windowsService.getRecentPathsList();
const { folders, files } = this.historyService.getRecentPathsList();
// Folders
if (folders.length > 0) {
@ -448,7 +450,7 @@ export class CodeMenu {
const openInNewWindow = this.isOptionClick(event);
const success = !!this.windowsService.open({ context: OpenContext.MENU, cli: this.environmentService.args, pathsToOpen: [path], forceNewWindow: openInNewWindow });
if (!success) {
this.windowsService.removeFromRecentPathsList(path);
this.historyService.removeFromRecentPathsList(path);
}
}
}, false));

View file

@ -13,7 +13,6 @@ import * as types from 'vs/base/common/types';
import * as arrays from 'vs/base/common/arrays';
import { assign, mixin } from 'vs/base/common/objects';
import { IBackupMainService } from 'vs/platform/backup/common/backup';
import { trim } from 'vs/base/common/strings';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { IStorageService } from 'vs/platform/storage/node/storage';
import { CodeWindow, IWindowState as ISingleWindowState, defaultWindowState, WindowMode } from 'vs/code/electron-main/window';
@ -22,7 +21,6 @@ import { IPathWithLineAndColumn, parseLineAndColumnAware } from 'vs/code/node/pa
import { ILifecycleService, UnloadReason } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { getPathLabel } from 'vs/base/common/labels';
import { IWindowSettings, OpenContext, IPath, IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { getLastActiveWindow, findBestWindowOrFolder } from 'vs/code/node/windowsUtils';
import CommonEvent, { Emitter } from 'vs/base/common/event';
@ -30,7 +28,8 @@ import product from 'vs/platform/node/product';
import { ITelemetryService, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { isParent, isEqual, isEqualOrParent } from 'vs/platform/files/common/files';
import { KeyboardLayoutMonitor } from 'vs/code/electron-main/keyboard';
import { IWindowsMainService, IOpenConfiguration, IRecentPathsList } from "vs/platform/windows/electron-main/windows";
import { IWindowsMainService, IOpenConfiguration } from "vs/platform/windows/electron-main/windows";
import { IHistoryMainService } from "vs/platform/history/electron-main/historyMainService";
enum WindowError {
UNRESPONSIVE,
@ -70,9 +69,6 @@ export class WindowsManager implements IWindowsMainService {
_serviceBrand: any;
private static MAX_TOTAL_RECENT_ENTRIES = 100;
private static recentPathsListStorageKey = 'openedPathsList';
private static workingDirPickerStorageKey = 'pickerWorkingDir';
private static windowsStateStorageKey = 'windowsState';
@ -83,9 +79,6 @@ export class WindowsManager implements IWindowsMainService {
private windowsState: IWindowsState;
private lastClosedWindowState: IWindowState;
private _onRecentPathsChange = new Emitter<void>();
onRecentPathsChange: CommonEvent<void> = this._onRecentPathsChange.event;
private _onWindowReady = new Emitter<CodeWindow>();
onWindowReady: CommonEvent<CodeWindow> = this._onWindowReady.event;
@ -105,7 +98,8 @@ export class WindowsManager implements IWindowsMainService {
@ILifecycleService private lifecycleService: ILifecycleService,
@IBackupMainService private backupService: IBackupMainService,
@ITelemetryService private telemetryService: ITelemetryService,
@IConfigurationService private configurationService: IConfigurationService
@IConfigurationService private configurationService: IConfigurationService,
@IHistoryMainService private historyService: IHistoryMainService
) { }
public ready(initialUserEnv: platform.IProcessEnvironment): void {
@ -521,7 +515,7 @@ export class WindowsManager implements IWindowsMainService {
});
if (recentPaths.length) {
this.addToRecentPathsList(recentPaths);
this.historyService.addToRecentPathsList(recentPaths);
}
}
@ -531,103 +525,7 @@ export class WindowsManager implements IWindowsMainService {
return arrays.distinct(usedWindows);
}
public addToRecentPathsList(paths: { path: string; isFile?: boolean; }[]): void {
if (!paths || !paths.length) {
return;
}
const mru = this.getRecentPathsList();
paths.forEach(p => {
const { path, isFile } = p;
if (isFile) {
mru.files.unshift(path);
mru.files = arrays.distinct(mru.files, (f) => platform.isLinux ? f : f.toLowerCase());
} else {
mru.folders.unshift(path);
mru.folders = arrays.distinct(mru.folders, (f) => platform.isLinux ? f : f.toLowerCase());
}
// Make sure its bounded
mru.folders = mru.folders.slice(0, WindowsManager.MAX_TOTAL_RECENT_ENTRIES);
mru.files = mru.files.slice(0, WindowsManager.MAX_TOTAL_RECENT_ENTRIES);
});
this.storageService.setItem(WindowsManager.recentPathsListStorageKey, mru);
this._onRecentPathsChange.fire();
}
public removeFromRecentPathsList(path: string): void;
public removeFromRecentPathsList(paths: string[]): void;
public removeFromRecentPathsList(arg1: any): void {
let paths: string[];
if (Array.isArray(arg1)) {
paths = arg1;
} else {
paths = [arg1];
}
const mru = this.getRecentPathsList();
let update = false;
paths.forEach(path => {
let index = mru.files.indexOf(path);
if (index >= 0) {
mru.files.splice(index, 1);
update = true;
}
index = mru.folders.indexOf(path);
if (index >= 0) {
mru.folders.splice(index, 1);
update = true;
}
});
if (update) {
this.storageService.setItem(WindowsManager.recentPathsListStorageKey, mru);
this._onRecentPathsChange.fire();
}
}
public clearRecentPathsList(): void {
this.storageService.setItem(WindowsManager.recentPathsListStorageKey, { folders: [], files: [] });
app.clearRecentDocuments();
// Event
this._onRecentPathsChange.fire();
}
public getRecentPathsList(workspacePath?: string, filesToOpen?: IPath[]): IRecentPathsList {
let files: string[];
let folders: string[];
// Get from storage
const storedRecents = this.storageService.getItem<IRecentPathsList>(WindowsManager.recentPathsListStorageKey);
if (storedRecents) {
files = storedRecents.files || [];
folders = storedRecents.folders || [];
} else {
files = [];
folders = [];
}
// Add currently files to open to the beginning if any
if (filesToOpen) {
files.unshift(...filesToOpen.map(f => f.filePath));
}
// Add current workspace path to beginning if set
if (workspacePath) {
folders.unshift(workspacePath);
}
// Clear those dupes
files = arrays.distinct(files);
folders = arrays.distinct(folders);
return { files, folders };
}
private getWindowUserEnv(openConfig: IOpenConfiguration): platform.IProcessEnvironment {
return assign({}, this.initialUserEnv, openConfig.userEnv || {});
@ -705,7 +603,7 @@ export class WindowsManager implements IWindowsMainService {
{ workspacePath: candidate };
}
} catch (error) {
this.removeFromRecentPathsList(candidate); // since file does not seem to exist anymore, remove from recent
this.historyService.removeFromRecentPathsList(candidate); // since file does not seem to exist anymore, remove from recent
if (ignoreFileNotFound) {
return { filePath: candidate, createFilePath: true }; // assume this is a file that does not yet exist
@ -1182,68 +1080,6 @@ export class WindowsManager implements IWindowsMainService {
this._onWindowClose.fire(win.id);
}
public updateWindowsJumpList(): void {
if (!platform.isWindows) {
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
}
]
});
// Recent Folders
if (this.getRecentPathsList().folders.length > 0) {
// 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
this.removeFromRecentPathsList(app.getJumpListSettings().removedItems.map(r => trim(r.args, '"')));
// Add entries
jumpList.push({
type: 'custom',
name: nls.localize('recentFolders', "Recent Folders"),
items: this.getRecentPathsList().folders.slice(0, 7 /* limit number of entries here */).map(folder => {
return <Electron.JumpListItem>{
type: 'task',
title: path.basename(folder) || folder, // use the base name to show shorter entries in the list
description: nls.localize('folderDesc', "{0} {1}", path.basename(folder), getPathLabel(path.dirname(folder))),
program: process.execPath,
args: `"${folder}"`, // open folder (use quotes to support paths with whitespaces)
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) {
this.logService.log('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors
}
}
public quit(): void {
// If the user selected to exit from an extension development host window, do not quit, but just

View file

@ -0,0 +1,220 @@
/*---------------------------------------------------------------------------------------------
* 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 platform from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import * as arrays from 'vs/base/common/arrays';
import { trim } from 'vs/base/common/strings';
import { IStorageService } from 'vs/platform/storage/node/storage';
import { app } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
import { getPathLabel } from 'vs/base/common/labels';
import { IPath } from 'vs/platform/windows/common/windows';
import CommonEvent, { Emitter } from 'vs/base/common/event';
import { createDecorator } from "vs/platform/instantiation/common/instantiation";
export const IHistoryMainService = createDecorator<IHistoryMainService>('historyMainService');
export interface IRecentPathsList {
folders: string[];
files: string[];
}
export interface IHistoryMainService {
_serviceBrand: any;
// events
onRecentPathsChange: CommonEvent<void>;
// methods
addToRecentPathsList(paths: { path: string; isFile?: boolean; }[]): void;
getRecentPathsList(workspacePath?: string, filesToOpen?: IPath[]): IRecentPathsList;
removeFromRecentPathsList(path: string): void;
removeFromRecentPathsList(paths: string[]): void;
clearRecentPathsList(): void;
updateWindowsJumpList(): void;
}
export class HistoryMainService implements IHistoryMainService {
private static MAX_TOTAL_RECENT_ENTRIES = 100;
private static recentPathsListStorageKey = 'openedPathsList';
_serviceBrand: any;
private _onRecentPathsChange = new Emitter<void>();
onRecentPathsChange: CommonEvent<void> = this._onRecentPathsChange.event;
constructor(
@IStorageService private storageService: IStorageService,
@ILogService private logService: ILogService
) {
}
public addToRecentPathsList(paths: { path: string; isFile?: boolean; }[]): void {
if (!paths || !paths.length) {
return;
}
const mru = this.getRecentPathsList();
paths.forEach(p => {
const { path, isFile } = p;
if (isFile) {
mru.files.unshift(path);
mru.files = arrays.distinct(mru.files, (f) => platform.isLinux ? f : f.toLowerCase());
} else {
mru.folders.unshift(path);
mru.folders = arrays.distinct(mru.folders, (f) => platform.isLinux ? f : f.toLowerCase());
}
// Make sure its bounded
mru.folders = mru.folders.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES);
mru.files = mru.files.slice(0, HistoryMainService.MAX_TOTAL_RECENT_ENTRIES);
});
this.storageService.setItem(HistoryMainService.recentPathsListStorageKey, mru);
this._onRecentPathsChange.fire();
}
public removeFromRecentPathsList(path: string): void;
public removeFromRecentPathsList(paths: string[]): void;
public removeFromRecentPathsList(arg1: any): void {
let paths: string[];
if (Array.isArray(arg1)) {
paths = arg1;
} else {
paths = [arg1];
}
const mru = this.getRecentPathsList();
let update = false;
paths.forEach(path => {
let index = mru.files.indexOf(path);
if (index >= 0) {
mru.files.splice(index, 1);
update = true;
}
index = mru.folders.indexOf(path);
if (index >= 0) {
mru.folders.splice(index, 1);
update = true;
}
});
if (update) {
this.storageService.setItem(HistoryMainService.recentPathsListStorageKey, mru);
this._onRecentPathsChange.fire();
}
}
public clearRecentPathsList(): void {
this.storageService.setItem(HistoryMainService.recentPathsListStorageKey, { folders: [], files: [] });
app.clearRecentDocuments();
// Event
this._onRecentPathsChange.fire();
}
public getRecentPathsList(workspacePath?: string, filesToOpen?: IPath[]): IRecentPathsList {
let files: string[];
let folders: string[];
// Get from storage
const storedRecents = this.storageService.getItem<IRecentPathsList>(HistoryMainService.recentPathsListStorageKey);
if (storedRecents) {
files = storedRecents.files || [];
folders = storedRecents.folders || [];
} else {
files = [];
folders = [];
}
// Add currently files to open to the beginning if any
if (filesToOpen) {
files.unshift(...filesToOpen.map(f => f.filePath));
}
// Add current workspace path to beginning if set
if (workspacePath) {
folders.unshift(workspacePath);
}
// Clear those dupes
files = arrays.distinct(files);
folders = arrays.distinct(folders);
return { files, folders };
}
public updateWindowsJumpList(): void {
if (!platform.isWindows) {
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
}
]
});
// Recent Folders
if (this.getRecentPathsList().folders.length > 0) {
// 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
this.removeFromRecentPathsList(app.getJumpListSettings().removedItems.map(r => trim(r.args, '"')));
// Add entries
jumpList.push({
type: 'custom',
name: nls.localize('recentFolders', "Recent Folders"),
items: this.getRecentPathsList().folders.slice(0, 7 /* limit number of entries here */).map(folder => {
return <Electron.JumpListItem>{
type: 'task',
title: path.basename(folder) || folder, // use the base name to show shorter entries in the list
description: nls.localize('folderDesc', "{0} {1}", path.basename(folder), getPathLabel(path.dirname(folder))),
program: process.execPath,
args: `"${folder}"`, // open folder (use quotes to support paths with whitespaces)
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) {
this.logService.log('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors
}
}
}

View file

@ -16,7 +16,7 @@ export interface ILogService {
log(...args: any[]): void;
}
export class MainLogService implements ILogService {
export class LogMainService implements ILogService {
_serviceBrand: any;

View file

@ -43,7 +43,6 @@ export interface IWindowsMainService {
onWindowClose: Event<number>;
onWindowReload: Event<number>;
onPathsOpen: Event<IPath[]>;
onRecentPathsChange: Event<void>;
// methods
ready(initialUserEnv: IProcessEnvironment): void;
@ -63,12 +62,6 @@ export interface IWindowsMainService {
getWindowById(windowId: number): ICodeWindow;
getWindows(): ICodeWindow[];
getWindowCount(): number;
addToRecentPathsList(paths: { path: string; isFile?: boolean; }[]): void;
getRecentPathsList(workspacePath?: string, filesToOpen?: IPath[]): IRecentPathsList;
removeFromRecentPathsList(path: string): void;
removeFromRecentPathsList(paths: string[]): void;
clearRecentPathsList(): void;
updateWindowsJumpList(): void;
quit(): void;
}
@ -86,11 +79,6 @@ export interface IOpenConfiguration {
initialStartup?: boolean;
}
export interface IRecentPathsList {
folders: string[];
files: string[];
}
export interface ISharedProcess {
whenReady(): TPromise<void>;
toggle(): void;

View file

@ -18,6 +18,7 @@ import { IURLService } from 'vs/platform/url/common/url';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { ILifecycleService } from "vs/platform/lifecycle/electron-main/lifecycleMain";
import { IWindowsMainService, ISharedProcess } from "vs/platform/windows/electron-main/windows";
import { IHistoryMainService } from "vs/platform/history/electron-main/historyMainService";
export class WindowsService implements IWindowsService, IDisposable {
@ -33,7 +34,8 @@ export class WindowsService implements IWindowsService, IDisposable {
@IWindowsMainService private windowsMainService: IWindowsMainService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IURLService urlService: IURLService,
@ILifecycleService private lifecycleService: ILifecycleService
@ILifecycleService private lifecycleService: ILifecycleService,
@IHistoryMainService private historyService: IHistoryMainService
) {
chain(urlService.onOpenURL)
.filter(uri => uri.authority === 'file' && !!uri.path)
@ -124,19 +126,19 @@ export class WindowsService implements IWindowsService, IDisposable {
}
addToRecentlyOpen(paths: { path: string, isFile?: boolean }[]): TPromise<void> {
this.windowsMainService.addToRecentPathsList(paths);
this.historyService.addToRecentPathsList(paths);
return TPromise.as(null);
}
removeFromRecentlyOpen(paths: string[]): TPromise<void> {
this.windowsMainService.removeFromRecentPathsList(paths);
this.historyService.removeFromRecentPathsList(paths);
return TPromise.as(null);
}
clearRecentPathsList(): TPromise<void> {
this.windowsMainService.clearRecentPathsList();
this.historyService.clearRecentPathsList();
return TPromise.as(null);
}
@ -144,7 +146,7 @@ export class WindowsService implements IWindowsService, IDisposable {
const codeWindow = this.windowsMainService.getWindowById(windowId);
if (codeWindow) {
const { files, folders } = this.windowsMainService.getRecentPathsList(codeWindow.config.workspacePath, codeWindow.config.filesToOpen);
const { files, folders } = this.historyService.getRecentPathsList(codeWindow.config.workspacePath, codeWindow.config.filesToOpen);
return TPromise.as({ files, folders });
}