Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/widget/diffEditor/utils.ts
3296 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 { IDimension } from '../../../../base/browser/dom.js';
7
import { findLast } from '../../../../base/common/arraysFind.js';
8
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
9
import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js';
10
import { IObservable, IObservableWithChange, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableValue, transaction } from '../../../../base/common/observable.js';
11
import { ElementSizeObserver } from '../../config/elementSizeObserver.js';
12
import { ICodeEditor, IOverlayWidget, IViewZone } from '../../editorBrowser.js';
13
import { Position } from '../../../common/core/position.js';
14
import { Range } from '../../../common/core/range.js';
15
import { DetailedLineRangeMapping } from '../../../common/diff/rangeMapping.js';
16
import { IModelDeltaDecoration } from '../../../common/model.js';
17
import { TextLength } from '../../../common/core/text/textLength.js';
18
19
export function joinCombine<T>(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] {
20
if (arr1.length === 0) {
21
return arr2;
22
}
23
if (arr2.length === 0) {
24
return arr1;
25
}
26
27
const result: T[] = [];
28
let i = 0;
29
let j = 0;
30
while (i < arr1.length && j < arr2.length) {
31
const val1 = arr1[i];
32
const val2 = arr2[j];
33
const key1 = keySelector(val1);
34
const key2 = keySelector(val2);
35
36
if (key1 < key2) {
37
result.push(val1);
38
i++;
39
} else if (key1 > key2) {
40
result.push(val2);
41
j++;
42
} else {
43
result.push(combine(val1, val2));
44
i++;
45
j++;
46
}
47
}
48
while (i < arr1.length) {
49
result.push(arr1[i]);
50
i++;
51
}
52
while (j < arr2.length) {
53
result.push(arr2[j]);
54
j++;
55
}
56
return result;
57
}
58
59
// TODO make utility
60
export function applyObservableDecorations(editor: ICodeEditor, decorations: IObservable<IModelDeltaDecoration[]>): IDisposable {
61
const d = new DisposableStore();
62
const decorationsCollection = editor.createDecorationsCollection();
63
d.add(autorunOpts({ debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => {
64
const d = decorations.read(reader);
65
decorationsCollection.set(d);
66
}));
67
d.add({
68
dispose: () => {
69
decorationsCollection.clear();
70
}
71
});
72
return d;
73
}
74
75
export function appendRemoveOnDispose(parent: HTMLElement, child: HTMLElement) {
76
parent.appendChild(child);
77
return toDisposable(() => {
78
child.remove();
79
});
80
}
81
82
export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) {
83
parent.prepend(child);
84
return toDisposable(() => {
85
child.remove();
86
});
87
}
88
89
export class ObservableElementSizeObserver extends Disposable {
90
private readonly elementSizeObserver: ElementSizeObserver;
91
92
private readonly _width: ISettableObservable<number>;
93
public get width(): IObservable<number> { return this._width; }
94
95
private readonly _height: ISettableObservable<number>;
96
public get height(): IObservable<number> { return this._height; }
97
98
private _automaticLayout: boolean = false;
99
public get automaticLayout(): boolean { return this._automaticLayout; }
100
101
constructor(element: HTMLElement | null, dimension: IDimension | undefined) {
102
super();
103
104
this.elementSizeObserver = this._register(new ElementSizeObserver(element, dimension));
105
this._width = observableValue(this, this.elementSizeObserver.getWidth());
106
this._height = observableValue(this, this.elementSizeObserver.getHeight());
107
108
this._register(this.elementSizeObserver.onDidChange(e => transaction(tx => {
109
/** @description Set width/height from elementSizeObserver */
110
this._width.set(this.elementSizeObserver.getWidth(), tx);
111
this._height.set(this.elementSizeObserver.getHeight(), tx);
112
})));
113
}
114
115
public observe(dimension?: IDimension): void {
116
this.elementSizeObserver.observe(dimension);
117
}
118
119
public setAutomaticLayout(automaticLayout: boolean): void {
120
this._automaticLayout = automaticLayout;
121
if (automaticLayout) {
122
this.elementSizeObserver.startObserving();
123
} else {
124
this.elementSizeObserver.stopObserving();
125
}
126
}
127
}
128
129
export function animatedObservable(targetWindow: Window, base: IObservableWithChange<number, boolean>, store: DisposableStore): IObservable<number> {
130
let targetVal = base.get();
131
let startVal = targetVal;
132
let curVal = targetVal;
133
const result = observableValue('animatedValue', targetVal);
134
135
let animationStartMs: number = -1;
136
const durationMs = 300;
137
let animationFrame: number | undefined = undefined;
138
139
store.add(autorunHandleChanges({
140
changeTracker: {
141
createChangeSummary: () => ({ animate: false }),
142
handleChange: (ctx, s) => {
143
if (ctx.didChange(base)) {
144
s.animate = s.animate || ctx.change;
145
}
146
return true;
147
}
148
}
149
}, (reader, s) => {
150
/** @description update value */
151
if (animationFrame !== undefined) {
152
targetWindow.cancelAnimationFrame(animationFrame);
153
animationFrame = undefined;
154
}
155
156
startVal = curVal;
157
targetVal = base.read(reader);
158
animationStartMs = Date.now() - (s.animate ? 0 : durationMs);
159
160
update();
161
}));
162
163
function update() {
164
const passedMs = Date.now() - animationStartMs;
165
curVal = Math.floor(easeOutExpo(passedMs, startVal, targetVal - startVal, durationMs));
166
167
if (passedMs < durationMs) {
168
animationFrame = targetWindow.requestAnimationFrame(update);
169
} else {
170
curVal = targetVal;
171
}
172
173
result.set(curVal, undefined);
174
}
175
176
return result;
177
}
178
179
function easeOutExpo(t: number, b: number, c: number, d: number): number {
180
return t === d ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
181
}
182
183
export function deepMerge<T extends {}>(source1: T, source2: Partial<T>): T {
184
const result = {} as any as T;
185
for (const key in source1) {
186
result[key] = source1[key];
187
}
188
for (const key in source2) {
189
const source2Value = source2[key];
190
if (typeof result[key] === 'object' && source2Value && typeof source2Value === 'object') {
191
result[key] = deepMerge<any>(result[key], source2Value);
192
} else {
193
result[key] = source2Value as any;
194
}
195
}
196
return result;
197
}
198
199
export abstract class ViewZoneOverlayWidget extends Disposable {
200
constructor(
201
editor: ICodeEditor,
202
viewZone: PlaceholderViewZone,
203
htmlElement: HTMLElement,
204
) {
205
super();
206
207
this._register(new ManagedOverlayWidget(editor, htmlElement));
208
this._register(applyStyle(htmlElement, {
209
height: viewZone.actualHeight,
210
top: viewZone.actualTop,
211
}));
212
}
213
}
214
215
export interface IObservableViewZone extends IViewZone {
216
// Causes the view zone to relayout.
217
onChange?: IObservable<unknown>;
218
219
// Tells a view zone its id.
220
setZoneId?(zoneId: string): void;
221
}
222
223
export class PlaceholderViewZone implements IObservableViewZone {
224
public readonly domNode;
225
226
private readonly _actualTop;
227
private readonly _actualHeight;
228
229
public readonly actualTop: IObservable<number | undefined>;
230
public readonly actualHeight: IObservable<number | undefined>;
231
232
public readonly showInHiddenAreas;
233
234
public get afterLineNumber(): number { return this._afterLineNumber.get(); }
235
236
public readonly onChange?: IObservable<unknown>;
237
238
constructor(
239
private readonly _afterLineNumber: IObservable<number>,
240
public readonly heightInPx: number,
241
) {
242
this.domNode = document.createElement('div');
243
this._actualTop = observableValue<number | undefined>(this, undefined);
244
this._actualHeight = observableValue<number | undefined>(this, undefined);
245
this.actualTop = this._actualTop;
246
this.actualHeight = this._actualHeight;
247
this.showInHiddenAreas = true;
248
this.onChange = this._afterLineNumber;
249
this.onDomNodeTop = (top: number) => {
250
this._actualTop.set(top, undefined);
251
};
252
this.onComputedHeight = (height: number) => {
253
this._actualHeight.set(height, undefined);
254
};
255
}
256
257
onDomNodeTop;
258
259
onComputedHeight;
260
}
261
262
263
export class ManagedOverlayWidget implements IDisposable {
264
private static _counter = 0;
265
private readonly _overlayWidgetId = `managedOverlayWidget-${ManagedOverlayWidget._counter++}`;
266
267
private readonly _overlayWidget: IOverlayWidget = {
268
getId: () => this._overlayWidgetId,
269
getDomNode: () => this._domElement,
270
getPosition: () => null
271
};
272
273
constructor(
274
private readonly _editor: ICodeEditor,
275
private readonly _domElement: HTMLElement,
276
) {
277
this._editor.addOverlayWidget(this._overlayWidget);
278
}
279
280
dispose(): void {
281
this._editor.removeOverlayWidget(this._overlayWidget);
282
}
283
}
284
285
export interface CSSStyle {
286
height: number | string;
287
width: number | string;
288
top: number | string;
289
visibility: 'visible' | 'hidden' | 'collapse';
290
display: 'block' | 'inline' | 'inline-block' | 'flex' | 'none';
291
paddingLeft: number | string;
292
paddingRight: number | string;
293
}
294
295
export function applyStyle(domNode: HTMLElement, style: Partial<{ [TKey in keyof CSSStyle]: CSSStyle[TKey] | IObservable<CSSStyle[TKey] | undefined> | undefined }>) {
296
return autorun(reader => {
297
/** @description applyStyle */
298
for (let [key, val] of Object.entries(style)) {
299
if (val && typeof val === 'object' && 'read' in val) {
300
val = val.read(reader) as any;
301
}
302
if (typeof val === 'number') {
303
val = `${val}px`;
304
}
305
key = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
306
domNode.style[key as any] = val as any;
307
}
308
});
309
}
310
311
export function applyViewZones(editor: ICodeEditor, viewZones: IObservable<IObservableViewZone[]>, setIsUpdating?: (isUpdatingViewZones: boolean) => void, zoneIds?: Set<string>): IDisposable {
312
const store = new DisposableStore();
313
const lastViewZoneIds: string[] = [];
314
315
store.add(autorunWithStore((reader, store) => {
316
/** @description applyViewZones */
317
const curViewZones = viewZones.read(reader);
318
319
const viewZonIdsPerViewZone = new Map<IObservableViewZone, string>();
320
const viewZoneIdPerOnChangeObservable = new Map<IObservable<unknown>, string>();
321
322
// Add/remove view zones
323
if (setIsUpdating) { setIsUpdating(true); }
324
editor.changeViewZones(a => {
325
for (const id of lastViewZoneIds) { a.removeZone(id); zoneIds?.delete(id); }
326
lastViewZoneIds.length = 0;
327
328
for (const z of curViewZones) {
329
const id = a.addZone(z);
330
if (z.setZoneId) {
331
z.setZoneId(id);
332
}
333
lastViewZoneIds.push(id);
334
zoneIds?.add(id);
335
viewZonIdsPerViewZone.set(z, id);
336
}
337
});
338
if (setIsUpdating) { setIsUpdating(false); }
339
340
// Layout zone on change
341
store.add(autorunHandleChanges({
342
changeTracker: {
343
createChangeSummary() {
344
return { zoneIds: [] as string[] };
345
},
346
handleChange(context, changeSummary) {
347
const id = viewZoneIdPerOnChangeObservable.get(context.changedObservable);
348
if (id !== undefined) { changeSummary.zoneIds.push(id); }
349
return true;
350
},
351
}
352
}, (reader, changeSummary) => {
353
/** @description layoutZone on change */
354
for (const vz of curViewZones) {
355
if (vz.onChange) {
356
viewZoneIdPerOnChangeObservable.set(vz.onChange, viewZonIdsPerViewZone.get(vz)!);
357
vz.onChange.read(reader);
358
}
359
}
360
if (setIsUpdating) { setIsUpdating(true); }
361
editor.changeViewZones(a => { for (const id of changeSummary.zoneIds) { a.layoutZone(id); } });
362
if (setIsUpdating) { setIsUpdating(false); }
363
}));
364
}));
365
366
store.add({
367
dispose() {
368
if (setIsUpdating) { setIsUpdating(true); }
369
editor.changeViewZones(a => { for (const id of lastViewZoneIds) { a.removeZone(id); } });
370
zoneIds?.clear();
371
if (setIsUpdating) { setIsUpdating(false); }
372
}
373
});
374
375
return store;
376
}
377
378
export class DisposableCancellationTokenSource extends CancellationTokenSource {
379
public override dispose() {
380
super.dispose(true);
381
}
382
}
383
384
export function translatePosition(posInOriginal: Position, mappings: DetailedLineRangeMapping[]): Range {
385
const mapping = findLast(mappings, m => m.original.startLineNumber <= posInOriginal.lineNumber);
386
if (!mapping) {
387
// No changes before the position
388
return Range.fromPositions(posInOriginal);
389
}
390
391
if (mapping.original.endLineNumberExclusive <= posInOriginal.lineNumber) {
392
const newLineNumber = posInOriginal.lineNumber - mapping.original.endLineNumberExclusive + mapping.modified.endLineNumberExclusive;
393
return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));
394
}
395
396
if (!mapping.innerChanges) {
397
// Only for legacy algorithm
398
return Range.fromPositions(new Position(mapping.modified.startLineNumber, 1));
399
}
400
401
const innerMapping = findLast(mapping.innerChanges, m => m.originalRange.getStartPosition().isBeforeOrEqual(posInOriginal));
402
if (!innerMapping) {
403
const newLineNumber = posInOriginal.lineNumber - mapping.original.startLineNumber + mapping.modified.startLineNumber;
404
return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));
405
}
406
407
if (innerMapping.originalRange.containsPosition(posInOriginal)) {
408
return innerMapping.modifiedRange;
409
} else {
410
const l = lengthBetweenPositions(innerMapping.originalRange.getEndPosition(), posInOriginal);
411
return Range.fromPositions(l.addToPosition(innerMapping.modifiedRange.getEndPosition()));
412
}
413
}
414
415
function lengthBetweenPositions(position1: Position, position2: Position): TextLength {
416
if (position1.lineNumber === position2.lineNumber) {
417
return new TextLength(0, position2.column - position1.column);
418
} else {
419
return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1);
420
}
421
}
422
423
export function filterWithPrevious<T>(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] {
424
let prev: T | undefined;
425
return arr.filter(cur => {
426
const result = filter(cur, prev);
427
prev = cur;
428
return result;
429
});
430
}
431
432
export interface IRefCounted extends IDisposable {
433
createNewRef(): this;
434
}
435
436
export abstract class RefCounted<T> implements IDisposable, IReference<T> {
437
public static create<T extends IDisposable>(value: T, debugOwner: object | undefined = undefined): RefCounted<T> {
438
return new BaseRefCounted(value, value, debugOwner);
439
}
440
441
public static createWithDisposable<T extends IDisposable>(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted<T> {
442
const store = new DisposableStore();
443
store.add(disposable);
444
store.add(value);
445
return new BaseRefCounted(value, store, debugOwner);
446
}
447
448
public static createOfNonDisposable<T>(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted<T> {
449
return new BaseRefCounted(value, disposable, debugOwner);
450
}
451
452
public abstract createNewRef(debugOwner?: object | undefined): RefCounted<T>;
453
454
public abstract dispose(): void;
455
456
public abstract get object(): T;
457
}
458
459
class BaseRefCounted<T> extends RefCounted<T> {
460
private _refCount = 1;
461
private _isDisposed = false;
462
private readonly _owners: object[] = [];
463
464
constructor(
465
public override readonly object: T,
466
private readonly _disposable: IDisposable,
467
private readonly _debugOwner: object | undefined,
468
) {
469
super();
470
471
if (_debugOwner) {
472
this._addOwner(_debugOwner);
473
}
474
}
475
476
private _addOwner(debugOwner: object | undefined) {
477
if (debugOwner) {
478
this._owners.push(debugOwner);
479
}
480
}
481
482
public createNewRef(debugOwner?: object | undefined): RefCounted<T> {
483
this._refCount++;
484
if (debugOwner) {
485
this._addOwner(debugOwner);
486
}
487
return new ClonedRefCounted(this, debugOwner);
488
}
489
490
public dispose(): void {
491
if (this._isDisposed) { return; }
492
this._isDisposed = true;
493
this._decreaseRefCount(this._debugOwner);
494
}
495
496
public _decreaseRefCount(debugOwner?: object | undefined): void {
497
this._refCount--;
498
if (this._refCount === 0) {
499
this._disposable.dispose();
500
}
501
502
if (debugOwner) {
503
const idx = this._owners.indexOf(debugOwner);
504
if (idx !== -1) {
505
this._owners.splice(idx, 1);
506
}
507
}
508
}
509
}
510
511
class ClonedRefCounted<T> extends RefCounted<T> {
512
private _isDisposed = false;
513
constructor(
514
private readonly _base: BaseRefCounted<T>,
515
private readonly _debugOwner: object | undefined,
516
) {
517
super();
518
}
519
520
public get object(): T { return this._base.object; }
521
522
public createNewRef(debugOwner?: object | undefined): RefCounted<T> {
523
return this._base.createNewRef(debugOwner);
524
}
525
526
public dispose(): void {
527
if (this._isDisposed) { return; }
528
this._isDisposed = true;
529
this._base._decreaseRefCount(this._debugOwner);
530
}
531
}
532
533