• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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