Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/contextkey/browser/contextKeyService.ts
3296 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 { Emitter, Event, PauseableEmitter } from '../../../base/common/event.js';
7
import { Iterable } from '../../../base/common/iterator.js';
8
import { Disposable, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
9
import { MarshalledObject } from '../../../base/common/marshalling.js';
10
import { MarshalledId } from '../../../base/common/marshallingIds.js';
11
import { cloneAndChange, distinct } from '../../../base/common/objects.js';
12
import { TernarySearchTree } from '../../../base/common/ternarySearchTree.js';
13
import { URI } from '../../../base/common/uri.js';
14
import { localize } from '../../../nls.js';
15
import { CommandsRegistry } from '../../commands/common/commands.js';
16
import { ConfigurationTarget, IConfigurationService } from '../../configuration/common/configuration.js';
17
import { ContextKeyExpression, ContextKeyInfo, ContextKeyValue, IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, IScopedContextKeyService, RawContextKey } from '../common/contextkey.js';
18
import { ServicesAccessor } from '../../instantiation/common/instantiation.js';
19
20
const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context';
21
22
export class Context implements IContext {
23
24
protected _parent: Context | null;
25
protected _value: Record<string, any>;
26
protected _id: number;
27
28
constructor(id: number, parent: Context | null) {
29
this._id = id;
30
this._parent = parent;
31
this._value = Object.create(null);
32
this._value['_contextId'] = id;
33
}
34
35
public get value(): Record<string, any> {
36
return { ...this._value };
37
}
38
39
public setValue(key: string, value: any): boolean {
40
// console.log('SET ' + key + ' = ' + value + ' ON ' + this._id);
41
if (this._value[key] !== value) {
42
this._value[key] = value;
43
return true;
44
}
45
return false;
46
}
47
48
public removeValue(key: string): boolean {
49
// console.log('REMOVE ' + key + ' FROM ' + this._id);
50
if (key in this._value) {
51
delete this._value[key];
52
return true;
53
}
54
return false;
55
}
56
57
public getValue<T>(key: string): T | undefined {
58
const ret = this._value[key];
59
if (typeof ret === 'undefined' && this._parent) {
60
return this._parent.getValue<T>(key);
61
}
62
return ret;
63
}
64
65
public updateParent(parent: Context): void {
66
this._parent = parent;
67
}
68
69
public collectAllValues(): Record<string, any> {
70
let result = this._parent ? this._parent.collectAllValues() : Object.create(null);
71
result = { ...result, ...this._value };
72
delete result['_contextId'];
73
return result;
74
}
75
}
76
77
class NullContext extends Context {
78
79
static readonly INSTANCE = new NullContext();
80
81
constructor() {
82
super(-1, null);
83
}
84
85
public override setValue(key: string, value: any): boolean {
86
return false;
87
}
88
89
public override removeValue(key: string): boolean {
90
return false;
91
}
92
93
public override getValue<T>(key: string): T | undefined {
94
return undefined;
95
}
96
97
override collectAllValues(): { [key: string]: any } {
98
return Object.create(null);
99
}
100
}
101
102
class ConfigAwareContextValuesContainer extends Context {
103
private static readonly _keyPrefix = 'config.';
104
105
private readonly _values = TernarySearchTree.forConfigKeys<any>();
106
private readonly _listener: IDisposable;
107
108
constructor(
109
id: number,
110
private readonly _configurationService: IConfigurationService,
111
emitter: Emitter<IContextKeyChangeEvent>
112
) {
113
super(id, null);
114
115
this._listener = this._configurationService.onDidChangeConfiguration(event => {
116
if (event.source === ConfigurationTarget.DEFAULT) {
117
// new setting, reset everything
118
const allKeys = Array.from(this._values, ([k]) => k);
119
this._values.clear();
120
emitter.fire(new ArrayContextKeyChangeEvent(allKeys));
121
} else {
122
const changedKeys: string[] = [];
123
for (const configKey of event.affectedKeys) {
124
const contextKey = `config.${configKey}`;
125
126
const cachedItems = this._values.findSuperstr(contextKey);
127
if (cachedItems !== undefined) {
128
changedKeys.push(...Iterable.map(cachedItems, ([key]) => key));
129
this._values.deleteSuperstr(contextKey);
130
}
131
132
if (this._values.has(contextKey)) {
133
changedKeys.push(contextKey);
134
this._values.delete(contextKey);
135
}
136
}
137
138
emitter.fire(new ArrayContextKeyChangeEvent(changedKeys));
139
}
140
});
141
}
142
143
dispose(): void {
144
this._listener.dispose();
145
}
146
147
override getValue(key: string): any {
148
149
if (key.indexOf(ConfigAwareContextValuesContainer._keyPrefix) !== 0) {
150
return super.getValue(key);
151
}
152
153
if (this._values.has(key)) {
154
return this._values.get(key);
155
}
156
157
const configKey = key.substr(ConfigAwareContextValuesContainer._keyPrefix.length);
158
const configValue = this._configurationService.getValue(configKey);
159
let value: any = undefined;
160
switch (typeof configValue) {
161
case 'number':
162
case 'boolean':
163
case 'string':
164
value = configValue;
165
break;
166
default:
167
if (Array.isArray(configValue)) {
168
value = JSON.stringify(configValue);
169
} else {
170
value = configValue;
171
}
172
}
173
174
this._values.set(key, value);
175
return value;
176
}
177
178
override setValue(key: string, value: any): boolean {
179
return super.setValue(key, value);
180
}
181
182
override removeValue(key: string): boolean {
183
return super.removeValue(key);
184
}
185
186
override collectAllValues(): { [key: string]: any } {
187
const result: { [key: string]: any } = Object.create(null);
188
this._values.forEach((value, index) => result[index] = value);
189
return { ...result, ...super.collectAllValues() };
190
}
191
}
192
193
class ContextKey<T extends ContextKeyValue> implements IContextKey<T> {
194
195
private _service: AbstractContextKeyService;
196
private _key: string;
197
private _defaultValue: T | undefined;
198
199
constructor(service: AbstractContextKeyService, key: string, defaultValue: T | undefined) {
200
this._service = service;
201
this._key = key;
202
this._defaultValue = defaultValue;
203
this.reset();
204
}
205
206
public set(value: T): void {
207
this._service.setContext(this._key, value);
208
}
209
210
public reset(): void {
211
if (typeof this._defaultValue === 'undefined') {
212
this._service.removeContext(this._key);
213
} else {
214
this._service.setContext(this._key, this._defaultValue);
215
}
216
}
217
218
public get(): T | undefined {
219
return this._service.getContextKeyValue<T>(this._key);
220
}
221
}
222
223
class SimpleContextKeyChangeEvent implements IContextKeyChangeEvent {
224
constructor(readonly key: string) { }
225
affectsSome(keys: IReadableSet<string>): boolean {
226
return keys.has(this.key);
227
}
228
allKeysContainedIn(keys: IReadableSet<string>): boolean {
229
return this.affectsSome(keys);
230
}
231
}
232
233
class ArrayContextKeyChangeEvent implements IContextKeyChangeEvent {
234
constructor(readonly keys: string[]) { }
235
affectsSome(keys: IReadableSet<string>): boolean {
236
for (const key of this.keys) {
237
if (keys.has(key)) {
238
return true;
239
}
240
}
241
return false;
242
}
243
allKeysContainedIn(keys: IReadableSet<string>): boolean {
244
return this.keys.every(key => keys.has(key));
245
}
246
}
247
248
class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent {
249
constructor(readonly events: IContextKeyChangeEvent[]) { }
250
affectsSome(keys: IReadableSet<string>): boolean {
251
for (const e of this.events) {
252
if (e.affectsSome(keys)) {
253
return true;
254
}
255
}
256
return false;
257
}
258
allKeysContainedIn(keys: IReadableSet<string>): boolean {
259
return this.events.every(evt => evt.allKeysContainedIn(keys));
260
}
261
}
262
263
function allEventKeysInContext(event: IContextKeyChangeEvent, context: Record<string, any>): boolean {
264
return event.allKeysContainedIn(new Set(Object.keys(context)));
265
}
266
267
export abstract class AbstractContextKeyService extends Disposable implements IContextKeyService {
268
declare _serviceBrand: undefined;
269
270
protected _isDisposed: boolean;
271
protected _myContextId: number;
272
273
protected _onDidChangeContext = this._register(new PauseableEmitter<IContextKeyChangeEvent>({ merge: input => new CompositeContextKeyChangeEvent(input) }));
274
get onDidChangeContext() { return this._onDidChangeContext.event; }
275
276
constructor(myContextId: number) {
277
super();
278
this._isDisposed = false;
279
this._myContextId = myContextId;
280
}
281
282
public get contextId(): number {
283
return this._myContextId;
284
}
285
286
public createKey<T extends ContextKeyValue>(key: string, defaultValue: T | undefined): IContextKey<T> {
287
if (this._isDisposed) {
288
throw new Error(`AbstractContextKeyService has been disposed`);
289
}
290
return new ContextKey(this, key, defaultValue);
291
}
292
293
294
bufferChangeEvents(callback: Function): void {
295
this._onDidChangeContext.pause();
296
try {
297
callback();
298
} finally {
299
this._onDidChangeContext.resume();
300
}
301
}
302
303
public createScoped(domNode: IContextKeyServiceTarget): IScopedContextKeyService {
304
if (this._isDisposed) {
305
throw new Error(`AbstractContextKeyService has been disposed`);
306
}
307
return new ScopedContextKeyService(this, domNode);
308
}
309
310
createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {
311
if (this._isDisposed) {
312
throw new Error(`AbstractContextKeyService has been disposed`);
313
}
314
return new OverlayContextKeyService(this, overlay);
315
}
316
317
public contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
318
if (this._isDisposed) {
319
throw new Error(`AbstractContextKeyService has been disposed`);
320
}
321
const context = this.getContextValuesContainer(this._myContextId);
322
const result = (rules ? rules.evaluate(context) : true);
323
// console.group(rules.serialize() + ' -> ' + result);
324
// rules.keys().forEach(key => { console.log(key, ctx[key]); });
325
// console.groupEnd();
326
return result;
327
}
328
329
public getContextKeyValue<T>(key: string): T | undefined {
330
if (this._isDisposed) {
331
return undefined;
332
}
333
return this.getContextValuesContainer(this._myContextId).getValue<T>(key);
334
}
335
336
public setContext(key: string, value: any): void {
337
if (this._isDisposed) {
338
return;
339
}
340
const myContext = this.getContextValuesContainer(this._myContextId);
341
if (!myContext) {
342
return;
343
}
344
if (myContext.setValue(key, value)) {
345
this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));
346
}
347
}
348
349
public removeContext(key: string): void {
350
if (this._isDisposed) {
351
return;
352
}
353
if (this.getContextValuesContainer(this._myContextId).removeValue(key)) {
354
this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));
355
}
356
}
357
358
public getContext(target: IContextKeyServiceTarget | null): IContext {
359
if (this._isDisposed) {
360
return NullContext.INSTANCE;
361
}
362
return this.getContextValuesContainer(findContextAttr(target));
363
}
364
365
public abstract getContextValuesContainer(contextId: number): Context;
366
public abstract createChildContext(parentContextId?: number): number;
367
public abstract disposeContext(contextId: number): void;
368
public abstract updateParent(parentContextKeyService?: IContextKeyService): void;
369
370
public override dispose(): void {
371
super.dispose();
372
this._isDisposed = true;
373
}
374
}
375
376
export class ContextKeyService extends AbstractContextKeyService implements IContextKeyService {
377
378
private _lastContextId: number;
379
private readonly _contexts = new Map<number, Context>();
380
381
constructor(@IConfigurationService configurationService: IConfigurationService) {
382
super(0);
383
this._lastContextId = 0;
384
385
const myContext = this._register(new ConfigAwareContextValuesContainer(this._myContextId, configurationService, this._onDidChangeContext));
386
this._contexts.set(this._myContextId, myContext);
387
388
// Uncomment this to see the contexts continuously logged
389
// let lastLoggedValue: string | null = null;
390
// setInterval(() => {
391
// let values = Object.keys(this._contexts).map((key) => this._contexts[key]);
392
// let logValue = values.map(v => JSON.stringify(v._value, null, '\t')).join('\n');
393
// if (lastLoggedValue !== logValue) {
394
// lastLoggedValue = logValue;
395
// console.log(lastLoggedValue);
396
// }
397
// }, 2000);
398
}
399
400
public getContextValuesContainer(contextId: number): Context {
401
if (this._isDisposed) {
402
return NullContext.INSTANCE;
403
}
404
return this._contexts.get(contextId) || NullContext.INSTANCE;
405
}
406
407
public createChildContext(parentContextId: number = this._myContextId): number {
408
if (this._isDisposed) {
409
throw new Error(`ContextKeyService has been disposed`);
410
}
411
const id = (++this._lastContextId);
412
this._contexts.set(id, new Context(id, this.getContextValuesContainer(parentContextId)));
413
return id;
414
}
415
416
public disposeContext(contextId: number): void {
417
if (!this._isDisposed) {
418
this._contexts.delete(contextId);
419
}
420
}
421
422
public updateParent(_parentContextKeyService: IContextKeyService): void {
423
throw new Error('Cannot update parent of root ContextKeyService');
424
}
425
}
426
427
class ScopedContextKeyService extends AbstractContextKeyService {
428
429
private _parent: AbstractContextKeyService;
430
private _domNode: IContextKeyServiceTarget;
431
432
private readonly _parentChangeListener = this._register(new MutableDisposable());
433
434
constructor(parent: AbstractContextKeyService, domNode: IContextKeyServiceTarget) {
435
super(parent.createChildContext());
436
this._parent = parent;
437
this._updateParentChangeListener();
438
439
this._domNode = domNode;
440
if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
441
let extraInfo = '';
442
if ((this._domNode as HTMLElement).classList) {
443
extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', ');
444
}
445
446
console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`);
447
}
448
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
449
}
450
451
private _updateParentChangeListener(): void {
452
// Forward parent events to this listener. Parent will change.
453
this._parentChangeListener.value = this._parent.onDidChangeContext(e => {
454
const thisContainer = this._parent.getContextValuesContainer(this._myContextId);
455
const thisContextValues = thisContainer.value;
456
457
if (!allEventKeysInContext(e, thisContextValues)) {
458
this._onDidChangeContext.fire(e);
459
}
460
});
461
}
462
463
public override dispose(): void {
464
if (this._isDisposed) {
465
return;
466
}
467
468
this._parent.disposeContext(this._myContextId);
469
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
470
super.dispose();
471
}
472
473
public getContextValuesContainer(contextId: number): Context {
474
if (this._isDisposed) {
475
return NullContext.INSTANCE;
476
}
477
return this._parent.getContextValuesContainer(contextId);
478
}
479
480
public createChildContext(parentContextId: number = this._myContextId): number {
481
if (this._isDisposed) {
482
throw new Error(`ScopedContextKeyService has been disposed`);
483
}
484
return this._parent.createChildContext(parentContextId);
485
}
486
487
public disposeContext(contextId: number): void {
488
if (this._isDisposed) {
489
return;
490
}
491
this._parent.disposeContext(contextId);
492
}
493
494
public updateParent(parentContextKeyService: AbstractContextKeyService): void {
495
if (this._parent === parentContextKeyService) {
496
return;
497
}
498
499
const thisContainer = this._parent.getContextValuesContainer(this._myContextId);
500
const oldAllValues = thisContainer.collectAllValues();
501
this._parent = parentContextKeyService;
502
this._updateParentChangeListener();
503
const newParentContainer = this._parent.getContextValuesContainer(this._parent.contextId);
504
thisContainer.updateParent(newParentContainer);
505
506
const newAllValues = thisContainer.collectAllValues();
507
const allValuesDiff = {
508
...distinct(oldAllValues, newAllValues),
509
...distinct(newAllValues, oldAllValues)
510
};
511
const changedKeys = Object.keys(allValuesDiff);
512
513
this._onDidChangeContext.fire(new ArrayContextKeyChangeEvent(changedKeys));
514
}
515
}
516
517
class OverlayContext implements IContext {
518
519
constructor(private parent: IContext, private overlay: ReadonlyMap<string, any>) { }
520
521
getValue<T extends ContextKeyValue>(key: string): T | undefined {
522
return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getValue<T>(key);
523
}
524
}
525
526
class OverlayContextKeyService implements IContextKeyService {
527
528
declare _serviceBrand: undefined;
529
private overlay: Map<string, any>;
530
531
get contextId(): number {
532
return this.parent.contextId;
533
}
534
535
get onDidChangeContext(): Event<IContextKeyChangeEvent> {
536
return this.parent.onDidChangeContext;
537
}
538
539
constructor(private parent: AbstractContextKeyService | OverlayContextKeyService, overlay: Iterable<[string, any]>) {
540
this.overlay = new Map(overlay);
541
}
542
543
bufferChangeEvents(callback: Function): void {
544
this.parent.bufferChangeEvents(callback);
545
}
546
547
createKey<T extends ContextKeyValue>(): IContextKey<T> {
548
throw new Error('Not supported.');
549
}
550
551
getContext(target: IContextKeyServiceTarget | null): IContext {
552
return new OverlayContext(this.parent.getContext(target), this.overlay);
553
}
554
555
getContextValuesContainer(contextId: number): IContext {
556
const parentContext = this.parent.getContextValuesContainer(contextId);
557
return new OverlayContext(parentContext, this.overlay);
558
}
559
560
contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
561
const context = this.getContextValuesContainer(this.contextId);
562
const result = (rules ? rules.evaluate(context) : true);
563
return result;
564
}
565
566
getContextKeyValue<T>(key: string): T | undefined {
567
return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getContextKeyValue(key);
568
}
569
570
createScoped(): IScopedContextKeyService {
571
throw new Error('Not supported.');
572
}
573
574
createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {
575
return new OverlayContextKeyService(this, overlay);
576
}
577
578
updateParent(): void {
579
throw new Error('Not supported.');
580
}
581
}
582
583
function findContextAttr(domNode: IContextKeyServiceTarget | null): number {
584
while (domNode) {
585
if (domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
586
const attr = domNode.getAttribute(KEYBINDING_CONTEXT_ATTR);
587
if (attr) {
588
return parseInt(attr, 10);
589
}
590
return NaN;
591
}
592
domNode = domNode.parentElement;
593
}
594
return 0;
595
}
596
597
export function setContext(accessor: ServicesAccessor, contextKey: any, contextValue: any) {
598
const contextKeyService = accessor.get(IContextKeyService);
599
contextKeyService.createKey(String(contextKey), stringifyURIs(contextValue));
600
}
601
602
function stringifyURIs(contextValue: any): any {
603
return cloneAndChange(contextValue, (obj) => {
604
if (typeof obj === 'object' && (<MarshalledObject>obj).$mid === MarshalledId.Uri) {
605
return URI.revive(obj).toString();
606
}
607
if (obj instanceof URI) {
608
return obj.toString();
609
}
610
return undefined;
611
});
612
}
613
614
CommandsRegistry.registerCommand('_setContext', setContext);
615
616
CommandsRegistry.registerCommand({
617
id: 'getContextKeyInfo',
618
handler() {
619
return [...RawContextKey.all()].sort((a, b) => a.key.localeCompare(b.key));
620
},
621
metadata: {
622
description: localize('getContextKeyInfo', "A command that returns information about context keys"),
623
args: []
624
}
625
});
626
627
CommandsRegistry.registerCommand('_generateContextKeyInfo', function () {
628
const result: ContextKeyInfo[] = [];
629
const seen = new Set<string>();
630
for (const info of RawContextKey.all()) {
631
if (!seen.has(info.key)) {
632
seen.add(info.key);
633
result.push(info);
634
}
635
}
636
result.sort((a, b) => a.key.localeCompare(b.key));
637
console.log(JSON.stringify(result, undefined, 2));
638
});
639
640