Path: blob/main/src/vs/workbench/api/common/extHostConfiguration.ts
5237 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: unknown, key: string) {24if (key) {25const parts = key.split('.');26let node = tree;27for (let i = 0; node && i < parts.length; i++) {28node = (node as Record<string, unknown>)[parts[i]];29}30return node;31}32return undefined;33}3435export type ConfigurationInspect<T> = {36key: string;3738defaultValue?: T;39globalLocalValue?: T;40globalRemoteValue?: T;41globalValue?: T;42workspaceValue?: T;43workspaceFolderValue?: T;4445defaultLanguageValue?: T;46globalLocalLanguageValue?: T;47globalRemoteLanguageValue?: T;48globalLanguageValue?: T;49workspaceLanguageValue?: T;50workspaceFolderLanguageValue?: T;5152languageIds?: string[];53};5455function isUri(thing: unknown): thing is vscode.Uri {56return thing instanceof URI;57}5859function isResourceLanguage(thing: unknown): thing is { uri: URI; languageId: string } {60return isObject(thing)61&& (thing as Record<string, unknown>).uri instanceof URI62&& !!(thing as Record<string, unknown>).languageId63&& typeof (thing as Record<string, unknown>).languageId === 'string';64}6566function isLanguage(thing: unknown): thing is { languageId: string } {67return isObject(thing)68&& !(thing as Record<string, unknown>).uri69&& !!(thing as Record<string, unknown>).languageId70&& typeof (thing as Record<string, unknown>).languageId === 'string';71}7273function isWorkspaceFolder(thing: unknown): thing is vscode.WorkspaceFolder {74return isObject(thing)75&& (thing as Record<string, unknown>).uri instanceof URI76&& (!(thing as Record<string, unknown>).name || typeof (thing as Record<string, unknown>).name === 'string')77&& (!(thing as Record<string, unknown>).index || typeof (thing as Record<string, unknown>).index === 'number');78}7980function scopeToOverrides(scope: vscode.ConfigurationScope | undefined | null): IConfigurationOverrides | undefined {81if (isUri(scope)) {82return { resource: scope };83}84if (isResourceLanguage(scope)) {85return { resource: scope.uri, overrideIdentifier: scope.languageId };86}87if (isLanguage(scope)) {88return { overrideIdentifier: scope.languageId };89}90if (isWorkspaceFolder(scope)) {91return { resource: scope.uri };92}93if (scope === null) {94return { resource: null };95}96return undefined;97}9899export class ExtHostConfiguration implements ExtHostConfigurationShape {100101readonly _serviceBrand: undefined;102103private readonly _proxy: MainThreadConfigurationShape;104private readonly _logService: ILogService;105private readonly _extHostWorkspace: ExtHostWorkspace;106private readonly _barrier: Barrier;107private _actual: ExtHostConfigProvider | null;108109constructor(110@IExtHostRpcService extHostRpc: IExtHostRpcService,111@IExtHostWorkspace extHostWorkspace: IExtHostWorkspace,112@ILogService logService: ILogService,113) {114this._proxy = extHostRpc.getProxy(MainContext.MainThreadConfiguration);115this._extHostWorkspace = extHostWorkspace;116this._logService = logService;117this._barrier = new Barrier();118this._actual = null;119}120121public getConfigProvider(): Promise<ExtHostConfigProvider> {122return this._barrier.wait().then(_ => this._actual!);123}124125$initializeConfiguration(data: IConfigurationInitData): void {126this._actual = new ExtHostConfigProvider(this._proxy, this._extHostWorkspace, data, this._logService);127this._barrier.open();128}129130$acceptConfigurationChanged(data: IConfigurationInitData, change: IConfigurationChange): void {131this.getConfigProvider().then(provider => provider.$acceptConfigurationChanged(data, change));132}133}134135export class ExtHostConfigProvider {136137private readonly _onDidChangeConfiguration = new Emitter<vscode.ConfigurationChangeEvent>();138private readonly _proxy: MainThreadConfigurationShape;139private readonly _extHostWorkspace: ExtHostWorkspace;140private _configurationScopes: Map<string, ConfigurationScope | undefined>;141private _configuration: Configuration;142private _logService: ILogService;143144constructor(proxy: MainThreadConfigurationShape, extHostWorkspace: ExtHostWorkspace, data: IConfigurationInitData, logService: ILogService) {145this._proxy = proxy;146this._logService = logService;147this._extHostWorkspace = extHostWorkspace;148this._configuration = Configuration.parse(data, logService);149this._configurationScopes = this._toMap(data.configurationScopes);150}151152get onDidChangeConfiguration(): Event<vscode.ConfigurationChangeEvent> {153return this._onDidChangeConfiguration && this._onDidChangeConfiguration.event;154}155156$acceptConfigurationChanged(data: IConfigurationInitData, change: IConfigurationChange) {157const previous = { data: this._configuration.toData(), workspace: this._extHostWorkspace.workspace };158this._configuration = Configuration.parse(data, this._logService);159this._configurationScopes = this._toMap(data.configurationScopes);160this._onDidChangeConfiguration.fire(this._toConfigurationChangeEvent(change, previous));161}162163getConfiguration(section?: string, scope?: vscode.ConfigurationScope | null, extensionDescription?: IExtensionDescription): vscode.WorkspaceConfiguration {164const overrides = scopeToOverrides(scope) || {};165const config = this._toReadonlyValue(this._configuration.getValue(section, overrides, this._extHostWorkspace.workspace));166167if (section) {168this._validateConfigurationAccess(section, overrides, extensionDescription?.identifier);169}170171function parseConfigurationTarget(arg: boolean | ExtHostConfigurationTarget): ConfigurationTarget | null {172if (arg === undefined || arg === null) {173return null;174}175if (typeof arg === 'boolean') {176return arg ? ConfigurationTarget.USER : ConfigurationTarget.WORKSPACE;177}178179switch (arg) {180case ExtHostConfigurationTarget.Global: return ConfigurationTarget.USER;181case ExtHostConfigurationTarget.Workspace: return ConfigurationTarget.WORKSPACE;182case ExtHostConfigurationTarget.WorkspaceFolder: return ConfigurationTarget.WORKSPACE_FOLDER;183}184}185186const result: vscode.WorkspaceConfiguration = {187has(key: string): boolean {188return typeof lookUp(config, key) !== 'undefined';189},190get: <T>(key: string, defaultValue?: T) => {191this._validateConfigurationAccess(section ? `${section}.${key}` : key, overrides, extensionDescription?.identifier);192let result: unknown = lookUp(config, key);193if (typeof result === 'undefined') {194result = defaultValue;195} else {196let clonedConfig: unknown | undefined = undefined;197const cloneOnWriteProxy = (target: unknown, accessor: string): unknown => {198if (isObject(target)) {199let clonedTarget: unknown | undefined = undefined;200const cloneTarget = () => {201clonedConfig = clonedConfig ? clonedConfig : deepClone(config);202clonedTarget = clonedTarget ? clonedTarget : lookUp(clonedConfig, accessor);203};204return new Proxy(target, {205get: (target: Record<string, unknown>, property: PropertyKey) => {206if (typeof property === 'string' && property.toLowerCase() === 'tojson') {207cloneTarget();208return () => clonedTarget;209}210if (clonedConfig) {211clonedTarget = clonedTarget ? clonedTarget : lookUp(clonedConfig, accessor);212return (clonedTarget as Record<PropertyKey, unknown>)[property];213}214const result = (target as Record<PropertyKey, unknown>)[property];215if (typeof property === 'string') {216return cloneOnWriteProxy(result, `${accessor}.${property}`);217}218return result;219},220set: (_target: Record<string, unknown>, property: PropertyKey, value: unknown) => {221cloneTarget();222if (clonedTarget) {223(clonedTarget as Record<PropertyKey, unknown>)[property] = value;224}225return true;226},227deleteProperty: (_target: Record<string, unknown>, property: PropertyKey) => {228cloneTarget();229if (clonedTarget) {230delete (clonedTarget as Record<PropertyKey, unknown>)[property];231}232return true;233},234defineProperty: (_target: Record<string, unknown>, property: PropertyKey, descriptor: PropertyDescriptor) => {235cloneTarget();236if (clonedTarget) {237Object.defineProperty(clonedTarget as Record<string, unknown>, property, descriptor);238}239return true;240}241});242}243if (Array.isArray(target)) {244return deepClone(target);245}246return target;247};248result = cloneOnWriteProxy(result, key);249}250return result;251},252update: (key: string, value: unknown, extHostConfigurationTarget: ExtHostConfigurationTarget | boolean, scopeToLanguage?: boolean) => {253key = section ? `${section}.${key}` : key;254const target = parseConfigurationTarget(extHostConfigurationTarget);255if (value !== undefined) {256return this._proxy.$updateConfigurationOption(target, key, value, overrides, scopeToLanguage);257} else {258return this._proxy.$removeConfigurationOption(target, key, overrides, scopeToLanguage);259}260},261inspect: <T>(key: string): ConfigurationInspect<T> | undefined => {262key = section ? `${section}.${key}` : key;263const config = this._configuration.inspect<T>(key, overrides, this._extHostWorkspace.workspace);264if (config) {265return {266key,267268defaultValue: deepClone(config.policy?.value ?? config.default?.value),269globalLocalValue: deepClone(config.userLocal?.value),270globalRemoteValue: deepClone(config.userRemote?.value),271globalValue: deepClone(config.user?.value ?? config.application?.value),272workspaceValue: deepClone(config.workspace?.value),273workspaceFolderValue: deepClone(config.workspaceFolder?.value),274275defaultLanguageValue: deepClone(config.default?.override),276globalLocalLanguageValue: deepClone(config.userLocal?.override),277globalRemoteLanguageValue: deepClone(config.userRemote?.override),278globalLanguageValue: deepClone(config.user?.override ?? config.application?.override),279workspaceLanguageValue: deepClone(config.workspace?.override),280workspaceFolderLanguageValue: deepClone(config.workspaceFolder?.override),281282languageIds: deepClone(config.overrideIdentifiers)283};284}285return undefined;286}287};288289if (typeof config === 'object') {290mixin(result, config, false);291}292293return Object.freeze(result);294}295296private _toReadonlyValue(result: unknown): unknown {297const readonlyProxy = (target: unknown): unknown => {298return isObject(target) ?299new Proxy(target, {300get: (target: Record<string, unknown>, property: PropertyKey) => readonlyProxy((target as Record<PropertyKey, unknown>)[property]),301set: (_target: Record<string, unknown>, property: PropertyKey, _value: unknown) => { throw new Error(`TypeError: Cannot assign to read only property '${String(property)}' of object`); },302deleteProperty: (_target: Record<string, unknown>, property: PropertyKey) => { throw new Error(`TypeError: Cannot delete read only property '${String(property)}' of object`); },303defineProperty: (_target: Record<string, unknown>, property: PropertyKey) => { throw new Error(`TypeError: Cannot define property '${String(property)}' for a readonly object`); },304setPrototypeOf: (_target: unknown) => { throw new Error(`TypeError: Cannot set prototype for a readonly object`); },305isExtensible: () => false,306preventExtensions: () => true307}) : target;308};309return readonlyProxy(result);310}311312private _validateConfigurationAccess(key: string, overrides?: IConfigurationOverrides, extensionId?: ExtensionIdentifier): void {313const scope = OVERRIDE_PROPERTY_REGEX.test(key) ? ConfigurationScope.RESOURCE : this._configurationScopes.get(key);314const extensionIdText = extensionId ? `[${extensionId.value}] ` : '';315if (ConfigurationScope.RESOURCE === scope) {316if (typeof overrides?.resource === 'undefined') {317this._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.`);318}319return;320}321if (ConfigurationScope.WINDOW === scope) {322if (overrides?.resource) {323this._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'.`);324}325return;326}327}328329private _toConfigurationChangeEvent(change: IConfigurationChange, previous: { data: IConfigurationData; workspace: Workspace | undefined }): vscode.ConfigurationChangeEvent {330const event = new ConfigurationChangeEvent(change, previous, this._configuration, this._extHostWorkspace.workspace, this._logService);331return Object.freeze({332affectsConfiguration: (section: string, scope?: vscode.ConfigurationScope) => event.affectsConfiguration(section, scopeToOverrides(scope))333});334}335336private _toMap(scopes: [string, ConfigurationScope | undefined][]): Map<string, ConfigurationScope | undefined> {337return scopes.reduce((result, scope) => { result.set(scope[0], scope[1]); return result; }, new Map<string, ConfigurationScope | undefined>());338}339340}341342export const IExtHostConfiguration = createDecorator<IExtHostConfiguration>('IExtHostConfiguration');343export interface IExtHostConfiguration extends ExtHostConfiguration { }344345346