@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
564 lines (563 loc) • 16.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
d;
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param =
(this && this.__param) ||
function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex);
};
};
import { PauseableEmitter } from '@sussudio/base/common/event.mjs';
import { Iterable } from '@sussudio/base/common/iterator.mjs';
import { DisposableStore, MutableDisposable } from '@sussudio/base/common/lifecycle.mjs';
import { cloneAndChange, distinct } from '@sussudio/base/common/objects.mjs';
import { TernarySearchTree } from '@sussudio/base/common/ternarySearchTree.mjs';
import { URI } from '@sussudio/base/common/uri.mjs';
import { localize } from 'vscode-nls.mjs';
import { CommandsRegistry } from '../../commands/common/commands.mjs';
import { IConfigurationService } from '../../configuration/common/configuration.mjs';
import { IContextKeyService, RawContextKey } from '../common/contextkey.mjs';
const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context';
export class Context {
_parent;
_value;
_id;
constructor(id, parent) {
this._id = id;
this._parent = parent;
this._value = Object.create(null);
this._value['_contextId'] = id;
}
get value() {
return { ...this._value };
}
setValue(key, value) {
// console.log('SET ' + key + ' = ' + value + ' ON ' + this._id);
if (this._value[key] !== value) {
this._value[key] = value;
return true;
}
return false;
}
removeValue(key) {
// console.log('REMOVE ' + key + ' FROM ' + this._id);
if (key in this._value) {
delete this._value[key];
return true;
}
return false;
}
getValue(key) {
const ret = this._value[key];
if (typeof ret === 'undefined' && this._parent) {
return this._parent.getValue(key);
}
return ret;
}
updateParent(parent) {
this._parent = parent;
}
collectAllValues() {
let result = this._parent ? this._parent.collectAllValues() : Object.create(null);
result = { ...result, ...this._value };
delete result['_contextId'];
return result;
}
}
class NullContext extends Context {
static INSTANCE = new NullContext();
constructor() {
super(-1, null);
}
setValue(key, value) {
return false;
}
removeValue(key) {
return false;
}
getValue(key) {
return undefined;
}
collectAllValues() {
return Object.create(null);
}
}
class ConfigAwareContextValuesContainer extends Context {
_configurationService;
static _keyPrefix = 'config.';
_values = TernarySearchTree.forConfigKeys();
_listener;
constructor(id, _configurationService, emitter) {
super(id, null);
this._configurationService = _configurationService;
this._listener = this._configurationService.onDidChangeConfiguration((event) => {
if (event.source === 7 /* ConfigurationTarget.DEFAULT */) {
// new setting, reset everything
const allKeys = Array.from(this._values, ([k]) => k);
this._values.clear();
emitter.fire(new ArrayContextKeyChangeEvent(allKeys));
} else {
const changedKeys = [];
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() {
this._listener.dispose();
}
getValue(key) {
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 = undefined;
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;
}
setValue(key, value) {
return super.setValue(key, value);
}
removeValue(key) {
return super.removeValue(key);
}
collectAllValues() {
const result = Object.create(null);
this._values.forEach((value, index) => (result[index] = value));
return { ...result, ...super.collectAllValues() };
}
}
class ContextKey {
_service;
_key;
_defaultValue;
constructor(service, key, defaultValue) {
this._service = service;
this._key = key;
this._defaultValue = defaultValue;
this.reset();
}
set(value) {
this._service.setContext(this._key, value);
}
reset() {
if (typeof this._defaultValue === 'undefined') {
this._service.removeContext(this._key);
} else {
this._service.setContext(this._key, this._defaultValue);
}
}
get() {
return this._service.getContextKeyValue(this._key);
}
}
class SimpleContextKeyChangeEvent {
key;
constructor(key) {
this.key = key;
}
affectsSome(keys) {
return keys.has(this.key);
}
allKeysContainedIn(keys) {
return this.affectsSome(keys);
}
}
class ArrayContextKeyChangeEvent {
keys;
constructor(keys) {
this.keys = keys;
}
affectsSome(keys) {
for (const key of this.keys) {
if (keys.has(key)) {
return true;
}
}
return false;
}
allKeysContainedIn(keys) {
return this.keys.every((key) => keys.has(key));
}
}
class CompositeContextKeyChangeEvent {
events;
constructor(events) {
this.events = events;
}
affectsSome(keys) {
for (const e of this.events) {
if (e.affectsSome(keys)) {
return true;
}
}
return false;
}
allKeysContainedIn(keys) {
return this.events.every((evt) => evt.allKeysContainedIn(keys));
}
}
function allEventKeysInContext(event, context) {
return event.allKeysContainedIn(new Set(Object.keys(context)));
}
export class AbstractContextKeyService {
_isDisposed;
_myContextId;
_onDidChangeContext = new PauseableEmitter({ merge: (input) => new CompositeContextKeyChangeEvent(input) });
onDidChangeContext = this._onDidChangeContext.event;
constructor(myContextId) {
this._isDisposed = false;
this._myContextId = myContextId;
}
get contextId() {
return this._myContextId;
}
createKey(key, defaultValue) {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new ContextKey(this, key, defaultValue);
}
bufferChangeEvents(callback) {
this._onDidChangeContext.pause();
try {
callback();
} finally {
this._onDidChangeContext.resume();
}
}
createScoped(domNode) {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new ScopedContextKeyService(this, domNode);
}
createOverlay(overlay = Iterable.empty()) {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
return new OverlayContextKeyService(this, overlay);
}
contextMatchesRules(rules) {
if (this._isDisposed) {
throw new Error(`AbstractContextKeyService has been disposed`);
}
const context = this.getContextValuesContainer(this._myContextId);
const result = rules ? rules.evaluate(context) : true;
// console.group(rules.serialize() + ' -> ' + result);
// rules.keys().forEach(key => { console.log(key, ctx[key]); });
// console.groupEnd();
return result;
}
getContextKeyValue(key) {
if (this._isDisposed) {
return undefined;
}
return this.getContextValuesContainer(this._myContextId).getValue(key);
}
setContext(key, value) {
if (this._isDisposed) {
return;
}
const myContext = this.getContextValuesContainer(this._myContextId);
if (!myContext) {
return;
}
if (myContext.setValue(key, value)) {
this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));
}
}
removeContext(key) {
if (this._isDisposed) {
return;
}
if (this.getContextValuesContainer(this._myContextId).removeValue(key)) {
this._onDidChangeContext.fire(new SimpleContextKeyChangeEvent(key));
}
}
getContext(target) {
if (this._isDisposed) {
return NullContext.INSTANCE;
}
return this.getContextValuesContainer(findContextAttr(target));
}
}
let ContextKeyService = class ContextKeyService extends AbstractContextKeyService {
_lastContextId;
_contexts = new Map();
_toDispose = new DisposableStore();
constructor(configurationService) {
super(0);
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);
}
dispose() {
this._onDidChangeContext.dispose();
this._isDisposed = true;
this._toDispose.dispose();
}
getContextValuesContainer(contextId) {
if (this._isDisposed) {
return NullContext.INSTANCE;
}
return this._contexts.get(contextId) || NullContext.INSTANCE;
}
createChildContext(parentContextId = this._myContextId) {
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;
}
disposeContext(contextId) {
if (!this._isDisposed) {
this._contexts.delete(contextId);
}
}
updateParent(_parentContextKeyService) {
throw new Error('Cannot update parent of root ContextKeyService');
}
};
ContextKeyService = __decorate([__param(0, IConfigurationService)], ContextKeyService);
export { ContextKeyService };
class ScopedContextKeyService extends AbstractContextKeyService {
_parent;
_domNode;
_parentChangeListener = new MutableDisposable();
constructor(parent, domNode) {
super(parent.createChildContext());
this._parent = parent;
this._updateParentChangeListener();
this._domNode = domNode;
if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
let extraInfo = '';
if (this._domNode.classList) {
extraInfo = Array.from(this._domNode.classList.values()).join(', ');
}
console.error(`Element already has context attribute${extraInfo ? ': ' + extraInfo : ''}`);
}
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
}
_updateParentChangeListener() {
// Forward parent events to this listener. Parent will change.
this._parentChangeListener.value = this._parent.onDidChangeContext((e) => {
const thisContainer = this._parent.getContextValuesContainer(this._myContextId);
const thisContextValues = thisContainer.value;
if (!allEventKeysInContext(e, thisContextValues)) {
this._onDidChangeContext.fire(e);
}
});
}
dispose() {
if (this._isDisposed) {
return;
}
this._onDidChangeContext.dispose();
this._parent.disposeContext(this._myContextId);
this._parentChangeListener.dispose();
this._domNode.removeAttribute(KEYBINDING_CONTEXT_ATTR);
this._isDisposed = true;
}
getContextValuesContainer(contextId) {
if (this._isDisposed) {
return NullContext.INSTANCE;
}
return this._parent.getContextValuesContainer(contextId);
}
createChildContext(parentContextId = this._myContextId) {
if (this._isDisposed) {
throw new Error(`ScopedContextKeyService has been disposed`);
}
return this._parent.createChildContext(parentContextId);
}
disposeContext(contextId) {
if (this._isDisposed) {
return;
}
this._parent.disposeContext(contextId);
}
updateParent(parentContextKeyService) {
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 = {
...distinct(oldAllValues, newAllValues),
...distinct(newAllValues, oldAllValues),
};
const changedKeys = Object.keys(allValuesDiff);
this._onDidChangeContext.fire(new ArrayContextKeyChangeEvent(changedKeys));
}
}
class OverlayContext {
parent;
overlay;
constructor(parent, overlay) {
this.parent = parent;
this.overlay = overlay;
}
getValue(key) {
return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getValue(key);
}
}
class OverlayContextKeyService {
parent;
overlay;
get contextId() {
return this.parent.contextId;
}
get onDidChangeContext() {
return this.parent.onDidChangeContext;
}
constructor(parent, overlay) {
this.parent = parent;
this.overlay = new Map(overlay);
}
bufferChangeEvents(callback) {
this.parent.bufferChangeEvents(callback);
}
createKey() {
throw new Error('Not supported.');
}
getContext(target) {
return new OverlayContext(this.parent.getContext(target), this.overlay);
}
getContextValuesContainer(contextId) {
const parentContext = this.parent.getContextValuesContainer(contextId);
return new OverlayContext(parentContext, this.overlay);
}
contextMatchesRules(rules) {
const context = this.getContextValuesContainer(this.contextId);
const result = rules ? rules.evaluate(context) : true;
return result;
}
getContextKeyValue(key) {
return this.overlay.has(key) ? this.overlay.get(key) : this.parent.getContextKeyValue(key);
}
createScoped() {
throw new Error('Not supported.');
}
createOverlay(overlay = Iterable.empty()) {
return new OverlayContextKeyService(this, overlay);
}
updateParent() {
throw new Error('Not supported.');
}
dispose() {
// noop
}
}
function findContextAttr(domNode) {
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;
}
export function setContext(accessor, contextKey, contextValue) {
const contextKeyService = accessor.get(IContextKeyService);
contextKeyService.createKey(String(contextKey), stringifyURIs(contextValue));
}
function stringifyURIs(contextValue) {
return cloneAndChange(contextValue, (obj) => {
if (typeof obj === 'object' && obj.$mid === 1 /* MarshalledId.Uri */) {
return URI.revive(obj).toString();
}
if (obj instanceof URI) {
return obj.toString();
}
return undefined;
});
}
CommandsRegistry.registerCommand('_setContext', setContext);
CommandsRegistry.registerCommand({
id: 'getContextKeyInfo',
handler() {
return [...RawContextKey.all()].sort((a, b) => a.key.localeCompare(b.key));
},
description: {
description: localize('getContextKeyInfo', 'A command that returns information about context keys'),
args: [],
},
});
CommandsRegistry.registerCommand('_generateContextKeyInfo', function () {
const result = [];
const seen = new Set();
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));
console.log(JSON.stringify(result, undefined, 2));
});