diff --git a/doc/api/readline.md b/doc/api/readline.md index c01397d57108b6..6a2d8ebbd5b9e1 100644 --- a/doc/api/readline.md +++ b/doc/api/readline.md @@ -358,6 +358,10 @@ added: v0.1.98 only if `terminal` is set to `true` by the user or by an internal `output` check, otherwise the history caching mechanism is not initialized at all. * `prompt` - the prompt string to use. Default: `'> '` + * `crlfDelay` {number} If the delay between `\r` and `\n` exceeds + `crlfDelay` milliseconds, both `\r` and `\n` will be treated as separate + end-of-line input. Default to `100` milliseconds. + `crlfDelay` will be coerced to `[100, 2000]` range. The `readline.createInterface()` method creates a new `readline.Interface` instance. diff --git a/lib/readline.js b/lib/readline.js index f7591b7cc1663b..be21f4f7d07b9e 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -7,6 +7,8 @@ 'use strict'; const kHistorySize = 30; +const kMincrlfDelay = 100; +const kMaxcrlfDelay = 2000; const util = require('util'); const debug = util.debuglog('readline'); @@ -39,12 +41,13 @@ function Interface(input, output, completer, terminal) { return self; } - this._sawReturn = false; + this._sawReturnAt = 0; this.isCompletionEnabled = true; this._previousKey = null; EventEmitter.call(this); var historySize; + let crlfDelay; let prompt = '> '; if (arguments.length === 1) { @@ -56,6 +59,7 @@ function Interface(input, output, completer, terminal) { if (input.prompt !== undefined) { prompt = input.prompt; } + crlfDelay = input.crlfDelay; input = input.input; } @@ -84,6 +88,8 @@ function Interface(input, output, completer, terminal) { this.output = output; this.input = input; this.historySize = historySize; + this.crlfDelay = Math.max(kMincrlfDelay, + Math.min(kMaxcrlfDelay, crlfDelay >>> 0)); // Check arity, 2 - for async, 1 for sync if (typeof completer === 'function') { @@ -341,9 +347,10 @@ Interface.prototype._normalWrite = function(b) { return; } var string = this._decoder.write(b); - if (this._sawReturn) { + if (this._sawReturnAt && + Date.now() - this._sawReturnAt <= this.crlfDelay) { string = string.replace(/^\n/, ''); - this._sawReturn = false; + this._sawReturnAt = 0; } // Run test() on the new string chunk, not on the entire line buffer. @@ -354,7 +361,7 @@ Interface.prototype._normalWrite = function(b) { this._line_buffer = null; } if (newPartContainsEnding) { - this._sawReturn = string.endsWith('\r'); + this._sawReturnAt = string.endsWith('\r') ? Date.now() : 0; // got one or more newlines; process into "line" events var lines = string.split(lineEnding); @@ -842,20 +849,22 @@ Interface.prototype._ttyWrite = function(s, key) { /* No modifier keys used */ // \r bookkeeping is only relevant if a \n comes right after. - if (this._sawReturn && key.name !== 'enter') - this._sawReturn = false; + if (this._sawReturnAt && key.name !== 'enter') + this._sawReturnAt = 0; switch (key.name) { case 'return': // carriage return, i.e. \r - this._sawReturn = true; + this._sawReturnAt = Date.now(); this._line(); break; case 'enter': - if (this._sawReturn) - this._sawReturn = false; - else + // When key interval > crlfDelay + if (this._sawReturnAt === 0 || + Date.now() - this._sawReturnAt > this.crlfDelay) { this._line(); + } + this._sawReturnAt = 0; break; case 'backspace': diff --git a/test/parallel/test-readline-interface.js b/test/parallel/test-readline-interface.js index 4a4b2f896961e8..08dbdd488265ee 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -26,6 +26,34 @@ function isWarned(emitter) { return false; } +{ + // Default crlfDelay is 100ms + const fi = new FakeInput(); + const rli = new readline.Interface({ input: fi, output: fi }); + assert.strictEqual(rli.crlfDelay, 100); + rli.close(); +} + +{ + // Minimum crlfDelay is 100ms + const fi = new FakeInput(); + const rli = new readline.Interface({ input: fi, output: fi, crlfDelay: 0}); + assert.strictEqual(rli.crlfDelay, 100); + rli.close(); +} + +{ + // Maximum crlfDelay is 2000ms + const fi = new FakeInput(); + const rli = new readline.Interface({ + input: fi, + output: fi, + crlfDelay: 1 << 30 + }); + assert.strictEqual(rli.crlfDelay, 2000); + rli.close(); +} + [ true, false ].forEach(function(terminal) { var fi; var rli; @@ -199,6 +227,29 @@ function isWarned(emitter) { assert.equal(callCount, expectedLines.length); rli.close(); + // Emit two line events when the delay + // between \r and \n exceeds crlfDelay + { + const fi = new FakeInput(); + const delay = 200; + const rli = new readline.Interface({ + input: fi, + output: fi, + terminal: terminal, + crlfDelay: delay + }); + let callCount = 0; + rli.on('line', function(line) { + callCount++; + }); + fi.emit('data', '\r'); + setTimeout(common.mustCall(() => { + fi.emit('data', '\n'); + assert.equal(callCount, 2); + rli.close(); + }), delay * 2); + } + // \t when there is no completer function should behave like an ordinary // character fi = new FakeInput();