diff --git a/lib/readline.js b/lib/readline.js index 7c6f416d90..2562124b60 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -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; } diff --git a/lib/repl.js b/lib/repl.js index 52ae11c3a3..c0bd82b36c 100644 --- a/lib/repl.js +++ b/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. *