Path: blob/main/src/vs/platform/contextkey/browser/contextKeyService.ts
5240 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, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';8import { MarshalledObject } from '../../../base/common/marshalling.js';9import { MarshalledId } from '../../../base/common/marshallingIds.js';10import { cloneAndChange, distinct, equals } 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';18import { InputFocusedContext } from '../common/contextkeys.js';19import { mainWindow } from '../../../base/browser/window.js';20import { addDisposableListener, EventType, getActiveWindow, isEditableElement, onDidRegisterWindow, trackFocus } from '../../../base/browser/dom.js';2122const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context';2324export class Context implements IContext {2526protected _parent: Context | null;27protected _value: Record<string, any>;28protected _id: number;2930constructor(id: number, parent: Context | null) {31this._id = id;32this._parent = parent;33this._value = Object.create(null);34this._value['_contextId'] = id;35}3637public get value(): Record<string, any> {38return { ...this._value };39}4041public setValue(key: string, value: any): boolean {42// console.log('SET ' + key + ' = ' + value + ' ON ' + this._id);43if (!equals(this._value[key], value)) {44this._value[key] = value;45return true;46}47return false;48}4950public removeValue(key: string): boolean {51// console.log('REMOVE ' + key + ' FROM ' + this._id);52if (key in this._value) {53delete this._value[key];54return true;55}56return false;57}5859public getValue<T>(key: string): T | undefined {60const ret = this._value[key];61if (typeof ret === 'undefined' && this._parent) {62return this._parent.getValue<T>(key);63}64return ret;65}6667public updateParent(parent: Context): void {68this._parent = parent;69}7071public collectAllValues(): Record<string, any> {72let result = this._parent ? this._parent.collectAllValues() : Object.create(null);73result = { ...result, ...this._value };74delete result['_contextId'];75return result;76}77}7879class NullContext extends Context {8081static readonly INSTANCE = new NullContext();8283constructor() {84super(-1, null);85}8687public override setValue(key: string, value: any): boolean {88return false;89}9091public override removeValue(key: string): boolean {92return false;93}9495public override getValue<T>(key: string): T | undefined {96return undefined;97}9899override collectAllValues(): { [key: string]: any } {100return Object.create(null);101}102}103104class ConfigAwareContextValuesContainer extends Context {105private static readonly _keyPrefix = 'config.';106107private readonly _values = TernarySearchTree.forConfigKeys<any>();108private readonly _listener: IDisposable;109110constructor(111id: number,112private readonly _configurationService: IConfigurationService,113emitter: Emitter<IContextKeyChangeEvent>114) {115super(id, null);116117this._listener = this._configurationService.onDidChangeConfiguration(event => {118if (event.source === ConfigurationTarget.DEFAULT) {119// new setting, reset everything120const allKeys = Array.from(this._values, ([k]) => k);121this._values.clear();122emitter.fire(new ArrayContextKeyChangeEvent(allKeys));123} else {124const changedKeys: string[] = [];125for (const configKey of event.affectedKeys) {126const contextKey = `config.${configKey}`;127128const cachedItems = this._values.findSuperstr(contextKey);129if (cachedItems !== undefined) {130changedKeys.push(...Iterable.map(cachedItems, ([key]) => key));131this._values.deleteSuperstr(contextKey);132}133134if (this._values.has(contextKey)) {135changedKeys.push(contextKey);136this._values.delete(contextKey);137}138}139140emitter.fire(new ArrayContextKeyChangeEvent(changedKeys));141}142});143}144145dispose(): void {146this._listener.dispose();147}148149override getValue(key: string): any {150151if (key.indexOf(ConfigAwareContextValuesContainer._keyPrefix) !== 0) {152return super.getValue(key);153}154155if (this._values.has(key)) {156return this._values.get(key);157}158159const configKey = key.substr(ConfigAwareContextValuesContainer._keyPrefix.length);160const configValue = this._configurationService.getValue(configKey);161let value: any = undefined;162switch (typeof configValue) {163case 'number':164case 'boolean':165case 'string':166value = configValue;167break;168default:169if (Array.isArray(configValue)) {170value = JSON.stringify(configValue);171} else {172value = configValue;173}174}175176this._values.set(key, value);177return value;178}179180override setValue(key: string, value: any): boolean {181return super.setValue(key, value);182}183184override removeValue(key: string): boolean {185return super.removeValue(key);186}187188override collectAllValues(): { [key: string]: any } {189const result: { [key: string]: any } = Object.create(null);190this._values.forEach((value, index) => result[index] = value);191return { ...result, ...super.collectAllValues() };192}193}194195class ContextKey<T extends ContextKeyValue> implements IContextKey<T> {196197private _service: AbstractContextKeyService;198private _key: string;199private _defaultValue: T | undefined;200201constructor(service: AbstractContextKeyService, key: string, defaultValue: T | undefined) {202this._service = service;203this._key = key;204this._defaultValue = defaultValue;205this.reset();206}207208public set(value: T): void {209this._service.setContext(this._key, value);210}211212public reset(): void {213if (typeof this._defaultValue === 'undefined') {214this._service.removeContext(this._key);215} else {216this._service.setContext(this._key, this._defaultValue);217}218}219220public get(): T | undefined {221return this._service.getContextKeyValue<T>(this._key);222}223}224225class SimpleContextKeyChangeEvent implements IContextKeyChangeEvent {226constructor(readonly key: string) { }227affectsSome(keys: IReadableSet<string>): boolean {228return keys.has(this.key);229}230allKeysContainedIn(keys: IReadableSet<string>): boolean {231return this.affectsSome(keys);232}233}234235class ArrayContextKeyChangeEvent implements IContextKeyChangeEvent {236constructor(readonly keys: string[]) { }237affectsSome(keys: IReadableSet<string>): boolean {238for (const key of this.keys) {239if (keys.has(key)) {240return true;241}242}243return false;244}245allKeysContainedIn(keys: IReadableSet<string>): boolean {246return this.keys.every(key => keys.has(key));247}248}249250class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent {251constructor(readonly events: IContextKeyChangeEvent[]) { }252affectsSome(keys: IReadableSet<string>): boolean {253for (const e of this.events) {254if (e.affectsSome(keys)) {255return true;256}257}258return false;259}260allKeysContainedIn(keys: IReadableSet<string>): boolean {261return this.events.every(evt => evt.allKeysContainedIn(keys));262}263}264265function allEventKeysInContext(event: IContextKeyChangeEvent, context: Record<string, any>): boolean {266return event.allKeysContainedIn(new Set(Object.keys(context)));267}268269export abstract class AbstractContextKeyService extends Disposable implements IContextKeyService {270declare _serviceBrand: undefined;271272protected _isDisposed: boolean;273protected _myContextId: number;274275protected _onDidChangeContext = this._register(new PauseableEmitter<IContextKeyChangeEvent>({ merge: input => new CompositeContextKeyChangeEvent(input) }));276get onDidChangeContext() { return this._onDidChangeContext.event; }277278constructor(myContextId: number) {279super();280this._isDisposed = false;281this._myContextId = myContextId;282}283284public get contextId(): number {285return this._myContextId;286}287288public createKey<T extends ContextKeyValue>(key: string, defaultValue: T | undefined): IContextKey<T> {289if (this._isDisposed) {290throw new Error(`AbstractContextKeyService has been disposed`);291}292return new ContextKey(this, key, defaultValue);293}294295296bufferChangeEvents(callback: Function): void {297this._onDidChangeContext.pause();298try {299callback();300} finally {301this._onDidChangeContext.resume();302}303}304305public createScoped(domNode: IContextKeyServiceTarget): IScopedContextKeyService {306if (this._isDisposed) {307throw new Error(`AbstractContextKeyService has been disposed`);308}309return new ScopedContextKeyService(this, domNode);310}311312createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {313if (this._isDisposed) {314throw new Error(`AbstractContextKeyService has been disposed`);315}316return new OverlayContextKeyService(this, overlay);317}318319public contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {320if (this._isDisposed) {321throw new Error(`AbstractContextKeyService has been disposed`);322}323const context = this.getContextValuesContainer(this._myContextId);324const result = (rules ? rules.evaluate(context) : true);325// console.group(rules.serialize() + ' -> ' + result);326// rules.keys().forEach(key => { console.log(key, ctx[key]); });327// console.groupEnd();328return result;329}330331public getContextKeyValue<T>(key: string): T | undefined {332if (this._isDisposed) {333return undefined;334}335return this.getContextValuesContainer(this._myContextId).getValue<T>(key);336}337338public setContext(key: string, value: any): void {339if (this._isDisposed) {340return;341}342const myContext = this.getContextValuesContainer(this._myContextId);343if (!myContext) {344return;345}346if (myContext.setValue(key, value)) {347this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));348}349}350351public removeContext(key: string): void {352if (this._isDisposed) {353return;354}355if (this.getContextValuesContainer(this._myContextId).removeValue(key)) {356this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));357}358}359360public getContext(target: IContextKeyServiceTarget | null): IContext {361if (this._isDisposed) {362return NullContext.INSTANCE;363}364return this.getContextValuesContainer(findContextAttr(target));365}366367public abstract getContextValuesContainer(contextId: number): Context;368public abstract createChildContext(parentContextId?: number): number;369public abstract disposeContext(contextId: number): void;370public abstract updateParent(parentContextKeyService?: IContextKeyService): void;371372public override dispose(): void {373super.dispose();374this._isDisposed = true;375}376}377378export class ContextKeyService extends AbstractContextKeyService implements IContextKeyService {379380private _lastContextId: number;381private readonly _contexts = new Map<number, Context>();382383private inputFocusedContext: IContextKey<boolean>;384385constructor(@IConfigurationService configurationService: IConfigurationService) {386super(0);387this._lastContextId = 0;388this.inputFocusedContext = InputFocusedContext.bindTo(this);389390const myContext = this._register(new ConfigAwareContextValuesContainer(this._myContextId, configurationService, this._onDidChangeContext));391this._contexts.set(this._myContextId, myContext);392393// Uncomment this to see the contexts continuously logged394// let lastLoggedValue: string | null = null;395// setInterval(() => {396// let values = Object.keys(this._contexts).map((key) => this._contexts[key]);397// let logValue = values.map(v => JSON.stringify(v._value, null, '\t')).join('\n');398// if (lastLoggedValue !== logValue) {399// lastLoggedValue = logValue;400// console.log(lastLoggedValue);401// }402// }, 2000);403404this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {405const onFocusDisposables = disposables.add(new MutableDisposable<DisposableStore>());406disposables.add(addDisposableListener(window, EventType.FOCUS_IN, () => {407onFocusDisposables.value = new DisposableStore();408this.updateInputContextKeys(window.document, onFocusDisposables.value);409}, true));410}, { window: mainWindow, disposables: this._store }));411}412413private updateInputContextKeys(ownerDocument: Document, disposables: DisposableStore): void {414415function activeElementIsInput(): boolean {416return !!ownerDocument.activeElement && isEditableElement(ownerDocument.activeElement);417}418419const isInputFocused = activeElementIsInput();420this.inputFocusedContext.set(isInputFocused);421422if (isInputFocused) {423const tracker = disposables.add(trackFocus(ownerDocument.activeElement as HTMLElement));424Event.once(tracker.onDidBlur)(() => {425426// Ensure we are only updating the context key if we are427// still in the same document that we are tracking. This428// fixes a race condition in multi-window setups where429// the blur event arrives in the inactive window overwriting430// the context key of the active window. This is because431// blur events from the focus tracker are emitted with a432// timeout of 0.433434if (getActiveWindow().document === ownerDocument) {435this.inputFocusedContext.set(activeElementIsInput());436}437438tracker.dispose();439}, undefined, disposables);440}441}442443public getContextValuesContainer(contextId: number): Context {444if (this._isDisposed) {445return NullContext.INSTANCE;446}447return this._contexts.get(contextId) || NullContext.INSTANCE;448}449450public createChildContext(parentContextId: number = this._myContextId): number {451if (this._isDisposed) {452throw new Error(`ContextKeyService has been disposed`);453}454const id = (++this._lastContextId);455this._contexts.set(id, new Context(id, this.getContextValuesContainer(parentContextId)));456return id;457}458459public disposeContext(contextId: number): void {460if (!this._isDisposed) {461this._contexts.delete(contextId);462}463}464465public updateParent(_parentContextKeyService: IContextKeyService): void {466throw new Error('Cannot update parent of root ContextKeyService');467}468}469470class ScopedContextKeyService extends AbstractContextKeyService {471472private _parent: AbstractContextKeyService;473private _domNode: IContextKeyServiceTarget;474475private readonly _parentChangeListener = this._register(new MutableDisposable());476477constructor(parent: AbstractContextKeyService, domNode: IContextKeyServiceTarget) {478super(parent.createChildContext());479this._parent = parent;480this._updateParentChangeListener();481482this._domNode = domNode;483if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {484let extraInfo = '';485if ((this._domNode as HTMLElement).classList) {486extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(', ');487}488489console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`);490}491this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));492}493494private _updateParentChangeListener(): void {495// Forward parent events to this listener. Parent will change.496this._parentChangeListener.value = this._parent.onDidChangeContext(e => {497const thisContainer = this._parent.getContextValuesContainer(this._myContextId);498const thisContextValues = thisContainer.value;499500if (!allEventKeysInContext(e, thisContextValues)) {501this._onDidChangeContext.fire(e);502}503});504}505506public override dispose(): void {507if (this._isDisposed) {508return;509}510511this._parent.disposeContext(this._myContextId);512this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);513super.dispose();514}515516public getContextValuesContainer(contextId: number): Context {517if (this._isDisposed) {518return NullContext.INSTANCE;519}520return this._parent.getContextValuesContainer(contextId);521}522523public createChildContext(parentContextId: number = this._myContextId): number {524if (this._isDisposed) {525throw new Error(`ScopedContextKeyService has been disposed`);526}527return this._parent.createChildContext(parentContextId);528}529530public disposeContext(contextId: number): void {531if (this._isDisposed) {532return;533}534this._parent.disposeContext(contextId);535}536537public updateParent(parentContextKeyService: AbstractContextKeyService): void {538if (this._parent === parentContextKeyService) {539return;540}541542const thisContainer = this._parent.getContextValuesContainer(this._myContextId);543const oldAllValues = thisContainer.collectAllValues();544this._parent = parentContextKeyService;545this._updateParentChangeListener();546const newParentContainer = this._parent.getContextValuesContainer(this._parent.contextId);547thisContainer.updateParent(newParentContainer);548549const newAllValues = thisContainer.collectAllValues();550const allValuesDiff = {551...distinct(oldAllValues, newAllValues),552...distinct(newAllValues, oldAllValues)553};554const changedKeys = Object.keys(allValuesDiff);555556this._onDidChangeContext.fire(new ArrayContextKeyChangeEvent(changedKeys));557}558}559560class OverlayContext implements IContext {561562constructor(private parent: IContext, private overlay: ReadonlyMap<string, any>) { }563564getValue<T extends ContextKeyValue>(key: string): T | undefined {565return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getValue<T>(key);566}567}568569class OverlayContextKeyService implements IContextKeyService {570571declare _serviceBrand: undefined;572private overlay: Map<string, any>;573574get contextId(): number {575return this.parent.contextId;576}577578get onDidChangeContext(): Event<IContextKeyChangeEvent> {579return this.parent.onDidChangeContext;580}581582constructor(private parent: AbstractContextKeyService | OverlayContextKeyService, overlay: Iterable<[string, any]>) {583this.overlay = new Map(overlay);584}585586bufferChangeEvents(callback: Function): void {587this.parent.bufferChangeEvents(callback);588}589590createKey<T extends ContextKeyValue>(): IContextKey<T> {591throw new Error('Not supported.');592}593594getContext(target: IContextKeyServiceTarget | null): IContext {595return new OverlayContext(this.parent.getContext(target), this.overlay);596}597598getContextValuesContainer(contextId: number): IContext {599const parentContext = this.parent.getContextValuesContainer(contextId);600return new OverlayContext(parentContext, this.overlay);601}602603contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {604const context = this.getContextValuesContainer(this.contextId);605const result = (rules ? rules.evaluate(context) : true);606return result;607}608609getContextKeyValue<T>(key: string): T | undefined {610return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getContextKeyValue(key);611}612613createScoped(): IScopedContextKeyService {614throw new Error('Not supported.');615}616617createOverlay(overlay: Iterable<[string, any]> = Iterable.empty()): IContextKeyService {618return new OverlayContextKeyService(this, overlay);619}620621updateParent(): void {622throw new Error('Not supported.');623}624}625626function findContextAttr(domNode: IContextKeyServiceTarget | null): number {627while (domNode) {628if (domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {629const attr = domNode.getAttribute(KEYBINDING_CONTEXT_ATTR);630if (attr) {631return parseInt(attr, 10);632}633return NaN;634}635domNode = domNode.parentElement;636}637return 0;638}639640export function setContext(accessor: ServicesAccessor, contextKey: any, contextValue: any) {641const contextKeyService = accessor.get(IContextKeyService);642contextKeyService.createKey(String(contextKey), stringifyURIs(contextValue));643}644645function stringifyURIs(contextValue: any): any {646return cloneAndChange(contextValue, (obj) => {647if (typeof obj === 'object' && (<MarshalledObject>obj).$mid === MarshalledId.Uri) {648return URI.revive(obj).toString();649}650if (obj instanceof URI) {651return obj.toString();652}653return undefined;654});655}656657CommandsRegistry.registerCommand('_setContext', setContext);658659CommandsRegistry.registerCommand({660id: 'getContextKeyInfo',661handler() {662return [...RawContextKey.all()].sort((a, b) => a.key.localeCompare(b.key));663},664metadata: {665description: localize('getContextKeyInfo', "A command that returns information about context keys"),666args: []667}668});669670CommandsRegistry.registerCommand('_generateContextKeyInfo', function () {671const result: ContextKeyInfo[] = [];672const seen = new Set<string>();673for (const info of RawContextKey.all()) {674if (!seen.has(info.key)) {675seen.add(info.key);676result.push(info);677}678}679result.sort((a, b) => a.key.localeCompare(b.key));680console.log(JSON.stringify(result, undefined, 2));681});682683684