Created
August 9, 2010 16:53
-
-
Save trentm/515698 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
From e9c3a7813a017983423c0f8b0813fb45fa2b4440 Mon Sep 17 00:00:00 2001 | |
From: Trent Mick <trentm@gmail.com> | |
Date: Mon, 9 Aug 2010 02:18:32 -0700 | |
Subject: [PATCH 4/4] working first pass at tab-completion in the repl (see inline TODOs) | |
--- | |
lib/readline.js | 73 +++++++++++++++++++++----- | |
lib/repl.js | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- | |
2 files changed, 211 insertions(+), 15 deletions(-) | |
diff --git a/lib/readline.js b/lib/readline.js | |
index 75ccf23..7e5897e 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,54 @@ 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; | |
+ | |
+ //TODO: drop this so can get completion on all global vars. | |
+ if (!self.line.slice(0, self.cursor).trim()) { // just whitespace before cursor | |
+ self._insertString('\t'); | |
+ return; | |
+ } | |
+ | |
+ 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 +277,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,17 +325,7 @@ 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); | |
- } | |
+ this._insertString(c); | |
break; | |
} | |
}; | |
diff --git a/lib/repl.js b/lib/repl.js | |
index 52ae11c..e272dda 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) { | |
@@ -136,6 +138,155 @@ 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:XXX handle text being empty | |
+ // TODO: special completion in `require` calls. | |
+ | |
+ var completions = null, match, filter, completeOn; | |
+ | |
+ // REPL comments (e.g. ".break"). | |
+ var match = null; | |
+ match = line.match(/^\s*(\.\w*)$/); | |
+ if (match) { | |
+ completions = ['.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[line.length-1].match(/\w|\./)) { | |
+ var simpleExpressionPat = /(([a-zA-Z_]\w*)\.)*([a-zA-Z_]\w*)\.?$/; | |
+ match = simpleExpressionPat.exec(line); | |
+ if (match) { | |
+ completeOn = match[0]; | |
+ var expr; | |
+ 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, members; | |
+ if (!expr) { | |
+ completions = Object.getOwnPropertyNames(this.context); | |
+ // Global object properties | |
+ // (http://www.ecma-international.org/publications/standards/Ecma-262.htm) | |
+ completions = completions.concat(["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. | |
+ completions = completions.concat(["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") { | |
+ members = Object.getOwnPropertyNames(obj); | |
+ } else { | |
+ members = []; | |
+ } | |
+ members = members.concat(Object.getOwnPropertyNames(obj.constructor.prototype)); | |
+ // AFAICT there is no standard way to walk the prototype chain. | |
+ // Presumably this would be done by accessing what the spec calls | |
+ // the "[[Prototype]] internal property". As a limited workaround | |
+ // we'll use `obj.super_` as set by Node's `sys.inherits`. | |
+ try { | |
+ var sentinel = 3; | |
+ var c = obj.constructor; | |
+ while (c.super_ !== undefined) { | |
+ members = members.concat(Object.getOwnPropertyNames(c.super_.prototype)); | |
+ c = c.super_; | |
+ // Circular `super_` refs possible? Let's guard against that. | |
+ sentinel--; | |
+ if (sentinel <= 0) { | |
+ break; | |
+ } | |
+ } | |
+ } catch (e) { | |
+ //console.log("completion error walking `super_` chain:" + e); | |
+ } | |
+ members = members.concat(Object.getOwnPropertyNames(Object.prototype)); | |
+ } | |
+ | |
+ if (members) { | |
+ completions = members.map(function(member) { | |
+ return expr + '.' + member; | |
+ }) | |
+ if (filter) { | |
+ filter = expr + '.' + filter; | |
+ } | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ // Filter, sort, uniq the completions. | |
+ if (completions && filter) { | |
+ completions = completions.filter(function(elem) { | |
+ return elem.indexOf(filter) == 0; | |
+ }); | |
+ } | |
+ if (completions && completions.length) { | |
+ completions.sort(); | |
+ var uniq = [completions[0]]; | |
+ for (var i = 1; i < completions.length; i += 1) { | |
+ if (completions[i] !== uniq[uniq.length-1]) { | |
+ uniq.push(completions[i]); | |
+ } | |
+ } | |
+ completions = uniq; | |
+ } | |
+ | |
+ return [completions || [], completeOn]; | |
+}; | |
+ | |
+/** | |
* Used to parse and execute the Node REPL commands. | |
* | |
* @param {cmd} cmd The command entered to check | |
-- | |
1.7.1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment