Merge from master

This commit is contained in:
Raj Musuku
2019-02-21 17:56:04 -08:00
parent 5a146e34fa
commit 666ae11639
11482 changed files with 119352 additions and 255574 deletions

View File

@@ -3,23 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { app, ipcMain as ipc, systemPreferences } from 'electron';
import { app, ipcMain as ipc, systemPreferences, shell, Event, contentTracing, protocol } from 'electron';
import * as platform from 'vs/base/common/platform';
import { WindowsManager } from 'vs/code/electron-main/windows';
import { IWindowsService, OpenContext, ActiveWindowManager } from 'vs/platform/windows/common/windows';
import { WindowsChannel } from 'vs/platform/windows/common/windowsIpc';
import { WindowsChannel } from 'vs/platform/windows/node/windowsIpc';
import { WindowsService } from 'vs/platform/windows/electron-main/windowsService';
import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { ILifecycleService, LifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { getShellEnvironment } from 'vs/code/node/shellEnv';
import { IUpdateService } from 'vs/platform/update/common/update';
import { UpdateChannel } from 'vs/platform/update/common/updateIpc';
import { UpdateChannel } from 'vs/platform/update/node/updateIpc';
import { Server as ElectronIPCServer } from 'vs/base/parts/ipc/electron-main/ipc.electron-main';
import { Server, connect, Client } from 'vs/base/parts/ipc/node/ipc.net';
import { SharedProcess } from 'vs/code/electron-main/sharedProcess';
import { Mutex } from 'windows-mutex';
import { LaunchService, LaunchChannel, ILaunchService } from './launch';
import { LaunchService, LaunchChannel, ILaunchService } from 'vs/platform/launch/electron-main/launchService';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
@@ -28,49 +26,64 @@ import { IStateService } from 'vs/platform/state/common/state';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IURLService } from 'vs/platform/url/common/url';
import { URLHandlerChannelClient, URLServiceChannel } from 'vs/platform/url/common/urlIpc';
import { URLHandlerChannelClient, URLServiceChannel } from 'vs/platform/url/node/urlIpc';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { ITelemetryAppenderChannel, TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc';
import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc';
import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
import { getDelayedChannel } from 'vs/base/parts/ipc/common/ipc';
import { getDelayedChannel, StaticRouter } from 'vs/base/parts/ipc/node/ipc';
import product from 'vs/platform/node/product';
import pkg from 'vs/platform/node/package';
import { ProxyAuthHandler } from './auth';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ProxyAuthHandler } from 'vs/code/electron-main/auth';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { ConfigurationService } from 'vs/platform/configuration/node/configurationService';
import { TPromise } from 'vs/base/common/winjs.base';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { IHistoryMainService } from 'vs/platform/history/common/history';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { CodeWindow } from 'vs/code/electron-main/window';
import { KeyboardLayoutMonitor } from 'vs/code/electron-main/keyboard';
import URI from 'vs/base/common/uri';
import { WorkspacesChannel } from 'vs/platform/workspaces/common/workspacesIpc';
import { URI } from 'vs/base/common/uri';
import { WorkspacesChannel } from 'vs/platform/workspaces/node/workspacesIpc';
import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces';
import { getMachineId } from 'vs/base/node/id';
import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32';
import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux';
import { DarwinUpdateService } from 'vs/platform/update/electron-main/updateService.darwin';
import { IIssueService } from 'vs/platform/issue/common/issue';
import { IssueChannel } from 'vs/platform/issue/common/issueIpc';
import { IssueChannel } from 'vs/platform/issue/node/issueIpc';
import { IssueService } from 'vs/platform/issue/electron-main/issueService';
import { LogLevelSetterChannel } from 'vs/platform/log/common/logIpc';
import { LogLevelSetterChannel } from 'vs/platform/log/node/logIpc';
import * as errors from 'vs/base/common/errors';
import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener';
import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver';
import { connectRemoteAgentManagement, RemoteAgentConnectionContext } from 'vs/platform/remote/node/remoteAgentConnection';
import { IMenubarService } from 'vs/platform/menubar/common/menubar';
import { MenubarService } from 'vs/platform/menubar/electron-main/menubarService';
import { MenubarChannel } from 'vs/platform/menubar/common/menubarIpc';
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
import { CodeMenu } from 'vs/code/electron-main/menus';
import { MenubarChannel } from 'vs/platform/menubar/node/menubarIpc';
import { ILabelService, RegisterFormatterEvent } from 'vs/platform/label/common/label';
import { hasArgs } from 'vs/platform/environment/node/argv';
import { RunOnceScheduler } from 'vs/base/common/async';
import { registerContextMenuListener } from 'vs/base/parts/contextmenu/electron-main/contextmenu';
import { THEME_STORAGE_KEY, THEME_BG_STORAGE_KEY } from 'vs/code/electron-main/theme';
import { nativeSep, join } from 'vs/base/common/paths';
import { homedir } from 'os';
import { localize } from 'vs/nls';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/remote/node/remoteAgentFileSystemChannel';
import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap';
import { IStorageMainService, StorageMainService } from 'vs/platform/storage/node/storageMainService';
import { GlobalStorageDatabaseChannel } from 'vs/platform/storage/node/storageIpc';
import { generateUuid } from 'vs/base/common/uuid';
// {{SQL CARBON EDIT}}
import { CodeMenu } from 'sql/workbench/electron-browser/menus';
// {{SQL CARBON EDIT}} - End
export class CodeApplication {
export class CodeApplication extends Disposable {
private static readonly MACHINE_ID_KEY = 'telemetry.machineId';
private toDispose: IDisposable[];
private windowsMainService: IWindowsMainService;
private electronIpcServer: ElectronIPCServer;
@@ -88,9 +101,12 @@ export class CodeApplication {
@IConfigurationService private configurationService: ConfigurationService,
@IStateService private stateService: IStateService,
@IHistoryMainService private historyMainService: IHistoryMainService,
@IUriDisplayService private uriDisplayService: IUriDisplayService
@ILabelService private labelService: ILabelService
) {
this.toDispose = [mainIpcServer, configurationService];
super();
this._register(mainIpcServer);
this._register(configurationService);
this.registerListeners();
}
@@ -102,11 +118,11 @@ export class CodeApplication {
process.on('uncaughtException', err => this.onUnexpectedError(err));
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => errors.onUnexpectedError(reason));
app.on('will-quit', () => {
this.logService.trace('App#will-quit: disposing resources');
// Contextmenu via IPC support
registerContextMenuListener();
this.dispose();
});
// Dispose on shutdown
this.lifecycleService.onWillShutdown(() => this.dispose());
app.on('accessibility-support-changed', (event: Event, accessibilitySupportEnabled: boolean) => {
if (this.windowsMainService) {
@@ -123,40 +139,122 @@ export class CodeApplication {
}
});
const isValidWebviewSource = (source: string): boolean => {
if (!source) {
return false;
}
if (source === 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E') {
return true;
}
const srcUri: any = URI.parse(source.toLowerCase()).toString();
return srcUri.startsWith(URI.file(this.environmentService.appRoot.toLowerCase()).toString());
};
// Security related measures (https://electronjs.org/docs/tutorial/security)
// DO NOT CHANGE without consulting the documentation
app.on('web-contents-created', (event: any, contents) => {
contents.on('will-attach-webview', (event: Electron.Event, webPreferences, params) => {
// Ensure defaults
delete webPreferences.preload;
webPreferences.nodeIntegration = false;
// Verify URLs being loaded
if (isValidWebviewSource(params.src) && isValidWebviewSource(webPreferences.preloadURL)) {
if (this.isValidWebviewSource(params.src) && this.isValidWebviewSource(webPreferences.preloadURL)) {
return;
}
delete webPreferences.preloadUrl;
// Otherwise prevent loading
this.logService.error('webContents#web-contents-created: Prevented webview attach');
event.preventDefault();
});
contents.on('will-navigate', event => {
this.logService.error('webContents#will-navigate: Prevented webcontent navigation');
event.preventDefault();
});
contents.on('new-window', (event: Event, url: string) => {
event.preventDefault(); // prevent code that wants to open links
shell.openExternal(url);
});
});
const connectionPool: Map<string, ActiveConnection> = new Map<string, ActiveConnection>();
class ActiveConnection {
private _authority: string;
private _client: TPromise<Client<RemoteAgentConnectionContext>>;
private _disposeRunner: RunOnceScheduler;
constructor(authority: string, host: string, port: number) {
this._authority = authority;
this._client = connectRemoteAgentManagement(authority, host, port, `main`);
this._disposeRunner = new RunOnceScheduler(() => this._dispose(), 5000);
}
private _dispose(): void {
this._disposeRunner.dispose();
connectionPool.delete(this._authority);
this._client.then((connection) => {
connection.dispose();
});
}
public getClient(): TPromise<Client<RemoteAgentConnectionContext>> {
this._disposeRunner.schedule();
return this._client;
}
}
const resolvedAuthorities = new Map<string, ResolvedAuthority>();
ipc.on('vscode:remoteAuthorityResolved', (event: any, data: ResolvedAuthority) => {
resolvedAuthorities.set(data.authority, data);
});
const resolveAuthority = (authority: string): ResolvedAuthority | null => {
if (authority.indexOf('+') >= 0) {
if (resolvedAuthorities.has(authority)) {
return resolvedAuthorities.get(authority);
}
return null;
} else {
const [host, strPort] = authority.split(':');
const port = parseInt(strPort, 10);
return { authority, host, port, syncExtensions: false };
}
};
protocol.registerBufferProtocol(REMOTE_HOST_SCHEME, async (request, callback) => {
if (request.method !== 'GET') {
return callback(null);
}
const uri = URI.parse(request.url);
let activeConnection: ActiveConnection = null;
if (connectionPool.has(uri.authority)) {
activeConnection = connectionPool.get(uri.authority);
} else {
let resolvedAuthority = resolveAuthority(uri.authority);
if (!resolvedAuthority) {
callback(null);
return;
}
activeConnection = new ActiveConnection(uri.authority, resolvedAuthority.host, resolvedAuthority.port);
connectionPool.set(uri.authority, activeConnection);
}
try {
const rawClient = await activeConnection.getClient();
if (connectionPool.has(uri.authority)) { // not disposed in the meantime
const channel = rawClient.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME);
// TODO@alex don't use call directly, wrap it around a `RemoteExtensionsFileSystemProvider`
const fileContents = await channel.call<Uint8Array>('readFile', [uri]);
callback(Buffer.from(fileContents));
} else {
callback(null);
}
} catch (err) {
errors.onUnexpectedError(err);
callback(null);
}
});
let macOpenFileURIs: URI[] = [];
let runningTimeout: number = null;
let runningTimeout: any = null;
app.on('open-file', (event: Event, path: string) => {
this.logService.trace('App#open-file: ', path);
event.preventDefault();
@@ -189,15 +287,15 @@ export class CodeApplication {
this.windowsMainService.openNewWindow(OpenContext.DESKTOP); //macOS native tab "+" button
});
ipc.on('vscode:exit', (event: any, code: number) => {
ipc.on('vscode:exit', (event: Event, code: number) => {
this.logService.trace('IPC#vscode:exit', code);
this.dispose();
this.lifecycleService.kill(code);
});
ipc.on('vscode:fetchShellEnv', event => {
const webContents = event.sender.webContents;
ipc.on('vscode:fetchShellEnv', (event: Event) => {
const webContents = event.sender;
getShellEnvironment().then(shellEnv => {
if (!webContents.isDestroyed()) {
webContents.send('vscode:acceptShellEnv', shellEnv);
@@ -211,7 +309,7 @@ export class CodeApplication {
});
});
ipc.on('vscode:broadcast', (event: any, windowId: number, broadcast: { channel: string; payload: any; }) => {
ipc.on('vscode:broadcast', (event: Event, windowId: number, broadcast: { channel: string; payload: any; }) => {
if (this.windowsMainService && broadcast.channel && !isUndefinedOrNull(broadcast.payload)) {
this.logService.trace('IPC#vscode:broadcast', broadcast.channel, broadcast.payload);
@@ -223,8 +321,20 @@ export class CodeApplication {
}
});
ipc.on('vscode:uriDisplayRegisterFormater', (event: any, { scheme, formater }) => {
this.uriDisplayService.registerFormater(scheme, formater);
ipc.on('vscode:labelRegisterFormatter', (event: any, data: RegisterFormatterEvent) => {
this.labelService.registerFormatter(data.selector, data.formatter);
});
ipc.on('vscode:toggleDevTools', (event: Event) => {
event.sender.toggleDevTools();
});
ipc.on('vscode:openDevTools', (event: Event) => {
event.sender.openDevTools();
});
ipc.on('vscode:reloadWindow', (event: Event) => {
event.sender.reload();
});
// Keyboard layout changes
@@ -235,6 +345,20 @@ export class CodeApplication {
});
}
private isValidWebviewSource(source: string): boolean {
if (!source) {
return false;
}
if (source === 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E') {
return true;
}
const srcUri: any = URI.parse(source).fsPath.toLowerCase();
const rootUri = URI.file(this.environmentService.appRoot).fsPath.toLowerCase();
return srcUri.startsWith(rootUri + nativeSep);
}
private onUnexpectedError(err: Error): void {
if (err) {
@@ -262,8 +386,8 @@ export class CodeApplication {
if (event === 'vscode:changeColorTheme' && typeof payload === 'string') {
let data = JSON.parse(payload);
this.stateService.setItem(CodeWindow.themeStorageKey, data.id);
this.stateService.setItem(CodeWindow.themeBackgroundStorageKey, data.background);
this.stateService.setItem(THEME_STORAGE_KEY, data.baseTheme);
this.stateService.setItem(THEME_BG_STORAGE_KEY, data.background);
}
}
@@ -288,7 +412,7 @@ export class CodeApplication {
// See: https://github.com/Microsoft/vscode/issues/35361#issuecomment-399794085
try {
if (platform.isMacintosh && this.configurationService.getValue<boolean>('window.nativeTabs') === true && !systemPreferences.getUserDefault('NSUseImprovedLayoutPass', 'boolean')) {
systemPreferences.registerDefaults({ NSUseImprovedLayoutPass: true });
systemPreferences.setUserDefault('NSUseImprovedLayoutPass', 'boolean', true as any);
}
} catch (error) {
this.logService.error(error);
@@ -303,37 +427,73 @@ export class CodeApplication {
this.logService.trace(`Resolved machine identifier: ${machineId}`);
// Spawn shared process
this.sharedProcess = new SharedProcess(this.environmentService, this.lifecycleService, this.logService, machineId, this.userEnv);
this.sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);
this.sharedProcessClient = this.sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main'));
// Services
const appInstantiationService = this.initServices(machineId);
return this.initServices(machineId).then(appInstantiationService => {
let promise: TPromise<any> = TPromise.as(null);
// Create driver
if (this.environmentService.driverHandle) {
serveDriver(this.electronIpcServer, this.environmentService.driverHandle, this.environmentService, appInstantiationService).then(server => {
this.logService.info('Driver started at:', this.environmentService.driverHandle);
this.toDispose.push(server);
});
}
return promise.then(() => {
// Create driver
if (this.environmentService.driverHandle) {
serveDriver(this.electronIpcServer, this.environmentService.driverHandle, this.environmentService, appInstantiationService).then(server => {
this.logService.info('Driver started at:', this.environmentService.driverHandle);
this._register(server);
});
}
// Setup Auth Handler
const authHandler = appInstantiationService.createInstance(ProxyAuthHandler);
this.toDispose.push(authHandler);
this._register(authHandler);
// Open Windows
appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor));
const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor));
// Post Open Windows Tasks
appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor));
// Tracing: Stop tracing after windows are ready if enabled
if (this.environmentService.args.trace) {
this.stopTracingEventually(windows);
}
});
});
}
private stopTracingEventually(windows: ICodeWindow[]): void {
this.logService.info(`Tracing: waiting for windows to get ready...`);
let recordingStopped = false;
const stopRecording = (timeout) => {
if (recordingStopped) {
return;
}
recordingStopped = true; // only once
contentTracing.stopRecording(join(homedir(), `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`), path => {
if (!timeout) {
this.windowsMainService.showMessageBox({
type: 'info',
message: localize('trace.message', "Successfully created trace."),
detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path),
buttons: [localize('trace.ok', "Ok")]
}, this.windowsMainService.getLastActiveWindow());
} else {
this.logService.info(`Tracing: data recorded (after 30s timeout) to ${path}`);
}
});
};
// Wait up to 30s before creating the trace anyways
const timeoutHandle = setTimeout(() => stopRecording(true), 30000);
// Wait for all windows to get ready and stop tracing then
TPromise.join(windows.map(window => window.ready())).then(() => {
clearTimeout(timeoutHandle);
stopRecording(false);
});
}
private resolveMachineId(): TPromise<string> {
const machineId = this.stateService.getItem<string>(CodeApplication.MACHINE_ID_KEY);
if (machineId) {
@@ -349,40 +509,84 @@ export class CodeApplication {
});
}
private initServices(machineId: string): IInstantiationService {
private initServices(machineId: string): Thenable<IInstantiationService> {
const services = new ServiceCollection();
if (process.platform === 'win32') {
services.set(IUpdateService, new SyncDescriptor(Win32UpdateService));
} else if (process.platform === 'linux') {
services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService));
if (process.env.SNAP && process.env.SNAP_REVISION) {
services.set(IUpdateService, new SyncDescriptor(SnapUpdateService));
} else {
services.set(IUpdateService, new SyncDescriptor(LinuxUpdateService));
}
} else if (process.platform === 'darwin') {
services.set(IUpdateService, new SyncDescriptor(DarwinUpdateService));
}
services.set(IWindowsMainService, new SyncDescriptor(WindowsManager, machineId));
services.set(IWindowsService, new SyncDescriptor(WindowsService, this.sharedProcess));
services.set(IWindowsMainService, new SyncDescriptor(WindowsManager, [machineId]));
services.set(IWindowsService, new SyncDescriptor(WindowsService, [this.sharedProcess]));
services.set(ILaunchService, new SyncDescriptor(LaunchService));
services.set(IIssueService, new SyncDescriptor(IssueService, machineId, this.userEnv));
services.set(IIssueService, new SyncDescriptor(IssueService, [machineId, this.userEnv]));
services.set(IMenubarService, new SyncDescriptor(MenubarService));
services.set(IStorageMainService, new SyncDescriptor(StorageMainService));
// Telemtry
// Telemetry
if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) {
const channel = getDelayedChannel<ITelemetryAppenderChannel>(this.sharedProcessClient.then(c => c.getChannel('telemetryAppender')));
const channel = getDelayedChannel(this.sharedProcessClient.then(c => c.getChannel('telemetryAppender')));
const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(this.logService));
const commonProperties = resolveCommonProperties(product.commit, pkg.version, machineId, this.environmentService.installSourcePath);
const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath];
const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths };
services.set(ITelemetryService, new SyncDescriptor(TelemetryService, config));
services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [config]));
} else {
services.set(ITelemetryService, NullTelemetryService);
}
return this.instantiationService.createChild(services);
const appInstantiationService = this.instantiationService.createChild(services);
return appInstantiationService.invokeFunction(accessor => this.initStorageService(accessor)).then(() => appInstantiationService);
}
private openFirstWindow(accessor: ServicesAccessor): void {
private initStorageService(accessor: ServicesAccessor): Thenable<void> {
const storageMainService = accessor.get(IStorageMainService) as StorageMainService;
// Ensure to close storage on shutdown
this.lifecycleService.onWillShutdown(e => e.join(storageMainService.close()));
// Initialize storage service
return storageMainService.initialize().then(void 0, error => {
errors.onUnexpectedError(error);
this.logService.error(error);
}).then(() => {
// Apply global telemetry values as part of the initialization
// These are global across all windows and thereby should be
// written from the main process once.
const telemetryInstanceId = 'telemetry.instanceId';
const instanceId = storageMainService.get(telemetryInstanceId, null);
if (instanceId === null) {
storageMainService.store(telemetryInstanceId, generateUuid());
}
const telemetryFirstSessionDate = 'telemetry.firstSessionDate';
const firstSessionDate = storageMainService.get(telemetryFirstSessionDate, null);
if (firstSessionDate === null) {
storageMainService.store(telemetryFirstSessionDate, new Date().toUTCString());
}
const telemetryCurrentSessionDate = 'telemetry.currentSessionDate';
const telemetryLastSessionDate = 'telemetry.lastSessionDate';
const lastSessionDate = storageMainService.get(telemetryCurrentSessionDate, null); // previous session date was the "current" one at that time
const currentSessionDate = new Date().toUTCString(); // current session date is "now"
storageMainService.store(telemetryLastSessionDate, lastSessionDate);
storageMainService.store(telemetryCurrentSessionDate, currentSessionDate);
});
}
private openFirstWindow(accessor: ServicesAccessor): ICodeWindow[] {
const appInstantiationService = accessor.get(IInstantiationService);
// Register more Main IPC services
@@ -406,7 +610,7 @@ export class CodeApplication {
const windowsService = accessor.get(IWindowsService);
const windowsChannel = new WindowsChannel(windowsService);
this.electronIpcServer.registerChannel('windows', windowsChannel);
this.sharedProcessClient.done(client => client.registerChannel('windows', windowsChannel));
this.sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel));
const menubarService = accessor.get(IMenubarService);
const menubarChannel = new MenubarChannel(menubarService);
@@ -416,13 +620,17 @@ export class CodeApplication {
const urlChannel = new URLServiceChannel(urlService);
this.electronIpcServer.registerChannel('url', urlChannel);
const storageMainService = accessor.get(IStorageMainService);
const storageChannel = this._register(new GlobalStorageDatabaseChannel(storageMainService as StorageMainService));
this.electronIpcServer.registerChannel('storage', storageChannel);
// Log level management
const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService));
this.electronIpcServer.registerChannel('loglevel', logLevelChannel);
this.sharedProcessClient.done(client => client.registerChannel('loglevel', logLevelChannel));
this.sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel));
// Lifecycle
this.lifecycleService.ready();
(this.lifecycleService as LifecycleService).ready();
// Propagate to clients
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService); // TODO@Joao: unfold this
@@ -431,8 +639,8 @@ export class CodeApplication {
// Create a URL handler which forwards to the last active window
const activeWindowManager = new ActiveWindowManager(windowsService);
const route = () => activeWindowManager.getActiveClientId();
const urlHandlerChannel = this.electronIpcServer.getChannel('urlHandler', { routeCall: route, routeEvent: route });
const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
const urlHandlerChannel = this.electronIpcServer.getChannel('urlHandler', activeWindowRouter);
const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel);
// On Mac, Code can be running without any open windows, so we must create a window to handle urls,
@@ -460,33 +668,39 @@ export class CodeApplication {
// Watch Electron URLs and forward them to the UrlService
const urls = args['open-url'] ? args._urls : [];
const urlListener = new ElectronURLListener(urls, urlService, this.windowsMainService);
this.toDispose.push(urlListener);
this._register(urlListener);
this.windowsMainService.ready(this.userEnv);
// Open our first window
const macOpenFiles = (<any>global).macOpenFiles as string[];
const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
if (args['new-window'] && args._.length === 0 && (args['folder-uri'] || []).length === 0) {
this.windowsMainService.open({ context, cli: args, forceNewWindow: true, forceEmpty: true, initialStartup: true }); // new window if "-n" was used without paths
} else if (macOpenFiles && macOpenFiles.length && (!args._ || !args._.length || !args['folder-uri'] || !args['folder-uri'].length)) {
this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, urisToOpen: macOpenFiles.map(file => URI.file(file)), initialStartup: true }); // mac: open-file event received on startup
} else {
this.windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!args._.length && args['unity-launch']), diffMode: args.diff, initialStartup: true }); // default: read paths from cli
const hasCliArgs = hasArgs(args._);
const hasFolderURIs = hasArgs(args['folder-uri']);
const hasFileURIs = hasArgs(args['file-uri']);
if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
return this.windowsMainService.open({ context, cli: args, forceNewWindow: true, forceEmpty: true, initialStartup: true }); // new window if "-n" was used without paths
}
if (macOpenFiles && macOpenFiles.length && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
return this.windowsMainService.open({ context: OpenContext.DOCK, cli: args, urisToOpen: macOpenFiles.map(file => URI.file(file)), initialStartup: true }); // mac: open-file event received on startup
}
return this.windowsMainService.open({ context, cli: args, forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']), diffMode: args.diff, initialStartup: true }); // default: read paths from cli
}
private afterWindowOpen(accessor: ServicesAccessor): void {
const windowsMainService = accessor.get(IWindowsMainService);
let windowsMutex: Mutex = null;
let windowsMutex: Mutex | null = null;
if (platform.isWindows) {
// Setup Windows mutex
try {
const Mutex = (require.__$__nodeRequire('windows-mutex') as any).Mutex;
windowsMutex = new Mutex(product.win32MutexName);
this.toDispose.push({ dispose: () => windowsMutex.release() });
this._register(toDisposable(() => windowsMutex.release()));
} catch (e) {
if (!this.environmentService.isBuilt) {
windowsMainService.showMessageBox({
@@ -516,23 +730,21 @@ export class CodeApplication {
}
}
// TODO@sbatten: Remove when switching back to dynamic menu
// {{SQL CARBON EDIT}} - Use static menu for now
// Install Menu
const instantiationService = accessor.get(IInstantiationService);
const configurationService = accessor.get(IConfigurationService);
if (platform.isMacintosh || configurationService.getValue<string>('window.titleBarStyle') !== 'custom') {
instantiationService.createInstance(CodeMenu);
}
// {{SQL CARBON EDIT}} - End
// Jump List
this.historyMainService.updateWindowsJumpList();
this.historyMainService.onRecentlyOpenedChange(() => this.historyMainService.updateWindowsJumpList());
// Start shared process after a while
TPromise.timeout(3000).then(() => this.sharedProcess.spawn());
}
private dispose(): void {
this.toDispose = dispose(this.toDispose);
const sharedProcessSpawn = this._register(new RunOnceScheduler(() => getShellEnvironment().then(userEnv => this.sharedProcess.spawn(userEnv)), 3000));
sharedProcessSpawn.schedule();
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { localize } from 'vs/nls';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
@@ -14,8 +12,8 @@ import { BrowserWindow, app } from 'electron';
type LoginEvent = {
event: Electron.Event;
webContents: Electron.WebContents;
req: Electron.LoginRequest;
authInfo: Electron.LoginAuthInfo;
req: Electron.Request;
authInfo: Electron.AuthInfo;
cb: (username: string, password: string) => void;
};
@@ -56,10 +54,7 @@ export class ProxyAuthHandler {
width: 450,
height: 220,
show: true,
title: 'VS Code',
webPreferences: {
disableBlinkFeatures: 'Auxclick'
}
title: 'VS Code'
};
const focusedWindow = this.windowsMainService.getFocusedWindow();

View File

@@ -1,8 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/platform/update/node/update.config.contribution';

View File

@@ -1,304 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { WorkspaceStats, collectWorkspaceStats, collectLaunchConfigs, WorkspaceStatItem } from 'vs/base/node/stats';
import { IMainProcessInfo } from 'vs/code/electron-main/launch';
import { ProcessItem, listProcesses } from 'vs/base/node/ps';
import product from 'vs/platform/node/product';
import pkg from 'vs/platform/node/package';
import * as os from 'os';
import { virtualMachineHint } from 'vs/base/node/id';
import { repeat, pad } from 'vs/base/common/strings';
import { isWindows } from 'vs/base/common/platform';
import { app } from 'electron';
import { basename } from 'path';
import URI from 'vs/base/common/uri';
export interface VersionInfo {
vscodeVersion: string;
os: string;
}
export interface SystemInfo {
CPUs?: string;
'Memory (System)': string;
'Load (avg)'?: string;
VM: string;
'Screen Reader': string;
'Process Argv': string;
'GPU Status': Electron.GPUFeatureStatus;
}
export interface ProcessInfo {
cpu: number;
memory: number;
pid: number;
name: string;
}
export interface PerformanceInfo {
processInfo?: string;
workspaceInfo?: string;
}
export function getPerformanceInfo(info: IMainProcessInfo): Promise<PerformanceInfo> {
return listProcesses(info.mainPID).then(rootProcess => {
const workspaceInfoMessages = [];
// Workspace Stats
const workspaceStatPromises = [];
if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) {
info.windows.forEach(window => {
if (window.folderURIs.length === 0) {
return;
}
workspaceInfoMessages.push(`| Window (${window.title})`);
window.folderURIs.forEach(uriComponents => {
const folderUri = URI.revive(uriComponents);
if (folderUri.scheme === 'file') {
const folder = folderUri.fsPath;
workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => {
let countMessage = `${stats.fileCount} files`;
if (stats.maxFilesReached) {
countMessage = `more than ${countMessage}`;
}
workspaceInfoMessages.push(`| Folder (${basename(folder)}): ${countMessage}`);
workspaceInfoMessages.push(formatWorkspaceStats(stats));
const launchConfigs = await collectLaunchConfigs(folder);
if (launchConfigs.length > 0) {
workspaceInfoMessages.push(formatLaunchConfigs(launchConfigs));
}
}));
} else {
workspaceInfoMessages.push(`| Folder (${folderUri.toString()}): RPerformance stats not available.`);
}
});
});
}
return Promise.all(workspaceStatPromises).then(() => {
return {
processInfo: formatProcessList(info, rootProcess),
workspaceInfo: workspaceInfoMessages.join('\n')
};
}).catch(error => {
return {
processInfo: formatProcessList(info, rootProcess),
workspaceInfo: `Unable to calculate workspace stats: ${error}`
};
});
});
}
export function getSystemInfo(info: IMainProcessInfo): SystemInfo {
const MB = 1024 * 1024;
const GB = 1024 * MB;
const systemInfo: SystemInfo = {
'Memory (System)': `${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`,
VM: `${Math.round((virtualMachineHint.value() * 100))}%`,
'Screen Reader': `${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`,
'Process Argv': `${info.mainArguments.join(' ')}`,
'GPU Status': app.getGPUFeatureStatus()
};
const cpus = os.cpus();
if (cpus && cpus.length > 0) {
systemInfo.CPUs = `${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`;
}
if (!isWindows) {
systemInfo['Load (avg)'] = `${os.loadavg().map(l => Math.round(l)).join(', ')}`;
}
return systemInfo;
}
export function printDiagnostics(info: IMainProcessInfo): Promise<any> {
return listProcesses(info.mainPID).then(rootProcess => {
// Environment Info
console.log('');
console.log(formatEnvironment(info));
// Process List
console.log('');
console.log(formatProcessList(info, rootProcess));
// Workspace Stats
const workspaceStatPromises = [];
if (info.windows.some(window => window.folderURIs && window.folderURIs.length > 0)) {
console.log('');
console.log('Workspace Stats: ');
info.windows.forEach(window => {
if (window.folderURIs.length === 0) {
return;
}
console.log(`| Window (${window.title})`);
window.folderURIs.forEach(uriComponents => {
const folderUri = URI.revive(uriComponents);
if (folderUri.scheme === 'file') {
const folder = folderUri.fsPath;
workspaceStatPromises.push(collectWorkspaceStats(folder, ['node_modules', '.git']).then(async stats => {
let countMessage = `${stats.fileCount} files`;
if (stats.maxFilesReached) {
countMessage = `more than ${countMessage}`;
}
console.log(`| Folder (${basename(folder)}): ${countMessage}`);
console.log(formatWorkspaceStats(stats));
await collectLaunchConfigs(folder).then(launchConfigs => {
if (launchConfigs.length > 0) {
console.log(formatLaunchConfigs(launchConfigs));
}
});
}).catch(error => {
console.log(`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`);
}));
} else {
console.log(`| Folder (${folderUri.toString()}): Workspace stats not available.`);
}
});
});
}
return Promise.all(workspaceStatPromises).then(() => {
console.log('');
console.log('');
});
});
}
function formatWorkspaceStats(workspaceStats: WorkspaceStats): string {
const output: string[] = [];
const lineLength = 60;
let col = 0;
const appendAndWrap = (name: string, count: number) => {
const item = ` ${name}(${count})`;
if (col + item.length > lineLength) {
output.push(line);
line = '| ';
col = line.length;
}
else {
col += item.length;
}
line += item;
};
// File Types
let line = '| File types:';
const maxShown = 10;
let max = workspaceStats.fileTypes.length > maxShown ? maxShown : workspaceStats.fileTypes.length;
for (let i = 0; i < max; i++) {
const item = workspaceStats.fileTypes[i];
appendAndWrap(item.name, item.count);
}
output.push(line);
// Conf Files
if (workspaceStats.configFiles.length >= 0) {
line = '| Conf files:';
col = 0;
workspaceStats.configFiles.forEach((item) => {
appendAndWrap(item.name, item.count);
});
output.push(line);
}
return output.join('\n');
}
function formatLaunchConfigs(configs: WorkspaceStatItem[]): string {
const output: string[] = [];
let line = '| Launch Configs:';
configs.forEach(each => {
const item = each.count > 1 ? ` ${each.name}(${each.count})` : ` ${each.name}`;
line += item;
});
output.push(line);
return output.join('\n');
}
function expandGPUFeatures(): string {
const gpuFeatures = app.getGPUFeatureStatus();
const longestFeatureName = Math.max(...Object.keys(gpuFeatures).map(feature => feature.length));
// Make columns aligned by adding spaces after feature name
return Object.keys(gpuFeatures).map(feature => `${feature}: ${repeat(' ', longestFeatureName - feature.length)} ${gpuFeatures[feature]}`).join('\n ');
}
export function formatEnvironment(info: IMainProcessInfo): string {
const MB = 1024 * 1024;
const GB = 1024 * MB;
const output: string[] = [];
output.push(`Version: ${pkg.name} ${pkg.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`);
output.push(`OS Version: ${os.type()} ${os.arch()} ${os.release()}`);
const cpus = os.cpus();
if (cpus && cpus.length > 0) {
output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`);
}
output.push(`Memory (System): ${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`);
if (!isWindows) {
output.push(`Load (avg): ${os.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS
}
output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`);
output.push(`Screen Reader: ${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`);
output.push(`Process Argv: ${info.mainArguments.join(' ')}`);
output.push(`GPU Status: ${expandGPUFeatures()}`);
return output.join('\n');
}
function formatProcessList(info: IMainProcessInfo, rootProcess: ProcessItem): string {
const mapPidToWindowTitle = new Map<number, string>();
info.windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title));
const output: string[] = [];
output.push('CPU %\tMem MB\t PID\tProcess');
if (rootProcess) {
formatProcessItem(mapPidToWindowTitle, output, rootProcess, 0);
}
return output.join('\n');
}
function formatProcessItem(mapPidToWindowTitle: Map<number, string>, output: string[], item: ProcessItem, indent: number): void {
const isRoot = (indent === 0);
const MB = 1024 * 1024;
// Format name with indent
let name: string;
if (isRoot) {
name = `${product.applicationName} main`;
} else {
name = `${repeat(' ', indent)} ${item.name}`;
if (item.name === 'window') {
name = `${name} (${mapPidToWindowTitle.get(item.pid)})`;
}
}
const memory = process.platform === 'win32' ? item.mem : (os.totalmem() * (item.mem / 100));
output.push(`${pad(Number(item.load.toFixed(0)), 5, ' ')}\t${pad(Number((memory / MB).toFixed(0)), 6, ' ')}\t${pad(Number((item.pid).toFixed(0)), 6, ' ')}\t${name}`);
// Recurse into children if any
if (Array.isArray(item.children)) {
item.children.forEach(child => formatProcessItem(mapPidToWindowTitle, output, child, indent + 1));
}
}

View File

@@ -3,18 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nativeKeymap from 'native-keymap';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IStateService } from 'vs/platform/state/common/state';
import { Event, Emitter, once } from 'vs/base/common/event';
import { ConfigWatcher } from 'vs/base/node/config';
import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ipcMain as ipc } from 'electron';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { ILogService } from 'vs/platform/log/common/log';
import { Emitter } from 'vs/base/common/event';
export class KeyboardLayoutMonitor {
@@ -38,109 +29,4 @@ export class KeyboardLayoutMonitor {
}
return this._emitter.event(callback);
}
}
export interface IKeybinding {
id: string;
label: string;
isNative: boolean;
}
export class KeybindingsResolver {
private static readonly lastKnownKeybindingsMapStorageKey = 'lastKnownKeybindings';
private commandIds: Set<string>;
private keybindings: { [commandId: string]: IKeybinding };
private keybindingsWatcher: ConfigWatcher<IUserFriendlyKeybinding[]>;
private _onKeybindingsChanged = new Emitter<void>();
onKeybindingsChanged: Event<void> = this._onKeybindingsChanged.event;
constructor(
@IStateService private stateService: IStateService,
@IEnvironmentService environmentService: IEnvironmentService,
@IWindowsMainService private windowsMainService: IWindowsMainService,
@ILogService private logService: ILogService
) {
this.commandIds = new Set<string>();
this.keybindings = this.stateService.getItem<{ [id: string]: string; }>(KeybindingsResolver.lastKnownKeybindingsMapStorageKey) || Object.create(null);
this.keybindingsWatcher = new ConfigWatcher<IUserFriendlyKeybinding[]>(environmentService.appKeybindingsPath, { changeBufferDelay: 100, onError: error => this.logService.error(error) });
this.registerListeners();
}
private registerListeners(): void {
// Listen to resolved keybindings from window
ipc.on('vscode:keybindingsResolved', (event, rawKeybindings: string) => {
let keybindings: IKeybinding[] = [];
try {
keybindings = JSON.parse(rawKeybindings);
} catch (error) {
// Should not happen
}
// Fill hash map of resolved keybindings and check for changes
let keybindingsChanged = false;
let keybindingsCount = 0;
const resolvedKeybindings: { [commandId: string]: IKeybinding } = Object.create(null);
keybindings.forEach(keybinding => {
keybindingsCount++;
resolvedKeybindings[keybinding.id] = keybinding;
if (!this.keybindings[keybinding.id] || keybinding.label !== this.keybindings[keybinding.id].label) {
keybindingsChanged = true;
}
});
// A keybinding might have been unassigned, so we have to account for that too
if (Object.keys(this.keybindings).length !== keybindingsCount) {
keybindingsChanged = true;
}
if (keybindingsChanged) {
this.keybindings = resolvedKeybindings;
this.stateService.setItem(KeybindingsResolver.lastKnownKeybindingsMapStorageKey, this.keybindings); // keep to restore instantly after restart
this._onKeybindingsChanged.fire();
}
});
// Resolve keybindings when any first window is loaded
const onceOnWindowReady = once(this.windowsMainService.onWindowReady);
onceOnWindowReady(win => this.resolveKeybindings(win));
// Resolve keybindings again when keybindings.json changes
this.keybindingsWatcher.onDidUpdateConfiguration(() => this.resolveKeybindings());
// Resolve keybindings when window reloads because an installed extension could have an impact
this.windowsMainService.onWindowReload(() => this.resolveKeybindings());
}
private resolveKeybindings(win = this.windowsMainService.getLastActiveWindow()): void {
if (this.commandIds.size && win) {
const commandIds: string[] = [];
this.commandIds.forEach(id => commandIds.push(id));
win.sendWhenReady('vscode:resolveKeybindings', JSON.stringify(commandIds));
}
}
public getKeybinding(commandId: string): IKeybinding {
if (!commandId) {
return void 0;
}
if (!this.commandIds.has(commandId)) {
this.commandIds.add(commandId);
}
return this.keybindings[commandId];
}
public dispose(): void {
this._onKeybindingsChanged.dispose();
this.keybindingsWatcher.dispose();
}
}

View File

@@ -1,307 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { ILogService } from 'vs/platform/log/common/log';
import { IURLService } from 'vs/platform/url/common/url';
import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform';
import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { OpenContext, IWindowSettings } from 'vs/platform/windows/common/windows';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { whenDeleted } from 'vs/base/node/pfs';
import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import URI, { UriComponents } from 'vs/base/common/uri';
import { BrowserWindow } from 'electron';
import { Event } from 'vs/base/common/event';
export const ID = 'launchService';
export const ILaunchService = createDecorator<ILaunchService>(ID);
export interface IStartArguments {
args: ParsedArgs;
userEnv: IProcessEnvironment;
}
export interface IWindowInfo {
pid: number;
title: string;
folderURIs: UriComponents[];
}
export interface IMainProcessInfo {
mainPID: number;
mainArguments: string[];
windows: IWindowInfo[];
}
function parseOpenUrl(args: ParsedArgs): URI[] {
if (args['open-url'] && args._urls && args._urls.length > 0) {
// --open-url must contain -- followed by the url(s)
// process.argv is used over args._ as args._ are resolved to file paths at this point
return args._urls
.map(url => {
try {
return URI.parse(url);
} catch (err) {
return null;
}
})
.filter(uri => !!uri);
}
return [];
}
export interface ILaunchService {
_serviceBrand: any;
start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise<void>;
getMainProcessId(): TPromise<number>;
getMainProcessInfo(): TPromise<IMainProcessInfo>;
getLogsPath(): TPromise<string>;
}
export interface ILaunchChannel extends IChannel {
call(command: 'start', arg: IStartArguments): TPromise<void>;
call(command: 'get-main-process-id', arg: null): TPromise<any>;
call(command: 'get-main-process-info', arg: null): TPromise<any>;
call(command: 'get-logs-path', arg: null): TPromise<string>;
call(command: string, arg: any): TPromise<any>;
}
export class LaunchChannel implements ILaunchChannel {
constructor(private service: ILaunchService) { }
listen<T>(event: string): Event<T> {
throw new Error('No event found');
}
call(command: string, arg: any): TPromise<any> {
switch (command) {
case 'start':
const { args, userEnv } = arg as IStartArguments;
return this.service.start(args, userEnv);
case 'get-main-process-id':
return this.service.getMainProcessId();
case 'get-main-process-info':
return this.service.getMainProcessInfo();
case 'get-logs-path':
return this.service.getLogsPath();
}
return undefined;
}
}
export class LaunchChannelClient implements ILaunchService {
_serviceBrand: any;
constructor(private channel: ILaunchChannel) { }
start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise<void> {
return this.channel.call('start', { args, userEnv });
}
getMainProcessId(): TPromise<number> {
return this.channel.call('get-main-process-id', null);
}
getMainProcessInfo(): TPromise<IMainProcessInfo> {
return this.channel.call('get-main-process-info', null);
}
getLogsPath(): TPromise<string> {
return this.channel.call('get-logs-path', null);
}
}
export class LaunchService implements ILaunchService {
_serviceBrand: any;
constructor(
@ILogService private logService: ILogService,
@IWindowsMainService private windowsMainService: IWindowsMainService,
@IURLService private urlService: IURLService,
@IWorkspacesMainService private workspacesMainService: IWorkspacesMainService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IConfigurationService private readonly configurationService: IConfigurationService
) { }
start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise<void> {
this.logService.trace('Received data from other instance: ', args, userEnv);
const urlsToOpen = parseOpenUrl(args);
// Check early for open-url which is handled in URL service
if (urlsToOpen.length) {
let whenWindowReady = TPromise.as<any>(null);
// Create a window if there is none
if (this.windowsMainService.getWindowCount() === 0) {
const window = this.windowsMainService.openNewWindow(OpenContext.DESKTOP)[0];
whenWindowReady = window.ready();
}
// Make sure a window is open, ready to receive the url event
whenWindowReady.then(() => {
for (const url of urlsToOpen) {
this.urlService.open(url);
}
});
return TPromise.as(null);
}
// Otherwise handle in windows service
return this.startOpenWindow(args, userEnv);
}
private startOpenWindow(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise<void> {
const context = !!userEnv['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
let usedWindows: ICodeWindow[];
// Special case extension development
if (!!args.extensionDevelopmentPath) {
this.windowsMainService.openExtensionDevelopmentHostWindow({ context, cli: args, userEnv });
}
// Start without file/folder arguments
else if (args._.length === 0 && (args['folder-uri'] || []).length === 0) {
let openNewWindow = false;
// Force new window
if (args['new-window'] || args['unity-launch']) {
openNewWindow = true;
}
// Force reuse window
else if (args['reuse-window']) {
openNewWindow = false;
}
// Otherwise check for settings
else {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
const openWithoutArgumentsInNewWindowConfig = (windowConfig && windowConfig.openWithoutArgumentsInNewWindow) || 'default' /* default */;
switch (openWithoutArgumentsInNewWindowConfig) {
case 'on':
openNewWindow = true;
break;
case 'off':
openNewWindow = false;
break;
default:
openNewWindow = !isMacintosh; // prefer to restore running instance on macOS
}
}
if (openNewWindow) {
usedWindows = this.windowsMainService.open({ context, cli: args, userEnv, forceNewWindow: true, forceEmpty: true });
} else {
usedWindows = [this.windowsMainService.focusLastActive(args, context)];
}
}
// Start with file/folder arguments
else {
usedWindows = this.windowsMainService.open({
context,
cli: args,
userEnv,
forceNewWindow: args['new-window'],
preferNewWindow: !args['reuse-window'] && !args.wait,
forceReuseWindow: args['reuse-window'],
diffMode: args.diff,
addMode: args.add
});
}
// {{SQL CARBON EDIT}}
// give the first used window a chance to process the other command line arguments
if (args['reuse-window'] && usedWindows.length > 0 && usedWindows[0])
{
let window = usedWindows[0];
usedWindows[0].ready().then(() => window.send('ads:processCommandLine', args));
}
// {{SQL CARBON EDIT}}
// 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.
// In addition, we poll for the wait marker file to be deleted to return.
if (args.wait && usedWindows.length === 1 && usedWindows[0]) {
return TPromise.any([
this.windowsMainService.waitForWindowCloseOrLoad(usedWindows[0].id),
whenDeleted(args.waitMarkerFilePath)
]).then(() => void 0, () => void 0);
}
return TPromise.as(null);
}
getMainProcessId(): TPromise<number> {
this.logService.trace('Received request for process ID from other instance.');
return TPromise.as(process.pid);
}
getMainProcessInfo(): TPromise<IMainProcessInfo> {
this.logService.trace('Received request for main process info from other instance.');
const windows: IWindowInfo[] = [];
BrowserWindow.getAllWindows().forEach(window => {
const codeWindow = this.windowsMainService.getWindowById(window.id);
if (codeWindow) {
windows.push(this.codeWindowToInfo(codeWindow));
} else {
windows.push(this.browserWindowToInfo(window));
}
});
return TPromise.wrap({
mainPID: process.pid,
mainArguments: process.argv,
windows
} as IMainProcessInfo);
}
getLogsPath(): TPromise<string> {
this.logService.trace('Received request for logs path from other instance.');
return TPromise.as(this.environmentService.logsPath);
}
private codeWindowToInfo(window: ICodeWindow): IWindowInfo {
const folderURIs: URI[] = [];
if (window.openedFolderUri) {
folderURIs.push(window.openedFolderUri);
} else if (window.openedWorkspace) {
const rootFolders = this.workspacesMainService.resolveWorkspaceSync(window.openedWorkspace.configPath).folders;
rootFolders.forEach(root => {
folderURIs.push(root.uri);
});
}
return this.browserWindowToInfo(window.win, folderURIs);
}
private browserWindowToInfo(win: BrowserWindow, folderURIs: URI[] = []): IWindowInfo {
return {
pid: win.webContents.getOSProcessId(),
title: win.getTitle(),
folderURIs
} as IWindowInfo;
}
}

View File

@@ -3,20 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as os from 'os';
import * as cp from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { localize } from 'vs/nls';
import { ILaunchChannel } from 'vs/code/electron-main/launch';
import { TPromise } from 'vs/base/common/winjs.base';
import product from 'vs/platform/node/product';
import { IRequestService } from 'vs/platform/request/node/request';
import { IRequestContext } from 'vs/base/node/request';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ILaunchService } from 'vs/platform/launch/electron-main/launchService';
interface PostResult {
readonly blob_id: string;
@@ -34,7 +32,7 @@ class Endpoint {
}
export async function uploadLogs(
channel: ILaunchChannel,
launchService: ILaunchService,
requestService: IRequestService,
environmentService: IEnvironmentService
): Promise<any> {
@@ -44,7 +42,7 @@ export async function uploadLogs(
return;
}
const logsPath = await channel.call('get-logs-path', null);
const logsPath = await launchService.getLogsPath();
if (await promptUserToConfirmLogUpload(logsPath, environmentService)) {
console.log(localize('beginUploading', 'Uploading...'));
@@ -86,14 +84,14 @@ async function postLogs(
headers: {
'Content-Type': 'application/zip'
}
});
}, CancellationToken.None);
} catch (e) {
clearInterval(dotter);
console.log(localize('postError', 'Error posting logs: {0}', e));
throw e;
}
return new TPromise<PostResult>((res, reject) => {
return new Promise<PostResult>((resolve, reject) => {
const parts: Buffer[] = [];
result.stream.on('data', data => {
parts.push(data);
@@ -104,7 +102,7 @@ async function postLogs(
try {
const response = Buffer.concat(parts).toString('utf-8');
if (result.res.statusCode === 200) {
res(JSON.parse(response));
resolve(JSON.parse(response));
} else {
const errorMessage = localize('responseError', 'Error posting logs. Got {0} — {1}', result.res.statusCode, response);
console.log(errorMessage);
@@ -120,13 +118,13 @@ async function postLogs(
function zipLogs(
logsPath: string
): TPromise<string> {
): Promise<string> {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vscode-log-upload'));
const outZip = path.join(tempDir, 'logs.zip');
return new TPromise<string>((resolve, reject) => {
return new Promise<string>((resolve, reject) => {
doZip(logsPath, outZip, tempDir, (err, stdout, stderr) => {
if (err) {
console.error(localize('zipError', 'Error zipping logs: {0}', err));
console.error(localize('zipError', 'Error zipping logs: {0}', err.message));
reject(err);
} else {
resolve(outZip);

View File

@@ -3,9 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/code/electron-main/contributions';
import 'vs/code/code.main';
import { app, dialog } from 'electron';
import { assign } from 'vs/base/common/objects';
import * as platform from 'vs/base/common/platform';
@@ -16,10 +14,9 @@ import { mkdirp, readdir, rimraf } from 'vs/base/node/pfs';
import { validatePaths } from 'vs/code/node/paths';
import { LifecycleService, ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { Server, serve, connect } from 'vs/base/parts/ipc/node/ipc.net';
import { TPromise } from 'vs/base/common/winjs.base';
import { ILaunchChannel, LaunchChannelClient } from 'vs/code/electron-main/launch';
import { LaunchChannelClient } from 'vs/platform/launch/electron-main/launchService';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { InstantiationService } from 'vs/platform/instantiation/node/instantiationService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ILogService, ConsoleLogMainService, MultiplexLogService, getLogLevel } from 'vs/platform/log/common/log';
@@ -44,29 +41,26 @@ import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces
import { localize } from 'vs/nls';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { createSpdLogService } from 'vs/platform/log/node/spdlogService';
import { printDiagnostics } from 'vs/code/electron-main/diagnostics';
import { IDiagnosticsService, DiagnosticsService } from 'vs/platform/diagnostics/electron-main/diagnosticsService';
import { BufferLogService } from 'vs/platform/log/common/bufferLog';
import { uploadLogs } from 'vs/code/electron-main/logUploader';
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { CommandLineDialogService } from 'vs/platform/dialogs/node/dialogService';
import { IUriDisplayService, UriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
import { ILabelService, LabelService } from 'vs/platform/label/common/label';
import { createWaitMarkerFile } from 'vs/code/node/wait';
function createServices(args: ParsedArgs, bufferLogService: BufferLogService): IInstantiationService {
const services = new ServiceCollection();
const environmentService = new EnvironmentService(args, process.execPath);
const consoleLogService = new ConsoleLogMainService(getLogLevel(environmentService));
const logService = new MultiplexLogService([consoleLogService, bufferLogService]);
const uriDisplayService = new UriDisplayService(environmentService, undefined);
const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]);
process.once('exit', () => logService.dispose());
// Eventually cleanup
setTimeout(() => cleanupOlderLogs(environmentService).then(null, err => console.error(err)), 10000);
services.set(IEnvironmentService, environmentService);
services.set(IUriDisplayService, uriDisplayService);
services.set(ILabelService, new LabelService(environmentService, void 0, void 0));
services.set(ILogService, logService);
services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService));
services.set(IHistoryMainService, new SyncDescriptor(HistoryMainService));
@@ -77,6 +71,7 @@ function createServices(args: ParsedArgs, bufferLogService: BufferLogService): I
services.set(IURLService, new SyncDescriptor(URLService));
services.set(IBackupMainService, new SyncDescriptor(BackupMainService));
services.set(IDialogService, new SyncDescriptor(CommandLineDialogService));
services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService));
return new InstantiationService(services, true);
}
@@ -92,31 +87,33 @@ async function cleanupOlderLogs(environmentService: EnvironmentService): Promise
const oldSessions = allSessions.sort().filter((d, i) => d !== currentLog);
const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 9));
await TPromise.join(toDelete.map(name => rimraf(path.join(logsRoot, name))));
await Promise.all(toDelete.map(name => rimraf(path.join(logsRoot, name))));
}
function createPaths(environmentService: IEnvironmentService): TPromise<any> {
function createPaths(environmentService: IEnvironmentService): Thenable<any> {
const paths = [
environmentService.appSettingsHome,
environmentService.extensionsPath,
environmentService.nodeCachedDataDir,
environmentService.logsPath
environmentService.logsPath,
environmentService.globalStorageHome,
environmentService.workspaceStorageHome
];
return TPromise.join(paths.map(p => p && mkdirp(p))) as TPromise<any>;
return Promise.all(paths.map(path => path && mkdirp(path)));
}
class ExpectedError extends Error {
public readonly isExpected = true;
}
function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
function setupIPC(accessor: ServicesAccessor): Thenable<Server> {
const logService = accessor.get(ILogService);
const environmentService = accessor.get(IEnvironmentService);
const requestService = accessor.get(IRequestService);
const diagnosticsService = accessor.get(IDiagnosticsService);
function allowSetForegroundWindow(service: LaunchChannelClient): TPromise<void> {
let promise = TPromise.wrap<void>(void 0);
function allowSetForegroundWindow(service: LaunchChannelClient): Thenable<void> {
let promise: Thenable<void> = Promise.resolve();
if (platform.isWindows) {
promise = service.getMainProcessId()
.then(processId => {
@@ -134,7 +131,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
return promise;
}
function setup(retry: boolean): TPromise<Server> {
function setup(retry: boolean): Thenable<Server> {
return serve(environmentService.mainIPCHandle).then(server => {
// Print --status usage info
@@ -161,7 +158,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
return server;
}, err => {
if (err.code !== 'EADDRINUSE') {
return TPromise.wrapError<Server>(err);
return Promise.reject<Server>(err);
}
// Since we are the second instance, we do not want to show the dock
@@ -179,13 +176,13 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
logService.error(msg);
client.dispose();
return TPromise.wrapError<Server>(new Error(msg));
return Promise.reject(new Error(msg));
}
// Show a warning dialog after some timeout if it takes long to talk to the other instance
// Skip this if we are running with --wait where it is expected that we wait for a while.
// Also skip when gathering diagnostics (--status) which can take a longer time.
let startupWarningDialogHandle: number;
let startupWarningDialogHandle: any;
if (!environmentService.wait && !environmentService.status && !environmentService.args['upload-logs']) {
startupWarningDialogHandle = setTimeout(() => {
showStartupWarningDialog(
@@ -195,20 +192,20 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
}, 10000);
}
const channel = client.getChannel<ILaunchChannel>('launch');
const channel = client.getChannel('launch');
const service = new LaunchChannelClient(channel);
// Process Info
if (environmentService.args.status) {
return service.getMainProcessInfo().then(info => {
return printDiagnostics(info).then(() => TPromise.wrapError(new ExpectedError()));
return diagnosticsService.printDiagnostics(info).then(() => Promise.reject(new ExpectedError()));
});
}
// Log uploader
if (typeof environmentService.args['upload-logs'] !== 'undefined') {
return uploadLogs(channel, requestService, environmentService)
.then(() => TPromise.wrapError(new ExpectedError()));
return uploadLogs(service, requestService, environmentService)
.then(() => Promise.reject(new ExpectedError()));
}
logService.trace('Sending env to running instance...');
@@ -223,7 +220,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
clearTimeout(startupWarningDialogHandle);
}
return TPromise.wrapError(new ExpectedError('Sent env to running instance. Terminating...'));
return Promise.reject(new ExpectedError('Sent env to running instance. Terminating...'));
});
},
err => {
@@ -235,7 +232,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
);
}
return TPromise.wrapError<Server>(err);
return Promise.reject<Server>(err);
}
// it happens on Linux and OS X that the pipe is left behind
@@ -245,7 +242,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
fs.unlinkSync(environmentService.mainIPCHandle);
} catch (e) {
logService.warn('Could not delete obsolete instance handle', e);
return TPromise.wrapError<Server>(e);
return Promise.reject<Server>(e);
}
return setup(false);
@@ -293,12 +290,55 @@ function quit(accessor: ServicesAccessor, reason?: ExpectedError | Error): void
lifecycleService.kill(exitCode);
}
function main() {
function patchEnvironment(environmentService: IEnvironmentService): typeof process.env {
const instanceEnvironment: typeof process.env = {
VSCODE_IPC_HOOK: environmentService.mainIPCHandle,
VSCODE_NLS_CONFIG: process.env['VSCODE_NLS_CONFIG'],
VSCODE_LOGS: process.env['VSCODE_LOGS']
};
if (process.env['VSCODE_PORTABLE']) {
instanceEnvironment['VSCODE_PORTABLE'] = process.env['VSCODE_PORTABLE'];
}
assign(process.env, instanceEnvironment);
return instanceEnvironment;
}
function startup(args: ParsedArgs): void {
// We need to buffer the spdlog logs until we are sure
// we are the only instance running, otherwise we'll have concurrent
// log file access on Windows (https://github.com/Microsoft/vscode/issues/41218)
const bufferLogService = new BufferLogService();
const instantiationService = createServices(args, bufferLogService);
instantiationService.invokeFunction(accessor => {
const environmentService = accessor.get(IEnvironmentService);
// Patch `process.env` with the instance's environment
const instanceEnvironment = patchEnvironment(environmentService);
// Startup
return instantiationService
.invokeFunction(a => createPaths(a.get(IEnvironmentService)))
.then(() => instantiationService.invokeFunction(setupIPC))
.then(mainIpcServer => {
bufferLogService.logger = createSpdLogService('main', bufferLogService.getLevel(), environmentService.logsPath);
return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
});
}).then(null, err => instantiationService.invokeFunction(quit, err));
}
function main(): void {
// Set the error handler early enough so that we are not getting the
// default electron error dialog popping up
setUnexpectedErrorHandler(err => console.error(err));
// Parse arguments
let args: ParsedArgs;
try {
args = parseMainProcessArgv(process.argv);
@@ -307,40 +347,31 @@ function main() {
console.error(err.message);
app.exit(1);
return;
return void 0;
}
// We need to buffer the spdlog logs until we are sure
// we are the only instance running, otherwise we'll have concurrent
// log file access on Windows
// https://github.com/Microsoft/vscode/issues/41218
const bufferLogService = new BufferLogService();
const instantiationService = createServices(args, bufferLogService);
// If we are started with --wait create a random temporary file
// and pass it over to the starting instance. We can use this file
// to wait for it to be deleted to monitor that the edited file
// is closed and then exit the waiting process.
//
// Note: we are not doing this if the wait marker has been already
// added as argument. This can happen if Code was started from CLI.
if (args.wait && !args.waitMarkerFilePath) {
createWaitMarkerFile(args.verbose).then(waitMarkerFilePath => {
if (waitMarkerFilePath) {
process.argv.push('--waitMarkerFilePath', waitMarkerFilePath);
args.waitMarkerFilePath = waitMarkerFilePath;
}
return instantiationService.invokeFunction(accessor => {
startup(args);
});
}
// Patch `process.env` with the instance's environment
const environmentService = accessor.get(IEnvironmentService);
const instanceEnv: typeof process.env = {
VSCODE_IPC_HOOK: environmentService.mainIPCHandle,
VSCODE_NLS_CONFIG: process.env['VSCODE_NLS_CONFIG'],
VSCODE_LOGS: process.env['VSCODE_LOGS']
};
if (process.env['VSCODE_PORTABLE']) {
instanceEnv['VSCODE_PORTABLE'] = process.env['VSCODE_PORTABLE'];
}
assign(process.env, instanceEnv);
// Startup
return instantiationService.invokeFunction(a => createPaths(a.get(IEnvironmentService)))
.then(() => instantiationService.invokeFunction(setupIPC))
.then(mainIpcServer => {
bufferLogService.logger = createSpdLogService('main', bufferLogService.getLevel(), environmentService.logsPath);
return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnv).startup();
});
}).done(null, err => instantiationService.invokeFunction(quit, err));
// Otherwise just startup normally
else {
startup(args);
}
}
main();

View File

@@ -1,799 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { isMacintosh, language } from 'vs/base/common/platform';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { app, shell, Menu, MenuItem, BrowserWindow } from 'electron';
import { OpenContext, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUpdateService, StateType } from 'vs/platform/update/common/update';
import product from 'vs/platform/node/product';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { mnemonicMenuLabel as baseMnemonicLabel, unmnemonicLabel } from 'vs/base/common/labels';
import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows';
import { IHistoryMainService } from 'vs/platform/history/common/history';
import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction } from 'vs/platform/menubar/common/menubar';
import URI from 'vs/base/common/uri';
import { IUriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay';
const telemetryFrom = 'menu';
export class Menubar {
private static readonly MAX_MENU_RECENT_ENTRIES = 10;
private isQuitting: boolean;
private appMenuInstalled: boolean;
private closedLastWindow: boolean;
private menuUpdater: RunOnceScheduler;
private nativeTabMenuItems: Electron.MenuItem[];
private menubarMenus: IMenubarData;
private keybindings: { [commandId: string]: IMenubarKeybinding };
constructor(
@IUpdateService private updateService: IUpdateService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService private configurationService: IConfigurationService,
@IWindowsMainService private windowsMainService: IWindowsMainService,
@IEnvironmentService private environmentService: IEnvironmentService,
@ITelemetryService private telemetryService: ITelemetryService,
@IHistoryMainService private historyMainService: IHistoryMainService,
@IUriDisplayService private uriDisplayService: IUriDisplayService
) {
this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0);
this.keybindings = Object.create(null);
this.closedLastWindow = false;
this.install();
this.registerListeners();
}
private registerListeners(): void {
// Keep flag when app quits
app.on('will-quit', () => {
this.isQuitting = true;
});
// // Listen to some events from window service to update menu
this.historyMainService.onRecentlyOpenedChange(() => this.scheduleUpdateMenu());
this.windowsMainService.onWindowsCountChanged(e => this.onWindowsCountChanged(e));
// this.windowsMainService.onActiveWindowChanged(() => this.updateWorkspaceMenuItems());
// this.windowsMainService.onWindowReady(() => this.updateWorkspaceMenuItems());
// this.windowsMainService.onWindowClose(() => this.updateWorkspaceMenuItems());
// Listen to extension viewlets
// ipc.on('vscode:extensionViewlets', (event: any, rawExtensionViewlets: string) => {
// let extensionViewlets: IExtensionViewlet[] = [];
// try {
// extensionViewlets = JSON.parse(rawExtensionViewlets);
// } catch (error) {
// // Should not happen
// }
// if (extensionViewlets.length) {
// this.extensionViewlets = extensionViewlets;
// this.updateMenu();
// }
// });
// Update when auto save config changes
// this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e));
// Listen to update service
// this.updateService.onStateChange(() => this.updateMenu());
}
private get currentEnableMenuBarMnemonics(): boolean {
let enableMenuBarMnemonics = this.configurationService.getValue<boolean>('window.enableMenuBarMnemonics');
if (typeof enableMenuBarMnemonics !== 'boolean') {
enableMenuBarMnemonics = true;
}
return enableMenuBarMnemonics;
}
private get currentEnableNativeTabs(): boolean {
let enableNativeTabs = this.configurationService.getValue<boolean>('window.nativeTabs');
if (typeof enableNativeTabs !== 'boolean') {
enableNativeTabs = false;
}
return enableNativeTabs;
}
updateMenu(menus: IMenubarData, windowId: number, additionalKeybindings?: Array<IMenubarKeybinding>) {
this.menubarMenus = menus;
if (additionalKeybindings) {
additionalKeybindings.forEach(keybinding => {
this.keybindings[keybinding.id] = keybinding;
});
}
this.scheduleUpdateMenu();
}
private scheduleUpdateMenu(): void {
this.menuUpdater.schedule(); // buffer multiple attempts to update the menu
}
private doUpdateMenu(): 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 onWindowsCountChanged(e: IWindowsCountChangedEvent): void {
if (!isMacintosh) {
return;
}
// Update menu if window count goes from N > 0 or 0 > N to update menu item enablement
if ((e.oldCount === 0 && e.newCount > 0) || (e.oldCount > 0 && e.newCount === 0)) {
this.closedLastWindow = e.newCount === 0;
this.scheduleUpdateMenu();
}
// Update specific items that are dependent on window count
else if (this.currentEnableNativeTabs) {
this.nativeTabMenuItems.forEach(item => {
if (item) {
item.enabled = e.newCount > 1;
}
});
}
}
private install(): void {
// Menus
const menubar = new Menu();
// Mac: Application
let macApplicationMenuItem: Electron.MenuItem;
if (isMacintosh) {
const applicationMenu = new Menu();
macApplicationMenuItem = new MenuItem({ label: product.nameShort, submenu: applicationMenu });
this.setMacApplicationMenu(applicationMenu);
menubar.append(macApplicationMenuItem);
}
// Mac: Dock
if (isMacintosh && !this.appMenuInstalled) {
this.appMenuInstalled = true;
const dockMenu = new Menu();
dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openNewWindow(OpenContext.DOCK) }));
app.dock.setMenu(dockMenu);
}
// File
const fileMenu = new Menu();
const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu });
if (this.shouldDrawMenu('File')) {
if (this.shouldFallback('File')) {
this.setFallbackMenuById(fileMenu, 'File');
} else {
this.setMenuById(fileMenu, 'File');
}
menubar.append(fileMenuItem);
}
// Edit
const editMenu = new Menu();
const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu });
if (this.shouldDrawMenu('Edit')) {
this.setMenuById(editMenu, 'Edit');
menubar.append(editMenuItem);
}
// Selection
const selectionMenu = new Menu();
const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu });
if (this.shouldDrawMenu('Selection')) {
this.setMenuById(selectionMenu, 'Selection');
menubar.append(selectionMenuItem);
}
// View
const viewMenu = new Menu();
const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu });
if (this.shouldDrawMenu('View')) {
this.setMenuById(viewMenu, 'View');
menubar.append(viewMenuItem);
}
// Layout
const layoutMenu = new Menu();
const layoutMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mLayout', comment: ['&& denotes a mnemonic'] }, "&&Layout")), submenu: layoutMenu });
if (this.shouldDrawMenu('Layout')) {
this.setMenuById(layoutMenu, 'Layout');
menubar.append(layoutMenuItem);
}
// Go
const gotoMenu = new Menu();
const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu });
if (this.shouldDrawMenu('Go')) {
this.setMenuById(gotoMenu, 'Go');
menubar.append(gotoMenuItem);
}
// Debug
const debugMenu = new Menu();
const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug")), submenu: debugMenu });
if (this.shouldDrawMenu('Debug')) {
this.setMenuById(debugMenu, 'Debug');
menubar.append(debugMenuItem);
}
// Tasks
const taskMenu = new Menu();
const taskMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTask', comment: ['&& denotes a mnemonic'] }, "&&Tasks")), submenu: taskMenu });
if (this.shouldDrawMenu('Tasks')) {
this.setMenuById(taskMenu, 'Tasks');
menubar.append(taskMenuItem);
}
// Mac: Window
let macWindowMenuItem: Electron.MenuItem;
if (this.shouldDrawMenu('Window')) {
const windowMenu = new Menu();
macWindowMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize('mWindow', "Window")), submenu: windowMenu, role: 'window' });
this.setMacWindowMenu(windowMenu);
}
if (macWindowMenuItem) {
menubar.append(macWindowMenuItem);
}
// Preferences
if (!isMacintosh) {
const preferencesMenu = new Menu();
const preferencesMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu });
if (this.shouldDrawMenu('Preferences')) {
if (this.shouldFallback('Preferences')) {
this.setFallbackMenuById(preferencesMenu, 'Preferences');
} else {
this.setMenuById(preferencesMenu, 'Preferences');
}
menubar.append(preferencesMenuItem);
}
}
// Help
const helpMenu = new Menu();
const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' });
if (this.shouldDrawMenu('Help')) {
if (this.shouldFallback('Help')) {
this.setFallbackMenuById(helpMenu, 'Help');
} else {
this.setMenuById(helpMenu, 'Help');
}
menubar.append(helpMenuItem);
}
if (menubar.items && menubar.items.length > 0) {
Menu.setApplicationMenu(menubar);
} else {
Menu.setApplicationMenu(null);
}
}
private setMacApplicationMenu(macApplicationMenu: Electron.Menu): void {
const about = new MenuItem({ label: nls.localize('mAbout', "About {0}", product.nameLong), role: 'about' });
const checkForUpdates = this.getUpdateMenuItems();
let preferences;
if (this.shouldDrawMenu('Preferences')) {
const preferencesMenu = new Menu();
this.setMenuById(preferencesMenu, 'Preferences');
preferences = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu });
}
const servicesMenu = new Menu();
const services = new MenuItem({ label: nls.localize('mServices', "Services"), role: 'services', submenu: servicesMenu });
const hide = new MenuItem({ label: nls.localize('mHide', "Hide {0}", product.nameLong), role: 'hide', accelerator: 'Command+H' });
const hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideothers', accelerator: 'Command+Alt+H' });
const showAll = new MenuItem({ label: nls.localize('mShowAll', "Show All"), role: 'unhide' });
const quit = new MenuItem(this.likeAction('workbench.action.quit', {
label: nls.localize('miQuit', "Quit {0}", product.nameLong), click: () => {
if (this.windowsMainService.getWindowCount() === 0 || !!BrowserWindow.getFocusedWindow()) {
this.windowsMainService.quit(); // fix for https://github.com/Microsoft/vscode/issues/39191
}
}
}));
const actions = [about];
actions.push(...checkForUpdates);
if (preferences) {
actions.push(...[
__separator__(),
preferences
]);
}
actions.push(...[
__separator__(),
services,
__separator__(),
hide,
hideOthers,
showAll,
__separator__(),
quit
]);
actions.forEach(i => macApplicationMenu.append(i));
}
private shouldDrawMenu(menuId: string): boolean {
// We need to draw an empty menu to override the electron default
if (!isMacintosh && this.configurationService.getValue('window.titleBarStyle') === 'custom') {
return false;
}
switch (menuId) {
case 'File':
case 'Help':
if (isMacintosh) {
return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (!!this.menubarMenus && !!this.menubarMenus[menuId]);
}
case 'Window':
if (isMacintosh) {
return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || !!this.menubarMenus;
}
default:
return this.windowsMainService.getWindowCount() > 0 && (!!this.menubarMenus && !!this.menubarMenus[menuId]);
}
}
private shouldFallback(menuId: string): boolean {
return this.shouldDrawMenu(menuId) && (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow && isMacintosh);
}
private setFallbackMenuById(menu: Electron.Menu, menuId: string): void {
switch (menuId) {
case 'File':
const newFile = new MenuItem(this.likeAction('workbench.action.files.newUntitledFile', { label: this.mnemonicLabel(nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File")), click: () => this.windowsMainService.openNewWindow(OpenContext.MENU) }));
const newWindow = new MenuItem(this.likeAction('workbench.action.newWindow', { label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openNewWindow(OpenContext.MENU) }));
const open = new MenuItem(this.likeAction('workbench.action.files.openFileFolder', { label: this.mnemonicLabel(nls.localize({ key: 'miOpen', comment: ['&& denotes a mnemonic'] }, "&&Open...")), click: (menuItem, win, event) => this.windowsMainService.pickFileFolderAndOpen({ forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }) }));
const openWorkspace = new MenuItem(this.likeAction('workbench.action.openWorkspace', { label: this.mnemonicLabel(nls.localize({ key: 'miOpenWorkspace', comment: ['&& denotes a mnemonic'] }, "Open Wor&&kspace...")), click: (menuItem, win, event) => this.windowsMainService.pickWorkspaceAndOpen({ forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }) }));
const openRecentMenu = new Menu();
this.setFallbackMenuById(openRecentMenu, 'Recent');
const openRecent = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miOpenRecent', comment: ['&& denotes a mnemonic'] }, "Open &&Recent")), submenu: openRecentMenu });
menu.append(newFile);
menu.append(newWindow);
menu.append(__separator__());
menu.append(open);
menu.append(openWorkspace);
menu.append(openRecent);
break;
case 'Recent':
menu.append(this.createMenuItem(nls.localize({ key: 'miReopenClosedEditor', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed Editor"), 'workbench.action.reopenClosedEditor'));
this.insertRecentMenuItems(menu);
menu.append(__separator__());
menu.append(this.createMenuItem(nls.localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More..."), 'workbench.action.openRecent'));
menu.append(__separator__());
menu.append(new MenuItem(this.likeAction('workbench.action.clearRecentFiles', { label: this.mnemonicLabel(nls.localize({ key: 'miClearRecentOpen', comment: ['&& denotes a mnemonic'] }, "&&Clear Recently Opened")), click: () => this.historyMainService.clearRecentlyOpened() })));
break;
case 'Help':
let twitterItem: MenuItem;
if (product.twitterUrl) {
twitterItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join us on Twitter")), click: () => this.openUrl(product.twitterUrl, 'openTwitterUrl') });
}
let featureRequestsItem: MenuItem;
if (product.requestFeatureUrl) {
featureRequestsItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests")), click: () => this.openUrl(product.requestFeatureUrl, 'openUserVoiceUrl') });
}
let reportIssuesItem: MenuItem;
if (product.reportIssueUrl) {
const label = nls.localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue");
reportIssuesItem = new MenuItem({ label: this.mnemonicLabel(label), click: () => this.openUrl(product.reportIssueUrl, 'openReportIssues') });
}
let licenseItem: MenuItem;
if (product.privacyStatementUrl) {
licenseItem = new MenuItem({
label: this.mnemonicLabel(nls.localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License")), click: () => {
if (language) {
const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?';
this.openUrl(`${product.licenseUrl}${queryArgChar}lang=${language}`, 'openLicenseUrl');
} else {
this.openUrl(product.licenseUrl, 'openLicenseUrl');
}
}
});
}
let privacyStatementItem: MenuItem;
if (product.privacyStatementUrl) {
privacyStatementItem = new MenuItem({
label: this.mnemonicLabel(nls.localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "&&Privacy Statement")), click: () => {
if (language) {
const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?';
this.openUrl(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`, 'openPrivacyStatement');
} else {
this.openUrl(product.privacyStatementUrl, 'openPrivacyStatement');
}
}
});
}
if (twitterItem) { menu.append(twitterItem); }
if (featureRequestsItem) { menu.append(featureRequestsItem); }
if (reportIssuesItem) { menu.append(reportIssuesItem); }
if ((twitterItem || featureRequestsItem || reportIssuesItem) && (licenseItem || privacyStatementItem)) { menu.append(__separator__()); }
if (licenseItem) { menu.append(licenseItem); }
if (privacyStatementItem) { menu.append(privacyStatementItem); }
break;
}
}
private setMenu(menu: Electron.Menu, items: Array<MenubarMenuItem>) {
items.forEach((item: MenubarMenuItem) => {
if (isMenubarMenuItemSeparator(item)) {
menu.append(__separator__());
} else if (isMenubarMenuItemSubmenu(item)) {
const submenu = new Menu();
const submenuItem = new MenuItem({ label: this.mnemonicLabel(item.label), submenu: submenu });
this.setMenu(submenu, item.submenu.items);
menu.append(submenuItem);
} else if (isMenubarMenuItemAction(item)) {
if (item.id === 'workbench.action.openRecent') {
this.insertRecentMenuItems(menu);
} else if (item.id === 'workbench.action.showAboutDialog') {
this.insertCheckForUpdatesItems(menu);
}
// Store the keybinding
if (item.keybinding) {
this.keybindings[item.id] = item.keybinding;
} else if (this.keybindings[item.id]) {
this.keybindings[item.id] = undefined;
}
const menuItem = this.createMenuItem(item.label, item.id, item.enabled, item.checked);
menu.append(menuItem);
}
});
}
private setMenuById(menu: Electron.Menu, menuId: string): void {
if (this.menubarMenus && this.menubarMenus[menuId]) {
this.setMenu(menu, this.menubarMenus[menuId].items);
}
}
private insertCheckForUpdatesItems(menu: Electron.Menu) {
const updateItems = this.getUpdateMenuItems();
if (updateItems.length) {
updateItems.forEach(i => menu.append(i));
menu.append(__separator__());
}
}
private insertRecentMenuItems(menu: Electron.Menu) {
const { workspaces, files } = this.historyMainService.getRecentlyOpened();
// Workspaces
if (workspaces.length > 0) {
for (let i = 0; i < Menubar.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) {
menu.append(this.createOpenRecentMenuItem(workspaces[i], 'openRecentWorkspace', false));
}
menu.append(__separator__());
}
// Files
if (files.length > 0) {
for (let i = 0; i < Menubar.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) {
menu.append(this.createOpenRecentMenuItem(files[i], 'openRecentFile', true));
}
menu.append(__separator__());
}
}
private createOpenRecentMenuItem(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string, commandId: string, isFile: boolean): Electron.MenuItem {
let label: string;
let uri: URI;
if (isSingleFolderWorkspaceIdentifier(workspace)) {
label = unmnemonicLabel(getWorkspaceLabel(workspace, this.environmentService, this.uriDisplayService, { verbose: true }));
uri = workspace;
} else if (isWorkspaceIdentifier(workspace)) {
label = getWorkspaceLabel(workspace, this.environmentService, this.uriDisplayService, { verbose: true });
uri = URI.file(workspace.configPath);
} else {
uri = URI.file(workspace);
label = unmnemonicLabel(this.uriDisplayService.getLabel(uri));
}
return new MenuItem(this.likeAction(commandId, {
label,
click: (menuItem, win, event) => {
const openInNewWindow = this.isOptionClick(event);
const success = this.windowsMainService.open({
context: OpenContext.MENU,
cli: this.environmentService.args,
urisToOpen: [uri],
forceNewWindow: openInNewWindow,
forceOpenWorkspaceAsFile: isFile
}).length > 0;
if (!success) {
this.historyMainService.removeFromRecentlyOpened([workspace]);
}
}
}, false));
}
private isOptionClick(event: Electron.Event): boolean {
return event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey)));
}
private setMacWindowMenu(macWindowMenu: Electron.Menu): void {
const minimize = new MenuItem({ label: nls.localize('mMinimize', "Minimize"), role: 'minimize', accelerator: 'Command+M', enabled: this.windowsMainService.getWindowCount() > 0 });
const zoom = new MenuItem({ label: nls.localize('mZoom', "Zoom"), role: 'zoom', enabled: this.windowsMainService.getWindowCount() > 0 });
const bringAllToFront = new MenuItem({ label: nls.localize('mBringToFront', "Bring All to Front"), role: 'front', enabled: this.windowsMainService.getWindowCount() > 0 });
const switchWindow = this.createMenuItem(nls.localize({ key: 'miSwitchWindow', comment: ['&& denotes a mnemonic'] }, "Switch &&Window..."), 'workbench.action.switchWindow');
this.nativeTabMenuItems = [];
const nativeTabMenuItems: Electron.MenuItem[] = [];
if (this.currentEnableNativeTabs) {
const hasMultipleWindows = this.windowsMainService.getWindowCount() > 1;
this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mShowPreviousTab', "Show Previous Tab"), 'workbench.action.showPreviousWindowTab', hasMultipleWindows));
this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mShowNextTab', "Show Next Tab"), 'workbench.action.showNextWindowTab', hasMultipleWindows));
this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mMoveTabToNewWindow', "Move Tab to New Window"), 'workbench.action.moveWindowTabToNewWindow', hasMultipleWindows));
this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mMergeAllWindows', "Merge All Windows"), 'workbench.action.mergeAllWindowTabs', hasMultipleWindows));
nativeTabMenuItems.push(__separator__(), ...this.nativeTabMenuItems);
} else {
this.nativeTabMenuItems = [];
}
[
minimize,
zoom,
switchWindow,
...nativeTabMenuItems,
__separator__(),
bringAllToFront
].forEach(item => macWindowMenu.append(item));
}
private getUpdateMenuItems(): Electron.MenuItem[] {
const state = this.updateService.state;
switch (state.type) {
case StateType.Uninitialized:
return [];
case StateType.Idle:
return [new MenuItem({
label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => {
this.reportMenuActionTelemetry('CheckForUpdate');
const focusedWindow = this.windowsMainService.getFocusedWindow();
const context = focusedWindow ? { windowId: focusedWindow.id } : null;
this.updateService.checkForUpdates(context);
}, 0)
})];
case StateType.CheckingForUpdates:
return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking For Updates..."), enabled: false })];
case StateType.AvailableForDownload:
return [new MenuItem({
label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => {
this.updateService.downloadUpdate();
}
})];
case StateType.Downloading:
return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })];
case StateType.Downloaded:
return [new MenuItem({
label: nls.localize('miInstallUpdate', "Install Update..."), click: () => {
this.reportMenuActionTelemetry('InstallUpdate');
this.updateService.applyUpdate();
}
})];
case StateType.Updating:
return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })];
case StateType.Ready:
return [new MenuItem({
label: nls.localize('miRestartToUpdate', "Restart to Update..."), click: () => {
this.reportMenuActionTelemetry('RestartToUpdate');
this.updateService.quitAndInstall();
}
})];
}
}
private createMenuItem(label: string, commandId: string | string[], enabled?: boolean, checked?: boolean): Electron.MenuItem;
private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): Electron.MenuItem;
private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): Electron.MenuItem {
const label = this.mnemonicLabel(arg1);
const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: Electron.MenuItem, win: Electron.BrowserWindow, event: Electron.Event) => {
let commandId = arg2;
if (Array.isArray(arg2)) {
commandId = this.isOptionClick(event) ? arg2[1] : arg2[0]; // support alternative action if we got multiple action Ids and the option key was pressed while invoking
}
this.runActionInRenderer(commandId);
};
const enabled = typeof arg3 === 'boolean' ? arg3 : this.windowsMainService.getWindowCount() > 0;
const checked = typeof arg4 === 'boolean' ? arg4 : false;
const options: Electron.MenuItemConstructorOptions = {
label,
click,
enabled
};
if (checked) {
options['type'] = 'checkbox';
options['checked'] = checked;
}
let commandId: string;
if (typeof arg2 === 'string') {
commandId = arg2;
} else if (Array.isArray(arg2)) {
commandId = arg2[0];
}
// Add role for special case menu items
if (isMacintosh) {
if (commandId === 'editor.action.clipboardCutAction') {
options['role'] = 'cut';
} else if (commandId === 'editor.action.clipboardCopyAction') {
options['role'] = 'copy';
} else if (commandId === 'editor.action.clipboardPasteAction') {
options['role'] = 'paste';
}
}
return new MenuItem(this.withKeybinding(commandId, options));
}
private runActionInRenderer(id: string): void {
// We make sure to not run actions when the window has no focus, this helps
// for https://github.com/Microsoft/vscode/issues/25907 and specifically for
// https://github.com/Microsoft/vscode/issues/11928
const activeWindow = this.windowsMainService.getFocusedWindow();
if (activeWindow) {
this.windowsMainService.sendToFocused('vscode:runAction', { id, from: 'menu' } as IRunActionInWindowRequest);
}
}
private withKeybinding(commandId: string, options: Electron.MenuItemConstructorOptions): Electron.MenuItemConstructorOptions {
const binding = this.keybindings[commandId];
// Apply binding if there is one
if (binding && binding.label) {
// if the binding is native, we can just apply it
if (binding.isNative) {
options.accelerator = binding.label;
}
// the keybinding is not native so we cannot show it as part of the accelerator of
// the menu item. we fallback to a different strategy so that we always display it
else {
const bindingIndex = options.label.indexOf('[');
if (bindingIndex >= 0) {
options.label = `${options.label.substr(0, bindingIndex)} [${binding.label}]`;
} else {
options.label = `${options.label} [${binding.label}]`;
}
}
}
// Unset bindings if there is none
else {
options.accelerator = void 0;
}
return options;
}
private likeAction(commandId: string, options: Electron.MenuItemConstructorOptions, setAccelerator = !options.accelerator): Electron.MenuItemConstructorOptions {
if (setAccelerator) {
options = this.withKeybinding(commandId, options);
}
const originalClick = options.click;
options.click = (item, window, event) => {
this.reportMenuActionTelemetry(commandId);
if (originalClick) {
originalClick(item, window, event);
}
};
return options;
}
private openUrl(url: string, id: string): void {
shell.openExternal(url);
this.reportMenuActionTelemetry(id);
}
private reportMenuActionTelemetry(id: string): void {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id, from: telemetryFrom });
}
private mnemonicLabel(label: string): string {
return baseMnemonicLabel(label, !this.currentEnableMenuBarMnemonics);
}
}
function __separator__(): Electron.MenuItem {
return new MenuItem({ type: 'separator' });
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,37 +6,40 @@
import { assign } from 'vs/base/common/objects';
import { memoize } from 'vs/base/common/decorators';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { TPromise } from 'vs/base/common/winjs.base';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { BrowserWindow, ipcMain } from 'electron';
import { ISharedProcess } from 'vs/platform/windows/electron-main/windows';
import { Barrier } from 'vs/base/common/async';
import { ILogService } from 'vs/platform/log/common/log';
import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { IStateService } from 'vs/platform/state/common/state';
import { getBackgroundColor } from 'vs/code/electron-main/theme';
import { dispose, toDisposable, IDisposable } from 'vs/base/common/lifecycle';
export class SharedProcess implements ISharedProcess {
private barrier = new Barrier();
private window: Electron.BrowserWindow;
private window: Electron.BrowserWindow | null;
constructor(
private readonly environmentService: IEnvironmentService,
private readonly lifecycleService: ILifecycleService,
private readonly logService: ILogService,
private readonly machineId: string,
private readonly userEnv: IProcessEnvironment,
) {
}
private userEnv: NodeJS.ProcessEnv,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IStateService private readonly stateService: IStateService,
@ILogService private readonly logService: ILogService
) { }
@memoize
private get _whenReady(): TPromise<void> {
private get _whenReady(): Promise<void> {
this.window = new BrowserWindow({
show: false,
backgroundColor: getBackgroundColor(this.stateService),
webPreferences: {
images: false,
webaudio: false,
webgl: false
webgl: false,
disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer
}
});
const config = assign({
@@ -57,26 +60,34 @@ export class SharedProcess implements ISharedProcess {
e.preventDefault();
// Still hide the window though if visible
if (this.window.isVisible()) {
if (this.window && this.window.isVisible()) {
this.window.hide();
}
};
this.window.on('close', onClose);
this.lifecycleService.onShutdown(() => {
const disposables: IDisposable[] = [];
this.lifecycleService.onWillShutdown(() => {
dispose(disposables);
// Shut the shared process down when we are quitting
//
// Note: because we veto the window close, we must first remove our veto.
// Otherwise the application would never quit because the shared process
// window is refusing to close!
//
this.window.removeListener('close', onClose);
if (this.window) {
this.window.removeListener('close', onClose);
}
// Electron seems to crash on Windows without this setTimeout :|
setTimeout(() => {
try {
this.window.close();
if (this.window) {
this.window.close();
}
} catch (err) {
// ignore, as electron is already shutting down
}
@@ -85,7 +96,7 @@ export class SharedProcess implements ISharedProcess {
}, 0);
});
return new TPromise<void>((c, e) => {
return new Promise<void>(c => {
ipcMain.once('handshake:hello', ({ sender }: { sender: any }) => {
sender.send('handshake:hey there', {
sharedIPCHandle: this.environmentService.sharedIPCHandle,
@@ -93,21 +104,24 @@ export class SharedProcess implements ISharedProcess {
logLevel: this.logService.getLevel()
});
ipcMain.once('handshake:im ready', () => c(null));
disposables.push(toDisposable(() => sender.send('handshake:goodbye')));
ipcMain.once('handshake:im ready', () => c(void 0));
});
});
}
spawn(): void {
spawn(userEnv: NodeJS.ProcessEnv): void {
this.userEnv = { ...this.userEnv, ...userEnv };
this.barrier.open();
}
whenReady(): TPromise<void> {
return this.barrier.wait().then(() => this._whenReady);
async whenReady(): Promise<void> {
await this.barrier.wait();
await this._whenReady;
}
toggle(): void {
if (this.window.isVisible()) {
if (!this.window || this.window.isVisible()) {
this.hide();
} else {
this.show();
@@ -115,12 +129,16 @@ export class SharedProcess implements ISharedProcess {
}
show(): void {
this.window.show();
this.window.webContents.openDevTools();
if (this.window) {
this.window.show();
this.window.webContents.openDevTools();
}
}
hide(): void {
this.window.webContents.closeDevTools();
this.window.hide();
if (this.window) {
this.window.webContents.closeDevTools();
this.window.hide();
}
}
}

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isWindows, isMacintosh } from 'vs/base/common/platform';
import { systemPreferences } from 'electron';
import { IStateService } from 'vs/platform/state/common/state';
export const DEFAULT_BG_LIGHT = '#FFFFFF';
export const DEFAULT_BG_DARK = '#1E1E1E';
export const DEFAULT_BG_HC_BLACK = '#000000';
export const THEME_STORAGE_KEY = 'theme';
export const THEME_BG_STORAGE_KEY = 'themeBackground';
export function getBackgroundColor(stateService: IStateService): string {
if (isWindows && systemPreferences.isInvertedColorScheme()) {
return DEFAULT_BG_HC_BLACK;
}
let background = stateService.getItem<string | null>(THEME_BG_STORAGE_KEY, null);
if (!background) {
let baseTheme: string;
if (isWindows && systemPreferences.isInvertedColorScheme()) {
baseTheme = 'hc-black';
} else {
baseTheme = stateService.getItem<string>(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0];
}
background = (baseTheme === 'hc-black') ? DEFAULT_BG_HC_BLACK : (baseTheme === 'vs' ? DEFAULT_BG_LIGHT : DEFAULT_BG_DARK);
}
if (isMacintosh && background.toUpperCase() === DEFAULT_BG_DARK) {
background = '#171717'; // https://github.com/electron/electron/issues/5150
}
return background;
}

View File

@@ -3,29 +3,28 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as path from 'path';
import * as objects from 'vs/base/common/objects';
import * as nls from 'vs/nls';
import URI from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { IStateService } from 'vs/platform/state/common/state';
import { shell, screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage } from 'electron';
import { TPromise, TValueCallback } from 'vs/base/common/winjs.base';
import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage } from 'electron';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { ILogService } from 'vs/platform/log/common/log';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { parseArgs } from 'vs/platform/environment/node/argv';
import product from 'vs/platform/node/product';
import { IWindowSettings, MenuBarVisibility, IWindowConfiguration, ReadyState, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IWindowSettings, MenuBarVisibility, IWindowConfiguration, ReadyState, IRunActionInWindowRequest, getTitleBarStyle } from 'vs/platform/windows/common/windows';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
import { IWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces';
import { IBackupMainService } from 'vs/platform/backup/common/backup';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { mark, exportEntries } from 'vs/base/common/performance';
import * as perf from 'vs/base/common/performance';
import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/node/extensionGalleryService';
import { getBackgroundColor } from 'vs/code/electron-main/theme';
import { IStorageMainService } from 'vs/platform/storage/node/storageMainService';
export interface IWindowCreationOptions {
state: IWindowState;
@@ -53,18 +52,13 @@ interface ITouchBarSegment extends Electron.SegmentedControlSegment {
id: string;
}
export class CodeWindow implements ICodeWindow {
static readonly themeStorageKey = 'theme';
static readonly themeBackgroundStorageKey = 'themeBackground';
private static readonly DEFAULT_BG_LIGHT = '#FFFFFF';
private static readonly DEFAULT_BG_DARK = '#1E1E1E';
private static readonly DEFAULT_BG_HC_BLACK = '#000000';
export class CodeWindow extends Disposable implements ICodeWindow {
private static readonly MIN_WIDTH = 200;
private static readonly MIN_HEIGHT = 120;
private static readonly MAX_URL_LENGTH = 2 * 1024 * 1024; // https://cs.chromium.org/chromium/src/url/url_constants.cc?l=32
private hiddenTitleBarStyle: boolean;
private showTimeoutHandle: any;
private _id: number;
@@ -73,15 +67,14 @@ export class CodeWindow implements ICodeWindow {
private _readyState: ReadyState;
private windowState: IWindowState;
private currentMenuBarVisibility: MenuBarVisibility;
private toDispose: IDisposable[];
private representedFilename: string;
private whenReadyCallbacks: TValueCallback<ICodeWindow>[];
private whenReadyCallbacks: { (window: ICodeWindow): void }[];
private currentConfig: IWindowConfiguration;
private pendingLoadConfig: IWindowConfiguration;
private marketplaceHeadersPromise: TPromise<object>;
private marketplaceHeadersPromise: Thenable<object>;
private touchBarGroups: Electron.TouchBarSegmentedControl[];
@@ -92,13 +85,15 @@ export class CodeWindow implements ICodeWindow {
@IConfigurationService private configurationService: IConfigurationService,
@IStateService private stateService: IStateService,
@IWorkspacesMainService private workspacesMainService: IWorkspacesMainService,
@IBackupMainService private backupMainService: IBackupMainService
@IBackupMainService private backupMainService: IBackupMainService,
@IStorageMainService private storageMainService: IStorageMainService
) {
super();
this.touchBarGroups = [];
this._lastFocusTime = -1;
this._readyState = ReadyState.NONE;
this.whenReadyCallbacks = [];
this.toDispose = [];
// create browser window
this.createBrowserWindow(config);
@@ -124,24 +119,22 @@ export class CodeWindow implements ICodeWindow {
// in case we are maximized or fullscreen, only show later after the call to maximize/fullscreen (see below)
const isFullscreenOrMaximized = (this.windowState.mode === WindowMode.Maximized || this.windowState.mode === WindowMode.Fullscreen);
let backgroundColor = this.getBackgroundColor();
if (isMacintosh && backgroundColor.toUpperCase() === CodeWindow.DEFAULT_BG_DARK) {
backgroundColor = '#171717'; // https://github.com/electron/electron/issues/5150
}
const options: Electron.BrowserWindowConstructorOptions = {
width: this.windowState.width,
height: this.windowState.height,
x: this.windowState.x,
y: this.windowState.y,
backgroundColor,
backgroundColor: getBackgroundColor(this.stateService),
minWidth: CodeWindow.MIN_WIDTH,
minHeight: CodeWindow.MIN_HEIGHT,
show: !isFullscreenOrMaximized,
title: product.nameLong,
webPreferences: {
'backgroundThrottling': false, // by default if Code is in the background, intervals and timeouts get throttled,
disableBlinkFeatures: 'Auxclick' // disable auxclick events (see https://developers.google.com/web/updates/2016/10/auxclick)
// By default if Code is in the background, intervals and timeouts get throttled, so we
// want to enforce that Code stays in the foreground. This triggers a disable_hidden_
// flag that Electron provides via patch:
// https://github.com/electron/libchromiumcontent/blob/master/patches/common/chromium/disable_hidden.patch
'backgroundThrottling': false
}
};
@@ -151,6 +144,10 @@ export class CodeWindow implements ICodeWindow {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
if (isMacintosh && !this.useNativeFullScreen()) {
options.fullscreenable = false; // enables simple fullscreen mode
}
if (isMacintosh) {
options.acceptFirstMouse = true; // enabled by default
@@ -159,28 +156,14 @@ export class CodeWindow implements ICodeWindow {
}
}
let useNativeTabs = false;
if (isMacintosh && windowConfig && windowConfig.nativeTabs === true) {
options.tabbingIdentifier = product.nameShort; // this opts in to sierra tabs
useNativeTabs = true;
}
let useCustomTitleStyle = false;
// {{SQL CARBON EDIT}}
// if (isMacintosh) {
// turn-off custom menus to avoid bug calculating size of SQL editor
//
// if (isMacintosh && (!windowConfig || !windowConfig.titleBarStyle || windowConfig.titleBarStyle === 'custom')) {
// const isDev = !this.environmentService.isBuilt || !!config.extensionDevelopmentPath;
// if (!isDev) {
// useCustomTitleStyle = true; // not enabled when developing due to https://github.com/electron/electron/issues/3647
// }
// }
if (useNativeTabs) {
useCustomTitleStyle = false; // native tabs on sierra do not work with custom title style
}
// const useCustomTitleStyle = getTitleBarStyle(this.configurationService, this.environmentService, !!config.extensionDevelopmentPath) === 'custom';
const useCustomTitleStyle = false;
if (useCustomTitleStyle) {
options.titleBarStyle = 'hidden';
this.hiddenTitleBarStyle = true;
@@ -193,7 +176,7 @@ export class CodeWindow implements ICodeWindow {
this._win = new BrowserWindow(options);
this._id = this._win.id;
if (useCustomTitleStyle) {
if (isMacintosh && useCustomTitleStyle) {
this._win.setSheetOffset(22); // offset dialogs by the height of the custom title bar if we have any
}
@@ -201,7 +184,7 @@ export class CodeWindow implements ICodeWindow {
this._win.maximize();
if (this.windowState.mode === WindowMode.Fullscreen) {
this._win.setFullScreen(true);
this.setFullScreen(true);
}
if (!this._win.isVisible()) {
@@ -284,6 +267,10 @@ export class CodeWindow implements ICodeWindow {
return this.currentConfig ? this.currentConfig.folderUri : void 0;
}
get remoteAuthority(): string {
return this.currentConfig ? this.currentConfig.remoteAuthority : void 0;
}
setReady(): void {
this._readyState = ReadyState.READY;
@@ -293,19 +280,19 @@ export class CodeWindow implements ICodeWindow {
}
}
ready(): TPromise<ICodeWindow> {
return new TPromise<ICodeWindow>((c) => {
if (this._readyState === ReadyState.READY) {
return c(this);
ready(): Thenable<ICodeWindow> {
return new Promise<ICodeWindow>(resolve => {
if (this.isReady) {
return resolve(this);
}
// otherwise keep and call later when we are ready
this.whenReadyCallbacks.push(c);
this.whenReadyCallbacks.push(resolve);
});
}
get readyState(): ReadyState {
return this._readyState;
get isReady(): boolean {
return this._readyState === ReadyState.READY;
}
private handleMarketplaceRequests(): void {
@@ -316,7 +303,7 @@ export class CodeWindow implements ICodeWindow {
// Inject headers when requests are incoming
const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*'];
this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details: any, cb: any) => {
this.marketplaceHeadersPromise.done(headers => {
this.marketplaceHeadersPromise.then(headers => {
cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) });
});
});
@@ -371,20 +358,26 @@ export class CodeWindow implements ICodeWindow {
// App commands support
this.registerNavigationListenerOn('app-command', 'browser-backward', 'browser-forward', false);
// 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 = Date.now();
});
// Simple fullscreen doesn't resize automatically when the resolution changes
if (isMacintosh) {
const displayMetricsChangedListener = () => {
if (this.isFullScreen() && !this.useNativeFullScreen()) {
this.setFullScreen(false);
this.setFullScreen(true);
}
};
screen.addListener('display-metrics-changed', displayMetricsChangedListener);
this._register(toDisposable(() => screen.removeListener('display-metrics-changed', displayMetricsChangedListener)));
}
// Window (Un)Maximize
this._win.on('maximize', (e) => {
this._win.on('maximize', e => {
if (this.currentConfig) {
this.currentConfig.maximized = true;
}
@@ -392,7 +385,7 @@ export class CodeWindow implements ICodeWindow {
app.emit('browser-window-maximize', e, this._win);
});
this._win.on('unmaximize', (e) => {
this._win.on('unmaximize', e => {
if (this.currentConfig) {
this.currentConfig.maximized = false;
}
@@ -414,21 +407,11 @@ export class CodeWindow implements ICodeWindow {
this.logService.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.environmentService.isBuilt) {
this._win.webContents.on('will-navigate', (event: Event) => {
if (event) {
event.preventDefault();
}
});
}
// Handle configuration changes
this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated()));
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated()));
// Handle Workspace events
this.toDispose.push(this.workspacesMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e)));
this._register(this.workspacesMainService.onUntitledWorkspaceDeleted(e => this.onUntitledWorkspaceDeleted(e)));
// TODO@Ben workaround for https://github.com/Microsoft/vscode/issues/13612
// It looks like smooth scrolling disappears as soon as the window is minimized
@@ -488,7 +471,7 @@ export class CodeWindow implements ICodeWindow {
private registerNavigationListenerOn(command: 'swipe' | 'app-command', back: 'left' | 'browser-backward', forward: 'right' | 'browser-forward', acrossEditors: boolean) {
this._win.on(command as 'swipe' /* | 'app-command' */, (e: Electron.Event, cmd: string) => {
if (this.readyState !== ReadyState.READY) {
if (!this.isReady) {
return; // window must be ready
}
@@ -500,11 +483,17 @@ export class CodeWindow implements ICodeWindow {
});
}
addTabbedWindow(window: ICodeWindow): void {
if (isMacintosh) {
this._win.addTabbedWindow(window.win);
}
}
load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): 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) {
if (this._readyState === ReadyState.NONE) {
this.currentConfig = config;
}
@@ -541,7 +530,7 @@ export class CodeWindow implements ICodeWindow {
}
// Load URL
mark('main:loadWindow');
perf.mark('main:loadWindow');
this._win.loadURL(this.getUrl(configuration));
// Make window visible if it did not open in N seconds because this indicates an error
@@ -601,7 +590,7 @@ export class CodeWindow implements ICodeWindow {
}
// Set fullscreen state
windowConfiguration.fullscreen = this._win.isFullScreen();
windowConfiguration.fullscreen = this.isFullScreen();
// Set Accessibility Config
let autoDetectHighContrast = true;
@@ -611,54 +600,45 @@ export class CodeWindow implements ICodeWindow {
windowConfiguration.highContrast = isWindows && autoDetectHighContrast && systemPreferences.isInvertedColorScheme();
windowConfiguration.accessibilitySupport = app.isAccessibilitySupportEnabled();
// Theme
windowConfiguration.baseTheme = this.getBaseTheme();
windowConfiguration.backgroundColor = this.getBackgroundColor();
// Title style related
windowConfiguration.maximized = this._win.isMaximized();
windowConfiguration.frameless = this.hasHiddenTitleBarStyle() && !isMacintosh;
// Perf Counters
windowConfiguration.perfEntries = exportEntries();
windowConfiguration.perfStartTime = (<any>global).perfStartTime;
windowConfiguration.perfWindowLoadTime = Date.now();
// Dump Perf Counters
windowConfiguration.perfEntries = perf.exportEntries();
// Parts splash
windowConfiguration.partsSplashData = this.storageMainService.get('parts-splash-data', void 0);
// Config (combination of process.argv and window configuration)
const environment = parseArgs(process.argv);
const config = objects.assign(environment, windowConfiguration);
for (let key in config) {
if (config[key] === void 0 || config[key] === null || config[key] === '') {
if (config[key] === void 0 || config[key] === null || config[key] === '' || config[key] === false) {
delete config[key]; // only send over properties that have a true value
}
}
return `${require.toUrl('vs/workbench/electron-browser/bootstrap/index.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
// In the unlikely event of the URL becoming larger than 2MB, remove parts of
// it that are not under our control. Mainly, the user environment can be very
// large depending on user configuration, so we can only remove it in that case.
let configUrl = this.doGetUrl(config);
if (configUrl.length > CodeWindow.MAX_URL_LENGTH) {
delete config.userEnv;
this.logService.warn('Application URL exceeds maximum of 2MB and was shortened.');
configUrl = this.doGetUrl(config);
if (configUrl.length > CodeWindow.MAX_URL_LENGTH) {
this.logService.error('Application URL exceeds maximum of 2MB and cannot be loaded.');
}
}
return configUrl;
}
private getBaseTheme(): string {
if (isWindows && systemPreferences.isInvertedColorScheme()) {
return 'hc-black';
}
const theme = this.stateService.getItem<string>(CodeWindow.themeStorageKey, 'vs-dark');
return theme.split(' ')[0];
}
private getBackgroundColor(): string {
if (isWindows && systemPreferences.isInvertedColorScheme()) {
return CodeWindow.DEFAULT_BG_HC_BLACK;
}
const background = this.stateService.getItem<string>(CodeWindow.themeBackgroundStorageKey, null);
if (!background) {
const baseTheme = this.getBaseTheme();
return baseTheme === 'hc-black' ? CodeWindow.DEFAULT_BG_HC_BLACK : (baseTheme === 'vs' ? CodeWindow.DEFAULT_BG_LIGHT : CodeWindow.DEFAULT_BG_DARK);
}
return background;
private doGetUrl(config: object): string {
return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
}
serializeWindowState(): IWindowState {
@@ -667,19 +647,27 @@ export class CodeWindow implements ICodeWindow {
}
// fullscreen gets special treatment
if (this._win.isFullScreen()) {
if (this.isFullScreen()) {
const display = screen.getDisplayMatching(this.getBounds());
return {
const defaultState = defaultWindowState();
const res = {
mode: WindowMode.Fullscreen,
display: display ? display.id : void 0,
// still carry over window dimensions from previous sessions!
width: this.windowState.width,
height: this.windowState.height,
x: this.windowState.x,
y: this.windowState.y
// Still carry over window dimensions from previous sessions
// if we can compute it in fullscreen state.
// does not seem possible in all cases on Linux for example
// (https://github.com/Microsoft/vscode/issues/58218) so we
// fallback to the defaults in that case.
width: this.windowState.width || defaultState.width,
height: this.windowState.height || defaultState.height,
x: this.windowState.x || 0,
y: this.windowState.y || 0
};
return res;
}
const state: IWindowState = Object.create(null);
@@ -825,15 +813,63 @@ export class CodeWindow implements ICodeWindow {
}
toggleFullScreen(): void {
const willBeFullScreen = !this._win.isFullScreen();
this.setFullScreen(!this.isFullScreen());
}
// set fullscreen flag on window
this._win.setFullScreen(willBeFullScreen);
private setFullScreen(fullscreen: boolean): void {
// respect configured menu bar visibility or default to toggle if not set
// Set fullscreen state
if (this.useNativeFullScreen()) {
this.setNativeFullScreen(fullscreen);
} else {
this.setSimpleFullScreen(fullscreen);
}
// Events
this.sendWhenReady(fullscreen ? 'vscode:enterFullScreen' : 'vscode:leaveFullScreen');
// Respect configured menu bar visibility or default to toggle if not set
this.setMenuBarVisibility(this.currentMenuBarVisibility, false);
}
isFullScreen(): boolean {
return this._win.isFullScreen() || this._win.isSimpleFullScreen();
}
private setNativeFullScreen(fullscreen: boolean): void {
if (this._win.isSimpleFullScreen()) {
this._win.setSimpleFullScreen(false);
}
this._win.setFullScreen(fullscreen);
}
private setSimpleFullScreen(fullscreen: boolean): void {
if (this._win.isFullScreen()) {
this._win.setFullScreen(false);
}
this._win.setSimpleFullScreen(fullscreen);
this._win.webContents.focus(); // workaround issue where focus is not going into window
}
private useNativeFullScreen(): boolean {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
if (!windowConfig || typeof windowConfig.nativeFullScreen !== 'boolean') {
return true; // default
}
if (windowConfig.nativeTabs) {
return true; // https://github.com/electron/electron/issues/16142
}
return windowConfig.nativeFullScreen !== false;
}
isMinimized(): boolean {
return this._win.isMinimized();
}
private getMenuBarVisibility(): MenuBarVisibility {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
if (!windowConfig || !windowConfig.menuBarVisibility) {
@@ -874,7 +910,7 @@ export class CodeWindow implements ICodeWindow {
}
private doSetMenuBarVisibility(visibility: MenuBarVisibility): void {
const isFullscreen = this._win.isFullScreen();
const isFullscreen = this.isFullScreen();
switch (visibility) {
case ('default'):
@@ -912,7 +948,11 @@ export class CodeWindow implements ICodeWindow {
break;
case 'Maximize':
default:
this.win.maximize();
if (this.win.isMaximized()) {
this.win.unmaximize();
} else {
this.win.maximize();
}
}
}
@@ -933,9 +973,11 @@ export class CodeWindow implements ICodeWindow {
}
sendWhenReady(channel: string, ...args: any[]): void {
this.ready().then(() => {
if (this.isReady) {
this.send(channel, ...args);
});
} else {
this.ready().then(() => this.send(channel, ...args));
}
}
send(channel: string, ...args: any[]): void {
@@ -1012,12 +1054,12 @@ export class CodeWindow implements ICodeWindow {
}
dispose(): void {
super.dispose();
if (this.showTimeoutHandle) {
clearTimeout(this.showTimeoutHandle);
}
this.toDispose = dispose(this.toDispose);
this._win = null; // Important to dereference the window object to allow for GC
}
}

File diff suppressed because it is too large Load Diff