Merge VS Code 1.21 source code (#1067)

* Initial VS Code 1.21 file copy with patches

* A few more merges

* Post npm install

* Fix batch of build breaks

* Fix more build breaks

* Fix more build errors

* Fix more build breaks

* Runtime fixes 1

* Get connection dialog working with some todos

* Fix a few packaging issues

* Copy several node_modules to package build to fix loader issues

* Fix breaks from master

* A few more fixes

* Make tests pass

* First pass of license header updates

* Second pass of license header updates

* Fix restore dialog issues

* Remove add additional themes menu items

* fix select box issues where the list doesn't show up

* formatting

* Fix editor dispose issue

* Copy over node modules to correct location on all platforms
This commit is contained in:
Karl Burtram
2018-04-04 15:27:51 -07:00
committed by GitHub
parent 5fba3e31b4
commit dafb780987
9412 changed files with 141255 additions and 98813 deletions

View File

@@ -5,7 +5,7 @@
'use strict';
import { app, ipcMain as ipc, BrowserWindow, dialog } from 'electron';
import { app, ipcMain as ipc, BrowserWindow } from 'electron';
import * as platform from 'vs/base/common/platform';
import { WindowsManager } from 'vs/code/electron-main/windows';
import { IWindowsService, OpenContext } from 'vs/platform/windows/common/windows';
@@ -16,7 +16,6 @@ import { CodeMenu } from 'vs/code/electron-main/menus';
import { getShellEnvironment } from 'vs/code/node/shellEnv';
import { IUpdateService } from 'vs/platform/update/common/update';
import { UpdateChannel } from 'vs/platform/update/common/updateIpc';
import { UpdateService } from 'vs/platform/update/electron-main/updateService';
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';
@@ -51,13 +50,18 @@ import { KeyboardLayoutMonitor } from 'vs/code/electron-main/keyboard';
import URI from 'vs/base/common/uri';
import { WorkspacesChannel } from 'vs/platform/workspaces/common/workspacesIpc';
import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces';
import { dirname, join } from 'path';
import { touch } from 'vs/base/node/pfs';
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 { IssueService } from 'vs/platform/issue/electron-main/issueService';
import { LogLevelSetterChannel } from 'vs/platform/log/common/logIpc';
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
export class CodeApplication {
private static readonly APP_ICON_REFRESH_KEY = 'macOSAppIconRefresh3';
private static readonly MACHINE_ID_KEY = 'telemetry.machineId';
private toDispose: IDisposable[];
@@ -87,26 +91,8 @@ export class CodeApplication {
private registerListeners(): void {
// We handle uncaught exceptions here to prevent electron from opening a dialog to the user
process.on('uncaughtException', (err: any) => {
if (err) {
// take only the message and stack property
const friendlyError = {
message: err.message,
stack: err.stack
};
// handle on client side
if (this.windowsMainService) {
this.windowsMainService.sendToFocused('vscode:reportError', JSON.stringify(friendlyError));
}
}
this.logService.error(`[uncaught exception in main]: ${err}`);
if (err.stack) {
this.logService.error(err.stack);
}
});
setUnexpectedErrorHandler(err => this.onUnexpectedError(err));
process.on('uncaughtException', err => this.onUnexpectedError(err));
app.on('will-quit', () => {
this.logService.trace('App#will-quit: disposing resources');
@@ -129,8 +115,16 @@ export class CodeApplication {
}
});
const isValidWebviewSource = (source: string) =>
!source || (URI.parse(source.toLowerCase()).toString() as any).startsWith(URI.file(this.environmentService.appRoot.toLowerCase()).toString());
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());
};
app.on('web-contents-created', (_event: any, contents) => {
contents.on('will-attach-webview', (event: Electron.Event, webPreferences, params) => {
@@ -229,6 +223,27 @@ export class CodeApplication {
});
}
private onUnexpectedError(err: Error): void {
if (err) {
// take only the message and stack property
const friendlyError = {
message: err.message,
stack: err.stack
};
// handle on client side
if (this.windowsMainService) {
this.windowsMainService.sendToFocused('vscode:reportError', JSON.stringify(friendlyError));
}
}
this.logService.error(`[uncaught exception in main]: ${err}`);
if (err.stack) {
this.logService.error(err.stack);
}
}
private onBroadcast(event: string, payload: any): void {
// Theme changes
@@ -262,7 +277,7 @@ export class CodeApplication {
this.logService.trace(`Resolved machine identifier: ${machineId}`);
// Spawn shared process
this.sharedProcess = new SharedProcess(this.environmentService, machineId, this.userEnv);
this.sharedProcess = new SharedProcess(this.environmentService, machineId, this.userEnv, this.logService);
this.toDispose.push(this.sharedProcess);
this.sharedProcessClient = this.sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main'));
@@ -299,10 +314,18 @@ export class CodeApplication {
private initServices(machineId: string): IInstantiationService {
const services = new ServiceCollection();
services.set(IUpdateService, new SyncDescriptor(UpdateService));
if (process.platform === 'win32') {
services.set(IUpdateService, new SyncDescriptor(Win32UpdateService));
} else if (process.platform === 'linux') {
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(ILaunchService, new SyncDescriptor(LaunchService));
services.set(IIssueService, new SyncDescriptor(IssueService, machineId));
// Telemtry
if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) {
@@ -347,6 +370,10 @@ export class CodeApplication {
const urlChannel = appInstantiationService.createInstance(URLChannel, urlService);
this.electronIpcServer.registerChannel('url', urlChannel);
const issueService = accessor.get(IIssueService);
const issueChannel = new IssueChannel(issueService);
this.electronIpcServer.registerChannel('issue', issueChannel);
const workspacesService = accessor.get(IWorkspacesMainService);
const workspacesChannel = appInstantiationService.createInstance(WorkspacesChannel, workspacesService);
this.electronIpcServer.registerChannel('workspaces', workspacesChannel);
@@ -356,6 +383,11 @@ export class CodeApplication {
this.electronIpcServer.registerChannel('windows', windowsChannel);
this.sharedProcessClient.done(client => client.registerChannel('windows', windowsChannel));
// Log level management
const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService));
this.electronIpcServer.registerChannel('loglevel', logLevelChannel);
this.sharedProcessClient.done(client => client.registerChannel('loglevel', logLevelChannel));
// Lifecycle
this.lifecycleService.ready();
@@ -376,6 +408,7 @@ export class CodeApplication {
private afterWindowOpen(accessor: ServicesAccessor): void {
const appInstantiationService = accessor.get(IInstantiationService);
const windowsMainService = accessor.get(IWindowsMainService);
let windowsMutex: Mutex = null;
if (platform.isWindows) {
@@ -387,7 +420,7 @@ export class CodeApplication {
this.toDispose.push({ dispose: () => windowsMutex.release() });
} catch (e) {
if (!this.environmentService.isBuilt) {
dialog.showMessageBox({
windowsMainService.showMessageBox({
title: product.nameLong,
type: 'warning',
message: 'Failed to load windows-mutex!',
@@ -403,7 +436,7 @@ export class CodeApplication {
<any>require.__$__nodeRequire('windows-foreground-love');
} catch (e) {
if (!this.environmentService.isBuilt) {
dialog.showMessageBox({
windowsMainService.showMessageBox({
title: product.nameLong,
type: 'warning',
message: 'Failed to load windows-foreground-love!',
@@ -423,20 +456,6 @@ export class CodeApplication {
// Start shared process here
this.sharedProcess.spawn();
// Helps application icon refresh after an update with new icon is installed (macOS)
// TODO@Ben remove after a couple of releases
if (platform.isMacintosh) {
if (!this.stateService.getItem(CodeApplication.APP_ICON_REFRESH_KEY)) {
this.stateService.setItem(CodeApplication.APP_ICON_REFRESH_KEY, true);
// 'exe' => /Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Electron
const appPath = dirname(dirname(dirname(app.getPath('exe'))));
const infoPlistPath = join(appPath, 'Contents', 'Info.plist');
touch(appPath).done(null, error => { /* ignore */ });
touch(infoPlistPath).done(null, error => { /* ignore */ });
}
}
}
private dispose(): void {

View File

@@ -1,125 +0,0 @@
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
body {
font-family: "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback";
font-size: 10pt;
background-color: #F3F3F3;
}
#main {
box-sizing: border-box;
padding: 10px;
}
h1 {
margin: 0;
padding: 10px 0;
font-size: 16px;
background-color: #555;
color: #f0f0f0;
text-align: center;
}
#form {
margin-top: 10px;
}
#username,
#password {
padding: 6px 10px;
font-size: 12px;
box-sizing: border-box;
width: 100%;
}
#buttons {
text-align: center;
}
p {
margin: 6px 0;
}
input {
font-family: "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback" !important;
}
</style>
</head>
<body>
<h1 id="title"></h1>
<section id="main">
<p id="message"></p>
<form id="form">
<p><input type="text" id="username" placeholder="Username" required/></p>
<p><input type="password" id="password" placeholder="Password" required/></p>
<p id="buttons">
<input id="ok" type="submit" value="OK" />
<input id="cancel" type="button" value="Cancel" />
</p>
</form>
</section>
</body>
<script>
const electron = require('electron');
const shell = electron.shell;
const ipc = electron.ipcRenderer;
const remote = electron.remote;
const currentWindow = remote.getCurrentWindow();
function promptForCredentials(data) {
return new Promise((c, e) => {
const $title = document.getElementById('title');
const $username = document.getElementById('username');
const $password = document.getElementById('password');
const $form = document.getElementById('form');
const $cancel = document.getElementById('cancel');
const $message = document.getElementById('message');
function submit() {
c({ username: $username.value, password: $password.value });
return false;
};
function cancel() {
c({ username: '', password: '' });
return false;
};
$form.addEventListener('submit', submit);
$cancel.addEventListener('click', cancel);
document.body.addEventListener('keydown', function (e) {
switch (e.keyCode) {
case 27: e.preventDefault(); e.stopPropagation(); return cancel();
case 13: e.preventDefault(); e.stopPropagation(); return submit();
}
});
$title.textContent = data.title;
$message.textContent = data.message;
$username.focus();
});
}
</script>
</html>

View File

@@ -68,7 +68,7 @@ export class ProxyAuthHandler {
const win = new BrowserWindow(opts);
const config = {};
const baseUrl = require.toUrl('./auth.html');
const baseUrl = require.toUrl('vs/code/electron-browser/proxy/auth.html');
const url = `${baseUrl}?config=${encodeURIComponent(JSON.stringify(config))}`;
const proxyUrl = `${authInfo.host}:${authInfo.port}`;
const title = localize('authRequire', "Proxy Authentication Required");

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* 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

@@ -17,6 +17,97 @@ import { isWindows } from 'vs/base/common/platform';
import { app } from 'electron';
import { basename } from 'path';
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;
}
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
if (info.windows.some(window => window.folders && window.folders.length > 0)) {
info.windows.forEach(window => {
if (window.folders.length === 0) {
return;
}
workspaceInfoMessages.push(`| Window (${window.title})`);
window.folders.forEach(folder => {
try {
const stats = collectWorkspaceStats(folder, ['node_modules', '.git']);
let countMessage = `${stats.fileCount} files`;
if (stats.maxFilesReached) {
countMessage = `more than ${countMessage}`;
}
workspaceInfoMessages.push(`| Folder (${basename(folder)}): ${countMessage}`);
workspaceInfoMessages.push(formatWorkspaceStats(stats));
const launchConfigs = collectLaunchConfigs(folder);
if (launchConfigs.length > 0) {
workspaceInfoMessages.push(formatLaunchConfigs(launchConfigs));
}
} catch (error) {
workspaceInfoMessages.push(`| Error: Unable to collect workpsace stats for folder ${folder} (${error.toString()})`);
}
});
});
}
return {
processInfo: formatProcessList(info, rootProcess),
workspaceInfo: workspaceInfoMessages.join('\n')
};
});
}
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(' ')}`
};
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 => {
@@ -83,7 +174,6 @@ function formatWorkspaceStats(workspaceStats: WorkspaceStats): string {
line += item;
};
// File Types
let line = '| File types:';
const maxShown = 10;
@@ -124,7 +214,7 @@ function formatEnvironment(info: IMainProcessInfo): string {
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()})`);
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})`);
@@ -135,6 +225,7 @@ function formatEnvironment(info: IMainProcessInfo): string {
}
output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`);
output.push(`Screen Reader: ${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`);
output.push(`Process Argv: ${info.mainArguments.join(' ')}`);
return output.join('\n');
}
@@ -145,9 +236,11 @@ function formatProcessList(info: IMainProcessInfo, rootProcess: ProcessItem): st
const output: string[] = [];
output.push('CPU %\tMem MB\tProcess');
output.push('CPU %\tMem MB\t PID\tProcess');
formatProcessItem(mapPidToWindowTitle, output, rootProcess, 0);
if (rootProcess) {
formatProcessItem(mapPidToWindowTitle, output, rootProcess, 0);
}
return output.join('\n');
}
@@ -169,10 +262,10 @@ function formatProcessItem(mapPidToWindowTitle: Map<number, string>, output: str
}
}
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${name}`);
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

@@ -10,12 +10,13 @@ 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 } from 'vs/base/common/platform';
import { ParsedArgs } from 'vs/platform/environment/common/environment';
import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { OpenContext } 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 { Schemas } from 'vs/base/common/network';
export const ID = 'launchService';
export const ILaunchService = createDecorator<ILaunchService>(ID);
@@ -33,6 +34,7 @@ export interface IWindowInfo {
export interface IMainProcessInfo {
mainPID: number;
mainArguments: string[];
windows: IWindowInfo[];
}
@@ -41,12 +43,14 @@ export interface ILaunchService {
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>;
}
@@ -65,6 +69,9 @@ export class LaunchChannel implements ILaunchChannel {
case 'get-main-process-info':
return this.service.getMainProcessInfo();
case 'get-logs-path':
return this.service.getLogsPath();
}
return undefined;
@@ -88,6 +95,10 @@ export class LaunchChannelClient implements ILaunchService {
public getMainProcessInfo(): TPromise<IMainProcessInfo> {
return this.channel.call('get-main-process-info', null);
}
public getLogsPath(): TPromise<string> {
return this.channel.call('get-logs-path', null);
}
}
export class LaunchService implements ILaunchService {
@@ -98,21 +109,35 @@ export class LaunchService implements ILaunchService {
@ILogService private logService: ILogService,
@IWindowsMainService private windowsMainService: IWindowsMainService,
@IURLService private urlService: IURLService,
@IWorkspacesMainService private workspacesMainService: IWorkspacesMainService
@IWorkspacesMainService private workspacesMainService: IWorkspacesMainService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) { }
public start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise<void> {
this.logService.trace('Received data from other instance: ', args, userEnv);
// Check early for open-url which is handled in URL service
const openUrl = (args['open-url'] ? args._urls : []) || [];
if (openUrl.length > 0) {
openUrl.forEach(url => this.urlService.open(url));
if (this.shouldOpenUrl(args)) {
return TPromise.as(null);
}
// Otherwise handle in windows service
return this.startOpenWindow(args, userEnv);
}
private shouldOpenUrl(args: ParsedArgs): boolean {
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
args._urls.forEach(url => this.urlService.open(url));
return true;
}
return false;
}
private startOpenWindow(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise<void> {
const context = !!userEnv['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
let usedWindows: ICodeWindow[];
if (!!args.extensionDevelopmentPath) {
@@ -158,12 +183,19 @@ export class LaunchService implements ILaunchService {
return TPromise.wrap({
mainPID: process.pid,
mainArguments: process.argv,
windows: this.windowsMainService.getWindows().map(window => {
return this.getWindowInfo(window);
})
} as IMainProcessInfo);
}
public getLogsPath(): TPromise<string> {
this.logService.trace('Received request for logs path from other instance.');
return TPromise.as(this.environmentService.logsPath);
}
private getWindowInfo(window: ICodeWindow): IWindowInfo {
const folders: string[] = [];
@@ -172,7 +204,7 @@ export class LaunchService implements ILaunchService {
} else if (window.openedWorkspace) {
const rootFolders = this.workspacesMainService.resolveWorkspaceSync(window.openedWorkspace.configPath).folders;
rootFolders.forEach(root => {
if (root.uri.scheme === 'file') {
if (root.uri.scheme === Schemas.file) { // todo@remote signal remote folders?
folders.push(root.uri.fsPath);
}
});
@@ -184,4 +216,4 @@ export class LaunchService implements ILaunchService {
folders
} as IWindowInfo;
}
}
}

View File

@@ -0,0 +1,155 @@
/*---------------------------------------------------------------------------------------------
* 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 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';
interface PostResult {
readonly blob_id: string;
}
class Endpoint {
private constructor(
public readonly url: string
) { }
public static getFromProduct(): Endpoint | undefined {
const logUploaderUrl = product.logUploaderUrl;
return logUploaderUrl ? new Endpoint(logUploaderUrl) : undefined;
}
}
export async function uploadLogs(
channel: ILaunchChannel,
requestService: IRequestService,
environmentService: IEnvironmentService
): TPromise<any> {
const endpoint = Endpoint.getFromProduct();
if (!endpoint) {
console.error(localize('invalidEndpoint', 'Invalid log uploader endpoint'));
return;
}
const logsPath = await channel.call('get-logs-path', null);
if (await promptUserToConfirmLogUpload(logsPath, environmentService)) {
console.log(localize('beginUploading', 'Uploading...'));
const outZip = await zipLogs(logsPath);
const result = await postLogs(endpoint, outZip, requestService);
console.log(localize('didUploadLogs', 'Upload successful! Log file ID: {0}', result.blob_id));
}
}
function promptUserToConfirmLogUpload(
logsPath: string,
environmentService: IEnvironmentService
): boolean {
const confirmKey = 'iConfirmLogsUpload';
if ((environmentService.args['upload-logs'] || '').toLowerCase() === confirmKey.toLowerCase()) {
return true;
} else {
const message = localize('logUploadPromptHeader', 'You are about to upload your session logs to a secure Microsoft endpoint that only Microsoft\'s members of the VS Code team can access.')
+ '\n\n' + localize('logUploadPromptBody', 'Session logs may contain personal information such as full paths or file contents. Please review and redact your session log files here: \'{0}\'', logsPath)
+ '\n\n' + localize('logUploadPromptBodyDetails', 'By continuing you confirm that you have reviewed and redacted your session log files and that you agree to Microsoft using them to debug VS Code.')
+ '\n\n' + localize('logUploadPromptAcceptInstructions', 'Please run code with \'--upload-logs={0}\' to proceed with upload', confirmKey);
console.log(message);
return false;
}
}
async function postLogs(
endpoint: Endpoint,
outZip: string,
requestService: IRequestService
): TPromise<PostResult> {
const dotter = setInterval(() => console.log('.'), 5000);
let result: IRequestContext;
try {
result = await requestService.request({
url: endpoint.url,
type: 'POST',
data: Buffer.from(fs.readFileSync(outZip)).toString('base64'),
headers: {
'Content-Type': 'application/zip'
}
});
} catch (e) {
clearInterval(dotter);
console.log(localize('postError', 'Error posting logs: {0}', e));
throw e;
}
return new TPromise<PostResult>((res, reject) => {
const parts: Buffer[] = [];
result.stream.on('data', data => {
parts.push(data);
});
result.stream.on('end', () => {
clearInterval(dotter);
try {
const response = Buffer.concat(parts).toString('utf-8');
if (result.res.statusCode === 200) {
res(JSON.parse(response));
} else {
const errorMessage = localize('responseError', 'Error posting logs. Got {0} — {1}', result.res.statusCode, response);
console.log(errorMessage);
reject(new Error(errorMessage));
}
} catch (e) {
console.log(localize('parseError', 'Error parsing response'));
reject(e);
}
});
});
}
function zipLogs(
logsPath: string
): TPromise<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) => {
doZip(logsPath, outZip, tempDir, (err, stdout, stderr) => {
if (err) {
console.error(localize('zipError', 'Error zipping logs: {0}', err));
reject(err);
} else {
resolve(outZip);
}
});
});
}
function doZip(
logsPath: string,
outZip: string,
tempDir: string,
callback: (error: Error, stdout: string, stderr: string) => void
) {
switch (os.platform()) {
case 'win32':
// Copy directory first to avoid file locking issues
const sub = path.join(tempDir, 'sub');
return cp.execFile('powershell', ['-Command',
`[System.IO.Directory]::CreateDirectory("${sub}"); Copy-Item -recurse "${logsPath}" "${sub}"; Compress-Archive -Path "${sub}" -DestinationPath "${outZip}"`],
{ cwd: logsPath },
callback);
default:
return cp.execFile('zip', ['-r', outZip, '.'], { cwd: logsPath }, callback);
}
}

View File

@@ -5,6 +5,7 @@
'use strict';
import 'vs/code/electron-main/contributions';
import { app, dialog } from 'electron';
import { assign } from 'vs/base/common/objects';
import * as platform from 'vs/base/common/platform';
@@ -21,7 +22,7 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ILogService, ConsoleLogMainService, MultiplexLogService } from 'vs/platform/log/common/log';
import { ILogService, ConsoleLogMainService, MultiplexLogService, getLogLevel } from 'vs/platform/log/common/log';
import { StateService } from 'vs/platform/state/node/stateService';
import { IStateService } from 'vs/platform/state/common/state';
import { IBackupMainService } from 'vs/platform/backup/common/backup';
@@ -42,16 +43,20 @@ import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/work
import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces';
import { localize } from 'vs/nls';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { createLogService } from 'vs/platform/log/node/spdlogService';
import { createSpdLogService } from 'vs/platform/log/node/spdlogService';
import { printDiagnostics } from 'vs/code/electron-main/diagnostics';
import { BufferLogService } from 'vs/platform/log/common/bufferLog';
import { uploadLogs } from 'vs/code/electron-main/logUploader';
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { IChoiceService } from 'vs/platform/dialogs/common/dialogs';
import { ChoiceCliService } from 'vs/platform/dialogs/node/choiceCli';
function createServices(args: ParsedArgs): IInstantiationService {
function createServices(args: ParsedArgs, bufferLogService: BufferLogService): IInstantiationService {
const services = new ServiceCollection();
const environmentService = new EnvironmentService(args, process.execPath);
const spdlogService = createLogService('main', environmentService);
const consoleLogService = new ConsoleLogMainService(environmentService);
const logService = new MultiplexLogService([consoleLogService, spdlogService]);
const consoleLogService = new ConsoleLogMainService(getLogLevel(environmentService));
const logService = new MultiplexLogService([consoleLogService, bufferLogService]);
process.once('exit', () => logService.dispose());
@@ -68,6 +73,7 @@ function createServices(args: ParsedArgs): IInstantiationService {
services.set(IRequestService, new SyncDescriptor(RequestService));
services.set(IURLService, new SyncDescriptor(URLService, args['open-url'] ? args._urls : []));
services.set(IBackupMainService, new SyncDescriptor(BackupMainService));
services.set(IChoiceService, new SyncDescriptor(ChoiceCliService));
return new InstantiationService(services, true);
}
@@ -104,6 +110,7 @@ class ExpectedError extends Error {
function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
const logService = accessor.get(ILogService);
const environmentService = accessor.get(IEnvironmentService);
const requestService = accessor.get(IRequestService);
function allowSetForegroundWindow(service: LaunchChannelClient): TPromise<void> {
let promise = TPromise.wrap<void>(void 0);
@@ -133,6 +140,12 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
throw new ExpectedError('Terminating...');
}
// Log uploader usage info
if (typeof environmentService.args['upload-logs'] !== 'undefined') {
logService.warn('Warning: The --upload-logs argument can only be used if Code is already running. Please run it again after Code has started.');
throw new ExpectedError('Terminating...');
}
// dock might be hidden at this case due to a retry
if (platform.isMacintosh) {
app.dock.show();
@@ -170,7 +183,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
// 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;
if (!environmentService.wait && !environmentService.status) {
if (!environmentService.wait && !environmentService.status && !environmentService.args['upload-logs']) {
startupWarningDialogHandle = setTimeout(() => {
showStartupWarningDialog(
localize('secondInstanceNoResponse', "Another instance of {0} is running but not responding", product.nameShort),
@@ -189,6 +202,12 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
});
}
// Log uploader
if (typeof environmentService.args['upload-logs'] !== 'undefined') {
return uploadLogs(channel, requestService, environmentService)
.then(() => TPromise.wrapError(new ExpectedError()));
}
logService.trace('Sending env to running instance...');
return allowSetForegroundWindow(service)
@@ -236,7 +255,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise<Server> {
}
function showStartupWarningDialog(message: string, detail: string): void {
dialog.showMessageBox(null, {
dialog.showMessageBox({
title: product.nameLong,
type: 'warning',
buttons: [mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
@@ -272,8 +291,12 @@ function quit(accessor: ServicesAccessor, reason?: ExpectedError | Error): void
}
function main() {
let args: ParsedArgs;
// Set the error handler early enough so that we are not getting the
// default electron error dialog popping up
setUnexpectedErrorHandler(err => console.error(err));
let args: ParsedArgs;
try {
args = parseMainProcessArgv(process.argv);
args = validatePaths(args);
@@ -284,7 +307,12 @@ function main() {
return;
}
const instantiationService = createServices(args);
// 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);
return instantiationService.invokeFunction(accessor => {
@@ -300,7 +328,10 @@ function main() {
// Startup
return instantiationService.invokeFunction(a => createPaths(a.get(IEnvironmentService)))
.then(() => instantiationService.invokeFunction(setupIPC))
.then(mainIpcServer => instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnv).startup());
.then(mainIpcServer => {
bufferLogService.logger = createSpdLogService('main', bufferLogService.getLevel(), environmentService.logsPath);
return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnv).startup();
});
}).done(null, err => instantiationService.invokeFunction(quit, err));
}

View File

@@ -9,16 +9,16 @@ import * as nls from 'vs/nls';
import { isMacintosh, isLinux, isWindows, language } from 'vs/base/common/platform';
import * as arrays from 'vs/base/common/arrays';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ipcMain as ipc, app, shell, dialog, Menu, MenuItem, BrowserWindow, clipboard } from 'electron';
import { OpenContext, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows';
import { ipcMain as ipc, app, shell, Menu, MenuItem, BrowserWindow } from 'electron';
import { OpenContext, IRunActionInWindowRequest, IWindowsService } from 'vs/platform/windows/common/windows';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { AutoSaveConfiguration } from 'vs/platform/files/common/files';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUpdateService, State as UpdateState } from 'vs/platform/update/common/update';
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, getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels';
import { mnemonicMenuLabel as baseMnemonicLabel, unmnemonicLabel, getPathLabel } from 'vs/base/common/labels';
import { KeybindingsResolver } from 'vs/code/electron-main/keyboard';
import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows';
import { IHistoryMainService } from 'vs/platform/history/common/history';
@@ -69,6 +69,7 @@ export class CodeMenu {
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService private configurationService: IConfigurationService,
@IWindowsMainService private windowsMainService: IWindowsMainService,
@IWindowsService private windowsService: IWindowsService,
@IEnvironmentService private environmentService: IEnvironmentService,
@ITelemetryService private telemetryService: ITelemetryService,
@IHistoryMainService private historyMainService: IHistoryMainService
@@ -246,6 +247,7 @@ export class CodeMenu {
const editMenu = new Menu();
const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu });
this.setEditMenu(editMenu);
// {{SQL CARBON EDIT}}
// Selection
// const selectionMenu = new Menu();
@@ -333,7 +335,7 @@ export class CodeMenu {
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 || !!this.windowsMainService.getFocusedWindow()) {
if (this.windowsMainService.getWindowCount() === 0 || !!BrowserWindow.getFocusedWindow()) {
this.windowsMainService.quit(); // fix for https://github.com/Microsoft/vscode/issues/39191
}
}
@@ -709,9 +711,11 @@ export class CodeMenu {
}
const commands = this.createMenuItem(nls.localize({ key: 'miCommandPalette', comment: ['&& denotes a mnemonic'] }, "&&Command Palette..."), 'workbench.action.showCommands');
const openView = this.createMenuItem(nls.localize({ key: 'miOpenView', comment: ['&& denotes a mnemonic'] }, "&&Open View..."), 'workbench.action.openView');
const fullscreen = new MenuItem(this.withKeybinding('workbench.action.toggleFullScreen', { label: this.mnemonicLabel(nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen")), click: () => this.windowsMainService.getLastActiveWindow().toggleFullScreen(), enabled: this.windowsMainService.getWindowCount() > 0 }));
const toggleZenMode = this.createMenuItem(nls.localize('miToggleZenMode', "Toggle Zen Mode"), 'workbench.action.toggleZenMode');
const toggleCenteredLayout = this.createMenuItem(nls.localize('miToggleCenteredLayout', "Toggle Centered Layout"), 'workbench.action.toggleCenteredLayout');
const toggleMenuBar = this.createMenuItem(nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar"), 'workbench.action.toggleMenuBar');
// {{SQL CARBON EDIT}}
//const splitEditor = this.createMenuItem(nls.localize({ key: 'miSplitEditor', comment: ['&& denotes a mnemonic'] }, "Split &&Editor"), 'workbench.action.splitEditor');
@@ -758,6 +762,7 @@ export class CodeMenu {
arrays.coalesce([
commands,
openView,
__separator__(),
servers,
tasks,
@@ -777,6 +782,7 @@ export class CodeMenu {
__separator__(),
fullscreen,
toggleZenMode,
toggleCenteredLayout,
isWindows || isLinux ? toggleMenuBar : void 0,
__separator__(),
// {{SQL CARBON EDIT}}
@@ -979,7 +985,7 @@ export class CodeMenu {
const label = nls.localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue");
if (this.windowsMainService.getWindowCount() > 0) {
reportIssuesItem = this.createMenuItem(label, 'workbench.action.reportIssues');
reportIssuesItem = this.createMenuItem(label, 'workbench.action.openIssueReporter');
} else {
reportIssuesItem = new MenuItem({ label: this.mnemonicLabel(label), click: () => this.openUrl(product.reportIssueUrl, 'openReportIssues') });
}
@@ -1037,7 +1043,7 @@ export class CodeMenu {
}
helpMenu.append(__separator__());
helpMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About")), click: () => this.openAboutDialog() }));
helpMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About")), click: () => this.windowsService.openAboutDialog() }));
}
}
@@ -1085,45 +1091,54 @@ export class CodeMenu {
}
private getUpdateMenuItems(): Electron.MenuItem[] {
switch (this.updateService.state) {
case UpdateState.Uninitialized:
const state = this.updateService.state;
switch (state.type) {
case StateType.Uninitialized:
return [];
case UpdateState.UpdateDownloaded:
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();
}
})];
case UpdateState.CheckingForUpdate:
return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking For Updates..."), enabled: false })];
case UpdateState.UpdateAvailable:
if (isLinux) {
return [new MenuItem({
label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => {
this.updateService.quitAndInstall();
}
})];
}
const updateAvailableLabel = isWindows
? nls.localize('miDownloadingUpdate', "Downloading Update...")
: nls.localize('miInstallingUpdate', "Installing Update...");
return [new MenuItem({ label: updateAvailableLabel, enabled: false })];
default:
const result = [new MenuItem({
label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => {
this.reportMenuActionTelemetry('CheckForUpdate');
this.updateService.checkForUpdates(true);
}, 0)
})];
return result;
}
}
@@ -1142,11 +1157,6 @@ export class CodeMenu {
const enabled = typeof arg3 === 'boolean' ? arg3 : this.windowsMainService.getWindowCount() > 0;
const checked = typeof arg4 === 'boolean' ? arg4 : false;
let commandId: string;
if (typeof arg2 === 'string') {
commandId = arg2;
}
const options: Electron.MenuItemConstructorOptions = {
label,
click,
@@ -1158,6 +1168,13 @@ export class CodeMenu {
options['checked'] = checked;
}
let commandId: string;
if (typeof arg2 === 'string') {
commandId = arg2;
} else if (Array.isArray(arg2)) {
commandId = arg2[0];
}
return new MenuItem(this.withKeybinding(commandId, options));
}
@@ -1241,41 +1258,6 @@ export class CodeMenu {
return options;
}
private openAboutDialog(): void {
const lastActiveWindow = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow();
const detail = nls.localize('aboutDetail',
"Version {0}\nCommit {1}\nDate {2}\nShell {3}\nRenderer {4}\nNode {5}\nArchitecture {6}",
app.getVersion(),
product.commit || 'Unknown',
product.date || 'Unknown',
process.versions['electron'],
process.versions['chrome'],
process.versions['node'],
process.arch
);
const buttons = [nls.localize('okButton', "OK")];
if (isWindows) {
buttons.push(mnemonicButtonLabel(nls.localize({ key: 'copy', comment: ['&& denotes a mnemonic'] }, "&&Copy"))); // https://github.com/Microsoft/vscode/issues/37608
}
const result = dialog.showMessageBox(lastActiveWindow && lastActiveWindow.win, {
title: product.nameLong,
type: 'info',
message: product.nameLong,
detail: `\n${detail}`,
buttons,
noLink: true
});
if (isWindows && result === 1) {
clipboard.writeText(detail);
}
this.reportMenuActionTelemetry('showAboutDialog');
}
private openUrl(url: string, id: string): void {
shell.openExternal(url);
this.reportMenuActionTelemetry(id);

View File

@@ -12,6 +12,7 @@ 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';
export class SharedProcess implements ISharedProcess {
@@ -23,7 +24,8 @@ export class SharedProcess implements ISharedProcess {
constructor(
private environmentService: IEnvironmentService,
private readonly machineId: string,
private readonly userEnv: IProcessEnvironment
private readonly userEnv: IProcessEnvironment,
private readonly logService: ILogService
) { }
@memoize
@@ -43,7 +45,7 @@ export class SharedProcess implements ISharedProcess {
userEnv: this.userEnv
});
const url = `${require.toUrl('vs/code/electron-browser/sharedProcess.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
const url = `${require.toUrl('vs/code/electron-browser/sharedProcess/sharedProcess.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
this.window.loadURL(url);
// Prevent the window from dying
@@ -75,7 +77,8 @@ export class SharedProcess implements ISharedProcess {
ipcMain.once('handshake:hello', ({ sender }: { sender: any }) => {
sender.send('handshake:hey there', {
sharedIPCHandle: this.environmentService.sharedIPCHandle,
args: this.environmentService.args
args: this.environmentService.args,
logLevel: this.logService.getLevel()
});
ipcMain.once('handshake:im ready', () => c(null));

View File

@@ -198,7 +198,7 @@ export class CodeWindow implements ICodeWindow {
this._win = new BrowserWindow(options);
this._id = this._win.id;
// TODO@Ben Bug in Electron (https://github.com/electron/electron/issues/10862). On multi-monitor setups,
// Bug in Electron (https://github.com/electron/electron/issues/10862). On multi-monitor setups,
// it can happen that the position we set to the window is not the correct one on the display.
// To workaround, we ask the window for its position and set it again if not matching.
// This only applies if the window is not fullscreen or maximized and multiple monitors are used.
@@ -559,6 +559,10 @@ export class CodeWindow implements ICodeWindow {
configuration['extensions-dir'] = cli['extensions-dir'];
}
if (cli) {
configuration['disable-extensions'] = cli['disable-extensions'];
}
configuration.isInitialStartup = false; // since this is a reload
// Load config
@@ -567,6 +571,10 @@ export class CodeWindow implements ICodeWindow {
private getUrl(windowConfiguration: IWindowConfiguration): string {
// Set window ID
windowConfiguration.windowId = this._win.id;
windowConfiguration.logLevel = this.logService.getLevel();
// Set zoomlevel
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
const zoomLevel = windowConfig && windowConfig.zoomLevel;
@@ -578,7 +586,11 @@ export class CodeWindow implements ICodeWindow {
windowConfiguration.fullscreen = this._win.isFullScreen();
// Set Accessibility Config
windowConfiguration.highContrast = isWindows && systemPreferences.isInvertedColorScheme() && (!windowConfig || windowConfig.autoDetectHighContrast);
let autoDetectHighContrast = true;
if (windowConfig && windowConfig.autoDetectHighContrast === false) {
autoDetectHighContrast = false;
}
windowConfiguration.highContrast = isWindows && autoDetectHighContrast && systemPreferences.isInvertedColorScheme();
windowConfiguration.accessibilitySupport = app.isAccessibilitySupportEnabled();
// Theme
@@ -588,14 +600,13 @@ export class CodeWindow implements ICodeWindow {
// Perf Counters
windowConfiguration.perfEntries = exportEntries();
windowConfiguration.perfStartTime = global.perfStartTime;
windowConfiguration.perfAppReady = global.perfAppReady;
windowConfiguration.perfWindowLoadTime = Date.now();
// 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]) {
if (config[key] === void 0 || config[key] === null || config[key] === '') {
delete config[key]; // only send over properties that have a true value
}
}
@@ -838,7 +849,7 @@ export class CodeWindow implements ICodeWindow {
this._win.setAutoHideMenuBar(true);
if (notify) {
this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the **Alt** key."));
this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the Alt-key."));
}
break;
@@ -955,7 +966,7 @@ export class CodeWindow implements ICodeWindow {
const segments: ITouchBarSegment[] = items.map(item => {
let icon: Electron.NativeImage;
if (item.iconPath) {
icon = nativeImage.createFromPath(item.iconPath);
icon = nativeImage.createFromPath(item.iconPath.dark);
if (icon.isEmpty()) {
icon = void 0;
}

View File

@@ -19,13 +19,13 @@ import { IPathWithLineAndColumn, parseLineAndColumnAware } from 'vs/code/node/pa
import { ILifecycleService, UnloadReason, IWindowUnloadEvent } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, INativeOpenDialogOptions, ReadyState, IPathsToWaitFor, IEnterWorkspaceResult } from 'vs/platform/windows/common/windows';
import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, INativeOpenDialogOptions, ReadyState, IPathsToWaitFor, IEnterWorkspaceResult, IMessageBoxResult } from 'vs/platform/windows/common/windows';
import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderPath } from 'vs/code/node/windowsFinder';
import CommonEvent, { Emitter } from 'vs/base/common/event';
import product from 'vs/platform/node/product';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isEqual } from 'vs/base/common/paths';
import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows';
import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { IHistoryMainService } from 'vs/platform/history/common/history';
import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { TPromise } from 'vs/base/common/winjs.base';
@@ -35,6 +35,7 @@ import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { Schemas } from 'vs/base/common/network';
import { normalizeNFC } from 'vs/base/common/strings';
import URI from 'vs/base/common/uri';
import { Queue } from 'vs/base/common/async';
enum WindowError {
UNRESPONSIVE,
@@ -45,10 +46,6 @@ interface INewWindowState extends ISingleWindowState {
hasDefaultState?: boolean;
}
interface ILegacyWindowState extends IWindowState {
workspacePath?: string;
}
interface IWindowState {
workspace?: IWorkspaceIdentifier;
folderPath?: string;
@@ -56,10 +53,6 @@ interface IWindowState {
uiState: ISingleWindowState;
}
interface ILegacyWindowsState extends IWindowsState {
openedFolders?: IWindowState[];
}
interface IWindowsState {
lastActiveWindow?: IWindowState;
lastPluginDevelopmentHostWindow?: IWindowState;
@@ -116,7 +109,7 @@ export class WindowsManager implements IWindowsMainService {
private windowsState: IWindowsState;
private lastClosedWindowState: IWindowState;
private fileDialog: FileDialog;
private dialogs: Dialogs;
private workspacesManager: WorkspacesManager;
private _onWindowReady = new Emitter<CodeWindow>();
@@ -151,39 +144,12 @@ export class WindowsManager implements IWindowsMainService {
@IInstantiationService private instantiationService: IInstantiationService
) {
this.windowsState = this.stateService.getItem<IWindowsState>(WindowsManager.windowsStateStorageKey) || { openedWindows: [] };
this.fileDialog = new FileDialog(environmentService, telemetryService, stateService, this);
this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, this);
this.migrateLegacyWindowState();
}
private migrateLegacyWindowState(): void {
const state: ILegacyWindowsState = this.windowsState;
// TODO@Ben migration from previous openedFolders to new openedWindows property
if (Array.isArray(state.openedFolders) && state.openedFolders.length > 0) {
state.openedWindows = state.openedFolders;
state.openedFolders = void 0;
} else if (!state.openedWindows) {
state.openedWindows = [];
if (!Array.isArray(this.windowsState.openedWindows)) {
this.windowsState.openedWindows = [];
}
// TODO@Ben migration from previous workspacePath in window state to folderPath
const states: ILegacyWindowState[] = [];
states.push(state.lastActiveWindow);
states.push(state.lastPluginDevelopmentHostWindow);
states.push(...state.openedWindows);
states.forEach(state => {
if (!state) {
return;
}
if (typeof state.workspacePath === 'string') {
state.folderPath = state.workspacePath;
state.workspacePath = void 0;
}
});
this.dialogs = new Dialogs(environmentService, telemetryService, stateService, this);
this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, this);
}
public ready(initialUserEnv: IProcessEnvironment): void {
@@ -277,7 +243,7 @@ export class WindowsManager implements IWindowsMainService {
// - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeQuit(0)
//
private onBeforeQuit(): void {
const currentWindowsState: ILegacyWindowsState = {
const currentWindowsState: IWindowsState = {
openedWindows: [],
lastPluginDevelopmentHostWindow: this.windowsState.lastPluginDevelopmentHostWindow,
lastActiveWindow: this.lastClosedWindowState
@@ -403,7 +369,7 @@ export class WindowsManager implements IWindowsMainService {
let foldersToRestore: string[] = [];
let workspacesToRestore: IWorkspaceIdentifier[] = [];
let emptyToRestore: string[] = [];
if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath) {
if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) {
foldersToRestore = this.backupMainService.getFolderBackupPaths();
workspacesToRestore = this.backupMainService.getWorkspaceBackups(); // collect from workspaces with hot-exit backups
@@ -825,12 +791,7 @@ export class WindowsManager implements IWindowsMainService {
noLink: true
};
const activeWindow = BrowserWindow.getFocusedWindow();
if (activeWindow) {
dialog.showMessageBox(activeWindow, options);
} else {
dialog.showMessageBox(options);
}
this.dialogs.showMessageBox(options, this.getFocusedWindow());
}
return path;
@@ -940,10 +901,6 @@ export class WindowsManager implements IWindowsMainService {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
restoreWindows = ((windowConfig && windowConfig.restoreWindows) || 'one') as RestoreWindowsSetting;
if (restoreWindows === 'one' /* default */ && windowConfig && windowConfig.reopenFolders) {
restoreWindows = windowConfig.reopenFolders; // TODO@Ben migration from deprecated window.reopenFolders setting
}
if (['all', 'folders', 'one', 'none'].indexOf(restoreWindows) === -1) {
restoreWindows = 'one';
}
@@ -987,10 +944,14 @@ export class WindowsManager implements IWindowsMainService {
};
}
// Folder
return {
folderPath: candidate
};
// Folder (we check for isDirectory() because e.g. paths like /dev/null
// are neither file nor folder but some external tools might pass them
// over to us)
else if (candidateStat.isDirectory()) {
return {
folderPath: candidate
};
}
}
} catch (error) {
this.historyMainService.removeFromRecentlyOpened([candidate]); // since file does not seem to exist anymore, remove from recent
@@ -1361,7 +1322,10 @@ export class WindowsManager implements IWindowsMainService {
}
if (e.window.config && !!e.window.config.extensionDevelopmentPath) {
return; // do not ask to save workspace when doing extension development
// do not ask to save workspace when doing extension development
// but still delete it.
this.workspacesMainService.deleteUntitledWorkspaceSync(workspace);
return;
}
if (windowClosing && !isMacintosh && this.getWindowCount() === 1) {
@@ -1369,7 +1333,17 @@ export class WindowsManager implements IWindowsMainService {
}
// Handle untitled workspaces with prompt as needed
this.workspacesManager.promptToSaveUntitledWorkspace(e, workspace);
e.veto(this.workspacesManager.promptToSaveUntitledWorkspace(this.getWindowById(e.window.id), workspace).then(veto => {
if (veto) {
return veto;
}
// Bug in electron: somehow we need this timeout so that the window closes properly. That
// might be related to the fact that the untitled workspace prompt shows up async and this
// code can execute before the dialog is fully closed which then blocks the window from closing.
// Issue: https://github.com/Microsoft/vscode/issues/41989
return TPromise.timeout(0).then(() => veto);
}));
}
public focusLastActive(cli: ParsedArgs, context: OpenContext): CodeWindow {
@@ -1457,48 +1431,48 @@ export class WindowsManager implements IWindowsMainService {
// Unresponsive
if (error === WindowError.UNRESPONSIVE) {
const result = dialog.showMessageBox(window.win, {
this.dialogs.showMessageBox({
title: product.nameLong,
type: 'warning',
buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'wait', comment: ['&& denotes a mnemonic'] }, "&&Keep Waiting")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
message: localize('appStalled', "The window is no longer responding"),
detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."),
noLink: true
}, window).then(result => {
if (!window.win) {
return; // Return early if the window has been going down already
}
if (result.button === 0) {
window.reload();
} else if (result.button === 2) {
this.onBeforeWindowClose(window); // 'close' event will not be fired on destroy(), so run it manually
window.win.destroy(); // make sure to destroy the window as it is unresponsive
}
});
if (!window.win) {
return; // Return early if the window has been going down already
}
if (result === 0) {
window.reload();
} else if (result === 2) {
this.onBeforeWindowClose(window); // 'close' event will not be fired on destroy(), so run it manually
window.win.destroy(); // make sure to destroy the window as it is unresponsive
}
}
// Crashed
else {
const result = dialog.showMessageBox(window.win, {
this.dialogs.showMessageBox({
title: product.nameLong,
type: 'warning',
buttons: [mnemonicButtonLabel(localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))],
message: localize('appCrashed', "The window has crashed"),
detail: localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."),
noLink: true
}, window).then(result => {
if (!window.win) {
return; // Return early if the window has been going down already
}
if (result.button === 0) {
window.reload();
} else if (result.button === 1) {
this.onBeforeWindowClose(window); // 'close' event will not be fired on destroy(), so run it manually
window.win.destroy(); // make sure to destroy the window as it has crashed
}
});
if (!window.win) {
return; // Return early if the window has been going down already
}
if (result === 0) {
window.reload();
} else if (result === 1) {
this.onBeforeWindowClose(window); // 'close' event will not be fired on destroy(), so run it manually
window.win.destroy(); // make sure to destroy the window as it has crashed
}
}
}
@@ -1558,7 +1532,19 @@ export class WindowsManager implements IWindowsMainService {
}
}
this.fileDialog.pickAndOpen(internalOptions);
this.dialogs.pickAndOpen(internalOptions);
}
public showMessageBox(options: Electron.MessageBoxOptions, win?: CodeWindow): TPromise<IMessageBoxResult> {
return this.dialogs.showMessageBox(options, win);
}
public showSaveDialog(options: Electron.SaveDialogOptions, win?: CodeWindow): TPromise<string> {
return this.dialogs.showSaveDialog(options, win);
}
public showOpenDialog(options: Electron.OpenDialogOptions, win?: CodeWindow): TPromise<string[]> {
return this.dialogs.showOpenDialog(options, win);
}
public quit(): void {
@@ -1584,20 +1570,25 @@ interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions {
pickFiles?: boolean;
}
class FileDialog {
class Dialogs {
private static readonly workingDirPickerStorageKey = 'pickerWorkingDir';
private mapWindowToDialogQueue: Map<number, Queue<any>>;
private noWindowDialogQueue: Queue<any>;
constructor(
private environmentService: IEnvironmentService,
private telemetryService: ITelemetryService,
private stateService: IStateService,
private windowsMainService: IWindowsMainService
private windowsMainService: IWindowsMainService,
) {
this.mapWindowToDialogQueue = new Map<number, Queue<any>>();
this.noWindowDialogQueue = new Queue<any>();
}
public pickAndOpen(options: INativeOpenDialogOptions): void {
this.getFileOrFolderPaths(options, (paths: string[]) => {
this.getFileOrFolderPaths(options).then(paths => {
const numberOfPaths = paths ? paths.length : 0;
// Telemetry
@@ -1623,7 +1614,7 @@ class FileDialog {
});
}
private getFileOrFolderPaths(options: IInternalNativeOpenDialogOptions, clb: (paths: string[]) => void): void {
private getFileOrFolderPaths(options: IInternalNativeOpenDialogOptions): TPromise<string[]> {
// Ensure dialog options
if (!options.dialogOptions) {
@@ -1632,7 +1623,7 @@ class FileDialog {
// Ensure defaultPath
if (!options.dialogOptions.defaultPath) {
options.dialogOptions.defaultPath = this.stateService.getItem<string>(FileDialog.workingDirPickerStorageKey);
options.dialogOptions.defaultPath = this.stateService.getItem<string>(Dialogs.workingDirPickerStorageKey);
}
// Ensure properties
@@ -1654,28 +1645,86 @@ class FileDialog {
// Show Dialog
const focusedWindow = this.windowsMainService.getWindowById(options.windowId) || this.windowsMainService.getFocusedWindow();
let paths = dialog.showOpenDialog(focusedWindow && focusedWindow.win, options.dialogOptions);
if (paths && paths.length > 0) {
if (isMacintosh) {
return this.showOpenDialog(options.dialogOptions, focusedWindow).then(paths => {
if (paths && paths.length > 0) {
// Remember path in storage for next time
this.stateService.setItem(Dialogs.workingDirPickerStorageKey, dirname(paths[0]));
return paths;
}
return void 0;
});
}
private getDialogQueue(window?: ICodeWindow): Queue<any> {
if (!window) {
return this.noWindowDialogQueue;
}
let windowDialogQueue = this.mapWindowToDialogQueue.get(window.id);
if (!windowDialogQueue) {
windowDialogQueue = new Queue<any>();
this.mapWindowToDialogQueue.set(window.id, windowDialogQueue);
}
return windowDialogQueue;
}
public showMessageBox(options: Electron.MessageBoxOptions, window?: ICodeWindow): TPromise<IMessageBoxResult> {
return this.getDialogQueue(window).queue(() => {
return new TPromise((c, e) => {
dialog.showMessageBox(window ? window.win : void 0, options, (response: number, checkboxChecked: boolean) => {
c({ button: response, checkboxChecked });
});
});
});
}
public showSaveDialog(options: Electron.SaveDialogOptions, window?: ICodeWindow): TPromise<string> {
function normalizePath(path: string): string {
if (path && isMacintosh) {
path = normalizeNFC(path); // normalize paths returned from the OS
}
return path;
}
return this.getDialogQueue(window).queue(() => {
return new TPromise((c, e) => {
dialog.showSaveDialog(window ? window.win : void 0, options, path => {
c(normalizePath(path));
});
});
});
}
public showOpenDialog(options: Electron.OpenDialogOptions, window?: ICodeWindow): TPromise<string[]> {
function normalizePaths(paths: string[]): string[] {
if (paths && paths.length > 0 && isMacintosh) {
paths = paths.map(path => normalizeNFC(path)); // normalize paths returned from the OS
}
// Remember path in storage for next time
this.stateService.setItem(FileDialog.workingDirPickerStorageKey, dirname(paths[0]));
// Return
return clb(paths);
return paths;
}
return clb(void (0));
return this.getDialogQueue(window).queue(() => {
return new TPromise((c, e) => {
dialog.showOpenDialog(window ? window.win : void 0, options, paths => {
c(normalizePaths(paths));
});
});
});
}
}
class WorkspacesManager {
constructor(
private workspacesService: IWorkspacesMainService,
private backupService: IBackupMainService,
private workspacesMainService: IWorkspacesMainService,
private backupMainService: IBackupMainService,
private environmentService: IEnvironmentService,
private windowsMainService: IWindowsMainService
) {
@@ -1690,26 +1739,33 @@ class WorkspacesManager {
}
public createAndEnterWorkspace(window: CodeWindow, folders?: IWorkspaceFolderCreationData[], path?: string): TPromise<IEnterWorkspaceResult> {
if (!window || !window.win || window.readyState !== ReadyState.READY || !this.isValidTargetWorkspacePath(window, path)) {
if (!window || !window.win || window.readyState !== ReadyState.READY) {
return TPromise.as(null); // return early if the window is not ready or disposed
}
return this.workspacesService.createWorkspace(folders).then(workspace => {
return this.doSaveAndOpenWorkspace(window, workspace, path);
return this.isValidTargetWorkspacePath(window, path).then(isValid => {
if (!isValid) {
return TPromise.as(null); // return early if the workspace is not valid
}
return this.workspacesMainService.createWorkspace(folders).then(workspace => {
return this.doSaveAndOpenWorkspace(window, workspace, path);
});
});
}
private isValidTargetWorkspacePath(window: CodeWindow, path?: string): boolean {
private isValidTargetWorkspacePath(window: CodeWindow, path?: string): TPromise<boolean> {
if (!path) {
return true;
return TPromise.wrap(true);
}
if (window.openedWorkspace && window.openedWorkspace.configPath === path) {
return false; // window is already opened on a workspace with that path
return TPromise.wrap(false); // window is already opened on a workspace with that path
}
// Prevent overwriting a workspace that is currently opened in another window
if (findWindowOnWorkspace(this.windowsMainService.getWindows(), { id: this.workspacesService.getWorkspaceId(path), configPath: path })) {
if (findWindowOnWorkspace(this.windowsMainService.getWindows(), { id: this.workspacesMainService.getWorkspaceId(path), configPath: path })) {
const options: Electron.MessageBoxOptions = {
title: product.nameLong,
type: 'info',
@@ -1719,23 +1775,16 @@ class WorkspacesManager {
noLink: true
};
const activeWindow = BrowserWindow.getFocusedWindow();
if (activeWindow) {
dialog.showMessageBox(activeWindow, options);
} else {
dialog.showMessageBox(options);
}
return false;
return this.windowsMainService.showMessageBox(options, this.windowsMainService.getFocusedWindow()).then(() => false);
}
return true; // OK
return TPromise.wrap(true); // OK
}
private doSaveAndOpenWorkspace(window: CodeWindow, workspace: IWorkspaceIdentifier, path?: string): TPromise<IEnterWorkspaceResult> {
let savePromise: TPromise<IWorkspaceIdentifier>;
if (path) {
savePromise = this.workspacesService.saveWorkspace(workspace, path);
savePromise = this.workspacesMainService.saveWorkspace(workspace, path);
} else {
savePromise = TPromise.as(workspace);
}
@@ -1746,7 +1795,7 @@ class WorkspacesManager {
// Register window for backups and migrate current backups over
let backupPath: string;
if (!window.config.extensionDevelopmentPath) {
backupPath = this.backupService.registerWorkspaceBackupSync(workspace, window.config.backupPath);
backupPath = this.backupMainService.registerWorkspaceBackupSync(workspace, window.config.backupPath);
}
// Update window configuration properly based on transition to workspace
@@ -1776,7 +1825,7 @@ class WorkspacesManager {
});
}
public promptToSaveUntitledWorkspace(e: IWindowUnloadEvent, workspace: IWorkspaceIdentifier): void {
public promptToSaveUntitledWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): TPromise<boolean> {
enum ConfirmResult {
SAVE,
DONT_SAVE,
@@ -1810,41 +1859,35 @@ class WorkspacesManager {
options.defaultId = 2;
}
const res = dialog.showMessageBox(e.window.win, options);
return this.windowsMainService.showMessageBox(options, window).then(res => {
switch (buttons[res.button].result) {
switch (buttons[res].result) {
// Cancel: veto unload
case ConfirmResult.CANCEL:
return true;
// Cancel: veto unload
case ConfirmResult.CANCEL:
e.veto(true);
break;
// Don't Save: delete workspace
case ConfirmResult.DONT_SAVE:
this.workspacesMainService.deleteUntitledWorkspaceSync(workspace);
return false;
// Don't Save: delete workspace
case ConfirmResult.DONT_SAVE:
this.workspacesService.deleteUntitledWorkspaceSync(workspace);
e.veto(false);
break;
// Save: save workspace, but do not veto unload
case ConfirmResult.SAVE: {
return this.windowsMainService.showSaveDialog({
buttonLabel: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")),
title: localize('saveWorkspace', "Save Workspace"),
filters: WORKSPACE_FILTER,
defaultPath: this.getUntitledWorkspaceSaveDialogDefaultPath(workspace)
}, window).then(target => {
if (target) {
return this.workspacesMainService.saveWorkspace(workspace, target).then(() => false, () => false);
}
// Save: save workspace, but do not veto unload
case ConfirmResult.SAVE: {
let target = dialog.showSaveDialog(e.window.win, {
buttonLabel: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")),
title: localize('saveWorkspace', "Save Workspace"),
filters: WORKSPACE_FILTER,
defaultPath: this.getUntitledWorkspaceSaveDialogDefaultPath(workspace)
});
if (target) {
if (isMacintosh) {
target = normalizeNFC(target); // normalize paths returned from the OS
}
e.veto(this.workspacesService.saveWorkspace(workspace, target).then(() => false, () => false));
} else {
e.veto(true); // keep veto if no target was provided
return true; // keep veto if no target was provided
});
}
}
}
});
}
private getUntitledWorkspaceSaveDialogDefaultPath(workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string {
@@ -1853,7 +1896,7 @@ class WorkspacesManager {
return dirname(workspace);
}
const resolvedWorkspace = this.workspacesService.resolveWorkspaceSync(workspace.configPath);
const resolvedWorkspace = this.workspacesMainService.resolveWorkspaceSync(workspace.configPath);
if (resolvedWorkspace && resolvedWorkspace.folders.length > 0) {
for (const folder of resolvedWorkspace.folders) {
if (folder.uri.scheme === Schemas.file) {