Path: blob/main/src/vs/platform/contextkey/browser/contextKeyService.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 { Emitter, Event, PauseableEmitter } from '../../../base/common/event.js';6import { Iterable } from '../../../base/common/iterator.js';7import { Disposable, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';8import { MarshalledObject } from '../../../base/common/marshalling.js';9import { MarshalledId } from '../../../base/common/marshallingIds.js';10import { cloneAndChange, distinct } from '../../../base/common/objects.js';11import { TernarySearchTree } from '../../../base/common/ternarySearchTree.js';12import { URI } from '../../../base/common/uri.js';13import { localize } from '../../../nls.js';14import { CommandsRegistry } from '../../commands/common/commands.js';15import { ConfigurationTarget, IConfigurationService } from '../../configuration/common/configuration.js';16import { ContextKeyExpression, ContextKeyInfo, ContextKeyValue, IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, IScopedContextKeyService, RawContextKey } from '../common/contextkey.js';17import { ServicesAccessor } from '../../instantiation/common/instantiation.js';1819const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context';2021export class Context implements IContext {2223protected _parent: Context | null;24protected _value: Record<string, any>;25protected _id: number;2627constructor(id: number, parent: Context | null) {28this._id = id;29this._parent = parent;30this._value = Object.create(null);31this._value['_contextId'] = id;32}3334public get value(): Record<string, any> {35return { ...this._value };36}3738public setValue(key: string, value: any): boolean {39// console.log('SET ' + key + ' = ' + value + ' ON ' + this._id);40if (this._value[key] !== value) {41this._value[key] = value;42return true;43}44return false;45}4647public removeValue(key: string): boolean {48// console.log('REMOVE ' + key + ' FROM ' + this._id);49if (key in this._value) {50delete this._value[key];51return true;52}53return false;54}5556public getValue<T>(key: string): T | undefined {57const ret = this._value[key];58if (typeof ret === 'undefined' && this._parent) {59return this._parent.getValue<T>(key);60}61return ret;62}6364public updateParent(parent: Context): void {65this._parent = parent;66}6768public collectAllValues(): Record<string, any> {69let result = this._parent ? this._parent.collectAllValues() : Object.create(null);70result = { ...result, ...this._value };71delete result['_contextId'];72return result;73}74}7576class NullContext extends Context {7778static readonly INSTANCE = new NullContext();7980constructor() {81super(-1, null);82}8384public override setValue(key: string, value: any): boolean {85return false;86}8788public override removeValue(key: string): boolean {89return false;90}9192public override getValue<T>(key: string): T | undefined {93return undefined;94}9596override collectAllValues(): { [key: string]: any } {97return Object.create(null);98}99}100101class ConfigAwareContextValuesContainer extends Context {102private static readonly _keyPrefix = 'config.';103104private readonly _values = TernarySearchTree.forConfigKeys<any>();105private readonly _listener: IDisposable;106107constructor(108id: number,109private readonly _configurationService: IConfigurationService,110emitter: Emitter<IContextKeyChangeEvent>111) {112super(id, null);113114this._listener = this._configurationService.onDidChangeConfiguration(event => {115if (event.source === ConfigurationTarget.DEFAULT) {116// new setting, reset everything117const allKeys = Array.from(this._values, ([k]) => k);118this._values.clear();119emitter.fire(new ArrayContextKeyChangeEvent(allKeys));120} else {121const changedKeys: string[] = [];122for (const configKey of event.affectedKeys) {123const contextKey = `config.${configKey}`;124125const cachedItems = this._values.findSuperstr(contextKey);126if (cachedItems !== undefined) {127changedKeys.push(...Iterable.map(cachedItems, ([key]) => key));128this._values.deleteSuperstr(contextKey);129}130131if (this._values.has(contextKey)) {132changedKeys.push(contextKey);133this._values.delete(contextKey);134}135}136137emitter.fire(new ArrayContextKeyChangeEvent(changedKeys));138}139});140}141142dispose(): void {143this._listener.dispose();144}145146override getValue(key: string): any {147148if (key.indexOf(ConfigAwareContextValuesContainer._keyPrefix) !== 0) {149return super.getValue(key);150}151152if (this._values.has(key)) {153return this._values.get(key);154}155156const configKey = key.substr(ConfigAwareContextValuesContainer._keyPrefix.length);157const configValue = this._configurationService.getValue(configKey);158let value: any = undefined;159switch (typeof configValue) {160case 'number':161case 'boolean':162case 'string':163value = configValue;164break;165default:166if (Array.isArray(configValue)) {167value = JSON.stringify(configValue);168} else {169value = configValue;170}171}172173this._values.set(key, value);174return value;175}176177override setValue(key: string, value: any): boolean {178return super.setValue(key, value);179}180181override removeValue(key: string): boolean {182return super.removeValue(key);183}184185override collectAllValues(): { [key: string]: any } {186const result: { [key: string]: any } = Object.create(null);187this._values.forEach((value, index) => result[index] = value);188return { ...result, ...super.collectAllValues() };189}190}191192class ContextKey<T extends ContextKeyValue> implements IContextKey<T> {193194private _service: AbstractContextKeyService;195private _key: string;196private _defaultValue: T | undefined;197198constructor(service: AbstractContextKeyService, key: string, defaultValue: T | undefined) {199this._service = service;200this._key = key;201this._defaultValue = defaultValue;202this.reset();203}204205public set(value: T): void {206this._service.setContext(this._key, value);207}208209public reset(): void {210if (typeof this._defaultValue === 'undefined') {211this._service.removeContext(this._key);212} else {213this._service.setContext(this._key, this._defaultValue);214}215}216217public get(): T | undefined {218return this._service.getContextKeyValue<T>(this._key);219}220}221222class SimpleContextKeyChangeEvent implements IContextKeyChangeEvent {223constructor(readonly key: string) { }224affectsSome(keys: IReadableSet<string>): boolean {225return keys.has(this.key);226}227allKeysContainedIn(keys: IReadableSet<string>): boolean {228return this.affectsSome(keys);229}230}231232class ArrayContextKeyChangeEvent implements IContextKeyChangeEvent {233constructor(readonly keys: string[]) { }234affectsSome(keys: IReadableSet<string>): boolean {235for (const key of this.keys) {236if (keys.has(key)) {237return true;238}239}240return false;241}242allKeysContainedIn(keys: IReadableSet<string>): boolean {243return this.keys.every(key => keys.has(key));244}245}246247class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent {248constructor(readonly events: IContextKeyChangeEvent[]) { }249affectsSome(keys: IReadableSet<string>): boolean {250for (const e of this.events) {251if (e.affectsSome(keys)) {252return true;253}254}255return false;256}257allKeysContainedIn(keys: IReadableSet<string>): boolean {258return this.events.every(evt => evt.allKeysContainedIn(keys));259}260}261262function allEventKeysInContext(event: IContextKeyChangeEvent, context: Record<string, any>): boolean {263return event.allKeysContainedIn(new Set(Object.keys(context)));264}265266export abstract class AbstractContextKeyService extends Disposable implements IContextKeyService {267declare _serviceBrand: undefined;268269protected _isDisposed: boolean;270protected _myContextId: number;271272protected _onDidChangeContext = this._register(new PauseableEmitter<IContextKeyChangeEvent>({ merge: input => new CompositeContextKeyChangeEvent(input) }));273get onDidChangeContext() { return this._onDidChangeContext.event; }274275constructor(myContextId: number) {276super();277this._isDisposed = false;278this._myContextId = myContextId;279}280281public get contextId(): number {282return this._myContextId;283}284285public createKey<T extends ContextKeyValue>(key: string, defaultValue: T | undefined): IContextKey<T> {286if (this._isDisposed) {287throw new Error(`AbstractContextKeyService has been disposed`);288}289return new ContextKey(this, key, defaultValue);290}291292293bufferChangeEvents(callback: Function): void {294this._onDidChangeContext.pause();295try {296callback();297} finally {298this._onDidChangeContext.resume();299}300}301302public createScoped(domNode: IContextKeyServiceTarget): IScopedContextKeyService {303if (this._isDisposed) {304throw new Error(`AbstractContextKeyService has been disposed`);305}306return new ScopedContextKeyService(this, domNode);307}308309createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {310if (this._isDisposed) {311throw new Error(`AbstractContextKeyService has been disposed`);312}313return new OverlayContextKeyService(this, overlay);314}315316public contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {317if (this._isDisposed) {318throw new Error(`AbstractContextKeyService has been disposed`);319}320const context = this.getContextValuesContainer(this._myContextId);321const result = (rules ? rules.evaluate(context) : true);322// console.group(rules.serialize() + ' -> ' + result);323// rules.keys().forEach(key => { console.log(key, ctx[key]); });324// console.groupEnd();325return result;326}327328public getContextKeyValue<T>(key: string): T | undefined {329if (this._isDisposed) {330return undefined;331}332return this.getContextValuesContainer(this._myContextId).getValue<T>(key);333}334335public setContext(key: string, value: any): void {336if (this._isDisposed) {337return;338}339const myContext = this.getContextValuesContainer(this._myContextId);340if (!myContext) {341return;342}343if (myContext.setValue(key, value)) {344this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));345}346}347348public removeContext(key: string): void {349if (this._isDisposed) {350return;351}352if (this.getContextValuesContainer(this._myContextId).removeValue(key)) {353this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));354}355}356357public getContext(target: IContextKeyServiceTarget | null): IContext {358if (this._isDisposed) {359return NullContext.INSTANCE;360}361return this.getContextValuesContainer(findContextAttr(target));362}363364public abstract getContextValuesContainer(contextId: number): Context;365public abstract createChildContext(parentContextId?: number): number;366public abstract disposeContext(contextId: number): void;367public abstract updateParent(parentContextKeyService?: IContextKeyService): void;368369public override dispose(): void {370super.dispose();371this._isDisposed = true;372}373}374375export class ContextKeyService extends AbstractContextKeyService implements IContextKeyService {376377private _lastContextId: number;378private readonly _contexts = new Map<number, Context>();379380constructor(@IConfigurationService configurationService: IConfigurationService) {381super(0);382this._lastContextId = 0;383384const myContext = this._register(new ConfigAwareContextValuesContainer(this._myContextId, configurationService, this._onDidChangeContext));385this._contexts.set(this._myContextId, myContext);386387// Uncomment this to see the contexts continuously logged388// let lastLoggedValue: string | null = null;389// setInterval(() => {390// let values = Object.keys(this._contexts).map((key) => this._contexts[key]);391// let logValue = values.map(v => JSON.stringify(v._value, null, '\t')).join('\n');392// if (lastLoggedValue !== logValue) {393// lastLoggedValue = logValue;394// console.log(lastLoggedValue);395// }396// }, 2000);397}398399public getContextValuesContainer(contextId: number): Context {400if (this._isDisposed) {401return NullContext.INSTANCE;402}403return this._contexts.get(contextId) || NullContext.INSTANCE;404}405406public createChildContext(parentContextId: number = this._myContextId): number {407if (this._isDisposed) {408throw new Error(`ContextKeyService has been disposed`);409}410const id = (++this._lastContextId);411this._contexts.set(id, new Context(id, this.getContextValuesContainer(parentContextId)));412return id;413}414415public disposeContext(contextId: number): void {416if (!this._isDisposed) {417this._contexts.delete(contextId);418}419}420421public updateParent(_parentContextKeyService: IContextKeyService): void {422throw new Error('Cannot update parent of root ContextKeyService');423}424}425426class ScopedContextKeyService extends AbstractContextKeyService {427428private _parent: AbstractContextKeyService;429private _domNode: IContextKeyServiceTarget;430431private readonly _parentChangeListener = this._register(new MutableDisposable());432433constructor(parent: AbstractContextKeyService, domNode: IContextKeyServiceTarget) {434super(parent.createChildContext());435this._parent = parent;436this._updateParentChangeListener();437438this._domNode = domNode;439if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {440let extraInfo = '';441if ((this._domNode as HTMLElement).classList) {442extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', ');443}444445console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`);446}447this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));448}449450private _updateParentChangeListener(): void {451// Forward parent events to this listener. Parent will change.452this._parentChangeListener.value = this._parent.onDidChangeContext(e => {453const thisContainer = this._parent.getContextValuesContainer(this._myContextId);454const thisContextValues = thisContainer.value;455456if (!allEventKeysInContext(e, thisContextValues)) {457this._onDidChangeContext.fire(e);458}459});460}461462public override dispose(): void {463if (this._isDisposed) {464return;465}466467this._parent.disposeContext(this._myContextId);468this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);469super.dispose();470}471472public getContextValuesContainer(contextId: number): Context {473if (this._isDisposed) {474return NullContext.INSTANCE;475}476return this._parent.getContextValuesContainer(contextId);477}478479public createChildContext(parentContextId: number = this._myContextId): number {480if (this._isDisposed) {481throw new Error(`ScopedContextKeyService has been disposed`);482}483return this._parent.createChildContext(parentContextId);484}485486public disposeContext(contextId: number): void {487if (this._isDisposed) {488return;489}490this._parent.disposeContext(contextId);491}492493public updateParent(parentContextKeyService: AbstractContextKeyService): void {494if (this._parent === parentContextKeyService) {495return;496}497498const thisContainer = this._parent.getContextValuesContainer(this._myContextId);499const oldAllValues = thisContainer.collectAllValues();500this._parent = parentContextKeyService;501this._updateParentChangeListener();502const newParentContainer = this._parent.getContextValuesContainer(this._parent.contextId);503thisContainer.updateParent(newParentContainer);504505const newAllValues = thisContainer.collectAllValues();506const allValuesDiff = {507...distinct(oldAllValues, newAllValues),508...distinct(newAllValues, oldAllValues)509};510const changedKeys = Object.keys(allValuesDiff);511512this._onDidChangeContext.fire(new ArrayContextKeyChangeEvent(changedKeys));513}514}515516class OverlayContext implements IContext {517518constructor(private parent: IContext, private overlay: ReadonlyMap<string, any>) { }519520getValue<T extends ContextKeyValue>(key: string): T | undefined {521return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getValue<T>(key);522}523}524525class OverlayContextKeyService implements IContextKeyService {526527declare _serviceBrand: undefined;528private overlay: Map<string, any>;529530get contextId(): number {531return this.parent.contextId;532}533534get onDidChangeContext(): Event<IContextKeyChangeEvent> {535return this.parent.onDidChangeContext;536}537538constructor(private parent: AbstractContextKeyService | OverlayContextKeyService, overlay: Iterable<[string, any]>) {539this.overlay = new Map(overlay);540}541542bufferChangeEvents(callback: Function): void {543this.parent.bufferChangeEvents(callback);544}545546createKey<T extends ContextKeyValue>(): IContextKey<T> {547throw new Error('Not supported.');548}549550getContext(target: IContextKeyServiceTarget | null): IContext {551return new OverlayContext(this.parent.getContext(target), this.overlay);552}553554getContextValuesContainer(contextId: number): IContext {555const parentContext = this.parent.getContextValuesContainer(contextId);556return new OverlayContext(parentContext, this.overlay);557}558559contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {560const context = this.getContextValuesContainer(this.contextId);561const result = (rules ? rules.evaluate(context) : true);562return result;563}564565getContextKeyValue<T>(key: string): T | undefined {566return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getContextKeyValue(key);567}568569createScoped(): IScopedContextKeyService {570throw new Error('Not supported.');571}572573createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {574return new OverlayContextKeyService(this, overlay);575}576577updateParent(): void {578throw new Error('Not supported.');579}580}581582function findContextAttr(domNode: IContextKeyServiceTarget | null): number {583while (domNode) {584if (domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {585const attr = domNode.getAttribute(KEYBINDING_CONTEXT_ATTR);586if (attr) {587return parseInt(attr, 10);588}589return NaN;590}591domNode = domNode.parentElement;592}593return 0;594}595596export function setContext(accessor: ServicesAccessor, contextKey: any, contextValue: any) {597const contextKeyService = accessor.get(IContextKeyService);598contextKeyService.createKey(String(contextKey), stringifyURIs(contextValue));599}600601function stringifyURIs(contextValue: any): any {602return cloneAndChange(contextValue, (obj) => {603if (typeof obj === 'object' && (<MarshalledObject>obj).$mid === MarshalledId.Uri) {604return URI.revive(obj).toString();605}606if (obj instanceof URI) {607return obj.toString();608}609return undefined;610});611}612613CommandsRegistry.registerCommand('_setContext', setContext);614615CommandsRegistry.registerCommand({616id: 'getContextKeyInfo',617handler() {618return [...RawContextKey.all()].sort((a, b) => a.key.localeCompare(b.key));619},620metadata: {621description: localize('getContextKeyInfo', "A command that returns information about context keys"),622args: []623}624});625626CommandsRegistry.registerCommand('_generateContextKeyInfo', function () {627const result: ContextKeyInfo[] = [];628const seen = new Set<string>();629for (const info of RawContextKey.all()) {630if (!seen.has(info.key)) {631seen.add(info.key);632result.push(info);633}634}635result.sort((a, b) => a.key.localeCompare(b.key));636console.log(JSON.stringify(result, undefined, 2));637});638639640