Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostConfiguration.ts
5237 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { mixin, deepClone } from '../../../base/common/objects.js';
7
import { Event, Emitter } from '../../../base/common/event.js';
8
import type * as vscode from 'vscode';
9
import { ExtHostWorkspace, IExtHostWorkspace } from './extHostWorkspace.js';
10
import { ExtHostConfigurationShape, MainThreadConfigurationShape, IConfigurationInitData, MainContext } from './extHost.protocol.js';
11
import { ConfigurationTarget as ExtHostConfigurationTarget } from './extHostTypes.js';
12
import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from '../../../platform/configuration/common/configuration.js';
13
import { Configuration, ConfigurationChangeEvent } from '../../../platform/configuration/common/configurationModels.js';
14
import { ConfigurationScope, OVERRIDE_PROPERTY_REGEX } from '../../../platform/configuration/common/configurationRegistry.js';
15
import { isObject } from '../../../base/common/types.js';
16
import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
17
import { Barrier } from '../../../base/common/async.js';
18
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
19
import { IExtHostRpcService } from './extHostRpcService.js';
20
import { ILogService } from '../../../platform/log/common/log.js';
21
import { Workspace } from '../../../platform/workspace/common/workspace.js';
22
import { URI } from '../../../base/common/uri.js';
23
24
function lookUp(tree: unknown, key: string) {
25
if (key) {
26
const parts = key.split('.');
27
let node = tree;
28
for (let i = 0; node && i < parts.length; i++) {
29
node = (node as Record<string, unknown>)[parts[i]];
30
}
31
return node;
32
}
33
return undefined;
34
}
35
36
export type ConfigurationInspect<T> = {
37
key: string;
38
39
defaultValue?: T;
40
globalLocalValue?: T;
41
globalRemoteValue?: T;
42
globalValue?: T;
43
workspaceValue?: T;
44
workspaceFolderValue?: T;
45
46
defaultLanguageValue?: T;
47
globalLocalLanguageValue?: T;
48
globalRemoteLanguageValue?: T;
49
globalLanguageValue?: T;
50
workspaceLanguageValue?: T;
51
workspaceFolderLanguageValue?: T;
52
53
languageIds?: string[];
54
};
55
56
function isUri(thing: unknown): thing is vscode.Uri {
57
return thing instanceof URI;
58
}
59
60
function isResourceLanguage(thing: unknown): thing is { uri: URI; languageId: string } {
61
return isObject(thing)
62
&& (thing as Record<string, unknown>).uri instanceof URI
63
&& !!(thing as Record<string, unknown>).languageId
64
&& typeof (thing as Record<string, unknown>).languageId === 'string';
65
}
66
67
function isLanguage(thing: unknown): thing is { languageId: string } {
68
return isObject(thing)
69
&& !(thing as Record<string, unknown>).uri
70
&& !!(thing as Record<string, unknown>).languageId
71
&& typeof (thing as Record<string, unknown>).languageId === 'string';
72
}
73
74
function isWorkspaceFolder(thing: unknown): thing is vscode.WorkspaceFolder {
75
return isObject(thing)
76
&& (thing as Record<string, unknown>).uri instanceof URI
77
&& (!(thing as Record<string, unknown>).name || typeof (thing as Record<string, unknown>).name === 'string')
78
&& (!(thing as Record<string, unknown>).index || typeof (thing as Record<string, unknown>).index === 'number');
79
}
80
81
function scopeToOverrides(scope: vscode.ConfigurationScope | undefined | null): IConfigurationOverrides | undefined {
82
if (isUri(scope)) {
83
return { resource: scope };
84
}
85
if (isResourceLanguage(scope)) {
86
return { resource: scope.uri, overrideIdentifier: scope.languageId };
87
}
88
if (isLanguage(scope)) {
89
return { overrideIdentifier: scope.languageId };
90
}
91
if (isWorkspaceFolder(scope)) {
92
return { resource: scope.uri };
93
}
94
if (scope === null) {
95
return { resource: null };
96
}
97
return undefined;
98
}
99
100
export class ExtHostConfiguration implements ExtHostConfigurationShape {
101
102
readonly _serviceBrand: undefined;
103
104
private readonly _proxy: MainThreadConfigurationShape;
105
private readonly _logService: ILogService;
106
private readonly _extHostWorkspace: ExtHostWorkspace;
107
private readonly _barrier: Barrier;
108
private _actual: ExtHostConfigProvider | null;
109
110
constructor(
111
@IExtHostRpcService extHostRpc: IExtHostRpcService,
112
@IExtHostWorkspace extHostWorkspace: IExtHostWorkspace,
113
@ILogService logService: ILogService,
114
) {
115
this._proxy = extHostRpc.getProxy(MainContext.MainThreadConfiguration);
116
this._extHostWorkspace = extHostWorkspace;
117
this._logService = logService;
118
this._barrier = new Barrier();
119
this._actual = null;
120
}
121
122
public getConfigProvider(): Promise<ExtHostConfigProvider> {
123
return this._barrier.wait().then(_ => this._actual!);
124
}
125
126
$initializeConfiguration(data: IConfigurationInitData): void {
127
this._actual = new ExtHostConfigProvider(this._proxy, this._extHostWorkspace, data, this._logService);
128
this._barrier.open();
129
}
130
131
$acceptConfigurationChanged(data: IConfigurationInitData, change: IConfigurationChange): void {
132
this.getConfigProvider().then(provider => provider.$acceptConfigurationChanged(data, change));
133
}
134
}
135
136
export class ExtHostConfigProvider {
137
138
private readonly _onDidChangeConfiguration = new Emitter<vscode.ConfigurationChangeEvent>();
139
private readonly _proxy: MainThreadConfigurationShape;
140
private readonly _extHostWorkspace: ExtHostWorkspace;
141
private _configurationScopes: Map<string, ConfigurationScope | undefined>;
142
private _configuration: Configuration;
143
private _logService: ILogService;
144
145
constructor(proxy: MainThreadConfigurationShape, extHostWorkspace: ExtHostWorkspace, data: IConfigurationInitData, logService: ILogService) {
146
this._proxy = proxy;
147
this._logService = logService;
148
this._extHostWorkspace = extHostWorkspace;
149
this._configuration = Configuration.parse(data, logService);
150
this._configurationScopes = this._toMap(data.configurationScopes);
151
}
152
153
get onDidChangeConfiguration(): Event<vscode.ConfigurationChangeEvent> {
154
return this._onDidChangeConfiguration && this._onDidChangeConfiguration.event;
155
}
156
157
$acceptConfigurationChanged(data: IConfigurationInitData, change: IConfigurationChange) {
158
const previous = { data: this._configuration.toData(), workspace: this._extHostWorkspace.workspace };
159
this._configuration = Configuration.parse(data, this._logService);
160
this._configurationScopes = this._toMap(data.configurationScopes);
161
this._onDidChangeConfiguration.fire(this._toConfigurationChangeEvent(change, previous));
162
}
163
164
getConfiguration(section?: string, scope?: vscode.ConfigurationScope | null, extensionDescription?: IExtensionDescription): vscode.WorkspaceConfiguration {
165
const overrides = scopeToOverrides(scope) || {};
166
const config = this._toReadonlyValue(this._configuration.getValue(section, overrides, this._extHostWorkspace.workspace));
167
168
if (section) {
169
this._validateConfigurationAccess(section, overrides, extensionDescription?.identifier);
170
}
171
172
function parseConfigurationTarget(arg: boolean | ExtHostConfigurationTarget): ConfigurationTarget | null {
173
if (arg === undefined || arg === null) {
174
return null;
175
}
176
if (typeof arg === 'boolean') {
177
return arg ? ConfigurationTarget.USER : ConfigurationTarget.WORKSPACE;
178
}
179
180
switch (arg) {
181
case ExtHostConfigurationTarget.Global: return ConfigurationTarget.USER;
182
case ExtHostConfigurationTarget.Workspace: return ConfigurationTarget.WORKSPACE;
183
case ExtHostConfigurationTarget.WorkspaceFolder: return ConfigurationTarget.WORKSPACE_FOLDER;
184
}
185
}
186
187
const result: vscode.WorkspaceConfiguration = {
188
has(key: string): boolean {
189
return typeof lookUp(config, key) !== 'undefined';
190
},
191
get: <T>(key: string, defaultValue?: T) => {
192
this._validateConfigurationAccess(section ? `${section}.${key}` : key, overrides, extensionDescription?.identifier);
193
let result: unknown = lookUp(config, key);
194
if (typeof result === 'undefined') {
195
result = defaultValue;
196
} else {
197
let clonedConfig: unknown | undefined = undefined;
198
const cloneOnWriteProxy = (target: unknown, accessor: string): unknown => {
199
if (isObject(target)) {
200
let clonedTarget: unknown | undefined = undefined;
201
const cloneTarget = () => {
202
clonedConfig = clonedConfig ? clonedConfig : deepClone(config);
203
clonedTarget = clonedTarget ? clonedTarget : lookUp(clonedConfig, accessor);
204
};
205
return new Proxy(target, {
206
get: (target: Record<string, unknown>, property: PropertyKey) => {
207
if (typeof property === 'string' && property.toLowerCase() === 'tojson') {
208
cloneTarget();
209
return () => clonedTarget;
210
}
211
if (clonedConfig) {
212
clonedTarget = clonedTarget ? clonedTarget : lookUp(clonedConfig, accessor);
213
return (clonedTarget as Record<PropertyKey, unknown>)[property];
214
}
215
const result = (target as Record<PropertyKey, unknown>)[property];
216
if (typeof property === 'string') {
217
return cloneOnWriteProxy(result, `${accessor}.${property}`);
218
}
219
return result;
220
},
221
set: (_target: Record<string, unknown>, property: PropertyKey, value: unknown) => {
222
cloneTarget();
223
if (clonedTarget) {
224
(clonedTarget as Record<PropertyKey, unknown>)[property] = value;
225
}
226
return true;
227
},
228
deleteProperty: (_target: Record<string, unknown>, property: PropertyKey) => {
229
cloneTarget();
230
if (clonedTarget) {
231
delete (clonedTarget as Record<PropertyKey, unknown>)[property];
232
}
233
return true;
234
},
235
defineProperty: (_target: Record<string, unknown>, property: PropertyKey, descriptor: PropertyDescriptor) => {
236
cloneTarget();
237
if (clonedTarget) {
238
Object.defineProperty(clonedTarget as Record<string, unknown>, property, descriptor);
239
}
240
return true;
241
}
242
});
243
}
244
if (Array.isArray(target)) {
245
return deepClone(target);
246
}
247
return target;
248
};
249
result = cloneOnWriteProxy(result, key);
250
}
251
return result;
252
},
253
update: (key: string, value: unknown, extHostConfigurationTarget: ExtHostConfigurationTarget | boolean, scopeToLanguage?: boolean) => {
254
key = section ? `${section}.${key}` : key;
255
const target = parseConfigurationTarget(extHostConfigurationTarget);
256
if (value !== undefined) {
257
return this._proxy.$updateConfigurationOption(target, key, value, overrides, scopeToLanguage);
258
} else {
259
return this._proxy.$removeConfigurationOption(target, key, overrides, scopeToLanguage);
260
}
261
},
262
inspect: <T>(key: string): ConfigurationInspect<T> | undefined => {
263
key = section ? `${section}.${key}` : key;
264
const config = this._configuration.inspect<T>(key, overrides, this._extHostWorkspace.workspace);
265
if (config) {
266
return {
267
key,
268
269
defaultValue: deepClone(config.policy?.value ?? config.default?.value),
270
globalLocalValue: deepClone(config.userLocal?.value),
271
globalRemoteValue: deepClone(config.userRemote?.value),
272
globalValue: deepClone(config.user?.value ?? config.application?.value),
273
workspaceValue: deepClone(config.workspace?.value),
274
workspaceFolderValue: deepClone(config.workspaceFolder?.value),
275
276
defaultLanguageValue: deepClone(config.default?.override),
277
globalLocalLanguageValue: deepClone(config.userLocal?.override),
278
globalRemoteLanguageValue: deepClone(config.userRemote?.override),
279
globalLanguageValue: deepClone(config.user?.override ?? config.application?.override),
280
workspaceLanguageValue: deepClone(config.workspace?.override),
281
workspaceFolderLanguageValue: deepClone(config.workspaceFolder?.override),
282
283
languageIds: deepClone(config.overrideIdentifiers)
284
};
285
}
286
return undefined;
287
}
288
};
289
290
if (typeof config === 'object') {
291
mixin(result, config, false);
292
}
293
294
return Object.freeze(result);
295
}
296
297
private _toReadonlyValue(result: unknown): unknown {
298
const readonlyProxy = (target: unknown): unknown => {
299
return isObject(target) ?
300
new Proxy(target, {
301
get: (target: Record<string, unknown>, property: PropertyKey) => readonlyProxy((target as Record<PropertyKey, unknown>)[property]),
302
set: (_target: Record<string, unknown>, property: PropertyKey, _value: unknown) => { throw new Error(`TypeError: Cannot assign to read only property '${String(property)}' of object`); },
303
deleteProperty: (_target: Record<string, unknown>, property: PropertyKey) => { throw new Error(`TypeError: Cannot delete read only property '${String(property)}' of object`); },
304
defineProperty: (_target: Record<string, unknown>, property: PropertyKey) => { throw new Error(`TypeError: Cannot define property '${String(property)}' for a readonly object`); },
305
setPrototypeOf: (_target: unknown) => { throw new Error(`TypeError: Cannot set prototype for a readonly object`); },
306
isExtensible: () => false,
307
preventExtensions: () => true
308
}) : target;
309
};
310
return readonlyProxy(result);
311
}
312
313
private _validateConfigurationAccess(key: string, overrides?: IConfigurationOverrides, extensionId?: ExtensionIdentifier): void {
314
const scope = OVERRIDE_PROPERTY_REGEX.test(key) ? ConfigurationScope.RESOURCE : this._configurationScopes.get(key);
315
const extensionIdText = extensionId ? `[${extensionId.value}] ` : '';
316
if (ConfigurationScope.RESOURCE === scope) {
317
if (typeof overrides?.resource === 'undefined') {
318
this._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.`);
319
}
320
return;
321
}
322
if (ConfigurationScope.WINDOW === scope) {
323
if (overrides?.resource) {
324
this._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'.`);
325
}
326
return;
327
}
328
}
329
330
private _toConfigurationChangeEvent(change: IConfigurationChange, previous: { data: IConfigurationData; workspace: Workspace | undefined }): vscode.ConfigurationChangeEvent {
331
const event = new ConfigurationChangeEvent(change, previous, this._configuration, this._extHostWorkspace.workspace, this._logService);
332
return Object.freeze({
333
affectsConfiguration: (section: string, scope?: vscode.ConfigurationScope) => event.affectsConfiguration(section, scopeToOverrides(scope))
334
});
335
}
336
337
private _toMap(scopes: [string, ConfigurationScope | undefined][]): Map<string, ConfigurationScope | undefined> {
338
return scopes.reduce((result, scope) => { result.set(scope[0], scope[1]); return result; }, new Map<string, ConfigurationScope | undefined>());
339
}
340
341
}
342
343
export const IExtHostConfiguration = createDecorator<IExtHostConfiguration>('IExtHostConfiguration');
344
export interface IExtHostConfiguration extends ExtHostConfiguration { }
345
346