Path: blob/main/src/vs/editor/browser/observableCodeEditor.ts
3292 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { equalsIfDefined, itemsEquals } from '../../base/common/equals.js';6import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js';7import { DebugLocation, IObservable, IObservableWithChange, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js';8import { EditorOption, FindComputedEditorOptionValueById } from '../common/config/editorOptions.js';9import { LineRange } from '../common/core/ranges/lineRange.js';10import { OffsetRange } from '../common/core/ranges/offsetRange.js';11import { Position } from '../common/core/position.js';12import { Selection } from '../common/core/selection.js';13import { ICursorSelectionChangedEvent } from '../common/cursorEvents.js';14import { IModelDeltaDecoration, ITextModel } from '../common/model.js';15import { IModelContentChangedEvent } from '../common/textModelEvents.js';16import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent, IOverlayWidget, IOverlayWidgetPosition, IPasteEvent } from './editorBrowser.js';17import { Point } from '../common/core/2d/point.js';1819/**20* Returns a facade for the code editor that provides observables for various states/events.21*/22export function observableCodeEditor(editor: ICodeEditor): ObservableCodeEditor {23return ObservableCodeEditor.get(editor);24}2526export class ObservableCodeEditor extends Disposable {27private static readonly _map = new Map<ICodeEditor, ObservableCodeEditor>();2829/**30* Make sure that editor is not disposed yet!31*/32public static get(editor: ICodeEditor): ObservableCodeEditor {33let result = ObservableCodeEditor._map.get(editor);34if (!result) {35result = new ObservableCodeEditor(editor);36ObservableCodeEditor._map.set(editor, result);37const d = editor.onDidDispose(() => {38const item = ObservableCodeEditor._map.get(editor);39if (item) {40ObservableCodeEditor._map.delete(editor);41item.dispose();42d.dispose();43}44});45}46return result;47}4849private _updateCounter;50private _currentTransaction: TransactionImpl | undefined;5152private _beginUpdate(): void {53this._updateCounter++;54if (this._updateCounter === 1) {55this._currentTransaction = new TransactionImpl(() => {56/** @description Update editor state */57});58}59}6061private _endUpdate(): void {62this._updateCounter--;63if (this._updateCounter === 0) {64const t = this._currentTransaction!;65this._currentTransaction = undefined;66t.finish();67}68}6970private constructor(public readonly editor: ICodeEditor) {71super();72this._updateCounter = 0;73this._currentTransaction = undefined;74this._model = observableValue(this, this.editor.getModel());75this.model = this._model;76this.isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly));77this._versionId = observableValueOpts<number | null, IModelContentChangedEvent | undefined>({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null);78this.versionId = this._versionId;79this._selections = observableValueOpts<Selection[] | null, ICursorSelectionChangedEvent | undefined>(80{ owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true },81this.editor.getSelections() ?? null82);83this.selections = this._selections;84this.positions = derivedOpts<readonly Position[] | null>(85{ owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) },86reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null87);88this.isFocused = observableFromEvent(this, e => {89const d1 = this.editor.onDidFocusEditorWidget(e);90const d2 = this.editor.onDidBlurEditorWidget(e);91return {92dispose() {93d1.dispose();94d2.dispose();95}96};97}, () => this.editor.hasWidgetFocus());98this.isTextFocused = observableFromEvent(this, e => {99const d1 = this.editor.onDidFocusEditorText(e);100const d2 = this.editor.onDidBlurEditorText(e);101return {102dispose() {103d1.dispose();104d2.dispose();105}106};107}, () => this.editor.hasTextFocus());108this.inComposition = observableFromEvent(this, e => {109const d1 = this.editor.onDidCompositionStart(() => {110e(undefined);111});112const d2 = this.editor.onDidCompositionEnd(() => {113e(undefined);114});115return {116dispose() {117d1.dispose();118d2.dispose();119}120};121}, () => this.editor.inComposition);122this.value = derivedWithSetter(this,123reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue() ?? ''; },124(value, tx) => {125const model = this.model.get();126if (model !== null) {127if (value !== model.getValue()) {128model.setValue(value);129}130}131}132);133this.valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; });134this.cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null);135this.cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null);136this.cursorLineNumber = derived<number | null>(this, reader => this.cursorPosition.read(reader)?.lineNumber ?? null);137this.onDidType = observableSignal<string>(this);138this.onDidPaste = observableSignal<IPasteEvent>(this);139this.scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop());140this.scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft());141this.layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo());142this.layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft);143this.layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft);144this.layoutInfoWidth = this.layoutInfo.map(l => l.width);145this.layoutInfoHeight = this.layoutInfo.map(l => l.height);146this.layoutInfoMinimap = this.layoutInfo.map(l => l.minimap);147this.layoutInfoVerticalScrollbarWidth = this.layoutInfo.map(l => l.verticalScrollbarWidth);148this.contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth());149this.contentHeight = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentHeight());150151this._widgetCounter = 0;152this.openedPeekWidgets = observableValue(this, 0);153154this._register(this.editor.onBeginUpdate(() => this._beginUpdate()));155this._register(this.editor.onEndUpdate(() => this._endUpdate()));156157this._register(this.editor.onDidChangeModel(() => {158this._beginUpdate();159try {160this._model.set(this.editor.getModel(), this._currentTransaction);161this._forceUpdate();162} finally {163this._endUpdate();164}165}));166167this._register(this.editor.onDidType((e) => {168this._beginUpdate();169try {170this._forceUpdate();171this.onDidType.trigger(this._currentTransaction, e);172} finally {173this._endUpdate();174}175}));176177this._register(this.editor.onDidPaste((e) => {178this._beginUpdate();179try {180this._forceUpdate();181this.onDidPaste.trigger(this._currentTransaction, e);182} finally {183this._endUpdate();184}185}));186187this._register(this.editor.onDidChangeModelContent(e => {188this._beginUpdate();189try {190this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, e);191this._forceUpdate();192} finally {193this._endUpdate();194}195}));196197this._register(this.editor.onDidChangeCursorSelection(e => {198this._beginUpdate();199try {200this._selections.set(this.editor.getSelections(), this._currentTransaction, e);201this._forceUpdate();202} finally {203this._endUpdate();204}205}));206207this.domNode = derived(reader => {208this.model.read(reader);209return this.editor.getDomNode();210});211}212213public forceUpdate(): void;214public forceUpdate<T>(cb: (tx: ITransaction) => T): T;215public forceUpdate<T>(cb?: (tx: ITransaction) => T): T {216this._beginUpdate();217try {218this._forceUpdate();219if (!cb) { return undefined as T; }220return cb(this._currentTransaction!);221} finally {222this._endUpdate();223}224}225226private _forceUpdate(): void {227this._beginUpdate();228try {229this._model.set(this.editor.getModel(), this._currentTransaction);230this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, undefined);231this._selections.set(this.editor.getSelections(), this._currentTransaction, undefined);232} finally {233this._endUpdate();234}235}236237private readonly _model;238public readonly model: IObservable<ITextModel | null>;239240public readonly isReadonly;241242private readonly _versionId;243public readonly versionId: IObservableWithChange<number | null, IModelContentChangedEvent | undefined>;244245private readonly _selections;246public readonly selections: IObservableWithChange<Selection[] | null, ICursorSelectionChangedEvent | undefined>;247248249public readonly positions;250251public readonly isFocused;252253public readonly isTextFocused;254255public readonly inComposition;256257public readonly value;258public readonly valueIsEmpty;259public readonly cursorSelection;260public readonly cursorPosition;261public readonly cursorLineNumber;262263public readonly onDidType;264public readonly onDidPaste;265266public readonly scrollTop;267public readonly scrollLeft;268269public readonly layoutInfo;270public readonly layoutInfoContentLeft;271public readonly layoutInfoDecorationsLeft;272public readonly layoutInfoWidth;273public readonly layoutInfoHeight;274public readonly layoutInfoMinimap;275public readonly layoutInfoVerticalScrollbarWidth;276277public readonly contentWidth;278public readonly contentHeight;279280public readonly domNode;281282public getOption<T extends EditorOption>(id: T, debugLocation = DebugLocation.ofCaller()): IObservable<FindComputedEditorOptionValueById<T>> {283return observableFromEvent(this, cb => this.editor.onDidChangeConfiguration(e => {284if (e.hasChanged(id)) { cb(undefined); }285}), () => this.editor.getOption(id), debugLocation);286}287288public setDecorations(decorations: IObservable<IModelDeltaDecoration[]>): IDisposable {289const d = new DisposableStore();290const decorationsCollection = this.editor.createDecorationsCollection();291d.add(autorunOpts({ owner: this, debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => {292const d = decorations.read(reader);293decorationsCollection.set(d);294}));295d.add({296dispose: () => {297decorationsCollection.clear();298}299});300return d;301}302303private _widgetCounter;304305public createOverlayWidget(widget: IObservableOverlayWidget): IDisposable {306const overlayWidgetId = 'observableOverlayWidget' + (this._widgetCounter++);307const w: IOverlayWidget = {308getDomNode: () => widget.domNode,309getPosition: () => widget.position.get(),310getId: () => overlayWidgetId,311allowEditorOverflow: widget.allowEditorOverflow,312getMinContentWidthInPx: () => widget.minContentWidthInPx.get(),313};314this.editor.addOverlayWidget(w);315const d = autorun(reader => {316widget.position.read(reader);317widget.minContentWidthInPx.read(reader);318this.editor.layoutOverlayWidget(w);319});320return toDisposable(() => {321d.dispose();322this.editor.removeOverlayWidget(w);323});324}325326public createContentWidget(widget: IObservableContentWidget): IDisposable {327const contentWidgetId = 'observableContentWidget' + (this._widgetCounter++);328const w: IContentWidget = {329getDomNode: () => widget.domNode,330getPosition: () => widget.position.get(),331getId: () => contentWidgetId,332allowEditorOverflow: widget.allowEditorOverflow,333};334this.editor.addContentWidget(w);335const d = autorun(reader => {336widget.position.read(reader);337this.editor.layoutContentWidget(w);338});339return toDisposable(() => {340d.dispose();341this.editor.removeContentWidget(w);342});343}344345public observeLineOffsetRange(lineRange: IObservable<LineRange>, store: DisposableStore): IObservable<OffsetRange> {346const start = this.observePosition(lineRange.map(r => new Position(r.startLineNumber, 1)), store);347const end = this.observePosition(lineRange.map(r => new Position(r.endLineNumberExclusive + 1, 1)), store);348349return derived(reader => {350start.read(reader);351end.read(reader);352const range = lineRange.read(reader);353const lineCount = this.model.read(reader)?.getLineCount();354const s = (355(typeof lineCount !== 'undefined' && range.startLineNumber > lineCount356? this.editor.getBottomForLineNumber(lineCount)357: this.editor.getTopForLineNumber(range.startLineNumber)358)359- this.scrollTop.read(reader)360);361const e = range.isEmpty ? s : (this.editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this.scrollTop.read(reader));362return new OffsetRange(s, e);363});364}365366public observePosition(position: IObservable<Position | null>, store: DisposableStore): IObservable<Point | null> {367let pos = position.get();368const result = observableValueOpts<Point | null>({ owner: this, debugName: () => `topLeftOfPosition${pos?.toString()}`, equalsFn: equalsIfDefined(Point.equals) }, new Point(0, 0));369const contentWidgetId = `observablePositionWidget` + (this._widgetCounter++);370const domNode = document.createElement('div');371const w: IContentWidget = {372getDomNode: () => domNode,373getPosition: () => {374return pos ? { preference: [ContentWidgetPositionPreference.EXACT], position: position.get() } : null;375},376getId: () => contentWidgetId,377allowEditorOverflow: false,378afterRender: (position, coordinate) => {379const model = this._model.get();380if (model && pos && pos.lineNumber > model.getLineCount()) {381// the position is after the last line382result.set(new Point(0, this.editor.getBottomForLineNumber(model.getLineCount()) - this.scrollTop.get()), undefined);383} else {384result.set(coordinate ? new Point(coordinate.left, coordinate.top) : null, undefined);385}386},387};388this.editor.addContentWidget(w);389store.add(autorun(reader => {390pos = position.read(reader);391this.editor.layoutContentWidget(w);392}));393store.add(toDisposable(() => {394this.editor.removeContentWidget(w);395}));396return result;397}398399public readonly openedPeekWidgets;400401isTargetHovered(predicate: (target: IEditorMouseEvent) => boolean, store: DisposableStore): IObservable<boolean> {402const isHovered = observableValue('isInjectedTextHovered', false);403store.add(this.editor.onMouseMove(e => {404const val = predicate(e);405isHovered.set(val, undefined);406}));407408store.add(this.editor.onMouseLeave(E => {409isHovered.set(false, undefined);410}));411return isHovered;412}413414observeLineHeightForPosition(position: IObservable<Position> | Position): IObservable<number>;415observeLineHeightForPosition(position: IObservable<null>): IObservable<null>;416observeLineHeightForPosition(position: IObservable<Position | null> | Position): IObservable<number | null> {417return derived(reader => {418const pos = position instanceof Position ? position : position.read(reader);419if (pos === null) {420return null;421}422423this.getOption(EditorOption.lineHeight).read(reader);424425return this.editor.getLineHeightForPosition(pos);426});427}428429observeLineHeightForLine(lineNumber: IObservable<number> | number): IObservable<number>;430observeLineHeightForLine(lineNumber: IObservable<null>): IObservable<null>;431observeLineHeightForLine(lineNumber: IObservable<number | null> | number): IObservable<number | null> {432if (typeof lineNumber === 'number') {433return this.observeLineHeightForPosition(new Position(lineNumber, 1));434}435436return derived(reader => {437const line = lineNumber.read(reader);438if (line === null) {439return null;440}441442return this.observeLineHeightForPosition(new Position(line, 1)).read(reader);443});444}445446observeLineHeightsForLineRange(lineNumber: IObservable<LineRange> | LineRange): IObservable<number[]> {447return derived(reader => {448const range = lineNumber instanceof LineRange ? lineNumber : lineNumber.read(reader);449450const heights: number[] = [];451for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) {452heights.push(this.observeLineHeightForLine(i).read(reader));453}454return heights;455});456}457458}459460interface IObservableOverlayWidget {461get domNode(): HTMLElement;462readonly position: IObservable<IOverlayWidgetPosition | null>;463readonly minContentWidthInPx: IObservable<number>;464get allowEditorOverflow(): boolean;465}466467interface IObservableContentWidget {468get domNode(): HTMLElement;469readonly position: IObservable<IContentWidgetPosition | null>;470get allowEditorOverflow(): boolean;471}472473474