diff --git a/lib/repl.js b/lib/repl.js index bbe658a8a51..3a5ac4346e7 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(); + } + }); } diff --git a/test/simple/test-repl-tab-complete.js b/test/simple/test-repl-tab-complete.js index d82531e0754..4fdd923c9fe 100644 --- a/test/simple/test-repl-tab-complete.js +++ b/test/simple/test-repl-tab-complete.js @@ -73,16 +73,16 @@ testMe.complete('inner.o', function (error, data) { putIn.run(['.clear']); -// Tab Complete will not return localy scoped variables +// Tab Complete will return a simple local variable putIn.run([ 'var top = function () {', 'var inner = {one:1};']); testMe.complete('inner.o', function (error, data) { - assert.deepEqual(data, doesNotBreak); + assert.deepEqual(data, works); }); // When you close the function scope tab complete will not return the -// localy scoped variable +// locally scoped variable putIn.run(['};']); testMe.complete('inner.o', function (error, data) { assert.deepEqual(data, doesNotBreak); @@ -90,3 +90,84 @@ testMe.complete('inner.o', function (error, data) { putIn.run(['.clear']); +// Tab Complete will return a complex local variable +putIn.run([ + 'var top = function () {', + 'var inner = {', + ' one:1', + '};']); +testMe.complete('inner.o', function (error, data) { + assert.deepEqual(data, works); +}); + +putIn.run(['.clear']); + +// Tab Complete will return a complex local variable even if the function +// has paramaters +putIn.run([ + 'var top = function (one, two) {', + 'var inner = {', + ' one:1', + '};']); +testMe.complete('inner.o', function (error, data) { + assert.deepEqual(data, works); +}); + +putIn.run(['.clear']); + +// Tab Complete will return a complex local variable even if the +// scope is nested inside an immediately executed function +putIn.run([ + 'var top = function () {', + '(function test () {', + 'var inner = {', + ' one:1', + '};']); +testMe.complete('inner.o', function (error, data) { + assert.deepEqual(data, works); +}); + +putIn.run(['.clear']); + +// currently does not work, but should not break note the inner function +// def has the params and { on a seperate line +putIn.run([ + 'var top = function () {', + 'r = function test (', + ' one, two) {', + 'var inner = {', + ' one:1', + '};']); +testMe.complete('inner.o', function (error, data) { + assert.deepEqual(data, doesNotBreak); +}); + +putIn.run(['.clear']); + +// currently does not work, but should not break, not the { +putIn.run([ + 'var top = function () {', + 'r = function test ()', + '{', + 'var inner = {', + ' one:1', + '};']); +testMe.complete('inner.o', function (error, data) { + assert.deepEqual(data, doesNotBreak); +}); + +putIn.run(['.clear']); + +// currently does not work, but should not break +putIn.run([ + 'var top = function () {', + 'r = function test (', + ')', + '{', + 'var inner = {', + ' one:1', + '};']); +testMe.complete('inner.o', function (error, data) { + assert.deepEqual(data, doesNotBreak); +}); + diff --git a/test/simple/test-repl.js b/test/simple/test-repl.js index 783c3ab4937..4fc65f3631b 100644 --- a/test/simple/test-repl.js +++ b/test/simple/test-repl.js @@ -86,7 +86,7 @@ function error_test() { tcp_test(); } - } else if (read_buffer === prompt_multiline) { + } else if (read_buffer.indexOf(prompt_multiline) !== -1) { // Check that you meant to send a multiline test assert.strictEqual(prompt_multiline, client_unix.expect); read_buffer = '';