readline: show completions only after 2nd TAB

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 <rtrott@gmail.com>
Reviewed-By: Trevor Norris <trev.norris@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Anna Henningsen 2016-07-15 23:33:16 +02:00
parent c809b88345
commit 1a9e247c79
No known key found for this signature in database
GPG Key ID: D8B9F5AEAE84E4CF
2 changed files with 25 additions and 18 deletions

View File

@ -41,6 +41,7 @@ function Interface(input, output, completer, terminal) {
this._sawReturn = false; this._sawReturn = false;
this.isCompletionEnabled = true; this.isCompletionEnabled = true;
this._previousKey = null;
EventEmitter.call(this); EventEmitter.call(this);
var historySize; var historySize;
@ -391,7 +392,7 @@ Interface.prototype._insertString = function(c) {
} }
}; };
Interface.prototype._tabComplete = function() { Interface.prototype._tabComplete = function(lastKeypressWasTab) {
var self = this; var self = this;
self.pause(); self.pause();
@ -407,9 +408,7 @@ Interface.prototype._tabComplete = function() {
const completeOn = rv[1]; // the text that was completed const completeOn = rv[1]; // the text that was completed
if (completions && completions.length) { if (completions && completions.length) {
// Apply/show completions. // Apply/show completions.
if (completions.length === 1) { if (lastKeypressWasTab) {
self._insertString(completions[0].slice(completeOn.length));
} else {
self._writeToOutput('\r\n'); self._writeToOutput('\r\n');
var width = completions.reduce(function(a, b) { var width = completions.reduce(function(a, b) {
return a.length > b.length ? a : b; return a.length > b.length ? a : b;
@ -429,16 +428,15 @@ Interface.prototype._tabComplete = function() {
} }
} }
handleGroup(self, group, width, maxColumns); handleGroup(self, group, width, maxColumns);
}
// If there is a common prefix to all matches, then apply that // If there is a common prefix to all matches, then apply that portion.
// portion. const f = completions.filter(function(e) { if (e) return e; });
var f = completions.filter(function(e) { if (e) return e; }); const prefix = commonPrefix(f);
var prefix = commonPrefix(f);
if (prefix.length > completeOn.length) { if (prefix.length > completeOn.length) {
self._insertString(prefix.slice(completeOn.length)); self._insertString(prefix.slice(completeOn.length));
} }
}
self._refreshLine(); self._refreshLine();
} }
}); });
@ -474,6 +472,7 @@ function commonPrefix(strings) {
if (!strings || strings.length == 0) { if (!strings || strings.length == 0) {
return ''; return '';
} }
if (strings.length === 1) return strings[0];
var sorted = strings.slice().sort(); var sorted = strings.slice().sort();
var min = sorted[0]; var min = sorted[0];
var max = sorted[sorted.length - 1]; var max = sorted[sorted.length - 1];
@ -688,7 +687,9 @@ Interface.prototype._moveCursor = function(dx) {
// handle a write from the tty // handle a write from the tty
Interface.prototype._ttyWrite = function(s, key) { Interface.prototype._ttyWrite = function(s, key) {
const previousKey = this._previousKey;
key = key || {}; key = key || {};
this._previousKey = key;
// Ignore escape key - Fixes #2876 // Ignore escape key - Fixes #2876
if (key.name == 'escape') return; if (key.name == 'escape') return;
@ -892,7 +893,8 @@ Interface.prototype._ttyWrite = function(s, key) {
case 'tab': case 'tab':
// If tab completion enabled, do that... // If tab completion enabled, do that...
if (typeof this.completer === 'function' && this.isCompletionEnabled) { if (typeof this.completer === 'function' && this.isCompletionEnabled) {
this._tabComplete(); const lastKeypressWasTab = previousKey && previousKey.name === 'tab';
this._tabComplete(lastKeypressWasTab);
break; break;
} }
// falls through // falls through

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
require('../common'); const common = require('../common');
const assert = require('assert'); const assert = require('assert');
const PassThrough = require('stream').PassThrough; const PassThrough = require('stream').PassThrough;
const readline = require('readline'); const readline = require('readline');
@ -26,12 +26,17 @@ oStream.on('data', function(data) {
output += data; output += data;
}); });
oStream.on('end', function() { oStream.on('end', common.mustCall(() => {
const expect = 'process.stdout\r\n' + const expect = 'process.stdout\r\n' +
'process.stdin\r\n' + 'process.stdin\r\n' +
'process.stderr'; 'process.stderr';
assert(new RegExp(expect).test(output)); assert(new RegExp(expect).test(output));
}); }));
iStream.write('process.std\t'); iStream.write('process.s\t');
assert(/process.std\b/.test(output)); // Completion works.
assert(!/stdout/.test(output)); // Completion doesnt show all results yet.
iStream.write('\t');
oStream.end(); oStream.end();