monaco-editor
Version:
A browser based code editor
287 lines (286 loc) • 12.4 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 { assertFn } from '../assert.js';
import { strictEquals } from '../equals.js';
import { DisposableStore } from '../lifecycle.js';
import { BaseObservable, _setDerivedOpts } from './base.js';
import { DebugNameData } from './debugName.js';
import { getLogger } from './logging.js';
export function derived(computeFnOrOwner, computeFn) {
if (computeFn !== undefined) {
return new Derived(new DebugNameData(computeFnOrOwner, undefined, computeFn), computeFn, undefined, undefined, undefined, strictEquals);
}
return new Derived(new DebugNameData(undefined, undefined, computeFnOrOwner), computeFnOrOwner, undefined, undefined, undefined, strictEquals);
}
export function derivedWithSetter(owner, computeFn, setter) {
return new DerivedWithSetter(new DebugNameData(owner, undefined, computeFn), computeFn, undefined, undefined, undefined, strictEquals, setter);
}
export function derivedOpts(options, computeFn) {
return new Derived(new DebugNameData(options.owner, options.debugName, options.debugReferenceFn), computeFn, undefined, undefined, options.onLastObserverRemoved, options.equalsFn ?? strictEquals);
}
_setDerivedOpts(derivedOpts);
/**
* Represents an observable that is derived from other observables.
* The value is only recomputed when absolutely needed.
*
* {@link computeFn} should start with a JS Doc using `@description` to name the derived.
*
* Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
* Use `handleChange` to add a reported change to the change summary.
* The compute function is given the last change summary.
* The change summary is discarded after the compute function was called.
*
* @see derived
*/
export function derivedHandleChanges(options, computeFn) {
return new Derived(new DebugNameData(options.owner, options.debugName, undefined), computeFn, options.createEmptyChangeSummary, options.handleChange, undefined, options.equalityComparer ?? strictEquals);
}
export function derivedWithStore(computeFnOrOwner, computeFnOrUndefined) {
let computeFn;
let owner;
if (computeFnOrUndefined === undefined) {
computeFn = computeFnOrOwner;
owner = undefined;
}
else {
owner = computeFnOrOwner;
computeFn = computeFnOrUndefined;
}
const store = new DisposableStore();
return new Derived(new DebugNameData(owner, undefined, computeFn), r => {
store.clear();
return computeFn(r, store);
}, undefined, undefined, () => store.dispose(), strictEquals);
}
export function derivedDisposable(computeFnOrOwner, computeFnOrUndefined) {
let computeFn;
let owner;
if (computeFnOrUndefined === undefined) {
computeFn = computeFnOrOwner;
owner = undefined;
}
else {
owner = computeFnOrOwner;
computeFn = computeFnOrUndefined;
}
let store = undefined;
return new Derived(new DebugNameData(owner, undefined, computeFn), r => {
if (!store) {
store = new DisposableStore();
}
else {
store.clear();
}
const result = computeFn(r);
if (result) {
store.add(result);
}
return result;
}, undefined, undefined, () => {
if (store) {
store.dispose();
store = undefined;
}
}, strictEquals);
}
export class Derived extends BaseObservable {
get debugName() {
return this._debugNameData.getDebugName(this) ?? '(anonymous)';
}
constructor(_debugNameData, _computeFn, createChangeSummary, _handleChange, _handleLastObserverRemoved = undefined, _equalityComparator) {
super();
this._debugNameData = _debugNameData;
this._computeFn = _computeFn;
this.createChangeSummary = createChangeSummary;
this._handleChange = _handleChange;
this._handleLastObserverRemoved = _handleLastObserverRemoved;
this._equalityComparator = _equalityComparator;
this.state = 0 /* DerivedState.initial */;
this.value = undefined;
this.updateCount = 0;
this.dependencies = new Set();
this.dependenciesToBeRemoved = new Set();
this.changeSummary = undefined;
this.changeSummary = this.createChangeSummary?.();
getLogger()?.handleDerivedCreated(this);
}
onLastObserverRemoved() {
/**
* We are not tracking changes anymore, thus we have to assume
* that our cache is invalid.
*/
this.state = 0 /* DerivedState.initial */;
this.value = undefined;
for (const d of this.dependencies) {
d.removeObserver(this);
}
this.dependencies.clear();
this._handleLastObserverRemoved?.();
}
get() {
if (this.observers.size === 0) {
// Without observers, we don't know when to clean up stuff.
// Thus, we don't cache anything to prevent memory leaks.
const result = this._computeFn(this, this.createChangeSummary?.());
// Clear new dependencies
this.onLastObserverRemoved();
return result;
}
else {
do {
// We might not get a notification for a dependency that changed while it is updating,
// thus we also have to ask all our depedencies if they changed in this case.
if (this.state === 1 /* DerivedState.dependenciesMightHaveChanged */) {
for (const d of this.dependencies) {
/** might call {@link handleChange} indirectly, which could make us stale */
d.reportChanges();
if (this.state === 2 /* DerivedState.stale */) {
// The other dependencies will refresh on demand, so early break
break;
}
}
}
// We called report changes of all dependencies.
// If we are still not stale, we can assume to be up to date again.
if (this.state === 1 /* DerivedState.dependenciesMightHaveChanged */) {
this.state = 3 /* DerivedState.upToDate */;
}
this._recomputeIfNeeded();
// In case recomputation changed one of our dependencies, we need to recompute again.
} while (this.state !== 3 /* DerivedState.upToDate */);
return this.value;
}
}
_recomputeIfNeeded() {
if (this.state === 3 /* DerivedState.upToDate */) {
return;
}
const emptySet = this.dependenciesToBeRemoved;
this.dependenciesToBeRemoved = this.dependencies;
this.dependencies = emptySet;
const hadValue = this.state !== 0 /* DerivedState.initial */;
const oldValue = this.value;
this.state = 3 /* DerivedState.upToDate */;
const changeSummary = this.changeSummary;
this.changeSummary = this.createChangeSummary?.();
try {
/** might call {@link handleChange} indirectly, which could invalidate us */
this.value = this._computeFn(this, changeSummary);
}
finally {
// We don't want our observed observables to think that they are (not even temporarily) not being observed.
// Thus, we only unsubscribe from observables that are definitely not read anymore.
for (const o of this.dependenciesToBeRemoved) {
o.removeObserver(this);
}
this.dependenciesToBeRemoved.clear();
}
const didChange = hadValue && !(this._equalityComparator(oldValue, this.value));
getLogger()?.handleDerivedRecomputed(this, {
oldValue,
newValue: this.value,
change: undefined,
didChange,
hadValue,
});
if (didChange) {
for (const r of this.observers) {
r.handleChange(this, undefined);
}
}
}
toString() {
return `LazyDerived<${this.debugName}>`;
}
// IObserver Implementation
beginUpdate(_observable) {
this.updateCount++;
const propagateBeginUpdate = this.updateCount === 1;
if (this.state === 3 /* DerivedState.upToDate */) {
this.state = 1 /* DerivedState.dependenciesMightHaveChanged */;
// If we propagate begin update, that will already signal a possible change.
if (!propagateBeginUpdate) {
for (const r of this.observers) {
r.handlePossibleChange(this);
}
}
}
if (propagateBeginUpdate) {
for (const r of this.observers) {
r.beginUpdate(this); // This signals a possible change
}
}
}
endUpdate(_observable) {
this.updateCount--;
if (this.updateCount === 0) {
// End update could change the observer list.
const observers = [...this.observers];
for (const r of observers) {
r.endUpdate(this);
}
}
assertFn(() => this.updateCount >= 0);
}
handlePossibleChange(observable) {
// In all other states, observers already know that we might have changed.
if (this.state === 3 /* DerivedState.upToDate */ && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
this.state = 1 /* DerivedState.dependenciesMightHaveChanged */;
for (const r of this.observers) {
r.handlePossibleChange(this);
}
}
}
handleChange(observable, change) {
if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
const shouldReact = this._handleChange ? this._handleChange({
changedObservable: observable,
change,
didChange: (o) => o === observable,
}, this.changeSummary) : true;
const wasUpToDate = this.state === 3 /* DerivedState.upToDate */;
if (shouldReact && (this.state === 1 /* DerivedState.dependenciesMightHaveChanged */ || wasUpToDate)) {
this.state = 2 /* DerivedState.stale */;
if (wasUpToDate) {
for (const r of this.observers) {
r.handlePossibleChange(this);
}
}
}
}
}
// IReader Implementation
readObservable(observable) {
// Subscribe before getting the value to enable caching
observable.addObserver(this);
/** This might call {@link handleChange} indirectly, which could invalidate us */
const value = observable.get();
// Which is why we only add the observable to the dependencies now.
this.dependencies.add(observable);
this.dependenciesToBeRemoved.delete(observable);
return value;
}
addObserver(observer) {
const shouldCallBeginUpdate = !this.observers.has(observer) && this.updateCount > 0;
super.addObserver(observer);
if (shouldCallBeginUpdate) {
observer.beginUpdate(this);
}
}
removeObserver(observer) {
const shouldCallEndUpdate = this.observers.has(observer) && this.updateCount > 0;
super.removeObserver(observer);
if (shouldCallEndUpdate) {
// Calling end update after removing the observer makes sure endUpdate cannot be called twice here.
observer.endUpdate(this);
}
}
}
export class DerivedWithSetter extends Derived {
constructor(debugNameData, computeFn, createChangeSummary, handleChange, handleLastObserverRemoved = undefined, equalityComparator, set) {
super(debugNameData, computeFn, createChangeSummary, handleChange, handleLastObserverRemoved, equalityComparator);
this.set = set;
}
}