Browse Source

[repl, readline] async interface

Add async completion and execution interface for repl and readline
Fedor Indutny 13 years ago
parent
commit
42b8b77d9f
  1. 120
      lib/readline.js
  2. 248
      lib/repl.js

120
lib/readline.js

@ -51,7 +51,10 @@ function Interface(input, output, completer) {
this.input = input;
input.resume();
this.completer = completer;
// Check arity, 2 - for async, 1 for sync
this.completer = completer.length === 2 ? completer : function(v, callback) {
callback(null, completer(v));
};
this.setPrompt('> ');
@ -247,68 +250,77 @@ Interface.prototype._insertString = function(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 {
self.output.write('\r\n');
var width = completions.reduce(function(a, b) {
return a.length > b.length ? a : b;
}).length + 2; // 2 space padding
var maxColumns = Math.floor(this.columns / width) || 1;
function handleGroup(group) {
if (group.length == 0) {
return;
}
var minRows = Math.ceil(group.length / maxColumns);
for (var row = 0; row < minRows; row++) {
for (var col = 0; col < maxColumns; col++) {
var idx = row * maxColumns + col;
if (idx >= group.length) {
break;
}
var item = group[idx];
self.output.write(item);
if (col < maxColumns - 1) {
for (var s = 0, itemLen = item.length; s < width - itemLen; s++) {
self.output.write(' ');
this.pause();
this.completer(self.line.slice(0, self.cursor), function(err, rv) {
self.resume();
if (err) {
// XXX Log it somewhere?
return;
}
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 {
self.output.write('\r\n');
var width = completions.reduce(function(a, b) {
return a.length > b.length ? a : b;
}).length + 2; // 2 space padding
var maxColumns = Math.floor(this.columns / width) || 1;
function handleGroup(group) {
if (group.length == 0) {
return;
}
var minRows = Math.ceil(group.length / maxColumns);
for (var row = 0; row < minRows; row++) {
for (var col = 0; col < maxColumns; col++) {
var idx = row * maxColumns + col;
if (idx >= group.length) {
break;
}
var item = group[idx];
self.output.write(item);
if (col < maxColumns - 1) {
for (var s = 0, itemLen = item.length; s < width - itemLen; s++) {
self.output.write(' ');
}
}
}
self.output.write('\r\n');
}
self.output.write('\r\n');
}
self.output.write('\r\n');
}
var group = [], c;
for (var i = 0, compLen = completions.length; i < compLen; i++) {
c = completions[i];
if (c === '') {
handleGroup(group);
group = [];
} else {
group.push(c);
var group = [], c;
for (var i = 0, compLen = completions.length; i < compLen; i++) {
c = completions[i];
if (c === '') {
handleGroup(group);
group = [];
} else {
group.push(c);
}
}
handleGroup(group);
// 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));
}
}
handleGroup(group);
// 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));
}
self._refreshLine();
self._refreshLine();
}
}
}
});
};

248
lib/repl.js

@ -64,9 +64,18 @@ module.paths = require('module')._nodeModulePaths(module.filename);
exports.writer = util.inspect;
function REPLServer(prompt, stream) {
function REPLServer(prompt, stream, options) {
var self = this;
self.eval = options && options.eval || function(code, context, file, cb) {
try {
var err, result = vm.runInContext(code, context, file);
} catch (e) {
err = e;
}
cb(err, result);
};
self.resetContext();
self.bufferedCommand = '';
@ -82,8 +91,8 @@ function REPLServer(prompt, stream) {
self.prompt = (prompt != undefined ? prompt : '> ');
function complete(text) {
return self.complete(text);
function complete(text, callback) {
self.complete(text, callback);
}
var rli = rl.createInterface(self.inputStream, self.outputStream, complete);
@ -140,68 +149,85 @@ function REPLServer(prompt, stream) {
}
if (!skipCatchall) {
// The catchall for errors
try {
self.bufferedCommand += cmd + '\n';
// This try is for determining if the command is complete, or should
// continue onto the next line.
try {
// We try to evaluate both expressions e.g.
// '{ a : 1 }'
// and statements e.g.
// 'for (var i = 0; i < 10; i++) console.log(i);'
var ret, success = false;
try {
// First we attempt to eval as expression with parens.
// This catches '{a : 1}' properly.
ret = vm.runInContext('(' + self.bufferedCommand + ')',
self.context,
'repl');
if (typeof ret !== 'function') success = true;
} catch (e) {
if (!(e && e.constructor && e.constructor.name === 'SyntaxError')) {
throw e;
self.bufferedCommand += cmd + '\n';
// This try is for determining if the command is complete, or should
// continue onto the next line.
// We try to evaluate both expressions e.g.
// '{ a : 1 }'
// and statements e.g.
// 'for (var i = 0; i < 10; i++) console.log(i);'
// First we attempt to eval as expression with parens.
// This catches '{a : 1}' properly.
function tryParens() {
var success = false;
self.eval('(' + self.bufferedCommand + ')',
self.context,
'repl',
function(e, ret) {
if (e) {
if (!(e && e.constructor &&
e.constructor.name === 'SyntaxError')) {
finish(e);
} else {
success = false;
tryExpr();
}
return;
}
if (!success) {
// Now as statement without parens.
ret = vm.runInContext(self.bufferedCommand, self.context, 'repl');
if (typeof ret !== 'function') {
return tryExpr(ret);
}
tryExpr();
});
};
// Now as statement without parens.
function tryExpr(ret) {
self.eval(self.bufferedCommand, self.context,
'repl', function(e, ret) {
if (ret !== undefined) {
self.context._ = ret;
self.outputStream.write(exports.writer(ret) + '\n');
}
self.bufferedCommand = '';
} catch (e) {
// instanceof doesn't work across context switches.
if (!(e && e.constructor && e.constructor.name === 'SyntaxError')) {
throw e;
// It could also be an error from JSON.parse
} else if (e &&
e.stack &&
e.stack.match(/^SyntaxError: Unexpected token .*\n/) &&
e.stack.match(/\n at Object.parse \(native\)\n/)) {
throw e;
if (e) {
// instanceof doesn't work across context switches.
if (!(e && e.constructor && e.constructor.name === 'SyntaxError')) {
return finish(e);
// It could also be an error from JSON.parse
} else if (e &&
e.stack &&
e.stack.match(/^SyntaxError: Unexpected token .*\n/) &&
e.stack.match(/\n at Object.parse \(native\)\n/)) {
return finish(e);
}
}
}
} catch (e) {
// On error: Print the error and clear the buffer
finish(null, ret);
});
};
return tryParens();
}
finish(null);
function finish(e, ret) {
if (e) {
if (e.stack) {
self.outputStream.write(e.stack + '\n');
} else {
self.outputStream.write(e.toString() + '\n');
}
// On error: Print the error and clear the buffer
self.bufferedCommand = '';
}
}
self.displayPrompt();
self.displayPrompt();
};
});
rli.addListener('close', function() {
@ -269,7 +295,7 @@ var simpleExpressionRE =
//
// Warning: This eval's code like "foo.bar.baz", so it will run property
// getter code.
REPLServer.prototype.complete = function(line) {
REPLServer.prototype.complete = function(line, callback) {
var completions;
// list of completion lists, one for each inheritance "level"
@ -406,84 +432,90 @@ REPLServer.prototype.complete = function(line) {
'while', 'with', 'yield']);
}
} else {
try {
obj = vm.runInContext(expr, this.context, 'repl');
} catch (e) {
//console.log("completion eval error, expr='"+expr+"': "+e);
}
if (obj != null) {
if (typeof obj === 'object' || typeof obj === 'function') {
memberGroups.push(Object.getOwnPropertyNames(obj));
}
// works for non-objects
try {
var p = Object.getPrototypeOf(obj);
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;
this.eval(expr, this.context, 'repl', function(e, obj) {
// if (e) console.log(e);
if (obj != null) {
if (typeof obj === 'object' || typeof obj === 'function') {
memberGroups.push(Object.getOwnPropertyNames(obj));
}
// works for non-objects
try {
var p = Object.getPrototypeOf(obj);
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);
}
} 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;
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);
// If reach this point - work like sync
finish(null, ret);
function finish(err, ret) {
if (err) throw err;
// 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;
}
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;
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();
}
completions.push(''); // separator btwn groups
}
while (completions.length && completions[completions.length - 1] === '') {
completions.pop();
}
}
return [completions || [], completeOn];
callback(null, [completions || [], completeOn]);
}
};

Loading…
Cancel
Save