| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 925dd8e commit 09ca8be
7 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -21,9 +21,11 @@ result. Input and output may be from `stdin` and `stdout`, respectively, or may | |||
| 21 | 21 | be connected to any Node.js [stream][]. | |
| 22 | 22 | ||
| 23 | 23 | Instances of [`repl.REPLServer`][] support automatic completion of inputs, | |
| 24 | - simplistic Emacs-style line editing, multi-line inputs, ANSI-styled output, | ||
| 25 | - saving and restoring current REPL session state, error recovery, and | ||
| 26 | - customizable evaluation functions. | ||
| 24 | + completion preview, simplistic Emacs-style line editing, multi-line inputs, | ||
| 25 | + [ZSH][] like reverse-i-search, ANSI-styled output, saving and restoring current | ||
| 26 | + REPL session state, error recovery, and customizable evaluation functions. | ||
| 27 | + Terminals that do not support ANSI-styles and Emacs-style line editing | ||
| 28 | + automatically fall back to a limited feature set. | ||
| 27 | 29 | ||
| 28 | 30 | ### Commands and Special Keys | |
| 29 | 31 | ||
@@ -232,6 +234,24 @@ undefined | |||
| 232 | 234 | undefined | |
| 233 | 235 | ``` | |
| 234 | 236 | ||
| 237 | + ### Reverse-i-search | ||
| 238 | + <!-- YAML | ||
| 239 | + added: REPLACEME | ||
| 240 | + --> | ||
| 241 | + | ||
| 242 | + The REPL supports bi-directional reverse-i-search similar to [ZSH][]. It is | ||
| 243 | + triggered with `<ctrl> + R` to search backwards and `<ctrl> + S` to search | ||
| 244 | + forwards. | ||
| 245 | + | ||
| 246 | + Duplicated history entires will be skipped. | ||
| 247 | + | ||
| 248 | + Entries are accepted as soon as any button is pressed that doesn't correspond | ||
| 249 | + with the reverse search. Cancelling is possible by pressing `escape` or | ||
| 250 | + `<ctrl> + C`. | ||
| 251 | + | ||
| 252 | + Changing the direction immediately searches for the next entry in the expected | ||
| 253 | + direction from the current position on. | ||
| 254 | + | ||
| 235 | 255 | ### Custom Evaluation Functions | |
| 236 | 256 | ||
| 237 | 257 | When a new [`repl.REPLServer`][] is created, a custom evaluation function may be | |
@@ -695,6 +715,7 @@ a `net.Server` and `net.Socket` instance, see: | |||
| 695 | 715 | For an example of running a REPL instance over [curl(1)][], see: | |
| 696 | 716 | <https://gist.github.com/TooTallNate/2053342>. | |
| 697 | 717 | ||
| 718 | + [ZSH]: https://en.wikipedia.org/wiki/Z_shell | ||
| 698 | 719 | [`'uncaughtException'`]: process.html#process_event_uncaughtexception | |
| 699 | 720 | [`--experimental-repl-await`]: cli.html#cli_experimental_repl_await | |
| 700 | 721 | [`ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE`]: errors.html#errors_err_domain_cannot_set_uncaught_exception_capture | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -24,6 +24,7 @@ const { | |||
| 24 | 24 | ||
| 25 | 25 | const { | |
| 26 | 26 | clearLine, | |
| 27 | + clearScreenDown, | ||
| 27 | 28 | cursorTo, | |
| 28 | 29 | moveCursor, | |
| 29 | 30 | } = require('readline'); | |
@@ -42,7 +43,13 @@ const inspectOptions = { | |||
| 42 | 43 | compact: true, | |
| 43 | 44 | breakLength: Infinity | |
| 44 | 45 | }; | |
| 45 | - const inspectedOptions = inspect(inspectOptions, { colors: false }); | ||
| 46 | + // Specify options that might change the output in a way that it's not a valid | ||
| 47 | + // stringified object anymore. | ||
| 48 | + const inspectedOptions = inspect(inspectOptions, { | ||
| 49 | + depth: 1, | ||
| 50 | + colors: false, | ||
| 51 | + showHidden: false | ||
| 52 | + }); | ||
| 46 | 53 | ||
| 47 | 54 | // If the error is that we've unexpectedly ended the input, | |
| 48 | 55 | // then let the user try to recover by adding more input. | |
@@ -393,8 +400,242 @@ function setupPreview(repl, contextSymbol, bufferSymbol, active) { | |||
| 393 | 400 | return { showPreview, clearPreview }; | |
| 394 | 401 | } | |
| 395 | 402 | ||
| 403 | + function setupReverseSearch(repl) { | ||
| 404 | + // Simple terminals can't use reverse search. | ||
| 405 | + if (process.env.TERM === 'dumb') { | ||
| 406 | + return { reverseSearch() { return false; } }; | ||
| 407 | + } | ||
| 408 | + | ||
| 409 | + const alreadyMatched = new Set(); | ||
| 410 | + const labels = { | ||
| 411 | + r: 'bck-i-search: ', | ||
| 412 | + s: 'fwd-i-search: ' | ||
| 413 | + }; | ||
| 414 | + let isInReverseSearch = false; | ||
| 415 | + let historyIndex = -1; | ||
| 416 | + let input = ''; | ||
| 417 | + let cursor = -1; | ||
| 418 | + let dir = 'r'; | ||
| 419 | + let lastMatch = -1; | ||
| 420 | + let lastCursor = -1; | ||
| 421 | + let promptPos; | ||
| 422 | + | ||
| 423 | + function checkAndSetDirectionKey(keyName) { | ||
| 424 | + if (!labels[keyName]) { | ||
| 425 | + return false; | ||
| 426 | + } | ||
| 427 | + if (dir !== keyName) { | ||
| 428 | + // Reset the already matched set in case the direction is changed. That | ||
| 429 | + // way it's possible to find those entries again. | ||
| 430 | + alreadyMatched.clear(); | ||
| 431 | + } | ||
| 432 | + dir = keyName; | ||
| 433 | + return true; | ||
| 434 | + } | ||
| 435 | + | ||
| 436 | + function goToNextHistoryIndex() { | ||
| 437 | + // Ignore this entry for further searches and continue to the next | ||
| 438 | + // history entry. | ||
| 439 | + alreadyMatched.add(repl.history[historyIndex]); | ||
| 440 | + historyIndex += dir === 'r' ? 1 : -1; | ||
| 441 | + cursor = -1; | ||
| 442 | + } | ||
| 443 | + | ||
| 444 | + function search() { | ||
| 445 | + // Just print an empty line in case the user removed the search parameter. | ||
| 446 | + if (input === '') { | ||
| 447 | + print(repl.line, `${labels[dir]}_`); | ||
| 448 | + return; | ||
| 449 | + } | ||
| 450 | + // Fix the bounds in case the direction has changed in the meanwhile. | ||
| 451 | + if (dir === 'r') { | ||
| 452 | + if (historyIndex < 0) { | ||
| 453 | + historyIndex = 0; | ||
| 454 | + } | ||
| 455 | + } else if (historyIndex >= repl.history.length) { | ||
| 456 | + historyIndex = repl.history.length - 1; | ||
| 457 | + } | ||
| 458 | + // Check the history entries until a match is found. | ||
| 459 | + while (historyIndex >= 0 && historyIndex < repl.history.length) { | ||
| 460 | + let entry = repl.history[historyIndex]; | ||
| 461 | + // Visualize all potential matches only once. | ||
| 462 | + if (alreadyMatched.has(entry)) { | ||
| 463 | + historyIndex += dir === 'r' ? 1 : -1; | ||
| 464 | + continue; | ||
| 465 | + } | ||
| 466 | + // Match the next entry either from the start or from the end, depending | ||
| 467 | + // on the current direction. | ||
| 468 | + if (dir === 'r') { | ||
| 469 | + // Update the cursor in case it's necessary. | ||
| 470 | + if (cursor === -1) { | ||
| 471 | + cursor = entry.length; | ||
| 472 | + } | ||
| 473 | + cursor = entry.lastIndexOf(input, cursor - 1); | ||
| 474 | + } else { | ||
| 475 | + cursor = entry.indexOf(input, cursor + 1); | ||
| 476 | + } | ||
| 477 | + // Match not found. | ||
| 478 | + if (cursor === -1) { | ||
| 479 | + goToNextHistoryIndex(); | ||
| 480 | + // Match found. | ||
| 481 | + } else { | ||
| 482 | + if (repl.useColors) { | ||
| 483 | + const start = entry.slice(0, cursor); | ||
| 484 | + const end = entry.slice(cursor + input.length); | ||
| 485 | + entry = `${start}\x1B[4m${input}\x1B[24m${end}`; | ||
| 486 | + } | ||
| 487 | + print(entry, `${labels[dir]}${input}_`, cursor); | ||
| 488 | + lastMatch = historyIndex; | ||
| 489 | + lastCursor = cursor; | ||
| 490 | + // Explicitly go to the next history item in case no further matches are | ||
| 491 | + // possible with the current entry. | ||
| 492 | + if ((dir === 'r' && cursor === 0) || | ||
| 493 | + (dir === 's' && entry.length === cursor + input.length)) { | ||
| 494 | + goToNextHistoryIndex(); | ||
| 495 | + } | ||
| 496 | + return; | ||
| 497 | + } | ||
| 498 | + } | ||
| 499 | + print(repl.line, `failed-${labels[dir]}${input}_`); | ||
| 500 | + } | ||
| 501 | + | ||
| 502 | + function print(outputLine, inputLine, cursor = repl.cursor) { | ||
| 503 | + // TODO(BridgeAR): Resizing the terminal window hides the overlay. To fix | ||
| 504 | + // that, readline must be aware of this information. It's probably best to | ||
| 505 | + // add a couple of properties to readline that allow to do the following: | ||
| 506 | + // 1. Add arbitrary data to the end of the current line while not counting | ||
| 507 | + // towards the line. This would be useful for the completion previews. | ||
| 508 | + // 2. Add arbitrary extra lines that do not count towards the regular line. | ||
| 509 | + // This would be useful for both, the input preview and the reverse | ||
| 510 | + // search. It might be combined with the first part? | ||
| 511 | + // 3. Add arbitrary input that is "on top" of the current line. That is | ||
| 512 | + // useful for the reverse search. | ||
| 513 | + // 4. To trigger the line refresh, functions should be used to pass through | ||
| 514 | + // the information. Alternatively, getters and setters could be used. | ||
| 515 | + // That might even be more elegant. | ||
| 516 | + // The data would then be accounted for when calling `_refreshLine()`. | ||
| 517 | + // This function would then look similar to: | ||
| 518 | + // repl.overlay(outputLine); | ||
| 519 | + // repl.addTrailingLine(inputLine); | ||
| 520 | + // repl.setCursor(cursor); | ||
| 521 | + // More potential improvements: use something similar to stream.cork(). | ||
| 522 | + // Multiple cursor moves on the same tick could be prevented in case all | ||
| 523 | + // writes from the same tick are combined and the cursor is moved at the | ||
| 524 | + // tick end instead of after each operation. | ||
| 525 | + let rows = 0; | ||
| 526 | + if (lastMatch !== -1) { | ||
| 527 | + const line = repl.history[lastMatch].slice(0, lastCursor); | ||
| 528 | + rows = repl._getDisplayPos(`${repl._prompt}${line}`).rows; | ||
| 529 | + cursorTo(repl.output, promptPos.cols); | ||
| 530 | + } else if (isInReverseSearch && repl.line !== '') { | ||
| 531 | + rows = repl._getCursorPos().rows; | ||
| 532 | + cursorTo(repl.output, promptPos.cols); | ||
| 533 | + } | ||
| 534 | + if (rows !== 0) | ||
| 535 | + moveCursor(repl.output, 0, -rows); | ||
| 536 | + | ||
| 537 | + if (isInReverseSearch) { | ||
| 538 | + clearScreenDown(repl.output); | ||
| 539 | + repl.output.write(`${outputLine}\n${inputLine}`); | ||
| 540 | + } else { | ||
| 541 | + repl.output.write(`\n${inputLine}`); | ||
| 542 | + } | ||
| 543 | + | ||
| 544 | + lastMatch = -1; | ||
| 545 | + | ||
| 546 | + // To know exactly how many rows we have to move the cursor back we need the | ||
| 547 | + // cursor rows, the output rows and the input rows. | ||
| 548 | + const prompt = repl._prompt; | ||
| 549 | + const cursorLine = `${prompt}${outputLine.slice(0, cursor)}`; | ||
| 550 | + const cursorPos = repl._getDisplayPos(cursorLine); | ||
| 551 | + const outputPos = repl._getDisplayPos(`${prompt}${outputLine}`); | ||
| 552 | + const inputPos = repl._getDisplayPos(inputLine); | ||
| 553 | + const inputRows = inputPos.rows - (inputPos.cols === 0 ? 1 : 0); | ||
| 554 | + | ||
| 555 | + rows = -1 - inputRows - (outputPos.rows - cursorPos.rows); | ||
| 556 | + | ||
| 557 | + moveCursor(repl.output, 0, rows); | ||
| 558 | + cursorTo(repl.output, cursorPos.cols); | ||
| 559 | + } | ||
| 560 | + | ||
| 561 | + function reset(string) { | ||
| 562 | + isInReverseSearch = string !== undefined; | ||
| 563 | + | ||
| 564 | + // In case the reverse search ends and a history entry is found, reset the | ||
| 565 | + // line to the found entry. | ||
| 566 | + if (!isInReverseSearch) { | ||
| 567 | + if (lastMatch !== -1) { | ||
| 568 | + repl.line = repl.history[lastMatch]; | ||
| 569 | + repl.cursor = lastCursor; | ||
| 570 | + repl.historyIndex = lastMatch; | ||
| 571 | + } | ||
| 572 | + | ||
| 573 | + lastMatch = -1; | ||
| 574 | + | ||
| 575 | + // Clear screen and write the current repl.line before exiting. | ||
| 576 | + cursorTo(repl.output, promptPos.cols); | ||
| 577 | + if (promptPos.rows !== 0) | ||
| 578 | + moveCursor(repl.output, 0, promptPos.rows); | ||
| 579 | + clearScreenDown(repl.output); | ||
| 580 | + if (repl.line !== '') { | ||
| 581 | + repl.output.write(repl.line); | ||
| 582 | + if (repl.line.length !== repl.cursor) { | ||
| 583 | + const { cols, rows } = repl._getCursorPos(); | ||
| 584 | + cursorTo(repl.output, cols); | ||
| 585 | + if (rows !== 0) | ||
| 586 | + moveCursor(repl.output, 0, rows); | ||
| 587 | + } | ||
| 588 | + } | ||
| 589 | + } | ||
| 590 | + | ||
| 591 | + input = string || ''; | ||
| 592 | + cursor = -1; | ||
| 593 | + historyIndex = repl.historyIndex; | ||
| 594 | + alreadyMatched.clear(); | ||
| 595 | + } | ||
| 596 | + | ||
| 597 | + function reverseSearch(string, key) { | ||
| 598 | + if (!isInReverseSearch) { | ||
| 599 | + if (key.ctrl && checkAndSetDirectionKey(key.name)) { | ||
| 600 | + historyIndex = repl.historyIndex; | ||
| 601 | + promptPos = repl._getDisplayPos(`${repl._prompt}`); | ||
| 602 | + print(repl.line, `${labels[dir]}_`); | ||
| 603 | + isInReverseSearch = true; | ||
| 604 | + } | ||
| 605 | + } else if (key.ctrl && checkAndSetDirectionKey(key.name)) { | ||
| 606 | + search(); | ||
| 607 | + } else if (key.name === 'backspace' || | ||
| 608 | + (key.ctrl && (key.name === 'h' || key.name === 'w'))) { | ||
| 609 | + reset(input.slice(0, input.length - 1)); | ||
| 610 | + search(); | ||
| 611 | + // Special handle <ctrl> + c and escape. Those should only cancel the | ||
| 612 | + // reverse search. The original line is visible afterwards again. | ||
| 613 | + } else if ((key.ctrl && key.name === 'c') || key.name === 'escape') { | ||
| 614 | + lastMatch = -1; | ||
| 615 | + reset(); | ||
| 616 | + return true; | ||
| 617 | + // End search in case either enter is pressed or if any non-reverse-search | ||
| 618 | + // key (combination) is pressed. | ||
| 619 | + } else if (key.ctrl || | ||
| 620 | + key.meta || | ||
| 621 | + key.name === 'return' || | ||
| 622 | + key.name === 'enter' || | ||
| 623 | + typeof string !== 'string' || | ||
| 624 | + string === '') { | ||
| 625 | + reset(); | ||
| 626 | + } else { | ||
| 627 | + reset(`${input}${string}`); | ||
| 628 | + search(); | ||
| 629 | + } | ||
| 630 | + return isInReverseSearch; | ||
| 631 | + } | ||
| 632 | + | ||
| 633 | + return { reverseSearch }; | ||
| 634 | + } | ||
| 635 | + | ||
| 396 | 636 | module.exports = { | |
| 397 | 637 | isRecoverableError, | |
| 398 | 638 | kStandaloneREPL: Symbol('kStandaloneREPL'), | |
| 399 | - setupPreview | ||
| 639 | + setupPreview, | ||
| 640 | + setupReverseSearch | ||
| 400 | 641 | }; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -102,6 +102,7 @@ const { | |||
| 102 | 102 | isRecoverableError, | |
| 103 | 103 | kStandaloneREPL, | |
| 104 | 104 | setupPreview, | |
| 105 | + setupReverseSearch, | ||
| 105 | 106 | } = require('internal/repl/utils'); | |
| 106 | 107 | const { | |
| 107 | 108 | getOwnNonIndexProperties, | |
@@ -810,6 +811,8 @@ function REPLServer(prompt, | |||
| 810 | 811 | } | |
| 811 | 812 | }); | |
| 812 | 813 | ||
| 814 | + const { reverseSearch } = setupReverseSearch(this); | ||
| 815 | + | ||
| 813 | 816 | const { | |
| 814 | 817 | clearPreview, | |
| 815 | 818 | showPreview | |
@@ -835,8 +838,10 @@ function REPLServer(prompt, | |||
| 835 | 838 | self.clearLine(); | |
| 836 | 839 | } | |
| 837 | 840 | clearPreview(); | |
| 838 | - ttyWrite(d, key); | ||
| 839 | - showPreview(); | ||
| 841 | + if (!reverseSearch(d, key)) { | ||
| 842 | + ttyWrite(d, key); | ||
| 843 | + showPreview(); | ||
| 844 | + } | ||
| 840 | 845 | return; | |
| 841 | 846 | } | |
| 842 | 847 | ||
@@ -1081,6 +1086,9 @@ REPLServer.prototype.complete = function() { | |||
| 1081 | 1086 | this.completer.apply(this, arguments); | |
| 1082 | 1087 | }; | |
| 1083 | 1088 | ||
| 1089 | + // TODO: Native module names should be auto-resolved. | ||
| 1090 | + // That improves the auto completion. | ||
| 1091 | + | ||
| 1084 | 1092 | // Provide a list of completions for the given leading text. This is | |
| 1085 | 1093 | // given to the readline interface for handling tab completion. | |
| 1086 | 1094 | // | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -340,7 +340,7 @@ function runTest() { | |||
| 340 | 340 | const output = chunk.toString(); | |
| 341 | 341 | ||
| 342 | 342 | if (!opts.showEscapeCodes && | |
| 343 | - output.charCodeAt(0) === 27 || /^[\r\n]+$/.test(output)) { | ||
| 343 | + (output[0] === '\x1B' || /^[\r\n]+$/.test(output))) { | ||
| 344 | 344 | return next(); | |
| 345 | 345 | } | |
| 346 | 346 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments