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