Merge from vscode 313ede61cbad8f9dc748907b3384e059ddddb79a (#7436)

* Merge from vscode 313ede61cbad8f9dc748907b3384e059ddddb79a

* fix strict null checks
This commit is contained in:
Anthony Dresser
2019-09-30 23:35:45 -07:00
committed by GitHub
parent 6ab03053a0
commit 084524cd2d
196 changed files with 2927 additions and 2547 deletions

View File

@@ -8,6 +8,7 @@ import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { Disposable } from 'vs/base/common/lifecycle';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
const SERVICE_NAME = 'VS Code';
const ACCOUNT = 'MyAccount';
@@ -23,9 +24,10 @@ export class AuthTokenService extends Disposable implements IAuthTokenService {
constructor(
@ICredentialsService private readonly credentialsService: ICredentialsService,
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
) {
super();
if (productService.settingsSyncStoreUrl) {
if (productService.settingsSyncStoreUrl && configurationService.getValue('configurationSync.enableAuth')) {
this._status = AuthTokenStatus.Inactive;
this.getToken().then(token => {
if (token) {

View File

@@ -0,0 +1,211 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { MessageBoxOptions, SaveDialogOptions, OpenDialogOptions, dialog, FileFilter, BrowserWindow } from 'electron';
import { Queue } from 'vs/base/common/async';
import { IStateService } from 'vs/platform/state/node/state';
import { isMacintosh } from 'vs/base/common/platform';
import { dirname } from 'vs/base/common/path';
import { normalizeNFC } from 'vs/base/common/normalization';
import { exists } from 'vs/base/node/pfs';
import { INativeOpenDialogOptions, MessageBoxReturnValue, SaveDialogReturnValue, OpenDialogReturnValue } from 'vs/platform/dialogs/node/dialogs';
import { withNullAsUndefined } from 'vs/base/common/types';
import { localize } from 'vs/nls';
import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
export const IDialogMainService = createDecorator<IDialogMainService>('dialogMainService');
export interface IDialogMainService {
_serviceBrand: undefined;
pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined>;
pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined>;
pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined>;
pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined>;
showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise<MessageBoxReturnValue>;
showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise<SaveDialogReturnValue>;
showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise<OpenDialogReturnValue>;
}
interface IInternalNativeOpenDialogOptions extends INativeOpenDialogOptions {
pickFolders?: boolean;
pickFiles?: boolean;
title: string;
buttonLabel?: string;
filters?: FileFilter[];
}
export class DialogMainService implements IDialogMainService {
_serviceBrand: undefined;
private static readonly workingDirPickerStorageKey = 'pickerWorkingDir';
private readonly mapWindowToDialogQueue: Map<number, Queue<void>>;
private readonly noWindowDialogQueue: Queue<void>;
constructor(
@IStateService private readonly stateService: IStateService
) {
this.mapWindowToDialogQueue = new Map<number, Queue<void>>();
this.noWindowDialogQueue = new Queue<void>();
}
pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
return this.doPick({ ...options, pickFolders: true, pickFiles: true, title: localize('open', "Open") }, window);
}
pickFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
return this.doPick({ ...options, pickFolders: true, title: localize('openFolder', "Open Folder") }, window);
}
pickFile(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
return this.doPick({ ...options, pickFiles: true, title: localize('openFile', "Open File") }, window);
}
pickWorkspace(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
const title = localize('openWorkspaceTitle', "Open Workspace");
const buttonLabel = mnemonicButtonLabel(localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, "&&Open"));
const filters = WORKSPACE_FILTER;
return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window);
}
private async doPick(options: IInternalNativeOpenDialogOptions, window?: BrowserWindow): Promise<string[] | undefined> {
// Ensure dialog options
const dialogOptions: OpenDialogOptions = {
title: options.title,
buttonLabel: options.buttonLabel,
filters: options.filters
};
// Ensure defaultPath
dialogOptions.defaultPath = options.defaultPath || this.stateService.getItem<string>(DialogMainService.workingDirPickerStorageKey);
// Ensure properties
if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') {
dialogOptions.properties = undefined; // let it override based on the booleans
if (options.pickFiles && options.pickFolders) {
dialogOptions.properties = ['multiSelections', 'openDirectory', 'openFile', 'createDirectory'];
}
}
if (!dialogOptions.properties) {
dialogOptions.properties = ['multiSelections', options.pickFolders ? 'openDirectory' : 'openFile', 'createDirectory'];
}
if (isMacintosh) {
dialogOptions.properties.push('treatPackageAsDirectory'); // always drill into .app files
}
// Show Dialog
const windowToUse = window || BrowserWindow.getFocusedWindow();
const result = await this.showOpenDialog(dialogOptions, withNullAsUndefined(windowToUse));
if (result && result.filePaths && result.filePaths.length > 0) {
// Remember path in storage for next time
this.stateService.setItem(DialogMainService.workingDirPickerStorageKey, dirname(result.filePaths[0]));
return result.filePaths;
}
return undefined; // {{SQL CARBON EDIT}} strict-null-check
}
private getDialogQueue(window?: BrowserWindow): 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;
}
showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise<MessageBoxReturnValue> {
return this.getDialogQueue(window).queue(async () => {
return new Promise(resolve => {
if (window) {
return dialog.showMessageBox(window, options, (response, checkboxChecked) => resolve({ response, checkboxChecked }));
}
return dialog.showMessageBox(options);
});
});
}
showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise<SaveDialogReturnValue> {
function normalizePath(path: string | undefined): string | undefined {
if (path && isMacintosh) {
path = normalizeNFC(path); // normalize paths returned from the OS
}
return path;
}
return this.getDialogQueue(window).queue(async () => {
return new Promise<SaveDialogReturnValue>(resolve => {
if (window) {
dialog.showSaveDialog(window, options, filePath => resolve({ filePath }));
} else {
dialog.showSaveDialog(options, filePath => resolve({ filePath }));
}
}).then(result => {
result.filePath = normalizePath(result.filePath);
return result;
});
});
}
showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise<OpenDialogReturnValue> {
function normalizePaths(paths: string[] | undefined): string[] | undefined {
if (paths && paths.length > 0 && isMacintosh) {
paths = paths.map(path => normalizeNFC(path)); // normalize paths returned from the OS
}
return paths;
}
return this.getDialogQueue(window).queue(async () => {
// Ensure the path exists (if provided)
if (options.defaultPath) {
const pathExists = await exists(options.defaultPath);
if (!pathExists) {
options.defaultPath = undefined;
}
}
// Show dialog
return new Promise<OpenDialogReturnValue>(resolve => {
if (window) {
dialog.showOpenDialog(window, options, filePaths => resolve({ filePaths }));
} else {
dialog.showOpenDialog(options, filePaths => resolve({ filePaths }));
}
}).then(result => {
result.filePaths = normalizePaths(result.filePaths);
return result;
});
});
}
}

View File

@@ -13,3 +13,16 @@ export interface INativeOpenDialogOptions {
telemetryEventName?: string;
telemetryExtraData?: ITelemetryData;
}
export interface MessageBoxReturnValue {
response: number;
checkboxChecked: boolean;
}
export interface SaveDialogReturnValue {
filePath?: string;
}
export interface OpenDialogReturnValue {
filePaths?: string[];
}

View File

@@ -5,16 +5,17 @@
import { Event } from 'vs/base/common/event';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app } from 'electron';
import { INativeOpenInWindowOptions } from 'vs/platform/windows/node/window';
import { MessageBoxOptions, shell, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app } from 'electron';
import { INativeOpenWindowOptions } from 'vs/platform/windows/node/window';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IOpenedWindow, OpenContext, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
import { INativeOpenDialogOptions, MessageBoxReturnValue, SaveDialogReturnValue, OpenDialogReturnValue } from 'vs/platform/dialogs/node/dialogs';
import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { AddFirstParameterToFunctions } from 'vs/base/common/types';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
export class ElectronMainService implements AddFirstParameterToFunctions<IElectronService, Promise<any> /* only methods, not events */, number /* window ID */> {
@@ -22,6 +23,7 @@ export class ElectronMainService implements AddFirstParameterToFunctions<IElectr
constructor(
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
@IDialogMainService private readonly dialogMainService: IDialogMainService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
) {
@@ -61,7 +63,7 @@ export class ElectronMainService implements AddFirstParameterToFunctions<IElectr
}
async getActiveWindowId(windowId: number): Promise<number | undefined> {
const activeWindow = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow();
const activeWindow = BrowserWindow.getFocusedWindow() || this.windowsMainService.getLastActiveWindow();
if (activeWindow) {
return activeWindow.id;
}
@@ -69,11 +71,17 @@ export class ElectronMainService implements AddFirstParameterToFunctions<IElectr
return undefined;
}
async openEmptyWindow(windowId: number, options?: IOpenEmptyWindowOptions): Promise<void> {
this.windowsMainService.openEmptyWindow(OpenContext.API, options);
openWindow(windowId: number, options?: IOpenEmptyWindowOptions): Promise<void>;
openWindow(windowId: number, toOpen: IWindowOpenable[], options?: INativeOpenWindowOptions): Promise<void>;
openWindow(windowId: number, arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: INativeOpenWindowOptions): Promise<void> {
if (Array.isArray(arg1)) {
return this.doOpenWindow(windowId, arg1, arg2);
}
return this.doOpenEmptyWindow(windowId, arg1);
}
async openInWindow(windowId: number, toOpen: IWindowOpenable[], options: INativeOpenInWindowOptions = Object.create(null)): Promise<void> {
private async doOpenWindow(windowId: number, toOpen: IWindowOpenable[], options: INativeOpenWindowOptions = Object.create(null)): Promise<void> {
if (toOpen.length > 0) {
this.windowsMainService.open({
context: OpenContext.API,
@@ -91,6 +99,10 @@ export class ElectronMainService implements AddFirstParameterToFunctions<IElectr
}
}
private async doOpenEmptyWindow(windowId: number, options?: IOpenEmptyWindowOptions): Promise<void> {
this.windowsMainService.openEmptyWindow(OpenContext.API, options);
}
async toggleFullScreen(windowId: number): Promise<void> {
const window = this.windowsMainService.getWindowById(windowId);
if (window) {
@@ -164,15 +176,24 @@ export class ElectronMainService implements AddFirstParameterToFunctions<IElectr
//#region Dialog
async showMessageBox(windowId: number, options: MessageBoxOptions): Promise<MessageBoxReturnValue> {
return this.windowsMainService.showMessageBox(options, this.windowsMainService.getWindowById(windowId));
return this.dialogMainService.showMessageBox(options, this.toBrowserWindow(windowId));
}
async showSaveDialog(windowId: number, options: SaveDialogOptions): Promise<SaveDialogReturnValue> {
return this.windowsMainService.showSaveDialog(options, this.windowsMainService.getWindowById(windowId));
return this.dialogMainService.showSaveDialog(options, this.toBrowserWindow(windowId));
}
async showOpenDialog(windowId: number, options: OpenDialogOptions): Promise<OpenDialogReturnValue> {
return this.windowsMainService.showOpenDialog(options, this.windowsMainService.getWindowById(windowId));
return this.dialogMainService.showOpenDialog(options, this.toBrowserWindow(windowId));
}
private toBrowserWindow(windowId: number): BrowserWindow | undefined {
const window = this.windowsMainService.getWindowById(windowId);
if (window) {
return window.win;
}
return undefined;
}
async pickFileFolderAndOpen(windowId: number, options: INativeOpenDialogOptions): Promise<void> {
@@ -214,7 +235,9 @@ export class ElectronMainService implements AddFirstParameterToFunctions<IElectr
}
async openExternal(windowId: number, url: string): Promise<boolean> {
return this.windowsMainService.openExternal(url);
shell.openExternal(url);
return true;
}
async updateTouchBar(windowId: number, items: ISerializableCommandAction[][]): Promise<void> {
@@ -229,7 +252,7 @@ export class ElectronMainService implements AddFirstParameterToFunctions<IElectr
//#region macOS Touchbar
async newWindowTab(): Promise<void> {
this.windowsMainService.openNewTabbedWindow(OpenContext.API);
this.windowsMainService.open({ context: OpenContext.API, cli: this.environmentService.args, forceNewTabbedWindow: true, forceEmpty: true });
}
async showPreviousWindowTab(): Promise<void> {
@@ -267,7 +290,7 @@ export class ElectronMainService implements AddFirstParameterToFunctions<IElectr
}
}
async closeWorkpsace(windowId: number): Promise<void> {
async closeWorkspace(windowId: number): Promise<void> {
const window = this.windowsMainService.getWindowById(windowId);
if (window) {
return this.windowsMainService.closeWorkspace(window);

View File

@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, CrashReporterStartOptions } from 'electron';
import { MessageBoxOptions, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, CrashReporterStartOptions } from 'electron';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWindowOpenable, IOpenEmptyWindowOptions, IOpenedWindow } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
import { INativeOpenDialogOptions, MessageBoxReturnValue, SaveDialogReturnValue, OpenDialogReturnValue } from 'vs/platform/dialogs/node/dialogs';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { ParsedArgs } from 'vscode-minimist';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { INativeOpenInWindowOptions } from 'vs/platform/windows/node/window';
import { INativeOpenWindowOptions } from 'vs/platform/windows/node/window';
export const IElectronService = createDecorator<IElectronService>('electronService');
@@ -33,8 +33,8 @@ export interface IElectronService {
getWindowCount(): Promise<number>;
getActiveWindowId(): Promise<number | undefined>;
openEmptyWindow(options?: IOpenEmptyWindowOptions): Promise<void>;
openInWindow(toOpen: IWindowOpenable[], options?: INativeOpenInWindowOptions): Promise<void>;
openWindow(options?: IOpenEmptyWindowOptions): Promise<void>;
openWindow(toOpen: IWindowOpenable[], options?: INativeOpenWindowOptions): Promise<void>;
toggleFullScreen(): Promise<void>;
@@ -76,7 +76,7 @@ export interface IElectronService {
// Lifecycle
relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): Promise<void>;
reload(): Promise<void>;
closeWorkpsace(): Promise<void>;
closeWorkspace(): Promise<void>;
closeWindow(): Promise<void>;
quit(): Promise<void>;

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ISharedProcess } from 'vs/platform/windows/electron-main/windows';
export const ISharedProcessMainService = createDecorator<ISharedProcessMainService>('sharedProcessMainService');
@@ -15,6 +14,12 @@ export interface ISharedProcessMainService {
whenSharedProcessReady(): Promise<void>;
toggleSharedProcessWindow(): Promise<void>;
}
export interface ISharedProcess {
whenReady(): Promise<void>;
toggle(): void;
}
export class SharedProcessMainService implements ISharedProcessMainService {
_serviceBrand: undefined;

View File

@@ -7,15 +7,16 @@ import { localize } from 'vs/nls';
import * as objects from 'vs/base/common/objects';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { IIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/node/issue';
import { BrowserWindow, ipcMain, screen, dialog, IpcMainEvent, Display } from 'electron';
import { BrowserWindow, ipcMain, screen, Event as IpcMainEvent, Display, shell } from 'electron';
import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService';
import { PerformanceInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics';
import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform';
import { ILogService } from 'vs/platform/log/common/log';
import { IWindowState, IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
import { IWindowState } from 'vs/platform/windows/electron-main/windows';
import { listProcesses } from 'vs/base/node/ps';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
const DEFAULT_BACKGROUND_COLOR = '#1E1E1E';
@@ -33,7 +34,7 @@ export class IssueMainService implements IIssueService {
@ILaunchMainService private readonly launchMainService: ILaunchMainService,
@ILogService private readonly logService: ILogService,
@IDiagnosticsService private readonly diagnosticsService: IDiagnosticsService,
@IWindowsMainService private readonly windowsMainService: IWindowsMainService
@IDialogMainService private readonly dialogMainService: IDialogMainService
) {
this.registerListeners();
}
@@ -89,7 +90,7 @@ export class IssueMainService implements IIssueService {
};
if (this._issueWindow) {
dialog.showMessageBox(this._issueWindow, messageOptions)
this.dialogMainService.showMessageBox(messageOptions, this._issueWindow)
.then(result => {
event.sender.send('vscode:issueReporterClipboardResponse', result.response === 0);
});
@@ -113,7 +114,7 @@ export class IssueMainService implements IIssueService {
};
if (this._issueWindow) {
dialog.showMessageBox(this._issueWindow, messageOptions)
this.dialogMainService.showMessageBox(messageOptions, this._issueWindow)
.then(result => {
if (result.response === 0) {
if (this._issueWindow) {
@@ -146,7 +147,7 @@ export class IssueMainService implements IIssueService {
});
ipcMain.on('vscode:openExternal', (_: unknown, arg: string) => {
this.windowsMainService.openExternal(arg);
shell.openExternal(arg);
});
ipcMain.on('vscode:closeIssueReporter', (event: IpcMainEvent) => {

View File

@@ -141,6 +141,7 @@ export class LaunchMainService implements ILaunchMainService {
}
}
// Open new Window
if (openNewWindow) {
usedWindows = this.windowsMainService.open({
context,
@@ -150,8 +151,18 @@ export class LaunchMainService implements ILaunchMainService {
forceEmpty: true,
waitMarkerFileURI
});
} else {
usedWindows = [this.windowsMainService.focusLastActive(args, context)];
}
// Focus existing window or open if none opened
else {
const lastActive = this.windowsMainService.getLastActiveWindow();
if (lastActive) {
lastActive.focus();
usedWindows = [lastActive];
} else {
usedWindows = this.windowsMainService.open({ context, cli: args, forceEmpty: true });
}
}
}
@@ -216,7 +227,7 @@ export class LaunchMainService implements ILaunchMainService {
mainPID: process.pid,
mainArguments: process.argv.slice(1),
windows,
screenReader: !!app.accessibilitySupportEnabled,
screenReader: !!app.isAccessibilitySupportEnabled(),
gpuFeatureStatus: app.getGPUFeatureStatus()
});
}

View File

@@ -87,4 +87,10 @@ export class BufferLogService extends AbstractLogService implements ILogService
this._logger.dispose();
}
}
flush(): void {
if (this._logger) {
this._logger.flush();
}
}
}

View File

@@ -78,8 +78,7 @@ export class FileLogService extends AbstractLogService implements ILogService {
}
}
flush(): Promise<void> {
return this.queue.queue(() => Promise.resolve());
flush(): void {
}
log(level: LogLevel, args: any[]): void {

View File

@@ -41,6 +41,11 @@ export interface ILogger extends IDisposable {
warn(message: string, ...args: any[]): void;
error(message: string | Error, ...args: any[]): void;
critical(message: string | Error, ...args: any[]): void;
/**
* An operation to flush the contents. Can be synchronous.
*/
flush(): void;
}
export interface ILogService extends ILogger {
@@ -69,6 +74,7 @@ export abstract class AbstractLogService extends Disposable {
getLevel(): LogLevel {
return this.level;
}
}
export class ConsoleLogMainService extends AbstractLogService implements ILogService {
@@ -145,6 +151,11 @@ export class ConsoleLogMainService extends AbstractLogService implements ILogSer
dispose(): void {
// noop
}
flush(): void {
// noop
}
}
export class ConsoleLogService extends AbstractLogService implements ILogService {
@@ -192,7 +203,13 @@ export class ConsoleLogService extends AbstractLogService implements ILogService
}
}
dispose(): void { }
dispose(): void {
// noop
}
flush(): void {
// noop
}
}
export class ConsoleLogInMainService extends AbstractLogService implements ILogService {
@@ -240,7 +257,13 @@ export class ConsoleLogInMainService extends AbstractLogService implements ILogS
}
}
dispose(): void { }
dispose(): void {
// noop
}
flush(): void {
// noop
}
}
export class MultiplexLogService extends AbstractLogService implements ILogService {
@@ -296,6 +319,12 @@ export class MultiplexLogService extends AbstractLogService implements ILogServi
}
}
flush(): void {
for (const logService of this.logServices) {
logService.flush();
}
}
dispose(): void {
for (const logService of this.logServices) {
logService.dispose();
@@ -346,6 +375,10 @@ export class DelegatedLogService extends Disposable implements ILogService {
critical(message: string | Error, ...args: any[]): void {
this.logService.critical(message, ...args);
}
flush(): void {
this.logService.flush();
}
}
export class NullLogService implements ILogService {
@@ -360,6 +393,7 @@ export class NullLogService implements ILogService {
error(message: string | Error, ...args: any[]): void { }
critical(message: string | Error, ...args: any[]): void { }
dispose(): void { }
flush(): void { }
}
export function getLogLevel(environmentService: IEnvironmentService): LogLevel {

View File

@@ -32,7 +32,7 @@ export class LoggerService extends Disposable implements ILoggerService {
if (resource.scheme === Schemas.file) {
const baseName = basename(resource);
const ext = extname(resource);
logger = new SpdLogService(baseName.substring(0, baseName.length - ext.length), dirname(resource).path, this.logService.getLevel());
logger = new SpdLogService(baseName.substring(0, baseName.length - ext.length), dirname(resource).fsPath, this.logService.getLevel());
} else {
logger = this.instantiationService.createInstance(FileLogService, basename(resource), resource, this.logService.getLevel());
}

View File

@@ -30,7 +30,7 @@ interface ILog {
message: string;
}
function log(logger: spdlog.RotatingLogger, level: LogLevel, message: string, sync: boolean): void {
function log(logger: spdlog.RotatingLogger, level: LogLevel, message: string): void {
switch (level) {
case LogLevel.Trace: logger.trace(message); break;
case LogLevel.Debug: logger.debug(message); break;
@@ -40,9 +40,6 @@ function log(logger: spdlog.RotatingLogger, level: LogLevel, message: string, sy
case LogLevel.Critical: logger.critical(message); break;
default: throw new Error('Invalid log level');
}
if (sync) {
logger.flush();
}
}
export class SpdLogService extends AbstractLogService implements ILogService {
@@ -53,7 +50,7 @@ export class SpdLogService extends AbstractLogService implements ILogService {
private _loggerCreationPromise: Promise<void> | undefined = undefined;
private _logger: spdlog.RotatingLogger | undefined;
constructor(private readonly name: string, private readonly logsFolder: string, level: LogLevel, private readonly sync: boolean = false) {
constructor(private readonly name: string, private readonly logsFolder: string, level: LogLevel) {
super();
this.setLevel(level);
this._createSpdLogLogger();
@@ -72,7 +69,7 @@ export class SpdLogService extends AbstractLogService implements ILogService {
this._logger = logger;
this._logger.setLevel(this.getLevel());
for (const { level, message } of this.buffer) {
log(this._logger, level, message, this.sync);
log(this._logger, level, message);
}
this.buffer = [];
}
@@ -83,7 +80,7 @@ export class SpdLogService extends AbstractLogService implements ILogService {
private _log(level: LogLevel, message: string): void {
if (this._logger) {
log(this._logger, level, message, this.sync);
log(this._logger, level, message);
} else if (this.getLevel() <= level) {
this.buffer.push({ level, message });
}
@@ -132,6 +129,14 @@ export class SpdLogService extends AbstractLogService implements ILogService {
}
}
flush(): void {
if (this._logger) {
this._logger.flush();
} else if (this._loggerCreationPromise) {
this._loggerCreationPromise.then(() => this.flush());
}
}
dispose(): void {
if (this._logger) {
this.disposeLogger();

View File

@@ -6,7 +6,7 @@
import * as nls from 'vs/nls';
import { isMacintosh, language } from 'vs/base/common/platform';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { app, shell, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, Event, KeyboardEvent } from 'electron';
import { app, shell, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, Event, Event as KeyboardEvent } from 'electron';
import { OpenContext, IRunActionInWindowRequest, getTitleBarStyle, IRunKeybindingInWindowRequest, IWindowOpenable } from 'vs/platform/windows/common/windows';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -368,13 +368,13 @@ export class Menubar {
const servicesMenu = new Menu();
const services = new MenuItem({ label: nls.localize('mServices', "Services"), role: 'services', submenu: servicesMenu });
const hide = new MenuItem({ label: nls.localize('mHide', "Hide {0}", product.nameLong), role: 'hide', accelerator: 'Command+H' });
const hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideOthers', accelerator: 'Command+Alt+H' });
const hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideothers', accelerator: 'Command+Alt+H' });
const showAll = new MenuItem({ label: nls.localize('mShowAll', "Show All"), role: 'unhide' });
const quit = new MenuItem(this.likeAction('workbench.action.quit', {
label: nls.localize('miQuit', "Quit {0}", product.nameLong), click: () => {
if (
this.windowsMainService.getWindowCount() === 0 || // allow to quit when no more windows are open
!!this.windowsMainService.getFocusedWindow() || // allow to quit when window has focus (fix for https://github.com/Microsoft/vscode/issues/39191)
this.windowsMainService.getWindowCount() === 0 || // allow to quit when no more windows are open
!!BrowserWindow.getFocusedWindow() || // allow to quit when window has focus (fix for https://github.com/Microsoft/vscode/issues/39191)
this.windowsMainService.getLastActiveWindow()!.isMinimized() // allow to quit when window has no focus but is minimized (https://github.com/Microsoft/vscode/issues/63000)
) {
this.windowsMainService.quit();
@@ -557,7 +557,7 @@ export class Menubar {
label: this.mnemonicLabel(nls.localize('miCheckForUpdates', "Check for &&Updates...")), click: () => setTimeout(() => {
this.reportMenuActionTelemetry('CheckForUpdate');
const focusedWindow = this.windowsMainService.getFocusedWindow();
const focusedWindow = BrowserWindow.getFocusedWindow();
const context = focusedWindow ? { windowId: focusedWindow.id } : null;
this.updateService.checkForUpdates(context);
}, 0)
@@ -697,15 +697,16 @@ export class Menubar {
private makeContextAwareClickHandler(click: () => void, contextSpecificHandlers: IMenuItemClickHandler): () => void {
return () => {
// No Active Window
const activeWindow = this.windowsMainService.getFocusedWindow();
const activeWindow = BrowserWindow.getFocusedWindow();
if (!activeWindow) {
return contextSpecificHandlers.inNoWindow();
}
// DevTools focused
if (activeWindow.win.webContents.isDevToolsFocused()) {
return contextSpecificHandlers.inDevTools(activeWindow.win.webContents.devToolsWebContents);
if (activeWindow.webContents.isDevToolsFocused()) {
return contextSpecificHandlers.inDevTools(activeWindow.webContents.devToolsWebContents);
}
// Finally execute command in Window
@@ -719,14 +720,15 @@ export class Menubar {
// https://github.com/Microsoft/vscode/issues/11928
// Still allow to run when the last active window is minimized though for
// https://github.com/Microsoft/vscode/issues/63000
let activeWindow = this.windowsMainService.getFocusedWindow();
if (!activeWindow) {
let activeBrowserWindow = BrowserWindow.getFocusedWindow();
if (!activeBrowserWindow) {
const lastActiveWindow = this.windowsMainService.getLastActiveWindow();
if (lastActiveWindow && lastActiveWindow.isMinimized()) {
activeWindow = lastActiveWindow;
activeBrowserWindow = lastActiveWindow.win;
}
}
const activeWindow = activeBrowserWindow ? this.windowsMainService.getWindowById(activeBrowserWindow.id) : undefined;
if (activeWindow) {
this.logService.trace('menubar#runActionInRenderer', invocation);

View File

@@ -27,7 +27,7 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address:
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
return undefined;
}
const localhostMatch = /^(localhost|127\.0\.0\.1):(\d+)$/.exec(uri.authority);
const localhostMatch = /^(localhost|127\.0\.0\.1|0\.0\.0\.0):(\d+)$/.exec(uri.authority);
if (!localhostMatch) {
return undefined;
}

View File

@@ -70,6 +70,7 @@ class TestableLogService extends AbstractLogService implements ILogService {
}
dispose(): void { }
flush(): void { }
}
suite('AIAdapter', () => {
@@ -198,4 +199,4 @@ suite('AIAdapter', () => {
}
}]));
});
});
});

View File

@@ -63,6 +63,12 @@ export function registerConfiguration(): IDisposable {
$ref: ignoredSettingsSchemaId,
additionalProperties: true,
uniqueItems: true
},
'configurationSync.enableAuth': {
'type': 'boolean',
description: localize('configurationSync.enableAuth', "Enables authentication and requires VS Code restart when changed"),
'default': false,
'scope': ConfigurationScope.APPLICATION
}
}
});

View File

@@ -44,4 +44,8 @@ export class UserDataSyncLogService extends AbstractLogService implements IUserD
this.logger.critical(message, ...args);
}
flush(): void {
this.logger.flush();
}
}

View File

@@ -131,27 +131,32 @@ export class UserDataAutoSync extends Disposable {
@IAuthTokenService private readonly authTokenService: IAuthTokenService,
) {
super();
this.updateEnablement();
this.sync(true);
this._register(Event.any<any>(authTokenService.onDidChangeStatus, userDataSyncService.onDidChangeStatus)(() => this.updateEnablement()));
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('configurationSync.enable'))(() => this.updateEnablement()));
this.updateEnablement(false);
this._register(Event.any<any>(authTokenService.onDidChangeStatus, userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true)));
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('configurationSync.enable'))(() => this.updateEnablement(true)));
// Sync immediately if there is a local change.
this._register(Event.debounce(this.userDataSyncService.onDidChangeLocal, () => undefined, 500)(() => this.sync(false)));
}
private updateEnablement(): void {
private updateEnablement(stopIfDisabled: boolean): void {
const enabled = this.isSyncEnabled();
if (this.enabled !== enabled) {
this.enabled = enabled;
if (this.enabled) {
this.userDataSyncLogService.info('Syncing configuration started');
this.sync(true);
} else {
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
if (this.enabled) {
this.userDataSyncLogService.info('Syncing configuration started');
this.sync(true);
return;
} else {
if (stopIfDisabled) {
this.userDataSyncService.stop();
this.userDataSyncLogService.info('Syncing configuration stopped.');
}
}
}
private async sync(loop: boolean): Promise<void> {

View File

@@ -19,15 +19,17 @@ export interface IOpenedWindow {
filename?: string;
}
export interface IOpenInWindowOptions {
forceNewWindow?: boolean;
export interface IBaseOpenWindowsOptions {
forceReuseWindow?: boolean;
}
export interface IOpenWindowOptions extends IBaseOpenWindowsOptions {
forceNewWindow?: boolean;
noRecentEntry?: boolean;
}
export interface IOpenEmptyWindowOptions {
reuse?: boolean;
export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions {
remoteAuthority?: string;
}

View File

@@ -9,10 +9,10 @@ import { ParsedArgs } from 'vs/platform/environment/common/environment';
import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { IWorkspaceIdentifier, IEnterWorkspaceResult } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { URI } from 'vs/base/common/uri';
import { MessageBoxReturnValue, SaveDialogReturnValue, OpenDialogReturnValue, Rectangle, BrowserWindow, MessageBoxOptions, SaveDialogOptions, OpenDialogOptions } from 'electron';
import { Rectangle, BrowserWindow } from 'electron';
export interface IWindowState {
width?: number;
@@ -86,38 +86,34 @@ export interface IWindowsCountChangedEvent {
}
export interface IWindowsMainService {
_serviceBrand: undefined;
// events
readonly onWindowReady: Event<ICodeWindow>;
readonly onWindowsCountChanged: Event<IWindowsCountChangedEvent>;
readonly onWindowClose: Event<number>;
// methods
reload(win: ICodeWindow, cli?: ParsedArgs): void;
enterWorkspace(win: ICodeWindow, path: URI): Promise<IEnterWorkspaceResult | undefined>;
closeWorkspace(win: ICodeWindow): void;
open(openConfig: IOpenConfiguration): ICodeWindow[];
openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): void;
openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[];
openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[];
pickFileFolderAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise<void>;
pickFolderAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise<void>;
pickFileAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise<void>;
pickWorkspaceAndOpen(options: INativeOpenDialogOptions, win?: ICodeWindow): Promise<void>;
showMessageBox(options: MessageBoxOptions, win?: ICodeWindow): Promise<MessageBoxReturnValue>;
showSaveDialog(options: SaveDialogOptions, win?: ICodeWindow): Promise<SaveDialogReturnValue>;
showOpenDialog(options: OpenDialogOptions, win?: ICodeWindow): Promise<OpenDialogReturnValue>;
focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow;
getLastActiveWindow(): ICodeWindow | undefined;
waitForWindowCloseOrLoad(windowId: number): Promise<void>;
openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[];
openNewTabbedWindow(context: OpenContext): ICodeWindow[];
openExternal(url: string): Promise<boolean>;
sendToFocused(channel: string, ...args: any[]): void;
sendToAll(channel: string, payload: any, windowIdsToIgnore?: number[]): void;
getFocusedWindow(): ICodeWindow | undefined;
getLastActiveWindow(): ICodeWindow | undefined;
getWindowById(windowId: number): ICodeWindow | undefined;
getWindows(): ICodeWindow[];
getWindowCount(): number;
waitForWindowCloseOrLoad(windowId: number): Promise<void>;
reload(win: ICodeWindow, cli?: ParsedArgs): void;
closeWorkspace(win: ICodeWindow): void;
quit(): void;
}
@@ -139,8 +135,3 @@ export interface IOpenConfiguration {
readonly initialStartup?: boolean;
readonly noRecentEntry?: boolean;
}
export interface ISharedProcess {
whenReady(): Promise<void>;
toggle(): void;
}

View File

@@ -3,12 +3,136 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IOpenInWindowOptions } from 'vs/platform/windows/common/windows';
import { OpenContext, IOpenWindowOptions } from 'vs/platform/windows/common/windows';
import { URI } from 'vs/base/common/uri';
import * as platform from 'vs/base/common/platform';
import * as extpath from 'vs/base/common/extpath';
import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { isEqual, isEqualOrParent } from 'vs/base/common/resources';
export interface INativeOpenInWindowOptions extends IOpenInWindowOptions {
export interface INativeOpenWindowOptions extends IOpenWindowOptions {
diffMode?: boolean;
addMode?: boolean;
gotoLineMode?: boolean;
waitMarkerFileURI?: URI;
}
export interface IWindowContext {
openedWorkspace?: IWorkspaceIdentifier;
openedFolderUri?: URI;
extensionDevelopmentPath?: string[];
lastFocusTime: number;
}
export interface IBestWindowOrFolderOptions<W extends IWindowContext> {
windows: W[];
newWindow: boolean;
context: OpenContext;
fileUri?: URI;
userHome?: string;
codeSettingsFolder?: string;
localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null;
}
export function findBestWindowOrFolderForFile<W extends IWindowContext>({ windows, newWindow, context, fileUri, localWorkspaceResolver: workspaceResolver }: IBestWindowOrFolderOptions<W>): W | undefined {
if (!newWindow && fileUri && (context === OpenContext.DESKTOP || context === OpenContext.CLI || context === OpenContext.DOCK)) {
const windowOnFilePath = findWindowOnFilePath(windows, fileUri, workspaceResolver);
if (windowOnFilePath) {
return windowOnFilePath;
}
}
return !newWindow ? getLastActiveWindow(windows) : undefined;
}
function findWindowOnFilePath<W extends IWindowContext>(windows: W[], fileUri: URI, localWorkspaceResolver: (workspace: IWorkspaceIdentifier) => IResolvedWorkspace | null): W | null {
// First check for windows with workspaces that have a parent folder of the provided path opened
for (const window of windows) {
const workspace = window.openedWorkspace;
if (workspace) {
const resolvedWorkspace = localWorkspaceResolver(workspace);
if (resolvedWorkspace) {
// workspace could be resolved: It's in the local file system
if (resolvedWorkspace.folders.some(folder => isEqualOrParent(fileUri, folder.uri))) {
return window;
}
} else {
// use the config path instead
if (isEqualOrParent(fileUri, workspace.configPath)) {
return window;
}
}
}
}
// Then go with single folder windows that are parent of the provided file path
const singleFolderWindowsOnFilePath = windows.filter(window => window.openedFolderUri && isEqualOrParent(fileUri, window.openedFolderUri));
if (singleFolderWindowsOnFilePath.length) {
return singleFolderWindowsOnFilePath.sort((a, b) => -(a.openedFolderUri!.path.length - b.openedFolderUri!.path.length))[0];
}
return null;
}
export function getLastActiveWindow<W extends IWindowContext>(windows: W[]): W | undefined {
const lastFocusedDate = Math.max.apply(Math, windows.map(window => window.lastFocusTime));
return windows.filter(window => window.lastFocusTime === lastFocusedDate)[0];
}
export function findWindowOnWorkspace<W extends IWindowContext>(windows: W[], workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)): W | null {
if (isSingleFolderWorkspaceIdentifier(workspace)) {
for (const window of windows) {
// match on folder
if (isSingleFolderWorkspaceIdentifier(workspace)) {
if (window.openedFolderUri && isEqual(window.openedFolderUri, workspace)) {
return window;
}
}
}
} else if (isWorkspaceIdentifier(workspace)) {
for (const window of windows) {
// match on workspace
if (window.openedWorkspace && window.openedWorkspace.id === workspace.id) {
return window;
}
}
}
return null;
}
export function findWindowOnExtensionDevelopmentPath<W extends IWindowContext>(windows: W[], extensionDevelopmentPaths: string[]): W | null {
const matches = (uriString: string): boolean => {
return extensionDevelopmentPaths.some(p => extpath.isEqual(p, uriString, !platform.isLinux /* ignorecase */));
};
for (const window of windows) {
// match on extension development path. The path can be one or more paths or uri strings, using paths.isEqual is not 100% correct but good enough
const currPaths = window.extensionDevelopmentPath;
if (currPaths && currPaths.some(p => matches(p))) {
return window;
}
}
return null;
}
export function findWindowOnWorkspaceOrFolderUri<W extends IWindowContext>(windows: W[], uri: URI | undefined): W | null {
if (!uri) {
return null;
}
for (const window of windows) {
// check for workspace config path
if (window.openedWorkspace && isEqual(window.openedWorkspace.configPath, uri)) {
return window;
}
// check for folder path
if (window.openedFolderUri && isEqual(window.openedFolderUri, uri)) {
return window;
}
}
return null;
}

View File

@@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as path from 'vs/base/common/path';
import { IBestWindowOrFolderOptions, IWindowContext, findBestWindowOrFolderForFile } from 'vs/platform/windows/node/window';
import { OpenContext } from 'vs/platform/windows/common/windows';
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { getPathFromAmdModule } from 'vs/base/common/amd';
const fixturesFolder = getPathFromAmdModule(require, './fixtures');
const testWorkspace: IWorkspaceIdentifier = {
id: Date.now().toString(),
configPath: URI.file(path.join(fixturesFolder, 'workspaces.json'))
};
const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath);
function options(custom?: Partial<IBestWindowOrFolderOptions<IWindowContext>>): IBestWindowOrFolderOptions<IWindowContext> {
return {
windows: [],
newWindow: false,
context: OpenContext.CLI,
codeSettingsFolder: '_vscode',
localWorkspaceResolver: workspace => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: testWorkspaceFolders } : null; },
...custom
};
}
const vscodeFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder')) };
const lastActiveWindow: IWindowContext = { lastFocusTime: 3, openedFolderUri: undefined };
const noVscodeFolderWindow: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) };
const windows: IWindowContext[] = [
vscodeFolderWindow,
lastActiveWindow,
noVscodeFolderWindow,
];
suite('WindowsFinder', () => {
test('New window without folder when no windows exist', () => {
assert.equal(findBestWindowOrFolderForFile(options()), null);
assert.equal(findBestWindowOrFolderForFile(options({
fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt'))
})), null);
assert.equal(findBestWindowOrFolderForFile(options({
fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')),
newWindow: true
})), null);
assert.equal(findBestWindowOrFolderForFile(options({
fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')),
})), null);
assert.equal(findBestWindowOrFolderForFile(options({
fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')),
context: OpenContext.API
})), null);
assert.equal(findBestWindowOrFolderForFile(options({
fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt'))
})), null);
assert.equal(findBestWindowOrFolderForFile(options({
fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'new_folder', 'new_file.txt'))
})), null);
});
test('New window without folder when windows exist', () => {
assert.equal(findBestWindowOrFolderForFile(options({
windows,
fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')),
newWindow: true
})), null);
});
test('Last active window', () => {
assert.equal(findBestWindowOrFolderForFile(options({
windows
})), lastActiveWindow);
assert.equal(findBestWindowOrFolderForFile(options({
windows,
fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder2', 'file.txt'))
})), lastActiveWindow);
assert.equal(findBestWindowOrFolderForFile(options({
windows: [lastActiveWindow, noVscodeFolderWindow],
fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt')),
})), lastActiveWindow);
assert.equal(findBestWindowOrFolderForFile(options({
windows,
fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt')),
context: OpenContext.API
})), lastActiveWindow);
});
test('Existing window with folder', () => {
assert.equal(findBestWindowOrFolderForFile(options({
windows,
fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'file.txt'))
})), noVscodeFolderWindow);
assert.equal(findBestWindowOrFolderForFile(options({
windows,
fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'file.txt'))
})), vscodeFolderWindow);
const window: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder')) };
assert.equal(findBestWindowOrFolderForFile(options({
windows: [window],
fileUri: URI.file(path.join(fixturesFolder, 'vscode_folder', 'nested_folder', 'subfolder', 'file.txt'))
})), window);
});
test('More specific existing window wins', () => {
const window: IWindowContext = { lastFocusTime: 2, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder')) };
const nestedFolderWindow: IWindowContext = { lastFocusTime: 1, openedFolderUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder')) };
assert.equal(findBestWindowOrFolderForFile(options({
windows: [window, nestedFolderWindow],
fileUri: URI.file(path.join(fixturesFolder, 'no_vscode_folder', 'nested_folder', 'subfolder', 'file.txt'))
})), nestedFolderWindow);
});
test('Workspace folder wins', () => {
const window: IWindowContext = { lastFocusTime: 1, openedWorkspace: testWorkspace };
assert.equal(findBestWindowOrFolderForFile(options({
windows: [window],
fileUri: URI.file(path.join(fixturesFolder, 'vscode_workspace_2_folder', 'nested_vscode_folder', 'subfolder', 'file.txt'))
})), window);
});
});

View File

@@ -31,7 +31,7 @@ export interface IWorkspacesService {
_serviceBrand: undefined;
// Management
enterWorkspace(path: URI): Promise<IEnterWorkspaceResult | undefined>;
enterWorkspace(path: URI): Promise<IEnterWorkspaceResult | null>;
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>;
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
getWorkspaceIdentifier(workspacePath: URI): Promise<IWorkspaceIdentifier>;

View File

@@ -23,6 +23,7 @@ import { getSimpleWorkspaceLabel } from 'vs/platform/label/common/label';
import { exists } from 'vs/base/node/pfs';
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Disposable } from 'vs/base/common/lifecycle';
export const IWorkspacesHistoryMainService = createDecorator<IWorkspacesHistoryMainService>('workspacesHistoryMainService');
@@ -40,7 +41,7 @@ export interface IWorkspacesHistoryMainService {
updateWindowsJumpList(): void;
}
export class WorkspacesHistoryMainService implements IWorkspacesHistoryMainService {
export class WorkspacesHistoryMainService extends Disposable implements IWorkspacesHistoryMainService {
private static readonly MAX_TOTAL_RECENT_ENTRIES = 100;
@@ -60,18 +61,27 @@ export class WorkspacesHistoryMainService implements IWorkspacesHistoryMainServi
private readonly _onRecentlyOpenedChange = new Emitter<void>();
readonly onRecentlyOpenedChange: CommonEvent<void> = this._onRecentlyOpenedChange.event;
private macOSRecentDocumentsUpdater: ThrottledDelayer<void>;
private macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer<void>(800));
constructor(
@IStateService private readonly stateService: IStateService,
@ILogService private readonly logService: ILogService,
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ILifecycleMainService lifecycleMainService: ILifecycleMainService
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService
) {
this.macOSRecentDocumentsUpdater = new ThrottledDelayer<void>(800);
super();
lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList());
this.registerListeners();
}
private registerListeners(): void {
// Install window jump list after opening window
this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => this.handleWindowsJumpList());
// Add to history when entering workspace
this._register(this.workspacesMainService.onWorkspaceEntered(event => this.addRecentlyOpened([{ workspace: event.workspace }])));
}
private handleWindowsJumpList(): void {

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder, IEnterWorkspaceResult } from 'vs/platform/workspaces/common/workspaces';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { join, dirname } from 'vs/base/common/path';
import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs';
@@ -17,31 +17,43 @@ import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { Disposable } from 'vs/base/common/lifecycle';
import { originalFSPath, isEqualOrParent, joinPath } from 'vs/base/common/resources';
import { originalFSPath, isEqualOrParent, joinPath, isEqual, basename } from 'vs/base/common/resources';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { localize } from 'vs/nls';
import product from 'vs/platform/product/common/product';
import { MessageBoxOptions, BrowserWindow } from 'electron';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
import { findWindowOnWorkspace } from 'vs/platform/windows/node/window';
export const IWorkspacesMainService = createDecorator<IWorkspacesMainService>('workspacesMainService');
export interface IWorkspaceEnteredEvent {
window: ICodeWindow;
workspace: IWorkspaceIdentifier;
}
export interface IWorkspacesMainService {
_serviceBrand: undefined;
readonly onUntitledWorkspaceDeleted: Event<IWorkspaceIdentifier>;
readonly onWorkspaceEntered: Event<IWorkspaceEnteredEvent>;
enterWorkspace(intoWindow: ICodeWindow, openedWindows: ICodeWindow[], path: URI): Promise<IEnterWorkspaceResult | null>;
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>;
createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier;
resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null;
isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean;
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void;
getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[];
isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean;
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>;
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null;
getWorkspaceIdentifier(workspacePath: URI): Promise<IWorkspaceIdentifier>;
}
@@ -59,9 +71,14 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter<IWorkspaceIdentifier>());
readonly onUntitledWorkspaceDeleted: Event<IWorkspaceIdentifier> = this._onUntitledWorkspaceDeleted.event;
private readonly _onWorkspaceEntered = this._register(new Emitter<IWorkspaceEnteredEvent>());
readonly onWorkspaceEntered: Event<IWorkspaceEnteredEvent> = this._onWorkspaceEntered.event;
constructor(
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ILogService private readonly logService: ILogService
@ILogService private readonly logService: ILogService,
@IBackupMainService private readonly backupMainService: IBackupMainService,
@IDialogMainService private readonly dialogMainService: IDialogMainService
) {
super();
@@ -236,6 +253,74 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
}
return untitledWorkspaces;
}
async enterWorkspace(window: ICodeWindow, windows: ICodeWindow[], path: URI): Promise<IEnterWorkspaceResult | null> {
if (!window || !window.win || !window.isReady) {
return null; // return early if the window is not ready or disposed
}
const isValid = await this.isValidTargetWorkspacePath(window, windows, path);
if (!isValid) {
return null; // return early if the workspace is not valid
}
const result = this.doEnterWorkspace(window, getWorkspaceIdentifier(path));
// Emit as event
this._onWorkspaceEntered.fire({ window, workspace: result.workspace });
return result;
}
private async isValidTargetWorkspacePath(window: ICodeWindow, windows: ICodeWindow[], path?: URI): Promise<boolean> {
if (!path) {
return true;
}
if (window.openedWorkspace && isEqual(window.openedWorkspace.configPath, path)) {
return false; // window is already opened on a workspace with that path
}
// Prevent overwriting a workspace that is currently opened in another window
if (findWindowOnWorkspace(windows, getWorkspaceIdentifier(path))) {
const options: MessageBoxOptions = {
title: product.nameLong,
type: 'info',
buttons: [localize('ok', "OK")],
message: localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)),
detail: localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again."),
noLink: true
};
await this.dialogMainService.showMessageBox(options, withNullAsUndefined(BrowserWindow.getFocusedWindow()));
return false;
}
return true; // OK
}
private doEnterWorkspace(window: ICodeWindow, workspace: IWorkspaceIdentifier): IEnterWorkspaceResult {
window.focus();
// Register window for backups and migrate current backups over
let backupPath: string | undefined;
if (!window.config.extensionDevelopmentPath) {
backupPath = this.backupMainService.registerWorkspaceBackupSync({ workspace, remoteAuthority: window.remoteAuthority }, window.config.backupPath);
}
// if the window was opened on an untitled workspace, delete it.
if (window.openedWorkspace && this.isUntitledWorkspace(window.openedWorkspace)) {
this.deleteUntitledWorkspaceSync(window.openedWorkspace);
}
// Update window configuration properly based on transition to workspace
window.config.folderUri = undefined;
window.config.workspace = workspace;
window.config.backupPath = backupPath;
return { workspace, backupPath };
}
}
function getWorkspaceId(configPath: URI): string {

View File

@@ -23,13 +23,13 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
//#region Workspace Management
async enterWorkspace(windowId: number, path: URI): Promise<IEnterWorkspaceResult | undefined> {
async enterWorkspace(windowId: number, path: URI): Promise<IEnterWorkspaceResult | null> {
const window = this.windowsMainService.getWindowById(windowId);
if (window) {
return this.windowsMainService.enterWorkspace(window, path);
return this.workspacesMainService.enterWorkspace(window, this.windowsMainService.getWindows(), path);
}
return undefined;
return null;
}
createUntitledWorkspace(windowId: number, folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier> {

View File

@@ -18,6 +18,7 @@ import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { isWindows } from 'vs/base/common/platform';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { dirname, joinPath } from 'vs/base/common/resources';
import { TestBackupMainService, TestDialogMainService } from 'vs/workbench/test/workbenchTestServices';
suite('WorkspacesMainService', () => {
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'workspacesservice');
@@ -43,7 +44,7 @@ suite('WorkspacesMainService', () => {
let service: WorkspacesMainService;
setup(async () => {
service = new WorkspacesMainService(environmentService, logService);
service = new WorkspacesMainService(environmentService, logService, new TestBackupMainService(), new TestDialogMainService());
// Delete any existing backups completely and then re-create it.
await pfs.rimraf(untitledWorkspacesHomePath, pfs.RimRafMode.MOVE);