UNPKG

monaco-editor-core

Version:

A browser based code editor

305 lines (304 loc) • 12.8 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from '../../../base/common/errors.js'; import { OffsetRange } from './offsetRange.js'; import { Range } from './range.js'; import { findFirstIdxMonotonousOrArrLen, findLastIdxMonotonous, findLastMonotonous } from '../../../base/common/arraysFind.js'; /** * A range of lines (1-based). */ export class LineRange { static fromRangeInclusive(range) { return new LineRange(range.startLineNumber, range.endLineNumber + 1); } /** * @param lineRanges An array of sorted line ranges. */ static joinMany(lineRanges) { if (lineRanges.length === 0) { return []; } let result = new LineRangeSet(lineRanges[0].slice()); for (let i = 1; i < lineRanges.length; i++) { result = result.getUnion(new LineRangeSet(lineRanges[i].slice())); } return result.ranges; } static join(lineRanges) { if (lineRanges.length === 0) { throw new BugIndicatingError('lineRanges cannot be empty'); } let startLineNumber = lineRanges[0].startLineNumber; let endLineNumberExclusive = lineRanges[0].endLineNumberExclusive; for (let i = 1; i < lineRanges.length; i++) { startLineNumber = Math.min(startLineNumber, lineRanges[i].startLineNumber); endLineNumberExclusive = Math.max(endLineNumberExclusive, lineRanges[i].endLineNumberExclusive); } return new LineRange(startLineNumber, endLineNumberExclusive); } static ofLength(startLineNumber, length) { return new LineRange(startLineNumber, startLineNumber + length); } /** * @internal */ static deserialize(lineRange) { return new LineRange(lineRange[0], lineRange[1]); } constructor(startLineNumber, endLineNumberExclusive) { if (startLineNumber > endLineNumberExclusive) { throw new BugIndicatingError(`startLineNumber ${startLineNumber} cannot be after endLineNumberExclusive ${endLineNumberExclusive}`); } this.startLineNumber = startLineNumber; this.endLineNumberExclusive = endLineNumberExclusive; } /** * Indicates if this line range contains the given line number. */ contains(lineNumber) { return this.startLineNumber <= lineNumber && lineNumber < this.endLineNumberExclusive; } /** * Indicates if this line range is empty. */ get isEmpty() { return this.startLineNumber === this.endLineNumberExclusive; } /** * Moves this line range by the given offset of line numbers. */ delta(offset) { return new LineRange(this.startLineNumber + offset, this.endLineNumberExclusive + offset); } deltaLength(offset) { return new LineRange(this.startLineNumber, this.endLineNumberExclusive + offset); } /** * The number of lines this line range spans. */ get length() { return this.endLineNumberExclusive - this.startLineNumber; } /** * Creates a line range that combines this and the given line range. */ join(other) { return new LineRange(Math.min(this.startLineNumber, other.startLineNumber), Math.max(this.endLineNumberExclusive, other.endLineNumberExclusive)); } toString() { return `[${this.startLineNumber},${this.endLineNumberExclusive})`; } /** * The resulting range is empty if the ranges do not intersect, but touch. * If the ranges don't even touch, the result is undefined. */ intersect(other) { const startLineNumber = Math.max(this.startLineNumber, other.startLineNumber); const endLineNumberExclusive = Math.min(this.endLineNumberExclusive, other.endLineNumberExclusive); if (startLineNumber <= endLineNumberExclusive) { return new LineRange(startLineNumber, endLineNumberExclusive); } return undefined; } intersectsStrict(other) { return this.startLineNumber < other.endLineNumberExclusive && other.startLineNumber < this.endLineNumberExclusive; } overlapOrTouch(other) { return this.startLineNumber <= other.endLineNumberExclusive && other.startLineNumber <= this.endLineNumberExclusive; } equals(b) { return this.startLineNumber === b.startLineNumber && this.endLineNumberExclusive === b.endLineNumberExclusive; } toInclusiveRange() { if (this.isEmpty) { return null; } return new Range(this.startLineNumber, 1, this.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER); } /** * @deprecated Using this function is discouraged because it might lead to bugs: The end position is not guaranteed to be a valid position! */ toExclusiveRange() { return new Range(this.startLineNumber, 1, this.endLineNumberExclusive, 1); } mapToLineArray(f) { const result = []; for (let lineNumber = this.startLineNumber; lineNumber < this.endLineNumberExclusive; lineNumber++) { result.push(f(lineNumber)); } return result; } forEach(f) { for (let lineNumber = this.startLineNumber; lineNumber < this.endLineNumberExclusive; lineNumber++) { f(lineNumber); } } /** * @internal */ serialize() { return [this.startLineNumber, this.endLineNumberExclusive]; } includes(lineNumber) { return this.startLineNumber <= lineNumber && lineNumber < this.endLineNumberExclusive; } /** * Converts this 1-based line range to a 0-based offset range (subtracts 1!). * @internal */ toOffsetRange() { return new OffsetRange(this.startLineNumber - 1, this.endLineNumberExclusive - 1); } } export class LineRangeSet { constructor( /** * Sorted by start line number. * No two line ranges are touching or intersecting. */ _normalizedRanges = []) { this._normalizedRanges = _normalizedRanges; } get ranges() { return this._normalizedRanges; } addRange(range) { if (range.length === 0) { return; } // Idea: Find joinRange such that: // replaceRange = _normalizedRanges.replaceRange(joinRange, range.joinAll(joinRange.map(idx => this._normalizedRanges[idx]))) // idx of first element that touches range or that is after range const joinRangeStartIdx = findFirstIdxMonotonousOrArrLen(this._normalizedRanges, r => r.endLineNumberExclusive >= range.startLineNumber); // idx of element after { last element that touches range or that is before range } const joinRangeEndIdxExclusive = findLastIdxMonotonous(this._normalizedRanges, r => r.startLineNumber <= range.endLineNumberExclusive) + 1; if (joinRangeStartIdx === joinRangeEndIdxExclusive) { // If there is no element that touches range, then joinRangeStartIdx === joinRangeEndIdxExclusive and that value is the index of the element after range this._normalizedRanges.splice(joinRangeStartIdx, 0, range); } else if (joinRangeStartIdx === joinRangeEndIdxExclusive - 1) { // Else, there is an element that touches range and in this case it is both the first and last element. Thus we can replace it const joinRange = this._normalizedRanges[joinRangeStartIdx]; this._normalizedRanges[joinRangeStartIdx] = joinRange.join(range); } else { // First and last element are different - we need to replace the entire range const joinRange = this._normalizedRanges[joinRangeStartIdx].join(this._normalizedRanges[joinRangeEndIdxExclusive - 1]).join(range); this._normalizedRanges.splice(joinRangeStartIdx, joinRangeEndIdxExclusive - joinRangeStartIdx, joinRange); } } contains(lineNumber) { const rangeThatStartsBeforeEnd = findLastMonotonous(this._normalizedRanges, r => r.startLineNumber <= lineNumber); return !!rangeThatStartsBeforeEnd && rangeThatStartsBeforeEnd.endLineNumberExclusive > lineNumber; } intersects(range) { const rangeThatStartsBeforeEnd = findLastMonotonous(this._normalizedRanges, r => r.startLineNumber < range.endLineNumberExclusive); return !!rangeThatStartsBeforeEnd && rangeThatStartsBeforeEnd.endLineNumberExclusive > range.startLineNumber; } getUnion(other) { if (this._normalizedRanges.length === 0) { return other; } if (other._normalizedRanges.length === 0) { return this; } const result = []; let i1 = 0; let i2 = 0; let current = null; while (i1 < this._normalizedRanges.length || i2 < other._normalizedRanges.length) { let next = null; if (i1 < this._normalizedRanges.length && i2 < other._normalizedRanges.length) { const lineRange1 = this._normalizedRanges[i1]; const lineRange2 = other._normalizedRanges[i2]; if (lineRange1.startLineNumber < lineRange2.startLineNumber) { next = lineRange1; i1++; } else { next = lineRange2; i2++; } } else if (i1 < this._normalizedRanges.length) { next = this._normalizedRanges[i1]; i1++; } else { next = other._normalizedRanges[i2]; i2++; } if (current === null) { current = next; } else { if (current.endLineNumberExclusive >= next.startLineNumber) { // merge current = new LineRange(current.startLineNumber, Math.max(current.endLineNumberExclusive, next.endLineNumberExclusive)); } else { // push result.push(current); current = next; } } } if (current !== null) { result.push(current); } return new LineRangeSet(result); } /** * Subtracts all ranges in this set from `range` and returns the result. */ subtractFrom(range) { // idx of first element that touches range or that is after range const joinRangeStartIdx = findFirstIdxMonotonousOrArrLen(this._normalizedRanges, r => r.endLineNumberExclusive >= range.startLineNumber); // idx of element after { last element that touches range or that is before range } const joinRangeEndIdxExclusive = findLastIdxMonotonous(this._normalizedRanges, r => r.startLineNumber <= range.endLineNumberExclusive) + 1; if (joinRangeStartIdx === joinRangeEndIdxExclusive) { return new LineRangeSet([range]); } const result = []; let startLineNumber = range.startLineNumber; for (let i = joinRangeStartIdx; i < joinRangeEndIdxExclusive; i++) { const r = this._normalizedRanges[i]; if (r.startLineNumber > startLineNumber) { result.push(new LineRange(startLineNumber, r.startLineNumber)); } startLineNumber = r.endLineNumberExclusive; } if (startLineNumber < range.endLineNumberExclusive) { result.push(new LineRange(startLineNumber, range.endLineNumberExclusive)); } return new LineRangeSet(result); } toString() { return this._normalizedRanges.map(r => r.toString()).join(', '); } getIntersection(other) { const result = []; let i1 = 0; let i2 = 0; while (i1 < this._normalizedRanges.length && i2 < other._normalizedRanges.length) { const r1 = this._normalizedRanges[i1]; const r2 = other._normalizedRanges[i2]; const i = r1.intersect(r2); if (i && !i.isEmpty) { result.push(i); } if (r1.endLineNumberExclusive < r2.endLineNumberExclusive) { i1++; } else { i2++; } } return new LineRangeSet(result); } getWithDelta(value) { return new LineRangeSet(this._normalizedRanges.map(r => r.delta(value))); } }