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
5230 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
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
185
const result = {} as any as T;
186
for (const key in source1) {
187
result[key] = source1[key];
188
}
189
for (const key in source2) {
190
const source2Value = source2[key];
191
if (typeof result[key] === 'object' && source2Value && typeof source2Value === 'object') {
192
// eslint-disable-next-line @typescript-eslint/no-explicit-any
193
result[key] = deepMerge<any>(result[key], source2Value);
194
} else {
195
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
196
result[key] = source2Value as any;
197
}
198
}
199
return result;
200
}
201
202
export abstract class ViewZoneOverlayWidget extends Disposable {
203
constructor(
204
editor: ICodeEditor,
205
viewZone: PlaceholderViewZone,
206
htmlElement: HTMLElement,
207
) {
208
super();
209
210
this._register(new ManagedOverlayWidget(editor, htmlElement));
211
this._register(applyStyle(htmlElement, {
212
height: viewZone.actualHeight,
213
top: viewZone.actualTop,
214
}));
215
}
216
}
217
218
export interface IObservableViewZone extends IViewZone {
219
// Causes the view zone to relayout.
220
onChange?: IObservable<unknown>;
221
222
// Tells a view zone its id.
223
setZoneId?(zoneId: string): void;
224
}
225
226
export class PlaceholderViewZone implements IObservableViewZone {
227
public readonly domNode;
228
229
private readonly _actualTop;
230
private readonly _actualHeight;
231
232
public readonly actualTop: IObservable<number | undefined>;
233
public readonly actualHeight: IObservable<number | undefined>;
234
235
public readonly showInHiddenAreas;
236
237
public get afterLineNumber(): number { return this._afterLineNumber.get(); }
238
239
public readonly onChange?: IObservable<unknown>;
240
241
constructor(
242
private readonly _afterLineNumber: IObservable<number>,
243
public readonly heightInPx: number,
244
) {
245
this.domNode = document.createElement('div');
246
this._actualTop = observableValue<number | undefined>(this, undefined);
247
this._actualHeight = observableValue<number | undefined>(this, undefined);
248
this.actualTop = this._actualTop;
249
this.actualHeight = this._actualHeight;
250
this.showInHiddenAreas = true;
251
this.onChange = this._afterLineNumber;
252
this.onDomNodeTop = (top: number) => {
253
this._actualTop.set(top, undefined);
254
};
255
this.onComputedHeight = (height: number) => {
256
this._actualHeight.set(height, undefined);
257
};
258
}
259
260
onDomNodeTop;
261
262
onComputedHeight;
263
}
264
265
266
export class ManagedOverlayWidget implements IDisposable {
267
private static _counter = 0;
268
private readonly _overlayWidgetId = `managedOverlayWidget-${ManagedOverlayWidget._counter++}`;
269
270
private readonly _overlayWidget: IOverlayWidget = {
271
getId: () => this._overlayWidgetId,
272
getDomNode: () => this._domElement,
273
getPosition: () => null
274
};
275
276
constructor(
277
private readonly _editor: ICodeEditor,
278
private readonly _domElement: HTMLElement,
279
) {
280
this._editor.addOverlayWidget(this._overlayWidget);
281
}
282
283
dispose(): void {
284
this._editor.removeOverlayWidget(this._overlayWidget);
285
}
286
}
287
288
export interface CSSStyle {
289
height: number | string;
290
width: number | string;
291
top: number | string;
292
visibility: 'visible' | 'hidden' | 'collapse';
293
display: 'block' | 'inline' | 'inline-block' | 'flex' | 'none';
294
paddingLeft: number | string;
295
paddingRight: number | string;
296
}
297
298
export function applyStyle(domNode: HTMLElement, style: Partial<{ [TKey in keyof CSSStyle]: CSSStyle[TKey] | IObservable<CSSStyle[TKey] | undefined> | undefined }>) {
299
return autorun(reader => {
300
/** @description applyStyle */
301
for (let [key, val] of Object.entries(style)) {
302
if (val && typeof val === 'object' && 'read' in val) {
303
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
304
val = val.read(reader) as any;
305
}
306
if (typeof val === 'number') {
307
val = `${val}px`;
308
}
309
key = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
310
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
311
domNode.style[key as any] = val as any;
312
}
313
});
314
}
315
316
export function applyViewZones(editor: ICodeEditor, viewZones: IObservable<IObservableViewZone[]>, setIsUpdating?: (isUpdatingViewZones: boolean) => void, zoneIds?: Set<string>): IDisposable {
317
const store = new DisposableStore();
318
const lastViewZoneIds: string[] = [];
319
320
store.add(autorunWithStore((reader, store) => {
321
/** @description applyViewZones */
322
const curViewZones = viewZones.read(reader);
323
324
const viewZonIdsPerViewZone = new Map<IObservableViewZone, string>();
325
const viewZoneIdPerOnChangeObservable = new Map<IObservable<unknown>, string>();
326
327
// Add/remove view zones
328
if (setIsUpdating) { setIsUpdating(true); }
329
editor.changeViewZones(a => {
330
for (const id of lastViewZoneIds) { a.removeZone(id); zoneIds?.delete(id); }
331
lastViewZoneIds.length = 0;
332
333
for (const z of curViewZones) {
334
const id = a.addZone(z);
335
if (z.setZoneId) {
336
z.setZoneId(id);
337
}
338
lastViewZoneIds.push(id);
339
zoneIds?.add(id);
340
viewZonIdsPerViewZone.set(z, id);
341
}
342
});
343
if (setIsUpdating) { setIsUpdating(false); }
344
345
// Layout zone on change
346
store.add(autorunHandleChanges({
347
changeTracker: {
348
createChangeSummary() {
349
return { zoneIds: [] as string[] };
350
},
351
handleChange(context, changeSummary) {
352
const id = viewZoneIdPerOnChangeObservable.get(context.changedObservable);
353
if (id !== undefined) { changeSummary.zoneIds.push(id); }
354
return true;
355
},
356
}
357
}, (reader, changeSummary) => {
358
/** @description layoutZone on change */
359
for (const vz of curViewZones) {
360
if (vz.onChange) {
361
viewZoneIdPerOnChangeObservable.set(vz.onChange, viewZonIdsPerViewZone.get(vz)!);
362
vz.onChange.read(reader);
363
}
364
}
365
if (setIsUpdating) { setIsUpdating(true); }
366
editor.changeViewZones(a => { for (const id of changeSummary.zoneIds) { a.layoutZone(id); } });
367
if (setIsUpdating) { setIsUpdating(false); }
368
}));
369
}));
370
371
store.add({
372
dispose() {
373
if (setIsUpdating) { setIsUpdating(true); }
374
editor.changeViewZones(a => { for (const id of lastViewZoneIds) { a.removeZone(id); } });
375
zoneIds?.clear();
376
if (setIsUpdating) { setIsUpdating(false); }
377
}
378
});
379
380
return store;
381
}
382
383
export class DisposableCancellationTokenSource extends CancellationTokenSource {
384
public override dispose() {
385
super.dispose(true);
386
}
387
}
388
389
export function translatePosition(posInOriginal: Position, mappings: DetailedLineRangeMapping[]): Range {
390
const mapping = findLast(mappings, m => m.original.startLineNumber <= posInOriginal.lineNumber);
391
if (!mapping) {
392
// No changes before the position
393
return Range.fromPositions(posInOriginal);
394
}
395
396
if (mapping.original.endLineNumberExclusive <= posInOriginal.lineNumber) {
397
const newLineNumber = posInOriginal.lineNumber - mapping.original.endLineNumberExclusive + mapping.modified.endLineNumberExclusive;
398
return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));
399
}
400
401
if (!mapping.innerChanges) {
402
// Only for legacy algorithm
403
return Range.fromPositions(new Position(mapping.modified.startLineNumber, 1));
404
}
405
406
const innerMapping = findLast(mapping.innerChanges, m => m.originalRange.getStartPosition().isBeforeOrEqual(posInOriginal));
407
if (!innerMapping) {
408
const newLineNumber = posInOriginal.lineNumber - mapping.original.startLineNumber + mapping.modified.startLineNumber;
409
return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));
410
}
411
412
if (innerMapping.originalRange.containsPosition(posInOriginal)) {
413
return innerMapping.modifiedRange;
414
} else {
415
const l = lengthBetweenPositions(innerMapping.originalRange.getEndPosition(), posInOriginal);
416
return Range.fromPositions(l.addToPosition(innerMapping.modifiedRange.getEndPosition()));
417
}
418
}
419
420
function lengthBetweenPositions(position1: Position, position2: Position): TextLength {
421
if (position1.lineNumber === position2.lineNumber) {
422
return new TextLength(0, position2.column - position1.column);
423
} else {
424
return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1);
425
}
426
}
427
428
export function filterWithPrevious<T>(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] {
429
let prev: T | undefined;
430
return arr.filter(cur => {
431
const result = filter(cur, prev);
432
prev = cur;
433
return result;
434
});
435
}
436
437
export interface IRefCounted extends IDisposable {
438
createNewRef(): this;
439
}
440
441
export abstract class RefCounted<T> implements IDisposable, IReference<T> {
442
public static create<T extends IDisposable>(value: T, debugOwner: object | undefined = undefined): RefCounted<T> {
443
return new BaseRefCounted(value, value, debugOwner);
444
}
445
446
public static createWithDisposable<T extends IDisposable>(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted<T> {
447
const store = new DisposableStore();
448
store.add(disposable);
449
store.add(value);
450
return new BaseRefCounted(value, store, debugOwner);
451
}
452
453
public static createOfNonDisposable<T>(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted<T> {
454
return new BaseRefCounted(value, disposable, debugOwner);
455
}
456
457
public abstract createNewRef(debugOwner?: object | undefined): RefCounted<T>;
458
459
public abstract dispose(): void;
460
461
public abstract get object(): T;
462
}
463
464
class BaseRefCounted<T> extends RefCounted<T> {
465
private _refCount = 1;
466
private _isDisposed = false;
467
private readonly _owners: object[] = [];
468
469
constructor(
470
public override readonly object: T,
471
private readonly _disposable: IDisposable,
472
private readonly _debugOwner: object | undefined,
473
) {
474
super();
475
476
if (_debugOwner) {
477
this._addOwner(_debugOwner);
478
}
479
}
480
481
private _addOwner(debugOwner: object | undefined) {
482
if (debugOwner) {
483
this._owners.push(debugOwner);
484
}
485
}
486
487
public createNewRef(debugOwner?: object | undefined): RefCounted<T> {
488
this._refCount++;
489
if (debugOwner) {
490
this._addOwner(debugOwner);
491
}
492
return new ClonedRefCounted(this, debugOwner);
493
}
494
495
public dispose(): void {
496
if (this._isDisposed) { return; }
497
this._isDisposed = true;
498
this._decreaseRefCount(this._debugOwner);
499
}
500
501
public _decreaseRefCount(debugOwner?: object | undefined): void {
502
this._refCount--;
503
if (this._refCount === 0) {
504
this._disposable.dispose();
505
}
506
507
if (debugOwner) {
508
const idx = this._owners.indexOf(debugOwner);
509
if (idx !== -1) {
510
this._owners.splice(idx, 1);
511
}
512
}
513
}
514
}
515
516
class ClonedRefCounted<T> extends RefCounted<T> {
517
private _isDisposed = false;
518
constructor(
519
private readonly _base: BaseRefCounted<T>,
520
private readonly _debugOwner: object | undefined,
521
) {
522
super();
523
}
524
525
public get object(): T { return this._base.object; }
526
527
public createNewRef(debugOwner?: object | undefined): RefCounted<T> {
528
return this._base.createNewRef(debugOwner);
529
}
530
531
public dispose(): void {
532
if (this._isDisposed) { return; }
533
this._isDisposed = true;
534
this._base._decreaseRefCount(this._debugOwner);
535
}
536
}
537
538