cm-tarnation
Version:
An alternative parser for CodeMirror 6
179 lines • 6.75 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/. */
import { search } from "../util";
import { Chunk } from "./chunk";
/**
* A `ChunkBuffer` stores `Chunk` objects that then store the actual tokens
* emitted by tokenizing. The creation of `Chunk` objects is fully
* automatic, all the parser needs to do is push the tokens to the buffer.
*
* Storing chunks instead of tokens allows the buffer to more easily manage
* a large number of tokens, and more importantly, allows the parser to use
* chunks as checkpoints, enabling it to only tokenize what it needs to and
* reuse everything else.
*/
export class ChunkBuffer {
/** The actual array of chunks that the buffer manages. */
chunks = [];
/** @param chunks - The chunks to populate the buffer with. */
constructor(chunks) {
if (chunks)
this.chunks = chunks;
}
/** The last chunk in the buffer. */
get last() {
if (!this.chunks.length)
return null;
return this.chunks[this.chunks.length - 1];
}
/** The last chunk in the buffer. */
set last(chunk) {
if (!chunk)
return; // just satisfying typescript here
if (!this.chunks.length)
this.chunks.push(chunk);
this.chunks[this.chunks.length - 1] = chunk;
}
/** Ensures there is at least one chunk in the buffer. */
ensureLast(pos, state) {
if (!this.last)
this.last = new Chunk(pos, state);
}
/** Retrieves a `Chunk` from the buffer. */
get(index) {
return this.chunks[index] ?? null;
}
/**
* Adds tokens to the buffer, splitting them automatically into chunks.
* Returns true if a new chunk was created.
*
* @param state - The state to be cached.
* @param token - The token to add to the buffer.
*/
add(state, token) {
let pushed = false;
let current = this.chunks[this.chunks.length - 1];
if (!current) {
current = new Chunk(token[1], state);
this.chunks.push(current);
pushed = true;
}
if (token[3]) {
if (current.length !== 0) {
current = new Chunk(current.to, state);
this.chunks.push(current);
pushed = true;
}
current.pushOpen(...token[3]);
}
current.add(token[0], token[1], token[2]);
if (token[4]) {
current.pushClose(...token[4]);
current = new Chunk(current.to, state);
this.chunks.push(current);
pushed = true;
}
return pushed;
}
/**
* Splits the buffer into a left and right section. The left section
* takes the indexed chunk, which will have its tokens cleared.
*
* @param index - The chunk index to split on.
*/
split(index) {
if (!this.get(index))
throw new Error("Tried to split buffer on invalid index!");
let left;
let right;
if (this.chunks.length <= 1) {
left = new ChunkBuffer(this.chunks.slice(0));
right = new ChunkBuffer(); // empty
}
else {
left = new ChunkBuffer(this.chunks.slice(0, index + 1));
right = new ChunkBuffer(this.chunks.slice(index + 1));
}
if (left.last) {
left.last = new Chunk(left.last.from, left.last.state.clone());
}
return { left, right };
}
/**
* Offsets every chunk's position whose index is past or at the given index.
*
* @param index - The index the slide will start at.
* @param offset - The positional offset that is applied to the chunks.
* @param cutLeft - If true, every chunk previous to `index` will be
* removed from the buffer.
*/
slide(index, offset, cutLeft = false) {
if (!this.get(index))
throw new Error("Tried to slide buffer on invalid index!");
if (this.chunks.length === 0)
return this;
if (this.chunks.length === 1) {
this.last.offset(offset);
return this;
}
if (cutLeft)
this.chunks = this.chunks.slice(index);
for (let idx = cutLeft ? 0 : index; idx < this.chunks.length; idx++) {
this.chunks[idx].offset(offset);
}
return this;
}
/**
* Links another `ChunkBuffer` to the end of this buffer.
*
* @param right - The buffer to link.
* @param max - If given, the maximum size of the buffer (by document
* position) will be clamped to below this number.
*/
link(right, max) {
this.chunks = [...this.chunks, ...right.chunks];
if (max) {
for (let idx = 0; idx < this.chunks.length; idx++) {
if (this.chunks[idx].to > max) {
this.chunks = this.chunks.slice(0, idx);
break;
}
}
}
return this;
}
/** Binary search comparator function. */
searchCmp = ({ from: pos }, target) => pos === target || pos - target;
/**
* Searches for the closest chunk to the given position.
*
* @param pos - The position to find.
* @param side - The side to search on. -1 is left (before), 1 is right
* (after). 0 is the default, and it means either side.
* @param precise - If true, the search will require an exact hit. If the
* search misses, it will return `null` for both the token and index.
*/
search(pos, side = 0, precise = false) {
const result = search(this.chunks, pos, this.searchCmp, { precise });
// null result or null resulting index
if (!result || !this.chunks[result.index]) {
return { chunk: null, index: null };
}
let { index } = result;
let chunk = this.chunks[index];
// direct hit or we don't care about sidedness
if (chunk.from === pos || side === 0)
return { chunk, index };
// correct for sidedness
while (chunk && (side === 1 ? chunk.from < pos : chunk.from > pos)) {
index = side === 1 ? index + 1 : index - 1;
chunk = this.chunks[index];
}
// no valid chunks
if (!chunk)
return { chunk: null, index: null };
return { chunk, index };
}
}
//# sourceMappingURL=buffer.js.map