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