diff --git a/doc/api/readline.md b/doc/api/readline.md index 2ffe08a31bc943..c9f8d88ac0f360 100644 --- a/doc/api/readline.md +++ b/doc/api/readline.md @@ -361,6 +361,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 84b83ebdb1b01c..19d35604843fa1 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'); @@ -40,12 +42,13 @@ function Interface(input, output, completer, terminal) { return self; } - this._sawReturn = false; + this._sawReturnAt = 0; this.isCompletionEnabled = true; this._sawKeyPress = false; EventEmitter.call(this); var historySize; + let crlfDelay; let prompt = '> '; if (arguments.length === 1) { @@ -57,6 +60,7 @@ function Interface(input, output, completer, terminal) { if (input.prompt !== undefined) { prompt = input.prompt; } + crlfDelay = input.crlfDelay; input = input.input; } @@ -85,6 +89,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') { @@ -345,9 +351,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. @@ -358,7 +365,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); @@ -846,20 +853,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 aa0d6925abb7f1..e5ab1415002b3e 100644 --- a/test/parallel/test-readline-interface.js +++ b/test/parallel/test-readline-interface.js @@ -24,6 +24,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; @@ -197,6 +225,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();