Path: blob/main/src/vs/workbench/api/common/extHostConfiguration.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { mixin, deepClone } from '../../../base/common/objects.js';6import { Event, Emitter } from '../../../base/common/event.js';7import type * as vscode from 'vscode';8import { ExtHostWorkspace, IExtHostWorkspace } from './extHostWorkspace.js';9import { ExtHostConfigurationShape, MainThreadConfigurationShape, IConfigurationInitData, MainContext } from './extHost.protocol.js';10import { ConfigurationTarget as ExtHostConfigurationTarget } from './extHostTypes.js';11import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from '../../../platform/configuration/common/configuration.js';12import { Configuration, ConfigurationChangeEvent } from '../../../platform/configuration/common/configurationModels.js';13import { ConfigurationScope, OVERRIDE_PROPERTY_REGEX } from '../../../platform/configuration/common/configurationRegistry.js';14import { isObject } from '../../../base/common/types.js';15import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js';16import { Barrier } from '../../../base/common/async.js';17import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';18import { IExtHostRpcService } from './extHostRpcService.js';19import { ILogService } from '../../../platform/log/common/log.js';20import { Workspace } from '../../../platform/workspace/common/workspace.js';21import { URI } from '../../../base/common/uri.js';2223function lookUp(tree: any, key: string) {24if (key) {25const parts = key.split('.');26let node = tree;27for (let i = 0; node && i < parts.length; i++) {28node = node[parts[i]];29}30return node;31}32}3334export type ConfigurationInspect<T> = {35key: string;3637defaultValue?: T;38globalLocalValue?: T;39globalRemoteValue?: T;40globalValue?: T;41workspaceValue?: T;42workspaceFolderValue?: T;4344defaultLanguageValue?: T;45globalLocalLanguageValue?: T;46globalRemoteLanguageValue?: T;47globalLanguageValue?: T;48workspaceLanguageValue?: T;49workspaceFolderLanguageValue?: T;5051languageIds?: string[];52};5354function isUri(thing: any): thing is vscode.Uri {55return thing instanceof URI;56}5758function isResourceLanguage(thing: any): thing is { uri: URI; languageId: string } {59return thing60&& thing.uri instanceof URI61&& (thing.languageId && typeof thing.languageId === 'string');62}6364function isLanguage(thing: any): thing is { languageId: string } {65return thing66&& !thing.uri67&& (thing.languageId && typeof thing.languageId === 'string');68}6970function isWorkspaceFolder(thing: any): thing is vscode.WorkspaceFolder {71return thing72&& thing.uri instanceof URI73&& (!thing.name || typeof thing.name === 'string')74&& (!thing.index || typeof thing.index === 'number');75}7677function scopeToOverrides(scope: vscode.ConfigurationScope | undefined | null): IConfigurationOverrides | undefined {78if (isUri(scope)) {79return { resource: scope };80}81if (isResourceLanguage(scope)) {82return { resource: scope.uri, overrideIdentifier: scope.languageId };83}84if (isLanguage(scope)) {85return { overrideIdentifier: scope.languageId };86}87if (isWorkspaceFolder(scope)) {88return { resource: scope.uri };89}90if (scope === null) {91return { resource: null };92}93return undefined;94}9596export class ExtHostConfiguration implements ExtHostConfigurationShape {9798readonly _serviceBrand: undefined;99100private readonly _proxy: MainThreadConfigurationShape;101private readonly _logService: ILogService;102private readonly _extHostWorkspace: ExtHostWorkspace;103private readonly _barrier: Barrier;104private _actual: ExtHostConfigProvider | null;105106constructor(107@IExtHostRpcService extHostRpc: IExtHostRpcService,108@IExtHostWorkspace extHostWorkspace: IExtHostWorkspace,109@ILogService logService: ILogService,110) {111this._proxy = extHostRpc.getProxy(MainContext.MainThreadConfiguration);112this._extHostWorkspace = extHostWorkspace;113this._logService = logService;114this._barrier = new Barrier();115this._actual = null;116}117118public getConfigProvider(): Promise<ExtHostConfigProvider> {119return this._barrier.wait().then(_ => this._actual!);120}121122$initializeConfiguration(data: IConfigurationInitData): void {123this._actual = new ExtHostConfigProvider(this._proxy, this._extHostWorkspace, data, this._logService);124this._barrier.open();125}126127$acceptConfigurationChanged(data: IConfigurationInitData, change: IConfigurationChange): void {128this.getConfigProvider().then(provider => provider.$acceptConfigurationChanged(data, change));129}130}131132export class ExtHostConfigProvider {133134private readonly _onDidChangeConfiguration = new Emitter<vscode.ConfigurationChangeEvent>();135private readonly _proxy: MainThreadConfigurationShape;136private readonly _extHostWorkspace: ExtHostWorkspace;137private _configurationScopes: Map<string, ConfigurationScope | undefined>;138private _configuration: Configuration;139private _logService: ILogService;140141constructor(proxy: MainThreadConfigurationShape, extHostWorkspace: ExtHostWorkspace, data: IConfigurationInitData, logService: ILogService) {142this._proxy = proxy;143this._logService = logService;144this._extHostWorkspace = extHostWorkspace;145this._configuration = Configuration.parse(data, logService);146this._configurationScopes = this._toMap(data.configurationScopes);147}148149get onDidChangeConfiguration(): Event<vscode.ConfigurationChangeEvent> {150return this._onDidChangeConfiguration && this._onDidChangeConfiguration.event;151}152153$acceptConfigurationChanged(data: IConfigurationInitData, change: IConfigurationChange) {154const previous = { data: this._configuration.toData(), workspace: this._extHostWorkspace.workspace };155this._configuration = Configuration.parse(data, this._logService);156this._configurationScopes = this._toMap(data.configurationScopes);157this._onDidChangeConfiguration.fire(this._toConfigurationChangeEvent(change, previous));158}159160getConfiguration(section?: string, scope?: vscode.ConfigurationScope | null, extensionDescription?: IExtensionDescription): vscode.WorkspaceConfiguration {161const overrides = scopeToOverrides(scope) || {};162const config = this._toReadonlyValue(this._configuration.getValue(section, overrides, this._extHostWorkspace.workspace));163164if (section) {165this._validateConfigurationAccess(section, overrides, extensionDescription?.identifier);166}167168function parseConfigurationTarget(arg: boolean | ExtHostConfigurationTarget): ConfigurationTarget | null {169if (arg === undefined || arg === null) {170return null;171}172if (typeof arg === 'boolean') {173return arg ? ConfigurationTarget.USER : ConfigurationTarget.WORKSPACE;174}175176switch (arg) {177case ExtHostConfigurationTarget.Global: return ConfigurationTarget.USER;178case ExtHostConfigurationTarget.Workspace: return ConfigurationTarget.WORKSPACE;179case ExtHostConfigurationTarget.WorkspaceFolder: return ConfigurationTarget.WORKSPACE_FOLDER;180}181}182183const result: vscode.WorkspaceConfiguration = {184has(key: string): boolean {185return typeof lookUp(config, key) !== 'undefined';186},187get: <T>(key: string, defaultValue?: T) => {188this._validateConfigurationAccess(section ? `${section}.${key}` : key, overrides, extensionDescription?.identifier);189let result = lookUp(config, key);190if (typeof result === 'undefined') {191result = defaultValue;192} else {193let clonedConfig: any | undefined = undefined;194const cloneOnWriteProxy = (target: any, accessor: string): any => {195if (isObject(target)) {196let clonedTarget: any | undefined = undefined;197const cloneTarget = () => {198clonedConfig = clonedConfig ? clonedConfig : deepClone(config);199clonedTarget = clonedTarget ? clonedTarget : lookUp(clonedConfig, accessor);200};201return new Proxy(target, {202get: (target: any, property: PropertyKey) => {203if (typeof property === 'string' && property.toLowerCase() === 'tojson') {204cloneTarget();205return () => clonedTarget;206}207if (clonedConfig) {208clonedTarget = clonedTarget ? clonedTarget : lookUp(clonedConfig, accessor);209return clonedTarget[property];210}211const result = target[property];212if (typeof property === 'string') {213return cloneOnWriteProxy(result, `${accessor}.${property}`);214}215return result;216},217set: (_target: any, property: PropertyKey, value: any) => {218cloneTarget();219if (clonedTarget) {220clonedTarget[property] = value;221}222return true;223},224deleteProperty: (_target: any, property: PropertyKey) => {225cloneTarget();226if (clonedTarget) {227delete clonedTarget[property];228}229return true;230},231defineProperty: (_target: any, property: PropertyKey, descriptor: any) => {232cloneTarget();233if (clonedTarget) {234Object.defineProperty(clonedTarget, property, descriptor);235}236return true;237}238});239}240if (Array.isArray(target)) {241return deepClone(target);242}243return target;244};245result = cloneOnWriteProxy(result, key);246}247return result;248},249update: (key: string, value: unknown, extHostConfigurationTarget: ExtHostConfigurationTarget | boolean, scopeToLanguage?: boolean) => {250key = section ? `${section}.${key}` : key;251const target = parseConfigurationTarget(extHostConfigurationTarget);252if (value !== undefined) {253return this._proxy.$updateConfigurationOption(target, key, value, overrides, scopeToLanguage);254} else {255return this._proxy.$removeConfigurationOption(target, key, overrides, scopeToLanguage);256}257},258inspect: <T>(key: string): ConfigurationInspect<T> | undefined => {259key = section ? `${section}.${key}` : key;260const config = this._configuration.inspect<T>(key, overrides, this._extHostWorkspace.workspace);261if (config) {262return {263key,264265defaultValue: deepClone(config.policy?.value ?? config.default?.value),266globalLocalValue: deepClone(config.userLocal?.value),267globalRemoteValue: deepClone(config.userRemote?.value),268globalValue: deepClone(config.user?.value ?? config.application?.value),269workspaceValue: deepClone(config.workspace?.value),270workspaceFolderValue: deepClone(config.workspaceFolder?.value),271272defaultLanguageValue: deepClone(config.default?.override),273globalLocalLanguageValue: deepClone(config.userLocal?.override),274globalRemoteLanguageValue: deepClone(config.userRemote?.override),275globalLanguageValue: deepClone(config.user?.override ?? config.application?.override),276workspaceLanguageValue: deepClone(config.workspace?.override),277workspaceFolderLanguageValue: deepClone(config.workspaceFolder?.override),278279languageIds: deepClone(config.overrideIdentifiers)280};281}282return undefined;283}284};285286if (typeof config === 'object') {287mixin(result, config, false);288}289290return Object.freeze(result);291}292293private _toReadonlyValue(result: any): any {294const readonlyProxy = (target: any): any => {295return isObject(target) ?296new Proxy(target, {297get: (target: any, property: PropertyKey) => readonlyProxy(target[property]),298set: (_target: any, property: PropertyKey, _value: any) => { throw new Error(`TypeError: Cannot assign to read only property '${String(property)}' of object`); },299deleteProperty: (_target: any, property: PropertyKey) => { throw new Error(`TypeError: Cannot delete read only property '${String(property)}' of object`); },300defineProperty: (_target: any, property: PropertyKey) => { throw new Error(`TypeError: Cannot define property '${String(property)}' for a readonly object`); },301setPrototypeOf: (_target: unknown) => { throw new Error(`TypeError: Cannot set prototype for a readonly object`); },302isExtensible: () => false,303preventExtensions: () => true304}) : target;305};306return readonlyProxy(result);307}308309private _validateConfigurationAccess(key: string, overrides?: IConfigurationOverrides, extensionId?: ExtensionIdentifier): void {310const scope = OVERRIDE_PROPERTY_REGEX.test(key) ? ConfigurationScope.RESOURCE : this._configurationScopes.get(key);311const extensionIdText = extensionId ? `[${extensionId.value}] ` : '';312if (ConfigurationScope.RESOURCE === scope) {313if (typeof overrides?.resource === 'undefined') {314this._logService.warn(`${extensionIdText}Accessing a resource scoped configuration without providing a resource is not expected. To get the effective value for '${key}', provide the URI of a resource or 'null' for any resource.`);315}316return;317}318if (ConfigurationScope.WINDOW === scope) {319if (overrides?.resource) {320this._logService.warn(`${extensionIdText}Accessing a window scoped configuration for a resource is not expected. To associate '${key}' to a resource, define its scope to 'resource' in configuration contributions in 'package.json'.`);321}322return;323}324}325326private _toConfigurationChangeEvent(change: IConfigurationChange, previous: { data: IConfigurationData; workspace: Workspace | undefined }): vscode.ConfigurationChangeEvent {327const event = new ConfigurationChangeEvent(change, previous, this._configuration, this._extHostWorkspace.workspace, this._logService);328return Object.freeze({329affectsConfiguration: (section: string, scope?: vscode.ConfigurationScope) => event.affectsConfiguration(section, scopeToOverrides(scope))330});331}332333private _toMap(scopes: [string, ConfigurationScope | undefined][]): Map<string, ConfigurationScope | undefined> {334return scopes.reduce((result, scope) => { result.set(scope[0], scope[1]); return result; }, new Map<string, ConfigurationScope | undefined>());335}336337}338339export const IExtHostConfiguration = createDecorator<IExtHostConfiguration>('IExtHostConfiguration');340export interface IExtHostConfiguration extends ExtHostConfiguration { }341342343