monaco-editor
Version:
A browser based code editor
414 lines (413 loc) • 14.6 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { findLast } from '../../../../base/common/arraysFind.js';
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
import { autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableValue, transaction } from '../../../../base/common/observable.js';
import { ElementSizeObserver } from '../../config/elementSizeObserver.js';
import { Position } from '../../../common/core/position.js';
import { Range } from '../../../common/core/range.js';
import { TextLength } from '../../../common/core/textLength.js';
export function joinCombine(arr1, arr2, keySelector, combine) {
if (arr1.length === 0) {
return arr2;
}
if (arr2.length === 0) {
return arr1;
}
const result = [];
let i = 0;
let j = 0;
while (i < arr1.length && j < arr2.length) {
const val1 = arr1[i];
const val2 = arr2[j];
const key1 = keySelector(val1);
const key2 = keySelector(val2);
if (key1 < key2) {
result.push(val1);
i++;
}
else if (key1 > key2) {
result.push(val2);
j++;
}
else {
result.push(combine(val1, val2));
i++;
j++;
}
}
while (i < arr1.length) {
result.push(arr1[i]);
i++;
}
while (j < arr2.length) {
result.push(arr2[j]);
j++;
}
return result;
}
// TODO make utility
export function applyObservableDecorations(editor, decorations) {
const d = new DisposableStore();
const decorationsCollection = editor.createDecorationsCollection();
d.add(autorunOpts({ debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => {
const d = decorations.read(reader);
decorationsCollection.set(d);
}));
d.add({
dispose: () => {
decorationsCollection.clear();
}
});
return d;
}
export function appendRemoveOnDispose(parent, child) {
parent.appendChild(child);
return toDisposable(() => {
child.remove();
});
}
export function prependRemoveOnDispose(parent, child) {
parent.prepend(child);
return toDisposable(() => {
child.remove();
});
}
export class ObservableElementSizeObserver extends Disposable {
get width() { return this._width; }
get height() { return this._height; }
get automaticLayout() { return this._automaticLayout; }
constructor(element, dimension) {
super();
this._automaticLayout = false;
this.elementSizeObserver = this._register(new ElementSizeObserver(element, dimension));
this._width = observableValue(this, this.elementSizeObserver.getWidth());
this._height = observableValue(this, this.elementSizeObserver.getHeight());
this._register(this.elementSizeObserver.onDidChange(e => transaction(tx => {
/** @description Set width/height from elementSizeObserver */
this._width.set(this.elementSizeObserver.getWidth(), tx);
this._height.set(this.elementSizeObserver.getHeight(), tx);
})));
}
observe(dimension) {
this.elementSizeObserver.observe(dimension);
}
setAutomaticLayout(automaticLayout) {
this._automaticLayout = automaticLayout;
if (automaticLayout) {
this.elementSizeObserver.startObserving();
}
else {
this.elementSizeObserver.stopObserving();
}
}
}
export function animatedObservable(targetWindow, base, store) {
let targetVal = base.get();
let startVal = targetVal;
let curVal = targetVal;
const result = observableValue('animatedValue', targetVal);
let animationStartMs = -1;
const durationMs = 300;
let animationFrame = undefined;
store.add(autorunHandleChanges({
createEmptyChangeSummary: () => ({ animate: false }),
handleChange: (ctx, s) => {
if (ctx.didChange(base)) {
s.animate = s.animate || ctx.change;
}
return true;
}
}, (reader, s) => {
/** @description update value */
if (animationFrame !== undefined) {
targetWindow.cancelAnimationFrame(animationFrame);
animationFrame = undefined;
}
startVal = curVal;
targetVal = base.read(reader);
animationStartMs = Date.now() - (s.animate ? 0 : durationMs);
update();
}));
function update() {
const passedMs = Date.now() - animationStartMs;
curVal = Math.floor(easeOutExpo(passedMs, startVal, targetVal - startVal, durationMs));
if (passedMs < durationMs) {
animationFrame = targetWindow.requestAnimationFrame(update);
}
else {
curVal = targetVal;
}
result.set(curVal, undefined);
}
return result;
}
function easeOutExpo(t, b, c, d) {
return t === d ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
}
export class ViewZoneOverlayWidget extends Disposable {
constructor(editor, viewZone, htmlElement) {
super();
this._register(new ManagedOverlayWidget(editor, htmlElement));
this._register(applyStyle(htmlElement, {
height: viewZone.actualHeight,
top: viewZone.actualTop,
}));
}
}
export class PlaceholderViewZone {
get afterLineNumber() { return this._afterLineNumber.get(); }
constructor(_afterLineNumber, heightInPx) {
this._afterLineNumber = _afterLineNumber;
this.heightInPx = heightInPx;
this.domNode = document.createElement('div');
this._actualTop = observableValue(this, undefined);
this._actualHeight = observableValue(this, undefined);
this.actualTop = this._actualTop;
this.actualHeight = this._actualHeight;
this.showInHiddenAreas = true;
this.onChange = this._afterLineNumber;
this.onDomNodeTop = (top) => {
this._actualTop.set(top, undefined);
};
this.onComputedHeight = (height) => {
this._actualHeight.set(height, undefined);
};
}
}
export class ManagedOverlayWidget {
static { this._counter = 0; }
constructor(_editor, _domElement) {
this._editor = _editor;
this._domElement = _domElement;
this._overlayWidgetId = `managedOverlayWidget-${ManagedOverlayWidget._counter++}`;
this._overlayWidget = {
getId: () => this._overlayWidgetId,
getDomNode: () => this._domElement,
getPosition: () => null
};
this._editor.addOverlayWidget(this._overlayWidget);
}
dispose() {
this._editor.removeOverlayWidget(this._overlayWidget);
}
}
export function applyStyle(domNode, style) {
return autorun(reader => {
/** @description applyStyle */
for (let [key, val] of Object.entries(style)) {
if (val && typeof val === 'object' && 'read' in val) {
val = val.read(reader);
}
if (typeof val === 'number') {
val = `${val}px`;
}
key = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
domNode.style[key] = val;
}
});
}
export function applyViewZones(editor, viewZones, setIsUpdating, zoneIds) {
const store = new DisposableStore();
const lastViewZoneIds = [];
store.add(autorunWithStore((reader, store) => {
/** @description applyViewZones */
const curViewZones = viewZones.read(reader);
const viewZonIdsPerViewZone = new Map();
const viewZoneIdPerOnChangeObservable = new Map();
// Add/remove view zones
if (setIsUpdating) {
setIsUpdating(true);
}
editor.changeViewZones(a => {
for (const id of lastViewZoneIds) {
a.removeZone(id);
zoneIds?.delete(id);
}
lastViewZoneIds.length = 0;
for (const z of curViewZones) {
const id = a.addZone(z);
if (z.setZoneId) {
z.setZoneId(id);
}
lastViewZoneIds.push(id);
zoneIds?.add(id);
viewZonIdsPerViewZone.set(z, id);
}
});
if (setIsUpdating) {
setIsUpdating(false);
}
// Layout zone on change
store.add(autorunHandleChanges({
createEmptyChangeSummary() {
return { zoneIds: [] };
},
handleChange(context, changeSummary) {
const id = viewZoneIdPerOnChangeObservable.get(context.changedObservable);
if (id !== undefined) {
changeSummary.zoneIds.push(id);
}
return true;
},
}, (reader, changeSummary) => {
/** @description layoutZone on change */
for (const vz of curViewZones) {
if (vz.onChange) {
viewZoneIdPerOnChangeObservable.set(vz.onChange, viewZonIdsPerViewZone.get(vz));
vz.onChange.read(reader);
}
}
if (setIsUpdating) {
setIsUpdating(true);
}
editor.changeViewZones(a => { for (const id of changeSummary.zoneIds) {
a.layoutZone(id);
} });
if (setIsUpdating) {
setIsUpdating(false);
}
}));
}));
store.add({
dispose() {
if (setIsUpdating) {
setIsUpdating(true);
}
editor.changeViewZones(a => { for (const id of lastViewZoneIds) {
a.removeZone(id);
} });
zoneIds?.clear();
if (setIsUpdating) {
setIsUpdating(false);
}
}
});
return store;
}
export class DisposableCancellationTokenSource extends CancellationTokenSource {
dispose() {
super.dispose(true);
}
}
export function translatePosition(posInOriginal, mappings) {
const mapping = findLast(mappings, m => m.original.startLineNumber <= posInOriginal.lineNumber);
if (!mapping) {
// No changes before the position
return Range.fromPositions(posInOriginal);
}
if (mapping.original.endLineNumberExclusive <= posInOriginal.lineNumber) {
const newLineNumber = posInOriginal.lineNumber - mapping.original.endLineNumberExclusive + mapping.modified.endLineNumberExclusive;
return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));
}
if (!mapping.innerChanges) {
// Only for legacy algorithm
return Range.fromPositions(new Position(mapping.modified.startLineNumber, 1));
}
const innerMapping = findLast(mapping.innerChanges, m => m.originalRange.getStartPosition().isBeforeOrEqual(posInOriginal));
if (!innerMapping) {
const newLineNumber = posInOriginal.lineNumber - mapping.original.startLineNumber + mapping.modified.startLineNumber;
return Range.fromPositions(new Position(newLineNumber, posInOriginal.column));
}
if (innerMapping.originalRange.containsPosition(posInOriginal)) {
return innerMapping.modifiedRange;
}
else {
const l = lengthBetweenPositions(innerMapping.originalRange.getEndPosition(), posInOriginal);
return Range.fromPositions(l.addToPosition(innerMapping.modifiedRange.getEndPosition()));
}
}
function lengthBetweenPositions(position1, position2) {
if (position1.lineNumber === position2.lineNumber) {
return new TextLength(0, position2.column - position1.column);
}
else {
return new TextLength(position2.lineNumber - position1.lineNumber, position2.column - 1);
}
}
export function filterWithPrevious(arr, filter) {
let prev;
return arr.filter(cur => {
const result = filter(cur, prev);
prev = cur;
return result;
});
}
export class RefCounted {
static create(value, debugOwner = undefined) {
return new BaseRefCounted(value, value, debugOwner);
}
static createWithDisposable(value, disposable, debugOwner = undefined) {
const store = new DisposableStore();
store.add(disposable);
store.add(value);
return new BaseRefCounted(value, store, debugOwner);
}
}
class BaseRefCounted extends RefCounted {
constructor(object, _disposable, _debugOwner) {
super();
this.object = object;
this._disposable = _disposable;
this._debugOwner = _debugOwner;
this._refCount = 1;
this._isDisposed = false;
this._owners = [];
if (_debugOwner) {
this._addOwner(_debugOwner);
}
}
_addOwner(debugOwner) {
if (debugOwner) {
this._owners.push(debugOwner);
}
}
createNewRef(debugOwner) {
this._refCount++;
if (debugOwner) {
this._addOwner(debugOwner);
}
return new ClonedRefCounted(this, debugOwner);
}
dispose() {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this._decreaseRefCount(this._debugOwner);
}
_decreaseRefCount(debugOwner) {
this._refCount--;
if (this._refCount === 0) {
this._disposable.dispose();
}
if (debugOwner) {
const idx = this._owners.indexOf(debugOwner);
if (idx !== -1) {
this._owners.splice(idx, 1);
}
}
}
}
class ClonedRefCounted extends RefCounted {
constructor(_base, _debugOwner) {
super();
this._base = _base;
this._debugOwner = _debugOwner;
this._isDisposed = false;
}
get object() { return this._base.object; }
createNewRef(debugOwner) {
return this._base.createNewRef(debugOwner);
}
dispose() {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this._base._decreaseRefCount(this._debugOwner);
}
}