Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save trentm/515698 to your computer and use it in GitHub Desktop.
Save trentm/515698 to your computer and use it in GitHub Desktop.
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