monaco-editor-core
Version:
A browser based code editor
193 lines (192 loc) • 7.65 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 { DisposableStore, markAsDisposed, toDisposable, trackDisposable } from '../lifecycle.js';
import { DebugNameData } from './debugName.js';
import { getLogger } from './logging.js';
/**
* Runs immediately and whenever a transaction ends and an observed observable changed.
* {@link fn} should start with a JS Doc using `@description` to name the autorun.
*/
export function autorun(fn) {
return new AutorunObserver(new DebugNameData(undefined, undefined, fn), fn, undefined, undefined);
}
/**
* Runs immediately and whenever a transaction ends and an observed observable changed.
* {@link fn} should start with a JS Doc using `@description` to name the autorun.
*/
export function autorunOpts(options, fn) {
return new AutorunObserver(new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn), fn, undefined, undefined);
}
/**
* Runs immediately and whenever a transaction ends and an observed observable changed.
* {@link fn} should start with a JS Doc using `@description` to name the autorun.
*
* Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
* Use `handleChange` to add a reported change to the change summary.
* The run function is given the last change summary.
* The change summary is discarded after the run function was called.
*
* @see autorun
*/
export function autorunHandleChanges(options, fn) {
return new AutorunObserver(new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn), fn, options.createEmptyChangeSummary, options.handleChange);
}
/**
* @see autorunHandleChanges (but with a disposable store that is cleared before the next run or on dispose)
*/
export function autorunWithStoreHandleChanges(options, fn) {
const store = new DisposableStore();
const disposable = autorunHandleChanges({
owner: options.owner,
debugName: options.debugName,
debugReferenceFn: options.debugReferenceFn ?? fn,
createEmptyChangeSummary: options.createEmptyChangeSummary,
handleChange: options.handleChange,
}, (reader, changeSummary) => {
store.clear();
fn(reader, changeSummary, store);
});
return toDisposable(() => {
disposable.dispose();
store.dispose();
});
}
/**
* @see autorun (but with a disposable store that is cleared before the next run or on dispose)
*/
export function autorunWithStore(fn) {
const store = new DisposableStore();
const disposable = autorunOpts({
owner: undefined,
debugName: undefined,
debugReferenceFn: fn,
}, reader => {
store.clear();
fn(reader, store);
});
return toDisposable(() => {
disposable.dispose();
store.dispose();
});
}
export class AutorunObserver {
get debugName() {
return this._debugNameData.getDebugName(this) ?? '(anonymous)';
}
constructor(_debugNameData, _runFn, createChangeSummary, _handleChange) {
this._debugNameData = _debugNameData;
this._runFn = _runFn;
this.createChangeSummary = createChangeSummary;
this._handleChange = _handleChange;
this.state = 2 /* AutorunState.stale */;
this.updateCount = 0;
this.disposed = false;
this.dependencies = new Set();
this.dependenciesToBeRemoved = new Set();
this.changeSummary = this.createChangeSummary?.();
getLogger()?.handleAutorunCreated(this);
this._runIfNeeded();
trackDisposable(this);
}
dispose() {
this.disposed = true;
for (const o of this.dependencies) {
o.removeObserver(this);
}
this.dependencies.clear();
markAsDisposed(this);
}
_runIfNeeded() {
if (this.state === 3 /* AutorunState.upToDate */) {
return;
}
const emptySet = this.dependenciesToBeRemoved;
this.dependenciesToBeRemoved = this.dependencies;
this.dependencies = emptySet;
this.state = 3 /* AutorunState.upToDate */;
const isDisposed = this.disposed;
try {
if (!isDisposed) {
getLogger()?.handleAutorunTriggered(this);
const changeSummary = this.changeSummary;
this.changeSummary = this.createChangeSummary?.();
this._runFn(this, changeSummary);
}
}
finally {
if (!isDisposed) {
getLogger()?.handleAutorunFinished(this);
}
// 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();
}
}
toString() {
return `Autorun<${this.debugName}>`;
}
// IObserver implementation
beginUpdate() {
if (this.state === 3 /* AutorunState.upToDate */) {
this.state = 1 /* AutorunState.dependenciesMightHaveChanged */;
}
this.updateCount++;
}
endUpdate() {
if (this.updateCount === 1) {
do {
if (this.state === 1 /* AutorunState.dependenciesMightHaveChanged */) {
this.state = 3 /* AutorunState.upToDate */;
for (const d of this.dependencies) {
d.reportChanges();
if (this.state === 2 /* AutorunState.stale */) {
// The other dependencies will refresh on demand
break;
}
}
}
this._runIfNeeded();
} while (this.state !== 3 /* AutorunState.upToDate */);
}
this.updateCount--;
assertFn(() => this.updateCount >= 0);
}
handlePossibleChange(observable) {
if (this.state === 3 /* AutorunState.upToDate */ && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
this.state = 1 /* AutorunState.dependenciesMightHaveChanged */;
}
}
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;
if (shouldReact) {
this.state = 2 /* AutorunState.stale */;
}
}
}
// IReader implementation
readObservable(observable) {
// In case the run action disposes the autorun
if (this.disposed) {
return observable.get();
}
observable.addObserver(this);
const value = observable.get();
this.dependencies.add(observable);
this.dependenciesToBeRemoved.delete(observable);
return value;
}
}
(function (autorun) {
autorun.Observer = AutorunObserver;
})(autorun || (autorun = {}));