Path: blob/main/src/vs/editor/browser/widget/diffEditor/utils.ts
5230 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 {183// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any184const result = {} as any as T;185for (const key in source1) {186result[key] = source1[key];187}188for (const key in source2) {189const source2Value = source2[key];190if (typeof result[key] === 'object' && source2Value && typeof source2Value === 'object') {191// eslint-disable-next-line @typescript-eslint/no-explicit-any192result[key] = deepMerge<any>(result[key], source2Value);193} else {194// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any195result[key] = source2Value as any;196}197}198return result;199}200201export abstract class ViewZoneOverlayWidget extends Disposable {202constructor(203editor: ICodeEditor,204viewZone: PlaceholderViewZone,205htmlElement: HTMLElement,206) {207super();208209this._register(new ManagedOverlayWidget(editor, htmlElement));210this._register(applyStyle(htmlElement, {211height: viewZone.actualHeight,212top: viewZone.actualTop,213}));214}215}216217export interface IObservableViewZone extends IViewZone {218// Causes the view zone to relayout.219onChange?: IObservable<unknown>;220221// Tells a view zone its id.222setZoneId?(zoneId: string): void;223}224225export class PlaceholderViewZone implements IObservableViewZone {226public readonly domNode;227228private readonly _actualTop;229private readonly _actualHeight;230231public readonly actualTop: IObservable<number | undefined>;232public readonly actualHeight: IObservable<number | undefined>;233234public readonly showInHiddenAreas;235236public get afterLineNumber(): number { return this._afterLineNumber.get(); }237238public readonly onChange?: IObservable<unknown>;239240constructor(241private readonly _afterLineNumber: IObservable<number>,242public readonly heightInPx: number,243) {244this.domNode = document.createElement('div');245this._actualTop = observableValue<number | undefined>(this, undefined);246this._actualHeight = observableValue<number | undefined>(this, undefined);247this.actualTop = this._actualTop;248this.actualHeight = this._actualHeight;249this.showInHiddenAreas = true;250this.onChange = this._afterLineNumber;251this.onDomNodeTop = (top: number) => {252this._actualTop.set(top, undefined);253};254this.onComputedHeight = (height: number) => {255this._actualHeight.set(height, undefined);256};257}258259onDomNodeTop;260261onComputedHeight;262}263264265export class ManagedOverlayWidget implements IDisposable {266private static _counter = 0;267private readonly _overlayWidgetId = `managedOverlayWidget-${ManagedOverlayWidget._counter++}`;268269private readonly _overlayWidget: IOverlayWidget = {270getId: () => this._overlayWidgetId,271getDomNode: () => this._domElement,272getPosition: () => null273};274275constructor(276private readonly _editor: ICodeEditor,277private readonly _domElement: HTMLElement,278) {279this._editor.addOverlayWidget(this._overlayWidget);280}281282dispose(): void {283this._editor.removeOverlayWidget(this._overlayWidget);284}285}286287export interface CSSStyle {288height: number | string;289width: number | string;290top: number | string;291visibility: 'visible' | 'hidden' | 'collapse';292display: 'block' | 'inline' | 'inline-block' | 'flex' | 'none';293paddingLeft: number | string;294paddingRight: number | string;295}296297export function applyStyle(domNode: HTMLElement, style: Partial<{ [TKey in keyof CSSStyle]: CSSStyle[TKey] | IObservable<CSSStyle[TKey] | undefined> | undefined }>) {298return autorun(reader => {299/** @description applyStyle */300for (let [key, val] of Object.entries(style)) {301if (val && typeof val === 'object' && 'read' in val) {302// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any303val = val.read(reader) as any;304}305if (typeof val === 'number') {306val = `${val}px`;307}308key = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());309// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any310domNode.style[key as any] = val as any;311}312});313}314315export function applyViewZones(editor: ICodeEditor, viewZones: IObservable<IObservableViewZone[]>, setIsUpdating?: (isUpdatingViewZones: boolean) => void, zoneIds?: Set<string>): IDisposable {316const store = new DisposableStore();317const lastViewZoneIds: string[] = [];318319store.add(autorunWithStore((reader, store) => {320/** @description applyViewZones */321const curViewZones = viewZones.read(reader);322323const viewZonIdsPerViewZone = new Map<IObservableViewZone, string>();324const viewZoneIdPerOnChangeObservable = new Map<IObservable<unknown>, string>();325326// Add/remove view zones327if (setIsUpdating) { setIsUpdating(true); }328editor.changeViewZones(a => {329for (const id of lastViewZoneIds) { a.removeZone(id); zoneIds?.delete(id); }330lastViewZoneIds.length = 0;331332for (const z of curViewZones) {333const id = a.addZone(z);334if (z.setZoneId) {335z.setZoneId(id);336}337lastViewZoneIds.push(id);338zoneIds?.add(id);339viewZonIdsPerViewZone.set(z, id);340}341});342if (setIsUpdating) { setIsUpdating(false); }343344// Layout zone on change345store.add(autorunHandleChanges({346changeTracker: {347createChangeSummary() {348return { zoneIds: [] as string[] };349},350handleChange(context, changeSummary) {351const id = viewZoneIdPerOnChangeObservable.get(context.changedObservable);352if (id !== undefined) { changeSummary.zoneIds.push(id); }353return true;354},355}356}, (reader, changeSummary) => {357/** @description layoutZone on change */358for (const vz of curViewZones) {359if (vz.onChange) {360viewZoneIdPerOnChangeObservable.set(vz.onChange, viewZonIdsPerViewZone.get(vz)!);361vz.onChange.read(reader);362}363}364if (setIsUpdating) { setIsUpdating(true); }365editor.changeViewZones(a => { for (const id of changeSummary.zoneIds) { a.layoutZone(id); } });366if (setIsUpdating) { setIsUpdating(false); }367}));368}));369370store.add({371dispose() {372if (setIsUpdating) { setIsUpdating(true); }373editor.changeViewZones(a => { for (const id of lastViewZoneIds) { a.removeZone(id); } });374zoneIds?.clear();375if (setIsUpdating) { setIsUpdating(false); }376}377});378379return store;380}381382export class DisposableCancellationTokenSource extends CancellationTokenSource {383public override dispose() {384super.dispose(true);385}386}387388export function translatePosition(posInOriginal: Position, mappings: DetailedLineRangeMapping[]): Range {389const mapping = findLast(mappings, m => m.original.startLineNumber <= posInOriginal.lineNumber);390if (!mapping) {391// No changes before the position392return Range.fromPositions(posInOriginal);393}394395if (mapping.original.endLineNumberExclusive <= posInOriginal.lineNumber) {396const newLineNumber = posInOriginal.lineNumber - mapping.original.endLineNumberExclusive + mapping.modified.endLineNumberExclusive;397return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));398}399400if (!mapping.innerChanges) {401// Only for legacy algorithm402return Range.fromPositions(new Position(mapping.modified.startLineNumber, 1));403}404405const innerMapping = findLast(mapping.innerChanges, m => m.originalRange.getStartPosition().isBeforeOrEqual(posInOriginal));406if (!innerMapping) {407const newLineNumber = posInOriginal.lineNumber - mapping.original.startLineNumber + mapping.modified.startLineNumber;408return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));409}410411if (innerMapping.originalRange.containsPosition(posInOriginal)) {412return innerMapping.modifiedRange;413} else {414const l = lengthBetweenPositions(innerMapping.originalRange.getEndPosition(), posInOriginal);415return Range.fromPositions(l.addToPosition(innerMapping.modifiedRange.getEndPosition()));416}417}418419function lengthBetweenPositions(position1: Position, position2: Position): TextLength {420if (position1.lineNumber === position2.lineNumber) {421return new TextLength(0, position2.column - position1.column);422} else {423return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1);424}425}426427export function filterWithPrevious<T>(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] {428let prev: T | undefined;429return arr.filter(cur => {430const result = filter(cur, prev);431prev = cur;432return result;433});434}435436export interface IRefCounted extends IDisposable {437createNewRef(): this;438}439440export abstract class RefCounted<T> implements IDisposable, IReference<T> {441public static create<T extends IDisposable>(value: T, debugOwner: object | undefined = undefined): RefCounted<T> {442return new BaseRefCounted(value, value, debugOwner);443}444445public static createWithDisposable<T extends IDisposable>(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted<T> {446const store = new DisposableStore();447store.add(disposable);448store.add(value);449return new BaseRefCounted(value, store, debugOwner);450}451452public static createOfNonDisposable<T>(value: T, disposable: IDisposable, debugOwner: object | undefined = undefined): RefCounted<T> {453return new BaseRefCounted(value, disposable, debugOwner);454}455456public abstract createNewRef(debugOwner?: object | undefined): RefCounted<T>;457458public abstract dispose(): void;459460public abstract get object(): T;461}462463class BaseRefCounted<T> extends RefCounted<T> {464private _refCount = 1;465private _isDisposed = false;466private readonly _owners: object[] = [];467468constructor(469public override readonly object: T,470private readonly _disposable: IDisposable,471private readonly _debugOwner: object | undefined,472) {473super();474475if (_debugOwner) {476this._addOwner(_debugOwner);477}478}479480private _addOwner(debugOwner: object | undefined) {481if (debugOwner) {482this._owners.push(debugOwner);483}484}485486public createNewRef(debugOwner?: object | undefined): RefCounted<T> {487this._refCount++;488if (debugOwner) {489this._addOwner(debugOwner);490}491return new ClonedRefCounted(this, debugOwner);492}493494public dispose(): void {495if (this._isDisposed) { return; }496this._isDisposed = true;497this._decreaseRefCount(this._debugOwner);498}499500public _decreaseRefCount(debugOwner?: object | undefined): void {501this._refCount--;502if (this._refCount === 0) {503this._disposable.dispose();504}505506if (debugOwner) {507const idx = this._owners.indexOf(debugOwner);508if (idx !== -1) {509this._owners.splice(idx, 1);510}511}512}513}514515class ClonedRefCounted<T> extends RefCounted<T> {516private _isDisposed = false;517constructor(518private readonly _base: BaseRefCounted<T>,519private readonly _debugOwner: object | undefined,520) {521super();522}523524public get object(): T { return this._base.object; }525526public createNewRef(debugOwner?: object | undefined): RefCounted<T> {527return this._base.createNewRef(debugOwner);528}529530public dispose(): void {531if (this._isDisposed) { return; }532this._isDisposed = true;533this._base._decreaseRefCount(this._debugOwner);534}535}536537538