monaco-editor-core
Version:
A browser based code editor
236 lines (235 loc) • 9.77 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 { equalsIfDefined, itemsEquals } from '../../base/common/equals.js';
import { Disposable, DisposableStore, toDisposable } from '../../base/common/lifecycle.js';
import { autorun, autorunOpts, autorunWithStoreHandleChanges, derived, derivedOpts, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js';
import { TransactionImpl } from '../../base/common/observableInternal/base.js';
import { derivedWithSetter } from '../../base/common/observableInternal/derived.js';
import { Selection } from '../common/core/selection.js';
/**
* Returns a facade for the code editor that provides observables for various states/events.
*/
export function observableCodeEditor(editor) {
return ObservableCodeEditor.get(editor);
}
export class ObservableCodeEditor extends Disposable {
static { this._map = new Map(); }
/**
* Make sure that editor is not disposed yet!
*/
static get(editor) {
let result = ObservableCodeEditor._map.get(editor);
if (!result) {
result = new ObservableCodeEditor(editor);
ObservableCodeEditor._map.set(editor, result);
const d = editor.onDidDispose(() => {
const item = ObservableCodeEditor._map.get(editor);
if (item) {
ObservableCodeEditor._map.delete(editor);
item.dispose();
d.dispose();
}
});
}
return result;
}
_beginUpdate() {
this._updateCounter++;
if (this._updateCounter === 1) {
this._currentTransaction = new TransactionImpl(() => {
/** @description Update editor state */
});
}
}
_endUpdate() {
this._updateCounter--;
if (this._updateCounter === 0) {
const t = this._currentTransaction;
this._currentTransaction = undefined;
t.finish();
}
}
constructor(editor) {
super();
this.editor = editor;
this._updateCounter = 0;
this._currentTransaction = undefined;
this._model = observableValue(this, this.editor.getModel());
this.model = this._model;
this.isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(92 /* EditorOption.readOnly */));
this._versionId = observableValueOpts({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null);
this.versionId = this._versionId;
this._selections = observableValueOpts({ owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true }, this.editor.getSelections() ?? null);
this.selections = this._selections;
this.isFocused = observableFromEvent(this, e => {
const d1 = this.editor.onDidFocusEditorWidget(e);
const d2 = this.editor.onDidBlurEditorWidget(e);
return {
dispose() {
d1.dispose();
d2.dispose();
}
};
}, () => this.editor.hasWidgetFocus());
this.value = derivedWithSetter(this, reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue() ?? ''; }, (value, tx) => {
const model = this.model.get();
if (model !== null) {
if (value !== model.getValue()) {
model.setValue(value);
}
}
});
this.valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; });
this.cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null);
this.onDidType = observableSignal(this);
this.scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop());
this.scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft());
this.layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo());
this.layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft);
this.layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft);
this.contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth());
this._overlayWidgetCounter = 0;
this._register(this.editor.onBeginUpdate(() => this._beginUpdate()));
this._register(this.editor.onEndUpdate(() => this._endUpdate()));
this._register(this.editor.onDidChangeModel(() => {
this._beginUpdate();
try {
this._model.set(this.editor.getModel(), this._currentTransaction);
this._forceUpdate();
}
finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidType((e) => {
this._beginUpdate();
try {
this._forceUpdate();
this.onDidType.trigger(this._currentTransaction, e);
}
finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidChangeModelContent(e => {
this._beginUpdate();
try {
this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, e);
this._forceUpdate();
}
finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidChangeCursorSelection(e => {
this._beginUpdate();
try {
this._selections.set(this.editor.getSelections(), this._currentTransaction, e);
this._forceUpdate();
}
finally {
this._endUpdate();
}
}));
}
forceUpdate(cb) {
this._beginUpdate();
try {
this._forceUpdate();
if (!cb) {
return undefined;
}
return cb(this._currentTransaction);
}
finally {
this._endUpdate();
}
}
_forceUpdate() {
this._beginUpdate();
try {
this._model.set(this.editor.getModel(), this._currentTransaction);
this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, undefined);
this._selections.set(this.editor.getSelections(), this._currentTransaction, undefined);
}
finally {
this._endUpdate();
}
}
getOption(id) {
return observableFromEvent(this, cb => this.editor.onDidChangeConfiguration(e => {
if (e.hasChanged(id)) {
cb(undefined);
}
}), () => this.editor.getOption(id));
}
setDecorations(decorations) {
const d = new DisposableStore();
const decorationsCollection = this.editor.createDecorationsCollection();
d.add(autorunOpts({ owner: this, debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => {
const d = decorations.read(reader);
decorationsCollection.set(d);
}));
d.add({
dispose: () => {
decorationsCollection.clear();
}
});
return d;
}
createOverlayWidget(widget) {
const overlayWidgetId = 'observableOverlayWidget' + (this._overlayWidgetCounter++);
const w = {
getDomNode: () => widget.domNode,
getPosition: () => widget.position.get(),
getId: () => overlayWidgetId,
allowEditorOverflow: widget.allowEditorOverflow,
getMinContentWidthInPx: () => widget.minContentWidthInPx.get(),
};
this.editor.addOverlayWidget(w);
const d = autorun(reader => {
widget.position.read(reader);
widget.minContentWidthInPx.read(reader);
this.editor.layoutOverlayWidget(w);
});
return toDisposable(() => {
d.dispose();
this.editor.removeOverlayWidget(w);
});
}
}
export function reactToChange(observable, cb) {
return autorunWithStoreHandleChanges({
createEmptyChangeSummary: () => ({ deltas: [], didChange: false }),
handleChange: (context, changeSummary) => {
if (context.didChange(observable)) {
const e = context.change;
if (e !== undefined) {
changeSummary.deltas.push(e);
}
changeSummary.didChange = true;
}
return true;
},
}, (reader, changeSummary) => {
const value = observable.read(reader);
if (changeSummary.didChange) {
cb(value, changeSummary.deltas);
}
});
}
export function reactToChangeWithStore(observable, cb) {
const store = new DisposableStore();
const disposable = reactToChange(observable, (value, deltas) => {
store.clear();
cb(value, deltas, store);
});
return {
dispose() {
disposable.dispose();
store.dispose();
}
};
}