• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeFilter,
5  ArrayPrototypeIncludes,
6  ArrayPrototypeMap,
7  Boolean,
8  FunctionPrototypeBind,
9  MathMin,
10  RegExpPrototypeExec,
11  SafeSet,
12  SafeStringIterator,
13  StringPrototypeEndsWith,
14  StringPrototypeIndexOf,
15  StringPrototypeLastIndexOf,
16  StringPrototypeReplaceAll,
17  StringPrototypeSlice,
18  StringPrototypeStartsWith,
19  StringPrototypeToLowerCase,
20  StringPrototypeTrim,
21  Symbol,
22} = primordials;
23
24const { tokTypes: tt, Parser: AcornParser } =
25  require('internal/deps/acorn/acorn/dist/acorn');
26
27const { sendInspectorCommand } = require('internal/util/inspector');
28
29const {
30  ERR_INSPECTOR_NOT_AVAILABLE,
31} = require('internal/errors').codes;
32
33const {
34  clearLine,
35  clearScreenDown,
36  cursorTo,
37  moveCursor,
38} = require('internal/readline/callbacks');
39
40const {
41  commonPrefix,
42  kSubstringSearch,
43} = require('internal/readline/utils');
44
45const {
46  getStringWidth,
47  inspect,
48} = require('internal/util/inspect');
49
50let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
51  debug = fn;
52});
53
54const previewOptions = {
55  colors: false,
56  depth: 1,
57  showHidden: false,
58};
59
60const REPL_MODE_STRICT = Symbol('repl-strict');
61
62// If the error is that we've unexpectedly ended the input,
63// then let the user try to recover by adding more input.
64// Note: `e` (the original exception) is not used by the current implementation,
65// but may be needed in the future.
66function isRecoverableError(e, code) {
67  // For similar reasons as `defaultEval`, wrap expressions starting with a
68  // curly brace with parenthesis.  Note: only the open parenthesis is added
69  // here as the point is to test for potentially valid but incomplete
70  // expressions.
71  if (RegExpPrototypeExec(/^\s*\{/, code) !== null &&
72      isRecoverableError(e, `(${code}`))
73    return true;
74
75  let recoverable = false;
76
77  // Determine if the point of any error raised is at the end of the input.
78  // There are two cases to consider:
79  //
80  //   1.  Any error raised after we have encountered the 'eof' token.
81  //       This prevents us from declaring partial tokens (like '2e') as
82  //       recoverable.
83  //
84  //   2.  Three cases where tokens can legally span lines.  This is
85  //       template, comment, and strings with a backslash at the end of
86  //       the line, indicating a continuation.  Note that we need to look
87  //       for the specific errors of 'unterminated' kind (not, for example,
88  //       a syntax error in a ${} expression in a template), and the only
89  //       way to do that currently is to look at the message.  Should Acorn
90  //       change these messages in the future, this will lead to a test
91  //       failure, indicating that this code needs to be updated.
92  //
93  const RecoverableParser = AcornParser
94    .extend(
95      (Parser) => {
96        return class extends Parser {
97          // eslint-disable-next-line no-useless-constructor
98          constructor(options, input, startPos) {
99            super(options, input, startPos);
100          }
101          nextToken() {
102            super.nextToken();
103            if (this.type === tt.eof)
104              recoverable = true;
105          }
106          raise(pos, message) {
107            switch (message) {
108              case 'Unterminated template':
109              case 'Unterminated comment':
110                recoverable = true;
111                break;
112
113              case 'Unterminated string constant': {
114                const token = StringPrototypeSlice(this.input,
115                                                   this.lastTokStart, this.pos);
116                // See https://www.ecma-international.org/ecma-262/#sec-line-terminators
117                if (RegExpPrototypeExec(/\\(?:\r\n?|\n|\u2028|\u2029)$/,
118                                        token) !== null) {
119                  recoverable = true;
120                }
121              }
122            }
123            super.raise(pos, message);
124          }
125        };
126      },
127    );
128
129  // Try to parse the code with acorn.  If the parse fails, ignore the acorn
130  // error and return the recoverable status.
131  try {
132    RecoverableParser.parse(code, { ecmaVersion: 'latest' });
133
134    // Odd case: the underlying JS engine (V8, Chakra) rejected this input
135    // but Acorn detected no issue.  Presume that additional text won't
136    // address this issue.
137    return false;
138  } catch {
139    return recoverable;
140  }
141}
142
143function setupPreview(repl, contextSymbol, bufferSymbol, active) {
144  // Simple terminals can't handle previews.
145  if (process.env.TERM === 'dumb' || !active) {
146    return { showPreview() {}, clearPreview() {} };
147  }
148
149  let inputPreview = null;
150
151  let previewCompletionCounter = 0;
152  let completionPreview = null;
153
154  let hasCompletions = false;
155
156  let wrapped = false;
157
158  let escaped = null;
159
160  function getPreviewPos() {
161    const displayPos = repl._getDisplayPos(`${repl.getPrompt()}${repl.line}`);
162    const cursorPos = repl.line.length !== repl.cursor ?
163      repl.getCursorPos() :
164      displayPos;
165    return { displayPos, cursorPos };
166  }
167
168  function isCursorAtInputEnd() {
169    const { cursorPos, displayPos } = getPreviewPos();
170    return cursorPos.rows === displayPos.rows &&
171           cursorPos.cols === displayPos.cols;
172  }
173
174  const clearPreview = (key) => {
175    if (inputPreview !== null) {
176      const { displayPos, cursorPos } = getPreviewPos();
177      const rows = displayPos.rows - cursorPos.rows + 1;
178      moveCursor(repl.output, 0, rows);
179      clearLine(repl.output);
180      moveCursor(repl.output, 0, -rows);
181      inputPreview = null;
182    }
183    if (completionPreview !== null) {
184      // Prevent cursor moves if not necessary!
185      const move = repl.line.length !== repl.cursor;
186      let pos, rows;
187      if (move) {
188        pos = getPreviewPos();
189        cursorTo(repl.output, pos.displayPos.cols);
190        rows = pos.displayPos.rows - pos.cursorPos.rows;
191        moveCursor(repl.output, 0, rows);
192      }
193      const totalLine = `${repl.getPrompt()}${repl.line}${completionPreview}`;
194      const newPos = repl._getDisplayPos(totalLine);
195      // Minimize work for the terminal. It is enough to clear the right part of
196      // the current line in case the preview is visible on a single line.
197      if (newPos.rows === 0 || (pos && pos.displayPos.rows === newPos.rows)) {
198        clearLine(repl.output, 1);
199      } else {
200        clearScreenDown(repl.output);
201      }
202      if (move) {
203        cursorTo(repl.output, pos.cursorPos.cols);
204        moveCursor(repl.output, 0, -rows);
205      }
206      if (!key.ctrl && !key.shift) {
207        if (key.name === 'escape') {
208          if (escaped === null && key.meta) {
209            escaped = repl.line;
210          }
211        } else if ((key.name === 'return' || key.name === 'enter') &&
212                   !key.meta &&
213                   escaped !== repl.line &&
214                   isCursorAtInputEnd()) {
215          repl._insertString(completionPreview);
216        }
217      }
218      completionPreview = null;
219    }
220    if (escaped !== repl.line) {
221      escaped = null;
222    }
223  };
224
225  function showCompletionPreview(line, insertPreview) {
226    previewCompletionCounter++;
227
228    const count = previewCompletionCounter;
229
230    repl.completer(line, (error, data) => {
231      // Tab completion might be async and the result might already be outdated.
232      if (count !== previewCompletionCounter) {
233        return;
234      }
235
236      if (error) {
237        debug('Error while generating completion preview', error);
238        return;
239      }
240
241      // Result and the text that was completed.
242      const { 0: rawCompletions, 1: completeOn } = data;
243
244      if (!rawCompletions || rawCompletions.length === 0) {
245        return;
246      }
247
248      hasCompletions = true;
249
250      // If there is a common prefix to all matches, then apply that portion.
251      const completions = ArrayPrototypeFilter(rawCompletions, Boolean);
252      const prefix = commonPrefix(completions);
253
254      // No common prefix found.
255      if (prefix.length <= completeOn.length) {
256        return;
257      }
258
259      const suffix = StringPrototypeSlice(prefix, completeOn.length);
260
261      if (insertPreview) {
262        repl._insertString(suffix);
263        return;
264      }
265
266      completionPreview = suffix;
267
268      const result = repl.useColors ?
269        `\u001b[90m${suffix}\u001b[39m` :
270        ` // ${suffix}`;
271
272      const { cursorPos, displayPos } = getPreviewPos();
273      if (repl.line.length !== repl.cursor) {
274        cursorTo(repl.output, displayPos.cols);
275        moveCursor(repl.output, 0, displayPos.rows - cursorPos.rows);
276      }
277      repl.output.write(result);
278      cursorTo(repl.output, cursorPos.cols);
279      const totalLine = `${repl.getPrompt()}${repl.line}${suffix}`;
280      const newPos = repl._getDisplayPos(totalLine);
281      const rows = newPos.rows - cursorPos.rows - (newPos.cols === 0 ? 1 : 0);
282      moveCursor(repl.output, 0, -rows);
283    });
284  }
285
286  function isInStrictMode(repl) {
287    return repl.replMode === REPL_MODE_STRICT || ArrayPrototypeIncludes(
288      ArrayPrototypeMap(process.execArgv,
289                        (e) => StringPrototypeReplaceAll(
290                          StringPrototypeToLowerCase(e),
291                          '_',
292                          '-',
293                        )),
294      '--use-strict');
295  }
296
297  // This returns a code preview for arbitrary input code.
298  function getInputPreview(input, callback) {
299    // For similar reasons as `defaultEval`, wrap expressions starting with a
300    // curly brace with parenthesis.
301    if (StringPrototypeStartsWith(input, '{') &&
302        !StringPrototypeEndsWith(input, ';') && !wrapped) {
303      input = `(${input})`;
304      wrapped = true;
305    }
306    sendInspectorCommand((session) => {
307      session.post('Runtime.evaluate', {
308        expression: input,
309        throwOnSideEffect: true,
310        timeout: 333,
311        contextId: repl[contextSymbol],
312      }, (error, preview) => {
313        if (error) {
314          callback(error);
315          return;
316        }
317        const { result } = preview;
318        if (result.value !== undefined) {
319          callback(null, inspect(result.value, previewOptions));
320        // Ignore EvalErrors, SyntaxErrors and ReferenceErrors. It is not clear
321        // where they came from and if they are recoverable or not. Other errors
322        // may be inspected.
323        } else if (preview.exceptionDetails &&
324                   (result.className === 'EvalError' ||
325                    result.className === 'SyntaxError' ||
326                    // Report ReferenceError in case the strict mode is active
327                    // for input that has no completions.
328                    (result.className === 'ReferenceError' &&
329                     (hasCompletions || !isInStrictMode(repl))))) {
330          callback(null, null);
331        } else if (result.objectId) {
332          // The writer options might change and have influence on the inspect
333          // output. The user might change e.g., `showProxy`, `getters` or
334          // `showHidden`. Use `inspect` instead of `JSON.stringify` to keep
335          // `Infinity` and similar intact.
336          const inspectOptions = inspect({
337            ...repl.writer.options,
338            colors: false,
339            depth: 1,
340            compact: true,
341            breakLength: Infinity,
342          }, previewOptions);
343          session.post('Runtime.callFunctionOn', {
344            functionDeclaration:
345              `(v) =>
346                    Reflect
347                    .getOwnPropertyDescriptor(globalThis, 'util')
348                    .get().inspect(v, ${inspectOptions})`,
349            objectId: result.objectId,
350            arguments: [result],
351          }, (error, preview) => {
352            if (error) {
353              callback(error);
354            } else {
355              callback(null, preview.result.value);
356            }
357          });
358        } else {
359          // Either not serializable or undefined.
360          callback(null, result.unserializableValue || result.type);
361        }
362      });
363    }, () => callback(new ERR_INSPECTOR_NOT_AVAILABLE()));
364  }
365
366  const showPreview = (showCompletion = true) => {
367    // Prevent duplicated previews after a refresh.
368    if (inputPreview !== null || !repl.isCompletionEnabled) {
369      return;
370    }
371
372    const line = StringPrototypeTrim(repl.line);
373
374    // Do not preview in case the line only contains whitespace.
375    if (line === '') {
376      return;
377    }
378
379    hasCompletions = false;
380
381    // Add the autocompletion preview.
382    if (showCompletion) {
383      const insertPreview = false;
384      showCompletionPreview(repl.line, insertPreview);
385    }
386
387    // Do not preview if the command is buffered.
388    if (repl[bufferSymbol]) {
389      return;
390    }
391
392    const inputPreviewCallback = (error, inspected) => {
393      if (inspected == null) {
394        return;
395      }
396
397      wrapped = false;
398
399      // Ignore the output if the value is identical to the current line.
400      if (line === inspected) {
401        return;
402      }
403
404      if (error) {
405        debug('Error while generating preview', error);
406        return;
407      }
408      // Do not preview `undefined` if colors are deactivated or explicitly
409      // requested.
410      if (inspected === 'undefined' &&
411          (!repl.useColors || repl.ignoreUndefined)) {
412        return;
413      }
414
415      inputPreview = inspected;
416
417      // Limit the output to maximum 250 characters. Otherwise it becomes a)
418      // difficult to read and b) non terminal REPLs would visualize the whole
419      // output.
420      let maxColumns = MathMin(repl.columns, 250);
421
422      // Support unicode characters of width other than one by checking the
423      // actual width.
424      if (inspected.length * 2 >= maxColumns &&
425          getStringWidth(inspected) > maxColumns) {
426        maxColumns -= 4 + (repl.useColors ? 0 : 3);
427        let res = '';
428        for (const char of new SafeStringIterator(inspected)) {
429          maxColumns -= getStringWidth(char);
430          if (maxColumns < 0)
431            break;
432          res += char;
433        }
434        inspected = `${res}...`;
435      }
436
437      // Line breaks are very rare and probably only occur in case of error
438      // messages with line breaks.
439      const lineBreakPos = StringPrototypeIndexOf(inspected, '\n');
440      if (lineBreakPos !== -1) {
441        inspected = `${StringPrototypeSlice(inspected, 0, lineBreakPos)}`;
442      }
443
444      const result = repl.useColors ?
445        `\u001b[90m${inspected}\u001b[39m` :
446        `// ${inspected}`;
447
448      const { cursorPos, displayPos } = getPreviewPos();
449      const rows = displayPos.rows - cursorPos.rows;
450      moveCursor(repl.output, 0, rows);
451      repl.output.write(`\n${result}`);
452      cursorTo(repl.output, cursorPos.cols);
453      moveCursor(repl.output, 0, -rows - 1);
454    };
455
456    let previewLine = line;
457
458    if (completionPreview !== null &&
459        isCursorAtInputEnd() &&
460        escaped !== repl.line) {
461      previewLine += completionPreview;
462    }
463
464    getInputPreview(previewLine, inputPreviewCallback);
465    if (wrapped) {
466      getInputPreview(previewLine, inputPreviewCallback);
467    }
468    wrapped = false;
469  };
470
471  // -------------------------------------------------------------------------//
472  // Replace multiple interface functions. This is required to fully support  //
473  // previews without changing readlines behavior.                            //
474  // -------------------------------------------------------------------------//
475
476  // Refresh prints the whole screen again and the preview will be removed
477  // during that procedure. Print the preview again. This also makes sure
478  // the preview is always correct after resizing the terminal window.
479  const originalRefresh = FunctionPrototypeBind(repl._refreshLine, repl);
480  repl._refreshLine = () => {
481    inputPreview = null;
482    originalRefresh();
483    showPreview();
484  };
485
486  let insertCompletionPreview = true;
487  // Insert the longest common suffix of the current input in case the user
488  // moves to the right while already being at the current input end.
489  const originalMoveCursor = FunctionPrototypeBind(repl._moveCursor, repl);
490  repl._moveCursor = (dx) => {
491    const currentCursor = repl.cursor;
492    originalMoveCursor(dx);
493    if (currentCursor + dx > repl.line.length &&
494        typeof repl.completer === 'function' &&
495        insertCompletionPreview) {
496      const insertPreview = true;
497      showCompletionPreview(repl.line, insertPreview);
498    }
499  };
500
501  // This is the only function that interferes with the completion insertion.
502  // Monkey patch it to prevent inserting the completion when it shouldn't be.
503  const originalClearLine = FunctionPrototypeBind(repl.clearLine, repl);
504  repl.clearLine = () => {
505    insertCompletionPreview = false;
506    originalClearLine();
507    insertCompletionPreview = true;
508  };
509
510  return { showPreview, clearPreview };
511}
512
513function setupReverseSearch(repl) {
514  // Simple terminals can't use reverse search.
515  if (process.env.TERM === 'dumb') {
516    return { reverseSearch() { return false; } };
517  }
518
519  const alreadyMatched = new SafeSet();
520  const labels = {
521    r: 'bck-i-search: ',
522    s: 'fwd-i-search: ',
523  };
524  let isInReverseSearch = false;
525  let historyIndex = -1;
526  let input = '';
527  let cursor = -1;
528  let dir = 'r';
529  let lastMatch = -1;
530  let lastCursor = -1;
531  let promptPos;
532
533  function checkAndSetDirectionKey(keyName) {
534    if (!labels[keyName]) {
535      return false;
536    }
537    if (dir !== keyName) {
538      // Reset the already matched set in case the direction is changed. That
539      // way it's possible to find those entries again.
540      alreadyMatched.clear();
541      dir = keyName;
542    }
543    return true;
544  }
545
546  function goToNextHistoryIndex() {
547    // Ignore this entry for further searches and continue to the next
548    // history entry.
549    alreadyMatched.add(repl.history[historyIndex]);
550    historyIndex += dir === 'r' ? 1 : -1;
551    cursor = -1;
552  }
553
554  function search() {
555    // Just print an empty line in case the user removed the search parameter.
556    if (input === '') {
557      print(repl.line, `${labels[dir]}_`);
558      return;
559    }
560    // Fix the bounds in case the direction has changed in the meanwhile.
561    if (dir === 'r') {
562      if (historyIndex < 0) {
563        historyIndex = 0;
564      }
565    } else if (historyIndex >= repl.history.length) {
566      historyIndex = repl.history.length - 1;
567    }
568    // Check the history entries until a match is found.
569    while (historyIndex >= 0 && historyIndex < repl.history.length) {
570      let entry = repl.history[historyIndex];
571      // Visualize all potential matches only once.
572      if (alreadyMatched.has(entry)) {
573        historyIndex += dir === 'r' ? 1 : -1;
574        continue;
575      }
576      // Match the next entry either from the start or from the end, depending
577      // on the current direction.
578      if (dir === 'r') {
579        // Update the cursor in case it's necessary.
580        if (cursor === -1) {
581          cursor = entry.length;
582        }
583        cursor = StringPrototypeLastIndexOf(entry, input, cursor - 1);
584      } else {
585        cursor = StringPrototypeIndexOf(entry, input, cursor + 1);
586      }
587      // Match not found.
588      if (cursor === -1) {
589        goToNextHistoryIndex();
590      // Match found.
591      } else {
592        if (repl.useColors) {
593          const start = StringPrototypeSlice(entry, 0, cursor);
594          const end = StringPrototypeSlice(entry, cursor + input.length);
595          entry = `${start}\x1B[4m${input}\x1B[24m${end}`;
596        }
597        print(entry, `${labels[dir]}${input}_`, cursor);
598        lastMatch = historyIndex;
599        lastCursor = cursor;
600        // Explicitly go to the next history item in case no further matches are
601        // possible with the current entry.
602        if ((dir === 'r' && cursor === 0) ||
603            (dir === 's' && entry.length === cursor + input.length)) {
604          goToNextHistoryIndex();
605        }
606        return;
607      }
608    }
609    print(repl.line, `failed-${labels[dir]}${input}_`);
610  }
611
612  function print(outputLine, inputLine, cursor = repl.cursor) {
613    // TODO(BridgeAR): Resizing the terminal window hides the overlay. To fix
614    // that, readline must be aware of this information. It's probably best to
615    // add a couple of properties to readline that allow to do the following:
616    // 1. Add arbitrary data to the end of the current line while not counting
617    //    towards the line. This would be useful for the completion previews.
618    // 2. Add arbitrary extra lines that do not count towards the regular line.
619    //    This would be useful for both, the input preview and the reverse
620    //    search. It might be combined with the first part?
621    // 3. Add arbitrary input that is "on top" of the current line. That is
622    //    useful for the reverse search.
623    // 4. To trigger the line refresh, functions should be used to pass through
624    //    the information. Alternatively, getters and setters could be used.
625    //    That might even be more elegant.
626    // The data would then be accounted for when calling `_refreshLine()`.
627    // This function would then look similar to:
628    //   repl.overlay(outputLine);
629    //   repl.addTrailingLine(inputLine);
630    //   repl.setCursor(cursor);
631    // More potential improvements: use something similar to stream.cork().
632    // Multiple cursor moves on the same tick could be prevented in case all
633    // writes from the same tick are combined and the cursor is moved at the
634    // tick end instead of after each operation.
635    let rows = 0;
636    if (lastMatch !== -1) {
637      const line = StringPrototypeSlice(repl.history[lastMatch], 0, lastCursor);
638      rows = repl._getDisplayPos(`${repl.getPrompt()}${line}`).rows;
639      cursorTo(repl.output, promptPos.cols);
640    } else if (isInReverseSearch && repl.line !== '') {
641      rows = repl.getCursorPos().rows;
642      cursorTo(repl.output, promptPos.cols);
643    }
644    if (rows !== 0)
645      moveCursor(repl.output, 0, -rows);
646
647    if (isInReverseSearch) {
648      clearScreenDown(repl.output);
649      repl.output.write(`${outputLine}\n${inputLine}`);
650    } else {
651      repl.output.write(`\n${inputLine}`);
652    }
653
654    lastMatch = -1;
655
656    // To know exactly how many rows we have to move the cursor back we need the
657    // cursor rows, the output rows and the input rows.
658    const prompt = repl.getPrompt();
659    const cursorLine = prompt + StringPrototypeSlice(outputLine, 0, cursor);
660    const cursorPos = repl._getDisplayPos(cursorLine);
661    const outputPos = repl._getDisplayPos(`${prompt}${outputLine}`);
662    const inputPos = repl._getDisplayPos(inputLine);
663    const inputRows = inputPos.rows - (inputPos.cols === 0 ? 1 : 0);
664
665    rows = -1 - inputRows - (outputPos.rows - cursorPos.rows);
666
667    moveCursor(repl.output, 0, rows);
668    cursorTo(repl.output, cursorPos.cols);
669  }
670
671  function reset(string) {
672    isInReverseSearch = string !== undefined;
673
674    // In case the reverse search ends and a history entry is found, reset the
675    // line to the found entry.
676    if (!isInReverseSearch) {
677      if (lastMatch !== -1) {
678        repl.line = repl.history[lastMatch];
679        repl.cursor = lastCursor;
680        repl.historyIndex = lastMatch;
681      }
682
683      lastMatch = -1;
684
685      // Clear screen and write the current repl.line before exiting.
686      cursorTo(repl.output, promptPos.cols);
687      moveCursor(repl.output, 0, promptPos.rows);
688      clearScreenDown(repl.output);
689      if (repl.line !== '') {
690        repl.output.write(repl.line);
691        if (repl.line.length !== repl.cursor) {
692          const { cols, rows } = repl.getCursorPos();
693          cursorTo(repl.output, cols);
694          moveCursor(repl.output, 0, rows);
695        }
696      }
697    }
698
699    input = string || '';
700    cursor = -1;
701    historyIndex = repl.historyIndex;
702    alreadyMatched.clear();
703  }
704
705  function reverseSearch(string, key) {
706    if (!isInReverseSearch) {
707      if (key.ctrl && checkAndSetDirectionKey(key.name)) {
708        historyIndex = repl.historyIndex;
709        promptPos = repl._getDisplayPos(`${repl.getPrompt()}`);
710        print(repl.line, `${labels[dir]}_`);
711        isInReverseSearch = true;
712      }
713    } else if (key.ctrl && checkAndSetDirectionKey(key.name)) {
714      search();
715    } else if (key.name === 'backspace' ||
716        (key.ctrl && (key.name === 'h' || key.name === 'w'))) {
717      reset(StringPrototypeSlice(input, 0, input.length - 1));
718      search();
719      // Special handle <ctrl> + c and escape. Those should only cancel the
720      // reverse search. The original line is visible afterwards again.
721    } else if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
722      lastMatch = -1;
723      reset();
724      return true;
725      // End search in case either enter is pressed or if any non-reverse-search
726      // key (combination) is pressed.
727    } else if (key.ctrl ||
728               key.meta ||
729               key.name === 'return' ||
730               key.name === 'enter' ||
731               typeof string !== 'string' ||
732               string === '') {
733      reset();
734      repl[kSubstringSearch] = '';
735    } else {
736      reset(`${input}${string}`);
737      search();
738    }
739    return isInReverseSearch;
740  }
741
742  return { reverseSearch };
743}
744
745module.exports = {
746  REPL_MODE_SLOPPY: Symbol('repl-sloppy'),
747  REPL_MODE_STRICT,
748  isRecoverableError,
749  kStandaloneREPL: Symbol('kStandaloneREPL'),
750  setupPreview,
751  setupReverseSearch,
752};
753