Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/observableCodeEditor.ts
3292 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { equalsIfDefined, itemsEquals } from '../../base/common/equals.js';
7
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js';
8
import { DebugLocation, IObservable, IObservableWithChange, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js';
9
import { EditorOption, FindComputedEditorOptionValueById } from '../common/config/editorOptions.js';
10
import { LineRange } from '../common/core/ranges/lineRange.js';
11
import { OffsetRange } from '../common/core/ranges/offsetRange.js';
12
import { Position } from '../common/core/position.js';
13
import { Selection } from '../common/core/selection.js';
14
import { ICursorSelectionChangedEvent } from '../common/cursorEvents.js';
15
import { IModelDeltaDecoration, ITextModel } from '../common/model.js';
16
import { IModelContentChangedEvent } from '../common/textModelEvents.js';
17
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent, IOverlayWidget, IOverlayWidgetPosition, IPasteEvent } from './editorBrowser.js';
18
import { Point } from '../common/core/2d/point.js';
19
20
/**
21
* Returns a facade for the code editor that provides observables for various states/events.
22
*/
23
export function observableCodeEditor(editor: ICodeEditor): ObservableCodeEditor {
24
return ObservableCodeEditor.get(editor);
25
}
26
27
export class ObservableCodeEditor extends Disposable {
28
private static readonly _map = new Map<ICodeEditor, ObservableCodeEditor>();
29
30
/**
31
* Make sure that editor is not disposed yet!
32
*/
33
public static get(editor: ICodeEditor): ObservableCodeEditor {
34
let result = ObservableCodeEditor._map.get(editor);
35
if (!result) {
36
result = new ObservableCodeEditor(editor);
37
ObservableCodeEditor._map.set(editor, result);
38
const d = editor.onDidDispose(() => {
39
const item = ObservableCodeEditor._map.get(editor);
40
if (item) {
41
ObservableCodeEditor._map.delete(editor);
42
item.dispose();
43
d.dispose();
44
}
45
});
46
}
47
return result;
48
}
49
50
private _updateCounter;
51
private _currentTransaction: TransactionImpl | undefined;
52
53
private _beginUpdate(): void {
54
this._updateCounter++;
55
if (this._updateCounter === 1) {
56
this._currentTransaction = new TransactionImpl(() => {
57
/** @description Update editor state */
58
});
59
}
60
}
61
62
private _endUpdate(): void {
63
this._updateCounter--;
64
if (this._updateCounter === 0) {
65
const t = this._currentTransaction!;
66
this._currentTransaction = undefined;
67
t.finish();
68
}
69
}
70
71
private constructor(public readonly editor: ICodeEditor) {
72
super();
73
this._updateCounter = 0;
74
this._currentTransaction = undefined;
75
this._model = observableValue(this, this.editor.getModel());
76
this.model = this._model;
77
this.isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly));
78
this._versionId = observableValueOpts<number | null, IModelContentChangedEvent | undefined>({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null);
79
this.versionId = this._versionId;
80
this._selections = observableValueOpts<Selection[] | null, ICursorSelectionChangedEvent | undefined>(
81
{ owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true },
82
this.editor.getSelections() ?? null
83
);
84
this.selections = this._selections;
85
this.positions = derivedOpts<readonly Position[] | null>(
86
{ owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) },
87
reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null
88
);
89
this.isFocused = observableFromEvent(this, e => {
90
const d1 = this.editor.onDidFocusEditorWidget(e);
91
const d2 = this.editor.onDidBlurEditorWidget(e);
92
return {
93
dispose() {
94
d1.dispose();
95
d2.dispose();
96
}
97
};
98
}, () => this.editor.hasWidgetFocus());
99
this.isTextFocused = observableFromEvent(this, e => {
100
const d1 = this.editor.onDidFocusEditorText(e);
101
const d2 = this.editor.onDidBlurEditorText(e);
102
return {
103
dispose() {
104
d1.dispose();
105
d2.dispose();
106
}
107
};
108
}, () => this.editor.hasTextFocus());
109
this.inComposition = observableFromEvent(this, e => {
110
const d1 = this.editor.onDidCompositionStart(() => {
111
e(undefined);
112
});
113
const d2 = this.editor.onDidCompositionEnd(() => {
114
e(undefined);
115
});
116
return {
117
dispose() {
118
d1.dispose();
119
d2.dispose();
120
}
121
};
122
}, () => this.editor.inComposition);
123
this.value = derivedWithSetter(this,
124
reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue() ?? ''; },
125
(value, tx) => {
126
const model = this.model.get();
127
if (model !== null) {
128
if (value !== model.getValue()) {
129
model.setValue(value);
130
}
131
}
132
}
133
);
134
this.valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; });
135
this.cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null);
136
this.cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null);
137
this.cursorLineNumber = derived<number | null>(this, reader => this.cursorPosition.read(reader)?.lineNumber ?? null);
138
this.onDidType = observableSignal<string>(this);
139
this.onDidPaste = observableSignal<IPasteEvent>(this);
140
this.scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop());
141
this.scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft());
142
this.layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo());
143
this.layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft);
144
this.layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft);
145
this.layoutInfoWidth = this.layoutInfo.map(l => l.width);
146
this.layoutInfoHeight = this.layoutInfo.map(l => l.height);
147
this.layoutInfoMinimap = this.layoutInfo.map(l => l.minimap);
148
this.layoutInfoVerticalScrollbarWidth = this.layoutInfo.map(l => l.verticalScrollbarWidth);
149
this.contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth());
150
this.contentHeight = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentHeight());
151
152
this._widgetCounter = 0;
153
this.openedPeekWidgets = observableValue(this, 0);
154
155
this._register(this.editor.onBeginUpdate(() => this._beginUpdate()));
156
this._register(this.editor.onEndUpdate(() => this._endUpdate()));
157
158
this._register(this.editor.onDidChangeModel(() => {
159
this._beginUpdate();
160
try {
161
this._model.set(this.editor.getModel(), this._currentTransaction);
162
this._forceUpdate();
163
} finally {
164
this._endUpdate();
165
}
166
}));
167
168
this._register(this.editor.onDidType((e) => {
169
this._beginUpdate();
170
try {
171
this._forceUpdate();
172
this.onDidType.trigger(this._currentTransaction, e);
173
} finally {
174
this._endUpdate();
175
}
176
}));
177
178
this._register(this.editor.onDidPaste((e) => {
179
this._beginUpdate();
180
try {
181
this._forceUpdate();
182
this.onDidPaste.trigger(this._currentTransaction, e);
183
} finally {
184
this._endUpdate();
185
}
186
}));
187
188
this._register(this.editor.onDidChangeModelContent(e => {
189
this._beginUpdate();
190
try {
191
this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, e);
192
this._forceUpdate();
193
} finally {
194
this._endUpdate();
195
}
196
}));
197
198
this._register(this.editor.onDidChangeCursorSelection(e => {
199
this._beginUpdate();
200
try {
201
this._selections.set(this.editor.getSelections(), this._currentTransaction, e);
202
this._forceUpdate();
203
} finally {
204
this._endUpdate();
205
}
206
}));
207
208
this.domNode = derived(reader => {
209
this.model.read(reader);
210
return this.editor.getDomNode();
211
});
212
}
213
214
public forceUpdate(): void;
215
public forceUpdate<T>(cb: (tx: ITransaction) => T): T;
216
public forceUpdate<T>(cb?: (tx: ITransaction) => T): T {
217
this._beginUpdate();
218
try {
219
this._forceUpdate();
220
if (!cb) { return undefined as T; }
221
return cb(this._currentTransaction!);
222
} finally {
223
this._endUpdate();
224
}
225
}
226
227
private _forceUpdate(): void {
228
this._beginUpdate();
229
try {
230
this._model.set(this.editor.getModel(), this._currentTransaction);
231
this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, undefined);
232
this._selections.set(this.editor.getSelections(), this._currentTransaction, undefined);
233
} finally {
234
this._endUpdate();
235
}
236
}
237
238
private readonly _model;
239
public readonly model: IObservable<ITextModel | null>;
240
241
public readonly isReadonly;
242
243
private readonly _versionId;
244
public readonly versionId: IObservableWithChange<number | null, IModelContentChangedEvent | undefined>;
245
246
private readonly _selections;
247
public readonly selections: IObservableWithChange<Selection[] | null, ICursorSelectionChangedEvent | undefined>;
248
249
250
public readonly positions;
251
252
public readonly isFocused;
253
254
public readonly isTextFocused;
255
256
public readonly inComposition;
257
258
public readonly value;
259
public readonly valueIsEmpty;
260
public readonly cursorSelection;
261
public readonly cursorPosition;
262
public readonly cursorLineNumber;
263
264
public readonly onDidType;
265
public readonly onDidPaste;
266
267
public readonly scrollTop;
268
public readonly scrollLeft;
269
270
public readonly layoutInfo;
271
public readonly layoutInfoContentLeft;
272
public readonly layoutInfoDecorationsLeft;
273
public readonly layoutInfoWidth;
274
public readonly layoutInfoHeight;
275
public readonly layoutInfoMinimap;
276
public readonly layoutInfoVerticalScrollbarWidth;
277
278
public readonly contentWidth;
279
public readonly contentHeight;
280
281
public readonly domNode;
282
283
public getOption<T extends EditorOption>(id: T, debugLocation = DebugLocation.ofCaller()): IObservable<FindComputedEditorOptionValueById<T>> {
284
return observableFromEvent(this, cb => this.editor.onDidChangeConfiguration(e => {
285
if (e.hasChanged(id)) { cb(undefined); }
286
}), () => this.editor.getOption(id), debugLocation);
287
}
288
289
public setDecorations(decorations: IObservable<IModelDeltaDecoration[]>): IDisposable {
290
const d = new DisposableStore();
291
const decorationsCollection = this.editor.createDecorationsCollection();
292
d.add(autorunOpts({ owner: this, debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => {
293
const d = decorations.read(reader);
294
decorationsCollection.set(d);
295
}));
296
d.add({
297
dispose: () => {
298
decorationsCollection.clear();
299
}
300
});
301
return d;
302
}
303
304
private _widgetCounter;
305
306
public createOverlayWidget(widget: IObservableOverlayWidget): IDisposable {
307
const overlayWidgetId = 'observableOverlayWidget' + (this._widgetCounter++);
308
const w: IOverlayWidget = {
309
getDomNode: () => widget.domNode,
310
getPosition: () => widget.position.get(),
311
getId: () => overlayWidgetId,
312
allowEditorOverflow: widget.allowEditorOverflow,
313
getMinContentWidthInPx: () => widget.minContentWidthInPx.get(),
314
};
315
this.editor.addOverlayWidget(w);
316
const d = autorun(reader => {
317
widget.position.read(reader);
318
widget.minContentWidthInPx.read(reader);
319
this.editor.layoutOverlayWidget(w);
320
});
321
return toDisposable(() => {
322
d.dispose();
323
this.editor.removeOverlayWidget(w);
324
});
325
}
326
327
public createContentWidget(widget: IObservableContentWidget): IDisposable {
328
const contentWidgetId = 'observableContentWidget' + (this._widgetCounter++);
329
const w: IContentWidget = {
330
getDomNode: () => widget.domNode,
331
getPosition: () => widget.position.get(),
332
getId: () => contentWidgetId,
333
allowEditorOverflow: widget.allowEditorOverflow,
334
};
335
this.editor.addContentWidget(w);
336
const d = autorun(reader => {
337
widget.position.read(reader);
338
this.editor.layoutContentWidget(w);
339
});
340
return toDisposable(() => {
341
d.dispose();
342
this.editor.removeContentWidget(w);
343
});
344
}
345
346
public observeLineOffsetRange(lineRange: IObservable<LineRange>, store: DisposableStore): IObservable<OffsetRange> {
347
const start = this.observePosition(lineRange.map(r => new Position(r.startLineNumber, 1)), store);
348
const end = this.observePosition(lineRange.map(r => new Position(r.endLineNumberExclusive + 1, 1)), store);
349
350
return derived(reader => {
351
start.read(reader);
352
end.read(reader);
353
const range = lineRange.read(reader);
354
const lineCount = this.model.read(reader)?.getLineCount();
355
const s = (
356
(typeof lineCount !== 'undefined' && range.startLineNumber > lineCount
357
? this.editor.getBottomForLineNumber(lineCount)
358
: this.editor.getTopForLineNumber(range.startLineNumber)
359
)
360
- this.scrollTop.read(reader)
361
);
362
const e = range.isEmpty ? s : (this.editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this.scrollTop.read(reader));
363
return new OffsetRange(s, e);
364
});
365
}
366
367
public observePosition(position: IObservable<Position | null>, store: DisposableStore): IObservable<Point | null> {
368
let pos = position.get();
369
const result = observableValueOpts<Point | null>({ owner: this, debugName: () => `topLeftOfPosition${pos?.toString()}`, equalsFn: equalsIfDefined(Point.equals) }, new Point(0, 0));
370
const contentWidgetId = `observablePositionWidget` + (this._widgetCounter++);
371
const domNode = document.createElement('div');
372
const w: IContentWidget = {
373
getDomNode: () => domNode,
374
getPosition: () => {
375
return pos ? { preference: [ContentWidgetPositionPreference.EXACT], position: position.get() } : null;
376
},
377
getId: () => contentWidgetId,
378
allowEditorOverflow: false,
379
afterRender: (position, coordinate) => {
380
const model = this._model.get();
381
if (model && pos && pos.lineNumber > model.getLineCount()) {
382
// the position is after the last line
383
result.set(new Point(0, this.editor.getBottomForLineNumber(model.getLineCount()) - this.scrollTop.get()), undefined);
384
} else {
385
result.set(coordinate ? new Point(coordinate.left, coordinate.top) : null, undefined);
386
}
387
},
388
};
389
this.editor.addContentWidget(w);
390
store.add(autorun(reader => {
391
pos = position.read(reader);
392
this.editor.layoutContentWidget(w);
393
}));
394
store.add(toDisposable(() => {
395
this.editor.removeContentWidget(w);
396
}));
397
return result;
398
}
399
400
public readonly openedPeekWidgets;
401
402
isTargetHovered(predicate: (target: IEditorMouseEvent) => boolean, store: DisposableStore): IObservable<boolean> {
403
const isHovered = observableValue('isInjectedTextHovered', false);
404
store.add(this.editor.onMouseMove(e => {
405
const val = predicate(e);
406
isHovered.set(val, undefined);
407
}));
408
409
store.add(this.editor.onMouseLeave(E => {
410
isHovered.set(false, undefined);
411
}));
412
return isHovered;
413
}
414
415
observeLineHeightForPosition(position: IObservable<Position> | Position): IObservable<number>;
416
observeLineHeightForPosition(position: IObservable<null>): IObservable<null>;
417
observeLineHeightForPosition(position: IObservable<Position | null> | Position): IObservable<number | null> {
418
return derived(reader => {
419
const pos = position instanceof Position ? position : position.read(reader);
420
if (pos === null) {
421
return null;
422
}
423
424
this.getOption(EditorOption.lineHeight).read(reader);
425
426
return this.editor.getLineHeightForPosition(pos);
427
});
428
}
429
430
observeLineHeightForLine(lineNumber: IObservable<number> | number): IObservable<number>;
431
observeLineHeightForLine(lineNumber: IObservable<null>): IObservable<null>;
432
observeLineHeightForLine(lineNumber: IObservable<number | null> | number): IObservable<number | null> {
433
if (typeof lineNumber === 'number') {
434
return this.observeLineHeightForPosition(new Position(lineNumber, 1));
435
}
436
437
return derived(reader => {
438
const line = lineNumber.read(reader);
439
if (line === null) {
440
return null;
441
}
442
443
return this.observeLineHeightForPosition(new Position(line, 1)).read(reader);
444
});
445
}
446
447
observeLineHeightsForLineRange(lineNumber: IObservable<LineRange> | LineRange): IObservable<number[]> {
448
return derived(reader => {
449
const range = lineNumber instanceof LineRange ? lineNumber : lineNumber.read(reader);
450
451
const heights: number[] = [];
452
for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) {
453
heights.push(this.observeLineHeightForLine(i).read(reader));
454
}
455
return heights;
456
});
457
}
458
459
}
460
461
interface IObservableOverlayWidget {
462
get domNode(): HTMLElement;
463
readonly position: IObservable<IOverlayWidgetPosition | null>;
464
readonly minContentWidthInPx: IObservable<number>;
465
get allowEditorOverflow(): boolean;
466
}
467
468
interface IObservableContentWidget {
469
get domNode(): HTMLElement;
470
readonly position: IObservable<IContentWidgetPosition | null>;
471
get allowEditorOverflow(): boolean;
472
}
473
474