UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

1,403 lines (1,233 loc) 39.8 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/readline/interface.js import * as __hoisted_internal_events_symbols__ from "nstdlib/lib/internal/events/symbols"; import { codes as __codes__ } from "nstdlib/lib/internal/errors"; import { validateAbortSignal, validateArray, validateNumber, validateString, validateUint32, } from "nstdlib/lib/internal/validators"; import { kEmptyObject } from "nstdlib/lib/internal/util"; import { inspect, getStringWidth, stripVTControlCharacters, } from "nstdlib/lib/internal/util/inspect"; import * as EventEmitter from "nstdlib/lib/events"; import { addAbortListener } from "nstdlib/lib/internal/events/abort_listener"; import { charLengthAt, charLengthLeft, commonPrefix, kSubstringSearch, } from "nstdlib/lib/internal/readline/utils"; import { clearScreenDown, cursorTo, moveCursor, } from "nstdlib/lib/internal/readline/callbacks"; import { StringDecoder } from "nstdlib/lib/string_decoder"; import * as __hoisted_internal_readline_emitKeypressEvents__ from "nstdlib/lib/internal/readline/emitKeypressEvents"; const { ERR_INVALID_ARG_VALUE, ERR_USE_AFTER_CLOSE } = __codes__; let emitKeypressEvents; let kFirstEventParam; const kHistorySize = 30; const kMaxUndoRedoStackSize = 2048; const kMincrlfDelay = 100; // \r\n, \n, or \r followed by something other than \n const lineEnding = /\r?\n|\r(?!\n)/g; const kLineObjectStream = Symbol("line object stream"); const kQuestionCancel = Symbol("kQuestionCancel"); const kQuestion = Symbol("kQuestion"); // GNU readline library - keyseq-timeout is 500ms (default) const ESCAPE_CODE_TIMEOUT = 500; // Max length of the kill ring const kMaxLengthOfKillRing = 32; const kAddHistory = Symbol("_addHistory"); const kBeforeEdit = Symbol("_beforeEdit"); const kDecoder = Symbol("_decoder"); const kDeleteLeft = Symbol("_deleteLeft"); const kDeleteLineLeft = Symbol("_deleteLineLeft"); const kDeleteLineRight = Symbol("_deleteLineRight"); const kDeleteRight = Symbol("_deleteRight"); const kDeleteWordLeft = Symbol("_deleteWordLeft"); const kDeleteWordRight = Symbol("_deleteWordRight"); const kGetDisplayPos = Symbol("_getDisplayPos"); const kHistoryNext = Symbol("_historyNext"); const kHistoryPrev = Symbol("_historyPrev"); const kInsertString = Symbol("_insertString"); const kLine = Symbol("_line"); const kLine_buffer = Symbol("_line_buffer"); const kKillRing = Symbol("_killRing"); const kKillRingCursor = Symbol("_killRingCursor"); const kMoveCursor = Symbol("_moveCursor"); const kNormalWrite = Symbol("_normalWrite"); const kOldPrompt = Symbol("_oldPrompt"); const kOnLine = Symbol("_onLine"); const kPreviousKey = Symbol("_previousKey"); const kPrompt = Symbol("_prompt"); const kPushToKillRing = Symbol("_pushToKillRing"); const kPushToUndoStack = Symbol("_pushToUndoStack"); const kQuestionCallback = Symbol("_questionCallback"); const kRedo = Symbol("_redo"); const kRedoStack = Symbol("_redoStack"); const kRefreshLine = Symbol("_refreshLine"); const kSawKeyPress = Symbol("_sawKeyPress"); const kSawReturnAt = Symbol("_sawReturnAt"); const kSetRawMode = Symbol("_setRawMode"); const kTabComplete = Symbol("_tabComplete"); const kTabCompleter = Symbol("_tabCompleter"); const kTtyWrite = Symbol("_ttyWrite"); const kUndo = Symbol("_undo"); const kUndoStack = Symbol("_undoStack"); const kWordLeft = Symbol("_wordLeft"); const kWordRight = Symbol("_wordRight"); const kWriteToOutput = Symbol("_writeToOutput"); const kYank = Symbol("_yank"); const kYanking = Symbol("_yanking"); const kYankPop = Symbol("_yankPop"); function InterfaceConstructor(input, output, completer, terminal) { this[kSawReturnAt] = 0; // TODO(BridgeAR): Document this property. The name is not ideal, so we // might want to expose an alias and document that instead. this.isCompletionEnabled = true; this[kSawKeyPress] = false; this[kPreviousKey] = null; this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT; this.tabSize = 8; Function.prototype.call.call(EventEmitter, this); let history; let historySize; let removeHistoryDuplicates = false; let crlfDelay; let prompt = "> "; let signal; if (input?.input) { // An options object was given output = input.output; completer = input.completer; terminal = input.terminal; history = input.history; historySize = input.historySize; signal = input.signal; if (input.tabSize !== undefined) { validateUint32(input.tabSize, "tabSize", true); this.tabSize = input.tabSize; } removeHistoryDuplicates = input.removeHistoryDuplicates; if (input.prompt !== undefined) { prompt = input.prompt; } if (input.escapeCodeTimeout !== undefined) { if (Number.isFinite(input.escapeCodeTimeout)) { this.escapeCodeTimeout = input.escapeCodeTimeout; } else { throw new ERR_INVALID_ARG_VALUE( "input.escapeCodeTimeout", this.escapeCodeTimeout, ); } } if (signal) { validateAbortSignal(signal, "options.signal"); } crlfDelay = input.crlfDelay; input = input.input; } if (completer !== undefined && typeof completer !== "function") { throw new ERR_INVALID_ARG_VALUE("completer", completer); } if (history === undefined) { history = []; } else { validateArray(history, "history"); } if (historySize === undefined) { historySize = kHistorySize; } validateNumber(historySize, "historySize", 0); // Backwards compat; check the isTTY prop of the output stream // when `terminal` was not specified if (terminal === undefined && !(output === null || output === undefined)) { terminal = !!output.isTTY; } const self = this; this.line = ""; this[kSubstringSearch] = null; this.output = output; this.input = input; this[kUndoStack] = []; this[kRedoStack] = []; this.history = history; this.historySize = historySize; // The kill ring is a global list of blocks of text that were previously // killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest // element will be removed to make room for the latest deletion. With kill // ring, users are able to recall (yank) or cycle (yank pop) among previously // killed texts, quite similar to the behavior of Emacs. this[kKillRing] = []; this[kKillRingCursor] = 0; this.removeHistoryDuplicates = !!removeHistoryDuplicates; this.crlfDelay = crlfDelay ? Math.max(kMincrlfDelay, crlfDelay) : kMincrlfDelay; this.completer = completer; this.setPrompt(prompt); this.terminal = !!terminal; function onerror(err) { self.emit("error", err); } function ondata(data) { self[kNormalWrite](data); } function onend() { if ( typeof self[kLine_buffer] === "string" && self[kLine_buffer].length > 0 ) { self.emit("line", self[kLine_buffer]); } self.close(); } function ontermend() { if (typeof self.line === "string" && self.line.length > 0) { self.emit("line", self.line); } self.close(); } function onkeypress(s, key) { self[kTtyWrite](s, key); if (key && key.sequence) { // If the key.sequence is half of a surrogate pair // (>= 0xd800 and <= 0xdfff), refresh the line so // the character is displayed appropriately. const ch = String.prototype.codePointAt.call(key.sequence, 0); if (ch >= 0xd800 && ch <= 0xdfff) self[kRefreshLine](); } } function onresize() { self[kRefreshLine](); } this[kLineObjectStream] = undefined; input.on("error", onerror); if (!this.terminal) { function onSelfCloseWithoutTerminal() { input.removeListener("data", ondata); input.removeListener("error", onerror); input.removeListener("end", onend); } input.on("data", ondata); input.on("end", onend); self.once("close", onSelfCloseWithoutTerminal); this[kDecoder] = new StringDecoder("utf8"); } else { function onSelfCloseWithTerminal() { input.removeListener("keypress", onkeypress); input.removeListener("error", onerror); input.removeListener("end", ontermend); if (output !== null && output !== undefined) { output.removeListener("resize", onresize); } } emitKeypressEvents ??= __hoisted_internal_readline_emitKeypressEvents__; emitKeypressEvents(input, this); // `input` usually refers to stdin input.on("keypress", onkeypress); input.on("end", ontermend); this[kSetRawMode](true); this.terminal = true; // Cursor position on the line. this.cursor = 0; this.historyIndex = -1; if (output !== null && output !== undefined) output.on("resize", onresize); self.once("close", onSelfCloseWithTerminal); } if (signal) { const onAborted = () => self.close(); if (signal.aborted) { process.nextTick(onAborted); } else { const disposable = addAbortListener(signal, onAborted); self.once("close", disposable[Symbol.for("nodejs.dispose")]); } } // Current line this.line = ""; input.resume(); } Object.setPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype); Object.setPrototypeOf(InterfaceConstructor, EventEmitter); class Interface extends InterfaceConstructor { // eslint-disable-next-line no-useless-constructor constructor(input, output, completer, terminal) { super(input, output, completer, terminal); } get columns() { if (this.output && this.output.columns) return this.output.columns; return Infinity; } /** * Sets the prompt written to the output. * @param {string} prompt * @returns {void} */ setPrompt(prompt) { this[kPrompt] = prompt; } /** * Returns the current prompt used by `rl.prompt()`. * @returns {string} */ getPrompt() { return this[kPrompt]; } [kSetRawMode](mode) { const wasInRawMode = this.input.isRaw; if (typeof this.input.setRawMode === "function") { this.input.setRawMode(mode); } return wasInRawMode; } /** * Writes the configured `prompt` to a new line in `output`. * @param {boolean} [preserveCursor] * @returns {void} */ prompt(preserveCursor) { if (this.paused) this.resume(); if (this.terminal && process.env.TERM !== "dumb") { if (!preserveCursor) this.cursor = 0; this[kRefreshLine](); } else { this[kWriteToOutput](this[kPrompt]); } } [kQuestion](query, cb) { if (this.closed) { throw new ERR_USE_AFTER_CLOSE("readline"); } if (this[kQuestionCallback]) { this.prompt(); } else { this[kOldPrompt] = this[kPrompt]; this.setPrompt(query); this[kQuestionCallback] = cb; this.prompt(); } } [kOnLine](line) { if (this[kQuestionCallback]) { const cb = this[kQuestionCallback]; this[kQuestionCallback] = null; this.setPrompt(this[kOldPrompt]); cb(line); } else { this.emit("line", line); } } [kBeforeEdit](oldText, oldCursor) { this[kPushToUndoStack](oldText, oldCursor); } [kQuestionCancel]() { if (this[kQuestionCallback]) { this[kQuestionCallback] = null; this.setPrompt(this[kOldPrompt]); this.clearLine(); } } [kWriteToOutput](stringToWrite) { validateString(stringToWrite, "stringToWrite"); if (this.output !== null && this.output !== undefined) { this.output.write(stringToWrite); } } [kAddHistory]() { if (this.line.length === 0) return ""; // If the history is disabled then return the line if (this.historySize === 0) return this.line; // If the trimmed line is empty then return the line if (String.prototype.trim.call(this.line).length === 0) return this.line; if (this.history.length === 0 || this.history[0] !== this.line) { if (this.removeHistoryDuplicates) { // Remove older history line if identical to new one const dupIndex = Array.prototype.indexOf.call(this.history, this.line); if (dupIndex !== -1) Array.prototype.splice.call(this.history, dupIndex, 1); } Array.prototype.unshift.call(this.history, this.line); // Only store so many if (this.history.length > this.historySize) Array.prototype.pop.call(this.history); } this.historyIndex = -1; // The listener could change the history object, possibly // to remove the last added entry if it is sensitive and should // not be persisted in the history, like a password const line = this.history[0]; // Emit history event to notify listeners of update this.emit("history", this.history); return line; } [kRefreshLine]() { // line length const line = this[kPrompt] + this.line; const dispPos = this[kGetDisplayPos](line); const lineCols = dispPos.cols; const lineRows = dispPos.rows; // cursor position const cursorPos = this.getCursorPos(); // First move to the bottom of the current line, based on cursor pos const prevRows = this.prevRows || 0; if (prevRows > 0) { moveCursor(this.output, 0, -prevRows); } // Cursor to left edge. cursorTo(this.output, 0); // erase data clearScreenDown(this.output); // Write the prompt and the current buffer content. this[kWriteToOutput](line); // Force terminal to allocate a new line if (lineCols === 0) { this[kWriteToOutput](" "); } // Move cursor to original position. cursorTo(this.output, cursorPos.cols); const diff = lineRows - cursorPos.rows; if (diff > 0) { moveCursor(this.output, 0, -diff); } this.prevRows = cursorPos.rows; } /** * Closes the `readline.Interface` instance. * @returns {void} */ close() { if (this.closed) return; this.pause(); if (this.terminal) { this[kSetRawMode](false); } this.closed = true; this.emit("close"); } /** * Pauses the `input` stream. * @returns {void | Interface} */ pause() { if (this.paused) return; this.input.pause(); this.paused = true; this.emit("pause"); return this; } /** * Resumes the `input` stream if paused. * @returns {void | Interface} */ resume() { if (!this.paused) return; this.input.resume(); this.paused = false; this.emit("resume"); return this; } /** * Writes either `data` or a `key` sequence identified by * `key` to the `output`. * @param {string} d * @param {{ * ctrl?: boolean; * meta?: boolean; * shift?: boolean; * name?: string; * }} [key] * @returns {void} */ write(d, key) { if (this.paused) this.resume(); if (this.terminal) { this[kTtyWrite](d, key); } else { this[kNormalWrite](d); } } [kNormalWrite](b) { if (b === undefined) { return; } let string = this[kDecoder].write(b); if ( this[kSawReturnAt] && Date.now() - this[kSawReturnAt] <= this.crlfDelay ) { if (String.prototype.codePointAt.call(string) === 10) string = String.prototype.slice.call(string, 1); this[kSawReturnAt] = 0; } // Run test() on the new string chunk, not on the entire line buffer. let newPartContainsEnding = RegExp.prototype.exec.call(lineEnding, string); if (newPartContainsEnding !== null) { if (this[kLine_buffer]) { string = this[kLine_buffer] + string; this[kLine_buffer] = null; lineEnding.lastIndex = 0; // Start the search from the beginning of the string. newPartContainsEnding = RegExp.prototype.exec.call(lineEnding, string); } this[kSawReturnAt] = String.prototype.endsWith.call(string, "\r") ? Date.now() : 0; const indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex]; let nextMatch; while ( (nextMatch = RegExp.prototype.exec.call(lineEnding, string)) !== null ) { Array.prototype.push.call( indexes, nextMatch.index, lineEnding.lastIndex, ); } const lastIndex = indexes.length - 1; // Either '' or (conceivably) the unfinished portion of the next line this[kLine_buffer] = String.prototype.slice.call( string, indexes[lastIndex], ); for (let i = 1; i < lastIndex; i += 2) { this[kOnLine]( String.prototype.slice.call(string, indexes[i - 1], indexes[i]), ); } } else if (string) { // No newlines this time, save what we have for next time if (this[kLine_buffer]) { this[kLine_buffer] += string; } else { this[kLine_buffer] = string; } } } [kInsertString](c) { this[kBeforeEdit](this.line, this.cursor); if (this.cursor < this.line.length) { const beg = String.prototype.slice.call(this.line, 0, this.cursor); const end = String.prototype.slice.call( this.line, this.cursor, this.line.length, ); this.line = beg + c + end; this.cursor += c.length; this[kRefreshLine](); } else { const oldPos = this.getCursorPos(); this.line += c; this.cursor += c.length; const newPos = this.getCursorPos(); if (oldPos.rows < newPos.rows) { this[kRefreshLine](); } else { this[kWriteToOutput](c); } } } async [kTabComplete](lastKeypressWasTab) { this.pause(); const string = String.prototype.slice.call(this.line, 0, this.cursor); let value; try { value = await this.completer(string); } catch (err) { this[kWriteToOutput](`Tab completion error: ${inspect(err)}`); return; } finally { this.resume(); } this[kTabCompleter](lastKeypressWasTab, value); } [kTabCompleter](lastKeypressWasTab, { 0: completions, 1: completeOn }) { // Result and the text that was completed. if (!completions || completions.length === 0) { return; } // If there is a common prefix to all matches, then apply that portion. const prefix = commonPrefix( Array.prototype.filter.call(completions, (e) => e !== ""), ); if ( String.prototype.startsWith.call(prefix, completeOn) && prefix.length > completeOn.length ) { this[kInsertString]( String.prototype.slice.call(prefix, completeOn.length), ); return; } else if (!String.prototype.startsWith.call(completeOn, prefix)) { this.line = String.prototype.slice.call( this.line, 0, this.cursor - completeOn.length, ) + prefix + String.prototype.slice.call(this.line, this.cursor, this.line.length); this.cursor = this.cursor - completeOn.length + prefix.length; this[kRefreshLine](); return; } if (!lastKeypressWasTab) { return; } this[kBeforeEdit](this.line, this.cursor); // Apply/show completions. const completionsWidth = Array.prototype.map.call(completions, (e) => getStringWidth(e), ); const width = MathMaxApply(completionsWidth) + 2; // 2 space padding let maxColumns = Math.floor(this.columns / width) || 1; if (maxColumns === Infinity) { maxColumns = 1; } let output = "\r\n"; let lineIndex = 0; let whitespace = 0; for (let i = 0; i < completions.length; i++) { const completion = completions[i]; if (completion === "" || lineIndex === maxColumns) { output += "\r\n"; lineIndex = 0; whitespace = 0; } else { output += String.prototype.repeat.call(" ", whitespace); } if (completion !== "") { output += completion; whitespace = width - completionsWidth[i]; lineIndex++; } else { output += "\r\n"; } } if (lineIndex !== 0) { output += "\r\n\r\n"; } this[kWriteToOutput](output); this[kRefreshLine](); } [kWordLeft]() { if (this.cursor > 0) { // Reverse the string and match a word near beginning // to avoid quadratic time complexity const leading = String.prototype.slice.call(this.line, 0, this.cursor); const reversed = Array.prototype.join.call( Array.prototype.reverse.call(Array.from(leading)), "", ); const match = RegExp.prototype.exec.call( /^\s*(?:[^\w\s]+|\w+)?/, reversed, ); this[kMoveCursor](-match[0].length); } } [kWordRight]() { if (this.cursor < this.line.length) { const trailing = String.prototype.slice.call(this.line, this.cursor); const match = RegExp.prototype.exec.call( /^(?:\s+|[^\w\s]+|\w+)\s*/, trailing, ); this[kMoveCursor](match[0].length); } } [kDeleteLeft]() { if (this.cursor > 0 && this.line.length > 0) { this[kBeforeEdit](this.line, this.cursor); // The number of UTF-16 units comprising the character to the left const charSize = charLengthLeft(this.line, this.cursor); this.line = String.prototype.slice.call(this.line, 0, this.cursor - charSize) + String.prototype.slice.call(this.line, this.cursor, this.line.length); this.cursor -= charSize; this[kRefreshLine](); } } [kDeleteRight]() { if (this.cursor < this.line.length) { this[kBeforeEdit](this.line, this.cursor); // The number of UTF-16 units comprising the character to the left const charSize = charLengthAt(this.line, this.cursor); this.line = String.prototype.slice.call(this.line, 0, this.cursor) + String.prototype.slice.call( this.line, this.cursor + charSize, this.line.length, ); this[kRefreshLine](); } } [kDeleteWordLeft]() { if (this.cursor > 0) { this[kBeforeEdit](this.line, this.cursor); // Reverse the string and match a word near beginning // to avoid quadratic time complexity let leading = String.prototype.slice.call(this.line, 0, this.cursor); const reversed = Array.prototype.join.call( Array.prototype.reverse.call(Array.from(leading)), "", ); const match = RegExp.prototype.exec.call( /^\s*(?:[^\w\s]+|\w+)?/, reversed, ); leading = String.prototype.slice.call( leading, 0, leading.length - match[0].length, ); this.line = leading + String.prototype.slice.call(this.line, this.cursor, this.line.length); this.cursor = leading.length; this[kRefreshLine](); } } [kDeleteWordRight]() { if (this.cursor < this.line.length) { this[kBeforeEdit](this.line, this.cursor); const trailing = String.prototype.slice.call(this.line, this.cursor); const match = RegExp.prototype.exec.call(/^(?:\s+|\W+|\w+)\s*/, trailing); this.line = String.prototype.slice.call(this.line, 0, this.cursor) + String.prototype.slice.call(trailing, match[0].length); this[kRefreshLine](); } } [kDeleteLineLeft]() { this[kBeforeEdit](this.line, this.cursor); const del = String.prototype.slice.call(this.line, 0, this.cursor); this.line = String.prototype.slice.call(this.line, this.cursor); this.cursor = 0; this[kPushToKillRing](del); this[kRefreshLine](); } [kDeleteLineRight]() { this[kBeforeEdit](this.line, this.cursor); const del = String.prototype.slice.call(this.line, this.cursor); this.line = String.prototype.slice.call(this.line, 0, this.cursor); this[kPushToKillRing](del); this[kRefreshLine](); } [kPushToKillRing](del) { if (!del || del === this[kKillRing][0]) return; Array.prototype.unshift.call(this[kKillRing], del); this[kKillRingCursor] = 0; while (this[kKillRing].length > kMaxLengthOfKillRing) Array.prototype.pop.call(this[kKillRing]); } [kYank]() { if (this[kKillRing].length > 0) { this[kYanking] = true; this[kInsertString](this[kKillRing][this[kKillRingCursor]]); } } [kYankPop]() { if (!this[kYanking]) { return; } if (this[kKillRing].length > 1) { const lastYank = this[kKillRing][this[kKillRingCursor]]; this[kKillRingCursor]++; if (this[kKillRingCursor] >= this[kKillRing].length) { this[kKillRingCursor] = 0; } const currentYank = this[kKillRing][this[kKillRingCursor]]; const head = String.prototype.slice.call( this.line, 0, this.cursor - lastYank.length, ); const tail = String.prototype.slice.call(this.line, this.cursor); this.line = head + currentYank + tail; this.cursor = head.length + currentYank.length; this[kRefreshLine](); } } clearLine() { this[kMoveCursor](+Infinity); this[kWriteToOutput]("\r\n"); this.line = ""; this.cursor = 0; this.prevRows = 0; } [kLine]() { const line = this[kAddHistory](); this[kUndoStack] = []; this[kRedoStack] = []; this.clearLine(); this[kOnLine](line); } [kPushToUndoStack](text, cursor) { if ( Array.prototype.push.call(this[kUndoStack], { text, cursor }) > kMaxUndoRedoStackSize ) { Array.prototype.shift.call(this[kUndoStack]); } } [kUndo]() { if (this[kUndoStack].length <= 0) return; Array.prototype.push.call(this[kRedoStack], { text: this.line, cursor: this.cursor, }); const entry = Array.prototype.pop.call(this[kUndoStack]); this.line = entry.text; this.cursor = entry.cursor; this[kRefreshLine](); } [kRedo]() { if (this[kRedoStack].length <= 0) return; Array.prototype.push.call(this[kUndoStack], { text: this.line, cursor: this.cursor, }); const entry = Array.prototype.pop.call(this[kRedoStack]); this.line = entry.text; this.cursor = entry.cursor; this[kRefreshLine](); } // TODO(BridgeAR): Add underscores to the search part and a red background in // case no match is found. This should only be the visual part and not the // actual line content! // TODO(BridgeAR): In case the substring based search is active and the end is // reached, show a comment how to search the history as before. E.g., using // <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first // one. [kHistoryNext]() { if (this.historyIndex >= 0) { this[kBeforeEdit](this.line, this.cursor); const search = this[kSubstringSearch] || ""; let index = this.historyIndex - 1; while ( index >= 0 && (!String.prototype.startsWith.call(this.history[index], search) || this.line === this.history[index]) ) { index--; } if (index === -1) { this.line = search; } else { this.line = this.history[index]; } this.historyIndex = index; this.cursor = this.line.length; // Set cursor to end of line. this[kRefreshLine](); } } [kHistoryPrev]() { if (this.historyIndex < this.history.length && this.history.length) { this[kBeforeEdit](this.line, this.cursor); const search = this[kSubstringSearch] || ""; let index = this.historyIndex + 1; while ( index < this.history.length && (!String.prototype.startsWith.call(this.history[index], search) || this.line === this.history[index]) ) { index++; } if (index === this.history.length) { this.line = search; } else { this.line = this.history[index]; } this.historyIndex = index; this.cursor = this.line.length; // Set cursor to end of line. this[kRefreshLine](); } } // Returns the last character's display position of the given string [kGetDisplayPos](str) { let offset = 0; const col = this.columns; let rows = 0; str = stripVTControlCharacters(str); for (const char of new SafeStringIterator(str)) { if (char === "\n") { // Rows must be incremented by 1 even if offset = 0 or col = +Infinity. rows += Math.ceil(offset / col) || 1; offset = 0; continue; } // Tabs must be aligned by an offset of the tab size. if (char === "\t") { offset += this.tabSize - (offset % this.tabSize); continue; } const width = getStringWidth(char, false /* stripVTControlCharacters */); if (width === 0 || width === 1) { offset += width; } else { // width === 2 if ((offset + 1) % col === 0) { offset++; } offset += 2; } } const cols = offset % col; rows += (offset - cols) / col; return { cols, rows }; } /** * Returns the real position of the cursor in relation * to the input prompt + string. * @returns {{ * rows: number; * cols: number; * }} */ getCursorPos() { const strBeforeCursor = this[kPrompt] + String.prototype.slice.call(this.line, 0, this.cursor); return this[kGetDisplayPos](strBeforeCursor); } // This function moves cursor dx places to the right // (-dx for left) and refreshes the line if it is needed. [kMoveCursor](dx) { if (dx === 0) { return; } const oldPos = this.getCursorPos(); this.cursor += dx; // Bounds check if (this.cursor < 0) { this.cursor = 0; } else if (this.cursor > this.line.length) { this.cursor = this.line.length; } const newPos = this.getCursorPos(); // Check if cursor stayed on the line. if (oldPos.rows === newPos.rows) { const diffWidth = newPos.cols - oldPos.cols; moveCursor(this.output, diffWidth, 0); } else { this[kRefreshLine](); } } // Handle a write from the tty [kTtyWrite](s, key) { const previousKey = this[kPreviousKey]; key = key || kEmptyObject; this[kPreviousKey] = key; if (!key.meta || key.name !== "y") { // Reset yanking state unless we are doing yank pop. this[kYanking] = false; } // Activate or deactivate substring search. if ( (key.name === "up" || key.name === "down") && !key.ctrl && !key.meta && !key.shift ) { if (this[kSubstringSearch] === null) { this[kSubstringSearch] = String.prototype.slice.call( this.line, 0, this.cursor, ); } } else if (this[kSubstringSearch] !== null) { this[kSubstringSearch] = null; // Reset the index in case there's no match. if (this.history.length === this.historyIndex) { this.historyIndex = -1; } } // Undo & Redo if (typeof key.sequence === "string") { switch (String.prototype.codePointAt.call(key.sequence, 0)) { case 0x1f: this[kUndo](); return; case 0x1e: this[kRedo](); return; default: break; } } // Ignore escape key, fixes // https://github.com/nodejs/node-v0.x-archive/issues/2876. if (key.name === "escape") return; if (key.ctrl && key.shift) { /* Control and shift pressed */ switch (key.name) { // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is // identical to <ctrl>-h. It should have a unique escape sequence. case "backspace": this[kDeleteLineLeft](); break; case "delete": this[kDeleteLineRight](); break; } } else if (key.ctrl) { /* Control key pressed */ switch (key.name) { case "c": if (this.listenerCount("SIGINT") > 0) { this.emit("SIGINT"); } else { // This readline instance is finished this.close(); } break; case "h": // delete left this[kDeleteLeft](); break; case "d": // delete right or EOF if (this.cursor === 0 && this.line.length === 0) { // This readline instance is finished this.close(); } else if (this.cursor < this.line.length) { this[kDeleteRight](); } break; case "u": // Delete from current to start of line this[kDeleteLineLeft](); break; case "k": // Delete from current to end of line this[kDeleteLineRight](); break; case "a": // Go to the start of the line this[kMoveCursor](-Infinity); break; case "e": // Go to the end of the line this[kMoveCursor](+Infinity); break; case "b": // back one character this[kMoveCursor](-charLengthLeft(this.line, this.cursor)); break; case "f": // Forward one character this[kMoveCursor](+charLengthAt(this.line, this.cursor)); break; case "l": // Clear the whole screen cursorTo(this.output, 0, 0); clearScreenDown(this.output); this[kRefreshLine](); break; case "n": // next history item this[kHistoryNext](); break; case "p": // Previous history item this[kHistoryPrev](); break; case "y": // Yank killed string this[kYank](); break; case "z": if (process.platform === "win32") break; if (this.listenerCount("SIGTSTP") > 0) { this.emit("SIGTSTP"); } else { process.once("SIGCONT", () => { // Don't raise events if stream has already been abandoned. if (!this.paused) { // Stream must be paused and resumed after SIGCONT to catch // SIGINT, SIGTSTP, and EOF. this.pause(); this.emit("SIGCONT"); } // Explicitly re-enable "raw mode" and move the cursor to // the correct position. // See https://github.com/joyent/node/issues/3295. this[kSetRawMode](true); this[kRefreshLine](); }); this[kSetRawMode](false); process.kill(process.pid, "SIGTSTP"); } break; case "w": // Delete backwards to a word boundary // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is // identical to <ctrl>-h. It should have a unique escape sequence. // Falls through case "backspace": this[kDeleteWordLeft](); break; case "delete": // Delete forward to a word boundary this[kDeleteWordRight](); break; case "left": this[kWordLeft](); break; case "right": this[kWordRight](); break; } } else if (key.meta) { /* Meta key pressed */ switch (key.name) { case "b": // backward word this[kWordLeft](); break; case "f": // forward word this[kWordRight](); break; case "d": // delete forward word case "delete": this[kDeleteWordRight](); break; case "backspace": // Delete backwards to a word boundary this[kDeleteWordLeft](); break; case "y": // Doing yank pop this[kYankPop](); break; } } else { /* No modifier keys used */ // \r bookkeeping is only relevant if a \n comes right after. if (this[kSawReturnAt] && key.name !== "enter") this[kSawReturnAt] = 0; switch (key.name) { case "return": // Carriage return, i.e. \r this[kSawReturnAt] = Date.now(); this[kLine](); break; case "enter": // When key interval > crlfDelay if ( this[kSawReturnAt] === 0 || Date.now() - this[kSawReturnAt] > this.crlfDelay ) { this[kLine](); } this[kSawReturnAt] = 0; break; case "backspace": this[kDeleteLeft](); break; case "delete": this[kDeleteRight](); break; case "left": // Obtain the code point to the left this[kMoveCursor](-charLengthLeft(this.line, this.cursor)); break; case "right": this[kMoveCursor](+charLengthAt(this.line, this.cursor)); break; case "home": this[kMoveCursor](-Infinity); break; case "end": this[kMoveCursor](+Infinity); break; case "up": this[kHistoryPrev](); break; case "down": this[kHistoryNext](); break; case "tab": // If tab completion enabled, do that... if ( typeof this.completer === "function" && this.isCompletionEnabled ) { const lastKeypressWasTab = previousKey && previousKey.name === "tab"; this[kTabComplete](lastKeypressWasTab); break; } // falls through default: if (typeof s === "string" && s) { // Erase state of previous searches. lineEnding.lastIndex = 0; let nextMatch; // Keep track of the end of the last match. let lastIndex = 0; while ( (nextMatch = RegExp.prototype.exec.call(lineEnding, s)) !== null ) { this[kInsertString]( String.prototype.slice.call(s, lastIndex, nextMatch.index), ); ({ lastIndex } = lineEnding); this[kLine](); // Restore lastIndex as the call to kLine could have mutated it. lineEnding.lastIndex = lastIndex; } // This ensures that the last line is written if it doesn't end in a newline. // Note that the last line may be the first line, in which case this still works. this[kInsertString](String.prototype.slice.call(s, lastIndex)); } } } } /** * Creates an `AsyncIterator` object that iterates through * each line in the input stream as a string. * @typedef {{ * [Symbol.asyncIterator]: () => InterfaceAsyncIterator, * next: () => Promise<string> * }} InterfaceAsyncIterator * @returns {InterfaceAsyncIterator} */ [SymbolAsyncIterator]() { if (this[kLineObjectStream] === undefined) { kFirstEventParam ??= __hoisted_internal_events_symbols__.kFirstEventParam; this[kLineObjectStream] = EventEmitter.on(this, "line", { close: ["close"], highWaterMark: 1024, [kFirstEventParam]: true, }); } return this[kLineObjectStream]; } } export { Interface }; export { InterfaceConstructor }; export { kAddHistory }; export { kDecoder }; export { kDeleteLeft }; export { kDeleteLineLeft }; export { kDeleteLineRight }; export { kDeleteRight }; export { kDeleteWordLeft }; export { kDeleteWordRight }; export { kGetDisplayPos }; export { kHistoryNext }; export { kHistoryPrev }; export { kInsertString }; export { kLine }; export { kLine_buffer }; export { kMoveCursor }; export { kNormalWrite }; export { kOldPrompt }; export { kOnLine }; export { kPreviousKey }; export { kPrompt }; export { kQuestion }; export { kQuestionCallback }; export { kQuestionCancel }; export { kRefreshLine }; export { kSawKeyPress }; export { kSawReturnAt }; export { kSetRawMode }; export { kTabComplete }; export { kTabCompleter }; export { kTtyWrite }; export { kWordLeft }; export { kWordRight }; export { kWriteToOutput };