in shell/jrepl.js [367:649]
REPLServer.prototype.complete = function(line, callback) {
// There may be local variables to evaluate, try a nested REPL
if (this.bufferedCommand != undefined && this.bufferedCommand.length) {
// Get a new array of inputed lines
var tmp = this.lines.slice();
// Kill off all function declarations to push all local variables into
// global scope
this.lines.level.forEach(function (kill) {
if (kill.isFunction) {
tmp[kill.line] = '';
}
});
var flat = new ArrayStream(); // make a new "input" stream
var magic = new REPLServer('', flat); // make a nested REPL
magic.context = magic.createContext();
flat.run(tmp); // eval the flattened code
// all this is only profitable if the nested REPL
// does not have a bufferedCommand
if (!magic.bufferedCommand) {
return magic.complete(line, callback);
}
}
var completions;
// list of completion lists, one for each inheritance "level"
var completionGroups = [];
var completeOn, match, filter, i, j, group, c;
// REPL commands (e.g. ".break").
var match = null;
match = line.match(/^\s*(\.\w*)$/);
if (match) {
completionGroups.push(Object.keys(this.commands));
completeOn = match[1];
if (match[1].length > 1) {
filter = match[1];
}
completionGroupsLoaded();
} else if (match = line.match(requireRE)) {
// require('...<Tab>')
//TODO: suggest require.exts be exposed to be introspec registered
//extensions?
//TODO: suggest include the '.' in exts in internal repr: parity with
//`path.extname`.
var exts = ['.js', '.node'];
var indexRe = new RegExp('^index(' + exts.map(regexpEscape).join('|') +
')$');
completeOn = match[1];
var subdir = match[2] || '';
var filter = match[1];
var dir, files, f, name, base, ext, abs, subfiles, s;
group = [];
var paths = module.paths.concat(require('module').globalPaths);
for (i = 0; i < paths.length; i++) {
dir = path.resolve(paths[i], subdir);
try {
files = fs.readdirSync(dir);
} catch (e) {
continue;
}
for (f = 0; f < files.length; f++) {
name = files[f];
ext = path.extname(name);
base = name.slice(0, -ext.length);
if (base.match(/-\d+\.\d+(\.\d+)?/) || name === '.npm') {
// Exclude versioned names that 'npm' installs.
continue;
}
if (exts.indexOf(ext) !== -1) {
if (!subdir || base !== 'index') {
group.push(subdir + base);
}
} else {
abs = path.resolve(dir, name);
try {
if (fs.statSync(abs).isDirectory()) {
group.push(subdir + name + '/');
subfiles = fs.readdirSync(abs);
for (s = 0; s < subfiles.length; s++) {
if (indexRe.test(subfiles[s])) {
group.push(subdir + name);
}
}
}
} catch (e) {}
}
}
}
if (group.length) {
completionGroups.push(group);
}
if (!subdir) {
// Kind of lame that this needs to be updated manually.
// Intentionally excluding moved modules: posix, utils.
var builtinLibs = ['assert', 'buffer', 'child_process', 'crypto', 'dgram',
'dns', 'events', 'file', 'freelist', 'fs', 'http', 'net', 'os', 'path',
'querystring', 'readline', 'repl', 'string_decoder', 'util', 'tcp',
'url'];
completionGroups.push(builtinLibs);
}
completionGroupsLoaded();
// 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|\.|\$/)) {
match = simpleExpressionRE.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) {
// If context is instance of vm.ScriptContext
// Get global vars synchronously
if (this.useGlobal ||
this.context.constructor &&
this.context.constructor.name === 'Context') {
completionGroups.push(Object.getOwnPropertyNames(this.context));
addStandardGlobals();
completionGroupsLoaded();
} else {
this.eval('.scope', this.context, 'repl', function(err, globals) {
if (err || !globals) {
addStandardGlobals();
} else if (Array.isArray(globals[0])) {
// Add grouped globals
globals.forEach(function(group) {
completionGroups.push(group);
});
} else {
completionGroups.push(globals);
addStandardGlobals();
}
completionGroupsLoaded();
});
}
function addStandardGlobals() {
// 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 {
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 sentinel = 5;
var p;
if (typeof obj == 'object') {
p = Object.getPrototypeOf(obj);
} else {
p = obj.constructor ? obj.constructor.prototype : null;
}
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;
}
}
completionGroupsLoaded();
});
}
} else {
completionGroupsLoaded();
}
}
// Will be called when all completionGroups are in place
// Useful for async autocompletion
function completionGroupsLoaded(err) {
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;
}
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 (!hasOwnProperty(c)) {
completions.push(c);
uniq[c] = true;
}
}
completions.push(''); // separator btwn groups
}
while (completions.length && completions[completions.length - 1] === '') {
completions.pop();
}
}
callback(null, [completions || [], completeOn]);
}
};