Merge pull request #2682 from jashkenas/repl-rewrite

REPL rewrite
This commit is contained in:
Jeremy Ashkenas 2013-01-26 02:26:52 -08:00
commit a106fb451b
7 changed files with 233 additions and 449 deletions

View File

@ -172,6 +172,7 @@ runTests = (CoffeeScript) ->
# Convenience aliases.
global.CoffeeScript = CoffeeScript
global.Repl = require './lib/coffee-script/repl'
# Our test helper function for delimiting different test cases.
global.test = (description, fn) ->
@ -199,8 +200,8 @@ runTests = (CoffeeScript) ->
return no for el, idx in a when not arrayEgal el, b[idx]
yes
global.eq = (a, b, msg) -> ok egal(a, b), msg
global.arrayEq = (a, b, msg) -> ok arrayEgal(a,b), msg
global.eq = (a, b, msg) -> ok egal(a, b), msg ? "Expected #{a} to equal #{b}"
global.arrayEq = (a, b, msg) -> ok arrayEgal(a,b), msg ? "Expected #{a} to deep equal #{b}"
# When all the tests have run, collect and print errors.
# If a stacktrace is available, output the compiled function source.

View File

@ -67,7 +67,7 @@
loadRequires();
}
if (opts.interactive) {
return require('./repl');
return require('./repl').start();
}
if (opts.watch && !fs.watch) {
return printWarn("The --watch feature depends on Node v0.6.0+. You are running " + process.version + ".");
@ -79,7 +79,7 @@
return compileScript(null, sources[0]);
}
if (!sources.length) {
return require('./repl');
return require('./repl').start();
}
literals = opts.run ? sources.splice(1) : [];
process.argv = process.argv.slice(0, 2).concat(literals);

View File

@ -1,276 +1,104 @@
// Generated by CoffeeScript 1.5.0-pre
(function() {
var ACCESSOR, CoffeeScript, Module, REPL_PROMPT, REPL_PROMPT_CONTINUATION, REPL_PROMPT_MULTILINE, SIMPLEVAR, Script, autocomplete, backlog, completeAttribute, completeVariable, enableColours, error, getCompletions, inspect, multilineMode, pipedInput, readline, repl, run, stdin, stdout,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
var CoffeeScript, addMultilineHandler, merge, nodeREPL, replDefaults, vm;
stdin = process.openStdin();
vm = require('vm');
stdout = process.stdout;
nodeREPL = require('repl');
CoffeeScript = require('./coffee-script');
readline = require('readline');
merge = require('./helpers').merge;
inspect = require('util').inspect;
Script = require('vm').Script;
Module = require('module');
REPL_PROMPT = 'coffee> ';
REPL_PROMPT_MULTILINE = '------> ';
REPL_PROMPT_CONTINUATION = '......> ';
enableColours = false;
if (process.platform !== 'win32') {
enableColours = !process.env.NODE_DISABLE_COLORS;
}
error = function(err) {
return stdout.write((err.stack || err.toString()) + '\n');
};
ACCESSOR = /\s*([\w\.]+)(?:\.(\w*))$/;
SIMPLEVAR = /(\w+)$/i;
autocomplete = function(text) {
return completeAttribute(text) || completeVariable(text) || [[], text];
};
completeAttribute = function(text) {
var all, candidates, completions, key, match, obj, prefix, _i, _len, _ref;
if (match = text.match(ACCESSOR)) {
all = match[0], obj = match[1], prefix = match[2];
replDefaults = {
prompt: 'coffee> ',
"eval": function(input, context, filename, cb) {
var js;
input = input.replace(/\uFF00/g, '\n');
input = input.replace(/(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, '$1$2$3');
if (/^\s*$/.test(input)) {
return cb(null);
}
try {
obj = Script.runInThisContext(obj);
} catch (e) {
return;
js = CoffeeScript.compile("_=(" + input + "\n)", {
filename: filename,
bare: true
});
} catch (err) {
cb(err);
}
if (obj == null) {
return;
}
obj = Object(obj);
candidates = Object.getOwnPropertyNames(obj);
while (obj = Object.getPrototypeOf(obj)) {
_ref = Object.getOwnPropertyNames(obj);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
key = _ref[_i];
if (__indexOf.call(candidates, key) < 0) {
candidates.push(key);
}
}
}
completions = getCompletions(prefix, candidates);
return [completions, prefix];
return cb(null, vm.runInContext(js, context, filename));
}
};
completeVariable = function(text) {
var candidates, completions, free, key, keywords, r, vars, _i, _len, _ref;
free = (_ref = text.match(SIMPLEVAR)) != null ? _ref[1] : void 0;
if (text === "") {
free = "";
}
if (free != null) {
vars = Script.runInThisContext('Object.getOwnPropertyNames(Object(this))');
keywords = (function() {
var _i, _len, _ref1, _results;
_ref1 = CoffeeScript.RESERVED;
_results = [];
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
r = _ref1[_i];
if (r.slice(0, 2) !== '__') {
_results.push(r);
}
}
return _results;
})();
candidates = vars;
for (_i = 0, _len = keywords.length; _i < _len; _i++) {
key = keywords[_i];
if (__indexOf.call(candidates, key) < 0) {
candidates.push(key);
}
}
completions = getCompletions(free, candidates);
return [completions, free];
}
};
getCompletions = function(prefix, candidates) {
var el, _i, _len, _results;
_results = [];
for (_i = 0, _len = candidates.length; _i < _len; _i++) {
el = candidates[_i];
if (0 === el.indexOf(prefix)) {
_results.push(el);
}
}
return _results;
};
process.on('uncaughtException', error);
backlog = '';
run = function(buffer) {
var code, returnValue, _;
buffer = buffer.replace(/(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, "$1$2$3");
buffer = buffer.replace(/[\r\n]+$/, "");
if (multilineMode) {
backlog += "" + buffer + "\n";
repl.setPrompt(REPL_PROMPT_CONTINUATION);
repl.prompt();
return;
}
if (!buffer.toString().trim() && !backlog) {
repl.prompt();
return;
}
code = backlog += buffer;
if (code[code.length - 1] === '\\') {
backlog = "" + backlog.slice(0, -1) + "\n";
repl.setPrompt(REPL_PROMPT_CONTINUATION);
repl.prompt();
return;
}
repl.setPrompt(REPL_PROMPT);
backlog = '';
try {
_ = global._;
returnValue = CoffeeScript["eval"]("_=(" + code + "\n)", {
filename: 'repl',
modulename: 'repl'
});
if (returnValue === void 0) {
global._ = _;
}
repl.output.write("" + (inspect(returnValue, false, 2, enableColours)) + "\n");
} catch (err) {
error(err);
}
return repl.prompt();
};
if (stdin.readable && stdin.isRaw) {
pipedInput = '';
repl = {
prompt: function() {
return stdout.write(this._prompt);
},
setPrompt: function(p) {
return this._prompt = p;
},
input: stdin,
output: stdout,
on: function() {}
addMultilineHandler = function(repl) {
var inputStream, multiline, nodeLineListener, outputStream, rli;
rli = repl.rli, inputStream = repl.inputStream, outputStream = repl.outputStream;
multiline = {
enabled: false,
initialPrompt: repl.prompt.replace(/^[^> ]*/, function(x) {
return x.replace(/./g, '-');
}),
prompt: repl.prompt.replace(/^[^> ]*>?/, function(x) {
return x.replace(/./g, '.');
}),
buffer: ''
};
stdin.on('data', function(chunk) {
var line, lines, _i, _len, _ref;
pipedInput += chunk;
if (!/\n/.test(pipedInput)) {
nodeLineListener = rli.listeners('line')[0];
rli.removeListener('line', nodeLineListener);
rli.on('line', function(cmd) {
if (multiline.enabled) {
multiline.buffer += "" + cmd + "\n";
rli.setPrompt(multiline.prompt);
rli.prompt(true);
} else {
nodeLineListener(cmd);
}
});
return inputStream.on('keypress', function(char, key) {
if (!(key && key.ctrl && !key.meta && !key.shift && key.name === 'v')) {
return;
}
lines = pipedInput.split("\n");
pipedInput = lines[lines.length - 1];
_ref = lines.slice(0, -1);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
line = _ref[_i];
if (!(line)) {
continue;
if (multiline.enabled) {
if (!multiline.buffer.match(/\n/)) {
multiline.enabled = !multiline.enabled;
rli.setPrompt(repl.prompt);
rli.prompt(true);
return;
}
stdout.write("" + line + "\n");
run(line);
if ((rli.line != null) && !rli.line.match(/^\s*$/)) {
return;
}
multiline.enabled = !multiline.enabled;
rli.line = '';
rli.cursor = 0;
rli.output.cursorTo(0);
rli.output.clearLine(1);
multiline.buffer = multiline.buffer.replace(/\n/g, '\uFF00');
rli.emit('line', multiline.buffer);
multiline.buffer = '';
} else {
multiline.enabled = !multiline.enabled;
rli.setPrompt(multiline.initialPrompt);
rli.prompt(true);
}
});
stdin.on('end', function() {
var line, _i, _len, _ref;
_ref = pipedInput.trim().split("\n");
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
line = _ref[_i];
if (!(line)) {
continue;
}
stdout.write("" + line + "\n");
run(line);
};
module.exports = {
start: function(opts) {
var repl;
if (opts == null) {
opts = {};
}
stdout.write('\n');
return process.exit(0);
});
} else {
if (readline.createInterface.length < 3) {
repl = readline.createInterface(stdin, autocomplete);
stdin.on('data', function(buffer) {
return repl.write(buffer);
opts = merge(replDefaults, opts);
repl = nodeREPL.start(opts);
repl.on('exit', function() {
return repl.outputStream.write('\n');
});
} else {
repl = readline.createInterface(stdin, stdout, autocomplete);
addMultilineHandler(repl);
return repl;
}
}
multilineMode = false;
repl.input.on('keypress', function(char, key) {
var cursorPos, newPrompt;
if (!(key && key.ctrl && !key.meta && !key.shift && key.name === 'v')) {
return;
}
cursorPos = repl.cursor;
repl.output.cursorTo(0);
repl.output.clearLine(1);
multilineMode = !multilineMode;
if (!multilineMode && backlog) {
repl._line();
}
backlog = '';
repl.setPrompt((newPrompt = multilineMode ? REPL_PROMPT_MULTILINE : REPL_PROMPT));
repl.prompt();
return repl.output.cursorTo(newPrompt.length + (repl.cursor = cursorPos));
});
repl.input.on('keypress', function(char, key) {
if (!(multilineMode && repl.line)) {
return;
}
if (!(key && key.ctrl && !key.meta && !key.shift && key.name === 'd')) {
return;
}
multilineMode = false;
return repl._line();
});
repl.on('attemptClose', function() {
if (multilineMode) {
multilineMode = false;
repl.output.cursorTo(0);
repl.output.clearLine(1);
repl._onLine(repl.line);
return;
}
if (backlog || repl.line) {
backlog = '';
repl.historyIndex = -1;
repl.setPrompt(REPL_PROMPT);
repl.output.write('\n(^C again to quit)');
return repl._line((repl.line = ''));
} else {
return repl.close();
}
});
repl.on('close', function() {
repl.output.write('\n');
return repl.input.destroy();
});
repl.on('line', run);
repl.setPrompt(REPL_PROMPT);
repl.prompt();
};
}).call(this);

View File

@ -68,12 +68,12 @@ exports.run = ->
return usage() if opts.help
return version() if opts.version
loadRequires() if opts.require
return require './repl' if opts.interactive
return require('./repl').start() if opts.interactive
if opts.watch and !fs.watch
return printWarn "The --watch feature depends on Node v0.6.0+. You are running #{process.version}."
return compileStdio() if opts.stdio
return compileScript null, sources[0] if opts.eval
return require './repl' unless sources.length
return require('./repl').start() unless sources.length
literals = if opts.run then sources.splice 1 else []
process.argv = process.argv[0..1].concat literals
process.argv[0] = 'coffee'

View File

@ -1,197 +1,77 @@
# A very simple Read-Eval-Print-Loop. Compiles one line at a time to JavaScript
# and evaluates it. Good for simple tests, or poking around the **Node.js** API.
# Using it looks like this:
#
# coffee> console.log "#{num} bottles of beer" for num in [99..1]
# Start by opening up `stdin` and `stdout`.
stdin = process.openStdin()
stdout = process.stdout
# Require the **coffee-script** module to get access to the compiler.
vm = require 'vm'
nodeREPL = require 'repl'
CoffeeScript = require './coffee-script'
readline = require 'readline'
{inspect} = require 'util'
{Script} = require 'vm'
Module = require 'module'
{merge} = require './helpers'
# REPL Setup
replDefaults =
prompt: 'coffee> ',
eval: (input, context, filename, cb) ->
# XXX: multiline hack
input = input.replace /\uFF00/g, '\n'
# strip single-line comments
input = input.replace /(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, '$1$2$3'
# empty command
return cb null if /^\s*$/.test input
# TODO: fix #1829: pass in-scope vars and avoid accidentally shadowing them by omitting those declarations
try
js = CoffeeScript.compile "_=(#{input}\n)", {filename, bare: yes}
catch err
cb err
cb null, vm.runInContext(js, context, filename)
# Config
REPL_PROMPT = 'coffee> '
REPL_PROMPT_MULTILINE = '------> '
REPL_PROMPT_CONTINUATION = '......> '
enableColours = no
unless process.platform is 'win32'
enableColours = not process.env.NODE_DISABLE_COLORS
addMultilineHandler = (repl) ->
{rli, inputStream, outputStream} = repl
# Log an error.
error = (err) ->
stdout.write (err.stack or err.toString()) + '\n'
multiline =
enabled: off
initialPrompt: repl.prompt.replace(/^[^> ]*/, (x) -> x.replace /./g, '-')
prompt: repl.prompt.replace(/^[^> ]*>?/, (x) -> x.replace /./g, '.')
buffer: ''
## Autocompletion
# Regexes to match complete-able bits of text.
ACCESSOR = /\s*([\w\.]+)(?:\.(\w*))$/
SIMPLEVAR = /(\w+)$/i
# Returns a list of completions, and the completed text.
autocomplete = (text) ->
completeAttribute(text) or completeVariable(text) or [[], text]
# Attempt to autocomplete a chained dotted attribute: `one.two.three`.
completeAttribute = (text) ->
if match = text.match ACCESSOR
[all, obj, prefix] = match
try obj = Script.runInThisContext obj
catch e
return
return unless obj?
obj = Object obj
candidates = Object.getOwnPropertyNames obj
while obj = Object.getPrototypeOf obj
for key in Object.getOwnPropertyNames obj when key not in candidates
candidates.push key
completions = getCompletions prefix, candidates
[completions, prefix]
# Attempt to autocomplete an in-scope free variable: `one`.
completeVariable = (text) ->
free = text.match(SIMPLEVAR)?[1]
free = "" if text is ""
if free?
vars = Script.runInThisContext 'Object.getOwnPropertyNames(Object(this))'
keywords = (r for r in CoffeeScript.RESERVED when r[..1] isnt '__')
candidates = vars
for key in keywords when key not in candidates
candidates.push key
completions = getCompletions free, candidates
[completions, free]
# Return elements of candidates for which `prefix` is a prefix.
getCompletions = (prefix, candidates) ->
el for el in candidates when 0 is el.indexOf prefix
# Make sure that uncaught exceptions don't kill the REPL.
process.on 'uncaughtException', error
# The current backlog of multi-line code.
backlog = ''
# The main REPL function. **run** is called every time a line of code is entered.
# Attempt to evaluate the command. If there's an exception, print it out instead
# of exiting.
run = (buffer) ->
# remove single-line comments
buffer = buffer.replace /(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, "$1$2$3"
# remove trailing newlines
buffer = buffer.replace /[\r\n]+$/, ""
if multilineMode
backlog += "#{buffer}\n"
repl.setPrompt REPL_PROMPT_CONTINUATION
repl.prompt()
# Proxy node's line listener
nodeLineListener = rli.listeners('line')[0]
rli.removeListener 'line', nodeLineListener
rli.on 'line', (cmd) ->
if multiline.enabled
multiline.buffer += "#{cmd}\n"
rli.setPrompt multiline.prompt
rli.prompt true
else
nodeLineListener cmd
return
if !buffer.toString().trim() and !backlog
repl.prompt()
# Handle Ctrl-v
inputStream.on 'keypress', (char, key) ->
return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
if multiline.enabled
# allow arbitrarily switching between modes any time before multiple lines are entered
unless multiline.buffer.match /\n/
multiline.enabled = not multiline.enabled
rli.setPrompt repl.prompt
rli.prompt true
return
# no-op unless the current line is empty
return if rli.line? and not rli.line.match /^\s*$/
# eval, print, loop
multiline.enabled = not multiline.enabled
rli.line = ''
rli.cursor = 0
rli.output.cursorTo 0
rli.output.clearLine 1
# XXX: multiline hack
multiline.buffer = multiline.buffer.replace /\n/g, '\uFF00'
rli.emit 'line', multiline.buffer
multiline.buffer = ''
else
multiline.enabled = not multiline.enabled
rli.setPrompt multiline.initialPrompt
rli.prompt true
return
code = backlog += buffer
if code[code.length - 1] is '\\'
backlog = "#{backlog[...-1]}\n"
repl.setPrompt REPL_PROMPT_CONTINUATION
repl.prompt()
return
repl.setPrompt REPL_PROMPT
backlog = ''
try
_ = global._
returnValue = CoffeeScript.eval "_=(#{code}\n)", {
filename: 'repl'
modulename: 'repl'
}
if returnValue is undefined
global._ = _
repl.output.write "#{inspect returnValue, no, 2, enableColours}\n"
catch err
error err
repl.prompt()
if stdin.readable and stdin.isRaw
# handle piped input
pipedInput = ''
repl =
prompt: -> stdout.write @_prompt
setPrompt: (p) -> @_prompt = p
input: stdin
output: stdout
on: ->
stdin.on 'data', (chunk) ->
pipedInput += chunk
return unless /\n/.test pipedInput
lines = pipedInput.split "\n"
pipedInput = lines[lines.length - 1]
for line in lines[...-1] when line
stdout.write "#{line}\n"
run line
return
stdin.on 'end', ->
for line in pipedInput.trim().split "\n" when line
stdout.write "#{line}\n"
run line
stdout.write '\n'
process.exit 0
else
# Create the REPL by listening to **stdin**.
if readline.createInterface.length < 3
repl = readline.createInterface stdin, autocomplete
stdin.on 'data', (buffer) -> repl.write buffer
else
repl = readline.createInterface stdin, stdout, autocomplete
multilineMode = off
# Handle multi-line mode switch
repl.input.on 'keypress', (char, key) ->
# test for Ctrl-v
return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
cursorPos = repl.cursor
repl.output.cursorTo 0
repl.output.clearLine 1
multilineMode = not multilineMode
repl._line() if not multilineMode and backlog
backlog = ''
repl.setPrompt (newPrompt = if multilineMode then REPL_PROMPT_MULTILINE else REPL_PROMPT)
repl.prompt()
repl.output.cursorTo newPrompt.length + (repl.cursor = cursorPos)
# Handle Ctrl-d press at end of last line in multiline mode
repl.input.on 'keypress', (char, key) ->
return unless multilineMode and repl.line
# test for Ctrl-d
return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'd'
multilineMode = off
repl._line()
repl.on 'attemptClose', ->
if multilineMode
multilineMode = off
repl.output.cursorTo 0
repl.output.clearLine 1
repl._onLine repl.line
return
if backlog or repl.line
backlog = ''
repl.historyIndex = -1
repl.setPrompt REPL_PROMPT
repl.output.write '\n(^C again to quit)'
repl._line (repl.line = '')
else
repl.close()
repl.on 'close', ->
repl.output.write '\n'
repl.input.destroy()
repl.on 'line', run
repl.setPrompt REPL_PROMPT
repl.prompt()
module.exports =
start: (opts = {}) ->
opts = merge replDefaults, opts
repl = nodeREPL.start opts
repl.on 'exit', -> repl.outputStream.write '\n'
addMultilineHandler repl
repl

View File

@ -1,4 +1,80 @@
# REPL
# ----
Stream = require 'stream'
# TODO: add tests
class MockInputStream extends Stream
constructor: ->
@readable = true
resume: ->
emitLine: (val) ->
@emit 'data', new Buffer("#{val}\n")
class MockOutputStream extends Stream
constructor: ->
@writable = true
@written = []
write: (data) ->
#console.log 'output write', arguments
@written.push data
lastWrite: (fromEnd = -1) ->
@written[@written.length - 1 + fromEnd].replace /\n$/, ''
testRepl = (desc, fn) ->
input = new MockInputStream
output = new MockOutputStream
Repl.start {input, output}
test desc, -> fn input, output
ctrlV = { ctrl: true, name: 'v'}
testRepl "starts with coffee prompt", (input, output) ->
eq 'coffee> ', output.lastWrite(0)
testRepl "writes eval to output", (input, output) ->
input.emitLine '1+1'
eq '2', output.lastWrite()
testRepl "comments are ignored", (input, output) ->
input.emitLine '1 + 1 #foo'
eq '2', output.lastWrite()
testRepl "output in inspect mode", (input, output) ->
input.emitLine '"1 + 1\\n"'
eq "'1 + 1\\n'", output.lastWrite()
testRepl "variables are saved", (input, output) ->
input.emitLine "foo = 'foo'"
input.emitLine 'foobar = "#{foo}bar"'
eq "'foobar'", output.lastWrite()
testRepl "empty command evaluates to undefined", (input, output) ->
input.emitLine ''
eq 'undefined', output.lastWrite()
testRepl "ctrl-v toggles multiline prompt", (input, output) ->
input.emit 'keypress', null, ctrlV
eq '------> ', output.lastWrite(0)
input.emit 'keypress', null, ctrlV
eq 'coffee> ', output.lastWrite(0)
testRepl "multiline continuation changes prompt", (input, output) ->
input.emit 'keypress', null, ctrlV
input.emitLine ''
eq '....... ', output.lastWrite(0)
testRepl "evaluates multiline", (input, output) ->
# Stubs. Could assert on their use.
output.cursorTo = (pos) ->
output.clearLine = ->
input.emit 'keypress', null, ctrlV
input.emitLine 'do ->'
input.emitLine ' 1 + 1'
input.emit 'keypress', null, ctrlV
eq '2', output.lastWrite()

View File

@ -106,7 +106,6 @@
'option_parser'
'ranges'
'regexps'
'repl'
'scope'
'slicing_and_splicing'
'soaks'