UNPKG

cm-tarnation

Version:

An alternative parser for CodeMirror 6

203 lines (169 loc) 6.36 kB
/* 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 type { GrammarState } from "../grammar/state" import type { GrammarToken } from "../types" 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: Chunk[] = [] /** @param chunks - The chunks to populate the buffer with. */ constructor(chunks?: Chunk[]) { 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: Chunk | null) { 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: number, state: GrammarState) { if (!this.last) this.last = new Chunk(pos, state) } /** Retrieves a `Chunk` from the buffer. */ get(index: number): Chunk | null { 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: GrammarState, token: GrammarToken) { 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: number) { if (!this.get(index)) throw new Error("Tried to split buffer on invalid index!") let left: ChunkBuffer let right: ChunkBuffer 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: number, offset: number, 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: ChunkBuffer, max?: number) { 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. */ private searchCmp = ({ from: pos }: Chunk, target: number) => 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: number, side: 1 | 0 | -1 = 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 } } }