diff --git a/lib/internal/readline.js b/lib/internal/readline.js index 1cc0b58d6756a2..ce22fb9ffb33d8 100644 --- a/lib/internal/readline.js +++ b/lib/internal/readline.js @@ -376,11 +376,15 @@ function* emitKeys(stream) { key.name = ch.toLowerCase(); key.shift = /^[A-Z]$/.test(ch); key.meta = escaped; + } else if (escaped) { + // Escape sequence timeout + key.name = ch.length ? undefined : 'escape'; + key.meta = true; } key.sequence = s; - if (key.name !== undefined) { + if (s.length !== 0 && (key.name !== undefined || escaped)) { /* Named character or sequence */ stream.emit('keypress', escaped ? undefined : s, key); } else if (s.length === 1) { diff --git a/lib/readline.js b/lib/readline.js index 9d34bb740dbb04..f7591b7cc1663b 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -927,6 +927,9 @@ exports.Interface = Interface; const KEYPRESS_DECODER = Symbol('keypress-decoder'); const ESCAPE_DECODER = Symbol('escape-decoder'); +// GNU readline library - keyseq-timeout is 500ms (default) +const ESCAPE_CODE_TIMEOUT = 500; + function emitKeypressEvents(stream, iface) { if (stream[KEYPRESS_DECODER]) return; var StringDecoder = require('string_decoder').StringDecoder; // lazy load @@ -935,10 +938,15 @@ function emitKeypressEvents(stream, iface) { stream[ESCAPE_DECODER] = emitKeys(stream); stream[ESCAPE_DECODER].next(); + const escapeCodeTimeout = () => stream[ESCAPE_DECODER].next(''); + let timeoutId; + function onData(b) { if (stream.listenerCount('keypress') > 0) { var r = stream[KEYPRESS_DECODER].write(b); if (r) { + clearTimeout(timeoutId); + for (var i = 0; i < r.length; i++) { if (r[i] === '\t' && typeof r[i + 1] === 'string' && iface) { iface.isCompletionEnabled = false; @@ -946,6 +954,10 @@ function emitKeypressEvents(stream, iface) { try { stream[ESCAPE_DECODER].next(r[i]); + // Escape letter at the tail position + if (r[i] === '\x1b' && i + 1 === r.length) { + timeoutId = setTimeout(escapeCodeTimeout, ESCAPE_CODE_TIMEOUT); + } } catch (err) { // if the generator throws (it could happen in the `keypress` // event), we need to restart it. diff --git a/test/parallel/test-readline-keys.js b/test/parallel/test-readline-keys.js index ef9e2eba9090c9..6bbbb2e918470d 100644 --- a/test/parallel/test-readline-keys.js +++ b/test/parallel/test-readline-keys.js @@ -44,6 +44,49 @@ function addTest(sequences, expectedKeys) { assert.deepStrictEqual(keys, expectedKeys); } +// Simulate key interval test cases +// Returns a function that takes `next` test case and returns a thunk +// that can be called to run tests in sequence +// e.g. +// addKeyIntervalTest(..) +// (addKeyIntervalTest(..) +// (addKeyIntervalTest(..)(noop)))() +// where noop is a terminal function(() => {}). + +const addKeyIntervalTest = (sequences, expectedKeys, interval = 550, + assertDelay = 550) => { + return (next) => () => { + + if (!Array.isArray(sequences)) { + sequences = [ sequences ]; + } + + if (!Array.isArray(expectedKeys)) { + expectedKeys = [ expectedKeys ]; + } + + expectedKeys = expectedKeys.map(function(k) { + return k ? extend({ ctrl: false, meta: false, shift: false }, k) : k; + }); + + const keys = []; + fi.on('keypress', (s, k) => keys.push(k)); + + const emitKeys = ([head, ...tail]) => { + if (head) { + fi.write(head); + setTimeout(() => emitKeys(tail), interval); + } else { + setTimeout(() => { + next(); + assert.deepStrictEqual(keys, expectedKeys); + }, assertDelay); + } + }; + emitKeys(sequences); + }; +}; + // regular alphanumerics addTest('io.JS', [ { name: 'i', sequence: 'i' }, @@ -149,3 +192,22 @@ addTest('\x1b[31ma\x1b[39ma', [ { name: 'undefined', sequence: '\x1b[39m', code: '[39m' }, { name: 'a', sequence: 'a' }, ]); + +// Reduce array of addKeyIntervalTest(..) right to left +// with () => {} as initial function +const runKeyIntervalTests = [ + // escape character + addKeyIntervalTest('\x1b', [ + { name: 'escape', sequence: '\x1b', meta: true } + ]), + // chain of escape characters + addKeyIntervalTest('\x1b\x1b\x1b\x1b'.split(''), [ + { name: 'escape', sequence: '\x1b', meta: true }, + { name: 'escape', sequence: '\x1b', meta: true }, + { name: 'escape', sequence: '\x1b', meta: true }, + { name: 'escape', sequence: '\x1b', meta: true } + ]) +].reverse().reduce((acc, fn) => fn(acc), () => {}); + +// run key interval tests one after another +runKeyIntervalTests();