UNPKG

monaco-editor-core

Version:

A browser based code editor

269 lines (268 loc) • 8.72 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { strictEquals } from '../equals.js'; import { DebugNameData, getFunctionName } from './debugName.js'; import { getLogger } from './logging.js'; let _recomputeInitiallyAndOnChange; export function _setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange) { _recomputeInitiallyAndOnChange = recomputeInitiallyAndOnChange; } let _keepObserved; export function _setKeepObserved(keepObserved) { _keepObserved = keepObserved; } let _derived; /** * @internal * This is to allow splitting files. */ export function _setDerivedOpts(derived) { _derived = derived; } export class ConvenientObservable { get TChange() { return null; } reportChanges() { this.get(); } /** @sealed */ read(reader) { if (reader) { return reader.readObservable(this); } else { return this.get(); } } map(fnOrOwner, fnOrUndefined) { const owner = fnOrUndefined === undefined ? undefined : fnOrOwner; const fn = fnOrUndefined === undefined ? fnOrOwner : fnOrUndefined; return _derived({ owner, debugName: () => { const name = getFunctionName(fn); if (name !== undefined) { return name; } // regexp to match `x => x.y` or `x => x?.y` where x and y can be arbitrary identifiers (uses backref): const regexp = /^\s*\(?\s*([a-zA-Z_$][a-zA-Z_$0-9]*)\s*\)?\s*=>\s*\1(?:\??)\.([a-zA-Z_$][a-zA-Z_$0-9]*)\s*$/; const match = regexp.exec(fn.toString()); if (match) { return `${this.debugName}.${match[2]}`; } if (!owner) { return `${this.debugName} (mapped)`; } return undefined; }, debugReferenceFn: fn, }, (reader) => fn(this.read(reader), reader)); } /** * @sealed * Converts an observable of an observable value into a direct observable of the value. */ flatten() { return _derived({ owner: undefined, debugName: () => `${this.debugName} (flattened)`, }, (reader) => this.read(reader).read(reader)); } recomputeInitiallyAndOnChange(store, handleValue) { store.add(_recomputeInitiallyAndOnChange(this, handleValue)); return this; } /** * Ensures that this observable is observed. This keeps the cache alive. * However, in case of deriveds, it does not force eager evaluation (only when the value is read/get). * Use `recomputeInitiallyAndOnChange` for eager evaluation. */ keepObserved(store) { store.add(_keepObserved(this)); return this; } } export class BaseObservable extends ConvenientObservable { constructor() { super(...arguments); this.observers = new Set(); } addObserver(observer) { const len = this.observers.size; this.observers.add(observer); if (len === 0) { this.onFirstObserverAdded(); } } removeObserver(observer) { const deleted = this.observers.delete(observer); if (deleted && this.observers.size === 0) { this.onLastObserverRemoved(); } } onFirstObserverAdded() { } onLastObserverRemoved() { } } /** * Starts a transaction in which many observables can be changed at once. * {@link fn} should start with a JS Doc using `@description` to give the transaction a debug name. * Reaction run on demand or when the transaction ends. */ export function transaction(fn, getDebugName) { const tx = new TransactionImpl(fn, getDebugName); try { fn(tx); } finally { tx.finish(); } } let _globalTransaction = undefined; export function globalTransaction(fn) { if (_globalTransaction) { fn(_globalTransaction); } else { const tx = new TransactionImpl(fn, undefined); _globalTransaction = tx; try { fn(tx); } finally { tx.finish(); // During finish, more actions might be added to the transaction. // Which is why we only clear the global transaction after finish. _globalTransaction = undefined; } } } export async function asyncTransaction(fn, getDebugName) { const tx = new TransactionImpl(fn, getDebugName); try { await fn(tx); } finally { tx.finish(); } } /** * Allows to chain transactions. */ export function subtransaction(tx, fn, getDebugName) { if (!tx) { transaction(fn, getDebugName); } else { fn(tx); } } export class TransactionImpl { constructor(_fn, _getDebugName) { this._fn = _fn; this._getDebugName = _getDebugName; this.updatingObservers = []; getLogger()?.handleBeginTransaction(this); } getDebugName() { if (this._getDebugName) { return this._getDebugName(); } return getFunctionName(this._fn); } updateObserver(observer, observable) { // When this gets called while finish is active, they will still get considered this.updatingObservers.push({ observer, observable }); observer.beginUpdate(observable); } finish() { const updatingObservers = this.updatingObservers; for (let i = 0; i < updatingObservers.length; i++) { const { observer, observable } = updatingObservers[i]; observer.endUpdate(observable); } // Prevent anyone from updating observers from now on. this.updatingObservers = null; getLogger()?.handleEndTransaction(); } } export function observableValue(nameOrOwner, initialValue) { let debugNameData; if (typeof nameOrOwner === 'string') { debugNameData = new DebugNameData(undefined, nameOrOwner, undefined); } else { debugNameData = new DebugNameData(nameOrOwner, undefined, undefined); } return new ObservableValue(debugNameData, initialValue, strictEquals); } export class ObservableValue extends BaseObservable { get debugName() { return this._debugNameData.getDebugName(this) ?? 'ObservableValue'; } constructor(_debugNameData, initialValue, _equalityComparator) { super(); this._debugNameData = _debugNameData; this._equalityComparator = _equalityComparator; this._value = initialValue; } get() { return this._value; } set(value, tx, change) { if (change === undefined && this._equalityComparator(this._value, value)) { return; } let _tx; if (!tx) { tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`); } try { const oldValue = this._value; this._setValue(value); getLogger()?.handleObservableChanged(this, { oldValue, newValue: value, change, didChange: true, hadValue: true }); for (const observer of this.observers) { tx.updateObserver(observer, this); observer.handleChange(this, change); } } finally { if (_tx) { _tx.finish(); } } } toString() { return `${this.debugName}: ${this._value}`; } _setValue(newValue) { this._value = newValue; } } /** * A disposable observable. When disposed, its value is also disposed. * When a new value is set, the previous value is disposed. */ export function disposableObservableValue(nameOrOwner, initialValue) { let debugNameData; if (typeof nameOrOwner === 'string') { debugNameData = new DebugNameData(undefined, nameOrOwner, undefined); } else { debugNameData = new DebugNameData(nameOrOwner, undefined, undefined); } return new DisposableObservableValue(debugNameData, initialValue, strictEquals); } export class DisposableObservableValue extends ObservableValue { _setValue(newValue) { if (this._value === newValue) { return; } if (this._value) { this._value.dispose(); } this._value = newValue; } dispose() { this._value?.dispose(); } }