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

View File

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

View File

@ -1,276 +1,104 @@
// Generated by CoffeeScript 1.5.0-pre // Generated by CoffeeScript 1.5.0-pre
(function() { (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, var CoffeeScript, addMultilineHandler, merge, nodeREPL, replDefaults, vm;
__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; };
stdin = process.openStdin(); vm = require('vm');
stdout = process.stdout; nodeREPL = require('repl');
CoffeeScript = require('./coffee-script'); CoffeeScript = require('./coffee-script');
readline = require('readline'); merge = require('./helpers').merge;
inspect = require('util').inspect; replDefaults = {
prompt: 'coffee> ',
Script = require('vm').Script; "eval": function(input, context, filename, cb) {
var js;
Module = require('module'); input = input.replace(/\uFF00/g, '\n');
input = input.replace(/(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, '$1$2$3');
REPL_PROMPT = 'coffee> '; if (/^\s*$/.test(input)) {
return cb(null);
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];
try { try {
obj = Script.runInThisContext(obj); js = CoffeeScript.compile("_=(" + input + "\n)", {
} catch (e) { filename: filename,
return; bare: true
}
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];
}
};
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) { } catch (err) {
error(err); cb(err);
}
return cb(null, vm.runInContext(js, context, filename));
} }
return repl.prompt();
}; };
if (stdin.readable && stdin.isRaw) { addMultilineHandler = function(repl) {
pipedInput = ''; var inputStream, multiline, nodeLineListener, outputStream, rli;
repl = { rli = repl.rli, inputStream = repl.inputStream, outputStream = repl.outputStream;
prompt: function() { multiline = {
return stdout.write(this._prompt); enabled: false,
}, initialPrompt: repl.prompt.replace(/^[^> ]*/, function(x) {
setPrompt: function(p) { return x.replace(/./g, '-');
return this._prompt = p; }),
}, prompt: repl.prompt.replace(/^[^> ]*>?/, function(x) {
input: stdin, return x.replace(/./g, '.');
output: stdout, }),
on: function() {} buffer: ''
}; };
stdin.on('data', function(chunk) { nodeLineListener = rli.listeners('line')[0];
var line, lines, _i, _len, _ref; rli.removeListener('line', nodeLineListener);
pipedInput += chunk; rli.on('line', function(cmd) {
if (!/\n/.test(pipedInput)) { if (multiline.enabled) {
return; multiline.buffer += "" + cmd + "\n";
} rli.setPrompt(multiline.prompt);
lines = pipedInput.split("\n"); rli.prompt(true);
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;
}
stdout.write("" + line + "\n");
run(line);
}
});
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);
}
stdout.write('\n');
return process.exit(0);
});
} else { } else {
if (readline.createInterface.length < 3) { nodeLineListener(cmd);
repl = readline.createInterface(stdin, autocomplete); }
stdin.on('data', function(buffer) {
return repl.write(buffer);
}); });
} else { return inputStream.on('keypress', function(char, key) {
repl = readline.createInterface(stdin, stdout, autocomplete);
}
}
multilineMode = false;
repl.input.on('keypress', function(char, key) {
var cursorPos, newPrompt;
if (!(key && key.ctrl && !key.meta && !key.shift && key.name === 'v')) { if (!(key && key.ctrl && !key.meta && !key.shift && key.name === 'v')) {
return; return;
} }
cursorPos = repl.cursor; if (multiline.enabled) {
repl.output.cursorTo(0); if (!multiline.buffer.match(/\n/)) {
repl.output.clearLine(1); multiline.enabled = !multiline.enabled;
multilineMode = !multilineMode; rli.setPrompt(repl.prompt);
if (!multilineMode && backlog) { rli.prompt(true);
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; return;
} }
if (!(key && key.ctrl && !key.meta && !key.shift && key.name === 'd')) { if ((rli.line != null) && !rli.line.match(/^\s*$/)) {
return; return;
} }
multilineMode = false; multiline.enabled = !multiline.enabled;
return repl._line(); rli.line = '';
}); rli.cursor = 0;
rli.output.cursorTo(0);
repl.on('attemptClose', function() { rli.output.clearLine(1);
if (multilineMode) { multiline.buffer = multiline.buffer.replace(/\n/g, '\uFF00');
multilineMode = false; rli.emit('line', multiline.buffer);
repl.output.cursorTo(0); multiline.buffer = '';
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 { } else {
return repl.close(); multiline.enabled = !multiline.enabled;
rli.setPrompt(multiline.initialPrompt);
rli.prompt(true);
} }
}); });
};
repl.on('close', function() { module.exports = {
repl.output.write('\n'); start: function(opts) {
return repl.input.destroy(); var repl;
if (opts == null) {
opts = {};
}
opts = merge(replDefaults, opts);
repl = nodeREPL.start(opts);
repl.on('exit', function() {
return repl.outputStream.write('\n');
}); });
addMultilineHandler(repl);
repl.on('line', run); return repl;
}
repl.setPrompt(REPL_PROMPT); };
repl.prompt();
}).call(this); }).call(this);

View File

@ -68,12 +68,12 @@ exports.run = ->
return usage() if opts.help return usage() if opts.help
return version() if opts.version return version() if opts.version
loadRequires() if opts.require loadRequires() if opts.require
return require './repl' if opts.interactive return require('./repl').start() if opts.interactive
if opts.watch and !fs.watch if opts.watch and !fs.watch
return printWarn "The --watch feature depends on Node v0.6.0+. You are running #{process.version}." return printWarn "The --watch feature depends on Node v0.6.0+. You are running #{process.version}."
return compileStdio() if opts.stdio return compileStdio() if opts.stdio
return compileScript null, sources[0] if opts.eval 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 [] literals = if opts.run then sources.splice 1 else []
process.argv = process.argv[0..1].concat literals process.argv = process.argv[0..1].concat literals
process.argv[0] = 'coffee' 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 vm = require 'vm'
# and evaluates it. Good for simple tests, or poking around the **Node.js** API. nodeREPL = require 'repl'
# 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.
CoffeeScript = require './coffee-script' CoffeeScript = require './coffee-script'
readline = require 'readline' {merge} = require './helpers'
{inspect} = require 'util'
{Script} = require 'vm'
Module = require 'module'
# REPL Setup replDefaults =
prompt: 'coffee> ',
# Config eval: (input, context, filename, cb) ->
REPL_PROMPT = 'coffee> ' # XXX: multiline hack
REPL_PROMPT_MULTILINE = '------> ' input = input.replace /\uFF00/g, '\n'
REPL_PROMPT_CONTINUATION = '......> ' # strip single-line comments
enableColours = no input = input.replace /(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, '$1$2$3'
unless process.platform is 'win32' # empty command
enableColours = not process.env.NODE_DISABLE_COLORS return cb null if /^\s*$/.test input
# TODO: fix #1829: pass in-scope vars and avoid accidentally shadowing them by omitting those declarations
# Log an error.
error = (err) ->
stdout.write (err.stack or err.toString()) + '\n'
## 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()
return
if !buffer.toString().trim() and !backlog
repl.prompt()
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 try
_ = global._ js = CoffeeScript.compile "_=(#{input}\n)", {filename, bare: yes}
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 catch err
error err cb err
repl.prompt() cb null, vm.runInContext(js, context, filename)
if stdin.readable and stdin.isRaw addMultilineHandler = (repl) ->
# handle piped input {rli, inputStream, outputStream} = repl
pipedInput = ''
repl = multiline =
prompt: -> stdout.write @_prompt enabled: off
setPrompt: (p) -> @_prompt = p initialPrompt: repl.prompt.replace(/^[^> ]*/, (x) -> x.replace /./g, '-')
input: stdin prompt: repl.prompt.replace(/^[^> ]*>?/, (x) -> x.replace /./g, '.')
output: stdout buffer: ''
on: ->
stdin.on 'data', (chunk) -> # Proxy node's line listener
pipedInput += chunk nodeLineListener = rli.listeners('line')[0]
return unless /\n/.test pipedInput rli.removeListener 'line', nodeLineListener
lines = pipedInput.split "\n" rli.on 'line', (cmd) ->
pipedInput = lines[lines.length - 1] if multiline.enabled
for line in lines[...-1] when line multiline.buffer += "#{cmd}\n"
stdout.write "#{line}\n" rli.setPrompt multiline.prompt
run line rli.prompt true
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 else
repl = readline.createInterface stdin, stdout, autocomplete nodeLineListener cmd
return
multilineMode = off # Handle Ctrl-v
inputStream.on 'keypress', (char, key) ->
# 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' return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
cursorPos = repl.cursor if multiline.enabled
repl.output.cursorTo 0 # allow arbitrarily switching between modes any time before multiple lines are entered
repl.output.clearLine 1 unless multiline.buffer.match /\n/
multilineMode = not multilineMode multiline.enabled = not multiline.enabled
repl._line() if not multilineMode and backlog rli.setPrompt repl.prompt
backlog = '' rli.prompt true
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 return
if backlog or repl.line # no-op unless the current line is empty
backlog = '' return if rli.line? and not rli.line.match /^\s*$/
repl.historyIndex = -1 # eval, print, loop
repl.setPrompt REPL_PROMPT multiline.enabled = not multiline.enabled
repl.output.write '\n(^C again to quit)' rli.line = ''
repl._line (repl.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 else
repl.close() multiline.enabled = not multiline.enabled
rli.setPrompt multiline.initialPrompt
rli.prompt true
return
repl.on 'close', -> module.exports =
repl.output.write '\n' start: (opts = {}) ->
repl.input.destroy() opts = merge replDefaults, opts
repl = nodeREPL.start opts
repl.on 'line', run repl.on 'exit', -> repl.outputStream.write '\n'
addMultilineHandler repl
repl.setPrompt REPL_PROMPT repl
repl.prompt()

View File

@ -1,4 +1,80 @@
# REPL # 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' 'option_parser'
'ranges' 'ranges'
'regexps' 'regexps'
'repl'
'scope' 'scope'
'slicing_and_splicing' 'slicing_and_splicing'
'soaks' 'soaks'