diff options
author | seebees <seebees@gmail.com> | 2011-11-11 17:44:39 -0800 |
---|---|---|
committer | Ryan Dahl <ry@tinyclouds.org> | 2011-11-12 11:34:55 -0800 |
commit | 3421f433519411c290c732c64dadbb6547e42235 (patch) | |
tree | d6bd2427ef01f7028259343c25a640aa70a956d6 /lib/repl.js | |
parent | b00f5e2e14b98188d275be411a3ce03bffce69af (diff) | |
download | node-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.js | 153 |
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(); + } + }); } |