diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index d98ce059c77..e9758098d43 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -65,7 +65,6 @@ export interface IConfigurationChangeEvent { export interface IConfigurationService { _serviceBrand: any; - userSettingsResource: URI; onDidChangeConfiguration: Event; getConfigurationData(): IConfigurationData | null; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 196b47933c1..210ac5080a3 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -97,6 +97,8 @@ export interface IEnvironmentService { appNameLong: string; appQuality?: string; appSettingsHome: URI; + settingsResource: URI; + keybindingsResource: URI; keyboardLayoutResource: URI; machineSettingsHome: URI; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index aa956e5f306..12881effa53 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -114,6 +114,9 @@ export class EnvironmentService implements IEnvironmentService { @memoize get appSettingsHome(): URI { return URI.file(path.join(this.userDataPath, 'User')); } + @memoize + get settingsResource(): URI { return resources.joinPath(this.appSettingsHome, 'settings.json'); } + @memoize get machineSettingsHome(): URI { return URI.file(path.join(this.userDataPath, 'Machine')); } @@ -132,6 +135,9 @@ export class EnvironmentService implements IEnvironmentService { @memoize get settingsSearchUrl(): string | undefined { return product.settingsSearchUrl; } + @memoize + get keybindingsResource(): URI { return resources.joinPath(this.appSettingsHome, 'keybindings.json'); } + @memoize get keyboardLayoutResource(): URI { return resources.joinPath(this.appSettingsHome, 'keyboardLayout.json'); } diff --git a/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts index 530ffae4699..9f728745344 100644 --- a/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/electron-browser/telemetryService.test.ts @@ -11,7 +11,6 @@ import * as Errors from 'vs/base/common/errors'; import * as sinon from 'sinon'; import { getConfigurationValue } from 'vs/platform/configuration/common/configuration'; import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; -import { URI } from 'vs/base/common/uri'; class TestTelemetryAppender implements ITelemetryAppender { @@ -770,7 +769,6 @@ suite('TelemetryService', () => { appender: testAppender }, { _serviceBrand: undefined, - userSettingsResource: URI.file('settings.json'), getValue() { return { enableTelemetry: enableTelemetry diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index a8eeae2cdc0..15542b6087e 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -35,12 +35,11 @@ import { SignService } from 'vs/platform/sign/browser/signService'; import { hash } from 'vs/base/common/hash'; import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import { ProductService } from 'vs/platform/product/browser/productService'; -import { FileUserDataService } from 'vs/workbench/services/userData/common/fileUserDataService'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; +import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; import { UserDataFileSystemProvider } from 'vs/workbench//services/userData/common/userDataFileSystemProvider'; -import { CustomUserDataService } from '../services/userData/common/customUserDataService'; -import { joinPath } from 'vs/base/common/resources'; -import { InMemoryUserDataProvider } from '../services/userData/common/inMemoryUserDataProvider'; +import { joinPath, dirname } from 'vs/base/common/resources'; +import { InMemoryUserDataProvider } from 'vs/workbench/services/userData/common/inMemoryUserDataProvider'; +import { IUserDataProvider } from 'vs/workbench/services/userData/common/userData'; class CodeRendererMain extends Disposable { @@ -92,7 +91,8 @@ class CodeRendererMain extends Disposable { serviceCollection.set(ILogService, logService); // Environment - const environmentService = new BrowserWorkbenchEnvironmentService(this.configuration); + const remoteUserDataUri = this.getRemoteUserDataUri(); + const environmentService = new BrowserWorkbenchEnvironmentService(this.configuration, remoteUserDataUri); serviceCollection.set(IWorkbenchEnvironmentService, environmentService); // Product @@ -123,15 +123,13 @@ class CodeRendererMain extends Disposable { fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); } - // User Data Service - const userDataService = this.createUserDataService(fileService); - serviceCollection.set(IUserDataService, userDataService); - fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(userDataService)); + // User Data Provider + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(dirname(environmentService.settingsResource), this.getUserDataPovider(fileService, remoteUserDataUri))); const payload = await this.resolveWorkspaceInitializationPayload(); await Promise.all([ - this.createWorkspaceService(payload, environmentService, fileService, userDataService, remoteAgentService, logService).then(service => { + this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, logService).then(service => { // Workspace serviceCollection.set(IWorkspaceContextService, service); @@ -146,8 +144,8 @@ class CodeRendererMain extends Disposable { return { serviceCollection, logService }; } - private async createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: FileService, userDataService: IUserDataService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { - const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache: new ConfigurationCache() }, fileService, userDataService, remoteAgentService); + private async createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { + const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService); try { await workspaceService.initialize(payload); @@ -176,16 +174,13 @@ class CodeRendererMain extends Disposable { return { id: 'empty-window' }; } - private createUserDataService(fileService: IFileService): IUserDataService { + private getUserDataPovider(fileService: IFileService, remoteUserDataUri: URI | null): IUserDataProvider { if (this.configuration.userDataProvider) { - return this._register(new CustomUserDataService(this.configuration.userDataProvider)); - } else if (this.configuration.remoteAuthority) { - const remoteUserDataUri = this.getRemoteUserDataUri(); - if (remoteUserDataUri) { - return this._register(new FileUserDataService(remoteUserDataUri, fileService)); - } + return this.configuration.userDataProvider; + } else if (this.configuration.remoteAuthority && remoteUserDataUri) { + return this._register(new FileUserDataProvider(remoteUserDataUri, fileService)); } - return this._register(new CustomUserDataService(new InMemoryUserDataProvider())); + return this._register(new InMemoryUserDataProvider()); } private getRemoteUserDataUri(): URI | null { diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 02013097199..20c9985f632 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -39,8 +39,6 @@ import { ExplorerRootContext, ExplorerFolderContext } from 'vs/workbench/contrib import { ILabelService } from 'vs/platform/label/common/label'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; Registry.as(EditorExtensions.Editors).registerEditor( new EditorDescriptor( @@ -372,8 +370,6 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon constructor( @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @IKeybindingEditingService keybindingEditingService: IKeybindingEditingService, - @IConfigurationService configurationService: IConfigurationService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IWorkspaceContextService private readonly workpsaceContextService: IWorkspaceContextService, @ILabelService labelService: ILabelService, @@ -389,7 +385,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon dark: URI.parse(require.toUrl(`vs/workbench/contrib/preferences/browser/media/preferences-editor-inverse.svg`)) } }, - when: ResourceContextKey.Resource.isEqualTo(keybindingEditingService.userKeybindingsResource.toString()), + when: ResourceContextKey.Resource.isEqualTo(environmentService.keybindingsResource.toString()), group: 'navigation', order: 1 }); @@ -405,7 +401,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon dark: URI.parse(require.toUrl(`vs/workbench/contrib/preferences/browser/media/preferences-editor-inverse.svg`)) } }, - when: ResourceContextKey.Resource.isEqualTo(configurationService.userSettingsResource.toString()), + when: ResourceContextKey.Resource.isEqualTo(environmentService.settingsResource.toString()), group: 'navigation', order: 1 }); diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index 52097f9afc2..246eaf2ed11 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -14,6 +14,7 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import * as JSONContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -35,6 +36,7 @@ export class PreferencesContribution implements IWorkbenchContribution { @IPreferencesService private readonly preferencesService: IPreferencesService, @IModeService private readonly modeService: IModeService, @IEditorService private readonly editorService: IEditorService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService ) { @@ -77,7 +79,7 @@ export class PreferencesContribution implements IWorkbenchContribution { } // Global User Settings File - if (isEqual(resource, this.configurationService.userSettingsResource, !isLinux)) { + if (isEqual(resource, this.environmentService.settingsResource, !isLinux)) { return { override: this.preferencesService.openGlobalSettings(true, options, group) }; } diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 60dd333c222..014ac74121d 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -50,9 +50,9 @@ import { ConfigurationCache } from 'vs/workbench/services/configuration/node/con import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { SignService } from 'vs/platform/sign/node/signService'; import { ISignService } from 'vs/platform/sign/common/sign'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; -import { FileUserDataService } from 'vs/workbench/services/userData/common/fileUserDataService'; +import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; import { UserDataFileSystemProvider } from 'vs/workbench/services/userData/common/userDataFileSystemProvider'; +import { dirname } from 'vs/base/common/resources'; class CodeRendererMain extends Disposable { @@ -208,15 +208,13 @@ class CodeRendererMain extends Disposable { fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); } - // User Data Service - const userDataService = this._register(new FileUserDataService(environmentService.appSettingsHome, fileService)); - serviceCollection.set(IUserDataService, userDataService); - fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(userDataService)); + // User Data Provider + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(dirname(environmentService.settingsResource), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); const payload = await this.resolveWorkspaceInitializationPayload(environmentService); const services = await Promise.all([ - this.createWorkspaceService(payload, environmentService, fileService, userDataService, remoteAgentService, logService).then(service => { + this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, logService).then(service => { // Workspace serviceCollection.set(IWorkspaceContextService, service); @@ -312,8 +310,8 @@ class CodeRendererMain extends Disposable { return; } - private async createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: FileService, userDataService: IUserDataService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { - const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache: new ConfigurationCache(environmentService) }, fileService, userDataService, remoteAgentService); + private async createWorkspaceService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, logService: ILogService): Promise { + const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); try { await workspaceService.initialize(payload); diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 7907be1ca34..515c6df2fe3 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -9,10 +9,10 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as errors from 'vs/base/common/errors'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; -import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES, USER_CONFIGURATION_KEY, ConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES, ConfigurationFileService } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspaceFolder, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -23,27 +23,24 @@ import { Schemas } from 'vs/base/common/network'; import { IConfigurationModel } from 'vs/platform/configuration/common/configuration'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { hash } from 'vs/base/common/hash'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; export class UserConfiguration extends Disposable { - readonly resource: URI; - private readonly parser: ConfigurationModelParser; private readonly reloadConfigurationScheduler: RunOnceScheduler; protected readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; constructor( + private readonly userSettingsResource: URI, private readonly scopes: ConfigurationScope[] | undefined, - private readonly userDataService: IUserDataService + private readonly fileService: IFileService ) { super(); - this.resource = userDataService.toResource(USER_CONFIGURATION_KEY); - this.parser = new ConfigurationModelParser(USER_CONFIGURATION_KEY, this.scopes); + this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); - this._register(Event.filter(this.userDataService.onDidChange, e => e.contains(USER_CONFIGURATION_KEY))(() => this.reloadConfigurationScheduler.schedule())); + this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.userSettingsResource))(() => this.reloadConfigurationScheduler.schedule())); } async initialize(): Promise { @@ -52,8 +49,8 @@ export class UserConfiguration extends Disposable { async reload(): Promise { try { - const content = (await this.userDataService.read(USER_CONFIGURATION_KEY)) || '{}'; - this.parser.parseContent(content); + const content = await this.fileService.readFile(this.userSettingsResource); + this.parser.parseContent(content.value.toString() || '{}'); return this.parser.configurationModel; } catch (e) { return new ConfigurationModel(); diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 7aa440f3117..b2b2f3469f9 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -29,7 +29,7 @@ import { isEqual, dirname } from 'vs/base/common/resources'; import { mark } from 'vs/base/common/performance'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IFileService } from 'vs/platform/files/common/files'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class WorkspaceService extends Disposable implements IConfigurationService, IWorkspaceContextService { @@ -47,7 +47,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private cachedFolderConfigs: ResourceMap; private workspaceEditingQueue: Queue; - readonly userSettingsResource: URI; private readonly configurationFileService: ConfigurationFileService; protected readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); @@ -70,8 +69,8 @@ export class WorkspaceService extends Disposable implements IConfigurationServic constructor( { remoteAuthority, configurationCache }: { remoteAuthority?: string, configurationCache: IConfigurationCache }, + environmentService: IWorkbenchEnvironmentService, fileService: IFileService, - userDataService: IUserDataService, remoteAgentService: IRemoteAgentService ) { super(); @@ -82,8 +81,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic this.configurationFileService = new ConfigurationFileService(fileService); this._configuration = new Configuration(this.defaultConfiguration, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), this.workspace); this.cachedFolderConfigs = new ResourceMap(); - this.localUserConfiguration = this._register(new UserConfiguration(remoteAuthority ? LOCAL_MACHINE_SCOPES : undefined, userDataService)); - this.userSettingsResource = this.localUserConfiguration.resource; + this.localUserConfiguration = this._register(new UserConfiguration(environmentService.settingsResource, remoteAuthority ? LOCAL_MACHINE_SCOPES : undefined, fileService)); this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); if (remoteAuthority) { this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(remoteAuthority, configurationCache, this.configurationFileService, remoteAgentService)); diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 2910b6feccc..ca5948d81a6 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -8,7 +8,6 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IFileService } from 'vs/platform/files/common/files'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -export const USER_CONFIGURATION_KEY = 'settings.json'; export const FOLDER_CONFIG_FOLDER_NAME = '.vscode'; export const FOLDER_SETTINGS_NAME = 'settings'; export const FOLDER_SETTINGS_PATH = `${FOLDER_CONFIG_FOLDER_NAME}/${FOLDER_SETTINGS_NAME}.json`; diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index da010d44828..ffef24c5573 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -10,16 +10,18 @@ import * as strings from 'vs/base/common/strings'; import { setProperty } from 'vs/base/common/jsonEdit'; import { Queue } from 'vs/base/common/async'; import { Edit } from 'vs/base/common/jsonFormatter'; +import { IReference } from 'vs/base/common/lifecycle'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Registry } from 'vs/platform/registry/common/platform'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IConfigurationService, IConfigurationOverrides, keyFromOverrideIdentifier } from 'vs/platform/configuration/common/configuration'; -import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { OVERRIDE_PROPERTY_PATTERN, IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextModel } from 'vs/editor/common/model'; @@ -27,14 +29,6 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { withUndefinedAsNull, withNullAsUndefined } from 'vs/base/common/types'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { Emitter } from 'vs/base/common/event'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Schemas } from 'vs/base/common/network'; export const enum ConfigurationEditingErrorCode { @@ -101,6 +95,10 @@ export interface IConfigurationValue { } export interface IConfigurationEditingOptions { + /** + * If `true`, do not saves the configuration. Default is `false`. + */ + donotSave?: boolean; /** * If `true`, do not notifies the error to user by showing the message box. Default is `false`. */ @@ -118,127 +116,70 @@ export const enum EditableConfigurationTarget { WORKSPACE_FOLDER } -interface IConfigurationEditOperation extends IDisposable { - value: IConfigurationValue; +interface IConfigurationEditOperation extends IConfigurationValue { target: EditableConfigurationTarget; jsonPath: json.JSONPath; - resource: URI | null; + resource?: URI; workspaceStandAloneConfigurationKey?: string; - apply(save: boolean): Promise; + } interface ConfigurationEditingOptions extends IConfigurationEditingOptions { - donotSave?: boolean; force?: boolean; } -function toConfigurationEditingError(error: ConfigurationEditingErrorCode, operation: IConfigurationEditOperation, contextService: IWorkspaceContextService): ConfigurationEditingError { - switch (error) { +export class ConfigurationEditingService { - // API constraints - case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return new ConfigurationEditingError(nls.localize('errorUnknownKey', "Unable to write to {0} because {1} is not a registered configuration.", stringifyTarget(operation.target), operation.value.key), error); - case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION: return new ConfigurationEditingError(nls.localize('errorInvalidWorkspaceConfigurationApplication', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.value.key), error); - case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE: return new ConfigurationEditingError(nls.localize('errorInvalidWorkspaceConfigurationMachine', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.value.key), error); - case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION: return new ConfigurationEditingError(nls.localize('errorInvalidFolderConfiguration', "Unable to write to Folder Settings because {0} does not support the folder resource scope.", operation.value.key), error); - case ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET: return new ConfigurationEditingError(nls.localize('errorInvalidUserTarget', "Unable to write to User Settings because {0} does not support for global scope.", operation.value.key), error); - case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET: return new ConfigurationEditingError(nls.localize('errorInvalidWorkspaceTarget', "Unable to write to Workspace Settings because {0} does not support for workspace scope in a multi folder workspace.", operation.value.key), error); - case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET: return new ConfigurationEditingError(nls.localize('errorInvalidFolderTarget', "Unable to write to Folder Settings because no resource is provided."), error); - case ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED: return new ConfigurationEditingError(nls.localize('errorNoWorkspaceOpened', "Unable to write to {0} because no workspace is opened. Please open a workspace first and try again.", stringifyTarget(operation.target)), error); + public _serviceBrand: any; - // User issues - case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION: { - if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { - return new ConfigurationEditingError(nls.localize('errorInvalidTaskConfiguration', "Unable to write into the tasks configuration file. Please open it to correct errors/warnings in it and try again."), error); - } - if (operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY) { - return new ConfigurationEditingError(nls.localize('errorInvalidLaunchConfiguration', "Unable to write into the launch configuration file. Please open it to correct errors/warnings in it and try again."), error); - } - switch (operation.target) { - case EditableConfigurationTarget.USER_LOCAL: - return new ConfigurationEditingError(nls.localize('errorInvalidConfiguration', "Unable to write into user settings. Please open the user settings to correct errors/warnings in it and try again."), error); - case EditableConfigurationTarget.USER_REMOTE: - return new ConfigurationEditingError(nls.localize('errorInvalidRemoteConfiguration', "Unable to write into remote user settings. Please open the remote user settings to correct errors/warnings in it and try again."), error); - case EditableConfigurationTarget.WORKSPACE: - return new ConfigurationEditingError(nls.localize('errorInvalidConfigurationWorkspace', "Unable to write into workspace settings. Please open the workspace settings to correct errors/warnings in the file and try again."), error); - case EditableConfigurationTarget.WORKSPACE_FOLDER: - let workspaceFolderName: string = '<>'; - if (operation.resource) { - const folder = contextService.getWorkspaceFolder(operation.resource); - if (folder) { - workspaceFolderName = folder.name; - } - } - return new ConfigurationEditingError(nls.localize('errorInvalidConfigurationFolder', "Unable to write into folder settings. Please open the '{0}' folder settings to correct errors/warnings in it and try again.", workspaceFolderName), error); - } - return new ConfigurationEditingError('', error); - } - case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY: { - if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { - return new ConfigurationEditingError(nls.localize('errorTasksConfigurationFileDirty', "Unable to write into tasks configuration file because the file is dirty. Please save it first and then try again."), error); - } - if (operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY) { - return new ConfigurationEditingError(nls.localize('errorLaunchConfigurationFileDirty', "Unable to write into launch configuration file because the file is dirty. Please save it first and then try again."), error); - } - switch (operation.target) { - case EditableConfigurationTarget.USER_LOCAL: - return new ConfigurationEditingError(nls.localize('errorConfigurationFileDirty', "Unable to write into user settings because the file is dirty. Please save the user settings file first and then try again."), error); - case EditableConfigurationTarget.USER_REMOTE: - return new ConfigurationEditingError(nls.localize('errorRemoteConfigurationFileDirty', "Unable to write into remote user settings because the file is dirty. Please save the remote user settings file first and then try again."), error); - case EditableConfigurationTarget.WORKSPACE: - return new ConfigurationEditingError(nls.localize('errorConfigurationFileDirtyWorkspace', "Unable to write into workspace settings because the file is dirty. Please save the workspace settings file first and then try again."), error); - case EditableConfigurationTarget.WORKSPACE_FOLDER: - let workspaceFolderName: string = '<>'; - if (operation.resource) { - const folder = contextService.getWorkspaceFolder(operation.resource); - if (folder) { - workspaceFolderName = folder.name; - } - } - return new ConfigurationEditingError(nls.localize('errorConfigurationFileDirtyFolder', "Unable to write into folder settings because the file is dirty. Please save the '{0}' folder settings file first and then try again.", workspaceFolderName), error); - } - return new ConfigurationEditingError('', error); - } - } -} - -function stringifyTarget(target: EditableConfigurationTarget): string { - switch (target) { - case EditableConfigurationTarget.USER_LOCAL: - return nls.localize('userTarget', "User Settings"); - case EditableConfigurationTarget.USER_REMOTE: - return nls.localize('remoteUserTarget', "Remote User Settings"); - case EditableConfigurationTarget.WORKSPACE: - return nls.localize('workspaceTarget', "Workspace Settings"); - case EditableConfigurationTarget.WORKSPACE_FOLDER: - return nls.localize('folderTarget', "Folder Settings"); - } - return ''; -} - -abstract class ConfigurationEditOperation extends Disposable implements IConfigurationEditOperation { + private queue: Queue; + private remoteSettingsResource: URI | null; constructor( - readonly value: IConfigurationValue, - readonly target: EditableConfigurationTarget, - readonly jsonPath: json.JSONPath, - readonly resource: URI | null, - readonly workspaceStandAloneConfigurationKey: string | undefined, - protected readonly contextService: IWorkspaceContextService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IFileService private readonly fileService: IFileService, + @ITextModelService private readonly textModelResolverService: ITextModelService, + @ITextFileService private readonly textFileService: ITextFileService, + @INotificationService private readonly notificationService: INotificationService, + @IPreferencesService private readonly preferencesService: IPreferencesService, + @IEditorService private readonly editorService: IEditorService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService ) { - super(); + this.queue = new Queue(); + remoteAgentService.getEnvironment().then(environment => { + if (environment) { + this.remoteSettingsResource = environment.settingsPath; + } + }); } - async apply(save: boolean): Promise { - this.validate(); - const model = await this.resolve(); - if (this.hasParseErrors(model)) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION, this, this.contextService); - } - const edit = this.getEdits(model)[0]; - if (edit && this.applyEditsToBuffer(edit, model) && save) { - await this.save(model); - } + writeConfiguration(target: EditableConfigurationTarget, value: IConfigurationValue, options: IConfigurationEditingOptions = {}): Promise { + const operation = this.getConfigurationEditOperation(target, value, options.scopes || {}); + return Promise.resolve(this.queue.queue(() => this.doWriteConfiguration(operation, options) // queue up writes to prevent race conditions + .then(() => null, + error => { + if (!options.donotNotifyError) { + this.onError(error, operation, options.scopes); + } + return Promise.reject(error); + }))); + } + private doWriteConfiguration(operation: IConfigurationEditOperation, options: ConfigurationEditingOptions): Promise { + const checkDirtyConfiguration = !(options.force || options.donotSave); + const saveConfiguration = options.force || !options.donotSave; + return this.resolveAndValidate(operation.target, operation, checkDirtyConfiguration, options.scopes || {}) + .then(reference => this.writeToBuffer(reference.object.textEditorModel, operation, saveConfiguration) + .then(() => reference.dispose())); + } + + private async writeToBuffer(model: ITextModel, operation: IConfigurationEditOperation, save: boolean): Promise { + const edit = this.getEdits(model, operation)[0]; + if (edit && this.applyEditsToBuffer(edit, model) && save) { + return this.textFileService.save(operation.resource!, { skipSaveParticipants: true /* programmatic change */ }); + } } private applyEditsToBuffer(edit: Edit, model: ITextModel): boolean { @@ -254,239 +195,6 @@ abstract class ConfigurationEditOperation extends Disposable implements IConfigu return false; } - private getEdits(model: ITextModel): Edit[] { - const { tabSize, insertSpaces } = model.getOptions(); - const eol = model.getEOL(); - - // Without jsonPath, the entire configuration file is being replaced, so we just use JSON.stringify - if (!this.jsonPath.length) { - const content = JSON.stringify(this.value.value, null, insertSpaces ? strings.repeat(' ', tabSize) : '\t'); - return [{ - content, - length: model.getValue().length, - offset: 0 - }]; - } - - return setProperty(model.getValue(), this.jsonPath, this.value.value, { tabSize, insertSpaces, eol }); - } - - private hasParseErrors(model: ITextModel): boolean { - // If we write to a workspace standalone file and replace the entire contents (no key provided) - // we can return here because any parse errors can safely be ignored since all contents are replaced - if (this.workspaceStandAloneConfigurationKey && !this.value.key) { - return false; - } - const parseErrors: json.ParseError[] = []; - json.parse(model.getValue(), parseErrors); - return parseErrors.length > 0; - } - - protected abstract validate(): void; - protected abstract save(model: ITextModel): Promise; - protected abstract resolve(): Promise; -} - -class ResourceConfigurationEditOperation extends ConfigurationEditOperation { - - private resolvePromise: Promise | undefined = undefined; - - constructor( - value: IConfigurationValue, - target: EditableConfigurationTarget, - jsonPath: json.JSONPath, - readonly resource: URI, - workspaceStandAloneConfigurationKey: string | undefined, - private readonly checkDirty: boolean, - @IFileService private readonly fileService: IFileService, - @ITextFileService private readonly textFileService: ITextFileService, - @ITextModelService private readonly textModelResolverService: ITextModelService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IWorkspaceContextService contextService: IWorkspaceContextService - ) { - super( - value, - target, - jsonPath, - resource, - workspaceStandAloneConfigurationKey, - contextService - ); - } - - protected async save(model: ITextModel): Promise { - await this.textFileService.save(this.resource, { skipSaveParticipants: true /* programmatic change */ }); - } - - protected async resolve(): Promise { - if (!this.resolvePromise) { - this.resolvePromise = this._resolve(); - } - return this.resolvePromise; - } - - protected validate(): void { - if (this.checkDirty && this.textFileService.isDirty(this.resource)) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY, this, this.contextService); - } - - // Any key must be a known setting from the registry (unless this is a standalone config) - if (!this.workspaceStandAloneConfigurationKey) { - const validKeys = this.configurationService.keys().default; - if (validKeys.indexOf(this.value.key) < 0 && !OVERRIDE_PROPERTY_PATTERN.test(this.value.key)) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY, this, this.contextService); - } - } - - if (this.workspaceStandAloneConfigurationKey) { - // Global tasks and launches are not supported - if (this.target === EditableConfigurationTarget.USER_LOCAL || this.target === EditableConfigurationTarget.USER_REMOTE) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET, this, this.contextService); - } - - // Workspace tasks are not supported - if (this.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && this.target === EditableConfigurationTarget.WORKSPACE) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET, this, this.contextService); - } - } - - // Target cannot be workspace or folder if no workspace opened - if ((this.target === EditableConfigurationTarget.WORKSPACE || this.target === EditableConfigurationTarget.WORKSPACE_FOLDER) && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED, this, this.contextService); - } - - if (this.target === EditableConfigurationTarget.WORKSPACE) { - if (!this.workspaceStandAloneConfigurationKey) { - const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); - if (configurationProperties[this.value.key].scope === ConfigurationScope.APPLICATION) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, this, this.contextService); - } - if (configurationProperties[this.value.key].scope === ConfigurationScope.MACHINE) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE, this, this.contextService); - } - } - } - - if (this.target === EditableConfigurationTarget.WORKSPACE_FOLDER) { - if (!this.resource) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, this, this.contextService); - } - - if (!this.workspaceStandAloneConfigurationKey) { - const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); - if (configurationProperties[this.value.key].scope !== ConfigurationScope.RESOURCE) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION, this, this.contextService); - } - } - } - - if (!this.resource) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, this, this.contextService); - } - } - - private async _resolve(): Promise { - const exists = await this.fileService.exists(this.resource); - if (!exists) { - await this.textFileService.write(this.resource, '{}', { encoding: 'utf8' }); - } - const reference = this._register(await this.textModelResolverService.createModelReference(this.resource)); - return reference.object.textEditorModel; - } -} - -class UserConfigurationEditOperation extends ConfigurationEditOperation { - - private resolvePromise: Promise | undefined = undefined; - - constructor( - value: IConfigurationValue, - jsonPath: json.JSONPath, - @IUserDataService private readonly userDataService: IUserDataService, - @IModelService private readonly modelService: IModelService, - @IModeService private readonly modeService: IModeService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IWorkspaceContextService contextService: IWorkspaceContextService - ) { - super( - value, - EditableConfigurationTarget.USER_LOCAL, - jsonPath, - null, - undefined, - contextService - ); - } - - protected async save(model: ITextModel): Promise { - await this.userDataService.write(USER_CONFIGURATION_KEY, model.getValue()); - } - - protected validate(): void { - // Any key must be a known setting from the registry - const validKeys = this.configurationService.keys().default; - if (validKeys.indexOf(this.value.key) < 0 && !OVERRIDE_PROPERTY_PATTERN.test(this.value.key)) { - throw toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY, this, this.contextService); - } - } - - protected resolve(): Promise { - if (!this.resolvePromise) { - this.resolvePromise = this._resolve(); - } - return this.resolvePromise; - } - - private async _resolve(): Promise { - const content = (await this.userDataService.read(USER_CONFIGURATION_KEY)) || '{}'; - const languageIdentifier = this.modeService.getLanguageIdentifier('jsonc'); - const model = this.modelService.createModel(content, languageIdentifier ? { languageIdentifier, onDidChange: new Emitter().event, dispose: () => { } } : null, this.configurationService.userSettingsResource.with({ scheme: Schemas.vscode })); - this._register(toDisposable(() => { - model.dispose(); - this.modelService.destroyModel(model.uri); - })); - return model; - } -} - -export class ConfigurationEditingService { - - public _serviceBrand: any; - - private queue: Queue; - private remoteSettingsResource: URI | null; - - constructor( - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @INotificationService private readonly notificationService: INotificationService, - @IPreferencesService private readonly preferencesService: IPreferencesService, - @IEditorService private readonly editorService: IEditorService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - this.queue = new Queue(); - remoteAgentService.getEnvironment().then(environment => { - if (environment) { - this.remoteSettingsResource = environment.settingsPath; - } - }); - } - - writeConfiguration(target: EditableConfigurationTarget, value: IConfigurationValue, options: ConfigurationEditingOptions = {}): Promise { - return this.queue.queue(async () => { // queue up writes to prevent race conditions - const operation = this.getConfigurationEditOperation(target, value, options.scopes || {}, !(options.force || options.donotSave)); - try { - await operation.apply(options.force || !options.donotSave); - operation.dispose(); - } catch (error) { - if (!options.donotNotifyError) { - this.onError(error, operation, options.scopes); - } - return Promise.reject(error); - } - }); - } - private onError(error: ConfigurationEditingError, operation: IConfigurationEditOperation, scopes: IConfigurationOverrides | undefined): void { switch (error.code) { case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION: @@ -530,7 +238,7 @@ export class ConfigurationEditingService { [{ label: nls.localize('saveAndRetry', "Save and Retry"), run: () => { - const key = operation.value.key ? `${operation.workspaceStandAloneConfigurationKey}.${operation.value.key}` : operation.workspaceStandAloneConfigurationKey!; + const key = operation.key ? `${operation.workspaceStandAloneConfigurationKey}.${operation.key}` : operation.workspaceStandAloneConfigurationKey!; this.writeConfiguration(operation.target, { key, value: operation.value }, { force: true, scopes }); } }, @@ -543,7 +251,7 @@ export class ConfigurationEditingService { this.notificationService.prompt(Severity.Error, error.message, [{ label: nls.localize('saveAndRetry', "Save and Retry"), - run: () => this.writeConfiguration(operation.target, { key: operation.value.key, value: operation.value }, { force: true, scopes }) + run: () => this.writeConfiguration(operation.target, { key: operation.key, value: operation.value }, { force: true, scopes }) }, { label: nls.localize('open', "Open Settings"), @@ -579,7 +287,205 @@ export class ConfigurationEditingService { this.editorService.openEditor({ resource }); } - private getConfigurationEditOperation(target: EditableConfigurationTarget, config: IConfigurationValue, overrides: IConfigurationOverrides, checkDirty: boolean): IConfigurationEditOperation { + private reject(code: ConfigurationEditingErrorCode, target: EditableConfigurationTarget, operation: IConfigurationEditOperation): Promise { + const message = this.toErrorMessage(code, target, operation); + + return Promise.reject(new ConfigurationEditingError(message, code)); + } + + private toErrorMessage(error: ConfigurationEditingErrorCode, target: EditableConfigurationTarget, operation: IConfigurationEditOperation): string { + switch (error) { + + // API constraints + case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to {0} because {1} is not a registered configuration.", this.stringifyTarget(target), operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION: return nls.localize('errorInvalidWorkspaceConfigurationApplication', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE: return nls.localize('errorInvalidWorkspaceConfigurationMachine', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION: return nls.localize('errorInvalidFolderConfiguration', "Unable to write to Folder Settings because {0} does not support the folder resource scope.", operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET: return nls.localize('errorInvalidUserTarget', "Unable to write to User Settings because {0} does not support for global scope.", operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET: return nls.localize('errorInvalidWorkspaceTarget', "Unable to write to Workspace Settings because {0} does not support for workspace scope in a multi folder workspace.", operation.key); + case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET: return nls.localize('errorInvalidFolderTarget', "Unable to write to Folder Settings because no resource is provided."); + case ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED: return nls.localize('errorNoWorkspaceOpened', "Unable to write to {0} because no workspace is opened. Please open a workspace first and try again.", this.stringifyTarget(target)); + + // User issues + case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION: { + if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { + return nls.localize('errorInvalidTaskConfiguration', "Unable to write into the tasks configuration file. Please open it to correct errors/warnings in it and try again."); + } + if (operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY) { + return nls.localize('errorInvalidLaunchConfiguration', "Unable to write into the launch configuration file. Please open it to correct errors/warnings in it and try again."); + } + switch (target) { + case EditableConfigurationTarget.USER_LOCAL: + return nls.localize('errorInvalidConfiguration', "Unable to write into user settings. Please open the user settings to correct errors/warnings in it and try again."); + case EditableConfigurationTarget.USER_REMOTE: + return nls.localize('errorInvalidRemoteConfiguration', "Unable to write into remote user settings. Please open the remote user settings to correct errors/warnings in it and try again."); + case EditableConfigurationTarget.WORKSPACE: + return nls.localize('errorInvalidConfigurationWorkspace', "Unable to write into workspace settings. Please open the workspace settings to correct errors/warnings in the file and try again."); + case EditableConfigurationTarget.WORKSPACE_FOLDER: + let workspaceFolderName: string = '<>'; + if (operation.resource) { + const folder = this.contextService.getWorkspaceFolder(operation.resource); + if (folder) { + workspaceFolderName = folder.name; + } + } + return nls.localize('errorInvalidConfigurationFolder', "Unable to write into folder settings. Please open the '{0}' folder settings to correct errors/warnings in it and try again.", workspaceFolderName); + } + return ''; + } + case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY: { + if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { + return nls.localize('errorTasksConfigurationFileDirty', "Unable to write into tasks configuration file because the file is dirty. Please save it first and then try again."); + } + if (operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY) { + return nls.localize('errorLaunchConfigurationFileDirty', "Unable to write into launch configuration file because the file is dirty. Please save it first and then try again."); + } + switch (target) { + case EditableConfigurationTarget.USER_LOCAL: + return nls.localize('errorConfigurationFileDirty', "Unable to write into user settings because the file is dirty. Please save the user settings file first and then try again."); + case EditableConfigurationTarget.USER_REMOTE: + return nls.localize('errorRemoteConfigurationFileDirty', "Unable to write into remote user settings because the file is dirty. Please save the remote user settings file first and then try again."); + case EditableConfigurationTarget.WORKSPACE: + return nls.localize('errorConfigurationFileDirtyWorkspace', "Unable to write into workspace settings because the file is dirty. Please save the workspace settings file first and then try again."); + case EditableConfigurationTarget.WORKSPACE_FOLDER: + let workspaceFolderName: string = '<>'; + if (operation.resource) { + const folder = this.contextService.getWorkspaceFolder(operation.resource); + if (folder) { + workspaceFolderName = folder.name; + } + } + return nls.localize('errorConfigurationFileDirtyFolder', "Unable to write into folder settings because the file is dirty. Please save the '{0}' folder settings file first and then try again.", workspaceFolderName); + } + return ''; + } + } + } + + private stringifyTarget(target: EditableConfigurationTarget): string { + switch (target) { + case EditableConfigurationTarget.USER_LOCAL: + return nls.localize('userTarget', "User Settings"); + case EditableConfigurationTarget.USER_REMOTE: + return nls.localize('remoteUserTarget', "Remote User Settings"); + case EditableConfigurationTarget.WORKSPACE: + return nls.localize('workspaceTarget', "Workspace Settings"); + case EditableConfigurationTarget.WORKSPACE_FOLDER: + return nls.localize('folderTarget', "Folder Settings"); + } + return ''; + } + + private getEdits(model: ITextModel, edit: IConfigurationEditOperation): Edit[] { + const { tabSize, insertSpaces } = model.getOptions(); + const eol = model.getEOL(); + const { value, jsonPath } = edit; + + // Without jsonPath, the entire configuration file is being replaced, so we just use JSON.stringify + if (!jsonPath.length) { + const content = JSON.stringify(value, null, insertSpaces ? strings.repeat(' ', tabSize) : '\t'); + return [{ + content, + length: model.getValue().length, + offset: 0 + }]; + } + + return setProperty(model.getValue(), jsonPath, value, { tabSize, insertSpaces, eol }); + } + + private async resolveModelReference(resource: URI): Promise> { + const exists = await this.fileService.exists(resource); + if (!exists) { + await this.textFileService.write(resource, '{}', { encoding: 'utf8' }); + } + return this.textModelResolverService.createModelReference(resource); + } + + private hasParseErrors(model: ITextModel, operation: IConfigurationEditOperation): boolean { + // If we write to a workspace standalone file and replace the entire contents (no key provided) + // we can return here because any parse errors can safely be ignored since all contents are replaced + if (operation.workspaceStandAloneConfigurationKey && !operation.key) { + return false; + } + const parseErrors: json.ParseError[] = []; + json.parse(model.getValue(), parseErrors); + return parseErrors.length > 0; + } + + private resolveAndValidate(target: EditableConfigurationTarget, operation: IConfigurationEditOperation, checkDirty: boolean, overrides: IConfigurationOverrides): Promise> { + + // Any key must be a known setting from the registry (unless this is a standalone config) + if (!operation.workspaceStandAloneConfigurationKey) { + const validKeys = this.configurationService.keys().default; + if (validKeys.indexOf(operation.key) < 0 && !OVERRIDE_PROPERTY_PATTERN.test(operation.key)) { + return this.reject(ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY, target, operation); + } + } + + if (operation.workspaceStandAloneConfigurationKey) { + // Global tasks and launches are not supported + if (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE) { + return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET, target, operation); + } + + // Workspace tasks are not supported + if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && operation.target === EditableConfigurationTarget.WORKSPACE) { + return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET, target, operation); + } + } + + // Target cannot be workspace or folder if no workspace opened + if ((target === EditableConfigurationTarget.WORKSPACE || target === EditableConfigurationTarget.WORKSPACE_FOLDER) && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + return this.reject(ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED, target, operation); + } + + if (target === EditableConfigurationTarget.WORKSPACE) { + if (!operation.workspaceStandAloneConfigurationKey) { + const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); + if (configurationProperties[operation.key].scope === ConfigurationScope.APPLICATION) { + return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, target, operation); + } + if (configurationProperties[operation.key].scope === ConfigurationScope.MACHINE) { + return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE, target, operation); + } + } + } + + if (target === EditableConfigurationTarget.WORKSPACE_FOLDER) { + if (!operation.resource) { + return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation); + } + + if (!operation.workspaceStandAloneConfigurationKey) { + const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); + if (configurationProperties[operation.key].scope !== ConfigurationScope.RESOURCE) { + return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION, target, operation); + } + } + } + + if (!operation.resource) { + return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation); + } + + return this.resolveModelReference(operation.resource) + .then(reference => { + const model = reference.object.textEditorModel; + + if (this.hasParseErrors(model, operation)) { + return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION, target, operation); + } + + // Target cannot be dirty if not writing into buffer + if (checkDirty && this.textFileService.isDirty(operation.resource)) { + return this.reject(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY, target, operation); + } + return reference; + }); + } + + private getConfigurationEditOperation(target: EditableConfigurationTarget, config: IConfigurationValue, overrides: IConfigurationOverrides): IConfigurationEditOperation { // Check for standalone workspace configurations if (config.key) { @@ -590,32 +496,29 @@ export class ConfigurationEditingService { // Check for prefix if (config.key === key) { const jsonPath = this.isWorkspaceConfigurationResource(resource) ? [key] : []; - return this.instantiationService.createInstance(ResourceConfigurationEditOperation, { key: jsonPath[jsonPath.length - 1], value: config.value }, target, jsonPath, resource, key, checkDirty); + return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource: withNullAsUndefined(resource), workspaceStandAloneConfigurationKey: key, target }; } // Check for prefix. const keyPrefix = `${key}.`; if (config.key.indexOf(keyPrefix) === 0) { const jsonPath = this.isWorkspaceConfigurationResource(resource) ? [key, config.key.substr(keyPrefix.length)] : [config.key.substr(keyPrefix.length)]; - return this.instantiationService.createInstance(ResourceConfigurationEditOperation, { key: jsonPath[jsonPath.length - 1], value: config.value }, target, jsonPath, resource, key, checkDirty); + return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource: withNullAsUndefined(resource), workspaceStandAloneConfigurationKey: key, target }; } } } let key = config.key; let jsonPath = overrides.overrideIdentifier ? [keyFromOverrideIdentifier(overrides.overrideIdentifier), key] : [key]; - if (target === EditableConfigurationTarget.USER_LOCAL) { - return this.instantiationService.createInstance(UserConfigurationEditOperation, { key, value: config.value }, jsonPath); - } - if (target === EditableConfigurationTarget.USER_REMOTE) { - return this.instantiationService.createInstance(ResourceConfigurationEditOperation, { key, value: config.value }, target, jsonPath, withNullAsUndefined(this.getConfigurationFileResource(target, config, '', null)), undefined, checkDirty); + if (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE) { + return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, config, '', null)), target }; } const resource = this.getConfigurationFileResource(target, config, FOLDER_SETTINGS_PATH, overrides.resource); if (this.isWorkspaceConfigurationResource(resource)) { jsonPath = ['settings', ...jsonPath]; } - return this.instantiationService.createInstance(ResourceConfigurationEditOperation, { key, value: config.value }, target, jsonPath, withNullAsUndefined(resource), undefined, checkDirty); + return { key, jsonPath, value: config.value, resource: withNullAsUndefined(resource), target }; } private isWorkspaceConfigurationResource(resource: URI | null): boolean { @@ -625,7 +528,7 @@ export class ConfigurationEditingService { private getConfigurationFileResource(target: EditableConfigurationTarget, config: IConfigurationValue, relativePath: string, resource: URI | null | undefined): URI | null { if (target === EditableConfigurationTarget.USER_LOCAL) { - return null; + return this.environmentService.settingsResource; } if (target === EditableConfigurationTarget.USER_REMOTE) { return this.remoteSettingsResource; diff --git a/src/vs/workbench/services/configuration/node/configurationCache.ts b/src/vs/workbench/services/configuration/node/configurationCache.ts index 67b46a14cb2..432c8ada983 100644 --- a/src/vs/workbench/services/configuration/node/configurationCache.ts +++ b/src/vs/workbench/services/configuration/node/configurationCache.ts @@ -7,12 +7,13 @@ import * as pfs from 'vs/base/node/pfs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { join } from 'vs/base/common/path'; import { IConfigurationCache, ConfigurationKey } from 'vs/workbench/services/configuration/common/configuration'; +import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService'; export class ConfigurationCache implements IConfigurationCache { private readonly cachedConfigurations: Map = new Map(); - constructor(private readonly environmentService: IEnvironmentService) { + constructor(private readonly environmentService: IWorkbenchEnvironmentService) { } read(key: ConfigurationKey): Promise { diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts index 44fc5a99eeb..0c295fcd0af 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts @@ -13,7 +13,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment'; import { parseArgs } from 'vs/platform/environment/node/argv'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; @@ -39,15 +38,17 @@ import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider'; import { IFileService } from 'vs/platform/files/common/files'; import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache'; -import { FileUserDataService } from 'vs/workbench/services/userData/common/fileUserDataService'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; import { dirname } from 'vs/base/common/resources'; import { KeybindingsEditingService, IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; +import { UserDataFileSystemProvider } from 'vs/workbench/services/userData/common/userDataFileSystemProvider'; +import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService'; +import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; -class SettingsTestEnvironmentService extends EnvironmentService { +class SettingsTestEnvironmentService extends WorkbenchEnvironmentService { constructor(args: ParsedArgs, _execPath: string, private _settingsPath: string) { - super(args, _execPath); + super(args, _execPath); } get appSettingsHome(): URI { return dirname(URI.file(this._settingsPath)); } @@ -111,9 +112,8 @@ suite('ConfigurationEditingService', () => { fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IRemoteAgentService, remoteAgentService); - const userDataService = new FileUserDataService(environmentService.appSettingsHome, fileService); - instantiationService.stub(IUserDataService, userDataService); - const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, fileService, userDataService, remoteAgentService); + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(environmentService.appSettingsHome.with({ scheme: Schemas.userData }), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); + const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); return workspaceService.initialize(noWorkspace ? { id: '' } : { folder: URI.file(workspaceDir), id: createHash('md5').update(URI.file(workspaceDir).toString()).digest('hex') }).then(() => { instantiationService.stub(IConfigurationService, workspaceService); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index 7be91c54df9..4554c25471e 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -11,7 +11,6 @@ import * as os from 'os'; import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs } from 'vs/platform/environment/node/argv'; import * as pfs from 'vs/base/node/pfs'; import * as uuid from 'vs/base/common/uuid'; @@ -45,14 +44,16 @@ import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEn import { IConfigurationCache } from 'vs/workbench/services/configuration/common/configuration'; import { VSBuffer } from 'vs/base/common/buffer'; import { SignService } from 'vs/platform/sign/browser/signService'; -import { FileUserDataService } from 'vs/workbench/services/userData/common/fileUserDataService'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; +import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; import { IKeybindingEditingService, KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; +import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService'; +import { UserDataFileSystemProvider } from 'vs/workbench/services/userData/common/userDataFileSystemProvider'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -class SettingsTestEnvironmentService extends EnvironmentService { +class SettingsTestEnvironmentService extends WorkbenchEnvironmentService { constructor(args: ParsedArgs, _execPath: string, private _settingsPath: string) { - super(args, _execPath); + super(args, _execPath); } get appSettingsHome(): URI { return dirname(URI.file(this._settingsPath)); } @@ -107,8 +108,8 @@ suite('WorkspaceContextService - Folder', () => { const globalSettingsFile = path.join(parentDir, 'settings.json'); const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile); const fileService = new FileService(new NullLogService()); - const userDataService = new FileUserDataService(environmentService.appSettingsHome, fileService); - workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, fileService, userDataService, new RemoteAgentService({}, environmentService, new RemoteAuthorityResolverService(), new SignService())); + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(environmentService.appSettingsHome.with({ scheme: Schemas.userData }), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); + workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService({}, environmentService, new RemoteAuthorityResolverService(), new SignService())); return (workspaceContextService).initialize(convertToWorkspacePayload(URI.file(folderDir))); }); }); @@ -172,8 +173,8 @@ suite('WorkspaceContextService - Workspace', () => { instantiationService.stub(IRemoteAgentService, remoteAgentService); const fileService = new FileService(new NullLogService()); fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); - const userDataService = new FileUserDataService(environmentService.appSettingsHome, fileService); - const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, fileService, userDataService, remoteAgentService); + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(environmentService.appSettingsHome.with({ scheme: Schemas.userData }), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); + const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService); @@ -231,8 +232,8 @@ suite('WorkspaceContextService - Workspace Editing', () => { instantiationService.stub(IRemoteAgentService, remoteAgentService); const fileService = new FileService(new NullLogService()); fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); - const userDataService = new FileUserDataService(environmentService.appSettingsHome, fileService); - const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, fileService, userDataService, remoteAgentService); + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(environmentService.appSettingsHome.with({ scheme: Schemas.userData }), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); + const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService); @@ -491,8 +492,8 @@ suite('WorkspaceService - Initialization', () => { instantiationService.stub(IRemoteAgentService, remoteAgentService); const fileService = new FileService(new NullLogService()); fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); - const userDataService = new FileUserDataService(environmentService.appSettingsHome, fileService); - const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, fileService, userDataService, remoteAgentService); + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(environmentService.appSettingsHome.with({ scheme: Schemas.userData }), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); + const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(IEnvironmentService, environmentService); @@ -754,9 +755,8 @@ suite('WorkspaceConfigurationService - Folder', () => { instantiationService.stub(IRemoteAgentService, remoteAgentService); const fileService = new FileService(new NullLogService()); fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); - const userDataService = new FileUserDataService(environmentService.appSettingsHome, fileService); - instantiationService.stub(IUserDataService, userDataService); - const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, fileService, userDataService, remoteAgentService); + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(environmentService.appSettingsHome.with({ scheme: Schemas.userData }), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); + const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(IEnvironmentService, environmentService); @@ -1040,7 +1040,7 @@ suite('WorkspaceConfigurationService - Folder', () => { suite('WorkspaceConfigurationService-Multiroot', () => { - let parentResource: string, workspaceContextService: IWorkspaceContextService, environmentService: IEnvironmentService, jsonEditingServce: IJSONEditingService, testObject: IConfigurationService, globalSettingsFile: string; + let parentResource: string, workspaceContextService: IWorkspaceContextService, environmentService: IWorkbenchEnvironmentService, jsonEditingServce: IJSONEditingService, testObject: IConfigurationService, globalSettingsFile: string; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); suiteSetup(() => { @@ -1084,13 +1084,12 @@ suite('WorkspaceConfigurationService-Multiroot', () => { instantiationService.stub(IRemoteAgentService, remoteAgentService); const fileService = new FileService(new NullLogService()); fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); - const userDataService = new FileUserDataService(environmentService.appSettingsHome, fileService); - instantiationService.stub(IUserDataService, userDataService); - const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, fileService, userDataService, remoteAgentService); + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(environmentService.appSettingsHome.with({ scheme: Schemas.userData }), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); + const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService); - instantiationService.stub(IEnvironmentService, environmentService); + instantiationService.stub(IWorkbenchEnvironmentService, environmentService); return workspaceService.initialize(getWorkspaceIdentifier(configPath)).then(() => { instantiationService.stub(IFileService, fileService); @@ -1487,9 +1486,9 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { const remoteAgentService = instantiationService.stub(IRemoteAgentService, >{ getEnvironment: () => remoteEnvironmentPromise }); const fileService = new FileService(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(environmentService.appSettingsHome.with({ scheme: Schemas.userData }), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); const configurationCache: IConfigurationCache = { read: () => Promise.resolve(''), write: () => Promise.resolve(), remove: () => Promise.resolve() }; - const userDataService = new FileUserDataService(environmentService.appSettingsHome, fileService); - testObject = new WorkspaceService({ configurationCache, remoteAuthority }, fileService, userDataService, remoteAgentService); + testObject = new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); instantiationService.stub(IEnvironmentService, environmentService); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index dfed462b94b..95f1bbf6a30 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -63,14 +63,23 @@ export class BrowserWorkbenchEnvironmentService implements IEnvironmentService { readonly configuration: IWindowConfiguration = new BrowserWindowConfiguration(); - constructor(configuration: IWorkbenchConstructionOptions) { + constructor(configuration: IWorkbenchConstructionOptions, remoteUserDataUri: URI | null) { this.args = { _: [] }; this.appRoot = '/web/'; this.appNameLong = 'Visual Studio Code - Web'; this.configuration.remoteAuthority = configuration.remoteAuthority; - this.appSettingsHome = joinPath(URI.revive(JSON.parse(document.getElementById('vscode-remote-user-data-uri')!.getAttribute('data-settings')!)), 'User'); + if (remoteUserDataUri) { + this.appSettingsHome = remoteUserDataUri || URI.file('/User').with({ scheme: Schemas.userData }); + this.settingsResource = joinPath(this.appSettingsHome, 'settings.json').with({ scheme: Schemas.userData }); + this.keybindingsResource = joinPath(this.appSettingsHome, 'keybindings.json').with({ scheme: Schemas.userData }); + } else { + const appSettingsHome = URI.file('/User').with({ scheme: Schemas.userData }); + this.settingsResource = joinPath(appSettingsHome, 'settings.json'); + this.keybindingsResource = joinPath(appSettingsHome, 'keybindings.json'); + } + this.keyboardLayoutResource = joinPath(this.appSettingsHome, 'keyboardLayout.json'); this.logsPath = '/web/logs'; @@ -95,6 +104,8 @@ export class BrowserWorkbenchEnvironmentService implements IEnvironmentService { appNameLong: string; appQuality?: string; appSettingsHome: URI; + settingsResource: URI; + keybindingsResource: URI; keyboardLayoutResource: URI; machineSettingsHome: URI; machineSettingsResource: URI; diff --git a/src/vs/workbench/services/environment/node/environmentService.ts b/src/vs/workbench/services/environment/node/environmentService.ts index db2a229c03b..bb72c8785b8 100644 --- a/src/vs/workbench/services/environment/node/environmentService.ts +++ b/src/vs/workbench/services/environment/node/environmentService.ts @@ -6,6 +6,10 @@ import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { memoize } from 'vs/base/common/decorators'; +import { URI } from 'vs/base/common/uri'; +import { joinPath } from 'vs/base/common/resources'; +import { Schemas } from 'vs/base/common/network'; export class WorkbenchEnvironmentService extends EnvironmentService implements IWorkbenchEnvironmentService { @@ -21,4 +25,10 @@ export class WorkbenchEnvironmentService extends EnvironmentService implements I get configuration(): IWindowConfiguration { return this._configuration; } + + @memoize + get settingsResource(): URI { return joinPath(this.appSettingsHome, 'settings.json').with({ scheme: Schemas.userData }); } + + @memoize + get keybindingsResource(): URI { return joinPath(this.appSettingsHome, 'keybindings.json').with({ scheme: Schemas.userData }); } } diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index f0ddd401ce9..fb7e5465845 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -19,7 +19,7 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService'; -import { IKeyboardEvent, IUserFriendlyKeybinding, KeybindingSource, IKeybindingService, IKeybindingEvent, USER_KEYBINDINGS_KEY } from 'vs/platform/keybinding/common/keybinding'; +import { IKeyboardEvent, IUserFriendlyKeybinding, KeybindingSource, IKeybindingService, IKeybindingEvent } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver'; import { IKeybindingItem, IKeybindingRule2, KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -36,15 +36,17 @@ import { MenuRegistry } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; // tslint:disable-next-line: import-patterns import { commandsExtensionPoint } from 'vs/workbench/api/common/menusExtensionPoint'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { URI } from 'vs/base/common/uri'; +import { IFileService, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; +import { dirname, isEqual } from 'vs/base/common/resources'; import { parse } from 'vs/base/common/json'; import * as objects from 'vs/base/common/objects'; import { IKeymapService } from 'vs/workbench/services/keybinding/common/keymapInfo'; import { getDispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig'; import { isArray } from 'vs/base/common/types'; import { INavigatorWithKeyboard } from 'vs/workbench/services/keybinding/common/navigatorKeyboard'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; interface ContributedKeyBinding { command: string; @@ -156,8 +158,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { @IConfigurationService configurationService: IConfigurationService, @IWindowService private readonly windowService: IWindowService, @IExtensionService extensionService: IExtensionService, - @IKeymapService private readonly keymapService: IKeymapService, - @IUserDataService userDataService: IUserDataService, + @IFileService fileService: IFileService, + @IKeymapService private readonly keymapService: IKeymapService ) { super(contextKeyService, commandService, telemetryService, notificationService); @@ -183,7 +185,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { this._cachedResolver = null; - this.userKeybindings = this._register(new UserKeybindings(userDataService)); + this.userKeybindings = this._register(new UserKeybindings(environmentService.keybindingsResource, fileService)); this.userKeybindings.initialize().then(() => { if (this.userKeybindings.keybindings.length) { this.updateResolver({ source: KeybindingSource.User }); @@ -550,40 +552,100 @@ class UserKeybindings extends Disposable { private _keybindings: IUserFriendlyKeybinding[] = []; get keybindings(): IUserFriendlyKeybinding[] { return this._keybindings; } - private readonly reloadConfigurationScheduler: RunOnceScheduler; - protected readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; + private fileWatcherDisposable: IDisposable = Disposable.None; + private directoryWatcherDisposable: IDisposable = Disposable.None; + constructor( - private readonly userDataService: IUserDataService + private readonly keybindingsResource: URI, + private readonly fileService: IFileService ) { super(); - this._register(Event.filter(this.userDataService.onDidChange, e => e.contains(USER_KEYBINDINGS_KEY))(() => this.reloadConfigurationScheduler.schedule())); + this._register(fileService.onFileChanges(e => this.handleFileEvents(e))); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(changed => { if (changed) { this._onDidChange.fire(); } }), 50)); + this._register(toDisposable(() => { + this.stopWatchingResource(); + this.stopWatchingDirectory(); + })); + } + + private watchResource(): void { + this.fileWatcherDisposable = this.fileService.watch(this.keybindingsResource); + } + + private stopWatchingResource(): void { + this.fileWatcherDisposable.dispose(); + this.fileWatcherDisposable = Disposable.None; + } + + private watchDirectory(): void { + const directory = dirname(this.keybindingsResource); + this.directoryWatcherDisposable = this.fileService.watch(directory); + } + + private stopWatchingDirectory(): void { + this.directoryWatcherDisposable.dispose(); + this.directoryWatcherDisposable = Disposable.None; } async initialize(): Promise { + const exists = await this.fileService.exists(this.keybindingsResource); + this.onResourceExists(exists); await this.reload(); } private async reload(): Promise { const existing = this._keybindings; try { - const content = (await this.userDataService.read(USER_KEYBINDINGS_KEY)) || '[]'; - const value = parse(content); + const content = await this.fileService.readFile(this.keybindingsResource); + const value = parse(content.value.toString()); this._keybindings = isArray(value) ? value : []; } catch (e) { this._keybindings = []; } return existing ? !objects.equals(existing, this._keybindings) : true; } + + private async handleFileEvents(event: FileChangesEvent): Promise { + const events = event.changes; + + let affectedByChanges = false; + + // Find changes that affect the resource + for (const event of events) { + affectedByChanges = isEqual(this.keybindingsResource, event.resource); + if (affectedByChanges) { + if (event.type === FileChangeType.ADDED) { + this.onResourceExists(true); + } else if (event.type === FileChangeType.DELETED) { + this.onResourceExists(false); + } + break; + } + } + + if (affectedByChanges) { + this.reloadConfigurationScheduler.schedule(); + } + } + + private onResourceExists(exists: boolean): void { + if (exists) { + this.stopWatchingDirectory(); + this.watchResource(); + } else { + this.stopWatchingResource(); + this.watchDirectory(); + } + } } let schemaId = 'vscode://schemas/keybindings'; diff --git a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts index f3cbd16af4b..49b53c2d234 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingEditing.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingEditing.ts @@ -8,24 +8,23 @@ import { Queue } from 'vs/base/common/async'; import * as json from 'vs/base/common/json'; import { setProperty } from 'vs/base/common/jsonEdit'; import { Edit } from 'vs/base/common/jsonFormatter'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IReference } from 'vs/base/common/lifecycle'; import { isArray } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; +import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; import { ServiceIdentifier, createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IUserFriendlyKeybinding, USER_KEYBINDINGS_KEY } from 'vs/platform/keybinding/common/keybinding'; +import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { Emitter } from 'vs/base/common/event'; -import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { Schemas } from 'vs/base/common/network'; -import { URI } from 'vs/base/common/uri'; export const IKeybindingEditingService = createDecorator('keybindingEditingService'); @@ -33,8 +32,6 @@ export interface IKeybindingEditingService { _serviceBrand: ServiceIdentifier; - userKeybindingsResource: URI; - editKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise; removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise; @@ -45,17 +42,18 @@ export interface IKeybindingEditingService { export class KeybindingsEditingService extends Disposable implements IKeybindingEditingService { public _serviceBrand: any; - - readonly userKeybindingsResource: URI; private queue: Queue; + private resource: URI = this.environmentService.keybindingsResource; + constructor( - @IUserDataService private readonly userDataService: IUserDataService, - @IModeService private readonly modeService: IModeService, - @IModelService private readonly modelService: IModelService + @ITextModelService private readonly textModelResolverService: ITextModelService, + @ITextFileService private readonly textFileService: ITextFileService, + @IFileService private readonly fileService: IFileService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvironmentService private readonly environmentService: IEnvironmentService ) { super(); - this.userKeybindingsResource = userDataService.toResource(USER_KEYBINDINGS_KEY); this.queue = new Queue(); } @@ -73,44 +71,45 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding private doEditKeybinding(keybindingItem: ResolvedKeybindingItem, key: string, when: string | undefined): Promise { return this.resolveAndValidate() - .then(model => { + .then(reference => { + const model = reference.object.textEditorModel; const userKeybindingEntries = json.parse(model.getValue()); const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries); this.updateKeybinding(keybindingItem, key, when, model, userKeybindingEntryIndex); if (keybindingItem.isDefault && keybindingItem.resolvedKeybinding) { this.removeDefaultKeybinding(keybindingItem, model); } - return this.save(model); + return this.save().then(() => reference.dispose()); }); } private doRemoveKeybinding(keybindingItem: ResolvedKeybindingItem): Promise { return this.resolveAndValidate() - .then(model => { + .then(reference => { + const model = reference.object.textEditorModel; if (keybindingItem.isDefault) { this.removeDefaultKeybinding(keybindingItem, model); } else { this.removeUserKeybinding(keybindingItem, model); } - return this.save(model); + return this.save().then(() => reference.dispose()); }); } private doResetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise { return this.resolveAndValidate() - .then(model => { + .then(reference => { + const model = reference.object.textEditorModel; if (!keybindingItem.isDefault) { this.removeUserKeybinding(keybindingItem, model); this.removeUnassignedDefaultKeybinding(keybindingItem, model); } - return this.save(model); + return this.save().then(() => reference.dispose()); }); } - private async save(model: ITextModel): Promise { - await this.userDataService.write(USER_KEYBINDINGS_KEY, model.getValue()); - model.dispose(); - this.modelService.destroyModel(model.uri); + private save(): Promise { + return this.textFileService.save(this.resource); } private updateKeybinding(keybindingItem: ResolvedKeybindingItem, newKey: string, when: string | undefined, model: ITextModel, userKeybindingEntryIndex: number): void { @@ -208,33 +207,45 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding } - private async resolveModel(): Promise { - const content = (await this.userDataService.read(USER_KEYBINDINGS_KEY)) || '[]'; - const languageIdentifier = this.modeService.getLanguageIdentifier('jsonc'); - return this.modelService.createModel(content, languageIdentifier ? { languageIdentifier, onDidChange: new Emitter().event, dispose: () => { } } : null, this.userKeybindingsResource.with({ scheme: Schemas.vscode })); + private resolveModelReference(): Promise> { + return this.fileService.exists(this.resource) + .then(exists => { + const EOL = this.configurationService.getValue<{}>('files', { overrideIdentifier: 'json' })['eol']; + const result: Promise = exists ? Promise.resolve(null) : this.textFileService.write(this.resource, this.getEmptyContent(EOL), { encoding: 'utf8' }); + return result.then(() => this.textModelResolverService.createModelReference(this.resource)); + }); } - private async resolveAndValidate(): Promise { - const model = await this.resolveModel(); - const EOL = model.getEOL(); - if (model.getValue()) { - const parsed = this.parse(model); - if (parsed.parseErrors.length) { - return Promise.reject(new Error(localize('parseErrors', "Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again."))); - } - if (parsed.result) { - if (!isArray(parsed.result)) { - return Promise.reject(new Error(localize('errorInvalidConfiguration', "Unable to write to the keybindings configuration file. It has an object which is not of type Array. Please open the file to clean up and try again."))); - } - } else { - const content = EOL + '[]'; - this.applyEditsToBuffer({ content, length: content.length, offset: model.getValue().length }, model); - } - } else { - const content = this.getEmptyContent(EOL); - this.applyEditsToBuffer({ content, length: content.length, offset: 0 }, model); + private resolveAndValidate(): Promise> { + + // Target cannot be dirty if not writing into buffer + if (this.textFileService.isDirty(this.resource)) { + return Promise.reject(new Error(localize('errorKeybindingsFileDirty', "Unable to write because the keybindings configuration file is dirty. Please save it first and then try again."))); } - return model; + + return this.resolveModelReference() + .then(reference => { + const model = reference.object.textEditorModel; + const EOL = model.getEOL(); + if (model.getValue()) { + const parsed = this.parse(model); + if (parsed.parseErrors.length) { + return Promise.reject(new Error(localize('parseErrors', "Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again."))); + } + if (parsed.result) { + if (!isArray(parsed.result)) { + return Promise.reject(new Error(localize('errorInvalidConfiguration', "Unable to write to the keybindings configuration file. It has an object which is not of type Array. Please open the file to clean up and try again."))); + } + } else { + const content = EOL + '[]'; + this.applyEditsToBuffer({ content, length: content.length, offset: model.getValue().length }, model); + } + } else { + const content = this.getEmptyContent(EOL); + this.applyEditsToBuffer({ content, length: content.length, offset: 0 }, model); + } + return reference; + }); } private parse(model: ITextModel): { result: IUserFriendlyKeybinding[], parseErrors: json.ParseError[] } { diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 0466789663a..d3fecf7080d 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -21,6 +21,7 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/resour import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; @@ -44,8 +45,12 @@ import { FileService } from 'vs/workbench/services/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider'; import { URI } from 'vs/base/common/uri'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; -import { FileUserDataService } from 'vs/workbench/services/userData/common/fileUserDataService'; +import { UserDataFileSystemProvider } from 'vs/workbench/services/userData/common/userDataFileSystemProvider'; +import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; +import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService'; +import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { parseArgs } from 'vs/platform/environment/node/argv'; +import { dirname } from 'vs/base/common/resources'; interface Modifiers { metaKey?: boolean; @@ -54,6 +59,14 @@ interface Modifiers { shiftKey?: boolean; } +class SettingsTestEnvironmentService extends WorkbenchEnvironmentService { + constructor(args: ParsedArgs, _execPath: string, private _appSettingsHome: URI) { + super(args, _execPath); + } + + get appSettingsHome(): URI { return this._appSettingsHome; } +} + suite('KeybindingsEditing', () => { let instantiationService: TestInstantiationService; @@ -67,6 +80,8 @@ suite('KeybindingsEditing', () => { instantiationService = new TestInstantiationService(); + const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, URI.file(testDir)); + instantiationService.stub(IEnvironmentService, environmentService); instantiationService.stub(IConfigurationService, ConfigurationService); instantiationService.stub(IConfigurationService, 'getValue', { 'eol': '\n' }); instantiationService.stub(IConfigurationService, 'onDidUpdateConfiguration', () => { }); @@ -84,8 +99,8 @@ suite('KeybindingsEditing', () => { instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); const fileService = new FileService(new NullLogService()); fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService())); + fileService.registerProvider(Schemas.userData, new UserDataFileSystemProvider(dirname(environmentService.keybindingsResource), new FileUserDataProvider(environmentService.appSettingsHome, fileService))); instantiationService.stub(IFileService, fileService); - instantiationService.stub(IUserDataService, new FileUserDataService(URI.file(testDir), fileService)); instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService)); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); @@ -124,6 +139,13 @@ suite('KeybindingsEditing', () => { error => assert.equal(error.message, 'Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again.')); }); + test('errors cases - dirty', () => { + instantiationService.stub(ITextFileService, 'isDirty', true); + return testObject.editKeybinding(aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }), 'alt+c', undefined) + .then(() => assert.fail('Should fail with dirty error'), + error => assert.equal(error.message, 'Unable to write because the keybindings configuration file is dirty. Please save it first and then try again.')); + }); + test('errors cases - did not find an array', () => { fs.writeFileSync(keybindingsFile, '{"key": "alt+c", "command": "hello"}'); return testObject.editKeybinding(aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }), 'alt+c', undefined) diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index ce7695b8f78..ae0e5cf50f4 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -20,6 +20,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -37,7 +38,6 @@ import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultSetti import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IKeybindingEditingService } from '../../keybinding/common/keybindingEditing'; const emptyEditableSettingsContent = '{\n}'; @@ -64,7 +64,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic @INotificationService private readonly notificationService: INotificationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingEditingService private readonly keybindingEditingService: IKeybindingEditingService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ITextModelService private readonly textModelResolverService: ITextModelService, @IKeybindingService keybindingService: IKeybindingService, @@ -273,7 +273,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic this.telemetryService.publicLog('openKeybindings', { textual }); if (textual) { const emptyContents = '// ' + nls.localize('emptyKeybindingsHeader', "Place your key bindings in this file to override the defaults") + '\n[\n]'; - const editableKeybindings = this.keybindingEditingService.userKeybindingsResource; + const editableKeybindings = this.environmentService.keybindingsResource; const openDefaultKeybindings = !!this.configurationService.getValue('workbench.settings.openDefaultKeybindings'); // Create as needed and open in editor @@ -524,9 +524,9 @@ export class PreferencesService extends Disposable implements IPreferencesServic switch (configurationTarget) { case ConfigurationTarget.USER: case ConfigurationTarget.USER_LOCAL: - return this.configurationService.userSettingsResource; + return this.environmentService.settingsResource; case ConfigurationTarget.USER_REMOTE: - return this.configurationService.userSettingsResource; + return this.environmentService.settingsResource; case ConfigurationTarget.WORKSPACE: if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { return null; @@ -629,4 +629,4 @@ export class PreferencesService extends Disposable implements IPreferencesServic } } -registerSingleton(IPreferencesService, PreferencesService); \ No newline at end of file +registerSingleton(IPreferencesService, PreferencesService); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 60c009b6b56..9a70fd94b6a 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -30,8 +30,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { isEqual, isEqualOrParent, extname, basename, joinPath } from 'vs/base/common/resources'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Schemas } from 'vs/base/common/network'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; export interface IBackupMetaData { mtime: number; @@ -106,9 +104,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @IBackupFileService private readonly backupFileService: IBackupFileService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @ILogService private readonly logService: ILogService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IKeybindingEditingService private readonly keybindingEditingService: IKeybindingEditingService + @ILogService private readonly logService: ILogService ) { super(modelService, modeService); @@ -779,12 +775,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } // Check for global settings file - if (isEqual(this.resource, this.configurationService.userSettingsResource, !isLinux)) { + if (isEqual(this.resource, this.environmentService.settingsResource, !isLinux)) { return 'global-settings'; } // Check for keybindings file - if (isEqual(this.resource, this.keybindingEditingService.userKeybindingsResource, !isLinux)) { + if (isEqual(this.resource, this.environmentService.keybindingsResource, !isLinux)) { return 'keybindings'; } diff --git a/src/vs/workbench/services/userData/common/customUserDataService.ts b/src/vs/workbench/services/userData/common/customUserDataService.ts deleted file mode 100644 index 491d2a9a319..00000000000 --- a/src/vs/workbench/services/userData/common/customUserDataService.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IUserDataService, IUserDataChangesEvent, IUserDataProvider, UserDataChangesEvent } from 'vs/workbench/services/userData/common/userData'; -import { URI } from 'vs/base/common/uri'; -import { Schemas } from 'vs/base/common/network'; -import { joinPath, relativePath } from 'vs/base/common/resources'; - -export class CustomUserDataService extends Disposable implements IUserDataService { - _serviceBrand: any; - - private readonly userDataHome: URI; - - private _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; - - constructor( - private readonly userDataProvider: IUserDataProvider - ) { - super(); - this.userDataHome = URI.file('/User').with({ scheme: Schemas.userData }); - this._register(this.userDataProvider.onDidChange(key => this._onDidChange.fire(new UserDataChangesEvent([key])))); - } - - read(key: string): Promise { - return this.userDataProvider.read(key); - } - - write(key: string, value: string): Promise { - return this.userDataProvider.write(key, value); - } - - toResource(key: string): URI { - return joinPath(this.userDataHome, key); - } - - toKey(resource: URI): string | undefined { - return relativePath(this.userDataHome, resource); - } - -} \ No newline at end of file diff --git a/src/vs/workbench/services/userData/common/fileUserDataService.ts b/src/vs/workbench/services/userData/common/fileUserDataProvider.ts similarity index 54% rename from src/vs/workbench/services/userData/common/fileUserDataService.ts rename to src/vs/workbench/services/userData/common/fileUserDataProvider.ts index ae44e1e5989..61f01d2f1eb 100644 --- a/src/vs/workbench/services/userData/common/fileUserDataService.ts +++ b/src/vs/workbench/services/userData/common/fileUserDataProvider.ts @@ -5,18 +5,16 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IUserDataService, IUserDataChangesEvent, UserDataChangesEvent } from 'vs/workbench/services/userData/common/userData'; +import { IUserDataProvider } from 'vs/workbench/services/userData/common/userData'; import { IFileService, FileChangesEvent } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; -import { Schemas } from 'vs/base/common/network'; -export class FileUserDataService extends Disposable implements IUserDataService { - _serviceBrand: any; +export class FileUserDataProvider extends Disposable implements IUserDataProvider { - private _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; + private _onDidChangeFile: Emitter = this._register(new Emitter()); + readonly onDidChangeFile: Event = this._onDidChangeFile.event; constructor( private readonly userDataHome: URI, @@ -32,46 +30,50 @@ export class FileUserDataService extends Disposable implements IUserDataService private handleFileChanges(event: FileChangesEvent): void { const changedKeys: string[] = []; for (const change of event.changes) { - if (change.resource.scheme !== Schemas.userData) { - const key = this.toKey(change.resource.with({ scheme: Schemas.userData })); + if (change.resource.scheme !== this.userDataHome.scheme) { + const key = this.toKey(change.resource); if (key) { changedKeys.push(key); } } } if (changedKeys.length) { - this._onDidChange.fire(new UserDataChangesEvent(changedKeys)); + this._onDidChangeFile.fire(changedKeys); } } - async read(key: string): Promise { - const resource = this.toFileResource(key); + async readFile(path: string): Promise { + const resource = this.toResource(path); try { const content = await this.fileService.readFile(resource); - return content.value.toString(); + return content.value; } catch (e) { const exists = await this.fileService.exists(resource); if (exists) { throw e; } } - return ''; + return VSBuffer.fromString(''); } - write(key: string, value: string): Promise { - return this.fileService.writeFile(this.toFileResource(key), VSBuffer.fromString(value)).then(() => undefined); + writeFile(path: string, value: VSBuffer): Promise { + return this.fileService.writeFile(this.toResource(path), value).then(() => undefined); } - private toFileResource(key: string): URI { + async readDirectory(path: string): Promise { + const result = await this.fileService.resolve(this.toResource(path)); + return result.children ? result.children.map(c => this.toKey(c.resource)!) : []; + } + + delete(path: string): Promise { + return this.fileService.del(this.toResource(path)); + } + + private toResource(key: string): URI { return resources.joinPath(this.userDataHome, ...key.split('/')); } - toResource(key: string): URI { - return this.toFileResource(key).with({ scheme: Schemas.userData }); + private toKey(resource: URI): string | undefined { + return resources.relativePath(this.userDataHome, resource); } - - toKey(resource: URI): string | undefined { - return resources.relativePath(this.userDataHome.with({ scheme: Schemas.userData }), resource); - } - } \ No newline at end of file diff --git a/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts b/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts index e4961722ae4..844430bbaf3 100644 --- a/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts +++ b/src/vs/workbench/services/userData/common/inMemoryUserDataProvider.ts @@ -6,12 +6,13 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IUserDataProvider } from 'vs/workbench/services/userData/common/userData'; +import { VSBuffer } from 'vs/base/common/buffer'; export class InMemoryUserDataProvider extends Disposable implements IUserDataProvider { _serviceBrand: any; - private _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; + private _onDidChangeFile: Emitter = this._register(new Emitter()); + readonly onDidChangeFile: Event = this._onDidChangeFile.event; private readonly store: Map = new Map(); @@ -20,18 +21,30 @@ export class InMemoryUserDataProvider extends Disposable implements IUserDataPro this._register(toDisposable(() => this.store.clear())); } - async read(key: string): Promise { - return this.getValue(key); + async readDirectory(path: string): Promise { + return []; } - async write(key: string, value: string): Promise { - if (value !== this.getValue(key)) { - if (value) { - this.store.set(key, value); + async readFile(path: string): Promise { + return VSBuffer.fromString(this.getValue(path)); + } + + async writeFile(path: string, value: VSBuffer): Promise { + const content = value.toString(); + if (content !== this.getValue(path)) { + if (content) { + this.store.set(path, content); + this._onDidChangeFile.fire([path]); } else { - this.store.delete(key); + this.delete(path); } - this._onDidChange.fire(key); + } + } + + async delete(path: string): Promise { + if (this.store.has(path)) { + this.store.delete(path); + this._onDidChangeFile.fire([path]); } } diff --git a/src/vs/workbench/services/userData/common/userData.ts b/src/vs/workbench/services/userData/common/userData.ts index aba73a83f61..f117d58b13d 100644 --- a/src/vs/workbench/services/userData/common/userData.ts +++ b/src/vs/workbench/services/userData/common/userData.ts @@ -3,60 +3,18 @@ * Licensed under the MIT License. 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 { TernarySearchTree } from 'vs/base/common/map'; - -export const IUserDataService = createDecorator('userDataService'); - -export interface IUserDataChangesEvent { - keys: string[]; - contains(keyOrSegment: string): boolean; -} - -export interface IUserDataService { - _serviceBrand: any; - - onDidChange: Event; - - toResource(key: string): URI; - - toKey(resource: URI): string | undefined; - - read(key: string): Promise; - - write(key: string, value: string): Promise; -} +import { VSBuffer } from 'vs/base/common/buffer'; export interface IUserDataProvider { - onDidChange: Event; + onDidChangeFile: Event; - read(key: string): Promise; + readFile(path: string): Promise; - write(key: string, value: string): Promise; + readDirectory(path: string): Promise; -} - -export class UserDataChangesEvent implements IUserDataChangesEvent { - - private _keysTree: TernarySearchTree | undefined = undefined; - - constructor(readonly keys: string[]) { } - - private get keysTree(): TernarySearchTree { - if (!this._keysTree) { - this._keysTree = TernarySearchTree.forPaths(); - for (const key of this.keys) { - this._keysTree.set(key, key); - } - } - return this._keysTree; - } - - contains(keyOrSegment: string): boolean { - return this.keysTree.findSubstr(keyOrSegment) !== undefined; - } + writeFile(path: string, content: VSBuffer): Promise; + delete(path: string): Promise; } \ No newline at end of file diff --git a/src/vs/workbench/services/userData/common/userDataFileSystemProvider.ts b/src/vs/workbench/services/userData/common/userDataFileSystemProvider.ts index 8b39afee00f..88b54acb2cc 100644 --- a/src/vs/workbench/services/userData/common/userDataFileSystemProvider.ts +++ b/src/vs/workbench/services/userData/common/userDataFileSystemProvider.ts @@ -5,14 +5,16 @@ import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { FileSystemProviderCapabilities, FileWriteOptions, IStat, FileType, FileDeleteOptions, IWatchOptions, FileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileChange, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; -import { IUserDataService } from 'vs/workbench/services/userData/common/userData'; +import { IUserDataProvider } from 'vs/workbench/services/userData/common/userData'; import { URI } from 'vs/base/common/uri'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event, Emitter } from 'vs/base/common/event'; +import * as resources from 'vs/base/common/resources'; +import { TernarySearchTree } from 'vs/base/common/map'; export class UserDataFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { - constructor(private readonly userDataService: IUserDataService) { super(); } + private readonly versions: Map = new Map(); readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.FileReadWrite; readonly onDidChangeCapabilities: Event = Event.None; @@ -20,53 +22,93 @@ export class UserDataFileSystemProvider extends Disposable implements IFileSyste private readonly _onDidChangeFile: Emitter = this._register(new Emitter()); readonly onDidChangeFile: Event = this._onDidChangeFile.event; - private versions: Map = new Map(); + + constructor( + private readonly userDataHome: URI, + private readonly userDataProvider: IUserDataProvider + ) { + super(); + } watch(resource: URI, opts: IWatchOptions): IDisposable { - const key = this.userDataService.toKey(resource); - if (!key) { + const path = this.toPath(resource); + if (!path) { throw new Error(`Invalud user data resource ${resource}`); } - return this.userDataService.onDidChange(e => { - if (e.contains(key)) { - this.versions.set(key, (this.versions.get(key) || 1) + 1); + return this.userDataProvider.onDidChangeFile(e => { + if (new UserDataChangesEvent(e).contains(path)) { + this.versions.set(path, (this.versions.get(path) || 1) + 1); this._onDidChangeFile.fire(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }]).changes); } }); } async stat(resource: URI): Promise { - const key = this.userDataService.toKey(resource); - if (!key) { + const path = this.toPath(resource); + if (!path) { throw new Error(`Invalud user data resource ${resource}`); } return { type: FileType.File, ctime: 0, - mtime: this.versions.get(key) || 0, + mtime: this.versions.get(path) || 0, size: 0 }; } - mkdir(resource: URI): Promise { throw new Error('not supported'); } - readdir(resource: URI): Promise<[string, FileType][]> { throw new Error('not supported'); } - delete(resource: URI, opts: FileDeleteOptions): Promise { throw new Error('not supported'); } + mkdir(resource: URI): Promise { throw new Error('not supported'); } + delete(resource: URI, opts: FileDeleteOptions): Promise { throw new Error('not supported'); } rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { throw new Error('not supported'); } async readFile(resource: URI): Promise { - const key = this.userDataService.toKey(resource); - if (!key) { + const path = this.toPath(resource); + if (!path) { throw new Error(`Invalud user data resource ${resource}`); } - const content = await this.userDataService.read(key); - return VSBuffer.fromString(content).buffer; + const content = await this.userDataProvider.readFile(path); + return content.buffer; + } + + async readdir(resource: URI): Promise<[string, FileType][]> { + const path = this.toPath(resource); + if (!path) { + throw new Error(`Invalud user data resource ${resource}`); + } + const children = await this.userDataProvider.readDirectory(path); + return children.map(c => [c, FileType.Unknown]); } writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { - const key = this.userDataService.toKey(resource); - if (!key) { + const path = this.toPath(resource); + if (!path) { throw new Error(`Invalud user data resource ${resource}`); } - return this.userDataService.write(key, VSBuffer.wrap(content).toString()); + return this.userDataProvider.writeFile(path, VSBuffer.wrap(content)); } + + private toPath(resource: URI): string | undefined { + return resources.relativePath(this.userDataHome, resource); + } +} + +class UserDataChangesEvent { + + private _pathsTree: TernarySearchTree | undefined = undefined; + + constructor(readonly paths: string[]) { } + + private get pathsTree(): TernarySearchTree { + if (!this._pathsTree) { + this._pathsTree = TernarySearchTree.forPaths(); + for (const path of this.paths) { + this._pathsTree.set(path, path); + } + } + return this._pathsTree; + } + + contains(keyOrSegment: string): boolean { + return this.pathsTree.findSubstr(keyOrSegment) !== undefined; + } + } \ No newline at end of file