Skip to content

Commit 3421f43

Browse files
seebeesry
authored andcommitted
.load, .save and local scope tab completion
Fixes nodejs#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.
1 parent b00f5e2 commit 3421f43

3 files changed

Lines changed: 237 additions & 5 deletions

File tree

lib/repl.js

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) {
207207

208208
function finish(e, ret) {
209209

210+
self.memory(cmd);
211+
210212
// If error was SyntaxError and not JSON.parse error
211213
if (isSyntaxError(e)) {
212214
// Start buffering data like that:
@@ -265,6 +267,9 @@ REPLServer.prototype.createContext = function() {
265267
context.global = context;
266268
context.global.global = context;
267269

270+
this.lines = [];
271+
this.lines.level = [];
272+
268273
return context;
269274
};
270275

@@ -278,7 +283,9 @@ REPLServer.prototype.resetContext = function(force) {
278283
};
279284

280285
REPLServer.prototype.displayPrompt = function() {
281-
this.rli.setPrompt(this.bufferedCommand.length ? '... ' : this.prompt);
286+
this.rli.setPrompt(this.bufferedCommand.length ?
287+
'...' + new Array(this.lines.level.length).join('..') + ' ' :
288+
this.prompt);
282289
this.rli.prompt();
283290
};
284291

@@ -287,6 +294,21 @@ REPLServer.prototype.displayPrompt = function() {
287294
REPLServer.prototype.readline = function(cmd) {
288295
};
289296

297+
// A stream to push an array into a REPL
298+
// used in REPLServer.complete
299+
function ArrayStream() {
300+
this.run = function (data) {
301+
var self = this;
302+
data.forEach(function (line) {
303+
self.emit('data', line);
304+
});
305+
}
306+
}
307+
util.inherits(ArrayStream, require('stream').Stream);
308+
ArrayStream.prototype.readable = true;
309+
ArrayStream.prototype.writable = true;
310+
ArrayStream.prototype.resume = function () {};
311+
ArrayStream.prototype.write = function () {};
290312

291313
var requireRE = /\brequire\s*\(['"](([\w\.\/-]+\/)?([\w\.\/-]*))/;
292314
var simpleExpressionRE =
@@ -304,6 +326,28 @@ var simpleExpressionRE =
304326
// Warning: This eval's code like "foo.bar.baz", so it will run property
305327
// getter code.
306328
REPLServer.prototype.complete = function(line, callback) {
329+
// There may be local variables to evaluate, try a nested REPL
330+
if (this.bufferedCommand != undefined && this.bufferedCommand.length) {
331+
// Get a new array of inputed lines
332+
var tmp = this.lines.slice();
333+
// Kill off all function declarations to push all local variables into
334+
// global scope
335+
this.lines.level.forEach(function (kill) {
336+
if (kill.isFunction) {
337+
tmp[kill.line] = '';
338+
}
339+
});
340+
var flat = new ArrayStream(); // make a new "input" stream
341+
var magic = new REPLServer('', flat); // make a nested REPL
342+
magic.context = magic.createContext();
343+
flat.run(tmp); // eval the flattened code
344+
// all this is only profitable if the nested REPL
345+
// does not have a bufferedCommand
346+
if (!magic.bufferedCommand) {
347+
return magic.complete(line, callback);
348+
}
349+
}
350+
307351
var completions;
308352

309353
// list of completion lists, one for each inheritance "level"
@@ -586,6 +630,77 @@ REPLServer.prototype.defineCommand = function(keyword, cmd) {
586630
this.commands['.' + keyword] = cmd;
587631
};
588632

633+
REPLServer.prototype.memory = function memory (cmd) {
634+
var self = this;
635+
636+
self.lines = self.lines || [];
637+
self.lines.level = self.lines.level || [];
638+
639+
// save the line so I can do magic later
640+
if (cmd) {
641+
// TODO should I tab the level?
642+
self.lines.push(new Array(self.lines.level.length).join(' ') + cmd);
643+
} else {
644+
// I don't want to not change the format too much...
645+
self.lines.push('');
646+
}
647+
648+
// I need to know "depth."
649+
// Because I can not tell the difference between a } that
650+
// closes an object literal and a } that closes a function
651+
if (cmd) {
652+
// going down is { and ( e.g. function () {
653+
// going up is } and )
654+
var dw = cmd.match(/{|\(/g);
655+
var up = cmd.match(/}|\)/g);
656+
up = up ? up.length : 0;
657+
dw = dw ? dw.length : 0;
658+
var depth = dw - up;
659+
660+
if (depth) {
661+
(function workIt(){
662+
if (depth > 0) {
663+
// going... down.
664+
// push the line#, depth count, and if the line is a function.
665+
// Since JS only has functional scope I only need to remove
666+
// "function () {" lines, clearly this will not work for
667+
// "function ()
668+
// {" but nothing should break, only tab completion for local
669+
// scope will not work for this function.
670+
self.lines.level.push({ line: self.lines.length - 1,
671+
depth: depth,
672+
isFunction: /\s*function\s*/.test(cmd)});
673+
} else if (depth < 0) {
674+
// going... up.
675+
var curr = self.lines.level.pop();
676+
if (curr) {
677+
var tmp = curr.depth + depth;
678+
if (tmp < 0) {
679+
//more to go, recurse
680+
depth += curr.depth;
681+
workIt();
682+
} else if (tmp > 0) {
683+
//remove and push back
684+
curr.depth += depth;
685+
self.lines.level.push(curr);
686+
}
687+
}
688+
}
689+
}());
690+
}
691+
692+
// it is possible to determine a syntax error at this point.
693+
// if the REPL still has a bufferedCommand and
694+
// self.lines.level.length === 0
695+
// TODO? keep a log of level so that any syntax breaking lines can
696+
// be cleared on .break and in the case of a syntax error?
697+
// TODO? if a log was kept, then I could clear the bufferedComand and
698+
// eval these lines and throw the syntax error
699+
} else {
700+
self.lines.level = [];
701+
}
702+
};
703+
589704

590705
function defineDefaultCommands(repl) {
591706
// TODO remove me after 0.3.x
@@ -625,6 +740,42 @@ function defineDefaultCommands(repl) {
625740
this.displayPrompt();
626741
}
627742
});
743+
744+
repl.defineCommand('save', {
745+
help: 'Save all evaluated commands in this REPL session to a file',
746+
action: function(file) {
747+
try {
748+
fs.writeFileSync(file, this.lines.join('\n') + '\n');
749+
this.outputStream.write('Session saved to:' + file + '\n');
750+
} catch (e) {
751+
this.outputStream.write('Failed to save:' + file+ '\n')
752+
}
753+
this.displayPrompt();
754+
}
755+
});
756+
757+
repl.defineCommand('load', {
758+
help: 'Load JS from a file into the REPL session',
759+
action: function(file) {
760+
try {
761+
var stats = fs.statSync(file);
762+
if (stats && stats.isFile()) {
763+
var self = this;
764+
var data = fs.readFileSync(file, 'utf8');
765+
var lines = data.split('\n');
766+
this.displayPrompt();
767+
lines.forEach(function (line) {
768+
if (line) {
769+
self.rli.write(line + '\n');
770+
}
771+
});
772+
}
773+
} catch (e) {
774+
this.outputStream.write('Failed to load:' + file + '\n');
775+
}
776+
this.displayPrompt();
777+
}
778+
});
628779
}
629780

630781

test/simple/test-repl-tab-complete.js

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,101 @@ testMe.complete('inner.o', function (error, data) {
7373

7474
putIn.run(['.clear']);
7575

76-
// Tab Complete will not return localy scoped variables
76+
// Tab Complete will return a simple local variable
7777
putIn.run([
7878
'var top = function () {',
7979
'var inner = {one:1};']);
8080
testMe.complete('inner.o', function (error, data) {
81-
assert.deepEqual(data, doesNotBreak);
81+
assert.deepEqual(data, works);
8282
});
8383

8484
// When you close the function scope tab complete will not return the
85-
// localy scoped variable
85+
// locally scoped variable
8686
putIn.run(['};']);
8787
testMe.complete('inner.o', function (error, data) {
8888
assert.deepEqual(data, doesNotBreak);
8989
});
9090

9191
putIn.run(['.clear']);
9292

93+
// Tab Complete will return a complex local variable
94+
putIn.run([
95+
'var top = function () {',
96+
'var inner = {',
97+
' one:1',
98+
'};']);
99+
testMe.complete('inner.o', function (error, data) {
100+
assert.deepEqual(data, works);
101+
});
102+
103+
putIn.run(['.clear']);
104+
105+
// Tab Complete will return a complex local variable even if the function
106+
// has paramaters
107+
putIn.run([
108+
'var top = function (one, two) {',
109+
'var inner = {',
110+
' one:1',
111+
'};']);
112+
testMe.complete('inner.o', function (error, data) {
113+
assert.deepEqual(data, works);
114+
});
115+
116+
putIn.run(['.clear']);
117+
118+
// Tab Complete will return a complex local variable even if the
119+
// scope is nested inside an immediately executed function
120+
putIn.run([
121+
'var top = function () {',
122+
'(function test () {',
123+
'var inner = {',
124+
' one:1',
125+
'};']);
126+
testMe.complete('inner.o', function (error, data) {
127+
assert.deepEqual(data, works);
128+
});
129+
130+
putIn.run(['.clear']);
131+
132+
// currently does not work, but should not break note the inner function
133+
// def has the params and { on a seperate line
134+
putIn.run([
135+
'var top = function () {',
136+
'r = function test (',
137+
' one, two) {',
138+
'var inner = {',
139+
' one:1',
140+
'};']);
141+
testMe.complete('inner.o', function (error, data) {
142+
assert.deepEqual(data, doesNotBreak);
143+
});
144+
145+
putIn.run(['.clear']);
146+
147+
// currently does not work, but should not break, not the {
148+
putIn.run([
149+
'var top = function () {',
150+
'r = function test ()',
151+
'{',
152+
'var inner = {',
153+
' one:1',
154+
'};']);
155+
testMe.complete('inner.o', function (error, data) {
156+
assert.deepEqual(data, doesNotBreak);
157+
});
158+
159+
putIn.run(['.clear']);
160+
161+
// currently does not work, but should not break
162+
putIn.run([
163+
'var top = function () {',
164+
'r = function test (',
165+
')',
166+
'{',
167+
'var inner = {',
168+
' one:1',
169+
'};']);
170+
testMe.complete('inner.o', function (error, data) {
171+
assert.deepEqual(data, doesNotBreak);
172+
});
173+

test/simple/test-repl.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function error_test() {
8686
tcp_test();
8787
}
8888

89-
} else if (read_buffer === prompt_multiline) {
89+
} else if (read_buffer.indexOf(prompt_multiline) !== -1) {
9090
// Check that you meant to send a multiline test
9191
assert.strictEqual(prompt_multiline, client_unix.expect);
9292
read_buffer = '';

0 commit comments

Comments
 (0)