fix reading configuration from workspace folders (#299302)

* fix reading configuration from workspace folders

* feedback
This commit is contained in:
Sandeep Somavarapu
2026-03-05 07:59:20 +01:00
committed by GitHub
parent bcd6b6b1df
commit 5ee6f4f532
6 changed files with 710 additions and 14 deletions

View File

@@ -2015,6 +2015,7 @@ export default tseslint.config(
'vs/editor/contrib/*/~',
'vs/workbench/~',
'vs/workbench/services/*/~',
'vs/sessions/services/*/~',
{
'when': 'test',
'pattern': 'vs/workbench/contrib/*/~'

View File

@@ -352,14 +352,15 @@ export class SessionsMain extends Disposable {
logService: ILogService,
policyService: IPolicyService
): Promise<{ configurationService: ConfigurationService; workspaceContextService: SessionsWorkspaceContextService }> {
const configurationService = new ConfigurationService(userDataProfileService.currentProfile.settingsResource, fileService, policyService, logService);
const workspaceContextService = new SessionsWorkspaceContextService(workspaceIdentifier, uriIdentityService);
const configurationService = new ConfigurationService(userDataProfileService, workspaceContextService, uriIdentityService, fileService, policyService, logService);
try {
await configurationService.initialize();
} catch (error) {
onUnexpectedError(error);
}
const workspaceContextService = new SessionsWorkspaceContextService(workspaceIdentifier, uriIdentityService, configurationService);
workspaceContextService.setConfigurationService(configurationService);
return { configurationService, workspaceContextService };
}

View File

@@ -3,25 +3,375 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../../base/common/event.js';
import { Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
import { ConfigurationService as BaseConfigurationService } from '../../../../platform/configuration/common/configurationService.js';
import { onUnexpectedError } from '../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../base/common/map.js';
import { URI } from '../../../../base/common/uri.js';
import { Queue } from '../../../../base/common/async.js';
import { VSBuffer } from '../../../../base/common/buffer.js';
import { JSONPath, ParseError, parse } from '../../../../base/common/json.js';
import { applyEdits, setProperty } from '../../../../base/common/jsonEdit.js';
import { Edit, FormattingOptions } from '../../../../base/common/jsonFormatter.js';
import { equals } from '../../../../base/common/objects.js';
import { distinct, equals as arrayEquals } from '../../../../base/common/arrays.js';
import { OS, OperatingSystem } from '../../../../base/common/platform.js';
import { IConfigurationChange, IConfigurationChangeEvent, IConfigurationData, IConfigurationOverrides, IConfigurationUpdateOptions, IConfigurationUpdateOverrides, IConfigurationValue, ConfigurationTarget, isConfigurationOverrides, isConfigurationUpdateOverrides } from '../../../../platform/configuration/common/configuration.js';
import { ConfigurationChangeEvent, ConfigurationModel } from '../../../../platform/configuration/common/configurationModels.js';
import { DefaultConfiguration, IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from '../../../../platform/configuration/common/configurations.js';
import { Extensions, IConfigurationRegistry, keyFromOverrideIdentifiers } from '../../../../platform/configuration/common/configurationRegistry.js';
import { IFileService, FileOperationError, FileOperationResult } from '../../../../platform/files/common/files.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IPolicyService, NullPolicyService } from '../../../../platform/policy/common/policy.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, IWorkbenchConfigurationService, RestrictedSettings } from '../../../../workbench/services/configuration/common/configuration.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent, IWorkspaceFolder, WorkbenchState, Workspace } from '../../../../platform/workspace/common/workspace.js';
import { FolderConfiguration, UserConfiguration } from '../../../../workbench/services/configuration/browser/configuration.js';
import { APPLICATION_SCOPES, APPLY_ALL_PROFILES_SETTING, FOLDER_CONFIG_FOLDER_NAME, FOLDER_SETTINGS_PATH, IWorkbenchConfigurationService, RestrictedSettings } from '../../../../workbench/services/configuration/common/configuration.js';
import { Configuration } from '../../../../workbench/services/configuration/common/configurationModels.js';
import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js';
// Import to register contributions
// Import to register configuration contributions
import '../../../../workbench/services/configuration/browser/configurationService.js';
export class ConfigurationService extends BaseConfigurationService implements IWorkbenchConfigurationService {
readonly restrictedSettings: RestrictedSettings = { default: [] };
export class ConfigurationService extends Disposable implements IWorkbenchConfigurationService {
declare readonly _serviceBrand: undefined;
private _configuration: Configuration;
private readonly defaultConfiguration: DefaultConfiguration;
private readonly policyConfiguration: IPolicyConfiguration;
private readonly userConfiguration: UserConfiguration;
private readonly cachedFolderConfigs = this._register(new DisposableMap<URI, FolderConfiguration>(new ResourceMap()));
private readonly _onDidChangeConfiguration = this._register(new Emitter<IConfigurationChangeEvent>());
readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event;
readonly onDidChangeRestrictedSettings = Event.None;
readonly restrictedSettings: RestrictedSettings = { default: [] };
private readonly configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
private readonly settingsResource: URI;
private readonly configurationEditing: ConfigurationEditing;
constructor(
userDataProfileService: IUserDataProfileService,
private readonly workspaceService: IWorkspaceContextService,
private readonly uriIdentityService: IUriIdentityService,
private readonly fileService: IFileService,
policyService: IPolicyService,
private readonly logService: ILogService,
) {
super();
this.settingsResource = userDataProfileService.currentProfile.settingsResource;
this.defaultConfiguration = this._register(new DefaultConfiguration(logService));
this.policyConfiguration = policyService instanceof NullPolicyService ? new NullPolicyConfiguration() : this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService));
this.userConfiguration = this._register(new UserConfiguration(userDataProfileService.currentProfile.settingsResource, userDataProfileService.currentProfile.tasksResource, userDataProfileService.currentProfile.mcpResource, {}, fileService, uriIdentityService, logService));
this.configurationEditing = new ConfigurationEditing(fileService, this);
this._configuration = new Configuration(
ConfigurationModel.createEmptyModel(logService),
ConfigurationModel.createEmptyModel(logService),
ConfigurationModel.createEmptyModel(logService),
ConfigurationModel.createEmptyModel(logService),
ConfigurationModel.createEmptyModel(logService),
ConfigurationModel.createEmptyModel(logService),
new ResourceMap(),
ConfigurationModel.createEmptyModel(logService),
new ResourceMap<ConfigurationModel>(),
this.workspaceService.getWorkspace() as Workspace,
this.logService
);
this._register(this.defaultConfiguration.onDidChangeConfiguration(({ defaults, properties }) => this.onDefaultConfigurationChanged(defaults, properties)));
this._register(this.policyConfiguration.onDidChangeConfiguration(configurationModel => this.onPolicyConfigurationChanged(configurationModel)));
this._register(this.userConfiguration.onDidChangeConfiguration(userConfiguration => this.onUserConfigurationChanged(userConfiguration)));
this._register(this.workspaceService.onWillChangeWorkspaceFolders(e => e.join(this.loadFolderConfigurations(e.changes.added))));
this._register(this.workspaceService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e)));
}
async initialize(): Promise<void> {
const [defaultModel, policyModel, userModel] = await Promise.all([
this.defaultConfiguration.initialize(),
this.policyConfiguration.initialize(),
this.userConfiguration.initialize()
]);
const workspace = this.workspaceService.getWorkspace() as Workspace;
this._configuration = new Configuration(
defaultModel,
policyModel,
ConfigurationModel.createEmptyModel(this.logService),
userModel,
ConfigurationModel.createEmptyModel(this.logService),
ConfigurationModel.createEmptyModel(this.logService),
new ResourceMap(),
ConfigurationModel.createEmptyModel(this.logService),
new ResourceMap<ConfigurationModel>(),
workspace,
this.logService
);
await this.loadFolderConfigurations(workspace.folders);
}
// #region IWorkbenchConfigurationService
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?: unknown, arg2?: unknown): unknown {
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: unknown): Promise<void>;
updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides): Promise<void>;
updateValue(key: string, value: unknown, target: ConfigurationTarget): Promise<void>;
updateValue(key: string, value: unknown, overrides: IConfigurationOverrides | IConfigurationUpdateOverrides, target: ConfigurationTarget, options?: IConfigurationUpdateOptions): Promise<void>;
async updateValue(key: string, value: unknown, arg3?: unknown, arg4?: unknown, _options?: IConfigurationUpdateOptions): Promise<void> {
const overrides: IConfigurationUpdateOverrides | undefined = isConfigurationUpdateOverrides(arg3) ? arg3
: isConfigurationOverrides(arg3) ? { resource: arg3.resource, overrideIdentifiers: arg3.overrideIdentifier ? [arg3.overrideIdentifier] : undefined } : undefined;
const target: ConfigurationTarget | undefined = (overrides ? arg4 : arg3) as ConfigurationTarget | undefined;
if (overrides?.overrideIdentifiers) {
overrides.overrideIdentifiers = distinct(overrides.overrideIdentifiers);
overrides.overrideIdentifiers = overrides.overrideIdentifiers.length ? overrides.overrideIdentifiers : undefined;
}
const inspect = this.inspect(key, { resource: overrides?.resource, overrideIdentifier: overrides?.overrideIdentifiers ? overrides.overrideIdentifiers[0] : undefined });
if (inspect.policyValue !== undefined) {
throw new Error(`Unable to write ${key} because it is configured in system policy.`);
}
// Remove the setting, if the value is same as default value
if (equals(value, inspect.defaultValue)) {
value = undefined;
}
if (overrides?.overrideIdentifiers?.length && overrides.overrideIdentifiers.length > 1) {
const overrideIdentifiers = overrides.overrideIdentifiers.sort();
const existingOverrides = this._configuration.localUserConfiguration.overrides.find(override => arrayEquals([...override.identifiers].sort(), overrideIdentifiers));
if (existingOverrides) {
overrides.overrideIdentifiers = existingOverrides.identifiers;
}
}
const path = overrides?.overrideIdentifiers?.length ? [keyFromOverrideIdentifiers(overrides.overrideIdentifiers), key] : [key];
const settingsResource = this.getSettingsResource(target, overrides?.resource ?? undefined);
await this.configurationEditing.write(settingsResource, path, value);
await this.reloadConfiguration();
}
private getSettingsResource(target: ConfigurationTarget | undefined, resource: URI | undefined): URI {
if (target === ConfigurationTarget.WORKSPACE_FOLDER || target === ConfigurationTarget.WORKSPACE) {
if (resource) {
const folder = this.workspaceService.getWorkspaceFolder(resource);
if (folder) {
return this.uriIdentityService.extUri.joinPath(folder.uri, FOLDER_SETTINGS_PATH);
}
}
}
return this.settingsResource;
}
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> {
return this._configuration.inspect<T>(key, overrides);
}
keys(): { default: string[]; policy: string[]; user: string[]; workspace: string[]; workspaceFolder: string[] } {
return this._configuration.keys();
}
async reloadConfiguration(_target?: ConfigurationTarget | IWorkspaceFolder): Promise<void> {
const userModel = await this.userConfiguration.initialize();
const previousData = this._configuration.toData();
const change = this._configuration.compareAndUpdateLocalUserConfiguration(userModel);
// Reload folder configurations
for (const folder of this.workspaceService.getWorkspace().folders) {
const folderConfiguration = this.cachedFolderConfigs.get(folder.uri);
if (folderConfiguration) {
const folderModel = await folderConfiguration.loadConfiguration();
const folderChange = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderModel);
change.keys.push(...folderChange.keys);
change.overrides.push(...folderChange.overrides);
}
}
this.triggerConfigurationChange(change, previousData, ConfigurationTarget.USER);
}
hasCachedConfigurationDefaultsOverrides(): boolean {
return false;
}
async whenRemoteConfigurationLoaded(): Promise<void> { }
isSettingAppliedForAllProfiles(key: string): boolean {
const scope = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties()[key]?.scope;
const scope = this.configurationRegistry.getConfigurationProperties()[key]?.scope;
if (scope && APPLICATION_SCOPES.includes(scope)) {
return true;
}
const allProfilesSettings = this.getValue<string[]>(APPLY_ALL_PROFILES_SETTING) ?? [];
return Array.isArray(allProfilesSettings) && allProfilesSettings.includes(key);
}
// #endregion
// #region Configuration change handlers
private onDefaultConfigurationChanged(defaults: ConfigurationModel, properties?: string[]): void {
const previousData = this._configuration.toData();
const change = this._configuration.compareAndUpdateDefaultConfiguration(defaults, properties);
this._configuration.updateLocalUserConfiguration(this.userConfiguration.reparse());
for (const folder of this.workspaceService.getWorkspace().folders) {
const folderConfiguration = this.cachedFolderConfigs.get(folder.uri);
if (folderConfiguration) {
this._configuration.updateFolderConfiguration(folder.uri, folderConfiguration.reparse());
}
}
this.triggerConfigurationChange(change, previousData, ConfigurationTarget.DEFAULT);
}
private onPolicyConfigurationChanged(policyConfiguration: ConfigurationModel): void {
const previousData = this._configuration.toData();
const change = this._configuration.compareAndUpdatePolicyConfiguration(policyConfiguration);
this.triggerConfigurationChange(change, previousData, ConfigurationTarget.DEFAULT);
}
private onUserConfigurationChanged(userConfiguration: ConfigurationModel): void {
const previousData = this._configuration.toData();
const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration);
this.triggerConfigurationChange(change, previousData, ConfigurationTarget.USER);
}
private onWorkspaceFoldersChanged(e: IWorkspaceFoldersChangeEvent): void {
// Remove configurations for removed folders
const previousData = this._configuration.toData();
const keys: string[] = [];
const overrides: [string, string[]][] = [];
for (const folder of e.removed) {
const change = this._configuration.compareAndDeleteFolderConfiguration(folder.uri);
keys.push(...change.keys);
overrides.push(...change.overrides);
this.cachedFolderConfigs.deleteAndDispose(folder.uri);
}
if (keys.length || overrides.length) {
this.triggerConfigurationChange({ keys, overrides }, previousData, ConfigurationTarget.WORKSPACE_FOLDER);
}
}
private onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder): void {
const folderConfiguration = this.cachedFolderConfigs.get(folder.uri);
if (folderConfiguration) {
folderConfiguration.loadConfiguration().then(configurationModel => {
const previousData = this._configuration.toData();
const change = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, configurationModel);
this.triggerConfigurationChange(change, previousData, ConfigurationTarget.WORKSPACE_FOLDER);
}, onUnexpectedError);
}
}
private async loadFolderConfigurations(folders: readonly IWorkspaceFolder[]): Promise<void> {
for (const folder of folders) {
let folderConfiguration = this.cachedFolderConfigs.get(folder.uri);
if (!folderConfiguration) {
folderConfiguration = new FolderConfiguration(false, folder, FOLDER_CONFIG_FOLDER_NAME, WorkbenchState.WORKSPACE, true, this.fileService, this.uriIdentityService, this.logService, { needsCaching: () => false, read: async () => '', write: async () => { }, remove: async () => { } });
folderConfiguration.addRelated(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder)));
this.cachedFolderConfigs.set(folder.uri, folderConfiguration);
}
const configurationModel = await folderConfiguration.loadConfiguration();
this._configuration.updateFolderConfiguration(folder.uri, configurationModel);
}
}
private triggerConfigurationChange(change: IConfigurationChange, previousData: IConfigurationData, target: ConfigurationTarget): void {
if (change.keys.length) {
const workspace = this.workspaceService.getWorkspace() as Workspace;
const event = new ConfigurationChangeEvent(change, { data: previousData, workspace }, this._configuration, workspace, this.logService);
event.source = target;
this._onDidChangeConfiguration.fire(event);
}
}
// #endregion
}
class ConfigurationEditing {
private readonly queue = new Queue<void>();
constructor(
private readonly fileService: IFileService,
private readonly configurationService: ConfigurationService,
) { }
write(settingsResource: URI, path: JSONPath, value: unknown): Promise<void> {
return this.queue.queue(() => this.doWriteConfiguration(settingsResource, path, value));
}
private async doWriteConfiguration(settingsResource: URI, path: JSONPath, value: unknown): Promise<void> {
let content: string;
try {
const fileContent = await this.fileService.readFile(settingsResource);
content = fileContent.value.toString();
} catch (error) {
if ((error as FileOperationError).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
content = '{}';
} else {
throw error;
}
}
const parseErrors: ParseError[] = [];
parse(content, parseErrors, { allowTrailingComma: true, allowEmptyContent: true });
if (parseErrors.length > 0) {
throw new Error('Unable to write into the settings file. Please open the file to correct errors/warnings in the file and try again.');
}
const edits = this.getEdits(content, path, value);
content = applyEdits(content, edits);
await this.fileService.writeFile(settingsResource, VSBuffer.fromString(content));
}
private getEdits(content: string, path: JSONPath, value: unknown): Edit[] {
const { tabSize, insertSpaces, eol } = this.formattingOptions;
if (!path.length) {
const newContent = JSON.stringify(value, null, insertSpaces ? ' '.repeat(tabSize) : '\t');
return [{
content: newContent,
length: content.length,
offset: 0
}];
}
return setProperty(content, path, value, { tabSize, insertSpaces, eol });
}
private _formattingOptions: Required<FormattingOptions> | undefined;
private get formattingOptions(): Required<FormattingOptions> {
if (!this._formattingOptions) {
let eol = OS === OperatingSystem.Linux || OS === OperatingSystem.Macintosh ? '\n' : '\r\n';
const configuredEol = this.configurationService.getValue<string>('files.eol', { overrideIdentifier: 'jsonc' });
if (configuredEol && typeof configuredEol === 'string' && configuredEol !== 'auto') {
eol = configuredEol;
}
this._formattingOptions = {
eol,
insertSpaces: !!this.configurationService.getValue('editor.insertSpaces', { overrideIdentifier: 'jsonc' }),
tabSize: this.configurationService.getValue('editor.tabSize', { overrideIdentifier: 'jsonc' })
};
}
return this._formattingOptions;
}
}

View File

@@ -0,0 +1,339 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { URI } from '../../../../../base/common/uri.js';
import { Registry } from '../../../../../platform/registry/common/platform.js';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../../platform/configuration/common/configurationRegistry.js';
import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js';
import { FileService } from '../../../../../platform/files/common/fileService.js';
import { NullLogService } from '../../../../../platform/log/common/log.js';
import { NullPolicyService } from '../../../../../platform/policy/common/policy.js';
import { VSBuffer } from '../../../../../base/common/buffer.js';
import { UriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentityService.js';
import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js';
import { joinPath } from '../../../../../base/common/resources.js';
import { Schemas } from '../../../../../base/common/network.js';
import { UserDataProfilesService } from '../../../../../platform/userDataProfile/common/userDataProfile.js';
import { UserDataProfileService } from '../../../../../workbench/services/userDataProfile/common/userDataProfileService.js';
import { FileUserDataProvider } from '../../../../../platform/userData/common/fileUserDataProvider.js';
import { TestEnvironmentService } from '../../../../../workbench/test/browser/workbenchTestServices.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
import { ConfigurationService } from '../../browser/configurationService.js';
import { SessionsWorkspaceContextService } from '../../../workspace/browser/workspaceContextService.js';
import { getWorkspaceIdentifier } from '../../../../../workbench/services/workspaces/browser/workspaces.js';
import { Event } from '../../../../../base/common/event.js';
import { IUserDataProfileService } from '../../../../../workbench/services/userDataProfile/common/userDataProfile.js';
const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' });
suite('Sessions ConfigurationService', () => {
let testObject: ConfigurationService;
let workspaceService: SessionsWorkspaceContextService;
let fileService: FileService;
let userDataProfileService: IUserDataProfileService;
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
suiteSetup(() => {
configurationRegistry.registerConfiguration({
'id': '_test_sessions',
'type': 'object',
'properties': {
'sessionsConfigurationService.testSetting': {
'type': 'string',
'default': 'defaultValue',
scope: ConfigurationScope.RESOURCE
},
'sessionsConfigurationService.machineSetting': {
'type': 'string',
'default': 'defaultValue',
scope: ConfigurationScope.MACHINE
},
'sessionsConfigurationService.applicationSetting': {
'type': 'string',
'default': 'defaultValue',
scope: ConfigurationScope.APPLICATION
},
}
});
});
setup(async () => {
const logService = new NullLogService();
fileService = disposables.add(new FileService(logService));
const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
disposables.add(fileService.registerProvider(ROOT.scheme, fileSystemProvider));
const environmentService = TestEnvironmentService;
const uriIdentityService = disposables.add(new UriIdentityService(fileService));
const userDataProfilesService = disposables.add(new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService));
disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, userDataProfilesService, uriIdentityService, logService))));
userDataProfileService = disposables.add(new UserDataProfileService(userDataProfilesService.defaultProfile));
const configResource = joinPath(ROOT, 'agent-sessions.code-workspace');
await fileService.writeFile(configResource, VSBuffer.fromString(JSON.stringify({ folders: [] })));
workspaceService = disposables.add(new SessionsWorkspaceContextService(getWorkspaceIdentifier(configResource), uriIdentityService));
testObject = disposables.add(new ConfigurationService(userDataProfileService, workspaceService, uriIdentityService, fileService, new NullPolicyService(), logService));
await testObject.initialize();
});
// #region Reading
test('defaults', () => {
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'defaultValue');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.machineSetting'), 'defaultValue');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.applicationSetting'), 'defaultValue');
});
test('user settings override defaults', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "userValue" }'));
await testObject.reloadConfiguration();
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'userValue');
}));
test('workspace folder settings override user settings', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'myFolder');
await fileService.createFolder(folder);
await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "userValue" }'));
await testObject.reloadConfiguration();
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }'));
await workspaceService.addFolders([{ uri: folder }]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue');
}));
test('folder settings are read when folders are added', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'addedFolder');
await fileService.createFolder(folder);
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }'));
await workspaceService.addFolders([{ uri: folder }]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue');
}));
test('folder settings are removed when folders are removed', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'removedFolder');
await fileService.createFolder(folder);
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }'));
await workspaceService.addFolders([{ uri: folder }]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue');
await workspaceService.removeFolders([folder]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'defaultValue');
}));
test('configuration change event is fired when folders with settings are removed', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'removedFolder2');
await fileService.createFolder(folder);
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }'));
await workspaceService.addFolders([{ uri: folder }]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue');
const promise = Event.toPromise(testObject.onDidChangeConfiguration);
await workspaceService.removeFolders([folder]);
const event = await promise;
assert.ok(event.affectsConfiguration('sessionsConfigurationService.testSetting'));
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'defaultValue');
}));
test('configuration change event is fired on user settings change', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const promise = Event.toPromise(testObject.onDidChangeConfiguration);
await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "userValue" }'));
await testObject.reloadConfiguration();
const event = await promise;
assert.ok(event.affectsConfiguration('sessionsConfigurationService.testSetting'));
}));
test('inspect returns correct values per layer', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'inspectFolder');
await fileService.createFolder(folder);
await fileService.writeFile(userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "userValue" }'));
await testObject.reloadConfiguration();
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }'));
await workspaceService.addFolders([{ uri: folder }]);
const inspection = testObject.inspect('sessionsConfigurationService.testSetting', { resource: folder });
assert.strictEqual(inspection.defaultValue, 'defaultValue');
assert.strictEqual(inspection.userValue, 'userValue');
assert.strictEqual(inspection.workspaceFolderValue, 'folderValue');
}));
test('application settings are not read from workspace folder', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'appFolder');
await fileService.createFolder(folder);
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.applicationSetting": "folderValue" }'));
await workspaceService.addFolders([{ uri: folder }]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.applicationSetting', { resource: folder }), 'defaultValue');
}));
test('machine settings are not read from workspace folder', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'machineFolder');
await fileService.createFolder(folder);
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.machineSetting": "folderValue" }'));
await workspaceService.addFolders([{ uri: folder }]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.machineSetting', { resource: folder }), 'defaultValue');
}));
test('folder settings change fires configuration change event', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'changeFolder');
await fileService.createFolder(folder);
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "initialValue" }'));
await workspaceService.addFolders([{ uri: folder }]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'initialValue');
const promise = Event.toPromise(testObject.onDidChangeConfiguration);
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "updatedValue" }'));
const event = await promise;
assert.ok(event.affectsConfiguration('sessionsConfigurationService.testSetting'));
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'updatedValue');
}));
// #endregion
// #region Writing
test('updateValue writes to user settings', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
await testObject.updateValue('sessionsConfigurationService.testSetting', 'writtenValue');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'writtenValue');
}));
test('updateValue persists to settings file', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
await testObject.updateValue('sessionsConfigurationService.testSetting', 'persistedValue');
const content = (await fileService.readFile(userDataProfileService.currentProfile.settingsResource)).value.toString();
assert.ok(content.includes('"sessionsConfigurationService.testSetting"'));
assert.ok(content.includes('persistedValue'));
}));
test('updateValue fires change event', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const promise = Event.toPromise(testObject.onDidChangeConfiguration);
await testObject.updateValue('sessionsConfigurationService.testSetting', 'eventValue');
const event = await promise;
assert.ok(event.affectsConfiguration('sessionsConfigurationService.testSetting'));
}));
test('updateValue removes setting when value equals default', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
await testObject.updateValue('sessionsConfigurationService.testSetting', 'nonDefault');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'nonDefault');
await testObject.updateValue('sessionsConfigurationService.testSetting', 'defaultValue');
const content = (await fileService.readFile(userDataProfileService.currentProfile.settingsResource)).value.toString();
assert.ok(!content.includes('sessionsConfigurationService.testSetting'));
}));
test('updateValue can update multiple settings', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
await testObject.updateValue('sessionsConfigurationService.testSetting', 'value1');
await testObject.updateValue('sessionsConfigurationService.machineSetting', 'value2');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'value1');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.machineSetting'), 'value2');
}));
test('updateValue with language override', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
await testObject.updateValue('sessionsConfigurationService.testSetting', 'langValue', { overrideIdentifier: 'jsonc' });
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { overrideIdentifier: 'jsonc' }), 'langValue');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'defaultValue');
}));
test('updateValue is reflected in inspect', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
await testObject.updateValue('sessionsConfigurationService.testSetting', 'inspectedValue');
const inspection = testObject.inspect('sessionsConfigurationService.testSetting');
assert.strictEqual(inspection.defaultValue, 'defaultValue');
assert.strictEqual(inspection.userValue, 'inspectedValue');
}));
// #endregion
// #region Workspace Folder - Read and Write
test('read setting from workspace folder', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'readFolder');
await fileService.createFolder(folder);
await fileService.writeFile(joinPath(folder, '.vscode', 'settings.json'), VSBuffer.fromString('{ "sessionsConfigurationService.testSetting": "folderValue" }'));
await workspaceService.addFolders([{ uri: folder }]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'defaultValue');
}));
test('write setting to workspace folder', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'writeFolder');
await fileService.createFolder(folder);
await workspaceService.addFolders([{ uri: folder }]);
await testObject.updateValue('sessionsConfigurationService.testSetting', 'writtenFolderValue', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'writtenFolderValue');
}));
test('write setting to workspace folder persists to folder settings file', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'persistFolder');
await fileService.createFolder(folder);
await workspaceService.addFolders([{ uri: folder }]);
await testObject.updateValue('sessionsConfigurationService.testSetting', 'persistedFolderValue', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER);
const content = (await fileService.readFile(joinPath(folder, '.vscode', 'settings.json'))).value.toString();
assert.ok(content.includes('"sessionsConfigurationService.testSetting"'));
assert.ok(content.includes('persistedFolderValue'));
}));
test('write setting to workspace folder does not affect user settings', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'isolateFolder');
await fileService.createFolder(folder);
await workspaceService.addFolders([{ uri: folder }]);
await testObject.updateValue('sessionsConfigurationService.testSetting', 'folderOnly', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderOnly');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'defaultValue');
}));
test('workspace folder setting overrides user setting for resource', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'overrideFolder');
await fileService.createFolder(folder);
await workspaceService.addFolders([{ uri: folder }]);
await testObject.updateValue('sessionsConfigurationService.testSetting', 'userValue');
await testObject.updateValue('sessionsConfigurationService.testSetting', 'folderValue', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue');
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting'), 'userValue');
}));
test('inspect shows workspace folder value after write', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'inspectWriteFolder');
await fileService.createFolder(folder);
await workspaceService.addFolders([{ uri: folder }]);
await testObject.updateValue('sessionsConfigurationService.testSetting', 'userVal');
await testObject.updateValue('sessionsConfigurationService.testSetting', 'folderVal', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER);
const inspection = testObject.inspect('sessionsConfigurationService.testSetting', { resource: folder });
assert.strictEqual(inspection.defaultValue, 'defaultValue');
assert.strictEqual(inspection.userValue, 'userVal');
assert.strictEqual(inspection.workspaceFolderValue, 'folderVal');
}));
test('removing folder clears its written settings', () => runWithFakedTimers<void>({ useFakeTimers: true }, async () => {
const folder = joinPath(ROOT, 'clearFolder');
await fileService.createFolder(folder);
await workspaceService.addFolders([{ uri: folder }]);
await testObject.updateValue('sessionsConfigurationService.testSetting', 'folderValue', { resource: folder }, ConfigurationTarget.WORKSPACE_FOLDER);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'folderValue');
await workspaceService.removeFolders([folder]);
assert.strictEqual(testObject.getValue('sessionsConfigurationService.testSetting', { resource: folder }), 'defaultValue');
}));
// #endregion
});

View File

@@ -35,7 +35,6 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork
constructor(
workspaceIdentifier: IWorkspaceIdentifier,
private readonly uriIdentityService: IUriIdentityService,
private readonly configurationService: IConfigurationService,
) {
super();
this.workspace = new Workspace(workspaceIdentifier.id, [], false, workspaceIdentifier.configPath, uri => uriIdentityService.extUri.ignorePathCasing(uri));
@@ -53,8 +52,13 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork
return WorkbenchState.WORKSPACE;
}
private _configurationService: IConfigurationService | undefined;
setConfigurationService(configurationService: IConfigurationService) {
this._configurationService = configurationService;
}
hasWorkspaceData(): boolean {
return this.configurationService.getValue('sessions.workspace.sendWorkspaceDataToExtHost') === true;
return this._configurationService?.getValue('sessions.workspace.sendWorkspaceDataToExtHost') === true;
}
getWorkspaceFolder(resource: URI): IWorkspaceFolder | null {
@@ -159,7 +163,8 @@ export class SessionsWorkspaceContextService extends Disposable implements IWork
// Update workspace
const workspaceIdentifier = getWorkspaceIdentifier(this.workspace.configuration!);
this.workspace = new Workspace(workspaceIdentifier.id, newFolders, false, workspaceIdentifier.configPath, uri => this.uriIdentityService.extUri.ignorePathCasing(uri));
const workspace = new Workspace(workspaceIdentifier.id, newFolders, false, workspaceIdentifier.configPath, uri => this.uriIdentityService.extUri.ignorePathCasing(uri));
this.workspace.update(workspace);
// Fire did change event
this._onDidChangeWorkspaceFolders.fire(changes);

View File

@@ -363,7 +363,7 @@ export class SettingsTargetsWidget extends Widget {
private async update(): Promise<void> {
this.settingsSwitcherBar.domNode.classList.toggle('empty-workbench', this.contextService.getWorkbenchState() === WorkbenchState.EMPTY);
this.userRemoteSettings.enabled = !!(this.options.enableRemoteSettings && this.environmentService.remoteAuthority);
this.workspaceSettings.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY;
this.workspaceSettings.enabled = this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && !this.environmentService.isSessionsWindow;
this.folderSettings.action.enabled = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && this.contextService.getWorkspace().folders.length > 0;
this.workspaceSettings.tooltip = localize('workspaceSettings', "Workspace");