Skip to content

Commit

Permalink
readline: show completions only after 2nd TAB
Browse files Browse the repository at this point in the history
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: #7665
PR-URL: #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>
  • Loading branch information
addaleax committed Aug 4, 2016
1 parent c809b88 commit 1a9e247
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 17 deletions.
28 changes: 15 additions & 13 deletions lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function Interface(input, output, completer, terminal) {

this._sawReturn = false;
this.isCompletionEnabled = true;
this._previousKey = null;

EventEmitter.call(this);
var historySize;
Expand Down Expand Up @@ -391,7 +392,7 @@ Interface.prototype._insertString = function(c) {
}
};

Interface.prototype._tabComplete = function() {
Interface.prototype._tabComplete = function(lastKeypressWasTab) {
var self = this;

self.pause();
Expand All @@ -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;
Expand All @@ -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();
}
});
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions test/parallel/test-readline-undefined-columns.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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();

0 comments on commit 1a9e247

Please sign in to comment.