From 1a9e247c794274942683d2e77db098c9494ddde5 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 15 Jul 2016 23:33:16 +0200 Subject: [PATCH] readline: show completions only after 2nd TAB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show `TAB` completion suggestions only after the user has pressed `TAB` twice in a row, so that the full list of suggestions doesn’t present a distraction. The first time a `TAB` key is pressed, only partial longest-common-prefix completion is performed. This moves the `readline` autocompletion a lot closer to what e.g. `bash` does. Fixes: https://github.com/nodejs/node/issues/7665 PR-URL: https://github.com/nodejs/node/pull/7754 Reviewed-By: Rich Trott Reviewed-By: Trevor Norris Reviewed-By: Ben Noordhuis Reviewed-By: James M Snell --- lib/readline.js | 28 ++++++++++--------- .../test-readline-undefined-columns.js | 13 ++++++--- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index 402923e925..9d34bb740d 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -41,6 +41,7 @@ function Interface(input, output, completer, terminal) { this._sawReturn = false; this.isCompletionEnabled = true; + this._previousKey = null; EventEmitter.call(this); var historySize; @@ -391,7 +392,7 @@ Interface.prototype._insertString = function(c) { } }; -Interface.prototype._tabComplete = function() { +Interface.prototype._tabComplete = function(lastKeypressWasTab) { var self = this; self.pause(); @@ -407,9 +408,7 @@ Interface.prototype._tabComplete = function() { const completeOn = rv[1]; // the text that was completed if (completions && completions.length) { // Apply/show completions. - if (completions.length === 1) { - self._insertString(completions[0].slice(completeOn.length)); - } else { + if (lastKeypressWasTab) { self._writeToOutput('\r\n'); var width = completions.reduce(function(a, b) { return a.length > b.length ? a : b; @@ -429,16 +428,15 @@ Interface.prototype._tabComplete = function() { } } handleGroup(self, group, width, maxColumns); + } - // If there is a common prefix to all matches, then apply that - // portion. - var f = completions.filter(function(e) { if (e) return e; }); - var prefix = commonPrefix(f); - if (prefix.length > completeOn.length) { - self._insertString(prefix.slice(completeOn.length)); - } - + // If there is a common prefix to all matches, then apply that portion. + const f = completions.filter(function(e) { if (e) return e; }); + const prefix = commonPrefix(f); + if (prefix.length > completeOn.length) { + self._insertString(prefix.slice(completeOn.length)); } + self._refreshLine(); } }); @@ -474,6 +472,7 @@ function commonPrefix(strings) { if (!strings || strings.length == 0) { return ''; } + if (strings.length === 1) return strings[0]; var sorted = strings.slice().sort(); var min = sorted[0]; var max = sorted[sorted.length - 1]; @@ -688,7 +687,9 @@ Interface.prototype._moveCursor = function(dx) { // handle a write from the tty Interface.prototype._ttyWrite = function(s, key) { + const previousKey = this._previousKey; key = key || {}; + this._previousKey = key; // Ignore escape key - Fixes #2876 if (key.name == 'escape') return; @@ -892,7 +893,8 @@ Interface.prototype._ttyWrite = function(s, key) { case 'tab': // If tab completion enabled, do that... if (typeof this.completer === 'function' && this.isCompletionEnabled) { - this._tabComplete(); + const lastKeypressWasTab = previousKey && previousKey.name === 'tab'; + this._tabComplete(lastKeypressWasTab); break; } // falls through diff --git a/test/parallel/test-readline-undefined-columns.js b/test/parallel/test-readline-undefined-columns.js index f3197ff59a..5f8a1c02d3 100644 --- a/test/parallel/test-readline-undefined-columns.js +++ b/test/parallel/test-readline-undefined-columns.js @@ -1,6 +1,6 @@ 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const PassThrough = require('stream').PassThrough; const readline = require('readline'); @@ -26,12 +26,17 @@ oStream.on('data', function(data) { output += data; }); -oStream.on('end', function() { +oStream.on('end', common.mustCall(() => { const expect = 'process.stdout\r\n' + 'process.stdin\r\n' + 'process.stderr'; assert(new RegExp(expect).test(output)); -}); +})); + +iStream.write('process.s\t'); + +assert(/process.std\b/.test(output)); // Completion works. +assert(!/stdout/.test(output)); // Completion doesn’t show all results yet. -iStream.write('process.std\t'); +iStream.write('\t'); oStream.end();