Merge from vscode e0762af258c0b20320ed03f3871a41967acc4421 (#7404)

* Merge from vscode e0762af258c0b20320ed03f3871a41967acc4421

* readd svgs
This commit is contained in:
Anthony Dresser
2019-09-27 11:13:19 -07:00
committed by GitHub
parent 6385443a4c
commit 07109617b5
348 changed files with 4219 additions and 4307 deletions

View File

@@ -49,7 +49,7 @@ const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(u
class TestBackupEnvironmentService extends WorkbenchEnvironmentService {
constructor(backupPath: string) {
super({ ...parseArgs(process.argv, OPTIONS), ...{ backupPath, 'user-data-dir': userdataDir } } as IWindowConfiguration, process.execPath);
super({ ...parseArgs(process.argv, OPTIONS), ...{ backupPath, 'user-data-dir': userdataDir } } as IWindowConfiguration, process.execPath, 0);
}
}

View File

@@ -15,6 +15,7 @@ export const machineSettingsSchemaId = 'vscode://schemas/settings/machine';
export const workspaceSettingsSchemaId = 'vscode://schemas/settings/workspace';
export const folderSettingsSchemaId = 'vscode://schemas/settings/folder';
export const launchSchemaId = 'vscode://schemas/launch';
export const tasksSchemaId = 'vscode://schemas/tasks';
export const LOCAL_MACHINE_SCOPES = [ConfigurationScope.APPLICATION, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE];
export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.MACHINE_OVERRIDABLE];

View File

@@ -1,107 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { writeFile } from 'vs/base/node/pfs';
import product from 'vs/platform/product/common/product';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ICommandService } from 'vs/platform/commands/common/commands';
interface IExportedConfigurationNode {
name: string;
description: string;
default: any;
type?: string | string[];
enum?: any[];
enumDescriptions?: string[];
}
interface IConfigurationExport {
settings: IExportedConfigurationNode[];
buildTime: number;
commit?: string;
buildNumber?: number;
}
export class DefaultConfigurationExportHelper {
constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IExtensionService private readonly extensionService: IExtensionService,
@ICommandService private readonly commandService: ICommandService) {
if (environmentService.args['export-default-configuration']) {
this.writeConfigModelAndQuit(environmentService.args['export-default-configuration']);
}
}
private writeConfigModelAndQuit(targetPath: string): Promise<void> {
return Promise.resolve(this.extensionService.whenInstalledExtensionsRegistered())
.then(() => this.writeConfigModel(targetPath))
.then(() => this.commandService.executeCommand('workbench.action.quit'))
.then(() => { });
}
private writeConfigModel(targetPath: string): Promise<void> {
const config = this.getConfigModel();
const resultString = JSON.stringify(config, undefined, ' ');
return writeFile(targetPath, resultString);
}
private getConfigModel(): IConfigurationExport {
const configRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
const configurations = configRegistry.getConfigurations().slice();
const settings: IExportedConfigurationNode[] = [];
const processProperty = (name: string, prop: IConfigurationPropertySchema) => {
const propDetails: IExportedConfigurationNode = {
name,
description: prop.description || prop.markdownDescription || '',
default: prop.default,
type: prop.type
};
if (prop.enum) {
propDetails.enum = prop.enum;
}
if (prop.enumDescriptions || prop.markdownEnumDescriptions) {
propDetails.enumDescriptions = prop.enumDescriptions || prop.markdownEnumDescriptions;
}
settings.push(propDetails);
};
const processConfig = (config: IConfigurationNode) => {
if (config.properties) {
for (let name in config.properties) {
processProperty(name, config.properties[name]);
}
}
if (config.allOf) {
config.allOf.forEach(processConfig);
}
};
configurations.forEach(processConfig);
const excludedProps = configRegistry.getExcludedConfigurationProperties();
for (let name in excludedProps) {
processProperty(name, excludedProps[name]);
}
const result: IConfigurationExport = {
settings: settings.sort((a, b) => a.name.localeCompare(b.name)),
buildTime: Date.now(),
commit: product.commit,
buildNumber: product.settingsSearchBuildId
};
return result;
}
}

View File

@@ -46,7 +46,7 @@ import { FileUserDataProvider } from 'vs/workbench/services/userData/common/file
class TestEnvironmentService extends WorkbenchEnvironmentService {
constructor(private _appSettingsHome: URI) {
super(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath);
super(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath, 0);
}
get appSettingsHome() { return this._appSettingsHome; }

View File

@@ -51,7 +51,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
class TestEnvironmentService extends WorkbenchEnvironmentService {
constructor(private _appSettingsHome: URI) {
super(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath);
super(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath, 0);
}
get appSettingsHome() { return this._appSettingsHome; }

View File

@@ -645,6 +645,6 @@ class MockInputsConfigurationService extends TestConfigurationService {
class MockWorkbenchEnvironmentService extends WorkbenchEnvironmentService {
constructor(env: platform.IProcessEnvironment) {
super({ userEnv: env } as IWindowConfiguration, process.execPath);
super({ userEnv: env } as IWindowConfiguration, process.execPath, 0);
}
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* 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';
export const ICredentialsService = createDecorator<ICredentialsService>('ICredentialsService');
export interface ICredentialsService {
_serviceBrand: undefined;
getPassword(service: string, account: string): Promise<string | null>;
setPassword(service: string, account: string, password: string): Promise<void>;
deletePassword(service: string, account: string): Promise<boolean>;
findPassword(service: string): Promise<string | null>;
findCredentials(service: string): Promise<Array<{ account: string, password: string }>>;
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
import { IdleValue } from 'vs/base/common/async';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';

View File

@@ -28,7 +28,7 @@ export interface IDecoration {
export interface IDecorationsProvider {
readonly label: string;
readonly onDidChange: Event<URI[]>;
readonly onDidChange: Event<readonly URI[]>;
provideDecorations(uri: URI, token: CancellationToken): IDecorationData | Promise<IDecorationData | undefined> | undefined;
}

View File

@@ -29,7 +29,7 @@ suite('DecorationsService', function () {
service.registerDecorationsProvider(new class implements IDecorationsProvider {
readonly label: string = 'Test';
readonly onDidChange: Event<URI[]> = Event.None;
readonly onDidChange: Event<readonly URI[]> = Event.None;
provideDecorations(uri: URI) {
callCounter += 1;
return new Promise<IDecorationData>(resolve => {
@@ -62,7 +62,7 @@ suite('DecorationsService', function () {
service.registerDecorationsProvider(new class implements IDecorationsProvider {
readonly label: string = 'Test';
readonly onDidChange: Event<URI[]> = Event.None;
readonly onDidChange: Event<readonly URI[]> = Event.None;
provideDecorations(uri: URI) {
callCounter += 1;
return { color: 'someBlue', tooltip: 'Z' };
@@ -80,7 +80,7 @@ suite('DecorationsService', function () {
let reg = service.registerDecorationsProvider(new class implements IDecorationsProvider {
readonly label: string = 'Test';
readonly onDidChange: Event<URI[]> = Event.None;
readonly onDidChange: Event<readonly URI[]> = Event.None;
provideDecorations(uri: URI) {
callCounter += 1;
return { color: 'someBlue', tooltip: 'J' };
@@ -155,7 +155,7 @@ suite('DecorationsService', function () {
let provider = new class implements IDecorationsProvider {
_onDidChange = new Emitter<URI[]>();
onDidChange: Event<URI[]> = this._onDidChange.event;
onDidChange: Event<readonly URI[]> = this._onDidChange.event;
label: string = 'foo';

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IWindowService, IURIToOpen, FileFilter } from 'vs/platform/windows/common/windows';
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { IWindowOpenable } from 'vs/platform/windows/common/windows';
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter } from 'vs/platform/dialogs/common/dialogs';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -18,15 +18,15 @@ import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFileService } from 'vs/platform/files/common/files';
import { isWeb } from 'vs/base/common/platform';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IHostService } from 'vs/workbench/services/host/browser/host';
export class AbstractFileDialogService {
export abstract class AbstractFileDialogService {
_serviceBrand: undefined;
constructor(
@IWindowService protected readonly windowService: IWindowService,
@IHostService protected readonly hostService: IHostService,
@IWorkspaceContextService protected readonly contextService: IWorkspaceContextService,
@IHistoryService protected readonly historyService: IHistoryService,
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
@@ -78,15 +78,7 @@ export class AbstractFileDialogService {
return this.defaultFilePath(schemeFilter);
}
protected addFileSchemaIfNeeded(schema: string): string[] {
// Include File schema unless the schema is web
// Don't allow untitled schema through.
if (isWeb) {
return schema === Schemas.untitled ? [Schemas.file] : [schema];
} else {
return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]);
}
}
protected abstract addFileSchemaIfNeeded(schema: string): string[];
protected async pickFileFolderAndOpenSimplified(schema: string, options: IPickAndOpenOptions, preferNewWindow: boolean): Promise<any> {
const title = nls.localize('openFileOrFolder.title', 'Open File Or Folder');
@@ -97,9 +89,9 @@ export class AbstractFileDialogService {
if (uri) {
const stat = await this.fileService.resolve(uri);
const toOpen: IURIToOpen = stat.isDirectory ? { folderUri: uri } : { fileUri: uri };
const toOpen: IWindowOpenable = stat.isDirectory ? { folderUri: uri } : { fileUri: uri };
if (stat.isDirectory || options.forceNewWindow || preferNewWindow) {
return this.windowService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow });
return this.hostService.openInWindow([toOpen], { forceNewWindow: options.forceNewWindow });
} else {
return this.openerService.open(uri);
}
@@ -113,7 +105,7 @@ export class AbstractFileDialogService {
const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
if (uri) {
if (options.forceNewWindow || preferNewWindow) {
return this.windowService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow });
return this.hostService.openInWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow });
} else {
return this.openerService.open(uri);
}
@@ -126,7 +118,7 @@ export class AbstractFileDialogService {
const uri = await this.pickResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
if (uri) {
return this.windowService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow });
return this.hostService.openInWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow });
}
}
@@ -137,7 +129,7 @@ export class AbstractFileDialogService {
const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems });
if (uri) {
return this.windowService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow });
return this.hostService.openInWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow });
}
}
@@ -184,7 +176,7 @@ export class AbstractFileDialogService {
return !this.environmentService.configuration.remoteAuthority ? Schemas.file : REMOTE_HOST_SCHEME;
}
protected getFileSystemSchema(options: { availableFileSystems?: string[], defaultUri?: URI }): string {
protected getFileSystemSchema(options: { availableFileSystems?: readonly string[], defaultUri?: URI }): string {
return options.availableFileSystems && options.availableFileSystems[0] || this.getSchemeFilterForWindow();
}
}

View File

@@ -7,6 +7,7 @@ import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialo
import { URI } from 'vs/base/common/uri';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { AbstractFileDialogService } from 'vs/workbench/services/dialogs/browser/abstractFileDialogService';
import { Schemas } from 'vs/base/common/network';
export class FileDialogService extends AbstractFileDialogService implements IFileDialogService {
@@ -64,6 +65,10 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const schema = this.getFileSystemSchema(options);
return this.showOpenDialogSimplified(schema, options);
}
protected addFileSchemaIfNeeded(schema: string): string[] {
return schema === Schemas.untitled ? [Schemas.file] : [schema];
}
}
registerSingleton(IFileDialogService, FileDialogService, true);

View File

@@ -210,7 +210,7 @@ export class SimpleFileDialog {
return resources.toLocalResource(URI.from({ scheme: this.scheme, path }), this.scheme === Schemas.file ? undefined : this.remoteAuthority);
}
private getScheme(available: string[] | undefined, defaultUri: URI | undefined): string {
private getScheme(available: readonly string[] | undefined, defaultUri: URI | undefined): string {
if (available) {
if (defaultUri && (available.indexOf(defaultUri.scheme) >= 0)) {
return defaultUri.scheme;
@@ -321,7 +321,7 @@ export class SimpleFileDialog {
isAcceptHandled = true;
isResolving++;
if (this.options.availableFileSystems && (this.options.availableFileSystems.length > 1)) {
this.options.availableFileSystems.shift();
this.options.availableFileSystems = this.options.availableFileSystems.slice(1);
}
this.filePickBox.hide();
if (isSave) {

View File

@@ -3,7 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWindowService, OpenDialogOptions, SaveDialogOptions, INativeOpenDialogOptions } from 'vs/platform/windows/common/windows';
import { SaveDialogOptions, OpenDialogOptions } from 'electron';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
@@ -23,7 +25,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
_serviceBrand: undefined;
constructor(
@IWindowService windowService: IWindowService,
@IHostService hostService: IHostService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IHistoryService historyService: IHistoryService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@@ -32,7 +34,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
@IFileService fileService: IFileService,
@IOpenerService openerService: IOpenerService,
@IElectronService private readonly electronService: IElectronService
) { super(windowService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService); }
) { super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService); }
private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions {
return {
@@ -172,6 +174,12 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
const result = await this.electronService.showOpenDialog(newOptions);
return result && Array.isArray(result.filePaths) && result.filePaths.length > 0 ? result.filePaths.map(URI.file) : undefined;
}
protected addFileSchemaIfNeeded(schema: string): string[] {
// Include File schema unless the schema is web
// Don't allow untitled schema through.
return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]);
}
}
registerSingleton(IFileDialogService, FileDialogService, true);

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* 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';
export const IElectronEnvironmentService = createDecorator<IElectronEnvironmentService>('electronEnvironmentService');
export interface IElectronEnvironmentService {
_serviceBrand: undefined;
readonly windowId: number;
readonly sharedIPCHandle: string;
}
export class ElectronEnvironmentService implements IElectronEnvironmentService {
_serviceBrand: undefined;
constructor(
public readonly windowId: number,
public readonly sharedIPCHandle: string
) { }
}

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { createChannelSender } from 'vs/base/parts/ipc/node/ipcChannelCreator';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
export class ElectronService {
_serviceBrand: undefined;
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
@IElectronEnvironmentService electronEnvironmentService: IElectronEnvironmentService
) {
return createChannelSender<IElectronService>(mainProcessService.getChannel('electron'), { context: electronEnvironmentService.windowId });
}
}
registerSingleton(IElectronService, ElectronService, true);

View File

@@ -85,6 +85,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
this.userRoamingDataHome = URI.file('/User').with({ scheme: Schemas.userData });
this.settingsResource = joinPath(this.userRoamingDataHome, 'settings.json');
this.settingsSyncPreviewResource = joinPath(this.userRoamingDataHome, '.settings.json');
this.userDataSyncLogResource = joinPath(options.logsPath, 'userDataSync.log');
this.keybindingsResource = joinPath(this.userRoamingDataHome, 'keybindings.json');
this.keyboardLayoutResource = joinPath(this.userRoamingDataHome, 'keyboardLayout.json');
this.localeResource = joinPath(this.userRoamingDataHome, 'locale.json');
@@ -142,10 +143,11 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
appSettingsHome: URI;
userRoamingDataHome: URI;
settingsResource: URI;
settingsSyncPreviewResource: URI;
keybindingsResource: URI;
keyboardLayoutResource: URI;
localeResource: URI;
settingsSyncPreviewResource: URI;
userDataSyncLogResource: URI;
machineSettingsHome: URI;
machineSettingsResource: URI;
globalStorageHome: string;
@@ -188,7 +190,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
}
get webviewResourceRoot(): string {
return `${this.webviewExternalEndpoint}/vscode-resource{{resource}}`;
return `${this.webviewExternalEndpoint}/vscode-resource/{{resource}}`;
}
get webviewCspSource(): string {

View File

@@ -23,12 +23,13 @@ export class WorkbenchEnvironmentService extends EnvironmentService implements I
return baseEndpoint.replace('{{commit}}', product.commit || '211fa02efe8c041fd7baa8ec3dce199d5185aa44');
}
readonly webviewResourceRoot = 'vscode-resource:{{resource}}';
readonly webviewResourceRoot = 'vscode-resource://{{resource}}';
readonly webviewCspSource = 'vscode-resource:';
constructor(
readonly configuration: IWindowConfiguration,
execPath: string
execPath: string,
private readonly windowId: number
) {
super(configuration, execPath);
@@ -43,7 +44,7 @@ export class WorkbenchEnvironmentService extends EnvironmentService implements I
get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); }
@memoize
get logFile(): URI { return URI.file(join(this.logsPath, `renderer${this.configuration.windowId}.log`)); }
get logFile(): URI { return URI.file(join(this.logsPath, `renderer${this.windowId}.log`)); }
get logExtensionHostCommunication(): boolean { return !!this.args.logExtensionHostCommunication; }

View File

@@ -26,8 +26,8 @@ export class ExtensionEnablementService extends Disposable implements IExtension
_serviceBrand: undefined;
private readonly _onEnablementChanged = new Emitter<IExtension[]>();
public readonly onEnablementChanged: Event<IExtension[]> = this._onEnablementChanged.event;
private readonly _onEnablementChanged = new Emitter<readonly IExtension[]>();
public readonly onEnablementChanged: Event<readonly IExtension[]> = this._onEnablementChanged.event;
private readonly storageManger: StorageManager;

View File

@@ -44,7 +44,7 @@ export interface IExtensionEnablementService {
/**
* Event to listen on for extension enablement changes
*/
onEnablementChanged: Event<IExtension[]>;
readonly onEnablementChanged: Event<readonly IExtension[]>;
/**
* Returns the enablement state for the given extension

View File

@@ -25,7 +25,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/common/extensionHostProcessManager';
@@ -38,6 +37,7 @@ import { Logger } from 'vs/workbench/services/extensions/common/extensionPoints'
import { flatten } from 'vs/base/common/arrays';
import { IStaticExtensionsService } from 'vs/workbench/services/extensions/common/staticExtensions';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
class DeltaExtensionsQueueItem {
constructor(
@@ -67,10 +67,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
@IWindowService protected readonly _windowService: IWindowService,
@IStaticExtensionsService private readonly _staticExtensions: IStaticExtensionsService,
@IElectronService private readonly _electronService: IElectronService,
@IHostService private readonly _hostService: IHostService
@IHostService private readonly _hostService: IHostService,
@IElectronEnvironmentService private readonly _electronEnvironmentService: IElectronEnvironmentService
) {
super(
instantiationService,
@@ -93,7 +93,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
this._remoteExtensionsEnvironmentData = new Map<string, IRemoteAgentEnvironment>();
this._extensionHostLogsLocation = URI.file(path.join(this._environmentService.logsPath, `exthost${this._windowService.windowId}`));
this._extensionHostLogsLocation = URI.file(path.join(this._environmentService.logsPath, `exthost${this._electronEnvironmentService.windowId}`));
this._extensionScanner = instantiationService.createInstance(CachedExtensionScanner);
this._deltaExtensionsQueue = [];

View File

@@ -1,156 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, IFileService } from 'vs/platform/files/common/files';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { ResourceMap } from 'vs/base/common/map';
import { onUnexpectedError } from 'vs/base/common/errors';
import { INotificationService, Severity, NeverShowAgainScope } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
import { FileService } from 'vs/platform/files/common/fileService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
export class WorkspaceWatcher extends Disposable {
private watches = new ResourceMap<IDisposable>();
constructor(
@IFileService private readonly fileService: FileService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@INotificationService private readonly notificationService: INotificationService,
@IOpenerService private readonly openerService: IOpenerService
) {
super();
this.registerListeners();
this.refresh();
}
private registerListeners(): void {
this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onDidChangeWorkspaceFolders(e)));
this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState()));
this._register(this.configurationService.onDidChangeConfiguration(e => this.onDidChangeConfiguration(e)));
this._register(this.fileService.onError(error => this.onError(error)));
}
private onDidChangeWorkspaceFolders(e: IWorkspaceFoldersChangeEvent): void {
// Removed workspace: Unwatch
for (const removed of e.removed) {
this.unwatchWorkspace(removed.uri);
}
// Added workspace: Watch
for (const added of e.added) {
this.watchWorkspace(added.uri);
}
}
private onDidChangeWorkbenchState(): void {
this.refresh();
}
private onDidChangeConfiguration(e: IConfigurationChangeEvent): void {
if (e.affectsConfiguration('files.watcherExclude')) {
this.refresh();
}
}
private onError(error: Error): void {
const msg = error.toString();
// Forward to unexpected error handler
onUnexpectedError(msg);
// Detect if we run < .NET Framework 4.5
if (msg.indexOf('System.MissingMethodException') >= 0) {
this.notificationService.prompt(
Severity.Warning,
localize('netVersionError', "The Microsoft .NET Framework 4.5 is required. Please follow the link to install it."),
[{
label: localize('installNet', "Download .NET Framework 4.5"),
run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?LinkId=786533'))
}],
{
sticky: true,
neverShowAgain: { id: 'ignoreNetVersionError', isSecondary: true, scope: NeverShowAgainScope.WORKSPACE }
}
);
}
// Detect if we run into ENOSPC issues
if (msg.indexOf('ENOSPC') >= 0) {
this.notificationService.prompt(
Severity.Warning,
localize('enospcError', "Unable to watch for file changes in this large workspace. Please follow the instructions link to resolve this issue."),
[{
label: localize('learnMore', "Instructions"),
run: () => this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=867693'))
}],
{
sticky: true,
neverShowAgain: { id: 'ignoreEnospcError', isSecondary: true, scope: NeverShowAgainScope.WORKSPACE }
}
);
}
}
private watchWorkspace(resource: URI) {
// Compute the watcher exclude rules from configuration
const excludes: string[] = [];
const config = this.configurationService.getValue<IFilesConfiguration>({ resource });
if (config.files && config.files.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) {
excludes.push(key);
}
}
}
// Watch workspace
const disposable = this.fileService.watch(resource, { recursive: true, excludes });
this.watches.set(resource, disposable);
}
private unwatchWorkspace(resource: URI) {
if (this.watches.has(resource)) {
dispose(this.watches.get(resource));
this.watches.delete(resource);
}
}
private refresh(): void {
// Unwatch all first
this.unwatchWorkspaces();
// Watch each workspace folder
for (const folder of this.contextService.getWorkspace().folders) {
this.watchWorkspace(folder.uri);
}
}
private unwatchWorkspaces() {
this.watches.forEach(disposable => dispose(disposable));
this.watches.clear();
}
dispose(): void {
super.dispose();
this.unwatchWorkspaces();
}
}
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkspaceWatcher, LifecyclePhase.Restored);

View File

@@ -19,7 +19,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { Event } from 'vs/base/common/event';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IWorkspacesHistoryService } from 'vs/workbench/services/workspace/common/workspacesHistoryService';
import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { getExcludes, ISearchConfiguration } from 'vs/workbench/services/search/common/search';
import { IExpression } from 'vs/base/common/glob';
@@ -138,7 +138,7 @@ export class HistoryService extends Disposable implements IHistoryService {
@IStorageService private readonly storageService: IStorageService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFileService private readonly fileService: IFileService,
@IWindowService private readonly windowService: IWindowService,
@IWorkspacesHistoryService private readonly workspacesHistoryService: IWorkspacesHistoryService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@@ -781,7 +781,7 @@ export class HistoryService extends Disposable implements IHistoryService {
const input = arg1 as IResourceInput;
this.windowService.removeFromRecentlyOpened([input.resource]);
this.workspacesHistoryService.removeFromRecentlyOpened([input.resource]);
}
private isFileOpened(resource: URI, group: IEditorGroup): boolean {

View File

@@ -3,21 +3,116 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IResourceEditor, IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWindowSettings, IWindowOpenable, IOpenInWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
import { pathsToEditors } from 'vs/workbench/common/editor';
import { IFileService } from 'vs/platform/files/common/files';
import { ILabelService } from 'vs/platform/label/common/label';
import { trackFocus } from 'vs/base/browser/dom';
import { Disposable } from 'vs/base/common/lifecycle';
export class BrowserHostService implements IHostService {
export class BrowserHostService extends Disposable implements IHostService {
_serviceBrand: undefined;
constructor(@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService) { }
//#region Events
get onDidChangeFocus(): Event<boolean> { return this._onDidChangeFocus; }
private _onDidChangeFocus: Event<boolean>;
//#endregion
constructor(
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IEditorService private readonly editorService: IEditorService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFileService private readonly fileService: IFileService,
@ILabelService private readonly labelService: ILabelService
) {
super();
this.registerListeners();
}
private registerListeners(): void {
// Track Focus on Window
const focusTracker = this._register(trackFocus(window));
this._onDidChangeFocus = Event.any(
Event.map(focusTracker.onDidFocus, () => this.hasFocus),
Event.map(focusTracker.onDidBlur, () => this.hasFocus)
);
}
//#region Window
readonly windowCount = Promise.resolve(1);
async openEmptyWindow(options?: { reuse?: boolean, remoteAuthority?: string }): Promise<void> {
async openInWindow(toOpen: IWindowOpenable[], options?: IOpenInWindowOptions): Promise<void> {
// TODO@Ben delegate to embedder
const { openFolderInNewWindow } = this.shouldOpenNewWindow(options);
for (let i = 0; i < toOpen.length; i++) {
const openable = toOpen[i];
openable.label = openable.label || this.getRecentLabel(openable);
// Folder
if (isFolderToOpen(openable)) {
const newAddress = `${document.location.origin}${document.location.pathname}?folder=${openable.folderUri.path}`;
if (openFolderInNewWindow) {
window.open(newAddress);
} else {
window.location.href = newAddress;
}
}
// Workspace
else if (isWorkspaceToOpen(openable)) {
const newAddress = `${document.location.origin}${document.location.pathname}?workspace=${openable.workspaceUri.path}`;
if (openFolderInNewWindow) {
window.open(newAddress);
} else {
window.location.href = newAddress;
}
}
// File
else if (isFileToOpen(openable)) {
const inputs: IResourceEditor[] = await pathsToEditors([openable], this.fileService);
this.editorService.openEditors(inputs);
}
}
}
private getRecentLabel(openable: IWindowOpenable): string {
if (isFolderToOpen(openable)) {
return this.labelService.getWorkspaceLabel(openable.folderUri, { verbose: true });
}
if (isWorkspaceToOpen(openable)) {
return this.labelService.getWorkspaceLabel({ id: '', configPath: openable.workspaceUri }, { verbose: true });
}
return this.labelService.getUriLabel(openable.fileUri);
}
private shouldOpenNewWindow(options: IOpenInWindowOptions = {}): { openFolderInNewWindow: boolean } {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
const openFolderInNewWindowConfig = (windowConfig && windowConfig.openFoldersInNewWindow) || 'default' /* default */;
let openFolderInNewWindow = !!options.forceNewWindow && !options.forceReuseWindow;
if (!options.forceNewWindow && !options.forceReuseWindow && (openFolderInNewWindowConfig === 'on' || openFolderInNewWindowConfig === 'off')) {
openFolderInNewWindow = (openFolderInNewWindowConfig === 'on');
}
return { openFolderInNewWindow };
}
async openEmptyWindow(options?: IOpenEmptyWindowOptions): Promise<void> {
// TODO@Ben delegate to embedder
const targetHref = `${document.location.origin}${document.location.pathname}?ew=true`;
if (options && options.reuse) {
@@ -61,6 +156,14 @@ export class BrowserHostService implements IHostService {
}
}
get hasFocus(): boolean {
return document.hasFocus();
}
async focus(): Promise<void> {
window.focus();
}
//#endregion
async restart(): Promise<void> {

View File

@@ -3,7 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWindowOpenable, IOpenInWindowOptions, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
export const IHostService = createDecorator<IHostService>('hostService');
@@ -11,6 +13,15 @@ export interface IHostService {
_serviceBrand: undefined;
//#region Events
/**
* Emitted when the window focus changes.
*/
readonly onDidChangeFocus: Event<boolean>;
//#endregion
//#region Window
/**
@@ -18,17 +29,32 @@ export interface IHostService {
*/
readonly windowCount: Promise<number>;
/**
* Opens the provided array of openables in a window with the provided options.
*/
openInWindow(toOpen: IWindowOpenable[], options?: IOpenInWindowOptions): Promise<void>;
/**
* Opens an empty window. The optional parameter allows to define if
* a new window should open or the existing one change to an empty.
*/
openEmptyWindow(options?: { reuse?: boolean, remoteAuthority?: string }): Promise<void>;
openEmptyWindow(options?: IOpenEmptyWindowOptions): Promise<void>;
/**
* Switch between fullscreen and normal window.
*/
toggleFullScreen(): Promise<void>;
/**
* Find out if the window has focus or not.
*/
readonly hasFocus: boolean;
/**
* Attempt to bring the window to the foreground and focus it.
*/
focus(): Promise<void>;
//#endregion
//#region Lifecycle

View File

@@ -3,21 +3,77 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILabelService } from 'vs/platform/label/common/label';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IWindowOpenable, IOpenInWindowOptions, isFolderToOpen, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
import { Disposable } from 'vs/base/common/lifecycle';
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
export class DesktopHostService implements IHostService {
export class DesktopHostService extends Disposable implements IHostService {
_serviceBrand: undefined;
constructor(@IElectronService private readonly electronService: IElectronService) { }
//#region Events
get onDidChangeFocus(): Event<boolean> { return this._onDidChangeFocus; }
private _onDidChangeFocus: Event<boolean> = Event.any(
Event.map(Event.filter(this.electronService.onWindowFocus, id => id === this.electronEnvironmentService.windowId), _ => true),
Event.map(Event.filter(this.electronService.onWindowBlur, id => id === this.electronEnvironmentService.windowId), _ => false)
);
//#endregion
constructor(
@IElectronService private readonly electronService: IElectronService,
@ILabelService private readonly labelService: ILabelService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IElectronEnvironmentService private readonly electronEnvironmentService: IElectronEnvironmentService
) {
super();
// Resolve initial window focus state
this._hasFocus = document.hasFocus();
electronService.isWindowFocused().then(focused => this._hasFocus = focused);
this.registerListeners();
}
private registerListeners(): void {
this._register(this.onDidChangeFocus(focus => this._hasFocus = focus));
}
//#region Window
get windowCount() { return this.electronService.windowCount(); }
private _hasFocus: boolean;
get hasFocus(): boolean { return this._hasFocus; }
openEmptyWindow(options?: { reuse?: boolean, remoteAuthority?: string }): Promise<void> {
get windowCount(): Promise<number> { return this.electronService.getWindowCount(); }
openInWindow(toOpen: IWindowOpenable[], options?: IOpenInWindowOptions): Promise<void> {
if (!!this.environmentService.configuration.remoteAuthority) {
toOpen.forEach(openable => openable.label = openable.label || this.getRecentLabel(openable));
}
return this.electronService.openInWindow(toOpen, options);
}
private getRecentLabel(openable: IWindowOpenable): string {
if (isFolderToOpen(openable)) {
return this.labelService.getWorkspaceLabel(openable.folderUri, { verbose: true });
}
if (isWorkspaceToOpen(openable)) {
return this.labelService.getWorkspaceLabel({ id: '', configPath: openable.workspaceUri }, { verbose: true });
}
return this.labelService.getUriLabel(openable.fileUri);
}
openEmptyWindow(options?: IOpenEmptyWindowOptions): Promise<void> {
return this.electronService.openEmptyWindow(options);
}
@@ -25,6 +81,10 @@ export class DesktopHostService implements IHostService {
return this.electronService.toggleFullScreen();
}
focus(): Promise<void> {
return this.electronService.focusWindow();
}
//#endregion
restart(): Promise<void> {

View File

@@ -29,7 +29,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtensionMessageCollector, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { IUserKeybindingItem, KeybindingIO, OutputBuilder } from 'vs/workbench/services/keybinding/common/keybindingIO';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { MenuRegistry } from 'vs/platform/actions/common/actions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
@@ -154,7 +154,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
@INotificationService notificationService: INotificationService,
@IEnvironmentService environmentService: IEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
@IWindowService private readonly windowService: IWindowService,
@IHostService private readonly hostService: IHostService,
@IExtensionService extensionService: IExtensionService,
@IFileService fileService: IFileService,
@IKeymapService private readonly keymapService: IKeymapService
@@ -291,7 +291,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
// it is possible that the document has lost focus, but the
// window is still focused, e.g. when a <webview> element
// has focus
return this.windowService.hasFocus;
return this.hostService.hasFocus;
}
private _resolveKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {

View File

@@ -53,7 +53,7 @@ import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
class TestEnvironmentService extends WorkbenchEnvironmentService {
constructor(private _appSettingsHome: URI) {
super(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath);
super(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath, 0);
}
get appSettingsHome() { return this._appSettingsHome; }

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ShutdownReason, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { AbstractLifecycleService } from 'vs/platform/lifecycle/common/lifecycleService';
import { localize } from 'vs/nls';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class BrowserLifecycleService extends AbstractLifecycleService {
_serviceBrand: undefined;
constructor(
@ILogService readonly logService: ILogService
) {
super(logService);
this.registerListeners();
}
private registerListeners(): void {
// Note: we cannot change this to window.addEventListener('beforeUnload')
// because it seems that mechanism does not allow for preventing the unload
window.onbeforeunload = () => this.onBeforeUnload();
}
private onBeforeUnload(): string | null {
let veto = false;
// Before Shutdown
this._onBeforeShutdown.fire({
veto(value) {
if (value === true) {
veto = true;
} else if (value instanceof Promise && !veto) {
console.warn(new Error('Long running onBeforeShutdown currently not supported in the web'));
veto = true;
}
},
reason: ShutdownReason.QUIT
});
// Veto: signal back to browser by returning a non-falsify return value
if (veto) {
return localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again.");
}
// No Veto: continue with Will Shutdown
this._onWillShutdown.fire({
join() {
console.warn(new Error('Long running onWillShutdown currently not supported in the web'));
},
reason: ShutdownReason.QUIT
});
// Finally end with Shutdown event
this._onShutdown.fire();
return null;
}
}
registerSingleton(ILifecycleService, BrowserLifecycleService);

View File

@@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ShutdownReason, StartupKind, handleVetos, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { ipcRenderer as ipc } from 'electron';
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { onUnexpectedError } from 'vs/base/common/errors';
import { AbstractLifecycleService } from 'vs/platform/lifecycle/common/lifecycleService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class NativeLifecycleService extends AbstractLifecycleService {
private static readonly LAST_SHUTDOWN_REASON_KEY = 'lifecyle.lastShutdownReason';
_serviceBrand: undefined;
private shutdownReason: ShutdownReason | undefined;
constructor(
@INotificationService private readonly notificationService: INotificationService,
@IElectronEnvironmentService private readonly electronEnvironmentService: IElectronEnvironmentService,
@IStorageService readonly storageService: IStorageService,
@ILogService readonly logService: ILogService
) {
super(logService);
this._startupKind = this.resolveStartupKind();
this.registerListeners();
}
private resolveStartupKind(): StartupKind {
const lastShutdownReason = this.storageService.getNumber(NativeLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE);
this.storageService.remove(NativeLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE);
let startupKind: StartupKind;
if (lastShutdownReason === ShutdownReason.RELOAD) {
startupKind = StartupKind.ReloadedWindow;
} else if (lastShutdownReason === ShutdownReason.LOAD) {
startupKind = StartupKind.ReopenedWindow;
} else {
startupKind = StartupKind.NewWindow;
}
this.logService.trace(`lifecycle: starting up (startup kind: ${this._startupKind})`);
return startupKind;
}
private registerListeners(): void {
const windowId = this.electronEnvironmentService.windowId;
// Main side indicates that window is about to unload, check for vetos
ipc.on('vscode:onBeforeUnload', (_event: unknown, reply: { okChannel: string, cancelChannel: string, reason: ShutdownReason }) => {
this.logService.trace(`lifecycle: onBeforeUnload (reason: ${reply.reason})`);
// trigger onBeforeShutdown events and veto collecting
this.handleBeforeShutdown(reply.reason).then(veto => {
if (veto) {
this.logService.trace('lifecycle: onBeforeUnload prevented via veto');
ipc.send(reply.cancelChannel, windowId);
} else {
this.logService.trace('lifecycle: onBeforeUnload continues without veto');
this.shutdownReason = reply.reason;
ipc.send(reply.okChannel, windowId);
}
});
});
// Main side indicates that we will indeed shutdown
ipc.on('vscode:onWillUnload', async (_event: unknown, reply: { replyChannel: string, reason: ShutdownReason }) => {
this.logService.trace(`lifecycle: onWillUnload (reason: ${reply.reason})`);
// trigger onWillShutdown events and joining
await this.handleWillShutdown(reply.reason);
// trigger onShutdown event now that we know we will quit
this._onShutdown.fire();
// acknowledge to main side
ipc.send(reply.replyChannel, windowId);
});
// Save shutdown reason to retrieve on next startup
this.storageService.onWillSaveState(e => {
if (e.reason === WillSaveStateReason.SHUTDOWN) {
this.storageService.store(NativeLifecycleService.LAST_SHUTDOWN_REASON_KEY, this.shutdownReason, StorageScope.WORKSPACE);
}
});
}
private handleBeforeShutdown(reason: ShutdownReason): Promise<boolean> {
const vetos: (boolean | Promise<boolean>)[] = [];
this._onBeforeShutdown.fire({
veto(value) {
vetos.push(value);
},
reason
});
return handleVetos(vetos, err => {
this.notificationService.error(toErrorMessage(err));
onUnexpectedError(err);
});
}
private async handleWillShutdown(reason: ShutdownReason): Promise<void> {
const joiners: Promise<void>[] = [];
this._onWillShutdown.fire({
join(promise) {
if (promise) {
joiners.push(promise);
}
},
reason
});
try {
await Promise.all(joiners);
} catch (error) {
this.notificationService.error(toErrorMessage(error));
onUnexpectedError(error);
}
}
}
registerSingleton(ILifecycleService, NativeLifecycleService);

View File

@@ -17,8 +17,8 @@ export abstract class KeyValueLogProvider extends Disposable implements IFileSys
readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite;
readonly onDidChangeCapabilities: Event<void> = Event.None;
private readonly _onDidChangeFile: Emitter<IFileChange[]> = this._register(new Emitter<IFileChange[]>());
readonly onDidChangeFile: Event<IFileChange[]> = this._onDidChangeFile.event;
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
private readonly versions: Map<string, number> = new Map<string, number>();

View File

@@ -21,6 +21,7 @@ import { toLocalISOString } from 'vs/base/common/date';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Emitter, Event } from 'vs/base/common/event';
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implements IOutputChannelModel {
@@ -203,6 +204,7 @@ export class OutputChannelModelService extends AsbtractOutputChannelModelService
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IElectronEnvironmentService private readonly electronEnvironmentService: IElectronEnvironmentService,
@IFileService private readonly fileService: IFileService
) {
super(instantiationService);
@@ -216,7 +218,7 @@ export class OutputChannelModelService extends AsbtractOutputChannelModelService
private _outputDir: Promise<URI> | null = null;
private get outputDir(): Promise<URI> {
if (!this._outputDir) {
const outputDir = URI.file(join(this.environmentService.logsPath, `output_${this.environmentService.configuration.windowId}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`));
const outputDir = URI.file(join(this.environmentService.logsPath, `output_${this.electronEnvironmentService.windowId}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`));
this._outputDir = this.fileService.createFolder(outputDir).then(() => outputDir);
}
return this._outputDir;

View File

@@ -9,7 +9,7 @@ import { localize } from 'vs/nls';
import { IDisposable, dispose, DisposableStore, MutableDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IProgressService, IProgressOptions, IProgressStep, ProgressLocation, IProgress, Progress, IProgressCompositeOptions, IProgressNotificationOptions, IProgressRunner, IProgressIndicator } from 'vs/platform/progress/common/progress';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { StatusbarAlignment, IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
import { StatusbarAlignment, IStatusbarService } from 'vs/workbench/services/statusbar/common/statusbar';
import { timeout } from 'vs/base/common/async';
import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity';
import { INotificationService, Severity, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification';

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
import { connect } from 'vs/base/parts/ipc/node/ipc.net';
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class SharedProcessService implements ISharedProcessService {
_serviceBrand: undefined;
private withSharedProcessConnection: Promise<Client<string>>;
private sharedProcessMainChannel: IChannel;
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
@IElectronEnvironmentService environmentService: IElectronEnvironmentService
) {
this.sharedProcessMainChannel = mainProcessService.getChannel('sharedProcess');
this.withSharedProcessConnection = this.whenSharedProcessReady()
.then(() => connect(environmentService.sharedIPCHandle, `window:${environmentService.windowId}`));
}
whenSharedProcessReady(): Promise<void> {
return this.sharedProcessMainChannel.call('whenSharedProcessReady');
}
getChannel(channelName: string): IChannel {
return getDelayedChannel(this.withSharedProcessConnection.then(connection => connection.getChannel(channelName)));
}
registerChannel(channelName: string, channel: IServerChannel<string>): void {
this.withSharedProcessConnection.then(connection => connection.registerChannel(channelName, channel));
}
toggleSharedProcessWindow(): Promise<void> {
return this.sharedProcessMainChannel.call('toggleSharedProcessWindow');
}
}
registerSingleton(ISharedProcessService, SharedProcessService, true);

View File

@@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* 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 { IDisposable } from 'vs/base/common/lifecycle';
import { ThemeColor } from 'vs/platform/theme/common/themeService';
export const IStatusbarService = createDecorator<IStatusbarService>('statusbarService');
export const enum StatusbarAlignment {
LEFT,
RIGHT
}
/**
* A declarative way of describing a status bar entry
*/
export interface IStatusbarEntry {
/**
* The text to show for the entry. You can embed icons in the text by leveraging the syntax:
*
* `My text ${icon name} contains icons like ${icon name} this one.`
*/
readonly text: string;
/**
* An optional tooltip text to show when you hover over the entry
*/
readonly tooltip?: string;
/**
* An optional color to use for the entry
*/
readonly color?: string | ThemeColor;
/**
* An optional background color to use for the entry
*/
readonly backgroundColor?: string | ThemeColor;
/**
* An optional id of a command that is known to the workbench to execute on click
*/
readonly command?: string;
/**
* Optional arguments for the command.
*/
readonly arguments?: any[];
/**
* Wether to show a beak above the status bar entry.
*/
readonly showBeak?: boolean;
}
export interface IStatusbarService {
_serviceBrand: undefined;
/**
* Adds an entry to the statusbar with the given alignment and priority. Use the returned accessor
* to update or remove the statusbar entry.
*
* @param id identifier of the entry is needed to allow users to hide entries via settings
* @param name human readable name the entry is about
* @param alignment either LEFT or RIGHT
* @param priority items get arranged from highest priority to lowest priority from left to right
* in their respective alignment slot
*/
addEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority?: number): IStatusbarEntryAccessor;
/**
* Allows to update an entry's visibilty with the provided ID.
*/
updateEntryVisibility(id: string, visible: boolean): void;
}
export interface IStatusbarEntryAccessor extends IDisposable {
/**
* Allows to update an existing status bar entry.
*/
update(properties: IStatusbarEntry): void;
}

View File

@@ -38,7 +38,7 @@ export class TelemetryService extends Disposable implements ITelemetryService {
const channel = sharedProcessService.getChannel('telemetryAppender');
const config: ITelemetryServiceConfig = {
appender: combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(logService)),
commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId, productService.msftInternalDomains, environmentService.installSourcePath, environmentService.configuration.remoteAuthority),
commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId, productService.msftInternalDomains, environmentService.installSourcePath, environmentService.configuration.remoteAuthority, environmentService.options && environmentService.options.resolveCommonTelemetryProperties),
piiPaths: environmentService.extensionsPath ? [environmentService.appRoot, environmentService.extensionsPath] : [environmentService.appRoot]
};

View File

@@ -660,7 +660,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return result;
}
protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: readonly string[]): Promise<URI | undefined> {
// Help user to find a name for the file by opening it first
await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true } });
@@ -668,7 +668,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return this.fileDialogService.pickFileToSave(this.getSaveDialogOptions(defaultUri, availableFileSystems));
}
private getSaveDialogOptions(defaultUri: URI, availableFileSystems?: string[]): ISaveDialogOptions {
private getSaveDialogOptions(defaultUri: URI, availableFileSystems?: readonly string[]): ISaveDialogOptions {
const options: ISaveDialogOptions = {
defaultUri,
title: nls.localize('saveAsTitle', "Save As"),

View File

@@ -38,8 +38,8 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
private readonly _onModelOrphanedChanged: Emitter<TextFileModelChangeEvent> = this._register(new Emitter<TextFileModelChangeEvent>());
readonly onModelOrphanedChanged: Event<TextFileModelChangeEvent> = this._onModelOrphanedChanged.event;
private _onModelsDirty!: Event<TextFileModelChangeEvent[]>;
get onModelsDirty(): Event<TextFileModelChangeEvent[]> {
private _onModelsDirty!: Event<readonly TextFileModelChangeEvent[]>;
get onModelsDirty(): Event<readonly TextFileModelChangeEvent[]> {
if (!this._onModelsDirty) {
this._onModelsDirty = this.debounce(this.onModelDirty);
}
@@ -47,8 +47,8 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
return this._onModelsDirty;
}
private _onModelsSaveError!: Event<TextFileModelChangeEvent[]>;
get onModelsSaveError(): Event<TextFileModelChangeEvent[]> {
private _onModelsSaveError!: Event<readonly TextFileModelChangeEvent[]>;
get onModelsSaveError(): Event<readonly TextFileModelChangeEvent[]> {
if (!this._onModelsSaveError) {
this._onModelsSaveError = this.debounce(this.onModelSaveError);
}
@@ -56,8 +56,8 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
return this._onModelsSaveError;
}
private _onModelsSaved!: Event<TextFileModelChangeEvent[]>;
get onModelsSaved(): Event<TextFileModelChangeEvent[]> {
private _onModelsSaved!: Event<readonly TextFileModelChangeEvent[]>;
get onModelsSaved(): Event<readonly TextFileModelChangeEvent[]> {
if (!this._onModelsSaved) {
this._onModelsSaved = this.debounce(this.onModelSaved);
}
@@ -65,8 +65,8 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
return this._onModelsSaved;
}
private _onModelsReverted!: Event<TextFileModelChangeEvent[]>;
get onModelsReverted(): Event<TextFileModelChangeEvent[]> {
private _onModelsReverted!: Event<readonly TextFileModelChangeEvent[]>;
get onModelsReverted(): Event<readonly TextFileModelChangeEvent[]> {
if (!this._onModelsReverted) {
this._onModelsReverted = this.debounce(this.onModelReverted);
}
@@ -95,7 +95,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
this.lifecycleService.onShutdown(this.dispose, this);
}
private debounce(event: Event<TextFileModelChangeEvent>): Event<TextFileModelChangeEvent[]> {
private debounce(event: Event<TextFileModelChangeEvent>): Event<readonly TextFileModelChangeEvent[]> {
return Event.debounce(event, (prev: TextFileModelChangeEvent[], cur: TextFileModelChangeEvent) => {
if (!prev) {
prev = [cur];

View File

@@ -412,10 +412,10 @@ export interface ITextFileEditorModelManager {
readonly onModelReverted: Event<TextFileModelChangeEvent>;
readonly onModelOrphanedChanged: Event<TextFileModelChangeEvent>;
readonly onModelsDirty: Event<TextFileModelChangeEvent[]>;
readonly onModelsSaveError: Event<TextFileModelChangeEvent[]>;
readonly onModelsSaved: Event<TextFileModelChangeEvent[]>;
readonly onModelsReverted: Event<TextFileModelChangeEvent[]>;
readonly onModelsDirty: Event<readonly TextFileModelChangeEvent[]>;
readonly onModelsSaveError: Event<readonly TextFileModelChangeEvent[]>;
readonly onModelsSaved: Event<readonly TextFileModelChangeEvent[]>;
readonly onModelsReverted: Event<readonly TextFileModelChangeEvent[]>;
get(resource: URI): ITextFileEditorModel | undefined;
@@ -433,7 +433,7 @@ export interface ISaveOptions {
overwriteEncoding?: boolean;
skipSaveParticipants?: boolean;
writeElevated?: boolean;
availableFileSystems?: string[];
availableFileSystems?: readonly string[];
}
export interface ILoadOptions {

View File

@@ -7,10 +7,9 @@ import * as sinon from 'sinon';
import * as platform from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { ILifecycleService, BeforeShutdownEvent, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService, TestFileService, TestHostService } from 'vs/workbench/test/workbenchTestServices';
import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestContextService, TestFileService, TestHostService } from 'vs/workbench/test/workbenchTestServices';
import { toResource } from 'vs/base/test/common/utils';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ConfirmResult } from 'vs/workbench/common/editor';
@@ -28,7 +27,6 @@ class ServiceAccessor {
@ILifecycleService public lifecycleService: TestLifecycleService,
@ITextFileService public textFileService: TestTextFileService,
@IUntitledEditorService public untitledEditorService: IUntitledEditorService,
@IWindowsService public windowsService: TestWindowsService,
@IWorkspaceContextService public contextService: TestContextService,
@IModelService public modelService: ModelServiceImpl,
@IFileService public fileService: TestFileService,

View File

@@ -34,22 +34,22 @@ export interface IUntitledEditorService {
/**
* Events for when untitled editors content changes (e.g. any keystroke).
*/
onDidChangeContent: Event<URI>;
readonly onDidChangeContent: Event<URI>;
/**
* Events for when untitled editors change (e.g. getting dirty, saved or reverted).
*/
onDidChangeDirty: Event<URI>;
readonly onDidChangeDirty: Event<URI>;
/**
* Events for when untitled editor encodings change.
*/
onDidChangeEncoding: Event<URI>;
readonly onDidChangeEncoding: Event<URI>;
/**
* Events for when untitled editors are disposed.
*/
onDidDisposeModel: Event<URI>;
readonly onDidDisposeModel: Event<URI>;
/**
* Returns if an untitled resource with the given URI exists.

View File

@@ -11,7 +11,7 @@ import { URLService } from 'vs/platform/url/node/urlService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import product from 'vs/platform/product/common/product';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
export class RelayURLService extends URLService implements IURLHandler {
@@ -20,7 +20,7 @@ export class RelayURLService extends URLService implements IURLHandler {
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
@IOpenerService openerService: IOpenerService,
@IWindowService private windowService: IWindowService
@IElectronEnvironmentService private electronEnvironmentService: IElectronEnvironmentService
) {
super();
@@ -35,9 +35,9 @@ export class RelayURLService extends URLService implements IURLHandler {
let query = uri.query;
if (!query) {
query = `windowId=${encodeURIComponent(this.windowService.windowId)}`;
query = `windowId=${encodeURIComponent(this.electronEnvironmentService.windowId)}`;
} else {
query += `&windowId=${encodeURIComponent(this.windowService.windowId)}`;
query += `&windowId=${encodeURIComponent(this.electronEnvironmentService.windowId)}`;
}
return uri.with({ query });

View File

@@ -17,8 +17,8 @@ export class FileUserDataProvider extends Disposable implements IFileSystemProvi
readonly capabilities: FileSystemProviderCapabilities = this.fileSystemProvider.capabilities;
readonly onDidChangeCapabilities: Event<void> = Event.None;
private readonly _onDidChangeFile: Emitter<IFileChange[]> = this._register(new Emitter<IFileChange[]>());
readonly onDidChangeFile: Event<IFileChange[]> = this._onDidChangeFile.event;
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
private readonly userDataHome: URI;
@@ -103,7 +103,7 @@ export class FileUserDataProvider extends Disposable implements IFileSystemProvi
return this.fileSystemProvider.delete(this.toFileSystemResource(resource), opts);
}
private handleFileChanges(changes: IFileChange[]): void {
private handleFileChanges(changes: readonly IFileChange[]): void {
const userDataChanges: IFileChange[] = [];
for (const change of changes) {
const userDataResource = this.toUserDataResource(change.resource);
@@ -139,4 +139,4 @@ export class FileUserDataProvider extends Disposable implements IFileSystemProvi
return null;
}
}
}

View File

@@ -205,8 +205,8 @@ export class InMemoryFileSystemProvider extends Disposable implements IFileSyste
// --- manage file events
private readonly _onDidChangeFile: Emitter<IFileChange[]> = this._register(new Emitter<IFileChange[]>());
readonly onDidChangeFile: Event<IFileChange[]> = this._onDidChangeFile.event;
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
private _bufferedChanges: IFileChange[] = [];
private _fireSoonHandle?: any;

View File

@@ -277,7 +277,7 @@ suite('FileUserDataProvider', () => {
class TestFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability {
constructor(readonly onDidChangeFile: Event<IFileChange[]>) { }
constructor(readonly onDidChangeFile: Event<readonly IFileChange[]>) { }
readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite;
@@ -309,7 +309,7 @@ suite('FileUserDataProvider - Watching', () => {
let userDataResource: URI;
const disposables = new DisposableStore();
const fileEventEmitter: Emitter<IFileChange[]> = new Emitter<IFileChange[]>();
const fileEventEmitter: Emitter<readonly IFileChange[]> = new Emitter<readonly IFileChange[]>();
disposables.add(fileEventEmitter);
setup(() => {

View File

@@ -27,20 +27,21 @@ class SettingsMergeService implements ISettingsMergeService {
@IModeService private readonly modeService: IModeService,
) { }
async merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: IStringDictionary<boolean>): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> {
async merge(localContent: string, remoteContent: string, baseContent: string | null, ignoredSettings: string[]): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> {
const local = parse(localContent);
const remote = parse(remoteContent);
const base = baseContent ? parse(baseContent) : null;
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
const localToRemote = this.compare(local, remote, ignoredSettings);
const localToRemote = this.compare(local, remote, ignored);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { mergeContent: localContent, hasChanges: false, hasConflicts: false };
}
const conflicts: Set<string> = new Set<string>();
const baseToLocal = base ? this.compare(base, local, ignoredSettings) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = base ? this.compare(base, remote, ignoredSettings) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToLocal = base ? this.compare(base, local, ignored) : { added: Object.keys(local).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const baseToRemote = base ? this.compare(base, remote, ignored) : { added: Object.keys(remote).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
const settingsPreviewModel = this.modelService.createModel(localContent, this.modeService.create('jsonc'));
// Removed settings in Local
@@ -151,11 +152,12 @@ class SettingsMergeService implements ISettingsMergeService {
return { mergeContent: settingsPreviewModel.getValue(), hasChanges: true, hasConflicts: conflicts.size > 0 };
}
async computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: IStringDictionary<boolean>): Promise<string> {
async computeRemoteContent(localContent: string, remoteContent: string, ignoredSettings: string[]): Promise<string> {
const remote = parse(remoteContent);
const remoteModel = this.modelService.createModel(localContent, this.modeService.create('jsonc'));
const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set<string>());
for (const key of Object.keys(ignoredSettings)) {
if (ignoredSettings[key]) {
if (ignored.has(key)) {
this.editSetting(remoteModel, key, undefined);
this.editSetting(remoteModel, key, remote[key]);
}
@@ -180,9 +182,9 @@ class SettingsMergeService implements ISettingsMergeService {
}
}
private compare(from: IStringDictionary<any>, to: IStringDictionary<any>, ignored: IStringDictionary<boolean>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = Object.keys(from).filter(key => !ignored[key]);
const toKeys = Object.keys(to).filter(key => !ignored[key]);
private compare(from: IStringDictionary<any>, to: IStringDictionary<any>, ignored: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = Object.keys(from).filter(key => !ignored.has(key));
const toKeys = Object.keys(to).filter(key => !ignored.has(key));
const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
const updated: Set<string> = new Set<string>();

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncStatus, SyncSource, IUserDataSyncService, ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync';
import { SyncStatus, SyncSource, IUserDataSyncService } from 'vs/platform/userDataSync/common/userDataSync';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
@@ -42,8 +42,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.channel.call('sync', [_continue]);
}
getRemoteExtensions(): Promise<ISyncExtension[]> {
return this.channel.call('getRemoteExtensions');
stop(): void {
this.channel.call('stop');
}
removeExtension(identifier: IExtensionIdentifier): Promise<void> {

View File

@@ -1,93 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IWindowService, IWindowsService, IOpenSettings, IURIToOpen, isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows';
import { IRecentlyOpened, IRecent } from 'vs/platform/history/common/history';
import { URI } from 'vs/base/common/uri';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILabelService } from 'vs/platform/label/common/label';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class WindowService extends Disposable implements IWindowService {
readonly onDidChangeFocus: Event<boolean>;
readonly onDidChangeMaximize: Event<boolean>;
_serviceBrand: undefined;
private _windowId: number;
private remoteAuthority: string | undefined;
private _hasFocus: boolean;
get hasFocus(): boolean { return this._hasFocus; }
constructor(
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IWindowsService private readonly windowsService: IWindowsService,
@ILabelService private readonly labelService: ILabelService
) {
super();
this._windowId = environmentService.configuration.windowId;
this.remoteAuthority = environmentService.configuration.remoteAuthority;
const onThisWindowFocus = Event.map(Event.filter(windowsService.onWindowFocus, id => id === this._windowId), _ => true);
const onThisWindowBlur = Event.map(Event.filter(windowsService.onWindowBlur, id => id === this._windowId), _ => false);
const onThisWindowMaximize = Event.map(Event.filter(windowsService.onWindowMaximize, id => id === this._windowId), _ => true);
const onThisWindowUnmaximize = Event.map(Event.filter(windowsService.onWindowUnmaximize, id => id === this._windowId), _ => false);
this.onDidChangeFocus = Event.any(onThisWindowFocus, onThisWindowBlur);
this.onDidChangeMaximize = Event.any(onThisWindowMaximize, onThisWindowUnmaximize);
this._hasFocus = document.hasFocus();
this.isFocused().then(focused => this._hasFocus = focused);
this._register(this.onDidChangeFocus(focus => this._hasFocus = focus));
}
get windowId(): number {
return this._windowId;
}
openWindow(uris: IURIToOpen[], options: IOpenSettings = {}): Promise<void> {
if (!!this.remoteAuthority) {
uris.forEach(u => u.label = u.label || this.getRecentLabel(u));
}
return this.windowsService.openWindow(this.windowId, uris, options);
}
getRecentlyOpened(): Promise<IRecentlyOpened> {
return this.windowsService.getRecentlyOpened(this.windowId);
}
addRecentlyOpened(recents: IRecent[]): Promise<void> {
return this.windowsService.addRecentlyOpened(recents);
}
removeFromRecentlyOpened(paths: URI[]): Promise<void> {
return this.windowsService.removeFromRecentlyOpened(paths);
}
focusWindow(): Promise<void> {
return this.windowsService.focusWindow(this.windowId);
}
isFocused(): Promise<boolean> {
return this.windowsService.isFocused(this.windowId);
}
private getRecentLabel(u: IURIToOpen): string {
if (isFolderToOpen(u)) {
return this.labelService.getWorkspaceLabel(u.folderUri, { verbose: true });
} else if (isWorkspaceToOpen(u)) {
return this.labelService.getWorkspaceLabel({ id: '', configPath: u.workspaceUri }, { verbose: true });
} else {
return this.labelService.getUriLabel(u.fileUri);
}
}
}
registerSingleton(IWindowService, WindowService);

View File

@@ -0,0 +1,347 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing';
import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IBackupFileService, toBackupWorkspaceResource } from 'vs/workbench/services/backup/common/backup';
import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { distinct } from 'vs/base/common/arrays';
import { isEqual, getComparisonKey } from 'vs/base/common/resources';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IHostService } from 'vs/workbench/services/host/browser/host';
export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditingService {
_serviceBrand: undefined;
constructor(
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
@IWorkspaceContextService private readonly contextService: WorkspaceService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionService private readonly extensionService: IExtensionService,
@IBackupFileService private readonly backupFileService: IBackupFileService,
@INotificationService private readonly notificationService: INotificationService,
@ICommandService private readonly commandService: ICommandService,
@IFileService private readonly fileService: IFileService,
@ITextFileService private readonly textFileService: ITextFileService,
@IWorkspacesService protected readonly workspacesService: IWorkspacesService,
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IDialogService protected readonly dialogService: IDialogService,
@IHostService protected readonly hostService: IHostService
) { }
pickNewWorkspacePath(): Promise<URI | undefined> {
return this.fileDialogService.showSaveDialog({
saveLabel: mnemonicButtonLabel(nls.localize('save', "Save")),
title: nls.localize('saveWorkspace', "Save Workspace"),
filters: WORKSPACE_FILTER,
defaultUri: this.fileDialogService.defaultWorkspacePath()
});
}
updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise<void> {
const folders = this.contextService.getWorkspace().folders;
let foldersToDelete: URI[] = [];
if (typeof deleteCount === 'number') {
foldersToDelete = folders.slice(index, index + deleteCount).map(f => f.uri);
}
const wantsToDelete = foldersToDelete.length > 0;
const wantsToAdd = Array.isArray(foldersToAdd) && foldersToAdd.length > 0;
if (!wantsToAdd && !wantsToDelete) {
return Promise.resolve(); // return early if there is nothing to do
}
// Add Folders
if (wantsToAdd && !wantsToDelete && Array.isArray(foldersToAdd)) {
return this.doAddFolders(foldersToAdd, index, donotNotifyError);
}
// Delete Folders
if (wantsToDelete && !wantsToAdd) {
return this.removeFolders(foldersToDelete);
}
// Add & Delete Folders
else {
// if we are in single-folder state and the folder is replaced with
// other folders, we handle this specially and just enter workspace
// mode with the folders that are being added.
if (this.includesSingleFolderWorkspace(foldersToDelete)) {
return this.createAndEnterWorkspace(foldersToAdd!);
}
// if we are not in workspace-state, we just add the folders
if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
return this.doAddFolders(foldersToAdd!, index, donotNotifyError);
}
// finally, update folders within the workspace
return this.doUpdateFolders(foldersToAdd!, foldersToDelete, index, donotNotifyError);
}
}
private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToDelete: URI[], index?: number, donotNotifyError: boolean = false): Promise<void> {
try {
await this.contextService.updateFolders(foldersToAdd, foldersToDelete, index);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
addFolders(foldersToAdd: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): Promise<void> {
return this.doAddFolders(foldersToAdd, undefined, donotNotifyError);
}
private async doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): Promise<void> {
const state = this.contextService.getWorkbenchState();
if (this.environmentService.configuration.remoteAuthority) {
// Do not allow workspace folders with scheme different than the current remote scheme
const schemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
if (schemas.length && foldersToAdd.some(f => schemas.indexOf(f.uri.scheme) === -1)) {
return Promise.reject(new Error(nls.localize('differentSchemeRoots', "Workspace folders from different providers are not allowed in the same workspace.")));
}
}
// If we are in no-workspace or single-folder workspace, adding folders has to
// enter a workspace.
if (state !== WorkbenchState.WORKSPACE) {
let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri }));
newWorkspaceFolders.splice(typeof index === 'number' ? index : newWorkspaceFolders.length, 0, ...foldersToAdd);
newWorkspaceFolders = distinct(newWorkspaceFolders, folder => getComparisonKey(folder.uri));
if (state === WorkbenchState.EMPTY && newWorkspaceFolders.length === 0 || state === WorkbenchState.FOLDER && newWorkspaceFolders.length === 1) {
return; // return if the operation is a no-op for the current state
}
return this.createAndEnterWorkspace(newWorkspaceFolders);
}
// Delegate addition of folders to workspace service otherwise
try {
await this.contextService.addFolders(foldersToAdd, index);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
async removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): Promise<void> {
// If we are in single-folder state and the opened folder is to be removed,
// we create an empty workspace and enter it.
if (this.includesSingleFolderWorkspace(foldersToRemove)) {
return this.createAndEnterWorkspace([]);
}
// Delegate removal of folders to workspace service otherwise
try {
await this.contextService.removeFolders(foldersToRemove);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
private includesSingleFolderWorkspace(folders: URI[]): boolean {
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
const workspaceFolder = this.contextService.getWorkspace().folders[0];
return (folders.some(folder => isEqual(folder, workspaceFolder.uri)));
}
return false;
}
async createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise<void> {
if (path && !await this.isValidTargetWorkspacePath(path)) {
return;
}
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
const untitledWorkspace = await this.workspacesService.createUntitledWorkspace(folders, remoteAuthority);
if (path) {
await this.saveWorkspaceAs(untitledWorkspace, path);
} else {
path = untitledWorkspace.configPath;
}
return this.enterWorkspace(path);
}
async saveAndEnterWorkspace(path: URI): Promise<void> {
if (!await this.isValidTargetWorkspacePath(path)) {
return;
}
const workspaceIdentifier = this.getCurrentWorkspaceIdentifier();
if (!workspaceIdentifier) {
return;
}
await this.saveWorkspaceAs(workspaceIdentifier, path);
return this.enterWorkspace(path);
}
async isValidTargetWorkspacePath(path: URI): Promise<boolean> {
return true; // OK
}
protected async saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise<any> {
const configPathURI = workspace.configPath;
// Return early if target is same as source
if (isEqual(configPathURI, targetConfigPathURI)) {
return;
}
// Read the contents of the workspace file, update it to new location and save it.
const raw = await this.fileService.readFile(configPathURI);
const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.value.toString(), configPathURI, targetConfigPathURI);
await this.textFileService.create(targetConfigPathURI, newRawWorkspaceContents, { overwrite: true });
}
private handleWorkspaceConfigurationEditingError(error: JSONEditingError): void {
switch (error.code) {
case JSONEditingErrorCode.ERROR_INVALID_FILE:
this.onInvalidWorkspaceConfigurationFileError();
break;
case JSONEditingErrorCode.ERROR_FILE_DIRTY:
this.onWorkspaceConfigurationFileDirtyError();
break;
default:
this.notificationService.error(error.message);
}
}
private onInvalidWorkspaceConfigurationFileError(): void {
const message = nls.localize('errorInvalidTaskConfiguration', "Unable to write into workspace configuration file. Please open the file to correct errors/warnings in it and try again.");
this.askToOpenWorkspaceConfigurationFile(message);
}
private onWorkspaceConfigurationFileDirtyError(): void {
const message = nls.localize('errorWorkspaceConfigurationFileDirty', "Unable to write into workspace configuration file because the file is dirty. Please save it and try again.");
this.askToOpenWorkspaceConfigurationFile(message);
}
private askToOpenWorkspaceConfigurationFile(message: string): void {
this.notificationService.prompt(Severity.Error, message,
[{
label: nls.localize('openWorkspaceConfigurationFile', "Open Workspace Configuration"),
run: () => this.commandService.executeCommand('workbench.action.openWorkspaceConfigFile')
}]
);
}
async enterWorkspace(path: URI): Promise<void> {
if (!!this.environmentService.extensionTestsLocationURI) {
throw new Error('Entering a new workspace is not possible in tests.');
}
const workspace = await this.workspacesService.getWorkspaceIdentifier(path);
// Settings migration (only if we come from a folder workspace)
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
await this.migrateWorkspaceSettings(workspace);
}
const workspaceImpl = this.contextService as WorkspaceService;
await workspaceImpl.initialize(workspace);
const result = await this.workspacesService.enterWorkspace(path);
if (result) {
// Migrate storage to new workspace
await this.migrateStorage(result.workspace);
// Reinitialize backup service
this.environmentService.configuration.backupPath = result.backupPath;
this.environmentService.configuration.backupWorkspaceResource = result.backupPath ? toBackupWorkspaceResource(result.backupPath, this.environmentService) : undefined;
if (this.backupFileService instanceof BackupFileService) {
this.backupFileService.reinitialize();
}
}
// TODO@aeschli: workaround until restarting works
if (this.environmentService.configuration.remoteAuthority) {
this.hostService.reload();
}
// Restart the extension host: entering a workspace means a new location for
// storage and potentially a change in the workspace.rootPath property.
else {
this.extensionService.restartExtensionHost();
}
}
private migrateStorage(toWorkspace: IWorkspaceIdentifier): Promise<void> {
return this.storageService.migrate(toWorkspace);
}
private migrateWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise<void> {
return this.doCopyWorkspaceSettings(toWorkspace, setting => setting.scope === ConfigurationScope.WINDOW);
}
copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise<void> {
return this.doCopyWorkspaceSettings(toWorkspace);
}
private doCopyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier, filter?: (config: IConfigurationPropertySchema) => boolean): Promise<void> {
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
const targetWorkspaceConfiguration: any = {};
for (const key of this.configurationService.keys().workspace) {
if (configurationProperties[key]) {
if (filter && !filter(configurationProperties[key])) {
continue;
}
targetWorkspaceConfiguration[key] = this.configurationService.inspect(key).workspace;
}
}
return this.jsonEditingService.write(toWorkspace.configPath, [{ key: 'settings', value: targetWorkspaceConfiguration }], true);
}
protected getCurrentWorkspaceIdentifier(): IWorkspaceIdentifier | undefined {
const workspace = this.contextService.getWorkspace();
if (workspace && workspace.configuration) {
return { id: workspace.id, configPath: workspace.configuration };
}
return undefined;
}
}

View File

@@ -3,463 +3,49 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing';
import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ConfigurationScope, IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IBackupFileService, toBackupWorkspaceResource } from 'vs/workbench/services/backup/common/backup';
import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { distinct } from 'vs/base/common/arrays';
import { isLinux, isWindows, isMacintosh, isWeb } from 'vs/base/common/platform';
import { isEqual, basename, isEqualOrParent, getComparisonKey } from 'vs/base/common/resources';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILabelService } from 'vs/platform/label/common/label';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { AbstractWorkspaceEditingService } from 'vs/workbench/services/workspace/browser/abstractWorkspaceEditingService';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class WorkspaceEditingService implements IWorkspaceEditingService {
export class BrowserWorkspaceEditingService extends AbstractWorkspaceEditingService {
_serviceBrand: undefined;
constructor(
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
@IWorkspaceContextService private readonly contextService: WorkspaceService,
@IWindowService private readonly windowService: IWindowService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IStorageService private readonly storageService: IStorageService,
@IExtensionService private readonly extensionService: IExtensionService,
@IBackupFileService private readonly backupFileService: IBackupFileService,
@INotificationService private readonly notificationService: INotificationService,
@ICommandService private readonly commandService: ICommandService,
@IFileService private readonly fileService: IFileService,
@ITextFileService private readonly textFileService: ITextFileService,
@IWindowsService private readonly windowsService: IWindowsService,
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IDialogService private readonly dialogService: IDialogService,
@ILifecycleService readonly lifecycleService: ILifecycleService,
@ILabelService readonly labelService: ILabelService,
@IHostService private readonly hostService: IHostService
@IJSONEditingService jsonEditingService: IJSONEditingService,
@IWorkspaceContextService contextService: WorkspaceService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService storageService: IStorageService,
@IExtensionService extensionService: IExtensionService,
@IBackupFileService backupFileService: IBackupFileService,
@INotificationService notificationService: INotificationService,
@ICommandService commandService: ICommandService,
@IFileService fileService: IFileService,
@ITextFileService textFileService: ITextFileService,
@IWorkspacesService workspacesService: IWorkspacesService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IFileDialogService fileDialogService: IFileDialogService,
@IDialogService dialogService: IDialogService,
@IHostService hostService: IHostService
) {
this.registerListeners();
}
private registerListeners(): void {
this.lifecycleService.onBeforeShutdown(e => {
if (isWeb) {
return; // no support for untitled in web
}
const saveOperation = this.saveUntitedBeforeShutdown(e.reason);
if (saveOperation) {
e.veto(saveOperation);
}
});
}
private async saveUntitedBeforeShutdown(reason: ShutdownReason): Promise<boolean> {
if (reason !== ShutdownReason.LOAD && reason !== ShutdownReason.CLOSE) {
return false; // only interested when window is closing or loading
}
const workspaceIdentifier = this.getCurrentWorkspaceIdentifier();
if (!workspaceIdentifier || !isEqualOrParent(workspaceIdentifier.configPath, this.environmentService.untitledWorkspacesHome)) {
return false; // only care about untitled workspaces to ask for saving
}
const windowCount = await this.hostService.windowCount;
if (reason === ShutdownReason.CLOSE && !isMacintosh && windowCount === 1) {
return false; // Windows/Linux: quits when last window is closed, so do not ask then
}
enum ConfirmResult {
SAVE,
DONT_SAVE,
CANCEL
}
const save = { label: mnemonicButtonLabel(nls.localize('save', "Save")), result: ConfirmResult.SAVE };
const dontSave = { label: mnemonicButtonLabel(nls.localize('doNotSave', "Don't Save")), result: ConfirmResult.DONT_SAVE };
const cancel = { label: nls.localize('cancel', "Cancel"), result: ConfirmResult.CANCEL };
const buttons: { label: string; result: ConfirmResult; }[] = [];
if (isWindows) {
buttons.push(save, dontSave, cancel);
} else if (isLinux) {
buttons.push(dontSave, cancel, save);
} else {
buttons.push(save, cancel, dontSave);
}
const message = nls.localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?");
const detail = nls.localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again.");
const cancelId = buttons.indexOf(cancel);
const { choice } = await this.dialogService.show(Severity.Warning, message, buttons.map(button => button.label), { detail, cancelId });
switch (buttons[choice].result) {
// Cancel: veto unload
case ConfirmResult.CANCEL:
return true;
// Don't Save: delete workspace
case ConfirmResult.DONT_SAVE:
this.workspacesService.deleteUntitledWorkspace(workspaceIdentifier);
return false;
// Save: save workspace, but do not veto unload if path provided
case ConfirmResult.SAVE: {
const newWorkspacePath = await this.pickNewWorkspacePath();
if (!newWorkspacePath) {
return true; // keep veto if no target was provided
}
try {
await this.saveWorkspaceAs(workspaceIdentifier, newWorkspacePath);
const newWorkspaceIdentifier = await this.workspacesService.getWorkspaceIdentifier(newWorkspacePath);
const label = this.labelService.getWorkspaceLabel(newWorkspaceIdentifier, { verbose: true });
this.windowService.addRecentlyOpened([{ label, workspace: newWorkspaceIdentifier }]);
this.workspacesService.deleteUntitledWorkspace(workspaceIdentifier);
} catch (error) {
// ignore
}
return false;
}
}
}
pickNewWorkspacePath(): Promise<URI | undefined> {
return this.fileDialogService.showSaveDialog({
saveLabel: mnemonicButtonLabel(nls.localize('save', "Save")),
title: nls.localize('saveWorkspace', "Save Workspace"),
filters: WORKSPACE_FILTER,
defaultUri: this.fileDialogService.defaultWorkspacePath()
});
}
updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise<void> {
const folders = this.contextService.getWorkspace().folders;
let foldersToDelete: URI[] = [];
if (typeof deleteCount === 'number') {
foldersToDelete = folders.slice(index, index + deleteCount).map(f => f.uri);
}
const wantsToDelete = foldersToDelete.length > 0;
const wantsToAdd = Array.isArray(foldersToAdd) && foldersToAdd.length > 0;
if (!wantsToAdd && !wantsToDelete) {
return Promise.resolve(); // return early if there is nothing to do
}
// Add Folders
if (wantsToAdd && !wantsToDelete && Array.isArray(foldersToAdd)) {
return this.doAddFolders(foldersToAdd, index, donotNotifyError);
}
// Delete Folders
if (wantsToDelete && !wantsToAdd) {
return this.removeFolders(foldersToDelete);
}
// Add & Delete Folders
else {
// if we are in single-folder state and the folder is replaced with
// other folders, we handle this specially and just enter workspace
// mode with the folders that are being added.
if (this.includesSingleFolderWorkspace(foldersToDelete)) {
return this.createAndEnterWorkspace(foldersToAdd!);
}
// if we are not in workspace-state, we just add the folders
if (this.contextService.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
return this.doAddFolders(foldersToAdd!, index, donotNotifyError);
}
// finally, update folders within the workspace
return this.doUpdateFolders(foldersToAdd!, foldersToDelete, index, donotNotifyError);
}
}
private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToDelete: URI[], index?: number, donotNotifyError: boolean = false): Promise<void> {
try {
await this.contextService.updateFolders(foldersToAdd, foldersToDelete, index);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
addFolders(foldersToAdd: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): Promise<void> {
return this.doAddFolders(foldersToAdd, undefined, donotNotifyError);
}
private async doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): Promise<void> {
const state = this.contextService.getWorkbenchState();
if (this.environmentService.configuration.remoteAuthority) {
// Do not allow workspace folders with scheme different than the current remote scheme
const schemas = this.contextService.getWorkspace().folders.map(f => f.uri.scheme);
if (schemas.length && foldersToAdd.some(f => schemas.indexOf(f.uri.scheme) === -1)) {
return Promise.reject(new Error(nls.localize('differentSchemeRoots', "Workspace folders from different providers are not allowed in the same workspace.")));
}
}
// If we are in no-workspace or single-folder workspace, adding folders has to
// enter a workspace.
if (state !== WorkbenchState.WORKSPACE) {
let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri }));
newWorkspaceFolders.splice(typeof index === 'number' ? index : newWorkspaceFolders.length, 0, ...foldersToAdd);
newWorkspaceFolders = distinct(newWorkspaceFolders, folder => getComparisonKey(folder.uri));
if (state === WorkbenchState.EMPTY && newWorkspaceFolders.length === 0 || state === WorkbenchState.FOLDER && newWorkspaceFolders.length === 1) {
return; // return if the operation is a no-op for the current state
}
return this.createAndEnterWorkspace(newWorkspaceFolders);
}
// Delegate addition of folders to workspace service otherwise
try {
await this.contextService.addFolders(foldersToAdd, index);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
async removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): Promise<void> {
// If we are in single-folder state and the opened folder is to be removed,
// we create an empty workspace and enter it.
if (this.includesSingleFolderWorkspace(foldersToRemove)) {
return this.createAndEnterWorkspace([]);
}
// Delegate removal of folders to workspace service otherwise
try {
await this.contextService.removeFolders(foldersToRemove);
} catch (error) {
if (donotNotifyError) {
throw error;
}
this.handleWorkspaceConfigurationEditingError(error);
}
}
private includesSingleFolderWorkspace(folders: URI[]): boolean {
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
const workspaceFolder = this.contextService.getWorkspace().folders[0];
return (folders.some(folder => isEqual(folder, workspaceFolder.uri)));
}
return false;
}
async createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise<void> {
if (path && !await this.isValidTargetWorkspacePath(path)) {
return;
}
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
const untitledWorkspace = await this.workspacesService.createUntitledWorkspace(folders, remoteAuthority);
if (path) {
await this.saveWorkspaceAs(untitledWorkspace, path);
} else {
path = untitledWorkspace.configPath;
}
return this.enterWorkspace(path);
}
async saveAndEnterWorkspace(path: URI): Promise<void> {
if (!await this.isValidTargetWorkspacePath(path)) {
return;
}
const workspaceIdentifier = this.getCurrentWorkspaceIdentifier();
if (!workspaceIdentifier) {
return;
}
await this.saveWorkspaceAs(workspaceIdentifier, path);
return this.enterWorkspace(path);
}
async isValidTargetWorkspacePath(path: URI): Promise<boolean> {
const windows = await this.windowsService.getWindows();
// Prevent overwriting a workspace that is currently opened in another window
if (windows.some(window => !!window.workspace && isEqual(window.workspace.configPath, path))) {
await this.dialogService.show(
Severity.Info,
nls.localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)),
[nls.localize('ok', "OK")],
{
detail: nls.localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again.")
}
);
return false;
}
return true; // OK
}
private async saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPathURI: URI): Promise<any> {
const configPathURI = workspace.configPath;
// Return early if target is same as source
if (isEqual(configPathURI, targetConfigPathURI)) {
return;
}
// Read the contents of the workspace file, update it to new location and save it.
const raw = await this.fileService.readFile(configPathURI);
const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.value.toString(), configPathURI, targetConfigPathURI);
await this.textFileService.create(targetConfigPathURI, newRawWorkspaceContents, { overwrite: true });
}
private handleWorkspaceConfigurationEditingError(error: JSONEditingError): void {
switch (error.code) {
case JSONEditingErrorCode.ERROR_INVALID_FILE:
this.onInvalidWorkspaceConfigurationFileError();
break;
case JSONEditingErrorCode.ERROR_FILE_DIRTY:
this.onWorkspaceConfigurationFileDirtyError();
break;
default:
this.notificationService.error(error.message);
}
}
private onInvalidWorkspaceConfigurationFileError(): void {
const message = nls.localize('errorInvalidTaskConfiguration', "Unable to write into workspace configuration file. Please open the file to correct errors/warnings in it and try again.");
this.askToOpenWorkspaceConfigurationFile(message);
}
private onWorkspaceConfigurationFileDirtyError(): void {
const message = nls.localize('errorWorkspaceConfigurationFileDirty', "Unable to write into workspace configuration file because the file is dirty. Please save it and try again.");
this.askToOpenWorkspaceConfigurationFile(message);
}
private askToOpenWorkspaceConfigurationFile(message: string): void {
this.notificationService.prompt(Severity.Error, message,
[{
label: nls.localize('openWorkspaceConfigurationFile', "Open Workspace Configuration"),
run: () => this.commandService.executeCommand('workbench.action.openWorkspaceConfigFile')
}]
);
}
async enterWorkspace(path: URI): Promise<void> {
if (!!this.environmentService.extensionTestsLocationURI) {
throw new Error('Entering a new workspace is not possible in tests.');
}
const workspace = await this.workspacesService.getWorkspaceIdentifier(path);
// Settings migration (only if we come from a folder workspace)
if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) {
await this.migrateWorkspaceSettings(workspace);
}
const workspaceImpl = this.contextService as WorkspaceService;
await workspaceImpl.initialize(workspace);
const result = await this.workspacesService.enterWorkspace(path);
if (result) {
// Migrate storage to new workspace
await this.migrateStorage(result.workspace);
// Reinitialize backup service
this.environmentService.configuration.backupPath = result.backupPath;
this.environmentService.configuration.backupWorkspaceResource = result.backupPath ? toBackupWorkspaceResource(result.backupPath, this.environmentService) : undefined;
if (this.backupFileService instanceof BackupFileService) {
this.backupFileService.reinitialize();
}
}
// TODO@aeschli: workaround until restarting works
if (this.environmentService.configuration.remoteAuthority) {
this.hostService.reload();
}
// Restart the extension host: entering a workspace means a new location for
// storage and potentially a change in the workspace.rootPath property.
else {
this.extensionService.restartExtensionHost();
}
}
private migrateStorage(toWorkspace: IWorkspaceIdentifier): Promise<void> {
return this.storageService.migrate(toWorkspace);
}
private migrateWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise<void> {
return this.doCopyWorkspaceSettings(toWorkspace, setting => setting.scope === ConfigurationScope.WINDOW);
}
copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise<void> {
return this.doCopyWorkspaceSettings(toWorkspace);
}
private doCopyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier, filter?: (config: IConfigurationPropertySchema) => boolean): Promise<void> {
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
const targetWorkspaceConfiguration: any = {};
for (const key of this.configurationService.keys().workspace) {
if (configurationProperties[key]) {
if (filter && !filter(configurationProperties[key])) {
continue;
}
targetWorkspaceConfiguration[key] = this.configurationService.inspect(key).workspace;
}
}
return this.jsonEditingService.write(toWorkspace.configPath, [{ key: 'settings', value: targetWorkspaceConfiguration }], true);
}
private getCurrentWorkspaceIdentifier(): IWorkspaceIdentifier | undefined {
const workspace = this.contextService.getWorkspace();
if (workspace && workspace.configuration) {
return { id: workspace.id, configPath: workspace.configuration };
}
return undefined;
super(jsonEditingService, contextService, configurationService, storageService, extensionService, backupFileService, notificationService, commandService, fileService, textFileService, workspacesService, environmentService, fileDialogService, dialogService, hostService);
}
}
registerSingleton(IWorkspaceEditingService, WorkspaceEditingService, true);
registerSingleton(IWorkspaceEditingService, BrowserWorkspaceEditingService, true);

View File

@@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { IRecent, IRecentlyOpened, isRecentFolder, isRecentFile } from 'vs/platform/workspaces/common/workspacesHistory';
import { IWorkspacesHistoryService } from 'vs/workbench/services/workspace/common/workspacesHistoryService';
import { restoreRecentlyOpened, toStoreData } from 'vs/platform/workspaces/common/workspacesHistoryStorage';
import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage';
import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ILogService } from 'vs/platform/log/common/log';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Disposable } from 'vs/base/common/lifecycle';
export class BrowserWorkspacesHistoryService extends Disposable implements IWorkspacesHistoryService {
static readonly RECENTLY_OPENED_KEY = 'recently.opened';
_serviceBrand: undefined;
private readonly _onRecentlyOpenedChange: Emitter<void> = this._register(new Emitter<void>());
readonly onRecentlyOpenedChange: Event<void> = this._onRecentlyOpenedChange.event;
constructor(
@IStorageService private readonly storageService: IStorageService,
@IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService,
@ILogService private readonly logService: ILogService,
) {
super();
this.addWorkspaceToRecentlyOpened();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.storageService.onDidChangeStorage(event => {
if (event.key === BrowserWorkspacesHistoryService.RECENTLY_OPENED_KEY && event.scope === StorageScope.GLOBAL) {
this._onRecentlyOpenedChange.fire();
}
}));
}
private addWorkspaceToRecentlyOpened(): void {
const workspace = this.workspaceService.getWorkspace();
switch (this.workspaceService.getWorkbenchState()) {
case WorkbenchState.FOLDER:
this.addRecentlyOpened([{ folderUri: workspace.folders[0].uri }]);
break;
case WorkbenchState.WORKSPACE:
this.addRecentlyOpened([{ workspace: { id: workspace.id, configPath: workspace.configuration! } }]);
break;
}
}
async getRecentlyOpened(): Promise<IRecentlyOpened> {
const recentlyOpenedRaw = this.storageService.get(BrowserWorkspacesHistoryService.RECENTLY_OPENED_KEY, StorageScope.GLOBAL);
if (recentlyOpenedRaw) {
return restoreRecentlyOpened(JSON.parse(recentlyOpenedRaw), this.logService);
}
return { workspaces: [], files: [] };
}
async addRecentlyOpened(recents: IRecent[]): Promise<void> {
const recentlyOpened = await this.getRecentlyOpened();
recents.forEach(recent => {
if (isRecentFile(recent)) {
this.doRemoveFromRecentlyOpened(recentlyOpened, [recent.fileUri]);
recentlyOpened.files.unshift(recent);
} else if (isRecentFolder(recent)) {
this.doRemoveFromRecentlyOpened(recentlyOpened, [recent.folderUri]);
recentlyOpened.workspaces.unshift(recent);
} else {
this.doRemoveFromRecentlyOpened(recentlyOpened, [recent.workspace.configPath]);
recentlyOpened.workspaces.unshift(recent);
}
});
return this.saveRecentlyOpened(recentlyOpened);
}
async removeFromRecentlyOpened(paths: URI[]): Promise<void> {
const recentlyOpened = await this.getRecentlyOpened();
this.doRemoveFromRecentlyOpened(recentlyOpened, paths);
return this.saveRecentlyOpened(recentlyOpened);
}
private doRemoveFromRecentlyOpened(recentlyOpened: IRecentlyOpened, paths: URI[]): void {
recentlyOpened.files = recentlyOpened.files.filter(file => {
return !paths.some(path => path.toString() === file.fileUri.toString());
});
recentlyOpened.workspaces = recentlyOpened.workspaces.filter(workspace => {
return !paths.some(path => path.toString() === (isRecentFolder(workspace) ? workspace.folderUri.toString() : workspace.workspace.configPath.toString()));
});
}
private async saveRecentlyOpened(data: IRecentlyOpened): Promise<void> {
return this.storageService.store(BrowserWorkspacesHistoryService.RECENTLY_OPENED_KEY, JSON.stringify(toStoreData(data)), StorageScope.GLOBAL);
}
async clearRecentlyOpened(): Promise<void> {
this.storageService.remove(BrowserWorkspacesHistoryService.RECENTLY_OPENED_KEY, StorageScope.GLOBAL);
}
}
registerSingleton(IWorkspacesHistoryService, BrowserWorkspacesHistoryService, true);

View File

@@ -4,9 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWorkspacesService, IWorkspaceFolderCreationData, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspacesService, IWorkspaceFolderCreationData, IWorkspaceIdentifier, IEnterWorkspaceResult } from 'vs/platform/workspaces/common/workspaces';
import { URI } from 'vs/base/common/uri';
import { IEnterWorkspaceResult } from 'vs/platform/windows/common/windows';
export class WorkspacesService implements IWorkspacesService {

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* 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 { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { IRecent, IRecentlyOpened } from 'vs/platform/workspaces/common/workspacesHistory';
export const IWorkspacesHistoryService = createDecorator<IWorkspacesHistoryService>('workspacesHistoryService');
export interface IWorkspacesHistoryService {
_serviceBrand: undefined;
readonly onRecentlyOpenedChange: Event<void>;
addRecentlyOpened(recents: IRecent[]): Promise<void>;
removeFromRecentlyOpened(workspaces: URI[]): Promise<void>;
clearRecentlyOpened(): Promise<void>;
getRecentlyOpened(): Promise<IRecentlyOpened>;
}

View File

@@ -0,0 +1,171 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { URI } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkspacesHistoryService } from 'vs/workbench/services/workspace/common/workspacesHistoryService';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { isEqual, basename, isEqualOrParent } from 'vs/base/common/resources';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILabelService } from 'vs/platform/label/common/label';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { AbstractWorkspaceEditingService } from 'vs/workbench/services/workspace/browser/abstractWorkspaceEditingService';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingService {
_serviceBrand: undefined;
constructor(
@IJSONEditingService jsonEditingService: IJSONEditingService,
@IWorkspaceContextService contextService: WorkspaceService,
@IElectronService private electronService: IElectronService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService storageService: IStorageService,
@IExtensionService extensionService: IExtensionService,
@IBackupFileService backupFileService: IBackupFileService,
@INotificationService notificationService: INotificationService,
@ICommandService commandService: ICommandService,
@IFileService fileService: IFileService,
@ITextFileService textFileService: ITextFileService,
@IWorkspacesHistoryService private readonly workspacesHistoryService: IWorkspacesHistoryService,
@IWorkspacesService workspacesService: IWorkspacesService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IFileDialogService fileDialogService: IFileDialogService,
@IDialogService protected dialogService: IDialogService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@ILabelService private readonly labelService: ILabelService,
@IHostService hostService: IHostService,
) {
super(jsonEditingService, contextService, configurationService, storageService, extensionService, backupFileService, notificationService, commandService, fileService, textFileService, workspacesService, environmentService, fileDialogService, dialogService, hostService);
this.registerListeners();
}
private registerListeners(): void {
this.lifecycleService.onBeforeShutdown(e => {
const saveOperation = this.saveUntitedBeforeShutdown(e.reason);
if (saveOperation) {
e.veto(saveOperation);
}
});
}
private async saveUntitedBeforeShutdown(reason: ShutdownReason): Promise<boolean> {
if (reason !== ShutdownReason.LOAD && reason !== ShutdownReason.CLOSE) {
return false; // only interested when window is closing or loading
}
const workspaceIdentifier = this.getCurrentWorkspaceIdentifier();
if (!workspaceIdentifier || !isEqualOrParent(workspaceIdentifier.configPath, this.environmentService.untitledWorkspacesHome)) {
return false; // only care about untitled workspaces to ask for saving
}
const windowCount = await this.hostService.windowCount;
if (reason === ShutdownReason.CLOSE && !isMacintosh && windowCount === 1) {
return false; // Windows/Linux: quits when last window is closed, so do not ask then
}
enum ConfirmResult {
SAVE,
DONT_SAVE,
CANCEL
}
const save = { label: mnemonicButtonLabel(nls.localize('save', "Save")), result: ConfirmResult.SAVE };
const dontSave = { label: mnemonicButtonLabel(nls.localize('doNotSave', "Don't Save")), result: ConfirmResult.DONT_SAVE };
const cancel = { label: nls.localize('cancel', "Cancel"), result: ConfirmResult.CANCEL };
const buttons: { label: string; result: ConfirmResult; }[] = [];
if (isWindows) {
buttons.push(save, dontSave, cancel);
} else if (isLinux) {
buttons.push(dontSave, cancel, save);
} else {
buttons.push(save, cancel, dontSave);
}
const message = nls.localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?");
const detail = nls.localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again.");
const cancelId = buttons.indexOf(cancel);
const { choice } = await this.dialogService.show(Severity.Warning, message, buttons.map(button => button.label), { detail, cancelId });
switch (buttons[choice].result) {
// Cancel: veto unload
case ConfirmResult.CANCEL:
return true;
// Don't Save: delete workspace
case ConfirmResult.DONT_SAVE:
this.workspacesService.deleteUntitledWorkspace(workspaceIdentifier);
return false;
// Save: save workspace, but do not veto unload if path provided
case ConfirmResult.SAVE: {
const newWorkspacePath = await this.pickNewWorkspacePath();
if (!newWorkspacePath) {
return true; // keep veto if no target was provided
}
try {
await this.saveWorkspaceAs(workspaceIdentifier, newWorkspacePath);
const newWorkspaceIdentifier = await this.workspacesService.getWorkspaceIdentifier(newWorkspacePath);
const label = this.labelService.getWorkspaceLabel(newWorkspaceIdentifier, { verbose: true });
this.workspacesHistoryService.addRecentlyOpened([{ label, workspace: newWorkspaceIdentifier }]);
this.workspacesService.deleteUntitledWorkspace(workspaceIdentifier);
} catch (error) {
// ignore
}
return false;
}
}
}
async isValidTargetWorkspacePath(path: URI): Promise<boolean> {
const windows = await this.electronService.getWindows();
// Prevent overwriting a workspace that is currently opened in another window
if (windows.some(window => !!window.workspace && isEqual(window.workspace.configPath, path))) {
await this.dialogService.show(
Severity.Info,
nls.localize('workspaceOpenedMessage', "Unable to save workspace '{0}'", basename(path)),
[nls.localize('ok', "OK")],
{
detail: nls.localize('workspaceOpenedDetail', "The workspace is already opened in another window. Please close that window first and then try again.")
}
);
return false;
}
return true; // OK
}
}
registerSingleton(IWorkspaceEditingService, NativeWorkspaceEditingService, true);

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IRecent, IRecentlyOpened } from 'vs/platform/workspaces/common/workspacesHistory';
import { IWorkspacesHistoryService } from 'vs/workbench/services/workspace/common/workspacesHistoryService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IElectronService } from 'vs/platform/electron/node/electron';
export class NativeWorkspacesHistoryService implements IWorkspacesHistoryService {
_serviceBrand: undefined;
readonly onRecentlyOpenedChange = this.electronService.onRecentlyOpenedChange;
constructor(
@IElectronService private readonly electronService: IElectronService
) { }
async getRecentlyOpened(): Promise<IRecentlyOpened> {
return this.electronService.getRecentlyOpened();
}
async addRecentlyOpened(recents: IRecent[]): Promise<void> {
return this.electronService.addRecentlyOpened(recents);
}
async removeFromRecentlyOpened(paths: URI[]): Promise<void> {
return this.electronService.removeFromRecentlyOpened(paths);
}
async clearRecentlyOpened(): Promise<void> {
return this.electronService.clearRecentlyOpened();
}
}
registerSingleton(IWorkspacesHistoryService, NativeWorkspacesHistoryService, true);

View File

@@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWorkspacesService, IWorkspaceIdentifier, IWorkspaceFolderCreationData, reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspacesService, IWorkspaceIdentifier, IWorkspaceFolderCreationData, reviveWorkspaceIdentifier, IEnterWorkspaceResult } from 'vs/platform/workspaces/common/workspaces';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { URI } from 'vs/base/common/uri';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWindowService, IEnterWorkspaceResult } from 'vs/platform/windows/common/windows';
import { IElectronEnvironmentService } from 'vs/workbench/services/electron/electron-browser/electronEnvironmentService';
export class WorkspacesService implements IWorkspacesService {
@@ -18,13 +18,13 @@ export class WorkspacesService implements IWorkspacesService {
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
@IWindowService private readonly windowService: IWindowService
@IElectronEnvironmentService private readonly electronEnvironmentService: IElectronEnvironmentService
) {
this.channel = mainProcessService.getChannel('workspaces');
}
async enterWorkspace(path: URI): Promise<IEnterWorkspaceResult | undefined> {
const result: IEnterWorkspaceResult = await this.channel.call('enterWorkspace', [this.windowService.windowId, path]);
const result: IEnterWorkspaceResult = await this.channel.call('enterWorkspace', [this.electronEnvironmentService.windowId, path]);
if (result) {
result.workspace = reviveWorkspaceIdentifier(result.workspace);
}