Browse Source

First pass at tab-completion in the REPL

v0.7.4-release
Trent Mick 14 years ago
committed by Ryan Dahl
parent
commit
1c9a85b9a6
  1. 71
      lib/readline.js
  2. 172
      lib/repl.js

71
lib/readline.js

@ -16,12 +16,13 @@ var stdio = process.binding('stdio');
exports.createInterface = function (output) { exports.createInterface = function (output, completer) {
return new Interface(output); return new Interface(output, completer);
}; };
function Interface (output) { function Interface (output, completer) {
this.output = output; this.output = output;
this.completer = completer;
this.setPrompt("node> "); this.setPrompt("node> ");
@ -126,6 +127,48 @@ Interface.prototype._normalWrite = function (b) {
this.emit('line', b.toString()); this.emit('line', b.toString());
}; };
Interface.prototype._insertString = function (c) {
//BUG: Problem when adding tabs with following content.
// Perhaps the bug is in _refreshLine(). Not sure.
// A hack would be to insert spaces instead of literal '\t'.
if (this.cursor < this.line.length) {
var beg = this.line.slice(0, this.cursor);
var end = this.line.slice(this.cursor, this.line.length);
this.line = beg + c + end;
this.cursor += c.length;
this._refreshLine();
} else {
this.line += c;
this.cursor += c.length;
this.output.write(c);
}
};
Interface.prototype._tabComplete = function () {
var self = this;
var rv = this.completer(self.line.slice(0, self.cursor));
var completions = rv[0],
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));
self._refreshLine();
} else {
//TODO: Multi-column display. Request to show if more than N completions.
self.output.write("\n");
completions.forEach(function (c) {
//TODO: try using '\r\n' instead of the following goop for getting to column 0
self.output.write('\x1b[0G');
self.output.write(c + "\n");
})
self.output.write('\n');
self._refreshLine();
}
}
};
Interface.prototype._historyNext = function () { Interface.prototype._historyNext = function () {
if (this.historyIndex > 0) { if (this.historyIndex > 0) {
this.historyIndex--; this.historyIndex--;
@ -228,6 +271,12 @@ Interface.prototype._ttyWrite = function (b) {
this._historyNext(); this._historyNext();
break; break;
case 9: // tab, completion
if (this.completer) {
this._tabComplete();
}
break;
case 16: // control-p, previous history item case 16: // control-p, previous history item
this._historyPrev(); this._historyPrev();
break; break;
@ -270,16 +319,12 @@ Interface.prototype._ttyWrite = function (b) {
default: default:
var c = b.toString('utf8'); var c = b.toString('utf8');
if (this.cursor < this.line.length) { var lines = c.split(/\r\n|\n|\r/);
var beg = this.line.slice(0, this.cursor); for (var i = 0; i < lines.length; i++) {
var end = this.line.slice(this.cursor, this.line.length); if (i > 0) {
this.line = beg + c + end; this._ttyWrite(new Buffer([13]));
this.cursor += c.length; }
this._refreshLine(); this._insertString(lines[i]);
} else {
this.line += c;
this.cursor += c.length;
this.output.write(c);
} }
break; break;
} }

172
lib/repl.js

@ -49,7 +49,9 @@ function REPLServer(prompt, stream) {
self.stream = stream || process.openStdin(); self.stream = stream || process.openStdin();
self.prompt = prompt || "node> "; self.prompt = prompt || "node> ";
var rli = self.rli = rl.createInterface(self.stream); var rli = self.rli = rl.createInterface(self.stream, function (text) {
return self.complete(text);
});
rli.setPrompt(self.prompt); rli.setPrompt(self.prompt);
self.stream.addListener("data", function (chunk) { self.stream.addListener("data", function (chunk) {
@ -135,6 +137,174 @@ REPLServer.prototype.displayPrompt = function () {
REPLServer.prototype.readline = function (cmd) { REPLServer.prototype.readline = function (cmd) {
}; };
/**
* Provide a list of completions for the given leading text. This is
* given to the readline interface for handling tab completion.
*
* @param {line} The text (preceding the cursor) to complete
* @returns {Array} Two elements: (1) an array of completions; and
* (2) the leading text completed.
*
* Example:
* complete('var foo = sys.')
* -> [['sys.print', 'sys.debug', 'sys.log', 'sys.inspect', 'sys.pump'],
* 'sys.' ]
*
* TODO: add warning about exec'ing code... property getters could be run
*/
REPLServer.prototype.complete = function (line) {
// TODO: special completion in `require` calls.
var completions,
completionGroups = [], // list of completion lists, one for each inheritance "level"
completeOn,
match, filter, i, j, group, c;
// REPL comments (e.g. ".break").
var match = null;
match = line.match(/^\s*(\.\w*)$/);
if (match) {
completionGroups.push(['.break', '.clear', '.exit', '.help']);
completeOn = match[1];
if (match[1].length > 1) {
filter = match[1];
}
}
// Handle variable member lookup.
// We support simple chained expressions like the following (no function
// calls, etc.). That is for simplicity and also because we *eval* that
// leading expression so for safety (see WARNING above) don't want to
// eval function calls.
//
// foo.bar<|> # completions for 'foo' with filter 'bar'
// spam.eggs.<|> # completions for 'spam.eggs' with filter ''
// foo<|> # all scope vars with filter 'foo'
// foo.<|> # completions for 'foo' with filter ''
else if (line.length === 0 || line[line.length-1].match(/\w|\./)) {
var simpleExpressionPat = /(([a-zA-Z_]\w*)\.)*([a-zA-Z_]\w*)\.?$/;
match = simpleExpressionPat.exec(line);
if (line.length === 0 || match) {
var expr;
completeOn = (match ? match[0] : "");
if (line.length === 0) {
filter = "";
expr = "";
} else if (line[line.length-1] === '.') {
filter = "";
expr = match[0].slice(0, match[0].length-1);
} else {
var bits = match[0].split('.');
filter = bits.pop();
expr = bits.join('.');
}
//console.log("expression completion: completeOn='"+completeOn+"' expr='"+expr+"'");
// Resolve expr and get its completions.
var obj, memberGroups = [];
if (!expr) {
completionGroups.push(Object.getOwnPropertyNames(this.context));
// Global object properties
// (http://www.ecma-international.org/publications/standards/Ecma-262.htm)
completionGroups.push(["NaN", "Infinity", "undefined",
"eval", "parseInt", "parseFloat", "isNaN", "isFinite", "decodeURI",
"decodeURIComponent", "encodeURI", "encodeURIComponent",
"Object", "Function", "Array", "String", "Boolean", "Number",
"Date", "RegExp", "Error", "EvalError", "RangeError",
"ReferenceError", "SyntaxError", "TypeError", "URIError",
"Math", "JSON"]);
// Common keywords. Exclude for completion on the empty string, b/c
// they just get in the way.
if (filter) {
completionGroups.push(["break", "case", "catch", "const",
"continue", "debugger", "default", "delete", "do", "else", "export",
"false", "finally", "for", "function", "if", "import", "in",
"instanceof", "let", "new", "null", "return", "switch", "this",
"throw", "true", "try", "typeof", "undefined", "var", "void",
"while", "with", "yield"])
}
} else {
try {
obj = evalcx(expr, this.context, "repl");
} catch (e) {
//console.log("completion eval error, expr='"+expr+"': "+e);
}
if (obj != null) {
//TODO: The following, for example, misses "Object.isSealed". Is there
// a way to introspec those? Need to hardcode?
if (typeof obj === "object" || typeof obj === "function") {
memberGroups.push(Object.getOwnPropertyNames(obj));
}
var p = obj.constructor.prototype; // works for non-objects
try {
var sentinel = 5;
while (p !== null) {
memberGroups.push(Object.getOwnPropertyNames(p));
p = Object.getPrototypeOf(p);
// Circular refs possible? Let's guard against that.
sentinel--;
if (sentinel <= 0) {
break;
}
}
} catch (e) {
//console.log("completion error walking prototype chain:" + e);
}
}
if (memberGroups.length) {
for (i = 0; i < memberGroups.length; i++) {
completionGroups.push(memberGroups[i].map(function(member) {
return expr + '.' + member;
}));
}
if (filter) {
filter = expr + '.' + filter;
}
}
}
}
}
// Filter, sort (within each group), uniq and merge the completion groups.
if (completionGroups.length && filter) {
var newCompletionGroups = [];
for (i = 0; i < completionGroups.length; i++) {
group = completionGroups[i].filter(function(elem) {
return elem.indexOf(filter) == 0;
});
if (group.length) {
newCompletionGroups.push(group);
}
}
completionGroups = newCompletionGroups;
}
if (completionGroups.length) {
var uniq = {}; // unique completions across all groups
completions = [];
// Completion group 0 is the "closest" (least far up the inheritance chain)
// so we put its completions last: to be closest in the REPL.
for (i = completionGroups.length - 1; i >= 0; i--) {
group = completionGroups[i];
group.sort();
for (var j = 0; j < group.length; j++) {
c = group[j];
if (!uniq.hasOwnProperty(c)) {
completions.push(c);
uniq[c] = true;
}
}
completions.push(""); // separator btwn groups
}
while (completions.length && completions[completions.length-1] === "") {
completions.pop();
}
}
return [completions || [], completeOn];
};
/** /**
* Used to parse and execute the Node REPL commands. * Used to parse and execute the Node REPL commands.
* *

Loading…
Cancel
Save