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