workbench/electron-main -> code/electron-main

This commit is contained in:
Joao Moreno
2016-05-02 15:05:23 +02:00
parent 8aeb918803
commit ca4519a3b2
23 changed files with 71 additions and 58 deletions

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as minimist from 'minimist';
import pkg from 'vs/code/node/package';
export interface ParsedArgs extends minimist.ParsedArgs {
help: boolean;
version: boolean;
wait: boolean;
diff: boolean;
goto: boolean;
'new-window': boolean;
'reuse-window': boolean;
locale: string;
'user-data-dir': string;
performance: boolean;
verbose: boolean;
logExtensionHostCommunication: boolean;
debugBrkFileWatcherPort: string;
'disable-extensions': boolean;
extensionHomePath: string;
extensionDevelopmentPath: string;
extensionTestsPath: string;
timestamp: string;
debugBrkPluginHost: string;
debugPluginHost: string;
}
const options: minimist.Opts = {
string: [
'locale',
'user-data-dir',
'extensionHomePath',
'extensionDevelopmentPath',
'extensionTestsPath',
'timestamp'
],
boolean: [
'help',
'version',
'wait',
'diff',
'goto',
'new-window',
'reuse-window',
'performance',
'verbose',
'logExtensionHostCommunication',
'disable-extensions'
],
alias: {
help: 'h',
version: 'v',
wait: 'w',
diff: 'd',
goto: 'g',
'new-window': 'n',
'reuse-window': 'r',
performance: 'p',
'disable-extensions': 'disableExtensions'
}
};
export function parseArgs(args: string[]) {
return minimist(args, options) as ParsedArgs;
}
const executable = 'code' + (os.platform() === 'win32' ? '.exe' : '');
const indent = ' ';
export const helpMessage = `Visual Studio Code v${ pkg.version }
Usage: ${ executable } [arguments] [paths...]
Options:
${ indent }-d, --diff Open a diff editor. Requires to pass two file paths
${ indent } as arguments.
${ indent }--disable-extensions Disable all installed extensions.
${ indent }-g, --goto Open the file at path at the line and column (add
${ indent } :line[:column] to path).
${ indent }-h, --help Print usage.
${ indent }--locale <locale> The locale to use (e.g. en-US or zh-TW).
${ indent }-n, --new-window Force a new instance of Code.
${ indent }-r, --reuse-window Force opening a file or folder in the last active
${ indent } window.
${ indent }--user-data-dir <dir> Specifies the directory that user data is kept in,
${ indent } useful when running as root.
${ indent }-v, --version Print version.
${ indent }-w, --wait Wait for the window to be closed before returning.`;

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* 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 events = require('events');
import { isString } from 'vs/base/common/types';
import { Promise } from 'vs/base/common/winjs.base';
import { json } from 'vs/base/node/request';
import { getProxyAgent } from 'vs/base/node/proxy';
import { ISettingsService } from 'vs/code/electron-main/settings';
import { IEnvironmentService } from 'vs/code/electron-main/env';
export interface IUpdate {
url: string;
name: string;
releaseNotes?: string;
version?: string;
}
export class LinuxAutoUpdaterImpl extends events.EventEmitter {
private url: string;
private currentRequest: Promise;
constructor(
@IEnvironmentService private envService: IEnvironmentService,
@ISettingsService private settingsManager: ISettingsService
) {
super();
this.url = null;
this.currentRequest = null;
}
setFeedURL(url: string): void {
this.url = url;
}
checkForUpdates(): void {
if (!this.url) {
throw new Error('No feed url set.');
}
if (this.currentRequest) {
return;
}
this.emit('checking-for-update');
const proxyUrl = this.settingsManager.getValue('http.proxy');
const strictSSL = this.settingsManager.getValue('http.proxyStrictSSL', true);
const agent = getProxyAgent(this.url, { proxyUrl, strictSSL });
this.currentRequest = json<IUpdate>({ url: this.url, agent })
.then(update => {
if (!update || !update.url || !update.version) {
this.emit('update-not-available');
} else {
this.emit('update-available', null, this.envService.product.downloadUrl);
}
})
.then(null, e => {
if (isString(e) && /^Server returned/.test(e)) {
return;
}
this.emit('update-not-available');
this.emit('error', e);
})
.then(() => this.currentRequest = null);
}
}

View File

@@ -0,0 +1,144 @@
/*---------------------------------------------------------------------------------------------
* 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 events = require('events');
import path = require('path');
import os = require('os');
import cp = require('child_process');
import pfs = require('vs/base/node/pfs');
import { mkdirp } from 'vs/base/node/extfs';
import { isString } from 'vs/base/common/types';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import { download, json } from 'vs/base/node/request';
import { getProxyAgent } from 'vs/base/node/proxy';
import { ISettingsService } from 'vs/code/electron-main/settings';
import { ILifecycleService } from 'vs/code/electron-main/lifecycle';
import { IEnvironmentService } from './env';
export interface IUpdate {
url: string;
name: string;
releaseNotes?: string;
version?: string;
}
export class Win32AutoUpdaterImpl extends events.EventEmitter {
private url: string;
private currentRequest: Promise;
constructor(
@ILifecycleService private lifecycleService: ILifecycleService,
@IEnvironmentService private envService: IEnvironmentService,
@ISettingsService private settingsManager: ISettingsService
) {
super();
this.url = null;
this.currentRequest = null;
}
public get cachePath(): TPromise<string> {
let result = path.join(os.tmpdir(), 'vscode-update');
return new TPromise<string>((c, e) => mkdirp(result, null, err => err ? e(err) : c(result)));
}
public setFeedURL(url: string): void {
this.url = url;
}
public checkForUpdates(): void {
if (!this.url) {
throw new Error('No feed url set.');
}
if (this.currentRequest) {
return;
}
this.emit('checking-for-update');
const proxyUrl = this.settingsManager.getValue('http.proxy');
const strictSSL = this.settingsManager.getValue('http.proxyStrictSSL', true);
const agent = getProxyAgent(this.url, { proxyUrl, strictSSL });
this.currentRequest = json<IUpdate>({ url: this.url, agent })
.then(update => {
if (!update || !update.url || !update.version) {
this.emit('update-not-available');
return this.cleanup();
}
this.emit('update-available');
return this.cleanup(update.version).then(() => {
return this.getUpdatePackagePath(update.version).then(updatePackagePath => {
return pfs.exists(updatePackagePath).then(exists => {
if (exists) {
return TPromise.as(updatePackagePath);
}
const url = update.url;
const downloadPath = `${updatePackagePath}.tmp`;
const agent = getProxyAgent(url, { proxyUrl, strictSSL });
return download(downloadPath, { url, agent, strictSSL })
.then(() => pfs.rename(downloadPath, updatePackagePath))
.then(() => updatePackagePath);
});
}).then(updatePackagePath => {
this.emit('update-downloaded',
{},
update.releaseNotes,
update.version,
new Date(),
this.url,
() => this.quitAndUpdate(updatePackagePath)
);
});
});
})
.then(null, e => {
if (isString(e) && /^Server returned/.test(e)) {
return;
}
this.emit('update-not-available');
this.emit('error', e);
})
.then(() => this.currentRequest = null);
}
private getUpdatePackagePath(version: string): TPromise<string> {
return this.cachePath.then(cachePath => path.join(cachePath, `CodeSetup-${ this.envService.quality }-${ version }.exe`));
}
private quitAndUpdate(updatePackagePath: string): void {
this.lifecycleService.quit().done(vetod => {
if (vetod) {
return;
}
cp.spawn(updatePackagePath, ['/silent', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore']
});
});
}
private cleanup(exceptVersion: string = null): Promise {
const filter = exceptVersion ? one => !(new RegExp(`${ this.envService.quality }-${ exceptVersion }\\.exe$`).test(one)) : () => true;
return this.cachePath
.then(cachePath => pfs.readdir(cachePath)
.then(all => Promise.join(all
.filter(filter)
.map(one => pfs.unlink(path.join(cachePath, one)).then(null, () => null))
))
);
}
}

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { spawn } from 'child_process';
import { assign } from 'vs/base/common/objects';
import { parseArgs, helpMessage } from './argv';
import pkg from 'vs/code/node/package';
export function main(args: string[]) {
const argv = parseArgs(args);
if (argv.help) {
console.log(helpMessage);
} else if (argv.version) {
console.log(pkg.version);
} else {
const env = assign({}, process.env, {
// this will signal Code that it was spawned from this module
'VSCODE_CLI': '1',
'ELECTRON_NO_ATTACH_CONSOLE': '1'
});
delete env['ATOM_SHELL_INTERNAL_RUN_AS_NODE'];
const child = spawn(process.execPath, args, {
detached: true,
stdio: 'ignore',
env
});
if (argv.wait) {
child.on('exit', process.exit);
return;
}
}
process.exit(0);
}
main(process.argv.slice(2));

View File

@@ -0,0 +1,380 @@
/*---------------------------------------------------------------------------------------------
* 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 crypto = require('crypto');
import fs = require('fs');
import path = require('path');
import os = require('os');
import {app} from 'electron';
import arrays = require('vs/base/common/arrays');
import strings = require('vs/base/common/strings');
import paths = require('vs/base/common/paths');
import platform = require('vs/base/common/platform');
import uri from 'vs/base/common/uri';
import types = require('vs/base/common/types');
import {ServiceIdentifier, createDecorator} from 'vs/platform/instantiation/common/instantiation';
import product, {IProductConfiguration} from 'vs/code/node/product';
import { parseArgs } from './argv';
export interface IProcessEnvironment {
[key: string]: string;
}
export interface ICommandLineArguments {
verboseLogging: boolean;
debugExtensionHostPort: number;
debugBrkExtensionHost: boolean;
debugBrkFileWatcherPort: number;
logExtensionHostCommunication: boolean;
disableExtensions: boolean;
extensionsHomePath: string;
extensionDevelopmentPath: string;
extensionTestsPath: string;
programStart: number;
pathArguments?: string[];
enablePerformance?: boolean;
openNewWindow?: boolean;
openInSameWindow?: boolean;
gotoLineMode?: boolean;
diffMode?: boolean;
locale?: string;
waitForWindowClose?: boolean;
}
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
export interface IEnvironmentService {
serviceId: ServiceIdentifier<any>;
cliArgs: ICommandLineArguments;
userExtensionsHome: string;
isTestingFromCli: boolean;
isBuilt: boolean;
product: IProductConfiguration;
updateUrl: string;
quality: string;
userHome: string;
appRoot: string;
currentWorkingDirectory: string;
version: string;
appHome: string;
appSettingsHome: string;
appSettingsPath: string;
appKeybindingsPath: string;
mainIPCHandle: string;
sharedIPCHandle: string;
}
function getNumericValue(value: string, defaultValue: number, fallback: number = void 0) {
const numericValue = parseInt(value);
if (types.isNumber(numericValue)) {
return numericValue;
}
if (value) {
return defaultValue;
}
return fallback;
}
export class EnvService implements IEnvironmentService {
serviceId = IEnvironmentService;
private _cliArgs: ICommandLineArguments;
get cliArgs(): ICommandLineArguments { return this._cliArgs; }
private _userExtensionsHome: string;
get userExtensionsHome(): string { return this._userExtensionsHome; }
private _isTestingFromCli: boolean;
get isTestingFromCli(): boolean { return this._isTestingFromCli; }
get isBuilt(): boolean { return !process.env['VSCODE_DEV']; }
get product(): IProductConfiguration { return product; }
get updateUrl(): string { return product.updateUrl; }
get quality(): string { return product.quality; }
private _userHome: string;
get userHome(): string { return this._userHome; }
private _appRoot: string;
get appRoot(): string { return this._appRoot; }
private _currentWorkingDirectory: string;
get currentWorkingDirectory(): string { return this._currentWorkingDirectory; }
private _version: string;
get version(): string { return this._version; }
private _appHome: string;
get appHome(): string { return this._appHome; }
private _appSettingsHome: string;
get appSettingsHome(): string { return this._appSettingsHome; }
private _appSettingsPath: string;
get appSettingsPath(): string { return this._appSettingsPath; }
private _appKeybindingsPath: string;
get appKeybindingsPath(): string { return this._appKeybindingsPath; }
private _mainIPCHandle: string;
get mainIPCHandle(): string { return this._mainIPCHandle; }
private _sharedIPCHandle: string;
get sharedIPCHandle(): string { return this._sharedIPCHandle; }
constructor() {
this._appRoot = path.dirname(uri.parse(require.toUrl('')).fsPath);
this._currentWorkingDirectory = process.env['VSCODE_CWD'] || process.cwd();
this._version = app.getVersion();
this._appHome = app.getPath('userData');
this._appSettingsHome = path.join(this._appHome, 'User');
// TODO move out of here!
if (!fs.existsSync(this._appSettingsHome)) {
fs.mkdirSync(this._appSettingsHome);
}
this._appSettingsPath = path.join(this._appSettingsHome, 'settings.json');
this._appKeybindingsPath = path.join(this._appSettingsHome, 'keybindings.json');
// Remove the Electron executable
let [, ...args] = process.argv;
// If dev, remove the first non-option argument: it's the app location
if (!this.isBuilt) {
const index = arrays.firstIndex(args, a => !/^-/.test(a));
if (index > -1) {
args.splice(index, 1);
}
}
// Finally, prepend any extra arguments from the 'argv' file
if (fs.existsSync(path.join(this._appRoot, 'argv'))) {
const extraargs: string[] = JSON.parse(fs.readFileSync(path.join(this._appRoot, 'argv'), 'utf8'));
args = [...extraargs, ...args];
}
const argv = parseArgs(args);
const debugBrkExtensionHostPort = getNumericValue(argv.debugBrkPluginHost, 5870);
const debugExtensionHostPort = getNumericValue(argv.debugPluginHost, 5870, this.isBuilt ? void 0 : 5870);
const pathArguments = parsePathArguments(this._currentWorkingDirectory, argv._, argv.goto);
const timestamp = parseInt(argv.timestamp);
const debugBrkFileWatcherPort = getNumericValue(argv.debugBrkFileWatcherPort, void 0);
this._cliArgs = Object.freeze({
pathArguments: pathArguments,
programStart: types.isNumber(timestamp) ? timestamp : 0,
enablePerformance: argv.performance,
verboseLogging: argv.verbose,
debugExtensionHostPort: debugBrkExtensionHostPort || debugExtensionHostPort,
debugBrkExtensionHost: !!debugBrkExtensionHostPort,
logExtensionHostCommunication: argv.logExtensionHostCommunication,
debugBrkFileWatcherPort: debugBrkFileWatcherPort,
openNewWindow: argv['new-window'],
openInSameWindow: argv['reuse-window'],
gotoLineMode: argv.goto,
diffMode: argv.diff && pathArguments.length === 2,
extensionsHomePath: normalizePath(argv.extensionHomePath),
extensionDevelopmentPath: normalizePath(argv.extensionDevelopmentPath),
extensionTestsPath: normalizePath(argv.extensionTestsPath),
disableExtensions: argv['disable-extensions'],
locale: argv.locale,
waitForWindowClose: argv.wait
});
this._isTestingFromCli = this.cliArgs.extensionTestsPath && !this.cliArgs.debugBrkExtensionHost;
this._userHome = path.join(app.getPath('home'), product.dataFolderName);
// TODO move out of here!
if (!fs.existsSync(this._userHome)) {
fs.mkdirSync(this._userHome);
}
this._userExtensionsHome = this.cliArgs.extensionsHomePath || path.join(this._userHome, 'extensions');
// TODO move out of here!
if (!fs.existsSync(this._userExtensionsHome)) {
fs.mkdirSync(this._userExtensionsHome);
}
this._mainIPCHandle = this.getMainIPCHandle();
this._sharedIPCHandle = this.getSharedIPCHandle();
}
private getMainIPCHandle(): string {
return this.getIPCHandleName() + (process.platform === 'win32' ? '-sock' : '.sock');
}
private getSharedIPCHandle(): string {
return this.getIPCHandleName() + '-shared' + (process.platform === 'win32' ? '-sock' : '.sock');
}
private getIPCHandleName(): string {
let handleName = app.getName();
if (!this.isBuilt) {
handleName += '-dev';
}
// Support to run VS Code multiple times as different user
// by making the socket unique over the logged in user
let userId = EnvService.getUniqueUserId();
if (userId) {
handleName += ('-' + userId);
}
if (process.platform === 'win32') {
return '\\\\.\\pipe\\' + handleName;
}
return path.join(os.tmpdir(), handleName);
}
private static getUniqueUserId(): string {
let username: string;
if (platform.isWindows) {
username = process.env.USERNAME;
} else {
username = process.env.USER;
}
if (!username) {
return ''; // fail gracefully if there is no user name
}
// use sha256 to ensure the userid value can be used in filenames and are unique
return crypto.createHash('sha256').update(username).digest('hex').substr(0, 6);
}
}
function parsePathArguments(cwd: string, args: string[], gotoLineMode?: boolean): string[] {
const result = args.map(arg => {
let pathCandidate = arg;
let parsedPath: IParsedPath;
if (gotoLineMode) {
parsedPath = parseLineAndColumnAware(arg);
pathCandidate = parsedPath.path;
}
if (pathCandidate) {
pathCandidate = preparePath(cwd, pathCandidate);
}
let realPath: string;
try {
realPath = fs.realpathSync(pathCandidate);
} catch (error) {
// in case of an error, assume the user wants to create this file
// if the path is relative, we join it to the cwd
realPath = path.normalize(path.isAbsolute(pathCandidate) ? pathCandidate : path.join(cwd, pathCandidate));
}
if (!paths.isValidBasename(path.basename(realPath))) {
return null; // do not allow invalid file names
}
if (gotoLineMode) {
parsedPath.path = realPath;
return toLineAndColumnPath(parsedPath);
}
return realPath;
});
const caseInsensitive = platform.isWindows || platform.isMacintosh;
const distinct = arrays.distinct(result, e => e && caseInsensitive ? e.toLowerCase() : e);
return arrays.coalesce(distinct);
}
function preparePath(cwd: string, p: string): string {
// Trim trailing quotes
if (platform.isWindows) {
p = strings.rtrim(p, '"'); // https://github.com/Microsoft/vscode/issues/1498
}
// Trim whitespaces
p = strings.trim(strings.trim(p, ' '), '\t');
if (platform.isWindows) {
// Resolve the path against cwd if it is relative
p = path.resolve(cwd, p);
// Trim trailing '.' chars on Windows to prevent invalid file names
p = strings.rtrim(p, '.');
}
return p;
}
function normalizePath(p?: string): string {
return p ? path.normalize(p) : p;
}
export function getPlatformIdentifier(): string {
if (process.platform === 'linux') {
return `linux-${process.arch}`;
}
return process.platform;
}
export interface IParsedPath {
path: string;
line?: number;
column?: number;
}
export function parseLineAndColumnAware(rawPath: string): IParsedPath {
let segments = rawPath.split(':'); // C:\file.txt:<line>:<column>
let path: string;
let line: number = null;
let column: number = null;
segments.forEach(segment => {
let segmentAsNumber = Number(segment);
if (!types.isNumber(segmentAsNumber)) {
path = !!path ? [path, segment].join(':') : segment; // a colon can well be part of a path (e.g. C:\...)
} else if (line === null) {
line = segmentAsNumber;
} else if (column === null) {
column = segmentAsNumber;
}
});
return {
path: path,
line: line !== null ? line : void 0,
column: column !== null ? column : line !== null ? 1 : void 0 // if we have a line, make sure column is also set
};
}
export function toLineAndColumnPath(parsedPath: IParsedPath): string {
let segments = [parsedPath.path];
if (types.isNumber(parsedPath.line)) {
segments.push(String(parsedPath.line));
}
if (types.isNumber(parsedPath.column)) {
segments.push(String(parsedPath.column));
}
return segments.join(':');
}

View File

@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* 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 { ICommandLineArguments, IProcessEnvironment } from 'vs/code/electron-main/env';
import { IWindowsService } from 'vs/code/electron-main/windows';
import { VSCodeWindow } from 'vs/code/electron-main/window';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { ILogService } from './log';
export interface ILaunchService {
start(args: ICommandLineArguments, userEnv: IProcessEnvironment): TPromise<void>;
}
export interface ILaunchChannel extends IChannel {
call(command: 'start', args: ICommandLineArguments, userEnv: IProcessEnvironment): TPromise<void>;
call(command: string, ...args: any[]): TPromise<any>;
}
export class LaunchChannel implements ILaunchChannel {
constructor(private service: ILaunchService) { }
call(command: string, ...args: any[]): TPromise<any> {
switch (command) {
case 'start': return this.service.start(args[0], args[1]);
}
}
}
export class LaunchChannelClient implements ILaunchService {
constructor(private channel: ILaunchChannel) { }
start(args: ICommandLineArguments, userEnv: IProcessEnvironment): TPromise<void> {
return this.channel.call('start', args, userEnv);
}
}
export class LaunchService implements ILaunchService {
constructor(
@ILogService private logService: ILogService,
@IWindowsService private windowsManager: IWindowsService
) {}
start(args: ICommandLineArguments, userEnv: IProcessEnvironment): TPromise<void> {
this.logService.log('Received data from other instance', args);
// Otherwise handle in windows manager
let usedWindows: VSCodeWindow[];
if (!!args.extensionDevelopmentPath) {
this.windowsManager.openPluginDevelopmentHostWindow({ cli: args, userEnv: userEnv });
} else if (args.pathArguments.length === 0 && args.openNewWindow) {
usedWindows = this.windowsManager.open({ cli: args, userEnv: userEnv, forceNewWindow: true, forceEmpty: true });
} else if (args.pathArguments.length === 0) {
usedWindows = [this.windowsManager.focusLastActive(args)];
} else {
usedWindows = this.windowsManager.open({
cli: args,
userEnv: userEnv,
forceNewWindow: args.waitForWindowClose || args.openNewWindow,
preferNewWindow: !args.openInSameWindow,
diffMode: args.diffMode
});
}
// If the other instance is waiting to be killed, we hook up a window listener if one window
// is being used and only then resolve the startup promise which will kill this second instance
if (args.waitForWindowClose && usedWindows && usedWindows.length === 1 && usedWindows[0]) {
const windowId = usedWindows[0].id;
return new TPromise<void>((c, e) => {
const unbind = this.windowsManager.onClose(id => {
if (id === windowId) {
unbind();
c(null);
}
});
});
}
return TPromise.as(null);
}
}

View File

@@ -0,0 +1,184 @@
/*---------------------------------------------------------------------------------------------
* 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 events = require('events');
import {ipcMain as ipc, app} from 'electron';
import {TPromise, TValueCallback} from 'vs/base/common/winjs.base';
import {ReadyState, VSCodeWindow} from 'vs/code/electron-main/window';
import env = require('vs/code/electron-main/env');
import {ServiceIdentifier, createDecorator} from 'vs/platform/instantiation/common/instantiation';
import {ILogService} from './log';
const EventTypes = {
BEFORE_QUIT: 'before-quit'
};
export const ILifecycleService = createDecorator<ILifecycleService>('lifecycleService');
export interface ILifecycleService {
serviceId: ServiceIdentifier<any>;
onBeforeQuit(clb: () => void): () => void;
ready(): void;
registerWindow(vscodeWindow: VSCodeWindow): void;
unload(vscodeWindow: VSCodeWindow): TPromise<boolean /* veto */>;
quit(): TPromise<boolean /* veto */>;
}
export class LifecycleService implements ILifecycleService {
serviceId = ILifecycleService;
private eventEmitter = new events.EventEmitter();
private windowToCloseRequest: { [windowId: string]: boolean };
private quitRequested: boolean;
private pendingQuitPromise: TPromise<boolean>;
private pendingQuitPromiseComplete: TValueCallback<boolean>;
private oneTimeListenerTokenGenerator: number;
constructor(
@env.IEnvironmentService private envService: env.IEnvironmentService,
@ILogService private logService: ILogService
) {
this.windowToCloseRequest = Object.create(null);
this.quitRequested = false;
this.oneTimeListenerTokenGenerator = 0;
}
/**
* Due to the way we handle lifecycle with eventing, the general app.on('before-quit')
* event cannot be used because it can be called twice on shutdown. Instead the onBeforeQuit
* handler in this module can be used and it is only called once on a shutdown sequence.
*/
onBeforeQuit(clb: () => void): () => void {
this.eventEmitter.addListener(EventTypes.BEFORE_QUIT, clb);
return () => this.eventEmitter.removeListener(EventTypes.BEFORE_QUIT, clb);
}
public ready(): void {
this.registerListeners();
}
private registerListeners(): void {
// before-quit
app.on('before-quit', (e) => {
this.logService.log('Lifecycle#before-quit');
if (!this.quitRequested) {
this.eventEmitter.emit(EventTypes.BEFORE_QUIT); // only send this if this is the first quit request we have
}
this.quitRequested = true;
});
// window-all-closed
app.on('window-all-closed', () => {
this.logService.log('Lifecycle#window-all-closed');
// Windows/Linux: we quit when all windows have closed
// Mac: we only quit when quit was requested
// --wait: we quit when all windows are closed
if (this.quitRequested || process.platform !== 'darwin' || this.envService.cliArgs.waitForWindowClose) {
app.quit();
}
});
}
public registerWindow(vscodeWindow: VSCodeWindow): void {
// Window Before Closing: Main -> Renderer
vscodeWindow.win.on('close', (e) => {
let windowId = vscodeWindow.id;
this.logService.log('Lifecycle#window-before-close', windowId);
// The window already acknowledged to be closed
if (this.windowToCloseRequest[windowId]) {
this.logService.log('Lifecycle#window-close', windowId);
delete this.windowToCloseRequest[windowId];
return;
}
// Otherwise prevent unload and handle it from window
e.preventDefault();
this.unload(vscodeWindow).done(veto => {
if (!veto) {
this.windowToCloseRequest[windowId] = true;
vscodeWindow.win.close();
} else {
this.quitRequested = false;
delete this.windowToCloseRequest[windowId];
}
});
});
}
public unload(vscodeWindow: VSCodeWindow): TPromise<boolean /* veto */> {
// Always allow to unload a window that is not yet ready
if (vscodeWindow.readyState !== ReadyState.READY) {
return TPromise.as<boolean>(false);
}
this.logService.log('Lifecycle#unload()', vscodeWindow.id);
return new TPromise<boolean>((c) => {
let oneTimeEventToken = this.oneTimeListenerTokenGenerator++;
let oneTimeOkEvent = 'vscode:ok' + oneTimeEventToken;
let oneTimeCancelEvent = 'vscode:cancel' + oneTimeEventToken;
ipc.once(oneTimeOkEvent, () => {
c(false); // no veto
});
ipc.once(oneTimeCancelEvent, () => {
// Any cancellation also cancels a pending quit if present
if (this.pendingQuitPromiseComplete) {
this.pendingQuitPromiseComplete(true /* veto */);
this.pendingQuitPromiseComplete = null;
this.pendingQuitPromise = null;
}
c(true); // veto
});
vscodeWindow.send('vscode:beforeUnload', { okChannel: oneTimeOkEvent, cancelChannel: oneTimeCancelEvent });
});
}
/**
* A promise that completes to indicate if the quit request has been veto'd
* by the user or not.
*/
public quit(): TPromise<boolean /* veto */> {
this.logService.log('Lifecycle#quit()');
if (!this.pendingQuitPromise) {
this.pendingQuitPromise = new TPromise<boolean>((c) => {
// Store as field to access it from a window cancellation
this.pendingQuitPromiseComplete = c;
app.once('will-quit', () => {
if (this.pendingQuitPromiseComplete) {
this.pendingQuitPromiseComplete(false /* no veto */);
this.pendingQuitPromiseComplete = null;
this.pendingQuitPromise = null;
}
});
app.quit();
});
}
return this.pendingQuitPromise;
}
}

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* 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 { ServiceIdentifier, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService } from './env';
export const ILogService = createDecorator<ILogService>('logService');
export interface ILogService {
serviceId: ServiceIdentifier<any>;
log(... args: any[]): void;
}
export class MainLogService implements ILogService {
serviceId = ILogService;
constructor(@IEnvironmentService private envService: IEnvironmentService) {
}
log(...args: any[]): void {
const { verboseLogging } = this.envService.cliArgs;
if (verboseLogging) {
console.log(`(${new Date().toLocaleTimeString()})`, ...args);
}
}
}

View File

@@ -0,0 +1,288 @@
/*---------------------------------------------------------------------------------------------
* 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 {app, ipcMain as ipc} from 'electron';
import fs = require('fs');
import nls = require('vs/nls');
import {assign} from 'vs/base/common/objects';
import platform = require('vs/base/common/platform');
import { IProcessEnvironment, IEnvironmentService, EnvService } from 'vs/code/electron-main/env';
import windows = require('vs/code/electron-main/windows');
import { ILifecycleService, LifecycleService } from 'vs/code/electron-main/lifecycle';
import { VSCodeMenu } from 'vs/code/electron-main/menus';
import {ISettingsService, SettingsManager} from 'vs/code/electron-main/settings';
import {IUpdateService, UpdateManager} from 'vs/code/electron-main/update-manager';
import {Server, serve, connect} from 'vs/base/parts/ipc/node/ipc.net';
import {getUnixUserEnvironment, IEnv} from 'vs/base/node/env';
import {TPromise} from 'vs/base/common/winjs.base';
import {AskpassChannel} from 'vs/workbench/parts/git/common/gitIpc';
import {GitAskpassService} from 'vs/workbench/parts/git/electron-main/askpassService';
import {spawnSharedProcess} from 'vs/code/electron-main/sharedProcess';
import {Mutex} from 'windows-mutex';
import {LaunchService, ILaunchChannel, LaunchChannel, LaunchChannelClient} from './launch';
import {ServicesAccessor, IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
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 './log';
import {IStorageService, StorageService} from './storage';
function quit(accessor: ServicesAccessor, error?: Error);
function quit(accessor: ServicesAccessor, message?: string);
function quit(accessor: ServicesAccessor, arg?: any) {
const logService = accessor.get(ILogService);
let exitCode = 0;
if (typeof arg === 'string') {
logService.log(arg);
} else {
exitCode = 1; // signal error to the outside
if (arg.stack) {
console.error(arg.stack);
} else {
console.error('Startup error: ' + arg.toString());
}
}
process.exit(exitCode); // in main, process.exit === app.exit
}
function main(accessor: ServicesAccessor, ipcServer: Server, userEnv: IProcessEnvironment): void {
const instantiationService = accessor.get(IInstantiationService);
const logService = accessor.get(ILogService);
const envService = accessor.get(IEnvironmentService);
const windowManager = accessor.get(windows.IWindowsService);
const lifecycleService = accessor.get(ILifecycleService);
const updateManager = accessor.get(IUpdateService);
const settingsManager = accessor.get(ISettingsService);
// We handle uncaught exceptions here to prevent electron from opening a dialog to the user
process.on('uncaughtException', (err: any) => {
if (err) {
// take only the message and stack property
let friendlyError = {
message: err.message,
stack: err.stack
};
// handle on client side
windowManager.sendToFocused('vscode:reportError', JSON.stringify(friendlyError));
}
console.error('[uncaught exception in main]: ' + err);
if (err.stack) {
console.error(err.stack);
}
});
logService.log('### VSCode main.js ###');
logService.log(envService.appRoot, envService.cliArgs);
// Setup Windows mutex
let windowsMutex: Mutex = null;
try {
const Mutex = (<any>require.__$__nodeRequire('windows-mutex')).Mutex;
windowsMutex = new Mutex(envService.product.win32MutexName);
} catch (e) {
// noop
}
// Register IPC services
const launchService = instantiationService.createInstance(LaunchService);
const launchChannel = new LaunchChannel(launchService);
ipcServer.registerChannel('launch', launchChannel);
const askpassService = new GitAskpassService();
const askpassChannel = new AskpassChannel(askpassService);
ipcServer.registerChannel('askpass', askpassChannel);
// Used by sub processes to communicate back to the main instance
process.env['VSCODE_PID'] = '' + process.pid;
process.env['VSCODE_IPC_HOOK'] = envService.mainIPCHandle;
process.env['VSCODE_SHARED_IPC_HOOK'] = envService.sharedIPCHandle;
// Spawn shared process
const sharedProcess = instantiationService.invokeFunction(spawnSharedProcess);
// Make sure we associate the program with the app user model id
// This will help Windows to associate the running program with
// any shortcut that is pinned to the taskbar and prevent showing
// two icons in the taskbar for the same app.
if (platform.isWindows && envService.product.win32AppUserModelId) {
app.setAppUserModelId(envService.product.win32AppUserModelId);
}
// Set programStart in the global scope
global.programStart = envService.cliArgs.programStart;
function dispose() {
if (ipcServer) {
ipcServer.dispose();
ipcServer = null;
}
sharedProcess.dispose();
if (windowsMutex) {
windowsMutex.release();
}
}
// Dispose on app quit
app.on('will-quit', () => {
logService.log('App#will-quit: disposing resources');
dispose();
});
// Dispose on vscode:exit
ipc.on('vscode:exit', (event, code: number) => {
logService.log('IPC#vscode:exit', code);
dispose();
process.exit(code); // in main, process.exit === app.exit
});
// Lifecycle
lifecycleService.ready();
// Load settings
settingsManager.loadSync();
// Propagate to clients
windowManager.ready(userEnv);
// Install Menu
const menuManager = instantiationService.createInstance(VSCodeMenu);
menuManager.ready();
// Install Tasks
if (platform.isWindows && envService.isBuilt) {
app.setUserTasks([
{
title: nls.localize('newWindow', "New Window"),
program: process.execPath,
arguments: '-n', // force new window
iconPath: process.execPath,
iconIndex: 0
}
]);
}
// Setup auto update
updateManager.initialize();
// Open our first window
if (envService.cliArgs.openNewWindow && envService.cliArgs.pathArguments.length === 0) {
windowManager.open({ cli: envService.cliArgs, forceNewWindow: true, forceEmpty: true }); // new window if "-n" was used without paths
} else if (global.macOpenFiles && global.macOpenFiles.length && (!envService.cliArgs.pathArguments || !envService.cliArgs.pathArguments.length)) {
windowManager.open({ cli: envService.cliArgs, pathsToOpen: global.macOpenFiles }); // mac: open-file event received on startup
} else {
windowManager.open({ cli: envService.cliArgs, forceNewWindow: envService.cliArgs.openNewWindow, diffMode: envService.cliArgs.diffMode }); // default: read paths from cli
}
}
function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
const logService = accessor.get(ILogService);
const envService = accessor.get(IEnvironmentService);
function setup(retry: boolean): TPromise<Server> {
return serve(envService.mainIPCHandle).then(server => {
if (platform.isMacintosh) {
app.dock.show(); // dock might be hidden at this case due to a retry
}
return server;
}, err => {
if (err.code !== 'EADDRINUSE') {
return TPromise.wrapError(err);
}
// Since we are the second instance, we do not want to show the dock
if (platform.isMacintosh) {
app.dock.hide();
}
// there's a running instance, let's connect to it
return connect(envService.mainIPCHandle).then(
client => {
// Tests from CLI require to be the only instance currently (TODO@Ben support multiple instances and output)
if (envService.isTestingFromCli) {
const msg = 'Running extension tests from the command line is currently only supported if no other instance of Code is running.';
console.error(msg);
client.dispose();
return TPromise.wrapError(msg);
}
logService.log('Sending env to running instance...');
const channel = client.getChannel<ILaunchChannel>('launch');
const service = new LaunchChannelClient(channel);
return service.start(envService.cliArgs, process.env)
.then(() => client.dispose())
.then(() => TPromise.wrapError('Sent env to running instance. Terminating...'));
},
err => {
if (!retry || platform.isWindows || err.code !== 'ECONNREFUSED') {
return TPromise.wrapError(err);
}
// it happens on Linux and OS X that the pipe is left behind
// let's delete it, since we can't connect to it
// and the retry the whole thing
try {
fs.unlinkSync(envService.mainIPCHandle);
} catch (e) {
logService.log('Fatal error deleting obsolete instance handle', e);
return TPromise.wrapError(e);
}
return setup(false);
}
);
});
}
return setup(true);
}
// TODO: isolate
const services = new ServiceCollection();
services.set(IEnvironmentService, new SyncDescriptor(EnvService));
services.set(ILogService, new SyncDescriptor(MainLogService));
services.set(windows.IWindowsService, new SyncDescriptor(windows.WindowsManager));
services.set(ILifecycleService, new SyncDescriptor(LifecycleService));
services.set(IStorageService, new SyncDescriptor(StorageService));
services.set(IUpdateService, new SyncDescriptor(UpdateManager));
services.set(ISettingsService, new SyncDescriptor(SettingsManager));
const instantiationService = new InstantiationService(services);
function getUserEnvironment(): TPromise<IEnv> {
return platform.isWindows ? TPromise.as({}) : getUnixUserEnvironment();
}
// On some platforms we need to manually read from the global environment variables
// and assign them to the process environment (e.g. when doubleclick app on Mac)
getUserEnvironment()
.then(userEnv => {
if (process.env['VSCODE_CLI'] !== '1') {
assign(process.env, userEnv);
}
// Make sure the NLS Config travels to the rendered process
// See also https://github.com/Microsoft/vscode/issues/4558
userEnv['VSCODE_NLS_CONFIG'] = process.env['VSCODE_NLS_CONFIG'];
return instantiationService.invokeFunction(setupIPC)
.then(ipcServer => instantiationService.invokeFunction(main, ipcServer, userEnv));
})
.done(null, err => instantiationService.invokeFunction(quit, err));

View File

@@ -0,0 +1,801 @@
/*---------------------------------------------------------------------------------------------
* 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 {ipcMain as ipc, app, shell, dialog, Menu, MenuItem} from 'electron';
import nls = require('vs/nls');
import platform = require('vs/base/common/platform');
import arrays = require('vs/base/common/arrays');
import { IWindowsService, WindowsManager, IOpenedPathsList } from 'vs/code/electron-main/windows';
import window = require('vs/code/electron-main/window');
import env = require('vs/code/electron-main/env');
import { IStorageService } from 'vs/code/electron-main/storage';
import { IUpdateService, State as UpdateState } from 'vs/code/electron-main/update-manager';
import {Keybinding} from 'vs/base/common/keyCodes';
interface IResolvedKeybinding {
id: string;
binding: number;
}
export class VSCodeMenu {
private static lastKnownKeybindingsMapStorageKey = 'lastKnownKeybindings';
private static MAX_RECENT_ENTRIES = 10;
private isQuitting: boolean;
private appMenuInstalled: boolean;
private actionIdKeybindingRequests: string[];
private mapLastKnownKeybindingToActionId: { [id: string]: string; };
private mapResolvedKeybindingToActionId: { [id: string]: string; };
private keybindingsResolved: boolean;
constructor(
@IStorageService private storageService: IStorageService,
@IUpdateService private updateManager: IUpdateService,
@IWindowsService private windowsManager: IWindowsService,
@env.IEnvironmentService private envService: env.IEnvironmentService
) {
this.actionIdKeybindingRequests = [];
this.mapResolvedKeybindingToActionId = Object.create(null);
this.mapLastKnownKeybindingToActionId = this.storageService.getItem<{ [id: string]: string; }>(VSCodeMenu.lastKnownKeybindingsMapStorageKey) || Object.create(null);
}
public ready(): void {
this.registerListeners();
this.install();
}
private registerListeners(): void {
// Keep flag when app quits
app.on('will-quit', () => {
this.isQuitting = true;
});
// Listen to "open" & "close" event from window manager
this.windowsManager.onOpen((paths) => this.onOpen(paths));
this.windowsManager.onClose(_ => this.onClose(this.windowsManager.getWindowCount()));
// Resolve keybindings when any first workbench is loaded
this.windowsManager.onReady((win) => this.resolveKeybindings(win));
// Listen to resolved keybindings
ipc.on('vscode:keybindingsResolved', (event, rawKeybindings) => {
let keybindings: IResolvedKeybinding[] = [];
try {
keybindings = JSON.parse(rawKeybindings);
} catch (error) {
// Should not happen
}
// Fill hash map of resolved keybindings
let needsMenuUpdate = false;
keybindings.forEach((keybinding) => {
let accelerator = new Keybinding(keybinding.binding)._toElectronAccelerator();
if (accelerator) {
this.mapResolvedKeybindingToActionId[keybinding.id] = accelerator;
if (this.mapLastKnownKeybindingToActionId[keybinding.id] !== accelerator) {
needsMenuUpdate = true; // we only need to update when something changed!
}
}
});
// A keybinding might have been unassigned, so we have to account for that too
if (Object.keys(this.mapLastKnownKeybindingToActionId).length !== Object.keys(this.mapResolvedKeybindingToActionId).length) {
needsMenuUpdate = true;
}
if (needsMenuUpdate) {
this.storageService.setItem(VSCodeMenu.lastKnownKeybindingsMapStorageKey, this.mapResolvedKeybindingToActionId); // keep to restore instantly after restart
this.mapLastKnownKeybindingToActionId = this.mapResolvedKeybindingToActionId; // update our last known map
this.updateMenu();
}
});
// Listen to update manager
this.updateManager.on('change', () => this.updateMenu());
}
private resolveKeybindings(win: window.VSCodeWindow): void {
if (this.keybindingsResolved) {
return; // only resolve once
}
this.keybindingsResolved = true;
// Resolve keybindings when workbench window is up
if (this.actionIdKeybindingRequests.length) {
win.send('vscode:resolveKeybindings', JSON.stringify(this.actionIdKeybindingRequests));
}
}
private updateMenu(): void {
// Due to limitations in Electron, it is not possible to update menu items dynamically. The suggested
// workaround from Electron is to set the application menu again.
// See also https://github.com/electron/electron/issues/846
//
// Run delayed to prevent updating menu while it is open
if (!this.isQuitting) {
setTimeout(() => {
if (!this.isQuitting) {
this.install();
}
}, 10 /* delay this because there is an issue with updating a menu when it is open */);
}
}
private onOpen(path: window.IPath): void {
this.addToOpenedPathsList(path.filePath || path.workspacePath, !!path.filePath);
this.updateMenu();
}
private onClose(remainingWindowCount: number): void {
if (remainingWindowCount === 0 && platform.isMacintosh) {
this.updateMenu();
}
}
private install(): void {
// Menus
let menubar = new Menu();
// Mac: Application
let macApplicationMenuItem: Electron.MenuItem;
if (platform.isMacintosh) {
let applicationMenu = new Menu();
macApplicationMenuItem = new MenuItem({ label: this.envService.product.nameShort, submenu: applicationMenu });
this.setMacApplicationMenu(applicationMenu);
}
// File
let fileMenu = new Menu();
let fileMenuItem = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu });
this.setFileMenu(fileMenu);
// Edit
let editMenu = new Menu();
let editMenuItem = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu });
this.setEditMenu(editMenu);
// View
let viewMenu = new Menu();
let viewMenuItem = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu });
this.setViewMenu(viewMenu);
// Goto
let gotoMenu = new Menu();
let gotoMenuItem = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Goto")), submenu: gotoMenu });
this.setGotoMenu(gotoMenu);
// Mac: Window
let macWindowMenuItem: Electron.MenuItem;
if (platform.isMacintosh) {
let windowMenu = new Menu();
macWindowMenuItem = new MenuItem({ label: mnemonicLabel(nls.localize('mWindow', "Window")), submenu: windowMenu, role: 'window' });
this.setMacWindowMenu(windowMenu);
}
// Help
let helpMenu = new Menu();
let helpMenuItem = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' });
this.setHelpMenu(helpMenu);
// Menu Structure
if (macApplicationMenuItem) {
menubar.append(macApplicationMenuItem);
}
menubar.append(fileMenuItem);
menubar.append(editMenuItem);
menubar.append(viewMenuItem);
menubar.append(gotoMenuItem);
if (macWindowMenuItem) {
menubar.append(macWindowMenuItem);
}
menubar.append(helpMenuItem);
Menu.setApplicationMenu(menubar);
// Dock Menu
if (platform.isMacintosh && !this.appMenuInstalled) {
this.appMenuInstalled = true;
let dockMenu = new Menu();
dockMenu.append(new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "&&New Window")), click: () => this.windowsManager.openNewWindow() }));
app.dock.setMenu(dockMenu);
}
}
private addToOpenedPathsList(path?: string, isFile?: boolean): void {
if (!path) {
return;
}
let mru = this.getOpenedPathsList();
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, VSCodeMenu.MAX_RECENT_ENTRIES);
mru.files = mru.files.slice(0, VSCodeMenu.MAX_RECENT_ENTRIES);
this.storageService.setItem(WindowsManager.openedPathsListStorageKey, mru);
}
private removeFromOpenedPathsList(path: string): void {
let mru = this.getOpenedPathsList();
let index = mru.files.indexOf(path);
if (index >= 0) {
mru.files.splice(index, 1);
}
index = mru.folders.indexOf(path);
if (index >= 0) {
mru.folders.splice(index, 1);
}
this.storageService.setItem(WindowsManager.openedPathsListStorageKey, mru);
}
private clearOpenedPathsList(): void {
this.storageService.setItem(WindowsManager.openedPathsListStorageKey, { folders: [], files: [] });
app.clearRecentDocuments();
this.updateMenu();
}
private getOpenedPathsList(): IOpenedPathsList {
let mru = this.storageService.getItem<IOpenedPathsList>(WindowsManager.openedPathsListStorageKey);
if (!mru) {
mru = { folders: [], files: [] };
}
return mru;
}
private setMacApplicationMenu(macApplicationMenu: Electron.Menu): void {
let about = new MenuItem({ label: nls.localize('mAbout', "About {0}", this.envService.product.nameLong), role: 'about' });
let checkForUpdates = this.getUpdateMenuItems();
let preferences = this.getPreferencesMenu();
let hide = new MenuItem({ label: nls.localize('mHide', "Hide {0}", this.envService.product.nameLong), role: 'hide', accelerator: 'Command+H' });
let hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideothers', accelerator: 'Command+Alt+H' });
let showAll = new MenuItem({ label: nls.localize('mShowAll', "Show All"), role: 'unhide' });
let quit = new MenuItem({ label: nls.localize('miQuit', "Quit {0}", this.envService.product.nameLong), click: () => this.quit(), accelerator: 'Command+Q' });
let actions = [about];
actions.push(...checkForUpdates);
actions.push(...[
__separator__(),
preferences,
__separator__(),
hide,
hideOthers,
showAll,
__separator__(),
quit
]);
actions.forEach(i => macApplicationMenu.append(i));
}
private setFileMenu(fileMenu: Electron.Menu): void {
let hasNoWindows = (this.windowsManager.getWindowCount() === 0);
let newFile: Electron.MenuItem;
if (hasNoWindows) {
newFile = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File")), accelerator: this.getAccelerator('workbench.action.files.newUntitledFile'), click: () => this.windowsManager.openNewWindow() });
} else {
newFile = this.createMenuItem(nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File"), 'workbench.action.files.newUntitledFile');
}
let open = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miOpen', comment: ['&& denotes a mnemonic'] }, "&&Open...")), accelerator: this.getAccelerator('workbench.action.files.openFileFolder'), click: () => this.windowsManager.openFileFolderPicker() });
let openFile = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miOpenFile', comment: ['&& denotes a mnemonic'] }, "&&Open File...")), accelerator: this.getAccelerator('workbench.action.files.openFile'), click: () => this.windowsManager.openFilePicker() });
let openFolder = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miOpenFolder', comment: ['&& denotes a mnemonic'] }, "Open &&Folder...")), accelerator: this.getAccelerator('workbench.action.files.openFolder'), click: () => this.windowsManager.openFolderPicker() });
let openRecentMenu = new Menu();
this.setOpenRecentMenu(openRecentMenu);
let openRecent = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent")), submenu: openRecentMenu, enabled: openRecentMenu.items.length > 0 });
let saveFile = this.createMenuItem(nls.localize({ key: 'miSave', comment: ['&& denotes a mnemonic'] }, "&&Save"), 'workbench.action.files.save', this.windowsManager.getWindowCount() > 0);
let saveFileAs = this.createMenuItem(nls.localize({ key: 'miSaveAs', comment: ['&& denotes a mnemonic'] }, "Save &&As..."), 'workbench.action.files.saveAs', this.windowsManager.getWindowCount() > 0);
let saveAllFiles = this.createMenuItem(nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll"), 'workbench.action.files.saveAll', this.windowsManager.getWindowCount() > 0);
let preferences = this.getPreferencesMenu();
let newWindow = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "&&New Window")), accelerator: this.getAccelerator('workbench.action.newWindow'), click: () => this.windowsManager.openNewWindow() });
let revertFile = this.createMenuItem(nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Revert F&&ile"), 'workbench.action.files.revert', this.windowsManager.getWindowCount() > 0);
let closeWindow = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Close &&Window")), accelerator: this.getAccelerator('workbench.action.closeWindow'), click: () => this.windowsManager.getLastActiveWindow().win.close(), enabled: this.windowsManager.getWindowCount() > 0 });
let closeFolder = this.createMenuItem(nls.localize({ key: 'miCloseFolder', comment: ['&& denotes a mnemonic'] }, "Close &&Folder"), 'workbench.action.closeFolder');
let closeEditor = this.createMenuItem(nls.localize({ key: 'miCloseEditor', comment: ['&& denotes a mnemonic'] }, "Close &&Editor"), 'workbench.action.closeActiveEditor');
let exit = this.createMenuItem(nls.localize({ key: 'miExit', comment: ['&& denotes a mnemonic'] }, "E&&xit"), () => this.quit());
arrays.coalesce([
newFile,
newWindow,
__separator__(),
platform.isMacintosh ? open : null,
!platform.isMacintosh ? openFile : null,
!platform.isMacintosh ? openFolder : null,
openRecent,
__separator__(),
saveFile,
saveFileAs,
saveAllFiles,
__separator__(),
!platform.isMacintosh ? preferences : null,
!platform.isMacintosh ? __separator__() : null,
revertFile,
closeEditor,
closeFolder,
!platform.isMacintosh ? closeWindow : null,
!platform.isMacintosh ? __separator__() : null,
!platform.isMacintosh ? exit : null
]).forEach((item) => fileMenu.append(item));
}
private getPreferencesMenu(): Electron.MenuItem {
let userSettings = this.createMenuItem(nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&User Settings"), 'workbench.action.openGlobalSettings');
let workspaceSettings = this.createMenuItem(nls.localize({ key: 'miOpenWorkspaceSettings', comment: ['&& denotes a mnemonic'] }, "&&Workspace Settings"), 'workbench.action.openWorkspaceSettings');
let kebindingSettings = this.createMenuItem(nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts"), 'workbench.action.openGlobalKeybindings');
let snippetsSettings = this.createMenuItem(nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets"), 'workbench.action.openSnippets');
let themeSelection = this.createMenuItem(nls.localize({ key: 'miSelectTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme"), 'workbench.action.selectTheme');
let preferencesMenu = new Menu();
preferencesMenu.append(userSettings);
preferencesMenu.append(workspaceSettings);
preferencesMenu.append(__separator__());
preferencesMenu.append(kebindingSettings);
preferencesMenu.append(__separator__());
preferencesMenu.append(snippetsSettings);
preferencesMenu.append(__separator__());
preferencesMenu.append(themeSelection);
return new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu });
}
private quit(): void {
// If the user selected to exit from an extension development host window, do not quit, but just
// close the window unless this is the last window that is opened.
let vscodeWindow = this.windowsManager.getFocusedWindow();
if (vscodeWindow && vscodeWindow.isPluginDevelopmentHost && this.windowsManager.getWindowCount() > 1) {
vscodeWindow.win.close();
}
// Otherwise: normal quit
else {
setTimeout(() => {
this.isQuitting = true;
app.quit();
}, 10 /* delay this because there is an issue with quitting while the menu is open */);
}
}
private setOpenRecentMenu(openRecentMenu: Electron.Menu): void {
openRecentMenu.append(this.createMenuItem(nls.localize({ key: 'miReopenClosedFile', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed File"), 'workbench.files.action.reopenClosedFile'));
let recentList = this.getOpenedPathsList();
// Folders
if (recentList.folders.length > 0) {
openRecentMenu.append(__separator__());
recentList.folders.forEach((folder, index) => {
if (index < VSCodeMenu.MAX_RECENT_ENTRIES) {
openRecentMenu.append(this.createOpenRecentMenuItem(folder));
}
});
}
// Files
let files = recentList.files;
if (platform.isMacintosh && recentList.files.length > 0) {
files = recentList.files.filter(f => recentList.folders.indexOf(f) < 0); // TODO@Ben migration (remove in the future)
}
if (files.length > 0) {
openRecentMenu.append(__separator__());
files.forEach((file, index) => {
if (index < VSCodeMenu.MAX_RECENT_ENTRIES) {
openRecentMenu.append(this.createOpenRecentMenuItem(file));
}
});
}
if (recentList.folders.length || files.length) {
openRecentMenu.append(__separator__());
openRecentMenu.append(new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miClearItems', comment: ['&& denotes a mnemonic'] }, "&&Clear Items")), click: () => this.clearOpenedPathsList() }));
}
}
private createOpenRecentMenuItem(path: string): Electron.MenuItem {
return new MenuItem({
label: path, click: () => {
let success = !!this.windowsManager.open({ cli: this.envService.cliArgs, pathsToOpen: [path] });
if (!success) {
this.removeFromOpenedPathsList(path);
this.updateMenu();
}
}
});
}
private createRoleMenuItem(label: string, actionId: string, role: string): Electron.MenuItem {
let options: Electron.MenuItemOptions = {
label: mnemonicLabel(label),
accelerator: this.getAccelerator(actionId),
role: role,
enabled: true
};
return new MenuItem(options);
}
private setEditMenu(winLinuxEditMenu: Electron.Menu): void {
let undo: Electron.MenuItem;
let redo: Electron.MenuItem;
let cut: Electron.MenuItem;
let copy: Electron.MenuItem;
let paste: Electron.MenuItem;
let selectAll: Electron.MenuItem;
if (platform.isMacintosh) {
undo = this.createDevToolsAwareMenuItem(nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"), 'undo', (devTools) => devTools.undo());
redo = this.createDevToolsAwareMenuItem(nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"), 'redo', (devTools) => devTools.redo());
cut = this.createRoleMenuItem(nls.localize({ key: 'miCut', comment: ['&& denotes a mnemonic'] }, "&&Cut"), 'editor.action.clipboardCutAction', 'cut');
copy = this.createRoleMenuItem(nls.localize({ key: 'miCopy', comment: ['&& denotes a mnemonic'] }, "C&&opy"), 'editor.action.clipboardCopyAction', 'copy');
paste = this.createRoleMenuItem(nls.localize({ key: 'miPaste', comment: ['&& denotes a mnemonic'] }, "&&Paste"), 'editor.action.clipboardPasteAction', 'paste');
selectAll = this.createDevToolsAwareMenuItem(nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"), 'editor.action.selectAll', (devTools) => devTools.selectAll());
} else {
undo = this.createMenuItem(nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"), 'undo');
redo = this.createMenuItem(nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"), 'redo');
cut = this.createMenuItem(nls.localize({ key: 'miCut', comment: ['&& denotes a mnemonic'] }, "&&Cut"), 'editor.action.clipboardCutAction');
copy = this.createMenuItem(nls.localize({ key: 'miCopy', comment: ['&& denotes a mnemonic'] }, "C&&opy"), 'editor.action.clipboardCopyAction');
paste = this.createMenuItem(nls.localize({ key: 'miPaste', comment: ['&& denotes a mnemonic'] }, "&&Paste"), 'editor.action.clipboardPasteAction');
selectAll = this.createMenuItem(nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"), 'editor.action.selectAll');
}
let find = this.createMenuItem(nls.localize({ key: 'miFind', comment: ['&& denotes a mnemonic'] }, "&&Find"), 'actions.find');
let replace = this.createMenuItem(nls.localize({ key: 'miReplace', comment: ['&& denotes a mnemonic'] }, "&&Replace"), 'editor.action.startFindReplaceAction');
let findInFiles = this.createMenuItem(nls.localize({ key: 'miFindInFiles', comment: ['&& denotes a mnemonic'] }, "Find &&in Files"), 'workbench.view.search');
[
undo,
redo,
__separator__(),
cut,
copy,
paste,
selectAll,
__separator__(),
find,
replace,
__separator__(),
findInFiles
].forEach((item) => winLinuxEditMenu.append(item));
}
private setViewMenu(viewMenu: Electron.Menu): void {
let explorer = this.createMenuItem(nls.localize({ key: 'miViewExplorer', comment: ['&& denotes a mnemonic'] }, "&&Explorer"), 'workbench.view.explorer');
let search = this.createMenuItem(nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search"), 'workbench.view.search');
let git = this.createMenuItem(nls.localize({ key: 'miViewGit', comment: ['&& denotes a mnemonic'] }, "&&Git"), 'workbench.view.git');
let debug = this.createMenuItem(nls.localize({ key: 'miViewDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug"), 'workbench.view.debug');
let commands = this.createMenuItem(nls.localize({ key: 'miCommandPalette', comment: ['&& denotes a mnemonic'] }, "&&Command Palette..."), 'workbench.action.showCommands');
let markers = this.createMenuItem(nls.localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Errors and Warnings..."), 'workbench.action.showErrorsWarnings');
let output = this.createMenuItem(nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "Toggle &&Output"), 'workbench.action.output.toggleOutput');
let debugConsole = this.createMenuItem(nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "Toggle De&&bug Console"), 'workbench.debug.action.toggleRepl');
let fullscreen = new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen")), accelerator: this.getAccelerator('workbench.action.toggleFullScreen'), click: () => this.windowsManager.getLastActiveWindow().toggleFullScreen(), enabled: this.windowsManager.getWindowCount() > 0 });
let toggleMenuBar = this.createMenuItem(nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar"), 'workbench.action.toggleMenuBar');
let splitEditor = this.createMenuItem(nls.localize({ key: 'miSplitEditor', comment: ['&& denotes a mnemonic'] }, "Split &&Editor"), 'workbench.action.splitEditor');
let toggleSidebar = this.createMenuItem(nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar"), 'workbench.action.toggleSidebarVisibility');
let moveSidebar = this.createMenuItem(nls.localize({ key: 'miMoveSidebar', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar"), 'workbench.action.toggleSidebarPosition');
let togglePanel = this.createMenuItem(nls.localize({ key: 'miTogglePanel', comment: ['&& denotes a mnemonic'] }, "Toggle &&Panel"), 'workbench.action.togglePanel');
const toggleWordWrap = this.createMenuItem(nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap"), 'editor.action.toggleWordWrap');
const toggleRenderWhitespace = this.createMenuItem(nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace"), 'editor.action.toggleRenderWhitespace');
let zoomIn = this.createMenuItem(nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom in"), 'workbench.action.zoomIn');
let zoomOut = this.createMenuItem(nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "Zoom o&&ut"), 'workbench.action.zoomOut');
arrays.coalesce([
explorer,
search,
git,
debug,
__separator__(),
commands,
markers,
__separator__(),
output,
debugConsole,
__separator__(),
fullscreen,
platform.isWindows ||  platform.isLinux ? toggleMenuBar : void 0,
__separator__(),
splitEditor,
toggleSidebar,
moveSidebar,
togglePanel,
__separator__(),
toggleWordWrap,
toggleRenderWhitespace,
__separator__(),
zoomIn,
zoomOut
]).forEach((item) => viewMenu.append(item));
}
private setGotoMenu(gotoMenu: Electron.Menu): void {
let back = this.createMenuItem(nls.localize({ key: 'miBack', comment: ['&& denotes a mnemonic'] }, "&&Back"), 'workbench.action.navigateBack');
let forward = this.createMenuItem(nls.localize({ key: 'miForward', comment: ['&& denotes a mnemonic'] }, "&&Forward"), 'workbench.action.navigateForward');
let navigateHistory = this.createMenuItem(nls.localize({ key: 'miNavigateHistory', comment: ['&& denotes a mnemonic'] }, "&&Navigate History"), 'workbench.action.openPreviousEditor');
let gotoFile = this.createMenuItem(nls.localize({ key: 'miGotoFile', comment: ['&& denotes a mnemonic'] }, "Go to &&File..."), 'workbench.action.quickOpen');
let gotoSymbol = this.createMenuItem(nls.localize({ key: 'miGotoSymbol', comment: ['&& denotes a mnemonic'] }, "Go to &&Symbol..."), 'workbench.action.gotoSymbol');
let gotoDefinition = this.createMenuItem(nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition"), 'editor.action.goToDeclaration');
let gotoLine = this.createMenuItem(nls.localize({ key: 'miGotoLine', comment: ['&& denotes a mnemonic'] }, "Go to &&Line..."), 'workbench.action.gotoLine');
[
back,
forward,
__separator__(),
navigateHistory,
__separator__(),
gotoFile,
gotoSymbol,
gotoDefinition,
gotoLine
].forEach((item) => gotoMenu.append(item));
}
private setMacWindowMenu(macWindowMenu: Electron.Menu): void {
let minimize = new MenuItem({ label: nls.localize('mMinimize', "Minimize"), role: 'minimize', accelerator: 'Command+M', enabled: this.windowsManager.getWindowCount() > 0 });
let close = new MenuItem({ label: nls.localize('mClose', "Close"), role: 'close', accelerator: 'Command+W', enabled: this.windowsManager.getWindowCount() > 0 });
let bringAllToFront = new MenuItem({ label: nls.localize('mBringToFront', "Bring All to Front"), role: 'front', enabled: this.windowsManager.getWindowCount() > 0 });
[
minimize,
close,
__separator__(),
bringAllToFront
].forEach((item) => macWindowMenu.append(item));
}
private toggleDevTools(): void {
let w = this.windowsManager.getFocusedWindow();
if (w && w.win) {
w.win.webContents.toggleDevTools();
}
}
private setHelpMenu(helpMenu: Electron.Menu): void {
let toggleDevToolsItem = new MenuItem({
label: mnemonicLabel(nls.localize({ key: 'miToggleDevTools', comment: ['&& denotes a mnemonic'] }, "&&Toggle Developer Tools")),
accelerator: this.getAccelerator('workbench.action.toggleDevTools'),
click: () => this.toggleDevTools(),
enabled: (this.windowsManager.getWindowCount() > 0)
});
arrays.coalesce([
this.envService.product.documentationUrl ? new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miDocumentation', comment: ['&& denotes a mnemonic'] }, "&&Documentation")), click: () => this.openUrl(this.envService.product.documentationUrl, 'openDocumentationUrl') }) : null,
this.envService.product.releaseNotesUrl ? new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miReleaseNotes', comment: ['&& denotes a mnemonic'] }, "&&Release Notes")), click: () => this.openUrl(this.envService.product.releaseNotesUrl, 'openReleaseNotesUrl') }) : null,
(this.envService.product.documentationUrl || this.envService.product.releaseNotesUrl) ? __separator__() : null,
this.envService.product.twitterUrl ? new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join us on Twitter")), click: () => this.openUrl(this.envService.product.twitterUrl, 'openTwitterUrl') }) : null,
this.envService.product.requestFeatureUrl ? new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Request Features")), click: () => this.openUrl(this.envService.product.requestFeatureUrl, 'openUserVoiceUrl') }) : null,
this.envService.product.reportIssueUrl ? new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miReportIssues', comment: ['&& denotes a mnemonic'] }, "Report &&Issues")), click: () => this.openUrl(this.envService.product.reportIssueUrl, 'openReportIssues') }) : null,
(this.envService.product.twitterUrl || this.envService.product.requestFeatureUrl || this.envService.product.reportIssueUrl) ? __separator__() : null,
this.envService.product.licenseUrl ? new MenuItem({
label: mnemonicLabel(nls.localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "&&View License")), click: () => {
if (platform.language) {
let queryArgChar = this.envService.product.licenseUrl.indexOf('?') > 0 ? '&' : '?';
this.openUrl(`${this.envService.product.licenseUrl}${queryArgChar}lang=${platform.language}`, 'openLicenseUrl');
} else {
this.openUrl(this.envService.product.licenseUrl, 'openLicenseUrl');
}
}
}) : null,
this.envService.product.privacyStatementUrl ? new MenuItem({
label: mnemonicLabel(nls.localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "&&Privacy Statement")), click: () => {
if (platform.language) {
let queryArgChar = this.envService.product.licenseUrl.indexOf('?') > 0 ? '&' : '?';
this.openUrl(`${this.envService.product.privacyStatementUrl}${queryArgChar}lang=${platform.language}`, 'openPrivacyStatement');
} else {
this.openUrl(this.envService.product.privacyStatementUrl, 'openPrivacyStatement');
}
}
}) : null,
(this.envService.product.licenseUrl || this.envService.product.privacyStatementUrl) ? __separator__() : null,
toggleDevToolsItem,
]).forEach((item) => helpMenu.append(item));
if (!platform.isMacintosh) {
const updateMenuItems = this.getUpdateMenuItems();
if (updateMenuItems.length) {
helpMenu.append(__separator__());
updateMenuItems.forEach(i => helpMenu.append(i));
}
helpMenu.append(__separator__());
helpMenu.append(new MenuItem({ label: mnemonicLabel(nls.localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About")), click: () => this.openAboutDialog() }));
}
}
private getUpdateMenuItems(): Electron.MenuItem[] {
switch (this.updateManager.state) {
case UpdateState.Uninitialized:
return [];
case UpdateState.UpdateDownloaded:
let update = this.updateManager.availableUpdate;
return [new MenuItem({
label: nls.localize('miRestartToUpdate', "Restart To Update..."), click: () => {
this.reportMenuActionTelemetry('RestartToUpdate');
update.quitAndUpdate();
}
})];
case UpdateState.CheckingForUpdate:
return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking For Updates..."), enabled: false })];
case UpdateState.UpdateAvailable:
if (platform.isLinux) {
const update = this.updateManager.availableUpdate;
return [new MenuItem({
label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => {
update.quitAndUpdate();
}
})];
}
let updateAvailableLabel = platform.isWindows
? nls.localize('miDownloadingUpdate', "Downloading Update...")
: nls.localize('miInstallingUpdate', "Installing Update...");
return [new MenuItem({ label: updateAvailableLabel, enabled: false })];
default:
let result = [new MenuItem({
label: nls.localize('miCheckForUpdates', "Check For Updates..."), click: () => setTimeout(() => {
this.reportMenuActionTelemetry('CheckForUpdate');
this.updateManager.checkForUpdates(true);
}, 0)
})];
if (this.updateManager.lastCheckDate) {
result.push(new MenuItem({ label: nls.localize('miLastCheckedAt', "Last checked at {0}", this.updateManager.lastCheckDate.toLocaleTimeString()), enabled: false }));
}
return result;
}
}
private createMenuItem(label: string, actionId: string, enabled?: boolean): Electron.MenuItem;
private createMenuItem(label: string, click: () => void, enabled?: boolean): Electron.MenuItem;
private createMenuItem(arg1: string, arg2: any, arg3?: boolean): Electron.MenuItem {
let label = mnemonicLabel(arg1);
let click: () => void = (typeof arg2 === 'function') ? arg2 : () => this.windowsManager.sendToFocused('vscode:runAction', arg2);
let enabled = typeof arg3 === 'boolean' ? arg3 : this.windowsManager.getWindowCount() > 0;
let actionId: string;
if (typeof arg2 === 'string') {
actionId = arg2;
}
let options: Electron.MenuItemOptions = {
label: label,
accelerator: this.getAccelerator(actionId),
click: click,
enabled: enabled
};
return new MenuItem(options);
}
private createDevToolsAwareMenuItem(label: string, actionId: string, devToolsFocusedFn: (contents: Electron.WebContents) => void): Electron.MenuItem {
return new MenuItem({
label: mnemonicLabel(label),
accelerator: this.getAccelerator(actionId),
enabled: this.windowsManager.getWindowCount() > 0,
click: () => {
let windowInFocus = this.windowsManager.getFocusedWindow();
if (!windowInFocus) {
return;
}
if (windowInFocus.win.isDevToolsFocused()) {
devToolsFocusedFn(windowInFocus.win.devToolsWebContents);
} else {
this.windowsManager.sendToFocused('vscode:runAction', actionId);
}
}
});
}
private getAccelerator(actionId: string): string {
if (actionId) {
let resolvedKeybinding = this.mapResolvedKeybindingToActionId[actionId];
if (resolvedKeybinding) {
return resolvedKeybinding; // keybinding is fully resolved
}
if (!this.keybindingsResolved) {
this.actionIdKeybindingRequests.push(actionId); // keybinding needs to be resolved
}
let lastKnownKeybinding = this.mapLastKnownKeybindingToActionId[actionId];
return lastKnownKeybinding; // return the last known keybining (chance of mismatch is very low unless it changed)
}
return void (0);
}
private openAboutDialog(): void {
let lastActiveWindow = this.windowsManager.getFocusedWindow() || this.windowsManager.getLastActiveWindow();
dialog.showMessageBox(lastActiveWindow && lastActiveWindow.win, {
title: this.envService.product.nameLong,
type: 'info',
message: this.envService.product.nameLong,
detail: nls.localize('aboutDetail',
"\nVersion {0}\nCommit {1}\nDate {2}\nShell {3}\nRenderer {4}\nNode {5}",
app.getVersion(),
this.envService.product.commit || 'Unknown',
this.envService.product.date || 'Unknown',
process.versions['electron'],
process.versions['chrome'],
process.versions['node']
),
buttons: [nls.localize('okButton', "OK")],
noLink: true
}, (result) => null);
this.reportMenuActionTelemetry('showAboutDialog');
}
private openUrl(url: string, id: string): void {
shell.openExternal(url);
this.reportMenuActionTelemetry(id);
}
private reportMenuActionTelemetry(id: string): void {
this.windowsManager.sendToFocused('vscode:telemetry', { eventName: 'workbenchActionExecuted', data: { id, from: 'menu' } });
}
}
function __separator__(): Electron.MenuItem {
return new MenuItem({ type: 'separator' });
}
function mnemonicLabel(label: string): string {
if (platform.isMacintosh) {
return label.replace(/&&/g, ''); // no mnemonic support on mac
}
return label.replace(/&&/g, '&');
}

View File

@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* 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 {app} from 'electron';
import { ServiceIdentifier, createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { UserSettings, ISettings } from 'vs/workbench/node/userSettings';
import { IEnvironmentService } from 'vs/code/electron-main/env';
import Event from 'vs/base/common/event';
export const ISettingsService = createDecorator<ISettingsService>('settingsService');
export interface ISettingsService {
serviceId: ServiceIdentifier<any>;
globalSettings: ISettings;
loadSync(): boolean;
getValue(key: string, fallback?: any): any;
onChange: Event<ISettings>;
}
export class SettingsManager extends UserSettings implements ISettingsService {
serviceId = ISettingsService;
constructor(@IEnvironmentService envService: IEnvironmentService) {
super(envService.appSettingsPath, envService.appKeybindingsPath);
app.on('will-quit', () => {
this.dispose();
});
}
loadSync(): boolean {
const settingsChanged = super.loadSync();
// Store into global so that any renderer can access the value with remote.getGlobal()
if (settingsChanged) {
global.globalSettingsValue = JSON.stringify(this.globalSettings);
}
return settingsChanged;
}
}

View File

@@ -0,0 +1,91 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import URI from 'vs/base/common/uri';
import { IDisposable } from 'vs/base/common/lifecycle';
import { assign } from 'vs/base/common/objects';
import { IEnvironment } from 'vs/platform/workspace/common/workspace';
import { IEnvironmentService } from 'vs/code/electron-main/env';
import { ISettingsService } from 'vs/code/electron-main/settings';
import { IUpdateService } from 'vs/code/electron-main/update-manager';
import {ServicesAccessor} from 'vs/platform/instantiation/common/instantiation';
const boostrapPath = URI.parse(require.toUrl('bootstrap')).fsPath;
function getEnvironment(envService: IEnvironmentService, updateManager: IUpdateService): IEnvironment {
let configuration: IEnvironment = assign({}, envService.cliArgs);
configuration.execPath = process.execPath;
configuration.appName = envService.product.nameLong;
configuration.appRoot = envService.appRoot;
configuration.version = envService.version;
configuration.commitHash = envService.product.commit;
configuration.appSettingsHome = envService.appSettingsHome;
configuration.appSettingsPath = envService.appSettingsPath;
configuration.appKeybindingsPath = envService.appKeybindingsPath;
configuration.userExtensionsHome = envService.userExtensionsHome;
configuration.isBuilt = envService.isBuilt;
configuration.updateFeedUrl = updateManager.feedUrl;
configuration.updateChannel = updateManager.channel;
configuration.extensionsGallery = envService.product.extensionsGallery;
return configuration;
}
function _spawnSharedProcess(envService: IEnvironmentService, updateManager: IUpdateService, settingsManager: ISettingsService): cp.ChildProcess {
// Make sure the nls configuration travels to the shared process.
const opts = {
env: assign(assign({}, process.env), {
AMD_ENTRYPOINT: 'vs/code/electron-main/sharedProcessMain'
})
};
const result = cp.fork(boostrapPath, ['--type=SharedProcess'], opts);
// handshake
result.once('message', () => {
result.send({
configuration: {
env: getEnvironment(envService, updateManager)
},
contextServiceOptions: {
globalSettings: settingsManager.globalSettings
}
});
});
return result;
}
let spawnCount = 0;
export function spawnSharedProcess(accessor: ServicesAccessor): IDisposable {
const envService = accessor.get(IEnvironmentService);
const updateManager = accessor.get(IUpdateService);
const settingsManager = accessor.get(ISettingsService);
let child: cp.ChildProcess;
const spawn = () => {
if (++spawnCount > 10) {
return;
}
child = _spawnSharedProcess(envService, updateManager, settingsManager);
child.on('exit', spawn);
};
spawn();
return {
dispose: () => {
if (child) {
child.removeListener('exit', spawn);
child.kill();
child = null;
}
}
};
}

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import platform = require('vs/base/common/platform');
import { serve, Server, connect } from 'vs/base/parts/ipc/node/ipc.net';
import { TPromise } from 'vs/base/common/winjs.base';
import { IConfiguration } from 'vs/platform/workspace/common/workspace';
import { WorkspaceContextService } from 'vs/workbench/services/workspace/common/contextService';
import { EventService } from 'vs/platform/event/common/eventService';
import { ExtensionsChannel } from 'vs/workbench/parts/extensions/common/extensionsIpc';
import { ExtensionsService } from 'vs/workbench/parts/extensions/node/extensionsService';
interface IInitData {
configuration: IConfiguration;
contextServiceOptions: { settings: any };
}
function quit(err?: Error) {
if (err) {
console.error(err);
}
process.exit(err ? 1 : 0);
}
/**
* Plan B is to kill oneself if one's parent dies. Much drama.
*/
function setupPlanB(parentPid: number): void {
setInterval(function () {
try {
process.kill(parentPid, 0); // throws an exception if the main process doesn't exist anymore.
} catch (e) {
process.exit();
}
}, 5000);
}
function main(server: Server, initData: IInitData): void {
const eventService = new EventService();
const contextService = new WorkspaceContextService(eventService, null, initData.configuration, initData.contextServiceOptions);
const extensionService = new ExtensionsService(contextService);
const channel = new ExtensionsChannel(extensionService);
server.registerChannel('extensions', channel);
// eventually clean up old extensions
setTimeout(() => extensionService.removeDeprecatedExtensions(), 5000);
}
function setupIPC(hook: string): TPromise<Server> {
function setup(retry: boolean): TPromise<Server> {
return serve(hook).then(null, err => {
if (!retry || platform.isWindows || err.code !== 'EADDRINUSE') {
return TPromise.wrapError(err);
}
// should retry, not windows and eaddrinuse
return connect(hook).then(
client => {
// we could connect to a running instance. this is not good, abort
client.dispose();
return TPromise.wrapError(new Error('There is an instance already running.'));
},
err => {
// it happens on Linux and OS X that the pipe is left behind
// let's delete it, since we can't connect to it
// and the retry the whole thing
try {
fs.unlinkSync(hook);
} catch (e) {
return TPromise.wrapError(new Error('Error deleting the shared ipc hook.'));
}
return setup(false);
}
);
});
}
return setup(true);
}
function handshake(): TPromise<IInitData> {
return new TPromise<IInitData>((c, e) => {
process.once('message', c);
process.once('error', e);
process.send('hello');
});
}
TPromise.join<any>([setupIPC(process.env['VSCODE_SHARED_IPC_HOOK']), handshake()])
.then(r => main(r[0], r[1]))
.then(() => setupPlanB(process.env['VSCODE_PID']))
.done(null, quit);

View File

@@ -0,0 +1,108 @@
/*---------------------------------------------------------------------------------------------
* 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 path = require('path');
import fs = require('fs');
import events = require('events');
import env = require('vs/code/electron-main/env');
import {ServiceIdentifier, createDecorator} from 'vs/platform/instantiation/common/instantiation';
const EventTypes = {
STORE: 'store'
};
export const IStorageService = createDecorator<IStorageService>('storageService');
export interface IStorageService {
serviceId: ServiceIdentifier<any>;
onStore<T>(clb: (key: string, oldValue: T, newValue: T) => void): () => void;
getItem<T>(key: string, defaultValue?: T): T;
setItem(key: string, data: any): void;
removeItem(key: string): void;
}
export class StorageService implements IStorageService {
serviceId = IStorageService;
private dbPath: string;
private database: any = null;
private eventEmitter = new events.EventEmitter();
constructor(@env.IEnvironmentService private envService: env.IEnvironmentService) {
this.dbPath = path.join(envService.appHome, 'storage.json');
}
onStore<T>(clb: (key: string, oldValue: T, newValue: T) => void): () => void {
this.eventEmitter.addListener(EventTypes.STORE, clb);
return () => this.eventEmitter.removeListener(EventTypes.STORE, clb);
}
getItem<T>(key: string, defaultValue?: T): T {
if (!this.database) {
this.database = this.load();
}
const res = this.database[key];
if (typeof res === 'undefined') {
return defaultValue;
}
return this.database[key];
}
setItem(key: string, data: any): void {
if (!this.database) {
this.database = this.load();
}
// Shortcut for primitives that did not change
if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') {
if (this.database[key] === data) {
return;
}
}
let oldValue = this.database[key];
this.database[key] = data;
this.save();
this.eventEmitter.emit(EventTypes.STORE, key, oldValue, data);
}
removeItem(key: string): void {
if (!this.database) {
this.database = this.load();
}
if (this.database[key]) {
let oldValue = this.database[key];
delete this.database[key];
this.save();
this.eventEmitter.emit(EventTypes.STORE, key, oldValue, null);
}
}
private load(): any {
try {
return JSON.parse(fs.readFileSync(this.dbPath).toString());
} catch (error) {
if (this.envService.cliArgs.verboseLogging) {
console.error(error);
}
return {};
}
}
private save(): void {
fs.writeFileSync(this.dbPath, JSON.stringify(this.database, null, 4));
}
}

View File

@@ -0,0 +1,252 @@
/*---------------------------------------------------------------------------------------------
* 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 fs = require('fs');
import path = require('path');
import events = require('events');
import electron = require('electron');
import platform = require('vs/base/common/platform');
import { IEnvironmentService, getPlatformIdentifier } from 'vs/code/electron-main/env';
import { ISettingsService } from 'vs/code/electron-main/settings';
import { Win32AutoUpdaterImpl } from 'vs/code/electron-main/auto-updater.win32';
import { LinuxAutoUpdaterImpl } from 'vs/code/electron-main/auto-updater.linux';
import { ILifecycleService } from 'vs/code/electron-main/lifecycle';
import { ServiceIdentifier, createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export enum State {
Uninitialized,
Idle,
CheckingForUpdate,
UpdateAvailable,
UpdateDownloaded
}
export enum ExplicitState {
Implicit,
Explicit
}
export interface IUpdate {
releaseNotes: string;
version: string;
date: Date;
quitAndUpdate: () => void;
}
interface IAutoUpdater extends NodeJS.EventEmitter {
setFeedURL(url: string): void;
checkForUpdates(): void;
}
export const IUpdateService = createDecorator<IUpdateService>('updateService');
export interface IUpdateService {
serviceId: ServiceIdentifier<any>;
feedUrl: string;
channel: string;
initialize(): void;
state: State;
availableUpdate: IUpdate;
lastCheckDate: Date;
checkForUpdates(explicit: boolean): void;
on(event: string, listener: Function): this;
}
export class UpdateManager extends events.EventEmitter implements IUpdateService {
serviceId = IUpdateService;
private _state: State;
private explicitState: ExplicitState;
private _availableUpdate: IUpdate;
private _lastCheckDate: Date;
private raw: IAutoUpdater;
private _feedUrl: string;
private _channel: string;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@ILifecycleService private lifecycleService: ILifecycleService,
@IEnvironmentService private envService: IEnvironmentService,
@ISettingsService private settingsManager: ISettingsService
) {
super();
this._state = State.Uninitialized;
this.explicitState = ExplicitState.Implicit;
this._availableUpdate = null;
this._lastCheckDate = null;
this._feedUrl = null;
this._channel = null;
if (platform.isWindows) {
this.raw = instantiationService.createInstance(Win32AutoUpdaterImpl);
} else if (platform.isLinux) {
this.raw = instantiationService.createInstance(LinuxAutoUpdaterImpl);
} else if (platform.isMacintosh) {
this.raw = electron.autoUpdater;
}
if (this.raw) {
this.initRaw();
}
}
private initRaw(): void {
this.raw.on('error', (event: any, message: string) => {
this.emit('error', event, message);
this.setState(State.Idle);
});
this.raw.on('checking-for-update', () => {
this.emit('checking-for-update');
this.setState(State.CheckingForUpdate);
});
this.raw.on('update-available', (event, url: string) => {
this.emit('update-available', url);
let data: IUpdate = null;
if (url) {
data = {
releaseNotes: '',
version: '',
date: new Date(),
quitAndUpdate: () => electron.shell.openExternal(url)
};
}
this.setState(State.UpdateAvailable, data);
});
this.raw.on('update-not-available', () => {
this.emit('update-not-available', this.explicitState === ExplicitState.Explicit);
this.setState(State.Idle);
});
this.raw.on('update-downloaded', (event: any, releaseNotes: string, version: string, date: Date, url: string, rawQuitAndUpdate: () => void) => {
let data: IUpdate = {
releaseNotes: releaseNotes,
version: version,
date: date,
quitAndUpdate: () => this.quitAndUpdate(rawQuitAndUpdate)
};
this.emit('update-downloaded', data);
this.setState(State.UpdateDownloaded, data);
});
}
private quitAndUpdate(rawQuitAndUpdate: () => void): void {
this.lifecycleService.quit().done(vetod => {
if (vetod) {
return;
}
// for some reason updating on Mac causes the local storage not to be flushed.
// we workaround this issue by forcing an explicit flush of the storage data.
// see also https://github.com/Microsoft/vscode/issues/172
if (platform.isMacintosh) {
electron.session.defaultSession.flushStorageData();
}
rawQuitAndUpdate();
});
}
public get feedUrl(): string {
return this._feedUrl;
}
public get channel(): string {
return this._channel;
}
public initialize(): void {
if (this.feedUrl) {
return; // already initialized
}
const channel = this.getUpdateChannel();
const feedUrl = this.getUpdateFeedUrl(channel);
if (!feedUrl) {
return; // updates not available
}
try {
this.raw.setFeedURL(feedUrl);
} catch (e) {
return; // application not signed
}
this._channel = channel;
this._feedUrl = feedUrl;
this.setState(State.Idle);
// Check for updates on startup after 30 seconds
let timer = setTimeout(() => this.checkForUpdates(), 30 * 1000);
// Clear timer when checking for update
this.on('error', (error: any, message: string) => console.error(error, message));
// Clear timer when checking for update
this.on('checking-for-update', () => clearTimeout(timer));
// If update not found, try again in 10 minutes
this.on('update-not-available', () => {
timer = setTimeout(() => this.checkForUpdates(), 10 * 60 * 1000);
});
}
public get state(): State {
return this._state;
}
public get availableUpdate(): IUpdate {
return this._availableUpdate;
}
public get lastCheckDate(): Date {
return this._lastCheckDate;
}
public checkForUpdates(explicit = false): void {
this.explicitState = explicit ? ExplicitState.Explicit : ExplicitState.Implicit;
this._lastCheckDate = new Date();
this.raw.checkForUpdates();
}
private setState(state: State, availableUpdate: IUpdate = null): void {
this._state = state;
this._availableUpdate = availableUpdate;
this.emit('change');
}
private getUpdateChannel(): string {
const channel = this.settingsManager.getValue('update.channel') || 'default';
return channel === 'none' ? null : this.envService.quality;
}
private getUpdateFeedUrl(channel: string): string {
if (!channel) {
return null;
}
if (platform.isWindows && !fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) {
return null;
}
if (!this.envService.updateUrl || !this.envService.product.commit) {
return null;
}
return `${ this.envService.updateUrl }/api/update/${ getPlatformIdentifier() }/${ channel }/${ this.envService.product.commit }`;
}
}

View File

@@ -0,0 +1,594 @@
/*---------------------------------------------------------------------------------------------
* 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 path = require('path');
import {shell, screen, BrowserWindow} from 'electron';
import {TPromise, TValueCallback} from 'vs/base/common/winjs.base';
import platform = require('vs/base/common/platform');
import objects = require('vs/base/common/objects');
import { ICommandLineArguments, IEnvironmentService, IProcessEnvironment } from 'vs/code/electron-main/env';
import storage = require('vs/code/electron-main/storage');
import { ILogService } from './log';
export interface IWindowState {
width?: number;
height?: number;
x?: number;
y?: number;
mode?: WindowMode;
}
export interface IWindowCreationOptions {
state: IWindowState;
extensionDevelopmentPath?: string;
}
export enum WindowMode {
Maximized,
Normal,
Minimized
}
export const defaultWindowState = function (mode = WindowMode.Normal): IWindowState {
return {
width: 1024,
height: 768,
mode: mode
};
};
export enum ReadyState {
/**
* This window has not loaded any HTML yet
*/
NONE,
/**
* This window is loading HTML
*/
LOADING,
/**
* This window is navigating to another HTML
*/
NAVIGATING,
/**
* This window is done loading HTML
*/
READY
}
export interface IPath {
// the workspace spath for a VSCode instance which can be null
workspacePath?: string;
// the file path to open within a VSCode instance
filePath?: string;
// the line number in the file path to open
lineNumber?: number;
// the column number in the file path to open
columnNumber?: number;
// indicator to create the file path in the VSCode instance
createFilePath?: boolean;
// indicator to install the extension (path to .vsix) in the VSCode instance
installExtensionPath?: boolean;
}
export interface IWindowConfiguration extends ICommandLineArguments {
execPath: string;
version: string;
appName: string;
applicationName: string;
darwinBundleIdentifier: string;
appSettingsHome: string;
appSettingsPath: string;
appKeybindingsPath: string;
userExtensionsHome: string;
mainIPCHandle: string;
sharedIPCHandle: string;
appRoot: string;
isBuilt: boolean;
commitHash: string;
updateFeedUrl: string;
updateChannel: string;
recentFiles: string[];
recentFolders: string[];
workspacePath?: string;
filesToOpen?: IPath[];
filesToCreate?: IPath[];
filesToDiff?: IPath[];
extensionsToInstall: string[];
crashReporter: Electron.CrashReporterStartOptions;
extensionsGallery: {
serviceUrl: string;
cacheUrl: string;
itemUrl: string;
};
extensionTips: { [id: string]: string; };
welcomePage: string;
releaseNotesUrl: string;
licenseUrl: string;
productDownloadUrl: string;
enableTelemetry: boolean;
userEnv: IProcessEnvironment;
aiConfig: {
key: string;
asimovKey: string;
};
sendASmile: {
reportIssueUrl: string,
requestFeatureUrl: string
};
}
export class VSCodeWindow {
public static menuBarHiddenKey = 'menuBarHidden';
public static themeStorageKey = 'theme';
private static MIN_WIDTH = 200;
private static MIN_HEIGHT = 120;
private showTimeoutHandle: any;
private _id: number;
private _win: Electron.BrowserWindow;
private _lastFocusTime: number;
private _readyState: ReadyState;
private _extensionDevelopmentPath: string;
private windowState: IWindowState;
private currentWindowMode: WindowMode;
private whenReadyCallbacks: TValueCallback<VSCodeWindow>[];
private currentConfig: IWindowConfiguration;
private pendingLoadConfig: IWindowConfiguration;
constructor(
config: IWindowCreationOptions,
@ILogService private logService: ILogService,
@IEnvironmentService private envService: IEnvironmentService,
@storage.IStorageService private storageService: storage.IStorageService
) {
this._lastFocusTime = -1;
this._readyState = ReadyState.NONE;
this._extensionDevelopmentPath = config.extensionDevelopmentPath;
this.whenReadyCallbacks = [];
// Load window state
this.restoreWindowState(config.state);
// For VS theme we can show directly because background is white
const usesLightTheme = /vs($| )/.test(this.storageService.getItem<string>(VSCodeWindow.themeStorageKey));
let showDirectly = true; // set to false to prevent background color flash (flash should be fixed for Electron >= 0.37.x)
if (showDirectly && !global.windowShow) {
global.windowShow = new Date().getTime();
}
let options: Electron.BrowserWindowOptions = {
width: this.windowState.width,
height: this.windowState.height,
x: this.windowState.x,
y: this.windowState.y,
backgroundColor: usesLightTheme ? '#FFFFFF' : platform.isMacintosh ? '#131313' : '#1E1E1E', // https://github.com/electron/electron/issues/5150
minWidth: VSCodeWindow.MIN_WIDTH,
minHeight: VSCodeWindow.MIN_HEIGHT,
show: showDirectly && this.currentWindowMode !== WindowMode.Maximized, // in case we are maximized, only show later after the call to maximize (see below)
title: this.envService.product.nameLong,
webPreferences: {
'backgroundThrottling': false // by default if Code is in the background, intervals and timeouts get throttled
}
};
if (platform.isLinux) {
options.icon = path.join(this.envService.appRoot, 'resources/linux/code.png'); // Windows and Mac are better off using the embedded icon(s)
}
// Create the browser window.
this._win = new BrowserWindow(options);
this._id = this._win.id;
if (showDirectly && this.currentWindowMode === WindowMode.Maximized) {
this.win.maximize();
if (!this.win.isVisible()) {
this.win.show(); // to reduce flicker from the default window size to maximize, we only show after maximize
}
}
if (showDirectly) {
this._lastFocusTime = new Date().getTime(); // since we show directly, we need to set the last focus time too
}
if (this.storageService.getItem<boolean>(VSCodeWindow.menuBarHiddenKey, false)) {
this.setMenuBarVisibility(false); // respect configured menu bar visibility
}
this.registerListeners();
}
public get isPluginDevelopmentHost(): boolean {
return !!this._extensionDevelopmentPath;
}
public get extensionDevelopmentPath(): string {
return this._extensionDevelopmentPath;
}
public get config(): IWindowConfiguration {
return this.currentConfig;
}
public get id(): number {
return this._id;
}
public get win(): Electron.BrowserWindow {
return this._win;
}
public focus(): void {
if (!this._win) {
return;
}
if (this._win.isMinimized()) {
this._win.restore();
}
this._win.focus();
}
public get lastFocusTime(): number {
return this._lastFocusTime;
}
public get openedWorkspacePath(): string {
return this.currentConfig.workspacePath;
}
public get openedFilePath(): string {
return this.currentConfig.filesToOpen && this.currentConfig.filesToOpen[0] && this.currentConfig.filesToOpen[0].filePath;
}
public setReady(): void {
this._readyState = ReadyState.READY;
// inform all waiting promises that we are ready now
while (this.whenReadyCallbacks.length) {
this.whenReadyCallbacks.pop()(this);
}
}
public ready(): TPromise<VSCodeWindow> {
return new TPromise<VSCodeWindow>((c) => {
if (this._readyState === ReadyState.READY) {
return c(this);
}
// otherwise keep and call later when we are ready
this.whenReadyCallbacks.push(c);
});
}
public get readyState(): ReadyState {
return this._readyState;
}
private registerListeners(): void {
// Remember that we loaded
this._win.webContents.on('did-finish-load', () => {
this._readyState = ReadyState.LOADING;
// Associate properties from the load request if provided
if (this.pendingLoadConfig) {
this.currentConfig = this.pendingLoadConfig;
this.pendingLoadConfig = null;
}
// To prevent flashing, we set the window visible after the page has finished to load but before VSCode is loaded
if (!this.win.isVisible()) {
if (!global.windowShow) {
global.windowShow = new Date().getTime();
}
if (this.currentWindowMode === WindowMode.Maximized) {
this.win.maximize();
}
if (!this.win.isVisible()) { // maximize also makes visible
this.win.show();
}
}
});
// App commands support
this._win.on('app-command', (e, cmd) => {
if (this.readyState !== ReadyState.READY) {
return; // window must be ready
}
// Support navigation via mouse buttons 4/5
if (cmd === 'browser-backward') {
this.send('vscode:runAction', 'workbench.action.navigateBack');
} else if (cmd === 'browser-forward') {
this.send('vscode:runAction', 'workbench.action.navigateForward');
}
});
// Handle code that wants to open links
this._win.webContents.on('new-window', (event: Event, url: string) => {
event.preventDefault();
shell.openExternal(url);
});
// Window Focus
this._win.on('focus', () => {
this._lastFocusTime = new Date().getTime();
});
// Window Failed to load
this._win.webContents.on('did-fail-load', (event: Event, errorCode: string, errorDescription: string) => {
console.warn('[electron event]: fail to load, ', errorDescription);
});
// Prevent any kind of navigation triggered by the user!
// But do not touch this in dev version because it will prevent "Reload" from dev tools
if (this.envService.isBuilt) {
this._win.webContents.on('will-navigate', (event: Event) => {
if (event) {
event.preventDefault();
}
});
}
}
public load(config: IWindowConfiguration): void {
// If this is the first time the window is loaded, we associate the paths
// directly with the window because we assume the loading will just work
if (this.readyState === ReadyState.NONE) {
this.currentConfig = config;
}
// Otherwise, the window is currently showing a folder and if there is an
// unload handler preventing the load, we cannot just associate the paths
// because the loading might be vetoed. Instead we associate it later when
// the window load event has fired.
else {
this.pendingLoadConfig = config;
this._readyState = ReadyState.NAVIGATING;
}
// Load URL
this._win.loadURL(this.getUrl(config));
// Make window visible if it did not open in N seconds because this indicates an error
if (!config.isBuilt) {
this.showTimeoutHandle = setTimeout(() => {
if (this._win && !this._win.isVisible() && !this._win.isMinimized()) {
this._win.show();
this._win.focus();
this._win.webContents.openDevTools();
}
}, 10000);
}
}
public reload(cli?: ICommandLineArguments): void {
// Inherit current properties but overwrite some
let configuration: IWindowConfiguration = objects.mixin({}, this.currentConfig);
delete configuration.filesToOpen;
delete configuration.filesToCreate;
delete configuration.filesToDiff;
delete configuration.extensionsToInstall;
// Some configuration things get inherited if the window is being reloaded and we are
// in plugin development mode. These options are all development related.
if (this.isPluginDevelopmentHost && cli) {
configuration.verboseLogging = cli.verboseLogging;
configuration.logExtensionHostCommunication = cli.logExtensionHostCommunication;
configuration.debugBrkFileWatcherPort = cli.debugBrkFileWatcherPort;
configuration.debugExtensionHostPort = cli.debugExtensionHostPort;
configuration.debugBrkExtensionHost = cli.debugBrkExtensionHost;
configuration.extensionsHomePath = cli.extensionsHomePath;
}
// Load config
this.load(configuration);
}
private getUrl(config: IWindowConfiguration): string {
let url = require.toUrl('vs/workbench/electron-browser/index.html');
// Config
url += '?config=' + encodeURIComponent(JSON.stringify(config));
return url;
}
public serializeWindowState(): IWindowState {
if (this.win.isFullScreen()) {
return defaultWindowState(); // ignore state when in fullscreen mode and return defaults
}
let state: IWindowState = Object.create(null);
let mode: WindowMode;
// get window mode
if (!platform.isMacintosh && this.win.isMaximized()) {
mode = WindowMode.Maximized;
} else if (this.win.isMinimized()) {
mode = WindowMode.Minimized;
} else {
mode = WindowMode.Normal;
}
// we don't want to save minimized state, only maximized or normal
if (mode === WindowMode.Maximized) {
state.mode = WindowMode.Maximized;
} else if (mode !== WindowMode.Minimized) {
state.mode = WindowMode.Normal;
}
// only consider non-minimized window states
if (mode === WindowMode.Normal || mode === WindowMode.Maximized) {
let pos = this.win.getPosition();
let size = this.win.getSize();
state.x = pos[0];
state.y = pos[1];
state.width = size[0];
state.height = size[1];
}
return state;
}
private restoreWindowState(state?: IWindowState): void {
if (state) {
try {
state = this.validateWindowState(state);
} catch (err) {
this.logService.log(`Unexpected error validating window state: ${err}\n${err.stack}`); // somehow display API can be picky about the state to validate
}
}
if (!state) {
state = defaultWindowState();
}
this.windowState = state;
this.currentWindowMode = this.windowState.mode;
}
private validateWindowState(state: IWindowState): IWindowState {
if (!state) {
return null;
}
if ([state.x, state.y, state.width, state.height].some(n => typeof n !== 'number')) {
return null;
}
if (state.width <= 0 || state.height <= 0) {
return null;
}
let displays = screen.getAllDisplays();
// Single Monitor: be strict about x/y positioning
if (displays.length === 1) {
let displayBounds = displays[0].bounds;
// Careful with maximized: in that mode x/y can well be negative!
if (state.mode !== WindowMode.Maximized && displayBounds.width > 0 && displayBounds.height > 0 /* Linux X11 sessions sometimes report wrong display bounds */) {
if (state.x < displayBounds.x) {
state.x = displayBounds.x; // prevent window from falling out of the screen to the left
}
if (state.y < displayBounds.y) {
state.y = displayBounds.y; // prevent window from falling out of the screen to the top
}
if (state.x > (displayBounds.x + displayBounds.width)) {
state.x = displayBounds.x; // prevent window from falling out of the screen to the right
}
if (state.y > (displayBounds.y + displayBounds.height)) {
state.y = displayBounds.y; // prevent window from falling out of the screen to the bottom
}
if (state.width > displayBounds.width) {
state.width = displayBounds.width; // prevent window from exceeding display bounds width
}
if (state.height > displayBounds.height) {
state.height = displayBounds.height; // prevent window from exceeding display bounds height
}
}
if (state.mode === WindowMode.Maximized) {
return defaultWindowState(WindowMode.Maximized); // when maximized, make sure we have good values when the user restores the window
}
return state;
}
// Multi Monitor: be less strict because metrics can be crazy
let bounds = { x: state.x, y: state.y, width: state.width, height: state.height };
let display = screen.getDisplayMatching(bounds);
if (display && display.bounds.x + display.bounds.width > bounds.x && display.bounds.y + display.bounds.height > bounds.y) {
if (state.mode === WindowMode.Maximized) {
let defaults = defaultWindowState(WindowMode.Maximized); // when maximized, make sure we have good values when the user restores the window
defaults.x = state.x; // carefull to keep x/y position so that the window ends up on the correct monitor
defaults.y = state.y;
return defaults;
}
return state;
}
return null;
}
public getBounds(): Electron.Bounds {
let pos = this.win.getPosition();
let dimension = this.win.getSize();
return { x: pos[0], y: pos[1], width: dimension[0], height: dimension[1] };
}
public toggleFullScreen(): void {
let willBeFullScreen = !this.win.isFullScreen();
this.win.setFullScreen(willBeFullScreen);
// Windows & Linux: Hide the menu bar but still allow to bring it up by pressing the Alt key
if (platform.isWindows || platform.isLinux) {
if (willBeFullScreen) {
this.setMenuBarVisibility(false);
} else {
this.setMenuBarVisibility(!this.storageService.getItem<boolean>(VSCodeWindow.menuBarHiddenKey, false)); // restore as configured
}
}
}
public setMenuBarVisibility(visible: boolean): void {
this.win.setMenuBarVisibility(visible);
this.win.setAutoHideMenuBar(!visible);
}
public sendWhenReady(channel: string, ...args: any[]): void {
this.ready().then(() => {
this.send(channel, ...args);
});
}
public send(channel: string, ...args: any[]): void {
this._win.webContents.send(channel, ...args);
}
public dispose(): void {
if (this.showTimeoutHandle) {
clearTimeout(this.showTimeoutHandle);
}
this._win = null; // Important to dereference the window object to allow for GC
}
}

File diff suppressed because it is too large Load Diff