summaryrefslogtreecommitdiff
path: root/lib/repl.js
diff options
context:
space:
mode:
authorseebees <seebees@gmail.com>2011-11-11 17:44:39 -0800
committerRyan Dahl <ry@tinyclouds.org>2011-11-12 11:34:55 -0800
commit3421f433519411c290c732c64dadbb6547e42235 (patch)
treed6bd2427ef01f7028259343c25a640aa70a956d6 /lib/repl.js
parentb00f5e2e14b98188d275be411a3ce03bffce69af (diff)
downloadnode-new-3421f433519411c290c732c64dadbb6547e42235.tar.gz
.load, .save and local scope tab completion
Fixes #2063. REPLServer.prototype.resetContext: Reset the line cache REPLServer.prototype.memory (don't know if I like that name, called from finish) pushes what cmd's have been executed against it into this.lines pushes the "tab depth" for bufferedCommands, in this.lines.level REPLServer.prototype.displayPrompt: Uses "tab depth" from this.lines.level to adjust the prompt to visually denote this depth e.g. > asdf = function () { … var inner = { ….. one:1 REPLServer.prototype.complete: Now notices if there is a bufferedCommand and attempts determine locally scoped variables by removing any functions from this.lines and evaling these lines in a nested REPL e.g. > asdf = function () { … var inner = { one: 1}; … inn\t will complete to 'inner' and inner.o\t will complete to 'inner.one' If the nested REPL still has a bufferedCommand it will falls back to the default. ArrayStream is a helper class for the nested REPL to get commands pushed to it. new REPLServer('', new ArrayStream()); Finally added two new REPL commands .save and .load, each takes 1 parameter, a file and attempts to save or load the file to or from the REPL respectively.
Diffstat (limited to 'lib/repl.js')
-rw-r--r--lib/repl.js153
1 files changed, 152 insertions, 1 deletions
diff --git a/lib/repl.js b/lib/repl.js
index bbe658a8a5..3a5ac4346e 100644
--- a/lib/repl.js
+++ b/lib/repl.js
@@ -207,6 +207,8 @@ function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) {
function finish(e, ret) {
+ self.memory(cmd);
+
// If error was SyntaxError and not JSON.parse error
if (isSyntaxError(e)) {
// Start buffering data like that:
@@ -265,6 +267,9 @@ REPLServer.prototype.createContext = function() {
context.global = context;
context.global.global = context;
+ this.lines = [];
+ this.lines.level = [];
+
return context;
};
@@ -278,7 +283,9 @@ REPLServer.prototype.resetContext = function(force) {
};
REPLServer.prototype.displayPrompt = function() {
- this.rli.setPrompt(this.bufferedCommand.length ? '... ' : this.prompt);
+ this.rli.setPrompt(this.bufferedCommand.length ?
+ '...' + new Array(this.lines.level.length).join('..') + ' ' :
+ this.prompt);
this.rli.prompt();
};
@@ -287,6 +294,21 @@ REPLServer.prototype.displayPrompt = function() {
REPLServer.prototype.readline = function(cmd) {
};
+// A stream to push an array into a REPL
+// used in REPLServer.complete
+function ArrayStream() {
+ this.run = function (data) {
+ var self = this;
+ data.forEach(function (line) {
+ self.emit('data', line);
+ });
+ }
+}
+util.inherits(ArrayStream, require('stream').Stream);
+ArrayStream.prototype.readable = true;
+ArrayStream.prototype.writable = true;
+ArrayStream.prototype.resume = function () {};
+ArrayStream.prototype.write = function () {};
var requireRE = /\brequire\s*\(['"](([\w\.\/-]+\/)?([\w\.\/-]*))/;
var simpleExpressionRE =
@@ -304,6 +326,28 @@ var simpleExpressionRE =
// Warning: This eval's code like "foo.bar.baz", so it will run property
// getter code.
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"
@@ -586,6 +630,77 @@ REPLServer.prototype.defineCommand = function(keyword, cmd) {
this.commands['.' + keyword] = cmd;
};
+REPLServer.prototype.memory = function memory (cmd) {
+ var self = this;
+
+ self.lines = self.lines || [];
+ self.lines.level = self.lines.level || [];
+
+ // save the line so I can do magic later
+ if (cmd) {
+ // TODO should I tab the level?
+ self.lines.push(new Array(self.lines.level.length).join(' ') + cmd);
+ } else {
+ // I don't want to not change the format too much...
+ self.lines.push('');
+ }
+
+ // I need to know "depth."
+ // Because I can not tell the difference between a } that
+ // closes an object literal and a } that closes a function
+ if (cmd) {
+ // going down is { and ( e.g. function () {
+ // going up is } and )
+ var dw = cmd.match(/{|\(/g);
+ var up = cmd.match(/}|\)/g);
+ up = up ? up.length : 0;
+ dw = dw ? dw.length : 0;
+ var depth = dw - up;
+
+ if (depth) {
+ (function workIt(){
+ if (depth > 0) {
+ // going... down.
+ // push the line#, depth count, and if the line is a function.
+ // Since JS only has functional scope I only need to remove
+ // "function () {" lines, clearly this will not work for
+ // "function ()
+ // {" but nothing should break, only tab completion for local
+ // scope will not work for this function.
+ self.lines.level.push({ line: self.lines.length - 1,
+ depth: depth,
+ isFunction: /\s*function\s*/.test(cmd)});
+ } else if (depth < 0) {
+ // going... up.
+ var curr = self.lines.level.pop();
+ if (curr) {
+ var tmp = curr.depth + depth;
+ if (tmp < 0) {
+ //more to go, recurse
+ depth += curr.depth;
+ workIt();
+ } else if (tmp > 0) {
+ //remove and push back
+ curr.depth += depth;
+ self.lines.level.push(curr);
+ }
+ }
+ }
+ }());
+ }
+
+ // it is possible to determine a syntax error at this point.
+ // if the REPL still has a bufferedCommand and
+ // self.lines.level.length === 0
+ // TODO? keep a log of level so that any syntax breaking lines can
+ // be cleared on .break and in the case of a syntax error?
+ // TODO? if a log was kept, then I could clear the bufferedComand and
+ // eval these lines and throw the syntax error
+ } else {
+ self.lines.level = [];
+ }
+};
+
function defineDefaultCommands(repl) {
// TODO remove me after 0.3.x
@@ -625,6 +740,42 @@ function defineDefaultCommands(repl) {
this.displayPrompt();
}
});
+
+ repl.defineCommand('save', {
+ help: 'Save all evaluated commands in this REPL session to a file',
+ action: function(file) {
+ try {
+ fs.writeFileSync(file, this.lines.join('\n') + '\n');
+ this.outputStream.write('Session saved to:' + file + '\n');
+ } catch (e) {
+ this.outputStream.write('Failed to save:' + file+ '\n')
+ }
+ this.displayPrompt();
+ }
+ });
+
+ repl.defineCommand('load', {
+ help: 'Load JS from a file into the REPL session',
+ action: function(file) {
+ try {
+ var stats = fs.statSync(file);
+ if (stats && stats.isFile()) {
+ var self = this;
+ var data = fs.readFileSync(file, 'utf8');
+ var lines = data.split('\n');
+ this.displayPrompt();
+ lines.forEach(function (line) {
+ if (line) {
+ self.rli.write(line + '\n');
+ }
+ });
+ }
+ } catch (e) {
+ this.outputStream.write('Failed to load:' + file + '\n');
+ }
+ this.displayPrompt();
+ }
+ });
}