Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

Commit

Permalink
.load, .save and local scope tab completion
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
seebees authored and ry committed Nov 12, 2011
1 parent b00f5e2 commit 3421f43
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 5 deletions.
153 changes: 152 additions & 1 deletion lib/repl.js
Expand Up @@ -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:
Expand Down Expand Up @@ -265,6 +267,9 @@ REPLServer.prototype.createContext = function() {
context.global = context;
context.global.global = context;

this.lines = [];
this.lines.level = [];

return context;
};

Expand All @@ -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();
};

Expand All @@ -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 =
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
});
}


Expand Down
87 changes: 84 additions & 3 deletions test/simple/test-repl-tab-complete.js
Expand Up @@ -73,20 +73,101 @@ 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);
});

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);
});

2 changes: 1 addition & 1 deletion test/simple/test-repl.js
Expand Up @@ -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 = '';
Expand Down

0 comments on commit 3421f43

Please sign in to comment.