Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,759 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { ConfigurationModel, ConfigurationModelParser, UserSettings } from 'vs/platform/configuration/common/configurationModels';
|
||||
import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels';
|
||||
import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } 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';
|
||||
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { equals } from 'vs/base/common/objects';
|
||||
import { IConfigurationModel } from 'vs/platform/configuration/common/configuration';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
|
||||
export class UserConfiguration extends Disposable {
|
||||
|
||||
private readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
|
||||
readonly onDidChangeConfiguration: Event<ConfigurationModel> = this._onDidChangeConfiguration.event;
|
||||
|
||||
private readonly userConfiguration: MutableDisposable<UserSettings | FileServiceBasedConfiguration> = this._register(new MutableDisposable<UserSettings | FileServiceBasedConfiguration>());
|
||||
private readonly reloadConfigurationScheduler: RunOnceScheduler;
|
||||
|
||||
get hasTasksLoaded(): boolean { return this.userConfiguration.value instanceof FileServiceBasedConfiguration; }
|
||||
|
||||
constructor(
|
||||
private readonly userSettingsResource: URI,
|
||||
private readonly scopes: ConfigurationScope[] | undefined,
|
||||
private readonly fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
this.userConfiguration.value = new UserSettings(this.userSettingsResource, this.scopes, this.fileService);
|
||||
this._register(this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule()));
|
||||
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50));
|
||||
}
|
||||
|
||||
async initialize(): Promise<ConfigurationModel> {
|
||||
return this.userConfiguration.value!.loadConfiguration();
|
||||
}
|
||||
|
||||
async reload(): Promise<ConfigurationModel> {
|
||||
if (this.hasTasksLoaded) {
|
||||
return this.userConfiguration.value!.loadConfiguration();
|
||||
}
|
||||
|
||||
const folder = resources.dirname(this.userSettingsResource);
|
||||
const standAloneConfigurationResources: [string, URI][] = [TASKS_CONFIGURATION_KEY].map(name => ([name, resources.joinPath(folder, `${name}.json`)]));
|
||||
const fileServiceBasedConfiguration = new FileServiceBasedConfiguration(folder.toString(), [this.userSettingsResource], standAloneConfigurationResources, this.scopes, this.fileService);
|
||||
const configurationModel = await fileServiceBasedConfiguration.loadConfiguration();
|
||||
this.userConfiguration.value = fileServiceBasedConfiguration;
|
||||
|
||||
// Check for value because userConfiguration might have been disposed.
|
||||
if (this.userConfiguration.value) {
|
||||
this._register(this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule()));
|
||||
}
|
||||
|
||||
return configurationModel;
|
||||
}
|
||||
|
||||
reprocess(): ConfigurationModel {
|
||||
return this.userConfiguration.value!.reprocess();
|
||||
}
|
||||
}
|
||||
|
||||
class FileServiceBasedConfiguration extends Disposable {
|
||||
|
||||
private readonly allResources: URI[];
|
||||
private _folderSettingsModelParser: ConfigurationModelParser;
|
||||
private _standAloneConfigurations: ConfigurationModel[];
|
||||
private _cache: ConfigurationModel;
|
||||
|
||||
private readonly changeEventTriggerScheduler: RunOnceScheduler;
|
||||
private readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
private readonly settingsResources: URI[],
|
||||
private readonly standAloneConfigurationResources: [string, URI][],
|
||||
private readonly scopes: ConfigurationScope[] | undefined,
|
||||
private fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
this.allResources = [...this.settingsResources, ...this.standAloneConfigurationResources.map(([, resource]) => resource)];
|
||||
this._folderSettingsModelParser = new ConfigurationModelParser(name, this.scopes);
|
||||
this._standAloneConfigurations = [];
|
||||
this._cache = new ConfigurationModel();
|
||||
|
||||
this.changeEventTriggerScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
|
||||
this._register(this.fileService.onDidFilesChange((e) => this.handleFileEvents(e)));
|
||||
}
|
||||
|
||||
async loadConfiguration(): Promise<ConfigurationModel> {
|
||||
const resolveContents = async (resources: URI[]): Promise<(string | undefined)[]> => {
|
||||
return Promise.all(resources.map(async resource => {
|
||||
try {
|
||||
const content = await this.fileService.readFile(resource);
|
||||
return content.value.toString();
|
||||
} catch (error) {
|
||||
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND
|
||||
&& (<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_DIRECTORY) {
|
||||
errors.onUnexpectedError(error);
|
||||
}
|
||||
}
|
||||
return '{}';
|
||||
}));
|
||||
};
|
||||
|
||||
const [settingsContents, standAloneConfigurationContents] = await Promise.all([
|
||||
resolveContents(this.settingsResources),
|
||||
resolveContents(this.standAloneConfigurationResources.map(([, resource]) => resource)),
|
||||
]);
|
||||
|
||||
// reset
|
||||
this._standAloneConfigurations = [];
|
||||
this._folderSettingsModelParser.parseContent('');
|
||||
|
||||
// parse
|
||||
if (settingsContents[0] !== undefined) {
|
||||
this._folderSettingsModelParser.parseContent(settingsContents[0]);
|
||||
}
|
||||
for (let index = 0; index < standAloneConfigurationContents.length; index++) {
|
||||
const contents = standAloneConfigurationContents[index];
|
||||
if (contents !== undefined) {
|
||||
const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(this.standAloneConfigurationResources[index][1].toString(), this.standAloneConfigurationResources[index][0]);
|
||||
standAloneConfigurationModelParser.parseContent(contents);
|
||||
this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel);
|
||||
}
|
||||
}
|
||||
|
||||
// Consolidate (support *.json files in the workspace settings folder)
|
||||
this.consolidate();
|
||||
|
||||
return this._cache;
|
||||
}
|
||||
|
||||
reprocess(): ConfigurationModel {
|
||||
const oldContents = this._folderSettingsModelParser.configurationModel.contents;
|
||||
this._folderSettingsModelParser.parse();
|
||||
if (!equals(oldContents, this._folderSettingsModelParser.configurationModel.contents)) {
|
||||
this.consolidate();
|
||||
}
|
||||
return this._cache;
|
||||
}
|
||||
|
||||
private consolidate(): void {
|
||||
this._cache = this._folderSettingsModelParser.configurationModel.merge(...this._standAloneConfigurations);
|
||||
}
|
||||
|
||||
protected async handleFileEvents(event: FileChangesEvent): Promise<void> {
|
||||
const isAffectedByChanges = (): boolean => {
|
||||
// One of the resources has changed
|
||||
if (this.allResources.some(resource => event.contains(resource))) {
|
||||
return true;
|
||||
}
|
||||
// One of the resource's parent got deleted
|
||||
if (this.allResources.some(resource => event.contains(resources.dirname(resource), FileChangeType.DELETED))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (isAffectedByChanges()) {
|
||||
this.changeEventTriggerScheduler.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class RemoteUserConfiguration extends Disposable {
|
||||
|
||||
private readonly _cachedConfiguration: CachedRemoteUserConfiguration;
|
||||
private readonly _fileService: IFileService;
|
||||
private _userConfiguration: FileServiceBasedRemoteUserConfiguration | CachedRemoteUserConfiguration;
|
||||
private _userConfigurationInitializationPromise: Promise<ConfigurationModel> | null = null;
|
||||
|
||||
private readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
|
||||
public readonly onDidChangeConfiguration: Event<ConfigurationModel> = this._onDidChangeConfiguration.event;
|
||||
|
||||
constructor(
|
||||
remoteAuthority: string,
|
||||
configurationCache: IConfigurationCache,
|
||||
fileService: IFileService,
|
||||
remoteAgentService: IRemoteAgentService
|
||||
) {
|
||||
super();
|
||||
this._fileService = fileService;
|
||||
this._userConfiguration = this._cachedConfiguration = new CachedRemoteUserConfiguration(remoteAuthority, configurationCache);
|
||||
remoteAgentService.getEnvironment().then(async environment => {
|
||||
if (environment) {
|
||||
const userConfiguration = this._register(new FileServiceBasedRemoteUserConfiguration(environment.settingsPath, REMOTE_MACHINE_SCOPES, this._fileService));
|
||||
this._register(userConfiguration.onDidChangeConfiguration(configurationModel => this.onDidUserConfigurationChange(configurationModel)));
|
||||
this._userConfigurationInitializationPromise = userConfiguration.initialize();
|
||||
const configurationModel = await this._userConfigurationInitializationPromise;
|
||||
this._userConfiguration.dispose();
|
||||
this._userConfiguration = userConfiguration;
|
||||
this.onDidUserConfigurationChange(configurationModel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async initialize(): Promise<ConfigurationModel> {
|
||||
if (this._userConfiguration instanceof FileServiceBasedRemoteUserConfiguration) {
|
||||
return this._userConfiguration.initialize();
|
||||
}
|
||||
|
||||
// Initialize cached configuration
|
||||
let configurationModel = await this._userConfiguration.initialize();
|
||||
if (this._userConfigurationInitializationPromise) {
|
||||
// Use user configuration
|
||||
configurationModel = await this._userConfigurationInitializationPromise;
|
||||
this._userConfigurationInitializationPromise = null;
|
||||
}
|
||||
|
||||
return configurationModel;
|
||||
}
|
||||
|
||||
reload(): Promise<ConfigurationModel> {
|
||||
return this._userConfiguration.reload();
|
||||
}
|
||||
|
||||
reprocess(): ConfigurationModel {
|
||||
return this._userConfiguration.reprocess();
|
||||
}
|
||||
|
||||
private onDidUserConfigurationChange(configurationModel: ConfigurationModel): void {
|
||||
this.updateCache(configurationModel);
|
||||
this._onDidChangeConfiguration.fire(configurationModel);
|
||||
}
|
||||
|
||||
private updateCache(configurationModel: ConfigurationModel): Promise<void> {
|
||||
return this._cachedConfiguration.updateConfiguration(configurationModel);
|
||||
}
|
||||
}
|
||||
|
||||
class FileServiceBasedRemoteUserConfiguration extends Disposable {
|
||||
|
||||
private readonly parser: ConfigurationModelParser;
|
||||
private readonly reloadConfigurationScheduler: RunOnceScheduler;
|
||||
protected readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
|
||||
readonly onDidChangeConfiguration: Event<ConfigurationModel> = this._onDidChangeConfiguration.event;
|
||||
|
||||
private fileWatcherDisposable: IDisposable = Disposable.None;
|
||||
private directoryWatcherDisposable: IDisposable = Disposable.None;
|
||||
|
||||
constructor(
|
||||
private readonly configurationResource: URI,
|
||||
private readonly scopes: ConfigurationScope[] | undefined,
|
||||
private readonly fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.parser = new ConfigurationModelParser(this.configurationResource.toString(), this.scopes);
|
||||
this._register(fileService.onDidFilesChange(e => this.handleFileEvents(e)));
|
||||
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50));
|
||||
this._register(toDisposable(() => {
|
||||
this.stopWatchingResource();
|
||||
this.stopWatchingDirectory();
|
||||
}));
|
||||
}
|
||||
|
||||
private watchResource(): void {
|
||||
this.fileWatcherDisposable = this.fileService.watch(this.configurationResource);
|
||||
}
|
||||
|
||||
private stopWatchingResource(): void {
|
||||
this.fileWatcherDisposable.dispose();
|
||||
this.fileWatcherDisposable = Disposable.None;
|
||||
}
|
||||
|
||||
private watchDirectory(): void {
|
||||
const directory = resources.dirname(this.configurationResource);
|
||||
this.directoryWatcherDisposable = this.fileService.watch(directory);
|
||||
}
|
||||
|
||||
private stopWatchingDirectory(): void {
|
||||
this.directoryWatcherDisposable.dispose();
|
||||
this.directoryWatcherDisposable = Disposable.None;
|
||||
}
|
||||
|
||||
async initialize(): Promise<ConfigurationModel> {
|
||||
const exists = await this.fileService.exists(this.configurationResource);
|
||||
this.onResourceExists(exists);
|
||||
return this.reload();
|
||||
}
|
||||
|
||||
async reload(): Promise<ConfigurationModel> {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.configurationResource);
|
||||
this.parser.parseContent(content.value.toString());
|
||||
return this.parser.configurationModel;
|
||||
} catch (e) {
|
||||
return new ConfigurationModel();
|
||||
}
|
||||
}
|
||||
|
||||
reprocess(): ConfigurationModel {
|
||||
this.parser.parse();
|
||||
return this.parser.configurationModel;
|
||||
}
|
||||
|
||||
private async handleFileEvents(event: FileChangesEvent): Promise<void> {
|
||||
|
||||
// Find changes that affect the resource
|
||||
let affectedByChanges = event.contains(this.configurationResource, FileChangeType.UPDATED);
|
||||
if (event.contains(this.configurationResource, FileChangeType.ADDED)) {
|
||||
affectedByChanges = true;
|
||||
this.onResourceExists(true);
|
||||
} else if (event.contains(this.configurationResource, FileChangeType.DELETED)) {
|
||||
affectedByChanges = true;
|
||||
this.onResourceExists(false);
|
||||
}
|
||||
|
||||
if (affectedByChanges) {
|
||||
this.reloadConfigurationScheduler.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
private onResourceExists(exists: boolean): void {
|
||||
if (exists) {
|
||||
this.stopWatchingDirectory();
|
||||
this.watchResource();
|
||||
} else {
|
||||
this.stopWatchingResource();
|
||||
this.watchDirectory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CachedRemoteUserConfiguration extends Disposable {
|
||||
|
||||
private readonly _onDidChange: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
|
||||
readonly onDidChange: Event<ConfigurationModel> = this._onDidChange.event;
|
||||
|
||||
private readonly key: ConfigurationKey;
|
||||
private configurationModel: ConfigurationModel;
|
||||
|
||||
constructor(
|
||||
remoteAuthority: string,
|
||||
private readonly configurationCache: IConfigurationCache
|
||||
) {
|
||||
super();
|
||||
this.key = { type: 'user', key: remoteAuthority };
|
||||
this.configurationModel = new ConfigurationModel();
|
||||
}
|
||||
|
||||
getConfigurationModel(): ConfigurationModel {
|
||||
return this.configurationModel;
|
||||
}
|
||||
|
||||
initialize(): Promise<ConfigurationModel> {
|
||||
return this.reload();
|
||||
}
|
||||
|
||||
reprocess(): ConfigurationModel {
|
||||
return this.configurationModel;
|
||||
}
|
||||
|
||||
async reload(): Promise<ConfigurationModel> {
|
||||
const content = await this.configurationCache.read(this.key);
|
||||
try {
|
||||
const parsed: IConfigurationModel = JSON.parse(content);
|
||||
this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides);
|
||||
} catch (e) {
|
||||
}
|
||||
return this.configurationModel;
|
||||
}
|
||||
|
||||
updateConfiguration(configurationModel: ConfigurationModel): Promise<void> {
|
||||
if (configurationModel.keys.length) {
|
||||
return this.configurationCache.write(this.key, JSON.stringify(configurationModel.toJSON()));
|
||||
} else {
|
||||
return this.configurationCache.remove(this.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceConfiguration extends Disposable {
|
||||
|
||||
private readonly _fileService: IFileService;
|
||||
private readonly _cachedConfiguration: CachedWorkspaceConfiguration;
|
||||
private _workspaceConfiguration: IWorkspaceConfiguration;
|
||||
private _workspaceConfigurationChangeDisposable: IDisposable = Disposable.None;
|
||||
private _workspaceIdentifier: IWorkspaceIdentifier | null = null;
|
||||
|
||||
private readonly _onDidUpdateConfiguration: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidUpdateConfiguration: Event<void> = this._onDidUpdateConfiguration.event;
|
||||
|
||||
private _initialized: boolean = false;
|
||||
get initialized(): boolean { return this._initialized; }
|
||||
constructor(
|
||||
private readonly configurationCache: IConfigurationCache,
|
||||
fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
this._fileService = fileService;
|
||||
this._workspaceConfiguration = this._cachedConfiguration = new CachedWorkspaceConfiguration(configurationCache);
|
||||
}
|
||||
|
||||
async initialize(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
|
||||
this._workspaceIdentifier = workspaceIdentifier;
|
||||
if (!this._initialized) {
|
||||
if (this.configurationCache.needsCaching(this._workspaceIdentifier.configPath)) {
|
||||
this._workspaceConfiguration = this._cachedConfiguration;
|
||||
this.waitAndInitialize(this._workspaceIdentifier);
|
||||
} else {
|
||||
this.doInitialize(new FileServiceBasedWorkspaceConfiguration(this._fileService));
|
||||
}
|
||||
}
|
||||
await this.reload();
|
||||
}
|
||||
|
||||
async reload(): Promise<void> {
|
||||
if (this._workspaceIdentifier) {
|
||||
await this._workspaceConfiguration.load(this._workspaceIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
getFolders(): IStoredWorkspaceFolder[] {
|
||||
return this._workspaceConfiguration.getFolders();
|
||||
}
|
||||
|
||||
setFolders(folders: IStoredWorkspaceFolder[], jsonEditingService: JSONEditingService): Promise<void> {
|
||||
if (this._workspaceIdentifier) {
|
||||
return jsonEditingService.write(this._workspaceIdentifier.configPath, [{ path: ['folders'], value: folders }], true)
|
||||
.then(() => this.reload());
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getConfiguration(): ConfigurationModel {
|
||||
return this._workspaceConfiguration.getWorkspaceSettings();
|
||||
}
|
||||
|
||||
reprocessWorkspaceSettings(): ConfigurationModel {
|
||||
this._workspaceConfiguration.reprocessWorkspaceSettings();
|
||||
return this.getConfiguration();
|
||||
}
|
||||
|
||||
private async waitAndInitialize(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
|
||||
await whenProviderRegistered(workspaceIdentifier.configPath, this._fileService);
|
||||
if (!(this._workspaceConfiguration instanceof FileServiceBasedWorkspaceConfiguration)) {
|
||||
const fileServiceBasedWorkspaceConfiguration = this._register(new FileServiceBasedWorkspaceConfiguration(this._fileService));
|
||||
await fileServiceBasedWorkspaceConfiguration.load(workspaceIdentifier);
|
||||
this.doInitialize(fileServiceBasedWorkspaceConfiguration);
|
||||
this.onDidWorkspaceConfigurationChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
private doInitialize(fileServiceBasedWorkspaceConfiguration: FileServiceBasedWorkspaceConfiguration): void {
|
||||
this._workspaceConfiguration.dispose();
|
||||
this._workspaceConfigurationChangeDisposable.dispose();
|
||||
this._workspaceConfiguration = this._register(fileServiceBasedWorkspaceConfiguration);
|
||||
this._workspaceConfigurationChangeDisposable = this._register(this._workspaceConfiguration.onDidChange(e => this.onDidWorkspaceConfigurationChange(true)));
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
private async onDidWorkspaceConfigurationChange(reload: boolean): Promise<void> {
|
||||
if (reload) {
|
||||
await this.reload();
|
||||
}
|
||||
this.updateCache();
|
||||
this._onDidUpdateConfiguration.fire();
|
||||
}
|
||||
|
||||
private updateCache(): Promise<void> {
|
||||
if (this._workspaceIdentifier && this.configurationCache.needsCaching(this._workspaceIdentifier.configPath) && this._workspaceConfiguration instanceof FileServiceBasedWorkspaceConfiguration) {
|
||||
return this._workspaceConfiguration.load(this._workspaceIdentifier)
|
||||
.then(() => this._cachedConfiguration.updateWorkspace(this._workspaceIdentifier!, this._workspaceConfiguration.getConfigurationModel()));
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
interface IWorkspaceConfiguration extends IDisposable {
|
||||
readonly onDidChange: Event<void>;
|
||||
workspaceConfigurationModelParser: WorkspaceConfigurationModelParser;
|
||||
workspaceSettings: ConfigurationModel;
|
||||
workspaceIdentifier: IWorkspaceIdentifier | null;
|
||||
load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void>;
|
||||
getConfigurationModel(): ConfigurationModel;
|
||||
getFolders(): IStoredWorkspaceFolder[];
|
||||
getWorkspaceSettings(): ConfigurationModel;
|
||||
reprocessWorkspaceSettings(): ConfigurationModel;
|
||||
}
|
||||
|
||||
class FileServiceBasedWorkspaceConfiguration extends Disposable implements IWorkspaceConfiguration {
|
||||
|
||||
workspaceConfigurationModelParser: WorkspaceConfigurationModelParser;
|
||||
workspaceSettings: ConfigurationModel;
|
||||
private _workspaceIdentifier: IWorkspaceIdentifier | null = null;
|
||||
private workspaceConfigWatcher: IDisposable;
|
||||
private readonly reloadConfigurationScheduler: RunOnceScheduler;
|
||||
|
||||
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
constructor(private fileService: IFileService) {
|
||||
super();
|
||||
|
||||
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser('');
|
||||
this.workspaceSettings = new ConfigurationModel();
|
||||
|
||||
this._register(fileService.onDidFilesChange(e => this.handleWorkspaceFileEvents(e)));
|
||||
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
|
||||
this.workspaceConfigWatcher = this._register(this.watchWorkspaceConfigurationFile());
|
||||
}
|
||||
|
||||
get workspaceIdentifier(): IWorkspaceIdentifier | null {
|
||||
return this._workspaceIdentifier;
|
||||
}
|
||||
|
||||
async load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
|
||||
if (!this._workspaceIdentifier || this._workspaceIdentifier.id !== workspaceIdentifier.id) {
|
||||
this._workspaceIdentifier = workspaceIdentifier;
|
||||
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(this._workspaceIdentifier.id);
|
||||
dispose(this.workspaceConfigWatcher);
|
||||
this.workspaceConfigWatcher = this._register(this.watchWorkspaceConfigurationFile());
|
||||
}
|
||||
let contents = '';
|
||||
try {
|
||||
const content = await this.fileService.readFile(this._workspaceIdentifier.configPath);
|
||||
contents = content.value.toString();
|
||||
} catch (error) {
|
||||
const exists = await this.fileService.exists(this._workspaceIdentifier.configPath);
|
||||
if (exists) {
|
||||
errors.onUnexpectedError(error);
|
||||
}
|
||||
}
|
||||
this.workspaceConfigurationModelParser.parseContent(contents);
|
||||
this.consolidate();
|
||||
}
|
||||
|
||||
getConfigurationModel(): ConfigurationModel {
|
||||
return this.workspaceConfigurationModelParser.configurationModel;
|
||||
}
|
||||
|
||||
getFolders(): IStoredWorkspaceFolder[] {
|
||||
return this.workspaceConfigurationModelParser.folders;
|
||||
}
|
||||
|
||||
getWorkspaceSettings(): ConfigurationModel {
|
||||
return this.workspaceSettings;
|
||||
}
|
||||
|
||||
reprocessWorkspaceSettings(): ConfigurationModel {
|
||||
this.workspaceConfigurationModelParser.reprocessWorkspaceSettings();
|
||||
this.consolidate();
|
||||
return this.getWorkspaceSettings();
|
||||
}
|
||||
|
||||
private consolidate(): void {
|
||||
this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel, this.workspaceConfigurationModelParser.tasksModel);
|
||||
}
|
||||
|
||||
private watchWorkspaceConfigurationFile(): IDisposable {
|
||||
return this._workspaceIdentifier ? this.fileService.watch(this._workspaceIdentifier.configPath) : Disposable.None;
|
||||
}
|
||||
|
||||
private handleWorkspaceFileEvents(event: FileChangesEvent): void {
|
||||
if (this._workspaceIdentifier) {
|
||||
|
||||
// Find changes that affect workspace file
|
||||
if (event.contains(this._workspaceIdentifier.configPath)) {
|
||||
this.reloadConfigurationScheduler.schedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfiguration {
|
||||
|
||||
private readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
workspaceConfigurationModelParser: WorkspaceConfigurationModelParser;
|
||||
workspaceSettings: ConfigurationModel;
|
||||
|
||||
constructor(private readonly configurationCache: IConfigurationCache) {
|
||||
super();
|
||||
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser('');
|
||||
this.workspaceSettings = new ConfigurationModel();
|
||||
}
|
||||
|
||||
async load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
|
||||
try {
|
||||
const key = this.getKey(workspaceIdentifier);
|
||||
const contents = await this.configurationCache.read(key);
|
||||
this.workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(key.key);
|
||||
this.workspaceConfigurationModelParser.parseContent(contents);
|
||||
this.workspaceSettings = this.workspaceConfigurationModelParser.settingsModel.merge(this.workspaceConfigurationModelParser.launchModel, this.workspaceConfigurationModelParser.tasksModel);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
get workspaceIdentifier(): IWorkspaceIdentifier | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
getConfigurationModel(): ConfigurationModel {
|
||||
return this.workspaceConfigurationModelParser.configurationModel;
|
||||
}
|
||||
|
||||
getFolders(): IStoredWorkspaceFolder[] {
|
||||
return this.workspaceConfigurationModelParser.folders;
|
||||
}
|
||||
|
||||
getWorkspaceSettings(): ConfigurationModel {
|
||||
return this.workspaceSettings;
|
||||
}
|
||||
|
||||
reprocessWorkspaceSettings(): ConfigurationModel {
|
||||
return this.workspaceSettings;
|
||||
}
|
||||
|
||||
async updateWorkspace(workspaceIdentifier: IWorkspaceIdentifier, configurationModel: ConfigurationModel): Promise<void> {
|
||||
try {
|
||||
const key = this.getKey(workspaceIdentifier);
|
||||
if (configurationModel.keys.length) {
|
||||
await this.configurationCache.write(key, JSON.stringify(configurationModel.toJSON().contents));
|
||||
} else {
|
||||
await this.configurationCache.remove(key);
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
private getKey(workspaceIdentifier: IWorkspaceIdentifier): ConfigurationKey {
|
||||
return {
|
||||
type: 'workspaces',
|
||||
key: workspaceIdentifier.id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface IFolderConfiguration extends IDisposable {
|
||||
readonly onDidChange: Event<void>;
|
||||
loadConfiguration(): Promise<ConfigurationModel>;
|
||||
reprocess(): ConfigurationModel;
|
||||
}
|
||||
|
||||
class CachedFolderConfiguration extends Disposable implements IFolderConfiguration {
|
||||
|
||||
private readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
private configurationModel: ConfigurationModel;
|
||||
private readonly key: ConfigurationKey;
|
||||
|
||||
constructor(
|
||||
folder: URI,
|
||||
configFolderRelativePath: string,
|
||||
private readonly configurationCache: IConfigurationCache
|
||||
) {
|
||||
super();
|
||||
this.key = { type: 'folder', key: hash(join(folder.path, configFolderRelativePath)).toString(16) };
|
||||
this.configurationModel = new ConfigurationModel();
|
||||
}
|
||||
|
||||
async loadConfiguration(): Promise<ConfigurationModel> {
|
||||
try {
|
||||
const contents = await this.configurationCache.read(this.key);
|
||||
const parsed: IConfigurationModel = JSON.parse(contents.toString());
|
||||
this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides);
|
||||
} catch (e) {
|
||||
}
|
||||
return this.configurationModel;
|
||||
}
|
||||
|
||||
async updateConfiguration(configurationModel: ConfigurationModel): Promise<void> {
|
||||
if (configurationModel.keys.length) {
|
||||
await this.configurationCache.write(this.key, JSON.stringify(configurationModel.toJSON()));
|
||||
} else {
|
||||
await this.configurationCache.remove(this.key);
|
||||
}
|
||||
}
|
||||
|
||||
reprocess(): ConfigurationModel {
|
||||
return this.configurationModel;
|
||||
}
|
||||
|
||||
getUnsupportedKeys(): string[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export class FolderConfiguration extends Disposable implements IFolderConfiguration {
|
||||
|
||||
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
private folderConfiguration: IFolderConfiguration;
|
||||
private folderConfigurationDisposable: IDisposable = Disposable.None;
|
||||
private readonly configurationFolder: URI;
|
||||
private cachedFolderConfiguration: CachedFolderConfiguration;
|
||||
|
||||
constructor(
|
||||
readonly workspaceFolder: IWorkspaceFolder,
|
||||
configFolderRelativePath: string,
|
||||
private readonly workbenchState: WorkbenchState,
|
||||
fileService: IFileService,
|
||||
private readonly configurationCache: IConfigurationCache
|
||||
) {
|
||||
super();
|
||||
|
||||
this.configurationFolder = resources.joinPath(workspaceFolder.uri, configFolderRelativePath);
|
||||
this.cachedFolderConfiguration = new CachedFolderConfiguration(workspaceFolder.uri, configFolderRelativePath, configurationCache);
|
||||
if (this.configurationCache.needsCaching(workspaceFolder.uri)) {
|
||||
this.folderConfiguration = this.cachedFolderConfiguration;
|
||||
whenProviderRegistered(workspaceFolder.uri, fileService)
|
||||
.then(() => {
|
||||
this.folderConfiguration.dispose();
|
||||
this.folderConfigurationDisposable.dispose();
|
||||
this.folderConfiguration = this.createFileServiceBasedConfiguration(fileService);
|
||||
this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange()));
|
||||
this.onDidFolderConfigurationChange();
|
||||
});
|
||||
} else {
|
||||
this.folderConfiguration = this.createFileServiceBasedConfiguration(fileService);
|
||||
this.folderConfigurationDisposable = this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange()));
|
||||
}
|
||||
}
|
||||
|
||||
loadConfiguration(): Promise<ConfigurationModel> {
|
||||
return this.folderConfiguration.loadConfiguration();
|
||||
}
|
||||
|
||||
reprocess(): ConfigurationModel {
|
||||
return this.folderConfiguration.reprocess();
|
||||
}
|
||||
|
||||
private onDidFolderConfigurationChange(): void {
|
||||
this.updateCache();
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
private createFileServiceBasedConfiguration(fileService: IFileService) {
|
||||
const settingsResources = [resources.joinPath(this.configurationFolder, `${FOLDER_SETTINGS_NAME}.json`)];
|
||||
const standAloneConfigurationResources: [string, URI][] = [TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY].map(name => ([name, resources.joinPath(this.configurationFolder, `${name}.json`)]));
|
||||
return new FileServiceBasedConfiguration(this.configurationFolder.toString(), settingsResources, standAloneConfigurationResources, WorkbenchState.WORKSPACE === this.workbenchState ? FOLDER_SCOPES : WORKSPACE_SCOPES, fileService);
|
||||
}
|
||||
|
||||
private updateCache(): Promise<void> {
|
||||
if (this.configurationCache.needsCaching(this.configurationFolder) && this.folderConfiguration instanceof FileServiceBasedConfiguration) {
|
||||
return this.folderConfiguration.loadConfiguration()
|
||||
.then(configurationModel => this.cachedFolderConfiguration.updateConfiguration(configurationModel));
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IConfigurationCache, ConfigurationKey } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
export class ConfigurationCache implements IConfigurationCache {
|
||||
|
||||
needsCaching(resource: URI): boolean {
|
||||
// Cache all non user data resources
|
||||
return resource.scheme !== Schemas.userData;
|
||||
}
|
||||
|
||||
async read(key: ConfigurationKey): Promise<string> {
|
||||
return '';
|
||||
}
|
||||
|
||||
async write(key: ConfigurationKey, content: string): Promise<void> {
|
||||
}
|
||||
|
||||
async remove(key: ConfigurationKey): Promise<void> {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,821 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { equals } from 'vs/base/common/objects';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Queue, Barrier, runWhenIdle } from 'vs/base/common/async';
|
||||
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { IWorkspaceContextService, Workspace as BaseWorkspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent, AllKeysConfigurationChangeEvent, mergeChanges } from 'vs/platform/configuration/common/configurationModels';
|
||||
import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData, IConfigurationService, IConfigurationValue, IConfigurationChange } from 'vs/platform/configuration/common/configuration';
|
||||
import { Configuration } from 'vs/workbench/services/configuration/common/configurationModels';
|
||||
import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings, machineOverridableSettings } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, isSingleFolderWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, IEmptyWorkspaceInitializationPayload, useSlashForPath, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ConfigurationEditingService, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService';
|
||||
import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, UserConfiguration } from 'vs/workbench/services/configuration/browser/configuration';
|
||||
import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService';
|
||||
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
|
||||
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 { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
|
||||
class Workspace extends BaseWorkspace {
|
||||
initialized: boolean = false;
|
||||
}
|
||||
|
||||
export class WorkspaceService extends Disposable implements IConfigurationService, IWorkspaceContextService {
|
||||
|
||||
public _serviceBrand: undefined;
|
||||
|
||||
private workspace!: Workspace;
|
||||
private completeWorkspaceBarrier: Barrier;
|
||||
private readonly configurationCache: IConfigurationCache;
|
||||
private _configuration: Configuration;
|
||||
private initialized: boolean = false;
|
||||
private defaultConfiguration: DefaultConfigurationModel;
|
||||
private localUserConfiguration: UserConfiguration;
|
||||
private remoteUserConfiguration: RemoteUserConfiguration | null = null;
|
||||
private workspaceConfiguration: WorkspaceConfiguration;
|
||||
private cachedFolderConfigs: ResourceMap<FolderConfiguration>;
|
||||
private workspaceEditingQueue: Queue<void>;
|
||||
|
||||
private readonly logService: ILogService;
|
||||
private readonly fileService: IFileService;
|
||||
|
||||
protected readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>());
|
||||
public readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event;
|
||||
|
||||
protected readonly _onDidChangeWorkspaceFolders: Emitter<IWorkspaceFoldersChangeEvent> = this._register(new Emitter<IWorkspaceFoldersChangeEvent>());
|
||||
public readonly onDidChangeWorkspaceFolders: Event<IWorkspaceFoldersChangeEvent> = this._onDidChangeWorkspaceFolders.event;
|
||||
|
||||
protected readonly _onDidChangeWorkspaceName: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeWorkspaceName: Event<void> = this._onDidChangeWorkspaceName.event;
|
||||
|
||||
protected readonly _onDidChangeWorkbenchState: Emitter<WorkbenchState> = this._register(new Emitter<WorkbenchState>());
|
||||
public readonly onDidChangeWorkbenchState: Event<WorkbenchState> = this._onDidChangeWorkbenchState.event;
|
||||
|
||||
// TODO@sandeep debt with cyclic dependencies
|
||||
private configurationEditingService!: ConfigurationEditingService;
|
||||
private jsonEditingService!: JSONEditingService;
|
||||
private cyclicDependencyReady!: Function;
|
||||
private cyclicDependency = new Promise<void>(resolve => this.cyclicDependencyReady = resolve);
|
||||
|
||||
constructor(
|
||||
{ remoteAuthority, configurationCache }: { remoteAuthority?: string, configurationCache: IConfigurationCache },
|
||||
environmentService: IWorkbenchEnvironmentService,
|
||||
fileService: IFileService,
|
||||
remoteAgentService: IRemoteAgentService,
|
||||
logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
// register defaults before creating default configuration model
|
||||
// so that the model is not required to be updated after registering
|
||||
if (environmentService.options?.configurationDefaults) {
|
||||
configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]);
|
||||
}
|
||||
|
||||
this.completeWorkspaceBarrier = new Barrier();
|
||||
this.defaultConfiguration = new DefaultConfigurationModel();
|
||||
this.configurationCache = configurationCache;
|
||||
this.fileService = fileService;
|
||||
this.logService = logService;
|
||||
this._configuration = new Configuration(this.defaultConfiguration, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.workspace);
|
||||
this.cachedFolderConfigs = new ResourceMap<FolderConfiguration>();
|
||||
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, fileService, remoteAgentService));
|
||||
this._register(this.remoteUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onRemoteUserConfigurationChanged(userConfiguration)));
|
||||
}
|
||||
this.workspaceConfiguration = this._register(new WorkspaceConfiguration(configurationCache, fileService));
|
||||
this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => {
|
||||
this.onWorkspaceConfigurationChanged().then(() => {
|
||||
this.workspace.initialized = this.workspaceConfiguration.initialized;
|
||||
this.checkAndMarkWorkspaceComplete();
|
||||
});
|
||||
}));
|
||||
|
||||
this._register(configurationRegistry.onDidUpdateConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties)));
|
||||
|
||||
this.workspaceEditingQueue = new Queue<void>();
|
||||
}
|
||||
|
||||
// Workspace Context Service Impl
|
||||
|
||||
public getCompleteWorkspace(): Promise<Workspace> {
|
||||
return this.completeWorkspaceBarrier.wait().then(() => this.getWorkspace());
|
||||
}
|
||||
|
||||
public getWorkspace(): Workspace {
|
||||
return this.workspace;
|
||||
}
|
||||
|
||||
public getWorkbenchState(): WorkbenchState {
|
||||
// Workspace has configuration file
|
||||
if (this.workspace.configuration) {
|
||||
return WorkbenchState.WORKSPACE;
|
||||
}
|
||||
|
||||
// Folder has single root
|
||||
if (this.workspace.folders.length === 1) {
|
||||
return WorkbenchState.FOLDER;
|
||||
}
|
||||
|
||||
// Empty
|
||||
return WorkbenchState.EMPTY;
|
||||
}
|
||||
|
||||
public getWorkspaceFolder(resource: URI): IWorkspaceFolder | null {
|
||||
return this.workspace.getFolder(resource);
|
||||
}
|
||||
|
||||
public addFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number): Promise<void> {
|
||||
return this.updateFolders(foldersToAdd, [], index);
|
||||
}
|
||||
|
||||
public removeFolders(foldersToRemove: URI[]): Promise<void> {
|
||||
return this.updateFolders([], foldersToRemove);
|
||||
}
|
||||
|
||||
public updateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): Promise<void> {
|
||||
return this.cyclicDependency.then(() => {
|
||||
return this.workspaceEditingQueue.queue(() => this.doUpdateFolders(foldersToAdd, foldersToRemove, index));
|
||||
});
|
||||
}
|
||||
|
||||
public isInsideWorkspace(resource: URI): boolean {
|
||||
return !!this.getWorkspaceFolder(resource);
|
||||
}
|
||||
|
||||
public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean {
|
||||
switch (this.getWorkbenchState()) {
|
||||
case WorkbenchState.FOLDER:
|
||||
return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && isEqual(workspaceIdentifier, this.workspace.folders[0].uri);
|
||||
case WorkbenchState.WORKSPACE:
|
||||
return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): Promise<void> {
|
||||
if (this.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
|
||||
return Promise.resolve(undefined); // we need a workspace to begin with
|
||||
}
|
||||
|
||||
if (foldersToAdd.length + foldersToRemove.length === 0) {
|
||||
return Promise.resolve(undefined); // nothing to do
|
||||
}
|
||||
|
||||
let foldersHaveChanged = false;
|
||||
|
||||
// Remove first (if any)
|
||||
let currentWorkspaceFolders = this.getWorkspace().folders;
|
||||
let newStoredFolders: IStoredWorkspaceFolder[] = currentWorkspaceFolders.map(f => f.raw).filter((folder, index): folder is IStoredWorkspaceFolder => {
|
||||
if (!isStoredWorkspaceFolder(folder)) {
|
||||
return true; // keep entries which are unrelated
|
||||
}
|
||||
|
||||
return !this.contains(foldersToRemove, currentWorkspaceFolders[index].uri); // keep entries which are unrelated
|
||||
});
|
||||
|
||||
const slashForPath = useSlashForPath(newStoredFolders);
|
||||
|
||||
foldersHaveChanged = currentWorkspaceFolders.length !== newStoredFolders.length;
|
||||
|
||||
// Add afterwards (if any)
|
||||
if (foldersToAdd.length) {
|
||||
|
||||
// Recompute current workspace folders if we have folders to add
|
||||
const workspaceConfigPath = this.getWorkspace().configuration!;
|
||||
const workspaceConfigFolder = dirname(workspaceConfigPath);
|
||||
currentWorkspaceFolders = toWorkspaceFolders(newStoredFolders, workspaceConfigPath);
|
||||
const currentWorkspaceFolderUris = currentWorkspaceFolders.map(folder => folder.uri);
|
||||
|
||||
const storedFoldersToAdd: IStoredWorkspaceFolder[] = [];
|
||||
|
||||
for (const folderToAdd of foldersToAdd) {
|
||||
const folderURI = folderToAdd.uri;
|
||||
if (this.contains(currentWorkspaceFolderUris, folderURI)) {
|
||||
continue; // already existing
|
||||
}
|
||||
try {
|
||||
const result = await this.fileService.resolve(folderURI);
|
||||
if (!result.isDirectory) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) { /* Ignore */ }
|
||||
storedFoldersToAdd.push(getStoredWorkspaceFolder(folderURI, false, folderToAdd.name, workspaceConfigFolder, slashForPath));
|
||||
}
|
||||
|
||||
// Apply to array of newStoredFolders
|
||||
if (storedFoldersToAdd.length > 0) {
|
||||
foldersHaveChanged = true;
|
||||
|
||||
if (typeof index === 'number' && index >= 0 && index < newStoredFolders.length) {
|
||||
newStoredFolders = newStoredFolders.slice(0);
|
||||
newStoredFolders.splice(index, 0, ...storedFoldersToAdd);
|
||||
} else {
|
||||
newStoredFolders = [...newStoredFolders, ...storedFoldersToAdd];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set folders if we recorded a change
|
||||
if (foldersHaveChanged) {
|
||||
return this.setFolders(newStoredFolders);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private setFolders(folders: IStoredWorkspaceFolder[]): Promise<void> {
|
||||
return this.cyclicDependency.then(() => {
|
||||
return this.workspaceConfiguration.setFolders(folders, this.jsonEditingService)
|
||||
.then(() => this.onWorkspaceConfigurationChanged());
|
||||
});
|
||||
}
|
||||
|
||||
private contains(resources: URI[], toCheck: URI): boolean {
|
||||
return resources.some(resource => isEqual(resource, toCheck));
|
||||
}
|
||||
|
||||
// Workspace Configuration Service Impl
|
||||
|
||||
getConfigurationData(): IConfigurationData {
|
||||
return this._configuration.toData();
|
||||
}
|
||||
|
||||
getValue<T>(): T;
|
||||
getValue<T>(section: string): T;
|
||||
getValue<T>(overrides: IConfigurationOverrides): T;
|
||||
getValue<T>(section: string, overrides: IConfigurationOverrides): T;
|
||||
getValue(arg1?: any, arg2?: any): any {
|
||||
const section = typeof arg1 === 'string' ? arg1 : undefined;
|
||||
const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : undefined;
|
||||
return this._configuration.getValue(section, overrides);
|
||||
}
|
||||
|
||||
updateValue(key: string, value: any): Promise<void>;
|
||||
updateValue(key: string, value: any, overrides: IConfigurationOverrides): Promise<void>;
|
||||
updateValue(key: string, value: any, target: ConfigurationTarget): Promise<void>;
|
||||
updateValue(key: string, value: any, overrides: IConfigurationOverrides, target: ConfigurationTarget): Promise<void>;
|
||||
updateValue(key: string, value: any, overrides: IConfigurationOverrides, target: ConfigurationTarget, donotNotifyError: boolean): Promise<void>;
|
||||
updateValue(key: string, value: any, arg3?: any, arg4?: any, donotNotifyError?: any): Promise<void> {
|
||||
return this.cyclicDependency.then(() => {
|
||||
const overrides = isConfigurationOverrides(arg3) ? arg3 : undefined;
|
||||
const target = this.deriveConfigurationTarget(key, value, overrides, overrides ? arg4 : arg3);
|
||||
return target ? this.writeConfigurationValue(key, value, target, overrides, donotNotifyError)
|
||||
: Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
reloadConfiguration(folder?: IWorkspaceFolder, key?: string): Promise<void> {
|
||||
if (folder) {
|
||||
return this.reloadWorkspaceFolderConfiguration(folder, key);
|
||||
}
|
||||
return this.reloadUserConfiguration()
|
||||
.then(({ local, remote }) => this.reloadWorkspaceConfiguration()
|
||||
.then(() => this.loadConfiguration(local, remote)));
|
||||
}
|
||||
|
||||
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> {
|
||||
return this._configuration.inspect<T>(key, overrides);
|
||||
}
|
||||
|
||||
keys(): {
|
||||
default: string[];
|
||||
user: string[];
|
||||
workspace: string[];
|
||||
workspaceFolder: string[];
|
||||
} {
|
||||
return this._configuration.keys();
|
||||
}
|
||||
|
||||
async initialize(arg: IWorkspaceInitializationPayload): Promise<void> {
|
||||
mark('willInitWorkspaceService');
|
||||
|
||||
const workspace = await this.createWorkspace(arg);
|
||||
await this.updateWorkspaceAndInitializeConfiguration(workspace);
|
||||
this.checkAndMarkWorkspaceComplete();
|
||||
|
||||
mark('didInitWorkspaceService');
|
||||
}
|
||||
|
||||
acquireInstantiationService(instantiationService: IInstantiationService): void {
|
||||
this.configurationEditingService = instantiationService.createInstance(ConfigurationEditingService);
|
||||
this.jsonEditingService = instantiationService.createInstance(JSONEditingService);
|
||||
|
||||
if (this.cyclicDependencyReady) {
|
||||
this.cyclicDependencyReady();
|
||||
} else {
|
||||
this.cyclicDependency = Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private createWorkspace(arg: IWorkspaceInitializationPayload): Promise<Workspace> {
|
||||
if (isWorkspaceIdentifier(arg)) {
|
||||
return this.createMultiFolderWorkspace(arg);
|
||||
}
|
||||
|
||||
if (isSingleFolderWorkspaceInitializationPayload(arg)) {
|
||||
return this.createSingleFolderWorkspace(arg);
|
||||
}
|
||||
|
||||
return this.createEmptyWorkspace(arg);
|
||||
}
|
||||
|
||||
private createMultiFolderWorkspace(workspaceIdentifier: IWorkspaceIdentifier): Promise<Workspace> {
|
||||
return this.workspaceConfiguration.initialize({ id: workspaceIdentifier.id, configPath: workspaceIdentifier.configPath })
|
||||
.then(() => {
|
||||
const workspaceConfigPath = workspaceIdentifier.configPath;
|
||||
const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), workspaceConfigPath);
|
||||
const workspaceId = workspaceIdentifier.id;
|
||||
const workspace = new Workspace(workspaceId, workspaceFolders, workspaceConfigPath);
|
||||
workspace.initialized = this.workspaceConfiguration.initialized;
|
||||
return workspace;
|
||||
});
|
||||
}
|
||||
|
||||
private createSingleFolderWorkspace(singleFolder: ISingleFolderWorkspaceInitializationPayload): Promise<Workspace> {
|
||||
const workspace = new Workspace(singleFolder.id, [toWorkspaceFolder(singleFolder.folder)]);
|
||||
workspace.initialized = true;
|
||||
return Promise.resolve(workspace);
|
||||
}
|
||||
|
||||
private createEmptyWorkspace(emptyWorkspace: IEmptyWorkspaceInitializationPayload): Promise<Workspace> {
|
||||
const workspace = new Workspace(emptyWorkspace.id);
|
||||
workspace.initialized = true;
|
||||
return Promise.resolve(workspace);
|
||||
}
|
||||
|
||||
private checkAndMarkWorkspaceComplete(): void {
|
||||
if (!this.completeWorkspaceBarrier.isOpen() && this.workspace.initialized) {
|
||||
this.completeWorkspaceBarrier.open();
|
||||
this.validateWorkspaceFoldersAndReload();
|
||||
}
|
||||
}
|
||||
|
||||
private updateWorkspaceAndInitializeConfiguration(workspace: Workspace): Promise<void> {
|
||||
const hasWorkspaceBefore = !!this.workspace;
|
||||
let previousState: WorkbenchState;
|
||||
let previousWorkspacePath: string | undefined;
|
||||
let previousFolders: WorkspaceFolder[];
|
||||
|
||||
if (hasWorkspaceBefore) {
|
||||
previousState = this.getWorkbenchState();
|
||||
previousWorkspacePath = this.workspace.configuration ? this.workspace.configuration.fsPath : undefined;
|
||||
previousFolders = this.workspace.folders;
|
||||
this.workspace.update(workspace);
|
||||
} else {
|
||||
this.workspace = workspace;
|
||||
}
|
||||
|
||||
return this.initializeConfiguration().then(() => {
|
||||
|
||||
// Trigger changes after configuration initialization so that configuration is up to date.
|
||||
if (hasWorkspaceBefore) {
|
||||
const newState = this.getWorkbenchState();
|
||||
if (previousState && newState !== previousState) {
|
||||
this._onDidChangeWorkbenchState.fire(newState);
|
||||
}
|
||||
|
||||
const newWorkspacePath = this.workspace.configuration ? this.workspace.configuration.fsPath : undefined;
|
||||
if (previousWorkspacePath && newWorkspacePath !== previousWorkspacePath || newState !== previousState) {
|
||||
this._onDidChangeWorkspaceName.fire();
|
||||
}
|
||||
|
||||
const folderChanges = this.compareFolders(previousFolders, this.workspace.folders);
|
||||
if (folderChanges && (folderChanges.added.length || folderChanges.removed.length || folderChanges.changed.length)) {
|
||||
this._onDidChangeWorkspaceFolders.fire(folderChanges);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!this.localUserConfiguration.hasTasksLoaded) {
|
||||
// Reload local user configuration again to load user tasks
|
||||
runWhenIdle(() => this.reloadLocalUserConfiguration(), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private compareFolders(currentFolders: IWorkspaceFolder[], newFolders: IWorkspaceFolder[]): IWorkspaceFoldersChangeEvent {
|
||||
const result: IWorkspaceFoldersChangeEvent = { added: [], removed: [], changed: [] };
|
||||
result.added = newFolders.filter(newFolder => !currentFolders.some(currentFolder => newFolder.uri.toString() === currentFolder.uri.toString()));
|
||||
for (let currentIndex = 0; currentIndex < currentFolders.length; currentIndex++) {
|
||||
let currentFolder = currentFolders[currentIndex];
|
||||
let newIndex = 0;
|
||||
for (newIndex = 0; newIndex < newFolders.length && currentFolder.uri.toString() !== newFolders[newIndex].uri.toString(); newIndex++) { }
|
||||
if (newIndex < newFolders.length) {
|
||||
if (currentIndex !== newIndex || currentFolder.name !== newFolders[newIndex].name) {
|
||||
result.changed.push(currentFolder);
|
||||
}
|
||||
} else {
|
||||
result.removed.push(currentFolder);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private initializeConfiguration(): Promise<void> {
|
||||
return this.initializeUserConfiguration()
|
||||
.then(({ local, remote }) => this.loadConfiguration(local, remote));
|
||||
}
|
||||
|
||||
private initializeUserConfiguration(): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> {
|
||||
return Promise.all([this.localUserConfiguration.initialize(), this.remoteUserConfiguration ? this.remoteUserConfiguration.initialize() : Promise.resolve(new ConfigurationModel())])
|
||||
.then(([local, remote]) => ({ local, remote }));
|
||||
}
|
||||
|
||||
private reloadUserConfiguration(): Promise<{ local: ConfigurationModel, remote: ConfigurationModel }> {
|
||||
return Promise.all([this.reloadLocalUserConfiguration(true), this.reloadRemoteUserConfiguration(true)]).then(([local, remote]) => ({ local, remote }));
|
||||
}
|
||||
|
||||
async reloadLocalUserConfiguration(donotTrigger?: boolean): Promise<ConfigurationModel> {
|
||||
const model = await this.localUserConfiguration.reload();
|
||||
if (!donotTrigger) {
|
||||
this.onLocalUserConfigurationChanged(model);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private async reloadRemoteUserConfiguration(donotTrigger?: boolean): Promise<ConfigurationModel> {
|
||||
if (this.remoteUserConfiguration) {
|
||||
const model = await this.remoteUserConfiguration.reload();
|
||||
if (!donotTrigger) {
|
||||
this.onRemoteUserConfigurationChanged(model);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
return new ConfigurationModel();
|
||||
}
|
||||
|
||||
private reloadWorkspaceConfiguration(key?: string): Promise<void> {
|
||||
const workbenchState = this.getWorkbenchState();
|
||||
if (workbenchState === WorkbenchState.FOLDER) {
|
||||
return this.onWorkspaceFolderConfigurationChanged(this.workspace.folders[0], key);
|
||||
}
|
||||
if (workbenchState === WorkbenchState.WORKSPACE) {
|
||||
return this.workspaceConfiguration.reload().then(() => this.onWorkspaceConfigurationChanged());
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private reloadWorkspaceFolderConfiguration(folder: IWorkspaceFolder, key?: string): Promise<void> {
|
||||
return this.onWorkspaceFolderConfigurationChanged(folder, key);
|
||||
}
|
||||
|
||||
private loadConfiguration(userConfigurationModel: ConfigurationModel, remoteUserConfigurationModel: ConfigurationModel): Promise<void> {
|
||||
// reset caches
|
||||
this.cachedFolderConfigs = new ResourceMap<FolderConfiguration>();
|
||||
|
||||
const folders = this.workspace.folders;
|
||||
return this.loadFolderConfigurations(folders)
|
||||
.then((folderConfigurations) => {
|
||||
|
||||
let workspaceConfiguration = this.getWorkspaceConfigurationModel(folderConfigurations);
|
||||
const folderConfigurationModels = new ResourceMap<ConfigurationModel>();
|
||||
folderConfigurations.forEach((folderConfiguration, index) => folderConfigurationModels.set(folders[index].uri, folderConfiguration));
|
||||
|
||||
const currentConfiguration = this._configuration;
|
||||
this._configuration = new Configuration(this.defaultConfiguration, userConfigurationModel, remoteUserConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.workspace);
|
||||
|
||||
if (this.initialized) {
|
||||
const change = this._configuration.compare(currentConfiguration);
|
||||
this.triggerConfigurationChange(change, { data: currentConfiguration.toData(), workspace: this.workspace }, ConfigurationTarget.WORKSPACE);
|
||||
} else {
|
||||
this._onDidChangeConfiguration.fire(new AllKeysConfigurationChangeEvent(this._configuration, this.workspace, ConfigurationTarget.WORKSPACE, this.getTargetConfiguration(ConfigurationTarget.WORKSPACE)));
|
||||
this.initialized = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getWorkspaceConfigurationModel(folderConfigurations: ConfigurationModel[]): ConfigurationModel {
|
||||
switch (this.getWorkbenchState()) {
|
||||
case WorkbenchState.FOLDER:
|
||||
return folderConfigurations[0];
|
||||
case WorkbenchState.WORKSPACE:
|
||||
return this.workspaceConfiguration.getConfiguration();
|
||||
default:
|
||||
return new ConfigurationModel();
|
||||
}
|
||||
}
|
||||
|
||||
private onDefaultConfigurationChanged(keys: string[]): void {
|
||||
this.defaultConfiguration = new DefaultConfigurationModel();
|
||||
if (this.workspace) {
|
||||
const previousData = this._configuration.toData();
|
||||
const change = this._configuration.compareAndUpdateDefaultConfiguration(this.defaultConfiguration, keys);
|
||||
if (this.remoteUserConfiguration) {
|
||||
this._configuration.updateLocalUserConfiguration(this.localUserConfiguration.reprocess());
|
||||
this._configuration.updateRemoteUserConfiguration(this.remoteUserConfiguration.reprocess());
|
||||
}
|
||||
if (this.getWorkbenchState() === WorkbenchState.FOLDER) {
|
||||
const folderConfiguration = this.cachedFolderConfigs.get(this.workspace.folders[0].uri);
|
||||
if (folderConfiguration) {
|
||||
this._configuration.updateWorkspaceConfiguration(folderConfiguration.reprocess());
|
||||
this._configuration.updateFolderConfiguration(this.workspace.folders[0].uri, folderConfiguration.reprocess());
|
||||
}
|
||||
} else {
|
||||
this._configuration.updateWorkspaceConfiguration(this.workspaceConfiguration.reprocessWorkspaceSettings());
|
||||
for (const folder of this.workspace.folders) {
|
||||
const folderConfiguration = this.cachedFolderConfigs.get(folder.uri);
|
||||
if (folderConfiguration) {
|
||||
this._configuration.updateFolderConfiguration(folder.uri, folderConfiguration.reprocess());
|
||||
}
|
||||
}
|
||||
}
|
||||
this.triggerConfigurationChange(change, { data: previousData, workspace: this.workspace }, ConfigurationTarget.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private onLocalUserConfigurationChanged(userConfiguration: ConfigurationModel): void {
|
||||
const previous = { data: this._configuration.toData(), workspace: this.workspace };
|
||||
const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration);
|
||||
this.triggerConfigurationChange(change, previous, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
private onRemoteUserConfigurationChanged(userConfiguration: ConfigurationModel): void {
|
||||
const previous = { data: this._configuration.toData(), workspace: this.workspace };
|
||||
const change = this._configuration.compareAndUpdateRemoteUserConfiguration(userConfiguration);
|
||||
this.triggerConfigurationChange(change, previous, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
private async onWorkspaceConfigurationChanged(): Promise<void> {
|
||||
if (this.workspace && this.workspace.configuration) {
|
||||
let newFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), this.workspace.configuration);
|
||||
|
||||
// Validate only if workspace is initialized
|
||||
if (this.workspace.initialized) {
|
||||
const { added, removed, changed } = this.compareFolders(this.workspace.folders, newFolders);
|
||||
|
||||
/* If changed validate new folders */
|
||||
if (added.length || removed.length || changed.length) {
|
||||
newFolders = await this.toValidWorkspaceFolders(newFolders);
|
||||
}
|
||||
/* Otherwise use existing */
|
||||
else {
|
||||
newFolders = this.workspace.folders;
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateWorkspaceConfiguration(newFolders, this.workspaceConfiguration.getConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
private async updateWorkspaceConfiguration(workspaceFolders: WorkspaceFolder[], configuration: ConfigurationModel): Promise<void> {
|
||||
const previous = { data: this._configuration.toData(), workspace: this.workspace };
|
||||
const change = this._configuration.compareAndUpdateWorkspaceConfiguration(configuration);
|
||||
const changes = this.compareFolders(this.workspace.folders, workspaceFolders);
|
||||
if (changes.added.length || changes.removed.length || changes.changed.length) {
|
||||
this.workspace.folders = workspaceFolders;
|
||||
const change = await this.onFoldersChanged();
|
||||
this.triggerConfigurationChange(change, previous, ConfigurationTarget.WORKSPACE_FOLDER);
|
||||
this._onDidChangeWorkspaceFolders.fire(changes);
|
||||
} else {
|
||||
this.triggerConfigurationChange(change, previous, ConfigurationTarget.WORKSPACE);
|
||||
}
|
||||
}
|
||||
|
||||
private onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder, key?: string): Promise<void> {
|
||||
return this.loadFolderConfigurations([folder])
|
||||
.then(([folderConfiguration]) => {
|
||||
const previous = { data: this._configuration.toData(), workspace: this.workspace };
|
||||
const folderConfiguraitonChange = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderConfiguration);
|
||||
if (this.getWorkbenchState() === WorkbenchState.FOLDER) {
|
||||
const workspaceConfigurationChange = this._configuration.compareAndUpdateWorkspaceConfiguration(folderConfiguration);
|
||||
this.triggerConfigurationChange(mergeChanges(folderConfiguraitonChange, workspaceConfigurationChange), previous, ConfigurationTarget.WORKSPACE);
|
||||
} else {
|
||||
this.triggerConfigurationChange(folderConfiguraitonChange, previous, ConfigurationTarget.WORKSPACE_FOLDER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async onFoldersChanged(): Promise<IConfigurationChange> {
|
||||
const changes: IConfigurationChange[] = [];
|
||||
|
||||
// Remove the configurations of deleted folders
|
||||
for (const key of this.cachedFolderConfigs.keys()) {
|
||||
if (!this.workspace.folders.filter(folder => folder.uri.toString() === key.toString())[0]) {
|
||||
const folderConfiguration = this.cachedFolderConfigs.get(key);
|
||||
folderConfiguration!.dispose();
|
||||
this.cachedFolderConfigs.delete(key);
|
||||
changes.push(this._configuration.compareAndDeleteFolderConfiguration(key));
|
||||
}
|
||||
}
|
||||
|
||||
const toInitialize = this.workspace.folders.filter(folder => !this.cachedFolderConfigs.has(folder.uri));
|
||||
if (toInitialize.length) {
|
||||
const folderConfigurations = await this.loadFolderConfigurations(toInitialize);
|
||||
folderConfigurations.forEach((folderConfiguration, index) => {
|
||||
changes.push(this._configuration.compareAndUpdateFolderConfiguration(toInitialize[index].uri, folderConfiguration));
|
||||
});
|
||||
}
|
||||
return mergeChanges(...changes);
|
||||
}
|
||||
|
||||
private loadFolderConfigurations(folders: IWorkspaceFolder[]): Promise<ConfigurationModel[]> {
|
||||
return Promise.all([...folders.map(folder => {
|
||||
let folderConfiguration = this.cachedFolderConfigs.get(folder.uri);
|
||||
if (!folderConfiguration) {
|
||||
folderConfiguration = new FolderConfiguration(folder, FOLDER_CONFIG_FOLDER_NAME, this.getWorkbenchState(), this.fileService, this.configurationCache);
|
||||
this._register(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder)));
|
||||
this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration));
|
||||
}
|
||||
return folderConfiguration.loadConfiguration();
|
||||
})]);
|
||||
}
|
||||
|
||||
private async validateWorkspaceFoldersAndReload(): Promise<void> {
|
||||
const validWorkspaceFolders = await this.toValidWorkspaceFolders(this.workspace.folders);
|
||||
const { removed } = this.compareFolders(this.workspace.folders, validWorkspaceFolders);
|
||||
if (removed.length) {
|
||||
await this.updateWorkspaceConfiguration(validWorkspaceFolders, this.workspaceConfiguration.getConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out workspace folders which are files (not directories)
|
||||
// Workspace folders those cannot be resolved are not filtered because they are handled by the Explorer.
|
||||
private async toValidWorkspaceFolders(workspaceFolders: WorkspaceFolder[]): Promise<WorkspaceFolder[]> {
|
||||
const validWorkspaceFolders: WorkspaceFolder[] = [];
|
||||
for (const workspaceFolder of workspaceFolders) {
|
||||
try {
|
||||
const result = await this.fileService.resolve(workspaceFolder.uri);
|
||||
if (!result.isDirectory) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.warn(`Ignoring the error while validating workspace folder ${workspaceFolder.uri.toString()} - ${toErrorMessage(e)}`);
|
||||
}
|
||||
validWorkspaceFolders.push(workspaceFolder);
|
||||
}
|
||||
return validWorkspaceFolders;
|
||||
}
|
||||
|
||||
private writeConfigurationValue(key: string, value: any, target: ConfigurationTarget, overrides: IConfigurationOverrides | undefined, donotNotifyError: boolean): Promise<void> {
|
||||
if (target === ConfigurationTarget.DEFAULT) {
|
||||
return Promise.reject(new Error('Invalid configuration target'));
|
||||
}
|
||||
|
||||
if (target === ConfigurationTarget.MEMORY) {
|
||||
const previous = { data: this._configuration.toData(), workspace: this.workspace };
|
||||
this._configuration.updateValue(key, value, overrides);
|
||||
this.triggerConfigurationChange({ keys: overrides?.overrideIdentifier ? [keyFromOverrideIdentifier(overrides.overrideIdentifier), key] : [key], overrides: overrides?.overrideIdentifier ? [[overrides?.overrideIdentifier, [key]]] : [] }, previous, target);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const editableConfigurationTarget = this.toEditableConfigurationTarget(target, key);
|
||||
if (!editableConfigurationTarget) {
|
||||
return Promise.reject(new Error('Invalid configuration target'));
|
||||
}
|
||||
|
||||
if (editableConfigurationTarget === EditableConfigurationTarget.USER_REMOTE && !this.remoteUserConfiguration) {
|
||||
return Promise.reject(new Error('Invalid configuration target'));
|
||||
}
|
||||
|
||||
return this.configurationEditingService.writeConfiguration(editableConfigurationTarget, { key, value }, { scopes: overrides, donotNotifyError })
|
||||
.then(() => {
|
||||
switch (editableConfigurationTarget) {
|
||||
case EditableConfigurationTarget.USER_LOCAL:
|
||||
return this.reloadLocalUserConfiguration().then(() => undefined);
|
||||
case EditableConfigurationTarget.USER_REMOTE:
|
||||
return this.reloadRemoteUserConfiguration().then(() => undefined);
|
||||
case EditableConfigurationTarget.WORKSPACE:
|
||||
return this.reloadWorkspaceConfiguration();
|
||||
case EditableConfigurationTarget.WORKSPACE_FOLDER:
|
||||
const workspaceFolder = overrides && overrides.resource ? this.workspace.getFolder(overrides.resource) : null;
|
||||
if (workspaceFolder) {
|
||||
return this.reloadWorkspaceFolderConfiguration(workspaceFolder, key);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
private deriveConfigurationTarget(key: string, value: any, overrides: IConfigurationOverrides | undefined, target: ConfigurationTarget): ConfigurationTarget | undefined {
|
||||
if (target) {
|
||||
return target;
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
// Ignore. But expected is to remove the value from all targets
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const inspect = this.inspect(key, overrides);
|
||||
if (equals(value, inspect.value)) {
|
||||
// No change. So ignore.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (inspect.workspaceFolderValue !== undefined) {
|
||||
return ConfigurationTarget.WORKSPACE_FOLDER;
|
||||
}
|
||||
|
||||
if (inspect.workspaceValue !== undefined) {
|
||||
return ConfigurationTarget.WORKSPACE;
|
||||
}
|
||||
|
||||
return ConfigurationTarget.USER;
|
||||
}
|
||||
|
||||
private triggerConfigurationChange(change: IConfigurationChange, previous: { data: IConfigurationData, workspace?: Workspace } | undefined, target: ConfigurationTarget): void {
|
||||
if (change.keys.length) {
|
||||
const configurationChangeEvent = new ConfigurationChangeEvent(change, previous, this._configuration, this.workspace);
|
||||
configurationChangeEvent.source = target;
|
||||
configurationChangeEvent.sourceConfig = this.getTargetConfiguration(target);
|
||||
this._onDidChangeConfiguration.fire(configurationChangeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private getTargetConfiguration(target: ConfigurationTarget): any {
|
||||
switch (target) {
|
||||
case ConfigurationTarget.DEFAULT:
|
||||
return this._configuration.defaults.contents;
|
||||
case ConfigurationTarget.USER:
|
||||
return this._configuration.userConfiguration.contents;
|
||||
case ConfigurationTarget.WORKSPACE:
|
||||
return this._configuration.workspaceConfiguration.contents;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private toEditableConfigurationTarget(target: ConfigurationTarget, key: string): EditableConfigurationTarget | null {
|
||||
if (target === ConfigurationTarget.USER) {
|
||||
if (this.inspect(key).userRemoteValue !== undefined) {
|
||||
return EditableConfigurationTarget.USER_REMOTE;
|
||||
}
|
||||
return EditableConfigurationTarget.USER_LOCAL;
|
||||
}
|
||||
if (target === ConfigurationTarget.USER_LOCAL) {
|
||||
return EditableConfigurationTarget.USER_LOCAL;
|
||||
}
|
||||
if (target === ConfigurationTarget.USER_REMOTE) {
|
||||
return EditableConfigurationTarget.USER_REMOTE;
|
||||
}
|
||||
if (target === ConfigurationTarget.WORKSPACE) {
|
||||
return EditableConfigurationTarget.WORKSPACE;
|
||||
}
|
||||
if (target === ConfigurationTarget.WORKSPACE_FOLDER) {
|
||||
return EditableConfigurationTarget.WORKSPACE_FOLDER;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterConfigurationSchemasContribution extends Disposable implements IWorkbenchContribution {
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
||||
) {
|
||||
super();
|
||||
this.registerConfigurationSchemas();
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
this._register(configurationRegistry.onDidUpdateConfiguration(e => this.registerConfigurationSchemas()));
|
||||
this._register(configurationRegistry.onDidSchemaChange(e => this.registerConfigurationSchemas()));
|
||||
}
|
||||
|
||||
private registerConfigurationSchemas(): void {
|
||||
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
|
||||
const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true };
|
||||
const userSettingsSchema: IJSONSchema = this.environmentService.remoteAuthority ? { properties: { ...applicationSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true } : allSettingsSchema;
|
||||
const machineSettingsSchema: IJSONSchema = { properties: { ...machineSettings.properties, ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true };
|
||||
const workspaceSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true };
|
||||
|
||||
jsonRegistry.registerSchema(defaultSettingsSchemaId, {
|
||||
properties: Object.keys(allSettings.properties).reduce<IJSONSchemaMap>((result, key) => { result[key] = { ...allSettings.properties[key], deprecationMessage: undefined }; return result; }, {}),
|
||||
patternProperties: Object.keys(allSettings.patternProperties).reduce<IJSONSchemaMap>((result, key) => { result[key] = { ...allSettings.patternProperties[key], deprecationMessage: undefined }; return result; }, {}),
|
||||
additionalProperties: true,
|
||||
allowTrailingCommas: true,
|
||||
allowComments: true
|
||||
});
|
||||
jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema);
|
||||
jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema);
|
||||
|
||||
if (WorkbenchState.WORKSPACE === this.workspaceContextService.getWorkbenchState()) {
|
||||
const folderSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true };
|
||||
jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema);
|
||||
jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema);
|
||||
} else {
|
||||
jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema);
|
||||
jsonRegistry.registerSchema(folderSettingsSchemaId, workspaceSettingsSchema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(RegisterConfigurationSchemasContribution, LifecyclePhase.Restored);
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
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`;
|
||||
|
||||
export const defaultSettingsSchemaId = 'vscode://schemas/settings/default';
|
||||
export const userSettingsSchemaId = 'vscode://schemas/settings/user';
|
||||
export const machineSettingsSchemaId = 'vscode://schemas/settings/machine';
|
||||
export const workspaceSettingsSchemaId = 'vscode://schemas/settings/workspace';
|
||||
export const folderSettingsSchemaId = 'vscode://schemas/settings/folder';
|
||||
export const launchSchemaId = 'vscode://schemas/launch';
|
||||
export const tasksSchemaId = 'vscode://schemas/tasks';
|
||||
|
||||
export const LOCAL_MACHINE_SCOPES = [ConfigurationScope.APPLICATION, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE];
|
||||
export const REMOTE_MACHINE_SCOPES = [ConfigurationScope.MACHINE, ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE];
|
||||
export const WORKSPACE_SCOPES = [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE];
|
||||
export const FOLDER_SCOPES = [ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE, ConfigurationScope.MACHINE_OVERRIDABLE];
|
||||
|
||||
export const TASKS_CONFIGURATION_KEY = 'tasks';
|
||||
export const LAUNCH_CONFIGURATION_KEY = 'launch';
|
||||
|
||||
export const WORKSPACE_STANDALONE_CONFIGURATIONS = Object.create(null);
|
||||
WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${TASKS_CONFIGURATION_KEY}.json`;
|
||||
WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${LAUNCH_CONFIGURATION_KEY}.json`;
|
||||
export const USER_STANDALONE_CONFIGURATIONS = Object.create(null);
|
||||
USER_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${TASKS_CONFIGURATION_KEY}.json`;
|
||||
|
||||
export type ConfigurationKey = { type: 'user' | 'workspaces' | 'folder', key: string };
|
||||
|
||||
export interface IConfigurationCache {
|
||||
|
||||
needsCaching(resource: URI): boolean;
|
||||
read(key: ConfigurationKey): Promise<string>;
|
||||
write(key: ConfigurationKey, content: string): Promise<void>;
|
||||
remove(key: ConfigurationKey): Promise<void>;
|
||||
|
||||
}
|
||||
|
||||
export const TASKS_DEFAULT = '{\n\t\"version\": \"2.0.0\",\n\t\"tasks\": []\n}';
|
||||
@@ -0,0 +1,621 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import * as json from 'vs/base/common/json';
|
||||
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_STANDALONE_CONFIGURATIONS, TASKS_DEFAULT } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
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';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
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';
|
||||
|
||||
export const enum ConfigurationEditingErrorCode {
|
||||
|
||||
/**
|
||||
* Error when trying to write a configuration key that is not registered.
|
||||
*/
|
||||
ERROR_UNKNOWN_KEY,
|
||||
|
||||
/**
|
||||
* Error when trying to write an application setting into workspace settings.
|
||||
*/
|
||||
ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION,
|
||||
|
||||
/**
|
||||
* Error when trying to write a machne setting into workspace settings.
|
||||
*/
|
||||
ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE,
|
||||
|
||||
/**
|
||||
* Error when trying to write an invalid folder configuration key to folder settings.
|
||||
*/
|
||||
ERROR_INVALID_FOLDER_CONFIGURATION,
|
||||
|
||||
/**
|
||||
* Error when trying to write to user target but not supported for provided key.
|
||||
*/
|
||||
ERROR_INVALID_USER_TARGET,
|
||||
|
||||
/**
|
||||
* Error when trying to write to user target but not supported for provided key.
|
||||
*/
|
||||
ERROR_INVALID_WORKSPACE_TARGET,
|
||||
|
||||
/**
|
||||
* Error when trying to write a configuration key to folder target
|
||||
*/
|
||||
ERROR_INVALID_FOLDER_TARGET,
|
||||
|
||||
/**
|
||||
* Error when trying to write to language specific setting but not supported for preovided key
|
||||
*/
|
||||
ERROR_INVALID_RESOURCE_LANGUAGE_CONFIGURATION,
|
||||
|
||||
/**
|
||||
* Error when trying to write to the workspace configuration without having a workspace opened.
|
||||
*/
|
||||
ERROR_NO_WORKSPACE_OPENED,
|
||||
|
||||
/**
|
||||
* Error when trying to write and save to the configuration file while it is dirty in the editor.
|
||||
*/
|
||||
ERROR_CONFIGURATION_FILE_DIRTY,
|
||||
|
||||
/**
|
||||
* Error when trying to write and save to the configuration file while it is not the latest in the disk.
|
||||
*/
|
||||
ERROR_CONFIGURATION_FILE_MODIFIED_SINCE,
|
||||
|
||||
/**
|
||||
* Error when trying to write to a configuration file that contains JSON errors.
|
||||
*/
|
||||
ERROR_INVALID_CONFIGURATION
|
||||
}
|
||||
|
||||
export class ConfigurationEditingError extends Error {
|
||||
constructor(message: string, public code: ConfigurationEditingErrorCode) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IConfigurationValue {
|
||||
key: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
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`.
|
||||
*/
|
||||
donotNotifyError?: boolean;
|
||||
/**
|
||||
* Scope of configuration to be written into.
|
||||
*/
|
||||
scopes?: IConfigurationOverrides;
|
||||
}
|
||||
|
||||
export const enum EditableConfigurationTarget {
|
||||
USER_LOCAL = 1,
|
||||
USER_REMOTE,
|
||||
WORKSPACE,
|
||||
WORKSPACE_FOLDER
|
||||
}
|
||||
|
||||
interface IConfigurationEditOperation extends IConfigurationValue {
|
||||
target: EditableConfigurationTarget;
|
||||
jsonPath: json.JSONPath;
|
||||
resource?: URI;
|
||||
workspaceStandAloneConfigurationKey?: string;
|
||||
|
||||
}
|
||||
|
||||
interface ConfigurationEditingOptions extends IConfigurationEditingOptions {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export class ConfigurationEditingService {
|
||||
|
||||
public _serviceBrand: undefined;
|
||||
|
||||
private queue: Queue<void>;
|
||||
private remoteSettingsResource: URI | null = null;
|
||||
|
||||
constructor(
|
||||
@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
|
||||
) {
|
||||
this.queue = new Queue<void>();
|
||||
remoteAgentService.getEnvironment().then(environment => {
|
||||
if (environment) {
|
||||
this.remoteSettingsResource = environment.settingsPath;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
writeConfiguration(target: EditableConfigurationTarget, value: IConfigurationValue, options: IConfigurationEditingOptions = {}): Promise<void> {
|
||||
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(() => { },
|
||||
async error => {
|
||||
if (!options.donotNotifyError) {
|
||||
await this.onError(error, operation, options.scopes);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
})));
|
||||
}
|
||||
|
||||
private async doWriteConfiguration(operation: IConfigurationEditOperation, options: ConfigurationEditingOptions): Promise<void> {
|
||||
const checkDirtyConfiguration = !(options.force || options.donotSave);
|
||||
const saveConfiguration = options.force || !options.donotSave;
|
||||
const reference = await this.resolveAndValidate(operation.target, operation, checkDirtyConfiguration, options.scopes || {});
|
||||
try {
|
||||
await this.writeToBuffer(reference.object.textEditorModel, operation, saveConfiguration);
|
||||
} catch (error) {
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE) {
|
||||
await this.textFileService.revert(operation.resource!);
|
||||
return this.reject(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_MODIFIED_SINCE, operation.target, operation);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
reference.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async writeToBuffer(model: ITextModel, operation: IConfigurationEditOperation, save: boolean): Promise<any> {
|
||||
const edit = this.getEdits(model, operation)[0];
|
||||
if (edit && this.applyEditsToBuffer(edit, model) && save) {
|
||||
await this.textFileService.save(operation.resource!, { skipSaveParticipants: true /* programmatic change */, ignoreErrorHandler: true /* handle error self */ });
|
||||
}
|
||||
}
|
||||
|
||||
private applyEditsToBuffer(edit: Edit, model: ITextModel): boolean {
|
||||
const startPosition = model.getPositionAt(edit.offset);
|
||||
const endPosition = model.getPositionAt(edit.offset + edit.length);
|
||||
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
|
||||
let currentText = model.getValueInRange(range);
|
||||
if (edit.content !== currentText) {
|
||||
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
|
||||
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async onError(error: ConfigurationEditingError, operation: IConfigurationEditOperation, scopes: IConfigurationOverrides | undefined): Promise<void> {
|
||||
switch (error.code) {
|
||||
case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION:
|
||||
this.onInvalidConfigurationError(error, operation);
|
||||
break;
|
||||
case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY:
|
||||
this.onConfigurationFileDirtyError(error, operation, scopes);
|
||||
break;
|
||||
case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_MODIFIED_SINCE:
|
||||
return this.doWriteConfiguration(operation, { scopes });
|
||||
default:
|
||||
this.notificationService.error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
private onInvalidConfigurationError(error: ConfigurationEditingError, operation: IConfigurationEditOperation,): void {
|
||||
const openStandAloneConfigurationActionLabel = operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY ? nls.localize('openTasksConfiguration', "Open Tasks Configuration")
|
||||
: operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY ? nls.localize('openLaunchConfiguration', "Open Launch Configuration")
|
||||
: null;
|
||||
if (openStandAloneConfigurationActionLabel) {
|
||||
this.notificationService.prompt(Severity.Error, error.message,
|
||||
[{
|
||||
label: openStandAloneConfigurationActionLabel,
|
||||
run: () => this.openFile(operation.resource!)
|
||||
}]
|
||||
);
|
||||
} else {
|
||||
this.notificationService.prompt(Severity.Error, error.message,
|
||||
[{
|
||||
label: nls.localize('open', "Open Settings"),
|
||||
run: () => this.openSettings(operation)
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private onConfigurationFileDirtyError(error: ConfigurationEditingError, operation: IConfigurationEditOperation, scopes: IConfigurationOverrides | undefined): void {
|
||||
const openStandAloneConfigurationActionLabel = operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY ? nls.localize('openTasksConfiguration', "Open Tasks Configuration")
|
||||
: operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY ? nls.localize('openLaunchConfiguration', "Open Launch Configuration")
|
||||
: null;
|
||||
if (openStandAloneConfigurationActionLabel) {
|
||||
this.notificationService.prompt(Severity.Error, error.message,
|
||||
[{
|
||||
label: nls.localize('saveAndRetry', "Save and Retry"),
|
||||
run: () => {
|
||||
const key = operation.key ? `${operation.workspaceStandAloneConfigurationKey}.${operation.key}` : operation.workspaceStandAloneConfigurationKey!;
|
||||
this.writeConfiguration(operation.target, { key, value: operation.value }, <ConfigurationEditingOptions>{ force: true, scopes });
|
||||
}
|
||||
},
|
||||
{
|
||||
label: openStandAloneConfigurationActionLabel,
|
||||
run: () => this.openFile(operation.resource!)
|
||||
}]
|
||||
);
|
||||
} else {
|
||||
this.notificationService.prompt(Severity.Error, error.message,
|
||||
[{
|
||||
label: nls.localize('saveAndRetry', "Save and Retry"),
|
||||
run: () => this.writeConfiguration(operation.target, { key: operation.key, value: operation.value }, <ConfigurationEditingOptions>{ force: true, scopes })
|
||||
},
|
||||
{
|
||||
label: nls.localize('open', "Open Settings"),
|
||||
run: () => this.openSettings(operation)
|
||||
}]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private openSettings(operation: IConfigurationEditOperation): void {
|
||||
switch (operation.target) {
|
||||
case EditableConfigurationTarget.USER_LOCAL:
|
||||
this.preferencesService.openGlobalSettings(true);
|
||||
break;
|
||||
case EditableConfigurationTarget.USER_REMOTE:
|
||||
this.preferencesService.openRemoteSettings();
|
||||
break;
|
||||
case EditableConfigurationTarget.WORKSPACE:
|
||||
this.preferencesService.openWorkspaceSettings(true);
|
||||
break;
|
||||
case EditableConfigurationTarget.WORKSPACE_FOLDER:
|
||||
if (operation.resource) {
|
||||
const workspaceFolder = this.contextService.getWorkspaceFolder(operation.resource);
|
||||
if (workspaceFolder) {
|
||||
this.preferencesService.openFolderSettings(workspaceFolder.uri, true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private openFile(resource: URI): void {
|
||||
this.editorService.openEditor({ resource });
|
||||
}
|
||||
|
||||
private reject<T = never>(code: ConfigurationEditingErrorCode, target: EditableConfigurationTarget, operation: IConfigurationEditOperation): Promise<T> {
|
||||
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_INVALID_RESOURCE_LANGUAGE_CONFIGURATION: return nls.localize('errorInvalidResourceLanguageConfiguraiton', "Unable to write to Language Settings because {0} is not a resource language setting.", operation.key);
|
||||
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 = '<<unknown>>';
|
||||
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);
|
||||
default:
|
||||
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 = '<<unknown>>';
|
||||
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);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_MODIFIED_SINCE:
|
||||
if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) {
|
||||
return nls.localize('errorTasksConfigurationFileModifiedSince', "Unable to write into tasks configuration file because the content of the file is newer.");
|
||||
}
|
||||
if (operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY) {
|
||||
return nls.localize('errorLaunchConfigurationFileModifiedSince', "Unable to write into launch configuration file because the content of the file is newer.");
|
||||
}
|
||||
switch (target) {
|
||||
case EditableConfigurationTarget.USER_LOCAL:
|
||||
return nls.localize('errorConfigurationFileModifiedSince', "Unable to write into user settings because the content of the file is newer.");
|
||||
case EditableConfigurationTarget.USER_REMOTE:
|
||||
return nls.localize('errorRemoteConfigurationFileModifiedSince', "Unable to write into remote user settings because the content of the file is newer.");
|
||||
case EditableConfigurationTarget.WORKSPACE:
|
||||
return nls.localize('errorConfigurationFileModifiedSinceWorkspace', "Unable to write into workspace settings because the content of the file is newer.");
|
||||
case EditableConfigurationTarget.WORKSPACE_FOLDER:
|
||||
return nls.localize('errorConfigurationFileModifiedSinceFolder', "Unable to write into folder settings because the content of the file is newer.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
default:
|
||||
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 ? ' '.repeat(tabSize) : '\t');
|
||||
return [{
|
||||
content,
|
||||
length: model.getValue().length,
|
||||
offset: 0
|
||||
}];
|
||||
}
|
||||
|
||||
return setProperty(model.getValue(), jsonPath, value, { tabSize, insertSpaces, eol });
|
||||
}
|
||||
|
||||
private defaultResourceValue(resource: URI): string {
|
||||
const basename: string = resources.basename(resource);
|
||||
const configurationValue: string = basename.substr(0, basename.length - resources.extname(resource).length);
|
||||
switch (configurationValue) {
|
||||
case TASKS_CONFIGURATION_KEY: return TASKS_DEFAULT;
|
||||
default: return '{}';
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
|
||||
const exists = await this.fileService.exists(resource);
|
||||
if (!exists) {
|
||||
await this.textFileService.write(resource, this.defaultResourceValue(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, { allowTrailingComma: true, allowEmptyContent: true });
|
||||
return parseErrors.length > 0;
|
||||
}
|
||||
|
||||
private resolveAndValidate(target: EditableConfigurationTarget, operation: IConfigurationEditOperation, checkDirty: boolean, overrides: IConfigurationOverrides): Promise<IReference<IResolvedTextEditorModel>> {
|
||||
|
||||
// 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 launches are not supported
|
||||
if ((operation.workspaceStandAloneConfigurationKey !== TASKS_CONFIGURATION_KEY) && (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE)) {
|
||||
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_USER_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 && !OVERRIDE_PROPERTY_PATTERN.test(operation.key)) {
|
||||
const configurationProperties = Registry.as<IConfigurationRegistry>(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 && !OVERRIDE_PROPERTY_PATTERN.test(operation.key)) {
|
||||
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
|
||||
if (!(configurationProperties[operation.key].scope === ConfigurationScope.RESOURCE || configurationProperties[operation.key].scope === ConfigurationScope.LANGUAGE_OVERRIDABLE)) {
|
||||
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION, target, operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (overrides.overrideIdentifier) {
|
||||
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
|
||||
if (configurationProperties[operation.key].scope !== ConfigurationScope.LANGUAGE_OVERRIDABLE) {
|
||||
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_RESOURCE_LANGUAGE_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)) {
|
||||
reference.dispose();
|
||||
return this.reject<typeof reference>(ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION, target, operation);
|
||||
}
|
||||
|
||||
// Target cannot be dirty if not writing into buffer
|
||||
if (checkDirty && operation.resource && this.textFileService.isDirty(operation.resource)) {
|
||||
reference.dispose();
|
||||
return this.reject<typeof reference>(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) {
|
||||
const standaloneConfigurationMap = target === EditableConfigurationTarget.USER_LOCAL ? USER_STANDALONE_CONFIGURATIONS : WORKSPACE_STANDALONE_CONFIGURATIONS;
|
||||
const standaloneConfigurationKeys = Object.keys(standaloneConfigurationMap);
|
||||
for (const key of standaloneConfigurationKeys) {
|
||||
const resource = this.getConfigurationFileResource(target, standaloneConfigurationMap[key], overrides.resource);
|
||||
|
||||
// Check for prefix
|
||||
if (config.key === key) {
|
||||
const jsonPath = this.isWorkspaceConfigurationResource(resource) ? [key] : [];
|
||||
return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource: withNullAsUndefined(resource), workspaceStandAloneConfigurationKey: key, target };
|
||||
}
|
||||
|
||||
// Check for prefix.<setting>
|
||||
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 { 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 || target === EditableConfigurationTarget.USER_REMOTE) {
|
||||
return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, '', null)), target };
|
||||
}
|
||||
|
||||
const resource = this.getConfigurationFileResource(target, FOLDER_SETTINGS_PATH, overrides.resource);
|
||||
if (this.isWorkspaceConfigurationResource(resource)) {
|
||||
jsonPath = ['settings', ...jsonPath];
|
||||
}
|
||||
return { key, jsonPath, value: config.value, resource: withNullAsUndefined(resource), target };
|
||||
}
|
||||
|
||||
private isWorkspaceConfigurationResource(resource: URI | null): boolean {
|
||||
const workspace = this.contextService.getWorkspace();
|
||||
return !!(workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath);
|
||||
}
|
||||
|
||||
private getConfigurationFileResource(target: EditableConfigurationTarget, relativePath: string, resource: URI | null | undefined): URI | null {
|
||||
if (target === EditableConfigurationTarget.USER_LOCAL) {
|
||||
if (relativePath) {
|
||||
return resources.joinPath(resources.dirname(this.environmentService.settingsResource), relativePath);
|
||||
} else {
|
||||
return this.environmentService.settingsResource;
|
||||
}
|
||||
}
|
||||
if (target === EditableConfigurationTarget.USER_REMOTE) {
|
||||
return this.remoteSettingsResource;
|
||||
}
|
||||
const workbenchState = this.contextService.getWorkbenchState();
|
||||
if (workbenchState !== WorkbenchState.EMPTY) {
|
||||
|
||||
const workspace = this.contextService.getWorkspace();
|
||||
|
||||
if (target === EditableConfigurationTarget.WORKSPACE) {
|
||||
if (workbenchState === WorkbenchState.WORKSPACE) {
|
||||
return withUndefinedAsNull(workspace.configuration);
|
||||
}
|
||||
if (workbenchState === WorkbenchState.FOLDER) {
|
||||
return workspace.folders[0].toResource(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (target === EditableConfigurationTarget.WORKSPACE_FOLDER) {
|
||||
if (resource) {
|
||||
const folder = this.contextService.getWorkspaceFolder(resource);
|
||||
if (folder) {
|
||||
return folder.toResource(relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { equals } from 'vs/base/common/objects';
|
||||
import { toValuesTree, IConfigurationModel, IConfigurationOverrides, IConfigurationValue, IConfigurationChange } from 'vs/platform/configuration/common/configuration';
|
||||
import { Configuration as BaseConfiguration, ConfigurationModelParser, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
|
||||
import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { Workspace } from 'vs/platform/workspace/common/workspace';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { OVERRIDE_PROPERTY_PATTERN, overrideIdentifierFromKey } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
|
||||
export class WorkspaceConfigurationModelParser extends ConfigurationModelParser {
|
||||
|
||||
private _folders: IStoredWorkspaceFolder[] = [];
|
||||
private _settingsModelParser: ConfigurationModelParser;
|
||||
private _launchModel: ConfigurationModel;
|
||||
private _tasksModel: ConfigurationModel;
|
||||
|
||||
constructor(name: string) {
|
||||
super(name);
|
||||
this._settingsModelParser = new ConfigurationModelParser(name, WORKSPACE_SCOPES);
|
||||
this._launchModel = new ConfigurationModel();
|
||||
this._tasksModel = new ConfigurationModel();
|
||||
}
|
||||
|
||||
get folders(): IStoredWorkspaceFolder[] {
|
||||
return this._folders;
|
||||
}
|
||||
|
||||
get settingsModel(): ConfigurationModel {
|
||||
return this._settingsModelParser.configurationModel;
|
||||
}
|
||||
|
||||
get launchModel(): ConfigurationModel {
|
||||
return this._launchModel;
|
||||
}
|
||||
|
||||
get tasksModel(): ConfigurationModel {
|
||||
return this._tasksModel;
|
||||
}
|
||||
|
||||
reprocessWorkspaceSettings(): void {
|
||||
this._settingsModelParser.parse();
|
||||
}
|
||||
|
||||
protected doParseRaw(raw: any): IConfigurationModel {
|
||||
this._folders = (raw['folders'] || []) as IStoredWorkspaceFolder[];
|
||||
this._settingsModelParser.parseRaw(raw['settings']);
|
||||
this._launchModel = this.createConfigurationModelFrom(raw, 'launch');
|
||||
this._tasksModel = this.createConfigurationModelFrom(raw, 'tasks');
|
||||
return super.doParseRaw(raw);
|
||||
}
|
||||
|
||||
private createConfigurationModelFrom(raw: any, key: string): ConfigurationModel {
|
||||
const data = raw[key];
|
||||
if (data) {
|
||||
const contents = toValuesTree(data, message => console.error(`Conflict in settings file ${this._name}: ${message}`));
|
||||
const scopedContents = Object.create(null);
|
||||
scopedContents[key] = contents;
|
||||
const keys = Object.keys(data).map(k => `${key}.${k}`);
|
||||
return new ConfigurationModel(scopedContents, keys, []);
|
||||
}
|
||||
return new ConfigurationModel();
|
||||
}
|
||||
}
|
||||
|
||||
export class StandaloneConfigurationModelParser extends ConfigurationModelParser {
|
||||
|
||||
constructor(name: string, private readonly scope: string) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
protected doParseRaw(raw: any): IConfigurationModel {
|
||||
const contents = toValuesTree(raw, message => console.error(`Conflict in settings file ${this._name}: ${message}`));
|
||||
const scopedContents = Object.create(null);
|
||||
scopedContents[this.scope] = contents;
|
||||
const keys = Object.keys(raw).map(key => `${this.scope}.${key}`);
|
||||
return { contents: scopedContents, keys, overrides: [] };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Configuration extends BaseConfiguration {
|
||||
|
||||
constructor(
|
||||
defaults: ConfigurationModel,
|
||||
localUser: ConfigurationModel,
|
||||
remoteUser: ConfigurationModel,
|
||||
workspaceConfiguration: ConfigurationModel,
|
||||
folders: ResourceMap<ConfigurationModel>,
|
||||
memoryConfiguration: ConfigurationModel,
|
||||
memoryConfigurationByResource: ResourceMap<ConfigurationModel>,
|
||||
private readonly _workspace?: Workspace) {
|
||||
super(defaults, localUser, remoteUser, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource);
|
||||
}
|
||||
|
||||
getValue(key: string | undefined, overrides: IConfigurationOverrides = {}): any {
|
||||
return super.getValue(key, overrides, this._workspace);
|
||||
}
|
||||
|
||||
inspect<C>(key: string, overrides: IConfigurationOverrides = {}): IConfigurationValue<C> {
|
||||
return super.inspect(key, overrides, this._workspace);
|
||||
}
|
||||
|
||||
keys(): {
|
||||
default: string[];
|
||||
user: string[];
|
||||
workspace: string[];
|
||||
workspaceFolder: string[];
|
||||
} {
|
||||
return super.keys(this._workspace);
|
||||
}
|
||||
|
||||
compareAndDeleteFolderConfiguration(folder: URI): IConfigurationChange {
|
||||
if (this._workspace && this._workspace.folders.length > 0 && this._workspace.folders[0].uri.toString() === folder.toString()) {
|
||||
// Do not remove workspace configuration
|
||||
return { keys: [], overrides: [] };
|
||||
}
|
||||
return super.compareAndDeleteFolderConfiguration(folder);
|
||||
}
|
||||
|
||||
compare(other: Configuration): IConfigurationChange {
|
||||
const compare = (fromKeys: string[], toKeys: string[], overrideIdentifier?: string): string[] => {
|
||||
const keys: string[] = [];
|
||||
keys.push(...toKeys.filter(key => fromKeys.indexOf(key) === -1));
|
||||
keys.push(...fromKeys.filter(key => toKeys.indexOf(key) === -1));
|
||||
keys.push(...fromKeys.filter(key => {
|
||||
// Ignore if the key does not exist in both models
|
||||
if (toKeys.indexOf(key) === -1) {
|
||||
return false;
|
||||
}
|
||||
// Compare workspace value
|
||||
if (!equals(this.getValue(key, { overrideIdentifier }), other.getValue(key, { overrideIdentifier }))) {
|
||||
return true;
|
||||
}
|
||||
// Compare workspace folder value
|
||||
return this._workspace && this._workspace.folders.some(folder => !equals(this.getValue(key, { resource: folder.uri, overrideIdentifier }), other.getValue(key, { resource: folder.uri, overrideIdentifier })));
|
||||
}));
|
||||
return keys;
|
||||
};
|
||||
const keys = compare(this.allKeys(), other.allKeys());
|
||||
const overrides: [string, string[]][] = [];
|
||||
for (const key of keys) {
|
||||
if (OVERRIDE_PROPERTY_PATTERN.test(key)) {
|
||||
const overrideIdentifier = overrideIdentifierFromKey(key);
|
||||
overrides.push([overrideIdentifier, compare(this.getAllKeysForOverrideIdentifier(overrideIdentifier), other.getAllKeysForOverrideIdentifier(overrideIdentifier), overrideIdentifier)]);
|
||||
}
|
||||
}
|
||||
return { keys, overrides };
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { JSONPath } from 'vs/base/common/json';
|
||||
|
||||
export const IJSONEditingService = createDecorator<IJSONEditingService>('jsonEditingService');
|
||||
|
||||
export const enum JSONEditingErrorCode {
|
||||
|
||||
/**
|
||||
* Error when trying to write and save to the file while it is dirty in the editor.
|
||||
*/
|
||||
ERROR_FILE_DIRTY,
|
||||
|
||||
/**
|
||||
* Error when trying to write to a file that contains JSON errors.
|
||||
*/
|
||||
ERROR_INVALID_FILE
|
||||
}
|
||||
|
||||
export class JSONEditingError extends Error {
|
||||
constructor(message: string, public code: JSONEditingErrorCode) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IJSONValue {
|
||||
path: JSONPath;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface IJSONEditingService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
write(resource: URI, values: IJSONValue[], save: boolean): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as json from 'vs/base/common/json';
|
||||
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 { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { IJSONEditingService, IJSONValue, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
|
||||
export class JSONEditingService implements IJSONEditingService {
|
||||
|
||||
public _serviceBrand: undefined;
|
||||
|
||||
private queue: Queue<void>;
|
||||
|
||||
constructor(
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@ITextModelService private readonly textModelResolverService: ITextModelService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService
|
||||
) {
|
||||
this.queue = new Queue<void>();
|
||||
}
|
||||
|
||||
write(resource: URI, values: IJSONValue[], save: boolean): Promise<void> {
|
||||
return Promise.resolve(this.queue.queue(() => this.doWriteConfiguration(resource, values, save))); // queue up writes to prevent race conditions
|
||||
}
|
||||
|
||||
private async doWriteConfiguration(resource: URI, values: IJSONValue[], save: boolean): Promise<void> {
|
||||
const reference = await this.resolveAndValidate(resource, save);
|
||||
try {
|
||||
await this.writeToBuffer(reference.object.textEditorModel, values, save);
|
||||
} finally {
|
||||
reference.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async writeToBuffer(model: ITextModel, values: IJSONValue[], save: boolean): Promise<any> {
|
||||
let hasEdits: boolean = false;
|
||||
for (const value of values) {
|
||||
const edit = this.getEdits(model, value)[0];
|
||||
hasEdits = this.applyEditsToBuffer(edit, model);
|
||||
}
|
||||
if (hasEdits && save) {
|
||||
return this.textFileService.save(model.uri);
|
||||
}
|
||||
}
|
||||
|
||||
private applyEditsToBuffer(edit: Edit, model: ITextModel): boolean {
|
||||
const startPosition = model.getPositionAt(edit.offset);
|
||||
const endPosition = model.getPositionAt(edit.offset + edit.length);
|
||||
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
|
||||
let currentText = model.getValueInRange(range);
|
||||
if (edit.content !== currentText) {
|
||||
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
|
||||
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private getEdits(model: ITextModel, configurationValue: IJSONValue): Edit[] {
|
||||
const { tabSize, insertSpaces } = model.getOptions();
|
||||
const eol = model.getEOL();
|
||||
const { path, value } = configurationValue;
|
||||
|
||||
// With empty path the entire file is being replaced, so we just use JSON.stringify
|
||||
if (!path.length) {
|
||||
const content = JSON.stringify(value, null, insertSpaces ? ' '.repeat(tabSize) : '\t');
|
||||
return [{
|
||||
content,
|
||||
length: content.length,
|
||||
offset: 0
|
||||
}];
|
||||
}
|
||||
|
||||
return setProperty(model.getValue(), path, value, { tabSize, insertSpaces, eol });
|
||||
}
|
||||
|
||||
private async resolveModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
|
||||
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): boolean {
|
||||
const parseErrors: json.ParseError[] = [];
|
||||
json.parse(model.getValue(), parseErrors, { allowTrailingComma: true, allowEmptyContent: true });
|
||||
return parseErrors.length > 0;
|
||||
}
|
||||
|
||||
private async resolveAndValidate(resource: URI, checkDirty: boolean): Promise<IReference<IResolvedTextEditorModel>> {
|
||||
const reference = await this.resolveModelReference(resource);
|
||||
|
||||
const model = reference.object.textEditorModel;
|
||||
|
||||
if (this.hasParseErrors(model)) {
|
||||
reference.dispose();
|
||||
return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_INVALID_FILE);
|
||||
}
|
||||
|
||||
// Target cannot be dirty if not writing into buffer
|
||||
if (checkDirty && this.textFileService.isDirty(resource)) {
|
||||
reference.dispose();
|
||||
return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_FILE_DIRTY);
|
||||
}
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
private reject<T>(code: JSONEditingErrorCode): Promise<T> {
|
||||
const message = this.toErrorMessage(code);
|
||||
return Promise.reject(new JSONEditingError(message, code));
|
||||
}
|
||||
|
||||
private toErrorMessage(error: JSONEditingErrorCode): string {
|
||||
switch (error) {
|
||||
// User issues
|
||||
case JSONEditingErrorCode.ERROR_INVALID_FILE: {
|
||||
return nls.localize('errorInvalidFile', "Unable to write into the file. Please open the file to correct errors/warnings in the file and try again.");
|
||||
}
|
||||
case JSONEditingErrorCode.ERROR_FILE_DIRTY: {
|
||||
return nls.localize('errorFileDirty', "Unable to write into the file because the file is dirty. Please save the file and try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IJSONEditingService, JSONEditingService, true);
|
||||
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService';
|
||||
import { IConfigurationCache, ConfigurationKey } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
export class ConfigurationCache implements IConfigurationCache {
|
||||
|
||||
private readonly cachedConfigurations: Map<string, CachedConfiguration> = new Map<string, CachedConfiguration>();
|
||||
|
||||
constructor(private readonly environmentService: INativeWorkbenchEnvironmentService) {
|
||||
}
|
||||
|
||||
needsCaching(resource: URI): boolean {
|
||||
// Cache all non native resources
|
||||
return ![Schemas.file, Schemas.userData].includes(resource.scheme);
|
||||
}
|
||||
|
||||
read(key: ConfigurationKey): Promise<string> {
|
||||
return this.getCachedConfiguration(key).read();
|
||||
}
|
||||
|
||||
write(key: ConfigurationKey, content: string): Promise<void> {
|
||||
return this.getCachedConfiguration(key).save(content);
|
||||
}
|
||||
|
||||
remove(key: ConfigurationKey): Promise<void> {
|
||||
return this.getCachedConfiguration(key).remove();
|
||||
}
|
||||
|
||||
private getCachedConfiguration({ type, key }: ConfigurationKey): CachedConfiguration {
|
||||
const k = `${type}:${key}`;
|
||||
let cachedConfiguration = this.cachedConfigurations.get(k);
|
||||
if (!cachedConfiguration) {
|
||||
cachedConfiguration = new CachedConfiguration({ type, key }, this.environmentService);
|
||||
this.cachedConfigurations.set(k, cachedConfiguration);
|
||||
}
|
||||
return cachedConfiguration;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class CachedConfiguration {
|
||||
|
||||
private cachedConfigurationFolderPath: string;
|
||||
private cachedConfigurationFilePath: string;
|
||||
|
||||
constructor(
|
||||
{ type, key }: ConfigurationKey,
|
||||
environmentService: INativeWorkbenchEnvironmentService
|
||||
) {
|
||||
this.cachedConfigurationFolderPath = join(environmentService.userDataPath, 'CachedConfigurations', type, key);
|
||||
this.cachedConfigurationFilePath = join(this.cachedConfigurationFolderPath, type === 'workspaces' ? 'workspace.json' : 'configuration.json');
|
||||
}
|
||||
|
||||
async read(): Promise<string> {
|
||||
try {
|
||||
const content = await pfs.readFile(this.cachedConfigurationFilePath);
|
||||
return content.toString();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async save(content: string): Promise<void> {
|
||||
const created = await this.createCachedFolder();
|
||||
if (created) {
|
||||
await pfs.writeFile(this.cachedConfigurationFilePath, content);
|
||||
}
|
||||
}
|
||||
|
||||
remove(): Promise<void> {
|
||||
return pfs.rimraf(this.cachedConfigurationFolderPath);
|
||||
}
|
||||
|
||||
private createCachedFolder(): Promise<boolean> {
|
||||
return Promise.resolve(pfs.exists(this.cachedConfigurationFolderPath))
|
||||
.then(undefined, () => false)
|
||||
.then(exists => exists ? exists : pfs.mkdirp(this.cachedConfigurationFolderPath).then(() => true, () => false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { StandaloneConfigurationModelParser, Configuration } from 'vs/workbench/services/configuration/common/configurationModels';
|
||||
import { ConfigurationModelParser, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
suite('FolderSettingsModelParser', () => {
|
||||
|
||||
suiteSetup(() => {
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'FolderSettingsModelParser_1',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'FolderSettingsModelParser.window': {
|
||||
'type': 'string',
|
||||
'default': 'isSet'
|
||||
},
|
||||
'FolderSettingsModelParser.resource': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.RESOURCE,
|
||||
},
|
||||
'FolderSettingsModelParser.resourceLanguage': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.LANGUAGE_OVERRIDABLE,
|
||||
},
|
||||
'FolderSettingsModelParser.application': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
},
|
||||
'FolderSettingsModelParser.machine': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.MACHINE
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('parse all folder settings', () => {
|
||||
const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]);
|
||||
|
||||
testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'application', 'FolderSettingsModelParser.machine': 'executable' }));
|
||||
|
||||
assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'window': 'window', 'resource': 'resource' } });
|
||||
});
|
||||
|
||||
test('parse resource folder settings', () => {
|
||||
const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE]);
|
||||
|
||||
testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'application', 'FolderSettingsModelParser.machine': 'executable' }));
|
||||
|
||||
assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } });
|
||||
});
|
||||
|
||||
test('parse resource and resource language settings', () => {
|
||||
const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.LANGUAGE_OVERRIDABLE]);
|
||||
|
||||
testObject.parseContent(JSON.stringify({ '[json]': { 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.resourceLanguage': 'resourceLanguage', 'FolderSettingsModelParser.application': 'application', 'FolderSettingsModelParser.machine': 'executable' } }));
|
||||
|
||||
assert.deepEqual(testObject.configurationModel.overrides, [{ 'contents': { 'FolderSettingsModelParser': { 'resource': 'resource', 'resourceLanguage': 'resourceLanguage' } }, 'identifiers': ['json'], 'keys': ['FolderSettingsModelParser.resource', 'FolderSettingsModelParser.resourceLanguage'] }]);
|
||||
});
|
||||
|
||||
test('reprocess folder settings excludes application and machine setting', () => {
|
||||
const testObject = new ConfigurationModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]);
|
||||
|
||||
testObject.parseContent(JSON.stringify({ 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.anotherApplicationSetting': 'executable' }));
|
||||
|
||||
assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource', 'anotherApplicationSetting': 'executable' } });
|
||||
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'FolderSettingsModelParser_2',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'FolderSettingsModelParser.anotherApplicationSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.APPLICATION
|
||||
},
|
||||
'FolderSettingsModelParser.anotherMachineSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet',
|
||||
scope: ConfigurationScope.MACHINE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testObject.parse();
|
||||
assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('StandaloneConfigurationModelParser', () => {
|
||||
|
||||
test('parse tasks stand alone configuration model', () => {
|
||||
const testObject = new StandaloneConfigurationModelParser('tasks', 'tasks');
|
||||
|
||||
testObject.parseContent(JSON.stringify({ 'version': '1.1.1', 'tasks': [] }));
|
||||
|
||||
assert.deepEqual(testObject.configurationModel.contents, { 'tasks': { 'version': '1.1.1', 'tasks': [] } });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('Workspace Configuration', () => {
|
||||
|
||||
const defaultConfigurationModel = toConfigurationModel({
|
||||
'editor.lineNumbers': 'on',
|
||||
'editor.fontSize': 12,
|
||||
'window.zoomLevel': 1,
|
||||
'[markdown]': {
|
||||
'editor.wordWrap': 'off'
|
||||
},
|
||||
'window.title': 'custom',
|
||||
'workbench.enableTabs': false,
|
||||
'editor.insertSpaces': true
|
||||
});
|
||||
|
||||
test('Test compare same configurations', () => {
|
||||
const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]);
|
||||
const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace);
|
||||
configuration1.updateDefaultConfiguration(defaultConfigurationModel);
|
||||
configuration1.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } }));
|
||||
configuration1.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' }));
|
||||
configuration1.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.fontSize': 14 }));
|
||||
configuration1.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'editor.wordWrap': 'on' }));
|
||||
|
||||
const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace);
|
||||
configuration2.updateDefaultConfiguration(defaultConfigurationModel);
|
||||
configuration2.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } }));
|
||||
configuration2.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' }));
|
||||
configuration2.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.fontSize': 14 }));
|
||||
configuration2.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'editor.wordWrap': 'on' }));
|
||||
|
||||
const actual = configuration2.compare(configuration1);
|
||||
|
||||
assert.deepEqual(actual, { keys: [], overrides: [] });
|
||||
});
|
||||
|
||||
test('Test compare different configurations', () => {
|
||||
const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]);
|
||||
const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace);
|
||||
configuration1.updateDefaultConfiguration(defaultConfigurationModel);
|
||||
configuration1.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } }));
|
||||
configuration1.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' }));
|
||||
configuration1.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.fontSize': 14 }));
|
||||
configuration1.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'editor.wordWrap': 'on' }));
|
||||
|
||||
const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), workspace);
|
||||
configuration2.updateDefaultConfiguration(defaultConfigurationModel);
|
||||
configuration2.updateLocalUserConfiguration(toConfigurationModel({ 'workbench.enableTabs': true, '[typescript]': { 'editor.insertSpaces': true } }));
|
||||
configuration2.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.fontSize': 11 }));
|
||||
configuration2.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.insertSpaces': true }));
|
||||
configuration2.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({
|
||||
'[markdown]': {
|
||||
'editor.wordWrap': 'on',
|
||||
'editor.lineNumbers': 'relative'
|
||||
},
|
||||
}));
|
||||
|
||||
const actual = configuration2.compare(configuration1);
|
||||
|
||||
assert.deepEqual(actual, { keys: ['editor.wordWrap', 'editor.fontSize', '[markdown]', 'window.title', 'workbench.enableTabs', '[typescript]'], overrides: [['markdown', ['editor.lineNumbers', 'editor.wordWrap']], ['typescript', ['editor.insertSpaces']]] });
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
function toConfigurationModel(obj: any): ConfigurationModel {
|
||||
const parser = new ConfigurationModelParser('test');
|
||||
parser.parseContent(JSON.stringify(obj));
|
||||
return parser.configurationModel;
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as sinon from 'sinon';
|
||||
import * as assert from 'assert';
|
||||
import * as os from 'os';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as fs from 'fs';
|
||||
import * as json from 'vs/base/common/json';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { TestProductService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { TestWorkbenchConfiguration, TestTextFileService } from 'vs/workbench/test/electron-browser/workbenchTestServices';
|
||||
import * as uuid from 'vs/base/common/uuid';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
|
||||
import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService';
|
||||
import { WORKSPACE_STANDALONE_CONFIGURATIONS, FOLDER_SETTINGS_PATH, USER_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService';
|
||||
import { mkdirp, rimraf, RimRafMode } from 'vs/base/node/pfs';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { CommandService } from 'vs/workbench/services/commands/common/commandService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createHash } from 'crypto';
|
||||
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ConfigurationCache } from 'vs/workbench/services/configuration/electron-browser/configurationCache';
|
||||
import { KeybindingsEditingService, IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing';
|
||||
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
|
||||
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
|
||||
|
||||
class TestWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService {
|
||||
|
||||
constructor(private _appSettingsHome: URI) {
|
||||
super(TestWorkbenchConfiguration, TestProductService);
|
||||
}
|
||||
|
||||
get appSettingsHome() { return this._appSettingsHome; }
|
||||
}
|
||||
|
||||
suite('ConfigurationEditingService', () => {
|
||||
|
||||
let instantiationService: TestInstantiationService;
|
||||
let testObject: ConfigurationEditingService;
|
||||
let parentDir: string;
|
||||
let workspaceDir: string;
|
||||
let globalSettingsFile: string;
|
||||
let globalTasksFile: string;
|
||||
let workspaceSettingsDir;
|
||||
|
||||
suiteSetup(() => {
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': '_test',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'configurationEditing.service.testSetting': {
|
||||
'type': 'string',
|
||||
'default': 'isSet'
|
||||
},
|
||||
'configurationEditing.service.testSettingTwo': {
|
||||
'type': 'string',
|
||||
'default': 'isSet'
|
||||
},
|
||||
'configurationEditing.service.testSettingThree': {
|
||||
'type': 'string',
|
||||
'default': 'isSet'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setup(() => {
|
||||
return setUpWorkspace()
|
||||
.then(() => setUpServices());
|
||||
});
|
||||
|
||||
async function setUpWorkspace(): Promise<void> {
|
||||
const id = uuid.generateUuid();
|
||||
parentDir = path.join(os.tmpdir(), 'vsctests', id);
|
||||
workspaceDir = path.join(parentDir, 'workspaceconfig', id);
|
||||
globalSettingsFile = path.join(workspaceDir, 'settings.json');
|
||||
globalTasksFile = path.join(workspaceDir, 'tasks.json');
|
||||
workspaceSettingsDir = path.join(workspaceDir, '.vscode');
|
||||
|
||||
return await mkdirp(workspaceSettingsDir, 493);
|
||||
}
|
||||
|
||||
function setUpServices(noWorkspace: boolean = false): Promise<void> {
|
||||
// Clear services if they are already created
|
||||
clearServices();
|
||||
|
||||
instantiationService = <TestInstantiationService>workbenchInstantiationService();
|
||||
const environmentService = new TestWorkbenchEnvironmentService(URI.file(workspaceDir));
|
||||
instantiationService.stub(IEnvironmentService, environmentService);
|
||||
const remoteAgentService = instantiationService.createInstance(RemoteAgentService);
|
||||
const fileService = new FileService(new NullLogService());
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, undefined, diskFileSystemProvider, environmentService, new NullLogService()));
|
||||
instantiationService.stub(IFileService, fileService);
|
||||
instantiationService.stub(IRemoteAgentService, remoteAgentService);
|
||||
const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService, new NullLogService());
|
||||
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);
|
||||
instantiationService.stub(IKeybindingEditingService, instantiationService.createInstance(KeybindingsEditingService));
|
||||
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
|
||||
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
|
||||
instantiationService.stub(ICommandService, CommandService);
|
||||
testObject = instantiationService.createInstance(ConfigurationEditingService);
|
||||
});
|
||||
}
|
||||
|
||||
teardown(() => {
|
||||
clearServices();
|
||||
if (workspaceDir) {
|
||||
return rimraf(workspaceDir, RimRafMode.MOVE);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
function clearServices(): void {
|
||||
if (instantiationService) {
|
||||
const configuraitonService = <WorkspaceService>instantiationService.get(IConfigurationService);
|
||||
if (configuraitonService) {
|
||||
configuraitonService.dispose();
|
||||
}
|
||||
instantiationService = null!;
|
||||
}
|
||||
}
|
||||
|
||||
test('errors cases - invalid key', () => {
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'unknown.key', value: 'value' })
|
||||
.then(() => assert.fail('Should fail with ERROR_UNKNOWN_KEY'),
|
||||
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY));
|
||||
});
|
||||
|
||||
test('errors cases - no workspace', () => {
|
||||
return setUpServices(true)
|
||||
.then(() => testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'configurationEditing.service.testSetting', value: 'value' }))
|
||||
.then(() => assert.fail('Should fail with ERROR_NO_WORKSPACE_OPENED'),
|
||||
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED));
|
||||
});
|
||||
|
||||
function errorCasesInvalidConfig(file: string, key: string) {
|
||||
fs.writeFileSync(file, ',,,,,,,,,,,,,,');
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key, value: 'value' })
|
||||
.then(() => assert.fail('Should fail with ERROR_INVALID_CONFIGURATION'),
|
||||
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION));
|
||||
}
|
||||
|
||||
test('errors cases - invalid configuration', () => {
|
||||
return errorCasesInvalidConfig(globalSettingsFile, 'configurationEditing.service.testSetting');
|
||||
});
|
||||
|
||||
test('errors cases - invalid global tasks configuration', () => {
|
||||
return errorCasesInvalidConfig(globalTasksFile, 'tasks.configurationEditing.service.testSetting');
|
||||
});
|
||||
|
||||
test('errors cases - dirty', () => {
|
||||
instantiationService.stub(ITextFileService, 'isDirty', true);
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' })
|
||||
.then(() => assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'),
|
||||
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY));
|
||||
});
|
||||
|
||||
test('dirty error is not thrown if not asked to save', () => {
|
||||
instantiationService.stub(ITextFileService, 'isDirty', true);
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotSave: true })
|
||||
.then(() => null, error => assert.fail('Should not fail.'));
|
||||
});
|
||||
|
||||
test('do not notify error', () => {
|
||||
instantiationService.stub(ITextFileService, 'isDirty', true);
|
||||
const target = sinon.stub();
|
||||
instantiationService.stub(INotificationService, <INotificationService>{ prompt: target, _serviceBrand: undefined, notify: null!, error: null!, info: null!, warn: null!, status: null!, setFilter: null! });
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotNotifyError: true })
|
||||
.then(() => assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'),
|
||||
(error: ConfigurationEditingError) => {
|
||||
assert.equal(false, target.calledOnce);
|
||||
assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY);
|
||||
});
|
||||
});
|
||||
|
||||
test('write one setting - empty file', () => {
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' })
|
||||
.then(() => {
|
||||
const contents = fs.readFileSync(globalSettingsFile).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
assert.equal(parsed['configurationEditing.service.testSetting'], 'value');
|
||||
});
|
||||
});
|
||||
|
||||
test('write one setting - existing file', () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "my.super.setting": "my.super.value" }');
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' })
|
||||
.then(() => {
|
||||
const contents = fs.readFileSync(globalSettingsFile).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
assert.equal(parsed['configurationEditing.service.testSetting'], 'value');
|
||||
assert.equal(parsed['my.super.setting'], 'my.super.value');
|
||||
});
|
||||
});
|
||||
|
||||
test('remove an existing setting - existing file', () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "my.super.setting": "my.super.value", "configurationEditing.service.testSetting": "value" }');
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: undefined })
|
||||
.then(() => {
|
||||
const contents = fs.readFileSync(globalSettingsFile).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
assert.deepEqual(Object.keys(parsed), ['my.super.setting']);
|
||||
assert.equal(parsed['my.super.setting'], 'my.super.value');
|
||||
});
|
||||
});
|
||||
|
||||
test('remove non existing setting - existing file', () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "my.super.setting": "my.super.value" }');
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: undefined })
|
||||
.then(() => {
|
||||
const contents = fs.readFileSync(globalSettingsFile).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
assert.deepEqual(Object.keys(parsed), ['my.super.setting']);
|
||||
assert.equal(parsed['my.super.setting'], 'my.super.value');
|
||||
});
|
||||
});
|
||||
|
||||
test('write overridable settings to user settings', () => {
|
||||
const key = '[language]';
|
||||
const value = { 'configurationEditing.service.testSetting': 'overridden value' };
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key, value })
|
||||
.then(() => {
|
||||
const contents = fs.readFileSync(globalSettingsFile).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
assert.deepEqual(parsed[key], value);
|
||||
});
|
||||
});
|
||||
|
||||
test('write overridable settings to workspace settings', () => {
|
||||
const key = '[language]';
|
||||
const value = { 'configurationEditing.service.testSetting': 'overridden value' };
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key, value })
|
||||
.then(() => {
|
||||
const target = path.join(workspaceDir, FOLDER_SETTINGS_PATH);
|
||||
const contents = fs.readFileSync(target).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
assert.deepEqual(parsed[key], value);
|
||||
});
|
||||
});
|
||||
|
||||
test('write overridable settings to workspace folder settings', () => {
|
||||
const key = '[language]';
|
||||
const value = { 'configurationEditing.service.testSetting': 'overridden value' };
|
||||
const folderSettingsFile = path.join(workspaceDir, FOLDER_SETTINGS_PATH);
|
||||
return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE_FOLDER, { key, value }, { scopes: { resource: URI.file(folderSettingsFile) } })
|
||||
.then(() => {
|
||||
const contents = fs.readFileSync(folderSettingsFile).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
assert.deepEqual(parsed[key], value);
|
||||
});
|
||||
});
|
||||
|
||||
function writeStandaloneSettingEmptyFile(configTarget: EditableConfigurationTarget, pathMap: any) {
|
||||
return testObject.writeConfiguration(configTarget, { key: 'tasks.service.testSetting', value: 'value' })
|
||||
.then(() => {
|
||||
const target = path.join(workspaceDir, pathMap['tasks']);
|
||||
const contents = fs.readFileSync(target).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
assert.equal(parsed['service.testSetting'], 'value');
|
||||
});
|
||||
}
|
||||
|
||||
test('write workspace standalone setting - empty file', () => {
|
||||
return writeStandaloneSettingEmptyFile(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
test('write user standalone setting - empty file', () => {
|
||||
return writeStandaloneSettingEmptyFile(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
function writeStandaloneSettingExitingFile(configTarget: EditableConfigurationTarget, pathMap: any) {
|
||||
const target = path.join(workspaceDir, pathMap['tasks']);
|
||||
fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }');
|
||||
return testObject.writeConfiguration(configTarget, { key: 'tasks.service.testSetting', value: 'value' })
|
||||
.then(() => {
|
||||
const contents = fs.readFileSync(target).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
assert.equal(parsed['service.testSetting'], 'value');
|
||||
assert.equal(parsed['my.super.setting'], 'my.super.value');
|
||||
});
|
||||
}
|
||||
|
||||
test('write workspace standalone setting - existing file', () => {
|
||||
return writeStandaloneSettingExitingFile(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
test('write user standalone setting - existing file', () => {
|
||||
return writeStandaloneSettingExitingFile(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
function writeStandaloneSettingEmptyFileFullJson(configTarget: EditableConfigurationTarget, pathMap: any) {
|
||||
return testObject.writeConfiguration(configTarget, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } })
|
||||
.then(() => {
|
||||
const target = path.join(workspaceDir, pathMap['tasks']);
|
||||
const contents = fs.readFileSync(target).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
|
||||
assert.equal(parsed['version'], '1.0.0');
|
||||
assert.equal(parsed['tasks'][0]['taskName'], 'myTask');
|
||||
});
|
||||
}
|
||||
|
||||
test('write workspace standalone setting - empty file - full JSON', () => {
|
||||
return writeStandaloneSettingEmptyFileFullJson(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
test('write user standalone setting - empty file - full JSON', () => {
|
||||
return writeStandaloneSettingEmptyFileFullJson(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
function writeStandaloneSettingExistingFileFullJson(configTarget: EditableConfigurationTarget, pathMap: any) {
|
||||
const target = path.join(workspaceDir, pathMap['tasks']);
|
||||
fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }');
|
||||
return testObject.writeConfiguration(configTarget, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } })
|
||||
.then(() => {
|
||||
const contents = fs.readFileSync(target).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
|
||||
assert.equal(parsed['version'], '1.0.0');
|
||||
assert.equal(parsed['tasks'][0]['taskName'], 'myTask');
|
||||
});
|
||||
}
|
||||
|
||||
test('write workspace standalone setting - existing file - full JSON', () => {
|
||||
return writeStandaloneSettingExistingFileFullJson(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
test('write user standalone setting - existing file - full JSON', () => {
|
||||
return writeStandaloneSettingExistingFileFullJson(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
function writeStandaloneSettingExistingFileWithJsonErrorFullJson(configTarget: EditableConfigurationTarget, pathMap: any) {
|
||||
const target = path.join(workspaceDir, pathMap['tasks']);
|
||||
fs.writeFileSync(target, '{ "my.super.setting": '); // invalid JSON
|
||||
return testObject.writeConfiguration(configTarget, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } })
|
||||
.then(() => {
|
||||
const contents = fs.readFileSync(target).toString('utf8');
|
||||
const parsed = json.parse(contents);
|
||||
|
||||
assert.equal(parsed['version'], '1.0.0');
|
||||
assert.equal(parsed['tasks'][0]['taskName'], 'myTask');
|
||||
});
|
||||
}
|
||||
|
||||
test('write workspace standalone setting - existing file with JSON errors - full JSON', () => {
|
||||
return writeStandaloneSettingExistingFileWithJsonErrorFullJson(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
test('write user standalone setting - existing file with JSON errors - full JSON', () => {
|
||||
return writeStandaloneSettingExistingFileWithJsonErrorFullJson(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
function writeStandaloneSettingShouldReplace(configTarget: EditableConfigurationTarget, pathMap: any) {
|
||||
const target = path.join(workspaceDir, pathMap['tasks']);
|
||||
fs.writeFileSync(target, `{
|
||||
"version": "1.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"taskName": "myTask1"
|
||||
},
|
||||
{
|
||||
"taskName": "myTask2"
|
||||
}
|
||||
]
|
||||
}`);
|
||||
return testObject.writeConfiguration(configTarget, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask1' }] } })
|
||||
.then(() => {
|
||||
const actual = fs.readFileSync(target).toString('utf8');
|
||||
const expected = JSON.stringify({ 'version': '1.0.0', tasks: [{ 'taskName': 'myTask1' }] }, null, '\t');
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
}
|
||||
|
||||
test('write workspace standalone setting should replace complete file', () => {
|
||||
return writeStandaloneSettingShouldReplace(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
|
||||
test('write user standalone setting should replace complete file', () => {
|
||||
return writeStandaloneSettingShouldReplace(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user