1'use strict'; 2 3const { 4 ArrayFrom, 5 ArrayPrototypeFilter, 6 ArrayPrototypeIndexOf, 7 ArrayPrototypeJoin, 8 ArrayPrototypeMap, 9 ArrayPrototypePop, 10 ArrayPrototypePush, 11 ArrayPrototypeReverse, 12 ArrayPrototypeSplice, 13 ArrayPrototypeShift, 14 ArrayPrototypeUnshift, 15 DateNow, 16 FunctionPrototypeCall, 17 MathCeil, 18 MathFloor, 19 MathMax, 20 MathMaxApply, 21 NumberIsFinite, 22 NumberIsNaN, 23 ObjectSetPrototypeOf, 24 RegExpPrototypeExec, 25 StringPrototypeCodePointAt, 26 StringPrototypeEndsWith, 27 StringPrototypeRepeat, 28 StringPrototypeSlice, 29 StringPrototypeStartsWith, 30 StringPrototypeTrim, 31 Symbol, 32 SymbolDispose, 33 SymbolAsyncIterator, 34 SafeStringIterator, 35} = primordials; 36 37const { codes } = require('internal/errors'); 38 39const { 40 ERR_INVALID_ARG_VALUE, 41 ERR_USE_AFTER_CLOSE, 42} = codes; 43const { 44 validateAbortSignal, 45 validateArray, 46 validateString, 47 validateUint32, 48} = require('internal/validators'); 49const { kEmptyObject } = require('internal/util'); 50const { 51 inspect, 52 getStringWidth, 53 stripVTControlCharacters, 54} = require('internal/util/inspect'); 55const EventEmitter = require('events'); 56const { 57 charLengthAt, 58 charLengthLeft, 59 commonPrefix, 60 kSubstringSearch, 61} = require('internal/readline/utils'); 62let emitKeypressEvents; 63const { 64 clearScreenDown, 65 cursorTo, 66 moveCursor, 67} = require('internal/readline/callbacks'); 68 69const { StringDecoder } = require('string_decoder'); 70 71// Lazy load Readable for startup performance. 72let Readable; 73 74const kHistorySize = 30; 75const kMaxUndoRedoStackSize = 2048; 76const kMincrlfDelay = 100; 77// \r\n, \n, or \r followed by something other than \n 78const lineEnding = /\r?\n|\r(?!\n)/g; 79 80const kLineObjectStream = Symbol('line object stream'); 81const kQuestionCancel = Symbol('kQuestionCancel'); 82 83// GNU readline library - keyseq-timeout is 500ms (default) 84const ESCAPE_CODE_TIMEOUT = 500; 85 86// Max length of the kill ring 87const kMaxLengthOfKillRing = 32; 88 89const kAddHistory = Symbol('_addHistory'); 90const kBeforeEdit = Symbol('_beforeEdit'); 91const kDecoder = Symbol('_decoder'); 92const kDeleteLeft = Symbol('_deleteLeft'); 93const kDeleteLineLeft = Symbol('_deleteLineLeft'); 94const kDeleteLineRight = Symbol('_deleteLineRight'); 95const kDeleteRight = Symbol('_deleteRight'); 96const kDeleteWordLeft = Symbol('_deleteWordLeft'); 97const kDeleteWordRight = Symbol('_deleteWordRight'); 98const kGetDisplayPos = Symbol('_getDisplayPos'); 99const kHistoryNext = Symbol('_historyNext'); 100const kHistoryPrev = Symbol('_historyPrev'); 101const kInsertString = Symbol('_insertString'); 102const kLine = Symbol('_line'); 103const kLine_buffer = Symbol('_line_buffer'); 104const kKillRing = Symbol('_killRing'); 105const kKillRingCursor = Symbol('_killRingCursor'); 106const kMoveCursor = Symbol('_moveCursor'); 107const kNormalWrite = Symbol('_normalWrite'); 108const kOldPrompt = Symbol('_oldPrompt'); 109const kOnLine = Symbol('_onLine'); 110const kPreviousKey = Symbol('_previousKey'); 111const kPrompt = Symbol('_prompt'); 112const kPushToKillRing = Symbol('_pushToKillRing'); 113const kPushToUndoStack = Symbol('_pushToUndoStack'); 114const kQuestionCallback = Symbol('_questionCallback'); 115const kRedo = Symbol('_redo'); 116const kRedoStack = Symbol('_redoStack'); 117const kRefreshLine = Symbol('_refreshLine'); 118const kSawKeyPress = Symbol('_sawKeyPress'); 119const kSawReturnAt = Symbol('_sawReturnAt'); 120const kSetRawMode = Symbol('_setRawMode'); 121const kTabComplete = Symbol('_tabComplete'); 122const kTabCompleter = Symbol('_tabCompleter'); 123const kTtyWrite = Symbol('_ttyWrite'); 124const kUndo = Symbol('_undo'); 125const kUndoStack = Symbol('_undoStack'); 126const kWordLeft = Symbol('_wordLeft'); 127const kWordRight = Symbol('_wordRight'); 128const kWriteToOutput = Symbol('_writeToOutput'); 129const kYank = Symbol('_yank'); 130const kYanking = Symbol('_yanking'); 131const kYankPop = Symbol('_yankPop'); 132 133function InterfaceConstructor(input, output, completer, terminal) { 134 this[kSawReturnAt] = 0; 135 // TODO(BridgeAR): Document this property. The name is not ideal, so we 136 // might want to expose an alias and document that instead. 137 this.isCompletionEnabled = true; 138 this[kSawKeyPress] = false; 139 this[kPreviousKey] = null; 140 this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT; 141 this.tabSize = 8; 142 143 FunctionPrototypeCall(EventEmitter, this); 144 145 let history; 146 let historySize; 147 let removeHistoryDuplicates = false; 148 let crlfDelay; 149 let prompt = '> '; 150 let signal; 151 152 if (input?.input) { 153 // An options object was given 154 output = input.output; 155 completer = input.completer; 156 terminal = input.terminal; 157 history = input.history; 158 historySize = input.historySize; 159 signal = input.signal; 160 if (input.tabSize !== undefined) { 161 validateUint32(input.tabSize, 'tabSize', true); 162 this.tabSize = input.tabSize; 163 } 164 removeHistoryDuplicates = input.removeHistoryDuplicates; 165 if (input.prompt !== undefined) { 166 prompt = input.prompt; 167 } 168 if (input.escapeCodeTimeout !== undefined) { 169 if (NumberIsFinite(input.escapeCodeTimeout)) { 170 this.escapeCodeTimeout = input.escapeCodeTimeout; 171 } else { 172 throw new ERR_INVALID_ARG_VALUE( 173 'input.escapeCodeTimeout', 174 this.escapeCodeTimeout, 175 ); 176 } 177 } 178 179 if (signal) { 180 validateAbortSignal(signal, 'options.signal'); 181 } 182 183 crlfDelay = input.crlfDelay; 184 input = input.input; 185 } 186 187 if (completer !== undefined && typeof completer !== 'function') { 188 throw new ERR_INVALID_ARG_VALUE('completer', completer); 189 } 190 191 if (history === undefined) { 192 history = []; 193 } else { 194 validateArray(history, 'history'); 195 } 196 197 if (historySize === undefined) { 198 historySize = kHistorySize; 199 } 200 201 if ( 202 typeof historySize !== 'number' || 203 NumberIsNaN(historySize) || 204 historySize < 0 205 ) { 206 throw new ERR_INVALID_ARG_VALUE.RangeError('historySize', historySize); 207 } 208 209 // Backwards compat; check the isTTY prop of the output stream 210 // when `terminal` was not specified 211 if (terminal === undefined && !(output === null || output === undefined)) { 212 terminal = !!output.isTTY; 213 } 214 215 const self = this; 216 217 this.line = ''; 218 this[kSubstringSearch] = null; 219 this.output = output; 220 this.input = input; 221 this[kUndoStack] = []; 222 this[kRedoStack] = []; 223 this.history = history; 224 this.historySize = historySize; 225 226 // The kill ring is a global list of blocks of text that were previously 227 // killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest 228 // element will be removed to make room for the latest deletion. With kill 229 // ring, users are able to recall (yank) or cycle (yank pop) among previously 230 // killed texts, quite similar to the behavior of Emacs. 231 this[kKillRing] = []; 232 this[kKillRingCursor] = 0; 233 234 this.removeHistoryDuplicates = !!removeHistoryDuplicates; 235 this.crlfDelay = crlfDelay ? 236 MathMax(kMincrlfDelay, crlfDelay) : 237 kMincrlfDelay; 238 this.completer = completer; 239 240 this.setPrompt(prompt); 241 242 this.terminal = !!terminal; 243 244 245 function onerror(err) { 246 self.emit('error', err); 247 } 248 249 function ondata(data) { 250 self[kNormalWrite](data); 251 } 252 253 function onend() { 254 if ( 255 typeof self[kLine_buffer] === 'string' && 256 self[kLine_buffer].length > 0 257 ) { 258 self.emit('line', self[kLine_buffer]); 259 } 260 self.close(); 261 } 262 263 function ontermend() { 264 if (typeof self.line === 'string' && self.line.length > 0) { 265 self.emit('line', self.line); 266 } 267 self.close(); 268 } 269 270 function onkeypress(s, key) { 271 self[kTtyWrite](s, key); 272 if (key && key.sequence) { 273 // If the key.sequence is half of a surrogate pair 274 // (>= 0xd800 and <= 0xdfff), refresh the line so 275 // the character is displayed appropriately. 276 const ch = StringPrototypeCodePointAt(key.sequence, 0); 277 if (ch >= 0xd800 && ch <= 0xdfff) self[kRefreshLine](); 278 } 279 } 280 281 function onresize() { 282 self[kRefreshLine](); 283 } 284 285 this[kLineObjectStream] = undefined; 286 287 input.on('error', onerror); 288 289 if (!this.terminal) { 290 function onSelfCloseWithoutTerminal() { 291 input.removeListener('data', ondata); 292 input.removeListener('error', onerror); 293 input.removeListener('end', onend); 294 } 295 296 input.on('data', ondata); 297 input.on('end', onend); 298 self.once('close', onSelfCloseWithoutTerminal); 299 this[kDecoder] = new StringDecoder('utf8'); 300 } else { 301 function onSelfCloseWithTerminal() { 302 input.removeListener('keypress', onkeypress); 303 input.removeListener('error', onerror); 304 input.removeListener('end', ontermend); 305 if (output !== null && output !== undefined) { 306 output.removeListener('resize', onresize); 307 } 308 } 309 310 emitKeypressEvents ??= require('internal/readline/emitKeypressEvents'); 311 emitKeypressEvents(input, this); 312 313 // `input` usually refers to stdin 314 input.on('keypress', onkeypress); 315 input.on('end', ontermend); 316 317 this[kSetRawMode](true); 318 this.terminal = true; 319 320 // Cursor position on the line. 321 this.cursor = 0; 322 323 this.historyIndex = -1; 324 325 if (output !== null && output !== undefined) 326 output.on('resize', onresize); 327 328 self.once('close', onSelfCloseWithTerminal); 329 } 330 331 if (signal) { 332 const onAborted = () => self.close(); 333 if (signal.aborted) { 334 process.nextTick(onAborted); 335 } else { 336 const disposable = EventEmitter.addAbortListener(signal, onAborted); 337 self.once('close', disposable[SymbolDispose]); 338 } 339 } 340 341 // Current line 342 this.line = ''; 343 344 input.resume(); 345} 346 347ObjectSetPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype); 348ObjectSetPrototypeOf(InterfaceConstructor, EventEmitter); 349 350class Interface extends InterfaceConstructor { 351 // eslint-disable-next-line no-useless-constructor 352 constructor(input, output, completer, terminal) { 353 super(input, output, completer, terminal); 354 } 355 get columns() { 356 if (this.output && this.output.columns) return this.output.columns; 357 return Infinity; 358 } 359 360 /** 361 * Sets the prompt written to the output. 362 * @param {string} prompt 363 * @returns {void} 364 */ 365 setPrompt(prompt) { 366 this[kPrompt] = prompt; 367 } 368 369 /** 370 * Returns the current prompt used by `rl.prompt()`. 371 * @returns {string} 372 */ 373 getPrompt() { 374 return this[kPrompt]; 375 } 376 377 [kSetRawMode](mode) { 378 const wasInRawMode = this.input.isRaw; 379 380 if (typeof this.input.setRawMode === 'function') { 381 this.input.setRawMode(mode); 382 } 383 384 return wasInRawMode; 385 } 386 387 /** 388 * Writes the configured `prompt` to a new line in `output`. 389 * @param {boolean} [preserveCursor] 390 * @returns {void} 391 */ 392 prompt(preserveCursor) { 393 if (this.paused) this.resume(); 394 if (this.terminal && process.env.TERM !== 'dumb') { 395 if (!preserveCursor) this.cursor = 0; 396 this[kRefreshLine](); 397 } else { 398 this[kWriteToOutput](this[kPrompt]); 399 } 400 } 401 402 question(query, cb) { 403 if (this.closed) { 404 throw new ERR_USE_AFTER_CLOSE('readline'); 405 } 406 if (this[kQuestionCallback]) { 407 this.prompt(); 408 } else { 409 this[kOldPrompt] = this[kPrompt]; 410 this.setPrompt(query); 411 this[kQuestionCallback] = cb; 412 this.prompt(); 413 } 414 } 415 416 [kOnLine](line) { 417 if (this[kQuestionCallback]) { 418 const cb = this[kQuestionCallback]; 419 this[kQuestionCallback] = null; 420 this.setPrompt(this[kOldPrompt]); 421 cb(line); 422 } else { 423 this.emit('line', line); 424 } 425 } 426 427 [kBeforeEdit](oldText, oldCursor) { 428 this[kPushToUndoStack](oldText, oldCursor); 429 } 430 431 [kQuestionCancel]() { 432 if (this[kQuestionCallback]) { 433 this[kQuestionCallback] = null; 434 this.setPrompt(this[kOldPrompt]); 435 this.clearLine(); 436 } 437 } 438 439 [kWriteToOutput](stringToWrite) { 440 validateString(stringToWrite, 'stringToWrite'); 441 442 if (this.output !== null && this.output !== undefined) { 443 this.output.write(stringToWrite); 444 } 445 } 446 447 [kAddHistory]() { 448 if (this.line.length === 0) return ''; 449 450 // If the history is disabled then return the line 451 if (this.historySize === 0) return this.line; 452 453 // If the trimmed line is empty then return the line 454 if (StringPrototypeTrim(this.line).length === 0) return this.line; 455 456 if (this.history.length === 0 || this.history[0] !== this.line) { 457 if (this.removeHistoryDuplicates) { 458 // Remove older history line if identical to new one 459 const dupIndex = ArrayPrototypeIndexOf(this.history, this.line); 460 if (dupIndex !== -1) ArrayPrototypeSplice(this.history, dupIndex, 1); 461 } 462 463 ArrayPrototypeUnshift(this.history, this.line); 464 465 // Only store so many 466 if (this.history.length > this.historySize) 467 ArrayPrototypePop(this.history); 468 } 469 470 this.historyIndex = -1; 471 472 // The listener could change the history object, possibly 473 // to remove the last added entry if it is sensitive and should 474 // not be persisted in the history, like a password 475 const line = this.history[0]; 476 477 // Emit history event to notify listeners of update 478 this.emit('history', this.history); 479 480 return line; 481 } 482 483 [kRefreshLine]() { 484 // line length 485 const line = this[kPrompt] + this.line; 486 const dispPos = this[kGetDisplayPos](line); 487 const lineCols = dispPos.cols; 488 const lineRows = dispPos.rows; 489 490 // cursor position 491 const cursorPos = this.getCursorPos(); 492 493 // First move to the bottom of the current line, based on cursor pos 494 const prevRows = this.prevRows || 0; 495 if (prevRows > 0) { 496 moveCursor(this.output, 0, -prevRows); 497 } 498 499 // Cursor to left edge. 500 cursorTo(this.output, 0); 501 // erase data 502 clearScreenDown(this.output); 503 504 // Write the prompt and the current buffer content. 505 this[kWriteToOutput](line); 506 507 // Force terminal to allocate a new line 508 if (lineCols === 0) { 509 this[kWriteToOutput](' '); 510 } 511 512 // Move cursor to original position. 513 cursorTo(this.output, cursorPos.cols); 514 515 const diff = lineRows - cursorPos.rows; 516 if (diff > 0) { 517 moveCursor(this.output, 0, -diff); 518 } 519 520 this.prevRows = cursorPos.rows; 521 } 522 523 /** 524 * Closes the `readline.Interface` instance. 525 * @returns {void} 526 */ 527 close() { 528 if (this.closed) return; 529 this.pause(); 530 if (this.terminal) { 531 this[kSetRawMode](false); 532 } 533 this.closed = true; 534 this.emit('close'); 535 } 536 537 /** 538 * Pauses the `input` stream. 539 * @returns {void | Interface} 540 */ 541 pause() { 542 if (this.paused) return; 543 this.input.pause(); 544 this.paused = true; 545 this.emit('pause'); 546 return this; 547 } 548 549 /** 550 * Resumes the `input` stream if paused. 551 * @returns {void | Interface} 552 */ 553 resume() { 554 if (!this.paused) return; 555 this.input.resume(); 556 this.paused = false; 557 this.emit('resume'); 558 return this; 559 } 560 561 /** 562 * Writes either `data` or a `key` sequence identified by 563 * `key` to the `output`. 564 * @param {string} d 565 * @param {{ 566 * ctrl?: boolean; 567 * meta?: boolean; 568 * shift?: boolean; 569 * name?: string; 570 * }} [key] 571 * @returns {void} 572 */ 573 write(d, key) { 574 if (this.paused) this.resume(); 575 if (this.terminal) { 576 this[kTtyWrite](d, key); 577 } else { 578 this[kNormalWrite](d); 579 } 580 } 581 582 [kNormalWrite](b) { 583 if (b === undefined) { 584 return; 585 } 586 let string = this[kDecoder].write(b); 587 if ( 588 this[kSawReturnAt] && 589 DateNow() - this[kSawReturnAt] <= this.crlfDelay 590 ) { 591 if (StringPrototypeCodePointAt(string) === 10) string = StringPrototypeSlice(string, 1); 592 this[kSawReturnAt] = 0; 593 } 594 595 // Run test() on the new string chunk, not on the entire line buffer. 596 let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string); 597 if (newPartContainsEnding !== null) { 598 if (this[kLine_buffer]) { 599 string = this[kLine_buffer] + string; 600 this[kLine_buffer] = null; 601 lineEnding.lastIndex = 0; // Start the search from the beginning of the string. 602 newPartContainsEnding = RegExpPrototypeExec(lineEnding, string); 603 } 604 this[kSawReturnAt] = StringPrototypeEndsWith(string, '\r') ? 605 DateNow() : 606 0; 607 608 const indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex]; 609 let nextMatch; 610 while ((nextMatch = RegExpPrototypeExec(lineEnding, string)) !== null) { 611 ArrayPrototypePush(indexes, nextMatch.index, lineEnding.lastIndex); 612 } 613 const lastIndex = indexes.length - 1; 614 // Either '' or (conceivably) the unfinished portion of the next line 615 this[kLine_buffer] = StringPrototypeSlice(string, indexes[lastIndex]); 616 for (let i = 1; i < lastIndex; i += 2) { 617 this[kOnLine](StringPrototypeSlice(string, indexes[i - 1], indexes[i])); 618 } 619 } else if (string) { 620 // No newlines this time, save what we have for next time 621 if (this[kLine_buffer]) { 622 this[kLine_buffer] += string; 623 } else { 624 this[kLine_buffer] = string; 625 } 626 } 627 } 628 629 [kInsertString](c) { 630 this[kBeforeEdit](this.line, this.cursor); 631 if (this.cursor < this.line.length) { 632 const beg = StringPrototypeSlice(this.line, 0, this.cursor); 633 const end = StringPrototypeSlice( 634 this.line, 635 this.cursor, 636 this.line.length, 637 ); 638 this.line = beg + c + end; 639 this.cursor += c.length; 640 this[kRefreshLine](); 641 } else { 642 const oldPos = this.getCursorPos(); 643 this.line += c; 644 this.cursor += c.length; 645 const newPos = this.getCursorPos(); 646 647 if (oldPos.rows < newPos.rows) { 648 this[kRefreshLine](); 649 } else { 650 this[kWriteToOutput](c); 651 } 652 } 653 } 654 655 async [kTabComplete](lastKeypressWasTab) { 656 this.pause(); 657 const string = StringPrototypeSlice(this.line, 0, this.cursor); 658 let value; 659 try { 660 value = await this.completer(string); 661 } catch (err) { 662 this[kWriteToOutput](`Tab completion error: ${inspect(err)}`); 663 return; 664 } finally { 665 this.resume(); 666 } 667 this[kTabCompleter](lastKeypressWasTab, value); 668 } 669 670 [kTabCompleter](lastKeypressWasTab, { 0: completions, 1: completeOn }) { 671 // Result and the text that was completed. 672 673 if (!completions || completions.length === 0) { 674 return; 675 } 676 677 // If there is a common prefix to all matches, then apply that portion. 678 const prefix = commonPrefix( 679 ArrayPrototypeFilter(completions, (e) => e !== ''), 680 ); 681 if (StringPrototypeStartsWith(prefix, completeOn) && 682 prefix.length > completeOn.length) { 683 this[kInsertString](StringPrototypeSlice(prefix, completeOn.length)); 684 return; 685 } else if (!StringPrototypeStartsWith(completeOn, prefix)) { 686 this.line = StringPrototypeSlice(this.line, 687 0, 688 this.cursor - completeOn.length) + 689 prefix + 690 StringPrototypeSlice(this.line, 691 this.cursor, 692 this.line.length); 693 this.cursor = this.cursor - completeOn.length + prefix.length; 694 this._refreshLine(); 695 return; 696 } 697 698 if (!lastKeypressWasTab) { 699 return; 700 } 701 702 this[kBeforeEdit](this.line, this.cursor); 703 704 // Apply/show completions. 705 const completionsWidth = ArrayPrototypeMap(completions, (e) => 706 getStringWidth(e), 707 ); 708 const width = MathMaxApply(completionsWidth) + 2; // 2 space padding 709 let maxColumns = MathFloor(this.columns / width) || 1; 710 if (maxColumns === Infinity) { 711 maxColumns = 1; 712 } 713 let output = '\r\n'; 714 let lineIndex = 0; 715 let whitespace = 0; 716 for (let i = 0; i < completions.length; i++) { 717 const completion = completions[i]; 718 if (completion === '' || lineIndex === maxColumns) { 719 output += '\r\n'; 720 lineIndex = 0; 721 whitespace = 0; 722 } else { 723 output += StringPrototypeRepeat(' ', whitespace); 724 } 725 if (completion !== '') { 726 output += completion; 727 whitespace = width - completionsWidth[i]; 728 lineIndex++; 729 } else { 730 output += '\r\n'; 731 } 732 } 733 if (lineIndex !== 0) { 734 output += '\r\n\r\n'; 735 } 736 this[kWriteToOutput](output); 737 this[kRefreshLine](); 738 } 739 740 [kWordLeft]() { 741 if (this.cursor > 0) { 742 // Reverse the string and match a word near beginning 743 // to avoid quadratic time complexity 744 const leading = StringPrototypeSlice(this.line, 0, this.cursor); 745 const reversed = ArrayPrototypeJoin( 746 ArrayPrototypeReverse(ArrayFrom(leading)), 747 '', 748 ); 749 const match = RegExpPrototypeExec(/^\s*(?:[^\w\s]+|\w+)?/, reversed); 750 this[kMoveCursor](-match[0].length); 751 } 752 } 753 754 [kWordRight]() { 755 if (this.cursor < this.line.length) { 756 const trailing = StringPrototypeSlice(this.line, this.cursor); 757 const match = RegExpPrototypeExec(/^(?:\s+|[^\w\s]+|\w+)\s*/, trailing); 758 this[kMoveCursor](match[0].length); 759 } 760 } 761 762 [kDeleteLeft]() { 763 if (this.cursor > 0 && this.line.length > 0) { 764 this[kBeforeEdit](this.line, this.cursor); 765 // The number of UTF-16 units comprising the character to the left 766 const charSize = charLengthLeft(this.line, this.cursor); 767 this.line = 768 StringPrototypeSlice(this.line, 0, this.cursor - charSize) + 769 StringPrototypeSlice(this.line, this.cursor, this.line.length); 770 771 this.cursor -= charSize; 772 this[kRefreshLine](); 773 } 774 } 775 776 [kDeleteRight]() { 777 if (this.cursor < this.line.length) { 778 this[kBeforeEdit](this.line, this.cursor); 779 // The number of UTF-16 units comprising the character to the left 780 const charSize = charLengthAt(this.line, this.cursor); 781 this.line = 782 StringPrototypeSlice(this.line, 0, this.cursor) + 783 StringPrototypeSlice( 784 this.line, 785 this.cursor + charSize, 786 this.line.length, 787 ); 788 this[kRefreshLine](); 789 } 790 } 791 792 [kDeleteWordLeft]() { 793 if (this.cursor > 0) { 794 this[kBeforeEdit](this.line, this.cursor); 795 // Reverse the string and match a word near beginning 796 // to avoid quadratic time complexity 797 let leading = StringPrototypeSlice(this.line, 0, this.cursor); 798 const reversed = ArrayPrototypeJoin( 799 ArrayPrototypeReverse(ArrayFrom(leading)), 800 '', 801 ); 802 const match = RegExpPrototypeExec(/^\s*(?:[^\w\s]+|\w+)?/, reversed); 803 leading = StringPrototypeSlice( 804 leading, 805 0, 806 leading.length - match[0].length, 807 ); 808 this.line = 809 leading + 810 StringPrototypeSlice(this.line, this.cursor, this.line.length); 811 this.cursor = leading.length; 812 this[kRefreshLine](); 813 } 814 } 815 816 [kDeleteWordRight]() { 817 if (this.cursor < this.line.length) { 818 this[kBeforeEdit](this.line, this.cursor); 819 const trailing = StringPrototypeSlice(this.line, this.cursor); 820 const match = RegExpPrototypeExec(/^(?:\s+|\W+|\w+)\s*/, trailing); 821 this.line = 822 StringPrototypeSlice(this.line, 0, this.cursor) + 823 StringPrototypeSlice(trailing, match[0].length); 824 this[kRefreshLine](); 825 } 826 } 827 828 [kDeleteLineLeft]() { 829 this[kBeforeEdit](this.line, this.cursor); 830 const del = StringPrototypeSlice(this.line, 0, this.cursor); 831 this.line = StringPrototypeSlice(this.line, this.cursor); 832 this.cursor = 0; 833 this[kPushToKillRing](del); 834 this[kRefreshLine](); 835 } 836 837 [kDeleteLineRight]() { 838 this[kBeforeEdit](this.line, this.cursor); 839 const del = StringPrototypeSlice(this.line, this.cursor); 840 this.line = StringPrototypeSlice(this.line, 0, this.cursor); 841 this[kPushToKillRing](del); 842 this[kRefreshLine](); 843 } 844 845 [kPushToKillRing](del) { 846 if (!del || del === this[kKillRing][0]) return; 847 ArrayPrototypeUnshift(this[kKillRing], del); 848 this[kKillRingCursor] = 0; 849 while (this[kKillRing].length > kMaxLengthOfKillRing) 850 ArrayPrototypePop(this[kKillRing]); 851 } 852 853 [kYank]() { 854 if (this[kKillRing].length > 0) { 855 this[kYanking] = true; 856 this[kInsertString](this[kKillRing][this[kKillRingCursor]]); 857 } 858 } 859 860 [kYankPop]() { 861 if (!this[kYanking]) { 862 return; 863 } 864 if (this[kKillRing].length > 1) { 865 const lastYank = this[kKillRing][this[kKillRingCursor]]; 866 this[kKillRingCursor]++; 867 if (this[kKillRingCursor] >= this[kKillRing].length) { 868 this[kKillRingCursor] = 0; 869 } 870 const currentYank = this[kKillRing][this[kKillRingCursor]]; 871 const head = 872 StringPrototypeSlice(this.line, 0, this.cursor - lastYank.length); 873 const tail = 874 StringPrototypeSlice(this.line, this.cursor); 875 this.line = head + currentYank + tail; 876 this.cursor = head.length + currentYank.length; 877 this[kRefreshLine](); 878 } 879 } 880 881 clearLine() { 882 this[kMoveCursor](+Infinity); 883 this[kWriteToOutput]('\r\n'); 884 this.line = ''; 885 this.cursor = 0; 886 this.prevRows = 0; 887 } 888 889 [kLine]() { 890 const line = this[kAddHistory](); 891 this[kUndoStack] = []; 892 this[kRedoStack] = []; 893 this.clearLine(); 894 this[kOnLine](line); 895 } 896 897 [kPushToUndoStack](text, cursor) { 898 if (ArrayPrototypePush(this[kUndoStack], { text, cursor }) > 899 kMaxUndoRedoStackSize) { 900 ArrayPrototypeShift(this[kUndoStack]); 901 } 902 } 903 904 [kUndo]() { 905 if (this[kUndoStack].length <= 0) return; 906 907 ArrayPrototypePush( 908 this[kRedoStack], 909 { text: this.line, cursor: this.cursor }, 910 ); 911 912 const entry = ArrayPrototypePop(this[kUndoStack]); 913 this.line = entry.text; 914 this.cursor = entry.cursor; 915 916 this[kRefreshLine](); 917 } 918 919 [kRedo]() { 920 if (this[kRedoStack].length <= 0) return; 921 922 ArrayPrototypePush( 923 this[kUndoStack], 924 { text: this.line, cursor: this.cursor }, 925 ); 926 927 const entry = ArrayPrototypePop(this[kRedoStack]); 928 this.line = entry.text; 929 this.cursor = entry.cursor; 930 931 this[kRefreshLine](); 932 } 933 934 // TODO(BridgeAR): Add underscores to the search part and a red background in 935 // case no match is found. This should only be the visual part and not the 936 // actual line content! 937 // TODO(BridgeAR): In case the substring based search is active and the end is 938 // reached, show a comment how to search the history as before. E.g., using 939 // <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first 940 // one. 941 [kHistoryNext]() { 942 if (this.historyIndex >= 0) { 943 this[kBeforeEdit](this.line, this.cursor); 944 const search = this[kSubstringSearch] || ''; 945 let index = this.historyIndex - 1; 946 while ( 947 index >= 0 && 948 (!StringPrototypeStartsWith(this.history[index], search) || 949 this.line === this.history[index]) 950 ) { 951 index--; 952 } 953 if (index === -1) { 954 this.line = search; 955 } else { 956 this.line = this.history[index]; 957 } 958 this.historyIndex = index; 959 this.cursor = this.line.length; // Set cursor to end of line. 960 this[kRefreshLine](); 961 } 962 } 963 964 [kHistoryPrev]() { 965 if (this.historyIndex < this.history.length && this.history.length) { 966 this[kBeforeEdit](this.line, this.cursor); 967 const search = this[kSubstringSearch] || ''; 968 let index = this.historyIndex + 1; 969 while ( 970 index < this.history.length && 971 (!StringPrototypeStartsWith(this.history[index], search) || 972 this.line === this.history[index]) 973 ) { 974 index++; 975 } 976 if (index === this.history.length) { 977 this.line = search; 978 } else { 979 this.line = this.history[index]; 980 } 981 this.historyIndex = index; 982 this.cursor = this.line.length; // Set cursor to end of line. 983 this[kRefreshLine](); 984 } 985 } 986 987 // Returns the last character's display position of the given string 988 [kGetDisplayPos](str) { 989 let offset = 0; 990 const col = this.columns; 991 let rows = 0; 992 str = stripVTControlCharacters(str); 993 for (const char of new SafeStringIterator(str)) { 994 if (char === '\n') { 995 // Rows must be incremented by 1 even if offset = 0 or col = +Infinity. 996 rows += MathCeil(offset / col) || 1; 997 offset = 0; 998 continue; 999 } 1000 // Tabs must be aligned by an offset of the tab size. 1001 if (char === '\t') { 1002 offset += this.tabSize - (offset % this.tabSize); 1003 continue; 1004 } 1005 const width = getStringWidth(char, false /* stripVTControlCharacters */); 1006 if (width === 0 || width === 1) { 1007 offset += width; 1008 } else { 1009 // width === 2 1010 if ((offset + 1) % col === 0) { 1011 offset++; 1012 } 1013 offset += 2; 1014 } 1015 } 1016 const cols = offset % col; 1017 rows += (offset - cols) / col; 1018 return { cols, rows }; 1019 } 1020 1021 /** 1022 * Returns the real position of the cursor in relation 1023 * to the input prompt + string. 1024 * @returns {{ 1025 * rows: number; 1026 * cols: number; 1027 * }} 1028 */ 1029 getCursorPos() { 1030 const strBeforeCursor = 1031 this[kPrompt] + StringPrototypeSlice(this.line, 0, this.cursor); 1032 return this[kGetDisplayPos](strBeforeCursor); 1033 } 1034 1035 // This function moves cursor dx places to the right 1036 // (-dx for left) and refreshes the line if it is needed. 1037 [kMoveCursor](dx) { 1038 if (dx === 0) { 1039 return; 1040 } 1041 const oldPos = this.getCursorPos(); 1042 this.cursor += dx; 1043 1044 // Bounds check 1045 if (this.cursor < 0) { 1046 this.cursor = 0; 1047 } else if (this.cursor > this.line.length) { 1048 this.cursor = this.line.length; 1049 } 1050 1051 const newPos = this.getCursorPos(); 1052 1053 // Check if cursor stayed on the line. 1054 if (oldPos.rows === newPos.rows) { 1055 const diffWidth = newPos.cols - oldPos.cols; 1056 moveCursor(this.output, diffWidth, 0); 1057 } else { 1058 this[kRefreshLine](); 1059 } 1060 } 1061 1062 // Handle a write from the tty 1063 [kTtyWrite](s, key) { 1064 const previousKey = this[kPreviousKey]; 1065 key = key || kEmptyObject; 1066 this[kPreviousKey] = key; 1067 1068 if (!key.meta || key.name !== 'y') { 1069 // Reset yanking state unless we are doing yank pop. 1070 this[kYanking] = false; 1071 } 1072 1073 // Activate or deactivate substring search. 1074 if ( 1075 (key.name === 'up' || key.name === 'down') && 1076 !key.ctrl && 1077 !key.meta && 1078 !key.shift 1079 ) { 1080 if (this[kSubstringSearch] === null) { 1081 this[kSubstringSearch] = StringPrototypeSlice( 1082 this.line, 1083 0, 1084 this.cursor, 1085 ); 1086 } 1087 } else if (this[kSubstringSearch] !== null) { 1088 this[kSubstringSearch] = null; 1089 // Reset the index in case there's no match. 1090 if (this.history.length === this.historyIndex) { 1091 this.historyIndex = -1; 1092 } 1093 } 1094 1095 // Undo & Redo 1096 if (typeof key.sequence === 'string') { 1097 switch (StringPrototypeCodePointAt(key.sequence, 0)) { 1098 case 0x1f: 1099 this[kUndo](); 1100 return; 1101 case 0x1e: 1102 this[kRedo](); 1103 return; 1104 default: 1105 break; 1106 } 1107 } 1108 1109 // Ignore escape key, fixes 1110 // https://github.com/nodejs/node-v0.x-archive/issues/2876. 1111 if (key.name === 'escape') return; 1112 1113 if (key.ctrl && key.shift) { 1114 /* Control and shift pressed */ 1115 switch (key.name) { 1116 // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is 1117 // identical to <ctrl>-h. It should have a unique escape sequence. 1118 case 'backspace': 1119 this[kDeleteLineLeft](); 1120 break; 1121 1122 case 'delete': 1123 this[kDeleteLineRight](); 1124 break; 1125 } 1126 } else if (key.ctrl) { 1127 /* Control key pressed */ 1128 1129 switch (key.name) { 1130 case 'c': 1131 if (this.listenerCount('SIGINT') > 0) { 1132 this.emit('SIGINT'); 1133 } else { 1134 // This readline instance is finished 1135 this.close(); 1136 } 1137 break; 1138 1139 case 'h': // delete left 1140 this[kDeleteLeft](); 1141 break; 1142 1143 case 'd': // delete right or EOF 1144 if (this.cursor === 0 && this.line.length === 0) { 1145 // This readline instance is finished 1146 this.close(); 1147 } else if (this.cursor < this.line.length) { 1148 this[kDeleteRight](); 1149 } 1150 break; 1151 1152 case 'u': // Delete from current to start of line 1153 this[kDeleteLineLeft](); 1154 break; 1155 1156 case 'k': // Delete from current to end of line 1157 this[kDeleteLineRight](); 1158 break; 1159 1160 case 'a': // Go to the start of the line 1161 this[kMoveCursor](-Infinity); 1162 break; 1163 1164 case 'e': // Go to the end of the line 1165 this[kMoveCursor](+Infinity); 1166 break; 1167 1168 case 'b': // back one character 1169 this[kMoveCursor](-charLengthLeft(this.line, this.cursor)); 1170 break; 1171 1172 case 'f': // Forward one character 1173 this[kMoveCursor](+charLengthAt(this.line, this.cursor)); 1174 break; 1175 1176 case 'l': // Clear the whole screen 1177 cursorTo(this.output, 0, 0); 1178 clearScreenDown(this.output); 1179 this[kRefreshLine](); 1180 break; 1181 1182 case 'n': // next history item 1183 this[kHistoryNext](); 1184 break; 1185 1186 case 'p': // Previous history item 1187 this[kHistoryPrev](); 1188 break; 1189 1190 case 'y': // Yank killed string 1191 this[kYank](); 1192 break; 1193 1194 case 'z': 1195 if (process.platform === 'win32') break; 1196 if (this.listenerCount('SIGTSTP') > 0) { 1197 this.emit('SIGTSTP'); 1198 } else { 1199 process.once('SIGCONT', () => { 1200 // Don't raise events if stream has already been abandoned. 1201 if (!this.paused) { 1202 // Stream must be paused and resumed after SIGCONT to catch 1203 // SIGINT, SIGTSTP, and EOF. 1204 this.pause(); 1205 this.emit('SIGCONT'); 1206 } 1207 // Explicitly re-enable "raw mode" and move the cursor to 1208 // the correct position. 1209 // See https://github.com/joyent/node/issues/3295. 1210 this[kSetRawMode](true); 1211 this[kRefreshLine](); 1212 }); 1213 this[kSetRawMode](false); 1214 process.kill(process.pid, 'SIGTSTP'); 1215 } 1216 break; 1217 1218 case 'w': // Delete backwards to a word boundary 1219 // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is 1220 // identical to <ctrl>-h. It should have a unique escape sequence. 1221 // Falls through 1222 case 'backspace': 1223 this[kDeleteWordLeft](); 1224 break; 1225 1226 case 'delete': // Delete forward to a word boundary 1227 this[kDeleteWordRight](); 1228 break; 1229 1230 case 'left': 1231 this[kWordLeft](); 1232 break; 1233 1234 case 'right': 1235 this[kWordRight](); 1236 break; 1237 } 1238 } else if (key.meta) { 1239 /* Meta key pressed */ 1240 1241 switch (key.name) { 1242 case 'b': // backward word 1243 this[kWordLeft](); 1244 break; 1245 1246 case 'f': // forward word 1247 this[kWordRight](); 1248 break; 1249 1250 case 'd': // delete forward word 1251 case 'delete': 1252 this[kDeleteWordRight](); 1253 break; 1254 1255 case 'backspace': // Delete backwards to a word boundary 1256 this[kDeleteWordLeft](); 1257 break; 1258 1259 case 'y': // Doing yank pop 1260 this[kYankPop](); 1261 break; 1262 } 1263 } else { 1264 /* No modifier keys used */ 1265 1266 // \r bookkeeping is only relevant if a \n comes right after. 1267 if (this[kSawReturnAt] && key.name !== 'enter') this[kSawReturnAt] = 0; 1268 1269 switch (key.name) { 1270 case 'return': // Carriage return, i.e. \r 1271 this[kSawReturnAt] = DateNow(); 1272 this[kLine](); 1273 break; 1274 1275 case 'enter': 1276 // When key interval > crlfDelay 1277 if ( 1278 this[kSawReturnAt] === 0 || 1279 DateNow() - this[kSawReturnAt] > this.crlfDelay 1280 ) { 1281 this[kLine](); 1282 } 1283 this[kSawReturnAt] = 0; 1284 break; 1285 1286 case 'backspace': 1287 this[kDeleteLeft](); 1288 break; 1289 1290 case 'delete': 1291 this[kDeleteRight](); 1292 break; 1293 1294 case 'left': 1295 // Obtain the code point to the left 1296 this[kMoveCursor](-charLengthLeft(this.line, this.cursor)); 1297 break; 1298 1299 case 'right': 1300 this[kMoveCursor](+charLengthAt(this.line, this.cursor)); 1301 break; 1302 1303 case 'home': 1304 this[kMoveCursor](-Infinity); 1305 break; 1306 1307 case 'end': 1308 this[kMoveCursor](+Infinity); 1309 break; 1310 1311 case 'up': 1312 this[kHistoryPrev](); 1313 break; 1314 1315 case 'down': 1316 this[kHistoryNext](); 1317 break; 1318 1319 case 'tab': 1320 // If tab completion enabled, do that... 1321 if ( 1322 typeof this.completer === 'function' && 1323 this.isCompletionEnabled 1324 ) { 1325 const lastKeypressWasTab = 1326 previousKey && previousKey.name === 'tab'; 1327 this[kTabComplete](lastKeypressWasTab); 1328 break; 1329 } 1330 // falls through 1331 default: 1332 if (typeof s === 'string' && s) { 1333 // Erase state of previous searches. 1334 lineEnding.lastIndex = 0; 1335 let nextMatch; 1336 // Keep track of the end of the last match. 1337 let lastIndex = 0; 1338 while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) { 1339 this[kInsertString](StringPrototypeSlice(s, lastIndex, nextMatch.index)); 1340 ({ lastIndex } = lineEnding); 1341 this[kLine](); 1342 // Restore lastIndex as the call to kLine could have mutated it. 1343 lineEnding.lastIndex = lastIndex; 1344 } 1345 // This ensures that the last line is written if it doesn't end in a newline. 1346 // Note that the last line may be the first line, in which case this still works. 1347 this[kInsertString](StringPrototypeSlice(s, lastIndex)); 1348 } 1349 } 1350 } 1351 } 1352 1353 /** 1354 * Creates an `AsyncIterator` object that iterates through 1355 * each line in the input stream as a string. 1356 * @typedef {{ 1357 * [Symbol.asyncIterator]: () => InterfaceAsyncIterator, 1358 * next: () => Promise<string> 1359 * }} InterfaceAsyncIterator 1360 * @returns {InterfaceAsyncIterator} 1361 */ 1362 [SymbolAsyncIterator]() { 1363 if (this[kLineObjectStream] === undefined) { 1364 if (Readable === undefined) { 1365 Readable = require('stream').Readable; 1366 } 1367 const readable = new Readable({ 1368 objectMode: true, 1369 read: () => { 1370 this.resume(); 1371 }, 1372 destroy: (err, cb) => { 1373 this.off('line', lineListener); 1374 this.off('close', closeListener); 1375 this.close(); 1376 cb(err); 1377 }, 1378 }); 1379 const lineListener = (input) => { 1380 if (!readable.push(input)) { 1381 // TODO(rexagod): drain to resume flow 1382 this.pause(); 1383 } 1384 }; 1385 const closeListener = () => { 1386 readable.push(null); 1387 }; 1388 const errorListener = (err) => { 1389 readable.destroy(err); 1390 }; 1391 this.on('error', errorListener); 1392 this.on('line', lineListener); 1393 this.on('close', closeListener); 1394 this[kLineObjectStream] = readable; 1395 } 1396 1397 return this[kLineObjectStream][SymbolAsyncIterator](); 1398 } 1399} 1400 1401module.exports = { 1402 Interface, 1403 InterfaceConstructor, 1404 kAddHistory, 1405 kDecoder, 1406 kDeleteLeft, 1407 kDeleteLineLeft, 1408 kDeleteLineRight, 1409 kDeleteRight, 1410 kDeleteWordLeft, 1411 kDeleteWordRight, 1412 kGetDisplayPos, 1413 kHistoryNext, 1414 kHistoryPrev, 1415 kInsertString, 1416 kLine, 1417 kLine_buffer, 1418 kMoveCursor, 1419 kNormalWrite, 1420 kOldPrompt, 1421 kOnLine, 1422 kPreviousKey, 1423 kPrompt, 1424 kQuestionCallback, 1425 kQuestionCancel, 1426 kRefreshLine, 1427 kSawKeyPress, 1428 kSawReturnAt, 1429 kSetRawMode, 1430 kTabComplete, 1431 kTabCompleter, 1432 kTtyWrite, 1433 kWordLeft, 1434 kWordRight, 1435 kWriteToOutput, 1436}; 1437