@difizen/mana-core
Version:
696 lines (591 loc) • 19.8 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { Emitter, Event } from '@difizen/mana-common';
import { Iterable } from '@difizen/mana-common';
import { PauseableEmitter } from '@difizen/mana-common';
import { objects } from '@difizen/mana-common';
import { prop } from '@difizen/mana-observable';
import { inject, injectable, singleton } from '@difizen/mana-syringe';
import type { CommandRegistry } from '../../command/index';
import type { CommandContribution } from '../../command/index';
import { DisposableStore, MutableDisposable } from '../../utils';
import type { IDisposable } from '../../utils/index';
import { TernarySearchTree } from '../../utils/map';
import {
ConfigurationTarget,
IConfigurationService,
} from './configuration/configuration';
import type {
IContext,
IContextKeyChangeEvent,
IContextKeyServiceTarget,
IReadableSet,
ContextKeyExpression,
ContextKeyInfo,
IContextKey,
} from './contextkey';
import {
SET_CONTEXT_COMMAND_ID,
RawContextKey,
IContextKeyService,
} from './contextkey';
const KEYBINDING_CONTEXT_ATTR = 'data-mana-keybinding-context';
export class Context implements IContext {
protected _parent: Context | null;
protected _value: Record<string, any>;
protected _id: number;
constructor(id: number, parent: Context | null) {
this._id = id;
this._parent = parent;
this._value = Object.create(null);
this._value['_contextId'] = id;
}
public setValue(key: string, value: any): boolean {
if (this._value[key] !== value) {
this._value[key] = value;
return true;
}
return false;
}
public removeValue(key: string): boolean {
if (key in this._value) {
delete this._value[key];
return true;
}
return false;
}
public getValue<T>(key: string): T | undefined {
const ret = this._value[key];
if (typeof ret === 'undefined' && this._parent) {
return this._parent.getValue<T>(key);
}
return ret;
}
public updateParent(parent: Context): void {
this._parent = parent;
}
public collectAllValues(): Record<string, any> {
let result = this._parent ? this._parent.collectAllValues() : Object.create(null);
result = { ...result, ...this._value };
delete result._contextId;
return result;
}
}
class NullContext extends Context {
static readonly INSTANCE = new NullContext();
constructor() {
super(-1, null);
}
public override setValue(_key: string, _value: any): boolean {
return false;
}
public override removeValue(_key: string): boolean {
return false;
}
public override getValue<T>(_key: string): T | undefined {
return undefined;
}
override collectAllValues(): Record<string, any> {
return Object.create(null);
}
}
class ConfigAwareContextValuesContainer extends Context {
private static readonly _keyPrefix = 'config.';
private readonly _values = TernarySearchTree.forConfigKeys<any>();
private readonly _listener: IDisposable;
private readonly _configurationService: IConfigurationService;
constructor(
id: number,
_configurationService: IConfigurationService,
emitter: Emitter<IContextKeyChangeEvent>,
) {
super(id, null);
this._configurationService = _configurationService;
this._listener = this._configurationService.onDidChangeConfiguration((event) => {
if (event.source === ConfigurationTarget.DEFAULT) {
// new setting, reset everything
const allKeys = Array.from(Iterable.map(this._values, ([k]) => k));
this._values.clear();
emitter.fire(new ArrayContextKeyChangeEvent(allKeys));
} else {
const changedKeys: string[] = [];
for (const configKey of event.affectedKeys) {
const contextKey = `config.${configKey}`;
const cachedItems = this._values.findSuperstr(contextKey);
if (cachedItems !== undefined) {
changedKeys.push(...Iterable.map(cachedItems, ([key]) => key));
this._values.deleteSuperstr(contextKey);
}
if (this._values.has(contextKey)) {
changedKeys.push(contextKey);
this._values.delete(contextKey);
}
}
emitter.fire(new ArrayContextKeyChangeEvent(changedKeys));
}
});
}
dispose(): void {
this._listener.dispose();
}
override getValue(key: string): any {
if (key.indexOf(ConfigAwareContextValuesContainer._keyPrefix) !== 0) {
return super.getValue(key);
}
if (this._values.has(key)) {
return this._values.get(key);
}
const configKey = key.substr(ConfigAwareContextValuesContainer._keyPrefix.length);
const configValue = this._configurationService.getValue(configKey);
let value: any;
switch (typeof configValue) {
case 'number':
case 'boolean':
case 'string':
value = configValue;
break;
default:
if (Array.isArray(configValue)) {
value = JSON.stringify(configValue);
} else {
value = configValue;
}
}
this._values.set(key, value);
return value;
}
override setValue(key: string, value: any): boolean {
return super.setValue(key, value);
}
override removeValue(key: string): boolean {
return super.removeValue(key);
}
override collectAllValues(): Record<string, any> {
const result: Record<string, any> = Object.create(null);
this._values.forEach((value, index) => (result[index] = value));
return { ...result, ...super.collectAllValues() };
}
}
class ContextKey<T> implements IContextKey<T> {
private _service: AbstractContextKeyService;
private _key: string;
private _defaultValue: T | undefined;
constructor(
service: AbstractContextKeyService,
key: string,
defaultValue: T | undefined,
) {
this._service = service;
this._key = key;
this._defaultValue = defaultValue;
this.reset();
}
public set(value: T): void {
this._service.setContext(this._key, value);
}
public reset(): void {
if (typeof this._defaultValue === 'undefined') {
this._service.removeContext(this._key);
} else {
this._service.setContext(this._key, this._defaultValue);
}
}
public get(): T | undefined {
return this._service.getContextKeyValue<T>(this._key);
}
}
class SimpleContextKeyChangeEvent implements IContextKeyChangeEvent {
readonly key: string;
constructor(key: string) {
this.key = key;
}
affectsSome(keys: IReadableSet<string>): boolean {
return keys.has(this.key);
}
}
class ArrayContextKeyChangeEvent implements IContextKeyChangeEvent {
readonly keys: string[];
constructor(keys: string[]) {
this.keys = keys;
}
affectsSome(keys: IReadableSet<string>): boolean {
for (const key of this.keys) {
if (keys.has(key)) {
return true;
}
}
return false;
}
}
class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent {
readonly events: IContextKeyChangeEvent[];
constructor(events: IContextKeyChangeEvent[]) {
this.events = events;
}
affectsSome(keys: IReadableSet<string>): boolean {
for (const e of this.events) {
if (e.affectsSome(keys)) {
return true;
}
}
return false;
}
}
export abstract class AbstractContextKeyService implements IContextKeyService {
_serviceBrand: undefined;
protected _isDisposed: boolean;
protected _myContextId: number;
protected _onDidChangeContext = new PauseableEmitter<IContextKeyChangeEvent>({
merge: (input) => new CompositeContextKeyChangeEvent(input),
});
readonly onDidChangeContext = this._onDidChangeContext.event;
constructor(myContextId: number) {
this._isDisposed = false;
this._myContextId = myContextId;
}
public get contextId(): number {
return this._myContextId;
}
abstract dispose(): void;
public createKey<T>(key: string, defaultValue: T | undefined): IContextKey<T> {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new ContextKey(this, key, defaultValue);
}
bufferChangeEvents(callback: () => void): void {
this._onDidChangeContext.pause();
try {
callback();
} finally {
this._onDidChangeContext.resume();
}
}
public createScoped(domNode: IContextKeyServiceTarget): IContextKeyService {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new ScopedContextKeyService(this, domNode) as unknown as IContextKeyService;
}
createOverlay(
overlay: Iterable<[string, any]> = Iterable.empty(),
): IContextKeyService {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new OverlayContextKeyService(this, overlay);
}
public contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
const context = this.getContextValuesContainer(this._myContextId);
if (!rules) {
return true;
}
return rules.evaluate(context);
// console.group(rules.serialize() + ' -> ' + result);
// rules.keys().forEach(key => { console.log(key, ctx[key]); });
// console.groupEnd();
// return result;
}
public getContextKeyValue<T>(key: string): T | undefined {
if (this._isDisposed) {
return undefined;
}
return this.getContextValuesContainer(this._myContextId).getValue<T>(key);
}
public setContext(key: string, value: any): void {
if (this._isDisposed) {
return;
}
const myContext = this.getContextValuesContainer(this._myContextId);
if (!myContext) {
return;
}
if (myContext.setValue(key, value)) {
this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));
}
}
public removeContext(key: string): void {
if (this._isDisposed) {
return;
}
if (this.getContextValuesContainer(this._myContextId).removeValue(key)) {
this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));
}
}
public getContext(target: IContextKeyServiceTarget | null): IContext {
if (this._isDisposed) {
return NullContext.INSTANCE;
}
return this.getContextValuesContainer(findContextAttr(target));
}
public abstract getContextValuesContainer(contextId: number): Context;
public abstract createChildContext(parentContextId?: number): number;
public abstract disposeContext(contextId: number): void;
public abstract updateParent(parentContextKeyService?: IContextKeyService): void;
}
export class VSContextKeyService
extends AbstractContextKeyService
implements IContextKeyService, CommandContribution
{
private _lastContextId: number;
private readonly _contexts = new Map<number, Context>();
private readonly _toDispose = new DisposableStore();
configurationService: IConfigurationService;
constructor(
configurationService: IConfigurationService,
) {
super(0);
this.configurationService = configurationService;
this._lastContextId = 0;
const myContext = new ConfigAwareContextValuesContainer(
this._myContextId,
configurationService,
this._onDidChangeContext,
);
this._contexts.set(this._myContextId, myContext);
this._toDispose.add(myContext);
// Uncomment this to see the contexts continuously logged
// let lastLoggedValue: string | null = null;
// setInterval(() => {
// let values = Object.keys(this._contexts).map((key) => this._contexts[key]);
// let logValue = values.map(v => JSON.stringify(v._value, null, '\t')).join('\n');
// if (lastLoggedValue !== logValue) {
// lastLoggedValue = logValue;
// console.log(lastLoggedValue);
// }
// }, 2000);
}
registerCommands(commands: CommandRegistry) {
commands.registerCommand(
{ id: SET_CONTEXT_COMMAND_ID },
{
execute: (contextKey: any, contextValue: any) => {
this.createKey(String(contextKey), contextValue);
},
},
);
commands.registerCommand(
{
id: 'getContextKeyInfo',
label:
'getContextKeyInfo, A command that returns information about context keys',
},
{
execute: () => {
return [...RawContextKey.all()].sort((a, b) => a.key.localeCompare(b.key));
},
},
);
commands.registerCommand(
{ id: '_generateContextKeyInfo' },
{
execute: () => {
const result: ContextKeyInfo[] = [];
const seen = new Set<string>();
for (const info of RawContextKey.all()) {
if (!seen.has(info.key)) {
seen.add(info.key);
result.push(info);
}
}
result.sort((a, b) => a.key.localeCompare(b.key));
},
},
);
}
public dispose(): void {
this._onDidChangeContext.dispose();
this._isDisposed = true;
this._toDispose.dispose();
}
public getContextValuesContainer(contextId: number): Context {
if (this._isDisposed) {
return NullContext.INSTANCE;
}
return this._contexts.get(contextId) || NullContext.INSTANCE;
}
public createChildContext(parentContextId: number = this._myContextId): number {
if (this._isDisposed) {
throw new Error(`ContextKeyService has been disposed`);
}
const id = ++this._lastContextId;
this._contexts.set(
id,
new Context(id, this.getContextValuesContainer(parentContextId)),
);
return id;
}
public disposeContext(contextId: number): void {
if (!this._isDisposed) {
this._contexts.delete(contextId);
}
}
public updateParent(_parentContextKeyService: IContextKeyService): void {
throw new Error('Cannot update parent of root ContextKeyService');
}
}
class ScopedContextKeyService extends AbstractContextKeyService {
private _parent: AbstractContextKeyService;
private _domNode: IContextKeyServiceTarget;
private readonly _parentChangeListener = new MutableDisposable();
constructor(parent: AbstractContextKeyService, domNode: IContextKeyServiceTarget) {
super(parent.createChildContext());
this._parent = parent;
this._updateParentChangeListener();
this._domNode = domNode;
if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
let extraInfo = '';
if ((this._domNode as HTMLElement).classList) {
extraInfo = Array.from((this._domNode as HTMLElement).classList.values()).join(
', ',
);
}
console.error(
`Element already has context attribute${extraInfo ? `: ${extraInfo}` : ''}`,
);
}
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
}
private _updateParentChangeListener(): void {
// Forward parent events to this listener. Parent will change.
this._parentChangeListener.value = this._parent.onDidChangeContext(
this._onDidChangeContext.fire,
this._onDidChangeContext,
);
}
public dispose(): void {
if (this._isDisposed) {
return;
}
this._onDidChangeContext.dispose();
this._parent.disposeContext(this._myContextId);
this._parentChangeListener.dispose();
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
this._isDisposed = true;
}
public getContextValuesContainer(contextId: number): Context {
if (this._isDisposed) {
return NullContext.INSTANCE;
}
return this._parent.getContextValuesContainer(contextId);
}
public createChildContext(parentContextId: number = this._myContextId): number {
if (this._isDisposed) {
throw new Error(`ScopedContextKeyService has been disposed`);
}
return this._parent.createChildContext(parentContextId);
}
public disposeContext(contextId: number): void {
if (this._isDisposed) {
return;
}
this._parent.disposeContext(contextId);
}
public updateParent(parentContextKeyService: AbstractContextKeyService): void {
const thisContainer = this._parent.getContextValuesContainer(this._myContextId);
const oldAllValues = thisContainer.collectAllValues();
this._parent = parentContextKeyService;
this._updateParentChangeListener();
const newParentContainer = this._parent.getContextValuesContainer(
this._parent.contextId,
);
thisContainer.updateParent(newParentContainer);
const newAllValues = thisContainer.collectAllValues();
const allValuesDiff = {
...objects.distinct(oldAllValues, newAllValues),
...objects.distinct(newAllValues, oldAllValues),
};
const changedKeys = Object.keys(allValuesDiff);
this._onDidChangeContext.fire(new ArrayContextKeyChangeEvent(changedKeys));
}
}
class OverlayContext implements IContext {
private parent: IContext;
private overlay: ReadonlyMap<string, any>;
constructor(parent: IContext, overlay: ReadonlyMap<string, any>) {
this.parent = parent;
this.overlay = overlay;
}
getValue<T>(key: string): T | undefined {
return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getValue(key);
}
}
export class OverlayContextKeyService implements IContextKeyService {
_serviceBrand: undefined;
private overlay: Map<string, any>;
private parent: AbstractContextKeyService | OverlayContextKeyService;
get contextId(): number {
return this.parent.contextId;
}
get onDidChangeContext(): Event<IContextKeyChangeEvent> {
return this.parent.onDidChangeContext;
}
constructor(
parent: AbstractContextKeyService | OverlayContextKeyService,
overlay: Iterable<[string, any]>,
) {
this.parent = parent;
this.overlay = new Map(overlay);
}
bufferChangeEvents(callback: () => void): void {
this.parent.bufferChangeEvents(callback);
}
createKey<T>(): IContextKey<T> {
throw new Error('Not supported.');
}
getContext(target: IContextKeyServiceTarget | null): IContext {
return new OverlayContext(this.parent.getContext(target), this.overlay);
}
getContextValuesContainer(contextId: number): IContext {
const parentContext = this.parent.getContextValuesContainer(contextId);
return new OverlayContext(parentContext, this.overlay);
}
contextMatchesRules(rules: ContextKeyExpression | undefined): boolean {
const context = this.getContextValuesContainer(this.contextId);
if (!rules) {
return true;
}
return rules.evaluate(context);
// return result;
}
getContextKeyValue<T>(key: string): T | undefined {
return this.overlay.has(key)
? this.overlay.get(key)
: this.parent.getContextKeyValue(key);
}
createScoped(): IContextKeyService {
throw new Error('Not supported.');
}
createOverlay(
overlay: Iterable<[string, any]> = Iterable.empty(),
): IContextKeyService {
return new OverlayContextKeyService(this, overlay);
}
updateParent(): void {
throw new Error('Not supported.');
}
dispose(): void {
// noop
}
}
function findContextAttr(domNode: IContextKeyServiceTarget | null): number {
while (domNode) {
if (domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
const attr = domNode.getAttribute(KEYBINDING_CONTEXT_ATTR);
if (attr) {
return parseInt(attr, 10);
}
return NaN;
}
domNode = domNode.parentElement;
}
return 0;
}