mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Merge from vscode e0762af258c0b20320ed03f3871a41967acc4421 (#7404)
* Merge from vscode e0762af258c0b20320ed03f3871a41967acc4421 * readd svgs
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
19
src/vs/workbench/services/credentials/common/credentials.ts
Normal file
19
src/vs/workbench/services/credentials/common/credentials.ts
Normal 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 }>>;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
) { }
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
88
src/vs/workbench/services/statusbar/common/statusbar.ts
Normal file
88
src/vs/workbench/services/statusbar/common/statusbar.ts
Normal 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;
|
||||
}
|
||||
@@ -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]
|
||||
};
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user