Path: blob/main/src/vs/editor/browser/widget/diffEditor/utils.ts
3296 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 { IDimension } from '../../../../base/browser/dom.js';6import { findLast } from '../../../../base/common/arraysFind.js';7import { CancellationTokenSource } from '../../../../base/common/cancellation.js';8import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js';9import { IObservable, IObservableWithChange, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableValue, transaction } from '../../../../base/common/observable.js';10import { ElementSizeObserver } from '../../config/elementSizeObserver.js';11import { ICodeEditor, IOverlayWidget, IViewZone } from '../../editorBrowser.js';12import { Position } from '../../../common/core/position.js';13import { Range } from '../../../common/core/range.js';14import { DetailedLineRangeMapping } from '../../../common/diff/rangeMapping.js';15import { IModelDeltaDecoration } from '../../../common/model.js';16import { TextLength } from '../../../common/core/text/textLength.js';1718export function joinCombine<T>(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] {19if (arr1.length === 0) {20return arr2;21}22if (arr2.length === 0) {23return arr1;24}2526const result: T[] = [];27let i = 0;28let j = 0;29while (i < arr1.length && j < arr2.length) {30const val1 = arr1[i];31const val2 = arr2[j];32const key1 = keySelector(val1);33const key2 = keySelector(val2);3435if (key1 < key2) {36result.push(val1);37i++;38} else if (key1 > key2) {39result.push(val2);40j++;41} else {42result.push(combine(val1, val2));43i++;44j++;45}46}47while (i < arr1.length) {48result.push(arr1[i]);49i++;50}51while (j < arr2.length) {52result.push(arr2[j]);53j++;54}55return result;56}5758// TODO make utility59export function applyObservableDecorations(editor: ICodeEditor, decorations: IObservable<IModelDeltaDecoration[]>): IDisposable {60const d = new DisposableStore();61const decorationsCollection = editor.createDecorationsCollection();62d.add(autorunOpts({ debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => {63const d = decorations.read(reader);64decorationsCollection.set(d);65}));66d.add({67dispose: () => {68decorationsCollection.clear();69}70});71return d;72}7374export function appendRemoveOnDispose(parent: HTMLElement, child: HTMLElement) {75parent.appendChild(child);76return toDisposable(() => {77child.remove();78});79}8081export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) {82parent.prepend(child);83return toDisposable(() => {84child.remove();85});86}8788export class ObservableElementSizeObserver extends Disposable {89private readonly elementSizeObserver: ElementSizeObserver;9091private readonly _width: ISettableObservable<number>;92public get width(): IObservable<number> { return this._width; }9394private readonly _height: ISettableObservable<number>;95public get height(): IObservable<number> { return this._height; }9697private _automaticLayout: boolean = false;98public get automaticLayout(): boolean { return this._automaticLayout; }99100constructor(element: HTMLElement | null, dimension: IDimension | undefined) {101super();102103this.elementSizeObserver = this._register(new ElementSizeObserver(element, dimension));104this._width = observableValue(this, this.elementSizeObserver.getWidth());105this._height = observableValue(this, this.elementSizeObserver.getHeight());106107this._register(this.elementSizeObserver.onDidChange(e => transaction(tx => {108/** @description Set width/height from elementSizeObserver */109this._width.set(this.elementSizeObserver.getWidth(), tx);110this._height.set(this.elementSizeObserver.getHeight(), tx);111})));112}113114public observe(dimension?: IDimension): void {115this.elementSizeObserver.observe(dimension);116}117118public setAutomaticLayout(automaticLayout: boolean): void {119this._automaticLayout = automaticLayout;120if (automaticLayout) {121this.elementSizeObserver.startObserving();122} else {123this.elementSizeObserver.stopObserving();124}125}126}127128export function animatedObservable(targetWindow: Window, base: IObservableWithChange<number, boolean>, store: DisposableStore): IObservable<number> {129let targetVal = base.get();130let startVal = targetVal;131let curVal = targetVal;132const result = observableValue('animatedValue', targetVal);133134let animationStartMs: number = -1;135const durationMs = 300;136let animationFrame: number | undefined = undefined;137138store.add(autorunHandleChanges({139changeTracker: {140createChangeSummary: () => ({ animate: false }),141handleChange: (ctx, s) => {142if (ctx.didChange(base)) {143s.animate = s.animate || ctx.change;144}145return true;146}147}148}, (reader, s) => {149/** @description update value */150if (animationFrame !== undefined) {151targetWindow.cancelAnimationFrame(animationFrame);152animationFrame = undefined;153}154155startVal = curVal;156targetVal = base.read(reader);157animationStartMs = Date.now() - (s.animate ? 0 : durationMs);158159update();160}));161162function update() {163const passedMs = Date.now() - animationStartMs;164curVal = Math.floor(easeOutExpo(passedMs, startVal, targetVal - startVal, durationMs));165166if (passedMs < durationMs) {167animationFrame = targetWindow.requestAnimationFrame(update);168} else {169curVal = targetVal;170}171172result.set(curVal, undefined);173}174175return result;176}177178function easeOutExpo(t: number, b: number, c: number, d: number): number {179return t === d ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;180}181182export function deepMerge<T extends {}>(source1: T, source2: Partial<T>): T {183const result = {} as any as T;184for (const key in source1) {185result[key] = source1[key];186}187for (const key in source2) {188const source2Value = source2[key];189if (typeof result[key] === 'object' && source2Value && typeof source2Value === 'object') {190result[key] = deepMerge<any>(result[key], source2Value);191} else {192result[key] = source2Value as any;193}194}195return result;196}197198export abstract class ViewZoneOverlayWidget extends Disposable {199constructor(200editor: ICodeEditor,201viewZone: PlaceholderViewZone,202htmlElement: HTMLElement,203) {204super();205206this._register(new ManagedOverlayWidget(editor, htmlElement));207this._register(applyStyle(htmlElement, {208height: viewZone.actualHeight,209top: viewZone.actualTop,210}));211}212}213214export interface IObservableViewZone extends IViewZone {215// Causes the view zone to relayout.216onChange?: IObservable<unknown>;217218// Tells a view zone its id.219setZoneId?(zoneId: string): void;220}221222export class PlaceholderViewZone implements IObservableViewZone {223public readonly domNode;224225private readonly _actualTop;226private readonly _actualHeight;227228public readonly actualTop: IObservable<number | undefined>;229public readonly actualHeight: IObservable<number | undefined>;230231public readonly showInHiddenAreas;232233public get afterLineNumber(): number { return this._afterLineNumber.get(); }234235public readonly onChange?: IObservable<unknown>;236237constructor(238private readonly _afterLineNumber: IObservable<number>,239public readonly heightInPx: number,240) {241this.domNode = document.createElement('div');242this._actualTop = observableValue<number | undefined>(this, undefined);243this._actualHeight = observableValue<number | undefined>(this, undefined);244this.actualTop = this._actualTop;245this.actualHeight = this._actualHeight;246this.showInHiddenAreas = true;247this.onChange = this._afterLineNumber;248this.onDomNodeTop = (top: number) => {249this._actualTop.set(top, undefined);250};251this.onComputedHeight = (height: number) => {252this._actualHeight.set(height, undefined);253};254}255256onDomNodeTop;257258onComputedHeight;259}260261262export class ManagedOverlayWidget implements IDisposable {263private static _counter = 0;264private readonly _overlayWidgetId = `managedOverlayWidget-${ManagedOverlayWidget._counter++}`;265266private readonly _overlayWidget: IOverlayWidget = {267getId: () => this._overlayWidgetId,268getDomNode: () => this._domElement,269getPosition: () => null270};271272constructor(273private readonly _editor: ICodeEditor,274private readonly _domElement: HTMLElement,275) {276this._editor.addOverlayWidget(this._overlayWidget);277}278279dispose(): void {280this._editor.removeOverlayWidget(this._overlayWidget);281}282}283284export interface CSSStyle {285height: number | string;286width: number | string;287top: number | string;288visibility: 'visible' | 'hidden' | 'collapse';289display: 'block' | 'inline' | 'inline-block' | 'flex' | 'none';290paddingLeft: number | string;291paddingRight: number | string;292}293294export function applyStyle(domNode: HTMLElement, style: Partial<{ [TKey in keyof CSSStyle]: CSSStyle[TKey] | IObservable<CSSStyle[TKey] | undefined> | undefined }>) {295return autorun(reader => {296/** @description applyStyle */297for (let [key, val] of Object.entries(style)) {298if (val && typeof val === 'object' && 'read' in val) {299val = val.read(reader) as any;300}301if (typeof val === 'number') {302val = `${val}px`;303}304key = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());305domNode.style[key as any] = val as any;306}307});308}309310export function applyViewZones(editor: ICodeEditor, viewZones: IObservable<IObservableViewZone[]>, setIsUpdating?: (isUpdatingViewZones: boolean) => void, zoneIds?: Set<string>): IDisposable {311const store = new DisposableStore();312const lastViewZoneIds: string[] = [];313314store.add(autorunWithStore((reader, store) => {315/** @description applyViewZones */316const curViewZones = viewZones.read(reader);317318const viewZonIdsPerViewZone = new Map<IObservableViewZone, string>();319const viewZoneIdPerOnChangeObservable = new Map<IObservable<unknown>, string>();320321// Add/remove view zones322if (setIsUpdating) { setIsUpdating(true); }323editor.changeViewZones(a => {324for (const id of lastViewZoneIds) { a.removeZone(id); zoneIds?.delete(id); }325lastViewZoneIds.length = 0;326327for (const z of curViewZones) {328const id = a.addZone(z);329if (z.setZoneId) {330z.setZoneId(id);331}332lastViewZoneIds.push(id);333zoneIds?.add(id);334viewZonIdsPerViewZone.set(z, id);335}336});337if (setIsUpdating) { setIsUpdating(false); }338339// Layout zone on change340store.add(autorunHandleChanges({341changeTracker: {342createChangeSummary() {343return { zoneIds: [] as string[] };344},345handleChange(context, changeSummary) {346const id = viewZoneIdPerOnChangeObservable.get(context.changedObservable);347if (id !== undefined) { changeSummary.zoneIds.push(id); }348return true;349},350}351}, (reader, changeSummary) => {352/** @description layoutZone on change */353for (const vz of curViewZones) {354if (vz.onChange) {355viewZoneIdPerOnChangeObservable.set(vz.onChange, viewZonIdsPerViewZone.get(vz)!);356vz.onChange.read(reader);357}358}359if (setIsUpdating) { setIsUpdating(true); }360editor.changeViewZones(a => { for (const id of changeSummary.zoneIds) { a.layoutZone(id); } });361if (setIsUpdating) { setIsUpdating(false); }362}));363}));364365store.add({366dispose() {367if (setIsUpdating) { setIsUpdating(true); }368editor.changeViewZones(a => { for (const id of lastViewZoneIds) { a.removeZone(id); } });369zoneIds?.clear();370if (setIsUpdating) { setIsUpdating(false); }371}372});373374return store;375}376377export class DisposableCancellationTokenSource extends CancellationTokenSource {378public override dispose() {379super.dispose(true);380}381}382383export function translatePosition(posInOriginal: Position, mappings: DetailedLineRangeMapping[]): Range {384const mapping = findLast(mappings, m => m.original.startLineNumber <= posInOriginal.lineNumber);385if (!mapping) {386// No changes before the position387return Range.fromPositions(posInOriginal);388}389390if (mapping.original.endLineNumberExclusive <= posInOriginal.lineNumber) {391const newLineNumber = posInOriginal.lineNumber - mapping.original.endLineNumberExclusive + mapping.modified.endLineNumberExclusive;392return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));393}394395if (!mapping.innerChanges) {396// Only for legacy algorithm397return Range.fromPositions(new Position(mapping.modified.startLineNumber, 1));398}399400const innerMapping = findLast(mapping.innerChanges, m => m.originalRange.getStartPosition().isBeforeOrEqual(posInOriginal));401if (!innerMapping) {402const newLineNumber = posInOriginal.lineNumber - mapping.original.startLineNumber + mapping.modified.startLineNumber;403return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));404}405406if (innerMapping.originalRange.containsPosition(posInOriginal)) {407return innerMapping.modifiedRange;408} else {409const l = lengthBetweenPositions(innerMapping.originalRange.getEndPosition(), posInOriginal);410return Range.fromPositions(l.addToPosition(innerMapping.modifiedRange.getEndPosition()));411}412}413414function lengthBetweenPositions(position1: Position, position2: Position): TextLength {415if (position1.lineNumber === position2.lineNumber) {416return new TextLength(0, position2.column - position1.column);417} else {418return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1);419}420}421422export function filterWithPrevious<T>(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] {423let prev: T | undefined;424return arr.filter(cur => {425const result = filter(cur, prev);426prev = cur;427return result;428});429}430431export interface IRefCounted extends IDisposable {432createNewRef(): this;433}434435export abstract class RefCounted<T> implements IDisposable, IReference<T> {436public static create<T extends IDisposable>(value: T, debugOwner: object | undefined = undefined): RefCounted<T> {437return new BaseRefCounted(value, value, debugOwner);438}439440public static createWithDisposable<T extends IDisposable>(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted<T> {441const store = new DisposableStore();442store.add(disposable);443store.add(value);444return new BaseRefCounted(value, store, debugOwner);445}446447public static createOfNonDisposable<T>(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted<T> {448return new BaseRefCounted(value, disposable, debugOwner);449}450451public abstract createNewRef(debugOwner?: object | undefined): RefCounted<T>;452453public abstract dispose(): void;454455public abstract get object(): T;456}457458class BaseRefCounted<T> extends RefCounted<T> {459private _refCount = 1;460private _isDisposed = false;461private readonly _owners: object[] = [];462463constructor(464public override readonly object: T,465private readonly _disposable: IDisposable,466private readonly _debugOwner: object | undefined,467) {468super();469470if (_debugOwner) {471this._addOwner(_debugOwner);472}473}474475private _addOwner(debugOwner: object | undefined) {476if (debugOwner) {477this._owners.push(debugOwner);478}479}480481public createNewRef(debugOwner?: object | undefined): RefCounted<T> {482this._refCount++;483if (debugOwner) {484this._addOwner(debugOwner);485}486return new ClonedRefCounted(this, debugOwner);487}488489public dispose(): void {490if (this._isDisposed) { return; }491this._isDisposed = true;492this._decreaseRefCount(this._debugOwner);493}494495public _decreaseRefCount(debugOwner?: object | undefined): void {496this._refCount--;497if (this._refCount === 0) {498this._disposable.dispose();499}500501if (debugOwner) {502const idx = this._owners.indexOf(debugOwner);503if (idx !== -1) {504this._owners.splice(idx, 1);505}506}507}508}509510class ClonedRefCounted<T> extends RefCounted<T> {511private _isDisposed = false;512constructor(513private readonly _base: BaseRefCounted<T>,514private readonly _debugOwner: object | undefined,515) {516super();517}518519public get object(): T { return this._base.object; }520521public createNewRef(debugOwner?: object | undefined): RefCounted<T> {522return this._base.createNewRef(debugOwner);523}524525public dispose(): void {526if (this._isDisposed) { return; }527this._isDisposed = true;528this._base._decreaseRefCount(this._debugOwner);529}530}531532533