Files
vscode/src/vs/code/electron-main/windows.ts
T
2016-05-16 16:41:38 +02:00

1275 lines
42 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as path from 'path';
import * as fs from 'fs';
import * as platform from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import * as paths from 'vs/base/common/paths';
import * as arrays from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';
import pkg from 'vs/platform/package';
import { EventEmitter } from 'events';
import { IStorageService } from 'vs/code/electron-main/storage';
import { IPath, VSCodeWindow, ReadyState, IWindowConfiguration, IWindowState as ISingleWindowState, defaultWindowState } from 'vs/code/electron-main/window';
import { ipcMain as ipc, app, screen, crashReporter, BrowserWindow, dialog } from 'electron';
import { ICommandLineArguments, IProcessEnvironment, IEnvironmentService, IParsedPath, parseLineAndColumnAware } from 'vs/code/electron-main/env';
import { ILifecycleService } from 'vs/code/electron-main/lifecycle';
import { ISettingsService } from 'vs/code/electron-main/settings';
import { IUpdateService, IUpdate } from 'vs/code/electron-main/update-manager';
import { ILogService } from './log';
import { ServiceIdentifier, createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
const EventTypes = {
OPEN: 'open',
CLOSE: 'close',
READY: 'ready'
};
enum WindowError {
UNRESPONSIVE,
CRASHED
}
export interface IOpenConfiguration {
cli: ICommandLineArguments;
userEnv?: IProcessEnvironment;
pathsToOpen?: string[];
preferNewWindow?: boolean;
forceNewWindow?: boolean;
forceEmpty?: boolean;
windowToUse?: VSCodeWindow;
diffMode?: boolean;
}
interface IWindowState {
workspacePath?: string;
uiState: ISingleWindowState;
}
interface IWindowsState {
lastActiveWindow?: IWindowState;
lastPluginDevelopmentHostWindow?: IWindowState;
openedFolders: IWindowState[];
}
export interface IOpenedPathsList {
folders: string[];
files: string[];
}
interface ILogEntry {
severity: string;
arguments: any;
}
interface INativeOpenDialogOptions {
pickFolders?: boolean;
pickFiles?: boolean;
}
export const IWindowsService = createDecorator<IWindowsService>('windowsService');
export interface IWindowsService {
serviceId: ServiceIdentifier<any>;
// TODO make proper events
// events
onOpen(clb: (path: IPath) => void): () => void;
onReady(clb: (win: VSCodeWindow) => void): () => void;
onClose(clb: (id: number) => void): () => void;
// methods
ready(initialUserEnv: IProcessEnvironment): void;
reload(win: VSCodeWindow, cli?: ICommandLineArguments): void;
open(openConfig: IOpenConfiguration): VSCodeWindow[];
openPluginDevelopmentHostWindow(openConfig: IOpenConfiguration): void;
openFileFolderPicker(forceNewWindow?: boolean): void;
openFilePicker(forceNewWindow?: boolean): void;
openFolderPicker(forceNewWindow?: boolean): void;
focusLastActive(cli: ICommandLineArguments): VSCodeWindow;
getLastActiveWindow(): VSCodeWindow;
findWindow(workspacePath: string, filePath?: string, extensionDevelopmentPath?: string): VSCodeWindow;
openNewWindow(): void;
sendToFocused(channel: string, ...args: any[]): void;
sendToAll(channel: string, payload: any, windowIdsToIgnore?: number[]): void;
getFocusedWindow(): VSCodeWindow;
getWindowById(windowId: number): VSCodeWindow;
getWindows(): VSCodeWindow[];
getWindowCount(): number;
}
export class WindowsManager implements IWindowsService {
serviceId = IWindowsService;
public static openedPathsListStorageKey = 'openedPathsList';
private static workingDirPickerStorageKey = 'pickerWorkingDir';
private static windowsStateStorageKey = 'windowsState';
private static WINDOWS: VSCodeWindow[] = [];
private eventEmitter = new EventEmitter();
private initialUserEnv: IProcessEnvironment;
private windowsState: IWindowsState;
constructor(
@IInstantiationService private instantiationService: IInstantiationService,
@ILogService private logService: ILogService,
@IStorageService private storageService: IStorageService,
@IEnvironmentService private envService: IEnvironmentService,
@ILifecycleService private lifecycleService: ILifecycleService,
@IUpdateService private updateManager: IUpdateService,
@ISettingsService private settingsManager: ISettingsService
) { }
onOpen(clb: (path: IPath) => void): () => void {
this.eventEmitter.addListener(EventTypes.OPEN, clb);
return () => this.eventEmitter.removeListener(EventTypes.OPEN, clb);
}
onReady(clb: (win: VSCodeWindow) => void): () => void {
this.eventEmitter.addListener(EventTypes.READY, clb);
return () => this.eventEmitter.removeListener(EventTypes.READY, clb);
}
onClose(clb: (id: number) => void): () => void {
this.eventEmitter.addListener(EventTypes.CLOSE, clb);
return () => this.eventEmitter.removeListener(EventTypes.CLOSE, clb);
}
public ready(initialUserEnv: IProcessEnvironment): void {
this.registerListeners();
this.initialUserEnv = initialUserEnv;
this.windowsState = this.storageService.getItem<IWindowsState>(WindowsManager.windowsStateStorageKey) || { openedFolders: [] };
}
private registerListeners(): void {
app.on('activate', (event: Event, hasVisibleWindows: boolean) => {
this.logService.log('App#activate');
// Mac only event: reopen last window when we get activated
if (!hasVisibleWindows) {
// We want to open the previously opened folder, so we dont pass on the path argument
let cliArgWithoutPath = objects.clone(this.envService.cliArgs);
cliArgWithoutPath.pathArguments = [];
this.windowsState.openedFolders = []; // make sure we do not restore too much
this.open({ cli: cliArgWithoutPath });
}
});
let macOpenFiles: string[] = [];
let runningTimeout: number = null;
app.on('open-file', (event: Event, path: string) => {
this.logService.log('App#open-file: ', path);
event.preventDefault();
// Keep in array because more might come!
macOpenFiles.push(path);
// Clear previous handler if any
if (runningTimeout !== null) {
clearTimeout(runningTimeout);
runningTimeout = null;
}
// Handle paths delayed in case more are coming!
runningTimeout = setTimeout(() => {
this.open({ cli: this.envService.cliArgs, pathsToOpen: macOpenFiles, preferNewWindow: true /* dropping on the dock prefers to open in a new window */ });
macOpenFiles = [];
runningTimeout = null;
}, 100);
});
this.settingsManager.onChange((newSettings) => {
this.sendToAll('vscode:optionsChange', JSON.stringify({ globalSettings: newSettings }));
}, this);
ipc.on('vscode:startCrashReporter', (event: any, config: any) => {
this.logService.log('IPC#vscode:startCrashReporter');
crashReporter.start(config);
});
ipc.on('vscode:windowOpen', (event, paths: string[], forceNewWindow?: boolean) => {
this.logService.log('IPC#vscode-windowOpen: ', paths);
if (paths && paths.length) {
this.open({ cli: this.envService.cliArgs, pathsToOpen: paths, forceNewWindow: forceNewWindow });
}
});
ipc.on('vscode:workbenchLoaded', (event, windowId: number) => {
this.logService.log('IPC#vscode-workbenchLoaded');
let win = this.getWindowById(windowId);
if (win) {
win.setReady();
// Event
this.eventEmitter.emit(EventTypes.READY, win);
}
});
ipc.on('vscode:openFilePicker', () => {
this.logService.log('IPC#vscode-openFilePicker');
this.openFilePicker();
});
ipc.on('vscode:openFolderPicker', (event, forceNewWindow?: boolean) => {
this.logService.log('IPC#vscode-openFolderPicker');
this.openFolderPicker(forceNewWindow);
});
ipc.on('vscode:openFileFolderPicker', (event, forceNewWindow?: boolean) => {
this.logService.log('IPC#vscode-openFileFolderPicker');
this.openFileFolderPicker(forceNewWindow);
});
ipc.on('vscode:closeFolder', (event, windowId: number) => {
this.logService.log('IPC#vscode-closeFolder');
let win = this.getWindowById(windowId);
if (win) {
this.open({ cli: this.envService.cliArgs, forceEmpty: true, windowToUse: win });
}
});
ipc.on('vscode:openNewWindow', () => {
this.logService.log('IPC#vscode-openNewWindow');
this.openNewWindow();
});
ipc.on('vscode:reloadWindow', (event, windowId: number) => {
this.logService.log('IPC#vscode:reloadWindow');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
this.reload(vscodeWindow);
}
});
ipc.on('vscode:toggleFullScreen', (event, windowId: number) => {
this.logService.log('IPC#vscode:toggleFullScreen');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
vscodeWindow.toggleFullScreen();
}
});
ipc.on('vscode:setFullScreen', (event, windowId: number, fullscreen: boolean) => {
this.logService.log('IPC#vscode:setFullScreen');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
vscodeWindow.win.setFullScreen(fullscreen);
}
});
ipc.on('vscode:toggleDevTools', (event, windowId: number) => {
this.logService.log('IPC#vscode:toggleDevTools');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
vscodeWindow.win.webContents.toggleDevTools();
}
});
ipc.on('vscode:openDevTools', (event, windowId: number) => {
this.logService.log('IPC#vscode:openDevTools');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
vscodeWindow.win.webContents.openDevTools();
vscodeWindow.win.show();
}
});
ipc.on('vscode:setRepresentedFilename', (event, windowId: number, fileName: string) => {
this.logService.log('IPC#vscode:setRepresentedFilename');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
vscodeWindow.win.setRepresentedFilename(fileName);
}
});
ipc.on('vscode:setMenuBarVisibility', (event, windowId: number, visibility: boolean) => {
this.logService.log('IPC#vscode:setMenuBarVisibility');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
vscodeWindow.win.setMenuBarVisibility(visibility);
}
});
ipc.on('vscode:flashFrame', (event, windowId: number) => {
this.logService.log('IPC#vscode:flashFrame');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
vscodeWindow.win.flashFrame(!vscodeWindow.win.isFocused());
}
});
ipc.on('vscode:focusWindow', (event, windowId: number) => {
this.logService.log('IPC#vscode:focusWindow');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
vscodeWindow.win.focus();
}
});
ipc.on('vscode:setDocumentEdited', (event, windowId: number, edited: boolean) => {
this.logService.log('IPC#vscode:setDocumentEdited');
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow && vscodeWindow.win.isDocumentEdited() !== edited) {
vscodeWindow.win.setDocumentEdited(edited);
}
});
ipc.on('vscode:toggleMenuBar', (event, windowId: number) => {
this.logService.log('IPC#vscode:toggleMenuBar');
// Update in settings
let menuBarHidden = this.storageService.getItem(VSCodeWindow.menuBarHiddenKey, false);
let newMenuBarHidden = !menuBarHidden;
this.storageService.setItem(VSCodeWindow.menuBarHiddenKey, newMenuBarHidden);
// Update across windows
WindowsManager.WINDOWS.forEach(w => w.setMenuBarVisibility(!newMenuBarHidden));
// Inform user if menu bar is now hidden
if (newMenuBarHidden) {
let vscodeWindow = this.getWindowById(windowId);
if (vscodeWindow) {
vscodeWindow.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the **Alt** key."));
}
}
});
ipc.on('vscode:broadcast', (event, windowId: number, target: string, broadcast: { channel: string; payload: any; }) => {
if (broadcast.channel && broadcast.payload) {
this.logService.log('IPC#vscode:broadcast', target, broadcast.channel, broadcast.payload);
// Handle specific events on main side
this.onBroadcast(broadcast.channel, broadcast.payload);
// Send to windows
if (target) {
const otherWindowsWithTarget = WindowsManager.WINDOWS.filter(w => w.id !== windowId && typeof w.openedWorkspacePath === 'string');
const directTargetMatch = otherWindowsWithTarget.filter(w => this.isPathEqual(target, w.openedWorkspacePath));
const parentTargetMatch = otherWindowsWithTarget.filter(w => paths.isEqualOrParent(target, w.openedWorkspacePath));
const targetWindow = directTargetMatch.length ? directTargetMatch[0] : parentTargetMatch[0]; // prefer direct match over parent match
if (targetWindow) {
targetWindow.send('vscode:broadcast', broadcast);
}
} else {
this.sendToAll('vscode:broadcast', broadcast, [windowId]);
}
}
});
ipc.on('vscode:log', (event, logEntry: ILogEntry) => {
let args = [];
try {
let parsed = JSON.parse(logEntry.arguments);
args.push(...Object.getOwnPropertyNames(parsed).map(o => parsed[o]));
} catch (error) {
args.push(logEntry.arguments);
}
console[logEntry.severity].apply(console, args);
});
ipc.on('vscode:closeExtensionHostWindow', (event, extensionDevelopmentPath: string) => {
this.logService.log('IPC#vscode:closeExtensionHostWindow', extensionDevelopmentPath);
const windowOnExtension = this.findWindow(null, null, extensionDevelopmentPath);
if (windowOnExtension) {
windowOnExtension.win.close();
}
});
this.updateManager.on('update-downloaded', (update: IUpdate) => {
this.sendToFocused('vscode:telemetry', { eventName: 'update:downloaded', data: { version: update.version } });
this.sendToAll('vscode:update-downloaded', JSON.stringify({
releaseNotes: update.releaseNotes,
version: update.version,
date: update.date
}));
});
ipc.on('vscode:update-apply', () => {
this.logService.log('IPC#vscode:update-apply');
if (this.updateManager.availableUpdate) {
this.updateManager.availableUpdate.quitAndUpdate();
}
});
this.updateManager.on('update-not-available', (explicit: boolean) => {
this.sendToFocused('vscode:telemetry', { eventName: 'update:notAvailable', data: { explicit } });
if (explicit) {
this.sendToFocused('vscode:update-not-available', '');
}
});
this.updateManager.on('update-available', (url: string) => {
if (url) {
this.sendToFocused('vscode:update-available', url);
}
});
this.lifecycleService.onBeforeQuit(() => {
// 0-1 window open: Do not keep the list but just rely on the active window to be stored
if (WindowsManager.WINDOWS.length < 2) {
this.windowsState.openedFolders = [];
return;
}
// 2-N windows open: Keep a list of windows that are opened on a specific folder to restore it in the next session as needed
this.windowsState.openedFolders = WindowsManager.WINDOWS.filter(w => w.readyState === ReadyState.READY && !!w.openedWorkspacePath && !w.isPluginDevelopmentHost).map(w => {
return <IWindowState>{
workspacePath: w.openedWorkspacePath,
uiState: w.serializeWindowState()
};
});
});
app.on('will-quit', () => {
this.storageService.setItem(WindowsManager.windowsStateStorageKey, this.windowsState);
});
let loggedStartupTimes = false;
this.onReady(window => {
if (loggedStartupTimes) {
return; // only for the first window
}
loggedStartupTimes = true;
window.send('vscode:telemetry', { eventName: 'startupTime', data: { ellapsed: Date.now() - global.vscodeStart } });
});
}
private onBroadcast(event: string, payload: any): void {
// Theme changes
if (event === 'vscode:changeTheme' && typeof payload === 'string') {
this.storageService.setItem(VSCodeWindow.themeStorageKey, payload);
}
}
public reload(win: VSCodeWindow, cli?: ICommandLineArguments): void {
// Only reload when the window has not vetoed this
this.lifecycleService.unload(win).done((veto) => {
if (!veto) {
win.reload(cli);
}
});
}
public open(openConfig: IOpenConfiguration): VSCodeWindow[] {
let iPathsToOpen: IPath[];
let usedWindows: VSCodeWindow[] = [];
// Find paths from provided paths if any
if (openConfig.pathsToOpen && openConfig.pathsToOpen.length > 0) {
iPathsToOpen = openConfig.pathsToOpen.map((pathToOpen) => {
let iPath = this.toIPath(pathToOpen, false, openConfig.cli && openConfig.cli.gotoLineMode);
// Warn if the requested path to open does not exist
if (!iPath) {
let options = {
title: this.envService.product.nameLong,
type: 'info',
buttons: [nls.localize('ok', "OK")],
message: nls.localize('pathNotExistTitle', "Path does not exist"),
detail: nls.localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", pathToOpen),
noLink: true
};
let activeWindow = BrowserWindow.getFocusedWindow();
if (activeWindow) {
dialog.showMessageBox(activeWindow, options);
} else {
dialog.showMessageBox(options);
}
}
return iPath;
});
// get rid of nulls
iPathsToOpen = arrays.coalesce(iPathsToOpen);
if (iPathsToOpen.length === 0) {
return null; // indicate to outside that open failed
}
}
// Check for force empty
else if (openConfig.forceEmpty) {
iPathsToOpen = [Object.create(null)];
}
// Otherwise infer from command line arguments
else {
let ignoreFileNotFound = openConfig.cli.pathArguments.length > 0; // we assume the user wants to create this file from command line
iPathsToOpen = this.cliToPaths(openConfig.cli, ignoreFileNotFound);
}
let filesToOpen: IPath[] = [];
let filesToDiff: IPath[] = [];
let foldersToOpen = iPathsToOpen.filter((iPath) => iPath.workspacePath && !iPath.filePath && !iPath.installExtensionPath);
let emptyToOpen = iPathsToOpen.filter((iPath) => !iPath.workspacePath && !iPath.filePath && !iPath.installExtensionPath);
let extensionsToInstall = iPathsToOpen.filter((iPath) => iPath.installExtensionPath).map(ipath => ipath.filePath);
let filesToCreate = iPathsToOpen.filter((iPath) => !!iPath.filePath && iPath.createFilePath && !iPath.installExtensionPath);
// Diff mode needs special care
let candidates = iPathsToOpen.filter((iPath) => !!iPath.filePath && !iPath.createFilePath && !iPath.installExtensionPath);
if (openConfig.diffMode) {
if (candidates.length === 2) {
filesToDiff = candidates;
} else {
emptyToOpen = [Object.create(null)]; // improper use of diffMode, open empty
}
foldersToOpen = []; // diff is always in empty workspace
filesToCreate = []; // diff ignores other files that do not exist
} else {
filesToOpen = candidates;
}
let configuration: IWindowConfiguration;
// Handle files to open/diff or to create when we dont open a folder
if (!foldersToOpen.length && (filesToOpen.length > 0 || filesToCreate.length > 0 || filesToDiff.length > 0 || extensionsToInstall.length > 0)) {
// Let the user settings override how files are open in a new window or same window unless we are forced
let openFilesInNewWindow: boolean;
if (openConfig.forceNewWindow) {
openFilesInNewWindow = true;
} else {
openFilesInNewWindow = openConfig.preferNewWindow;
if (openFilesInNewWindow && !openConfig.cli.extensionDevelopmentPath) { // can be overriden via settings (not for PDE though!)
openFilesInNewWindow = this.settingsManager.getValue('window.openFilesInNewWindow', openFilesInNewWindow);
}
}
// Open Files in last instance if any and flag tells us so
let lastActiveWindow = this.getLastActiveWindow();
if (!openFilesInNewWindow && lastActiveWindow) {
lastActiveWindow.focus();
lastActiveWindow.ready().then((readyWindow) => {
readyWindow.send('vscode:openFiles', {
filesToOpen: filesToOpen,
filesToCreate: filesToCreate,
filesToDiff: filesToDiff
});
if (extensionsToInstall.length) {
readyWindow.send('vscode:installExtensions', { extensionsToInstall });
}
});
usedWindows.push(lastActiveWindow);
}
// Otherwise open instance with files
else {
configuration = this.toConfiguration(openConfig.userEnv || this.initialUserEnv, openConfig.cli, null, filesToOpen, filesToCreate, filesToDiff, extensionsToInstall);
let browserWindow = this.openInBrowserWindow(configuration, true /* new window */);
usedWindows.push(browserWindow);
openConfig.forceNewWindow = true; // any other folders to open must open in new window then
}
}
// Handle folders to open
let openInNewWindow = openConfig.preferNewWindow || openConfig.forceNewWindow;
if (foldersToOpen.length > 0) {
// Check for existing instances
let windowsOnWorkspacePath = arrays.coalesce(foldersToOpen.map((iPath) => this.findWindow(iPath.workspacePath)));
if (windowsOnWorkspacePath.length > 0) {
let browserWindow = windowsOnWorkspacePath[0];
browserWindow.focus(); // just focus one of them
browserWindow.ready().then((readyWindow) => {
readyWindow.send('vscode:openFiles', {
filesToOpen: filesToOpen,
filesToCreate: filesToCreate,
filesToDiff: filesToDiff
});
if (extensionsToInstall.length) {
readyWindow.send('vscode:installExtensions', { extensionsToInstall });
}
});
usedWindows.push(browserWindow);
// Reset these because we handled them
filesToOpen = [];
filesToCreate = [];
filesToDiff = [];
extensionsToInstall = [];
openInNewWindow = true; // any other folders to open must open in new window then
}
// Open remaining ones
foldersToOpen.forEach((folderToOpen) => {
if (windowsOnWorkspacePath.some((win) => this.isPathEqual(win.openedWorkspacePath, folderToOpen.workspacePath))) {
return; // ignore folders that are already open
}
configuration = this.toConfiguration(openConfig.userEnv || this.initialUserEnv, openConfig.cli, folderToOpen.workspacePath, filesToOpen, filesToCreate, filesToDiff, extensionsToInstall);
let browserWindow = this.openInBrowserWindow(configuration, openInNewWindow, openInNewWindow ? void 0 : openConfig.windowToUse);
usedWindows.push(browserWindow);
// Reset these because we handled them
filesToOpen = [];
filesToCreate = [];
filesToDiff = [];
extensionsToInstall = [];
openInNewWindow = true; // any other folders to open must open in new window then
});
}
// Handle empty
if (emptyToOpen.length > 0) {
emptyToOpen.forEach(() => {
let configuration = this.toConfiguration(openConfig.userEnv || this.initialUserEnv, openConfig.cli);
let browserWindow = this.openInBrowserWindow(configuration, openInNewWindow, openInNewWindow ? void 0 : openConfig.windowToUse);
usedWindows.push(browserWindow);
openInNewWindow = true; // any other folders to open must open in new window then
});
}
// Remember in recent document list
iPathsToOpen.forEach((iPath) => {
if (iPath.filePath || iPath.workspacePath) {
app.addRecentDocument(iPath.filePath || iPath.workspacePath);
}
});
// Emit events
iPathsToOpen.forEach((iPath) => this.eventEmitter.emit(EventTypes.OPEN, iPath));
return arrays.distinct(usedWindows);
}
public openPluginDevelopmentHostWindow(openConfig: IOpenConfiguration): void {
// Reload an existing plugin development host window on the same path
// We currently do not allow more than one extension development window
// on the same plugin path.
let res = WindowsManager.WINDOWS.filter((w) => w.config && this.isPathEqual(w.config.extensionDevelopmentPath, openConfig.cli.extensionDevelopmentPath));
if (res && res.length === 1) {
this.reload(res[0], openConfig.cli);
res[0].focus(); // make sure it gets focus and is restored
return;
}
// Fill in previously opened workspace unless an explicit path is provided and we are not unit testing
if (openConfig.cli.pathArguments.length === 0 && !openConfig.cli.extensionTestsPath) {
let workspaceToOpen = this.windowsState.lastPluginDevelopmentHostWindow && this.windowsState.lastPluginDevelopmentHostWindow.workspacePath;
if (workspaceToOpen) {
openConfig.cli.pathArguments = [workspaceToOpen];
}
}
// Make sure we are not asked to open a path that is already opened
if (openConfig.cli.pathArguments.length > 0) {
res = WindowsManager.WINDOWS.filter((w) => w.openedWorkspacePath && openConfig.cli.pathArguments.indexOf(w.openedWorkspacePath) >= 0);
if (res.length) {
openConfig.cli.pathArguments = [];
}
}
// Open it
this.open({ cli: openConfig.cli, forceNewWindow: true, forceEmpty: openConfig.cli.pathArguments.length === 0 });
}
private toConfiguration(userEnv: IProcessEnvironment, cli: ICommandLineArguments, workspacePath?: string, filesToOpen?: IPath[], filesToCreate?: IPath[], filesToDiff?: IPath[], extensionsToInstall?: string[]): IWindowConfiguration {
let configuration: IWindowConfiguration = objects.mixin({}, cli); // inherit all properties from CLI
configuration.execPath = process.execPath;
configuration.workspacePath = workspacePath;
configuration.filesToOpen = filesToOpen;
configuration.filesToCreate = filesToCreate;
configuration.filesToDiff = filesToDiff;
configuration.extensionsToInstall = extensionsToInstall;
configuration.appName = this.envService.product.nameLong;
configuration.applicationName = this.envService.product.applicationName;
configuration.darwinBundleIdentifier = this.envService.product.darwinBundleIdentifier;
configuration.appRoot = this.envService.appRoot;
configuration.version = pkg.version;
configuration.commitHash = this.envService.product.commit;
configuration.appSettingsHome = this.envService.appSettingsHome;
configuration.appSettingsPath = this.envService.appSettingsPath;
configuration.appKeybindingsPath = this.envService.appKeybindingsPath;
configuration.userExtensionsHome = this.envService.userExtensionsHome;
configuration.extensionTips = this.envService.product.extensionTips;
configuration.mainIPCHandle = this.envService.mainIPCHandle;
configuration.sharedIPCHandle = this.envService.sharedIPCHandle;
configuration.isBuilt = this.envService.isBuilt;
configuration.crashReporter = this.envService.product.crashReporter;
configuration.extensionsGallery = this.envService.product.extensionsGallery;
configuration.welcomePage = this.envService.product.welcomePage;
configuration.productDownloadUrl = this.envService.product.downloadUrl;
configuration.releaseNotesUrl = this.envService.product.releaseNotesUrl;
configuration.licenseUrl = this.envService.product.licenseUrl;
configuration.updateFeedUrl = this.updateManager.feedUrl;
configuration.updateChannel = this.updateManager.channel;
configuration.aiConfig = this.envService.product.aiConfig;
configuration.sendASmile = this.envService.product.sendASmile;
configuration.enableTelemetry = this.envService.product.enableTelemetry;
configuration.userEnv = userEnv;
const recents = this.getRecentlyOpenedPaths(workspacePath, filesToOpen);
configuration.recentFiles = recents.files;
configuration.recentFolders = recents.folders;
return configuration;
}
private getRecentlyOpenedPaths(workspacePath?: string, filesToOpen?: IPath[]): IOpenedPathsList {
let files: string[];
let folders: string[];
// Get from storage
let storedRecents = this.storageService.getItem<IOpenedPathsList>(WindowsManager.openedPathsListStorageKey);
if (storedRecents) {
files = storedRecents.files || [];
folders = storedRecents.folders || [];
} else {
files = [];
folders = [];
}
// Add currently files to open to the beginning if any
if (filesToOpen) {
files.unshift(...filesToOpen.map(f => f.filePath));
}
// Add current workspace path to beginning if set
if (workspacePath) {
folders.unshift(workspacePath);
}
// Clear those dupes
files = arrays.distinct(files);
folders = arrays.distinct(folders);
// Make sure it is bounded
files = files.slice(0, 10);
folders = folders.slice(0, 10);
return { files, folders };
}
private toIPath(anyPath: string, ignoreFileNotFound?: boolean, gotoLineMode?: boolean): IPath {
if (!anyPath) {
return null;
}
let parsedPath: IParsedPath;
if (gotoLineMode) {
parsedPath = parseLineAndColumnAware(anyPath);
anyPath = parsedPath.path;
}
let candidate = path.normalize(anyPath);
try {
let candidateStat = fs.statSync(candidate);
if (candidateStat) {
return candidateStat.isFile() ?
{
filePath: candidate,
lineNumber: gotoLineMode ? parsedPath.line : void 0,
columnNumber: gotoLineMode ? parsedPath.column : void 0,
installExtensionPath: /\.vsix$/i.test(candidate)
} :
{ workspacePath: candidate };
}
} catch (error) {
if (ignoreFileNotFound) {
return { filePath: candidate, createFilePath: true }; // assume this is a file that does not yet exist
}
}
return null;
}
private cliToPaths(cli: ICommandLineArguments, ignoreFileNotFound?: boolean): IPath[] {
// Check for pass in candidate or last opened path
let candidates: string[] = [];
if (cli.pathArguments.length > 0) {
candidates = cli.pathArguments;
}
// No path argument, check settings for what to do now
else {
let reopenFolders = this.settingsManager.getValue('window.reopenFolders', 'one');
let lastActiveFolder = this.windowsState.lastActiveWindow && this.windowsState.lastActiveWindow.workspacePath;
// Restore all
if (reopenFolders === 'all') {
let lastOpenedFolders = this.windowsState.openedFolders.map(o => o.workspacePath);
// If we have a last active folder, move it to the end
if (lastActiveFolder) {
lastOpenedFolders.splice(lastOpenedFolders.indexOf(lastActiveFolder), 1);
lastOpenedFolders.push(lastActiveFolder);
}
candidates.push(...lastOpenedFolders);
}
// Restore last active
else if (lastActiveFolder && (reopenFolders === 'one' || reopenFolders !== 'none')) {
candidates.push(lastActiveFolder);
}
}
let iPaths = candidates.map((candidate) => this.toIPath(candidate, ignoreFileNotFound, cli.gotoLineMode)).filter((path) => !!path);
if (iPaths.length > 0) {
return iPaths;
}
// No path provided, return empty to open empty
return [Object.create(null)];
}
private openInBrowserWindow(configuration: IWindowConfiguration, forceNewWindow?: boolean, windowToUse?: VSCodeWindow): VSCodeWindow {
let vscodeWindow: VSCodeWindow;
if (!forceNewWindow) {
vscodeWindow = windowToUse || this.getLastActiveWindow();
if (vscodeWindow) {
vscodeWindow.focus();
}
}
// New window
if (!vscodeWindow) {
vscodeWindow = this.instantiationService.createInstance(VSCodeWindow, {
state: this.getNewWindowState(configuration),
extensionDevelopmentPath: configuration.extensionDevelopmentPath
});
WindowsManager.WINDOWS.push(vscodeWindow);
// Window Events
vscodeWindow.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own
vscodeWindow.win.webContents.on('devtools-reload-page', () => this.reload(vscodeWindow));
vscodeWindow.win.webContents.on('crashed', () => this.onWindowError(vscodeWindow, WindowError.CRASHED));
vscodeWindow.win.on('unresponsive', () => this.onWindowError(vscodeWindow, WindowError.UNRESPONSIVE));
vscodeWindow.win.on('close', () => this.onBeforeWindowClose(vscodeWindow));
vscodeWindow.win.on('closed', () => this.onWindowClosed(vscodeWindow));
// Lifecycle
this.lifecycleService.registerWindow(vscodeWindow);
}
// Existing window
else {
// Some configuration things get inherited if the window is being reused and we are
// in plugin development host mode. These options are all development related.
let currentWindowConfig = vscodeWindow.config;
if (!configuration.extensionDevelopmentPath && currentWindowConfig && !!currentWindowConfig.extensionDevelopmentPath) {
configuration.extensionDevelopmentPath = currentWindowConfig.extensionDevelopmentPath;
configuration.verboseLogging = currentWindowConfig.verboseLogging;
configuration.logExtensionHostCommunication = currentWindowConfig.logExtensionHostCommunication;
configuration.debugBrkFileWatcherPort = currentWindowConfig.debugBrkFileWatcherPort;
configuration.debugBrkExtensionHost = currentWindowConfig.debugBrkExtensionHost;
configuration.debugExtensionHostPort = currentWindowConfig.debugExtensionHostPort;
configuration.extensionsHomePath = currentWindowConfig.extensionsHomePath;
}
}
// Only load when the window has not vetoed this
this.lifecycleService.unload(vscodeWindow).done((veto) => {
if (!veto) {
// Load it
vscodeWindow.load(configuration);
}
});
return vscodeWindow;
}
private getNewWindowState(configuration: IWindowConfiguration): ISingleWindowState {
// plugin development host Window - load from stored settings if any
if (!!configuration.extensionDevelopmentPath && this.windowsState.lastPluginDevelopmentHostWindow) {
return this.windowsState.lastPluginDevelopmentHostWindow.uiState;
}
// Known Folder - load from stored settings if any
if (configuration.workspacePath) {
let stateForWorkspace = this.windowsState.openedFolders.filter(o => this.isPathEqual(o.workspacePath, configuration.workspacePath)).map(o => o.uiState);
if (stateForWorkspace.length) {
return stateForWorkspace[0];
}
}
// First Window
let lastActive = this.getLastActiveWindow();
if (!lastActive && this.windowsState.lastActiveWindow) {
return this.windowsState.lastActiveWindow.uiState;
}
//
// In any other case, we do not have any stored settings for the window state, so we come up with something smart
//
// We want the new window to open on the same display that the last active one is in
let displayToUse: Electron.Display;
let displays = screen.getAllDisplays();
// Single Display
if (displays.length === 1) {
displayToUse = displays[0];
}
// Multi Display
else {
// on mac there is 1 menu per window so we need to use the monitor where the cursor currently is
if (platform.isMacintosh) {
let cursorPoint = screen.getCursorScreenPoint();
displayToUse = screen.getDisplayNearestPoint(cursorPoint);
}
// if we have a last active window, use that display for the new window
if (!displayToUse && lastActive) {
displayToUse = screen.getDisplayMatching(lastActive.getBounds());
}
// fallback to first display
if (!displayToUse) {
displayToUse = displays[0];
}
}
let defaultState = defaultWindowState();
defaultState.x = displayToUse.bounds.x + (displayToUse.bounds.width / 2) - (defaultState.width / 2);
defaultState.y = displayToUse.bounds.y + (displayToUse.bounds.height / 2) - (defaultState.height / 2);
return this.ensureNoOverlap(defaultState);
}
private ensureNoOverlap(state: ISingleWindowState): ISingleWindowState {
if (WindowsManager.WINDOWS.length === 0) {
return state;
}
let existingWindowBounds = WindowsManager.WINDOWS.map((win) => win.getBounds());
while (existingWindowBounds.some((b) => b.x === state.x || b.y === state.y)) {
state.x += 30;
state.y += 30;
}
return state;
}
public openFileFolderPicker(forceNewWindow?: boolean): void {
this.doPickAndOpen({ pickFolders: true, pickFiles: true }, forceNewWindow);
}
public openFilePicker(forceNewWindow?: boolean): void {
this.doPickAndOpen({ pickFiles: true }, forceNewWindow);
}
public openFolderPicker(forceNewWindow?: boolean): void {
this.doPickAndOpen({ pickFolders: true }, forceNewWindow);
}
private doPickAndOpen(options: INativeOpenDialogOptions, forceNewWindow?: boolean): void {
this.getFileOrFolderPaths(options, (paths: string[]) => {
if (paths && paths.length) {
this.open({ cli: this.envService.cliArgs, pathsToOpen: paths, forceNewWindow });
}
});
}
private getFileOrFolderPaths(options: INativeOpenDialogOptions, clb: (paths: string[]) => void): void {
let workingDir = this.storageService.getItem<string>(WindowsManager.workingDirPickerStorageKey);
let focussedWindow = this.getFocusedWindow();
let pickerProperties: string[];
if (options.pickFiles && options.pickFolders) {
pickerProperties = ['multiSelections', 'openDirectory', 'openFile', 'createDirectory'];
} else {
pickerProperties = ['multiSelections', options.pickFolders ? 'openDirectory' : 'openFile', 'createDirectory'];
}
dialog.showOpenDialog(focussedWindow && focussedWindow.win, {
defaultPath: workingDir,
properties: pickerProperties
}, (paths) => {
if (paths && paths.length > 0) {
// Remember path in storage for next time
this.storageService.setItem(WindowsManager.workingDirPickerStorageKey, path.dirname(paths[0]));
// Return
clb(paths);
} else {
clb(void (0));
}
});
}
public focusLastActive(cli: ICommandLineArguments): VSCodeWindow {
let lastActive = this.getLastActiveWindow();
if (lastActive) {
lastActive.focus();
return lastActive;
}
// No window - open new one
this.windowsState.openedFolders = []; // make sure we do not open too much
const res = this.open({ cli: cli });
return res && res[0];
}
public getLastActiveWindow(): VSCodeWindow {
if (WindowsManager.WINDOWS.length) {
let lastFocussedDate = Math.max.apply(Math, WindowsManager.WINDOWS.map((w) => w.lastFocusTime));
let res = WindowsManager.WINDOWS.filter((w) => w.lastFocusTime === lastFocussedDate);
if (res && res.length) {
return res[0];
}
}
return null;
}
public findWindow(workspacePath: string, filePath?: string, extensionDevelopmentPath?: string): VSCodeWindow {
if (WindowsManager.WINDOWS.length) {
// Sort the last active window to the front of the array of windows to test
let windowsToTest = WindowsManager.WINDOWS.slice(0);
let lastActiveWindow = this.getLastActiveWindow();
if (lastActiveWindow) {
windowsToTest.splice(windowsToTest.indexOf(lastActiveWindow), 1);
windowsToTest.unshift(lastActiveWindow);
}
// Find it
let res = windowsToTest.filter((w) => {
// match on workspace
if (typeof w.openedWorkspacePath === 'string' && (this.isPathEqual(w.openedWorkspacePath, workspacePath))) {
return true;
}
// match on file
if (typeof w.openedFilePath === 'string' && this.isPathEqual(w.openedFilePath, filePath)) {
return true;
}
// match on file path
if (typeof w.openedWorkspacePath === 'string' && filePath && paths.isEqualOrParent(filePath, w.openedWorkspacePath)) {
return true;
}
// match on extension development path
if (typeof extensionDevelopmentPath === 'string' && w.extensionDevelopmentPath === extensionDevelopmentPath) {
return true;
}
return false;
});
if (res && res.length) {
return res[0];
}
}
return null;
}
public openNewWindow(): void {
this.open({ cli: this.envService.cliArgs, forceNewWindow: true, forceEmpty: true });
}
public sendToFocused(channel: string, ...args: any[]): void {
const focusedWindow = this.getFocusedWindow() || this.getLastActiveWindow();
if (focusedWindow) {
focusedWindow.sendWhenReady(channel, ...args);
}
}
public sendToAll(channel: string, payload: any, windowIdsToIgnore?: number[]): void {
WindowsManager.WINDOWS.forEach((w) => {
if (windowIdsToIgnore && windowIdsToIgnore.indexOf(w.id) >= 0) {
return; // do not send if we are instructed to ignore it
}
w.sendWhenReady(channel, payload);
});
}
public getFocusedWindow(): VSCodeWindow {
let win = BrowserWindow.getFocusedWindow();
if (win) {
return this.getWindowById(win.id);
}
return null;
}
public getWindowById(windowId: number): VSCodeWindow {
let res = WindowsManager.WINDOWS.filter((w) => w.id === windowId);
if (res && res.length === 1) {
return res[0];
}
return null;
}
public getWindows(): VSCodeWindow[] {
return WindowsManager.WINDOWS;
}
public getWindowCount(): number {
return WindowsManager.WINDOWS.length;
}
private onWindowError(vscodeWindow: VSCodeWindow, error: WindowError): void {
console.error(error === WindowError.CRASHED ? '[VS Code]: render process crashed!' : '[VS Code]: detected unresponsive');
// Unresponsive
if (error === WindowError.UNRESPONSIVE) {
dialog.showMessageBox(vscodeWindow.win, {
title: this.envService.product.nameLong,
type: 'warning',
buttons: [nls.localize('reopen', "Reopen"), nls.localize('wait', "Keep Waiting"), nls.localize('close', "Close")],
message: nls.localize('appStalled', "The window is no longer responding"),
detail: nls.localize('appStalledDetail', "You can reopen or close the window or keep waiting."),
noLink: true
}, (result) => {
if (result === 0) {
vscodeWindow.reload();
} else if (result === 2) {
this.onBeforeWindowClose(vscodeWindow); // 'close' event will not be fired on destroy(), so run it manually
vscodeWindow.win.destroy(); // make sure to destroy the window as it is unresponsive
}
});
}
// Crashed
else {
dialog.showMessageBox(vscodeWindow.win, {
title: this.envService.product.nameLong,
type: 'warning',
buttons: [nls.localize('reopen', "Reopen"), nls.localize('close', "Close")],
message: nls.localize('appCrashed', "The window has crashed"),
detail: nls.localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."),
noLink: true
}, (result) => {
if (result === 0) {
vscodeWindow.reload();
} else if (result === 1) {
this.onBeforeWindowClose(vscodeWindow); // 'close' event will not be fired on destroy(), so run it manually
vscodeWindow.win.destroy(); // make sure to destroy the window as it has crashed
}
});
}
}
private onBeforeWindowClose(win: VSCodeWindow): void {
if (win.readyState !== ReadyState.READY) {
return; // only persist windows that are fully loaded
}
// On Window close, update our stored state of this window
let state: IWindowState = { workspacePath: win.openedWorkspacePath, uiState: win.serializeWindowState() };
if (win.isPluginDevelopmentHost) {
this.windowsState.lastPluginDevelopmentHostWindow = state;
} else {
this.windowsState.lastActiveWindow = state;
this.windowsState.openedFolders.forEach(o => {
if (this.isPathEqual(o.workspacePath, win.openedWorkspacePath)) {
o.uiState = state.uiState;
}
});
}
}
private onWindowClosed(win: VSCodeWindow): void {
// Tell window
win.dispose();
// Remove from our list so that Electron can clean it up
let index = WindowsManager.WINDOWS.indexOf(win);
WindowsManager.WINDOWS.splice(index, 1);
// Emit
this.eventEmitter.emit(EventTypes.CLOSE, win.id);
}
private isPathEqual(pathA: string, pathB: string): boolean {
if (pathA === pathB) {
return true;
}
if (!pathA || !pathB) {
return false;
}
pathA = path.normalize(pathA);
pathB = path.normalize(pathB);
if (pathA === pathB) {
return true;
}
if (!platform.isLinux) {
pathA = pathA.toLowerCase();
pathB = pathB.toLowerCase();
}
return pathA === pathB;
}
}