First pass at tab-completion in the REPL
This commit is contained in:
parent
06634f48eb
commit
1c9a85b9a6
@ -16,12 +16,13 @@ var stdio = process.binding('stdio');
|
||||
|
||||
|
||||
|
||||
exports.createInterface = function (output) {
|
||||
return new Interface(output);
|
||||
exports.createInterface = function (output, completer) {
|
||||
return new Interface(output, completer);
|
||||
};
|
||||
|
||||
function Interface (output) {
|
||||
function Interface (output, completer) {
|
||||
this.output = output;
|
||||
this.completer = completer;
|
||||
|
||||
this.setPrompt("node> ");
|
||||
|
||||
@ -126,6 +127,48 @@ Interface.prototype._normalWrite = function (b) {
|
||||
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 () {
|
||||
if (this.historyIndex > 0) {
|
||||
this.historyIndex--;
|
||||
@ -228,6 +271,12 @@ Interface.prototype._ttyWrite = function (b) {
|
||||
this._historyNext();
|
||||
break;
|
||||
|
||||
case 9: // tab, completion
|
||||
if (this.completer) {
|
||||
this._tabComplete();
|
||||
}
|
||||
break;
|
||||
|
||||
case 16: // control-p, previous history item
|
||||
this._historyPrev();
|
||||
break;
|
||||
@ -270,16 +319,12 @@ Interface.prototype._ttyWrite = function (b) {
|
||||
|
||||
default:
|
||||
var c = b.toString('utf8');
|
||||
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);
|
||||
var lines = c.split(/\r\n|\n|\r/);
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (i > 0) {
|
||||
this._ttyWrite(new Buffer([13]));
|
||||
}
|
||||
this._insertString(lines[i]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
172
lib/repl.js
172
lib/repl.js
@ -49,7 +49,9 @@ function REPLServer(prompt, stream) {
|
||||
self.stream = stream || process.openStdin();
|
||||
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);
|
||||
|
||||
self.stream.addListener("data", function (chunk) {
|
||||
@ -135,6 +137,174 @@ REPLServer.prototype.displayPrompt = function () {
|
||||
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.
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user