cm-tarnation
Version:
An alternative parser for CodeMirror 6
211 lines • 8.5 kB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// disabled as it doesn't seem to be needed for performance
const LIMIT_TO_VIEWPORT = false;
/**
* The region of a document that should be parsed, along with other
* information such as what the edited range of the document was.
*/
export class ParseRegion {
/**
* @param input - The input to get the parse region for.
* @param ranges - The ranges of the document that should be parsed.
* @param fragments - Fragments that are used to compute the edited range.
*/
constructor(input, ranges, fragments, viewport) {
this.input = input;
this.from = ranges[0].from;
this.to = Math.min(input.length, ranges[ranges.length - 1].to);
this.original = { from: this.from, to: this.to, length: this.length };
this.ranges = ranges;
// get the edited range of the document,
// spanning from the start of the first edit to the end of the last edit
if (fragments?.length) {
let from, to, offset;
if (fragments.length === 1) {
const fragment = fragments[0];
// special case that seems to happen when scrolling,
// the fragment is the entire parsed range
if (fragment.offset === 0 && !fragment.openStart && fragment.openEnd) {
from = input.length;
to = input.length;
offset = 0;
}
else {
from = fragment.openStart ? this.from : fragment.to;
to = fragment.openStart ? fragment.from : this.to;
offset = -fragment.offset;
}
}
else {
const reversed = [...fragments].reverse();
const first = reversed.find(f => !f.openStart && f.openEnd) || fragments[0];
const last = fragments.find(f => f.openStart && !f.openEnd) || reversed[0];
from = first.openStart && first.openEnd ? first.from : first.to;
to = last.openStart && last.openEnd ? last.to : last.from;
offset = -last.offset;
// not sure why this is needed, something I don't understand about fragments
// usually if this is the case the parse was interrupted, and is being continued
if (from > to) {
from = this.to;
to = this.to;
offset = 0;
}
}
this.edit = { from, to, offset };
}
if (viewport) {
this.viewport = viewport;
if (LIMIT_TO_VIEWPORT) {
// we're gonna try to only parse just a bit past the viweport
// basically doubles the height of the viewport
// this adds a bit of a buffer between the actual end and the end of parsing
// otherwise if you scrolled too fast you'd see unparsed sections easily
const end = viewport.to + (viewport.to - viewport.from);
if (viewport.from < this.to && this.to > end)
this.to = end;
}
}
}
/** The length of the region. */
get length() {
return this.to - this.from;
}
/** True if we don't need to care about range handling. */
get contiguous() {
return this.ranges.length === 1;
}
/**
* Compensates for an adjustment to a position. That is, given the range
* `pos` is inside of, what position should adding `addition` return?
* This is for skipping past the gaps inbetween ranges.
*
* @param pos - The position to start from.
* @param addition - The amount to add to `pos`.
*/
compensate(pos, addition) {
const desired = pos + addition;
if (this.ranges.length === 1)
return desired;
const range = this.posRange(pos);
if (!range)
return desired;
// forwards compensation
if (desired > range.to) {
const next = this.posRange(pos, 1);
if (!next)
return desired;
const nextDesired = next.from + (desired - range.to) - 1;
// recursively compensate, if needed
if (nextDesired > next.to) {
return this.compensate(next.to, nextDesired - next.to);
}
return nextDesired;
}
// backwards compensation
else if (desired < range.from) {
const prev = this.posRange(pos, -1);
if (!prev)
return desired;
const prevDesired = prev.to + (desired - range.from) + 1;
// recursively compensate, if needed
if (prevDesired < prev.from) {
return this.compensate(prev.from, prevDesired - prev.from);
}
return prevDesired;
}
// no compensation needed
return desired;
}
/**
* Clamps a `to` value for a range to the end of the parse range that
* `from` is inside of.
*
* @param from - The `from` position, for which the `to` position will be
* clamped relative to.
* @param to - The `to` position, which will be clamped to the end of the
* range that `from` is inside of.
*/
clamp(from, to) {
const range = this.posRange(from);
if (!range)
return to;
return range.to;
}
/**
* Gets what range the given position is inside of. Returns `null` if the
* position can't be found inside of any range.
*
* @param pos - The position to get the range for.
* @param side - The side of the range to get. -1 returns the range
* previous, 1 returns the range after. Defaults to 0.
*/
posRange(pos, side = 0) {
if (this.ranges.length === 1)
return this.ranges[0];
for (let i = 0; i < this.ranges.length; i++) {
const range = this.ranges[i];
if (pos >= range.from && pos <= range.to) {
let final;
// prettier-ignore
switch (side) {
case -1:
final = this.ranges[i - 1];
break;
case 0:
final = range;
break;
case 1:
final = this.ranges[i + 1];
break;
}
return final ?? null;
}
}
return null;
}
/**
* Gets a substring of the current input, accounting for range handling
* automatically.
*
* @param pos - The position to start at.
* @param min - The minimum length of the substring.
* @param max - The maximum position of the end of the substring.
*/
read(pos, min, max) {
let str = "";
while (str.length <= min) {
str += this.input.chunk(pos + str.length);
const relative = pos + str.length;
if (relative >= max) {
const diff = relative - max;
if (diff)
str = str.slice(0, -diff);
break;
}
if (this.ranges.length !== 1) {
const actual = this.compensate(pos, str.length);
// end of input
if (actual >= max) {
const diff = actual - max;
if (diff)
str = str.slice(0, -diff);
break;
}
const clamped = this.clamp(pos, relative);
if (relative >= clamped) {
const diff = relative - clamped;
if (diff)
str = str.slice(0, -diff);
const next = this.posRange(clamped, 1);
if (!next)
break;
pos = next.from;
}
}
}
return str;
}
}
//# sourceMappingURL=region.js.map