Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127// Derived from Node.js v24.16.0 (MIT; Copyright Node.js contributors, and Joyent, Inc. and// other Node contributors). See LICENSE-node.// The in-memory half of Node v24's lib/internal/repl/history.js, which readline's// Interface keeps its history in. Persisting history to a file is the REPL's business// and is not here.import { invalidArgType } from "../errors";import { reverseString } from "./utils";
const kHistorySize = 30;
export interface HistoryContext { line: string; emit(event: "history", history: string[]): unknown;}
export interface HistoryOptions { size?: number; history?: string[]; removeHistoryDuplicates?: boolean;}
/** Multiline entries are stored reversed, newest line first. */const normalizeLineEndings = (line: string, from: string, to: string): string => reverseString(line, from, to);
export class ReplHistory { #context: HistoryContext; #removeHistoryDuplicates: boolean; #size: number; #history: string[]; #index = -1;
constructor(context: HistoryContext, options: HistoryOptions) { if (options.history !== undefined && !Array.isArray(options.history)) { throw invalidArgType("history", "an instance of Array", options.history); } if (options.size !== undefined) { if (typeof options.size !== "number") throw invalidArgType("size", "of type number", options.size); if (!(options.size >= 0)) { throw Object.assign(new RangeError(`The value of "size" is out of range. It must be >= 0. Received ${options.size}`), { code: "ERR_OUT_OF_RANGE", }); } } this.#context = context; this.#removeHistoryDuplicates = options.removeHistoryDuplicates || false; this.#size = options.size ?? kHistorySize; this.#history = options.history ?? []; }
addHistory(isMultiline: boolean, lastCommandErrored: boolean): string { const line = this.#context.line; if (line.length === 0) return ""; if (this.#size === 0) return line; if (line.trim().length === 0) return line; // Each line of a multiline entry was saved as it was typed; keep only the whole. if ((isMultiline && this.#index === -1) || lastCommandErrored) this.#history.shift(); const normalizedLine = normalizeLineEndings(line, "\n", "\r"); if (this.#history.length === 0 || this.#history[0] !== normalizedLine) { if (this.#removeHistoryDuplicates) { const dupIndex = this.#history.indexOf(line); if (dupIndex !== -1) this.#history.splice(dupIndex, 1); } this.#history.unshift(normalizedLine); if (this.#history.length > this.#size) this.#history.pop(); } this.#index = -1; const finalLine = isMultiline ? reverseString(this.#history[0]!) : this.#history[0]!; // A listener may edit the history, e.g. to drop a password before it is kept. this.#context.emit("history", this.#history); return finalLine; }
canNavigateToNext(): boolean { return this.#index > -1 && this.#history.length > 0; }
navigateToNext(substringSearch: string | null): string | null { if (!this.canNavigateToNext()) return null; const search = substringSearch || ""; let index = this.#index - 1; while (index >= 0 && (!this.#history[index]!.startsWith(search) || this.#context.line === this.#history[index])) index--; this.#index = index; if (index === -1) return search; return normalizeLineEndings(this.#history[index]!, "\r", "\n"); }
canNavigateToPrevious(): boolean { return this.#history.length !== this.#index && this.#history.length > 0; }
navigateToPrevious(substringSearch: string | null = ""): string | null { if (!this.canNavigateToPrevious()) return null; const search = substringSearch || ""; let index = this.#index + 1; while (index < this.#history.length && (!this.#history[index]!.startsWith(search) || this.#context.line === this.#history[index])) { index++; } this.#index = index; if (index === this.#history.length) return search; return normalizeLineEndings(this.#history[index]!, "\r", "\n"); }
get size(): number { return this.#size; }
get isFlushing(): boolean { return false; }
get history(): string[] { return this.#history; }
set history(value: string[]) { this.#history = value; }
get index(): number { return this.#index; }
set index(value: number) { this.#index = value; }}