monaco-editor-core
Version:
A browser based code editor
367 lines (366 loc) • 12.1 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from '../event.js';
import { DisposableStore, toDisposable } from '../lifecycle.js';
import { BaseObservable, ConvenientObservable, _setKeepObserved, _setRecomputeInitiallyAndOnChange, subtransaction, transaction } from './base.js';
import { DebugNameData } from './debugName.js';
import { derived, derivedOpts } from './derived.js';
import { getLogger } from './logging.js';
import { strictEquals } from '../equals.js';
/**
* Represents an efficient observable whose value never changes.
*/
export function constObservable(value) {
return new ConstObservable(value);
}
class ConstObservable extends ConvenientObservable {
constructor(value) {
super();
this.value = value;
}
get debugName() {
return this.toString();
}
get() {
return this.value;
}
addObserver(observer) {
// NO OP
}
removeObserver(observer) {
// NO OP
}
toString() {
return `Const: ${this.value}`;
}
}
export function observableFromEvent(...args) {
let owner;
let event;
let getValue;
if (args.length === 3) {
[owner, event, getValue] = args;
}
else {
[event, getValue] = args;
}
return new FromEventObservable(new DebugNameData(owner, undefined, getValue), event, getValue, () => FromEventObservable.globalTransaction, strictEquals);
}
export function observableFromEventOpts(options, event, getValue) {
return new FromEventObservable(new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue), event, getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals);
}
export class FromEventObservable extends BaseObservable {
constructor(_debugNameData, event, _getValue, _getTransaction, _equalityComparator) {
super();
this._debugNameData = _debugNameData;
this.event = event;
this._getValue = _getValue;
this._getTransaction = _getTransaction;
this._equalityComparator = _equalityComparator;
this.hasValue = false;
this.handleEvent = (args) => {
const newValue = this._getValue(args);
const oldValue = this.value;
const didChange = !this.hasValue || !(this._equalityComparator(oldValue, newValue));
let didRunTransaction = false;
if (didChange) {
this.value = newValue;
if (this.hasValue) {
didRunTransaction = true;
subtransaction(this._getTransaction(), (tx) => {
getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue });
for (const o of this.observers) {
tx.updateObserver(o, this);
o.handleChange(this, undefined);
}
}, () => {
const name = this.getDebugName();
return 'Event fired' + (name ? `: ${name}` : '');
});
}
this.hasValue = true;
}
if (!didRunTransaction) {
getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue });
}
};
}
getDebugName() {
return this._debugNameData.getDebugName(this);
}
get debugName() {
const name = this.getDebugName();
return 'From Event' + (name ? `: ${name}` : '');
}
onFirstObserverAdded() {
this.subscription = this.event(this.handleEvent);
}
onLastObserverRemoved() {
this.subscription.dispose();
this.subscription = undefined;
this.hasValue = false;
this.value = undefined;
}
get() {
if (this.subscription) {
if (!this.hasValue) {
this.handleEvent(undefined);
}
return this.value;
}
else {
// no cache, as there are no subscribers to keep it updated
const value = this._getValue(undefined);
return value;
}
}
}
(function (observableFromEvent) {
observableFromEvent.Observer = FromEventObservable;
function batchEventsGlobally(tx, fn) {
let didSet = false;
if (FromEventObservable.globalTransaction === undefined) {
FromEventObservable.globalTransaction = tx;
didSet = true;
}
try {
fn();
}
finally {
if (didSet) {
FromEventObservable.globalTransaction = undefined;
}
}
}
observableFromEvent.batchEventsGlobally = batchEventsGlobally;
})(observableFromEvent || (observableFromEvent = {}));
export function observableSignalFromEvent(debugName, event) {
return new FromEventObservableSignal(debugName, event);
}
class FromEventObservableSignal extends BaseObservable {
constructor(debugName, event) {
super();
this.debugName = debugName;
this.event = event;
this.handleEvent = () => {
transaction((tx) => {
for (const o of this.observers) {
tx.updateObserver(o, this);
o.handleChange(this, undefined);
}
}, () => this.debugName);
};
}
onFirstObserverAdded() {
this.subscription = this.event(this.handleEvent);
}
onLastObserverRemoved() {
this.subscription.dispose();
this.subscription = undefined;
}
get() {
// NO OP
}
}
export function observableSignal(debugNameOrOwner) {
if (typeof debugNameOrOwner === 'string') {
return new ObservableSignal(debugNameOrOwner);
}
else {
return new ObservableSignal(undefined, debugNameOrOwner);
}
}
class ObservableSignal extends BaseObservable {
get debugName() {
return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal';
}
toString() {
return this.debugName;
}
constructor(_debugName, _owner) {
super();
this._debugName = _debugName;
this._owner = _owner;
}
trigger(tx, change) {
if (!tx) {
transaction(tx => {
this.trigger(tx, change);
}, () => `Trigger signal ${this.debugName}`);
return;
}
for (const o of this.observers) {
tx.updateObserver(o, this);
o.handleChange(this, change);
}
}
get() {
// NO OP
}
}
/**
* This makes sure the observable is being observed and keeps its cache alive.
*/
export function keepObserved(observable) {
const o = new KeepAliveObserver(false, undefined);
observable.addObserver(o);
return toDisposable(() => {
observable.removeObserver(o);
});
}
_setKeepObserved(keepObserved);
/**
* This converts the given observable into an autorun.
*/
export function recomputeInitiallyAndOnChange(observable, handleValue) {
const o = new KeepAliveObserver(true, handleValue);
observable.addObserver(o);
if (handleValue) {
handleValue(observable.get());
}
else {
observable.reportChanges();
}
return toDisposable(() => {
observable.removeObserver(o);
});
}
_setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange);
export class KeepAliveObserver {
constructor(_forceRecompute, _handleValue) {
this._forceRecompute = _forceRecompute;
this._handleValue = _handleValue;
this._counter = 0;
}
beginUpdate(observable) {
this._counter++;
}
endUpdate(observable) {
this._counter--;
if (this._counter === 0 && this._forceRecompute) {
if (this._handleValue) {
this._handleValue(observable.get());
}
else {
observable.reportChanges();
}
}
}
handlePossibleChange(observable) {
// NO OP
}
handleChange(observable, change) {
// NO OP
}
}
export function derivedObservableWithCache(owner, computeFn) {
let lastValue = undefined;
const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => {
lastValue = computeFn(reader, lastValue);
return lastValue;
});
return observable;
}
export function derivedObservableWithWritableCache(owner, computeFn) {
let lastValue = undefined;
const onChange = observableSignal('derivedObservableWithWritableCache');
const observable = derived(owner, reader => {
onChange.read(reader);
lastValue = computeFn(reader, lastValue);
return lastValue;
});
return Object.assign(observable, {
clearCache: (tx) => {
lastValue = undefined;
onChange.trigger(tx);
},
setCache: (newValue, tx) => {
lastValue = newValue;
onChange.trigger(tx);
}
});
}
/**
* When the items array changes, referential equal items are not mapped again.
*/
export function mapObservableArrayCached(owner, items, map, keySelector) {
let m = new ArrayMap(map, keySelector);
const self = derivedOpts({
debugReferenceFn: map,
owner,
onLastObserverRemoved: () => {
m.dispose();
m = new ArrayMap(map);
}
}, (reader) => {
m.setItems(items.read(reader));
return m.getItems();
});
return self;
}
class ArrayMap {
constructor(_map, _keySelector) {
this._map = _map;
this._keySelector = _keySelector;
this._cache = new Map();
this._items = [];
}
dispose() {
this._cache.forEach(entry => entry.store.dispose());
this._cache.clear();
}
setItems(items) {
const newItems = [];
const itemsToRemove = new Set(this._cache.keys());
for (const item of items) {
const key = this._keySelector ? this._keySelector(item) : item;
let entry = this._cache.get(key);
if (!entry) {
const store = new DisposableStore();
const out = this._map(item, store);
entry = { out, store };
this._cache.set(key, entry);
}
else {
itemsToRemove.delete(key);
}
newItems.push(entry.out);
}
for (const item of itemsToRemove) {
const entry = this._cache.get(item);
entry.store.dispose();
this._cache.delete(item);
}
this._items = newItems;
}
getItems() {
return this._items;
}
}
export class ValueWithChangeEventFromObservable {
constructor(observable) {
this.observable = observable;
}
get onDidChange() {
return Event.fromObservableLight(this.observable);
}
get value() {
return this.observable.get();
}
}
export function observableFromValueWithChangeEvent(owner, value) {
if (value instanceof ValueWithChangeEventFromObservable) {
return value.observable;
}
return observableFromEvent(owner, value.onDidChange, () => value.value);
}
/**
* Works like a derived.
* However, if the value is not undefined, it is cached and will not be recomputed anymore.
* In that case, the derived will unsubscribe from its dependencies.
*/
export function derivedConstOnceDefined(owner, fn) {
return derivedObservableWithCache(owner, (reader, lastValue) => lastValue ?? fn(reader));
}