Merge branch 'node_repl_multiline' of https://github.com/asalant/coffee-script into repl-rewrite

This commit is contained in:
Michael Ficarra 2013-01-18 21:16:39 -06:00
commit 2e191dc0e7
6 changed files with 158 additions and 448 deletions

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,84 @@
// 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(code, context, file, cb) {
try {
obj = Script.runInThisContext(obj);
} catch (e) {
return;
}
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);
}
if (/^\(\s+\)$/.test(code)) {
return cb(null);
}
code = code.replace(/子/mg, '\n');
code = CoffeeScript.compile(code, {
filename: file,
bare: true
});
return cb(null, vm.runInContext(code, context, file));
} catch (err) {
return cb(err);
}
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) {
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,
prompt: new Array(repl.prompt.length).join('.') + ' ',
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 === true) {
multiline.buffer += "" + cmd + "\n";
return rli.prompt(true);
} else {
return 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;
multiline.enabled = !multiline.enabled;
if (multiline.enabled === false) {
if (!multiline.buffer.match(/\n/)) {
rli.setPrompt(repl.prompt);
rli.prompt(true);
return;
}
stdout.write("" + line + "\n");
run(line);
multiline.buffer = multiline.buffer.replace(/\n/mg, '子');
rli.emit('line', multiline.buffer);
return multiline.buffer = '';
} else {
rli.setPrompt(multiline.prompt);
return 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);
});
} else {
repl = readline.createInterface(stdin, stdout, autocomplete);
opts = merge(replDefaults, opts);
repl = nodeREPL.start(opts);
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,58 @@
# 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: (code, context, file, cb) ->
try
return cb(null) if /^\(\s+\)$/.test code # Empty command
code = code.replace //mg, '\n' # Temporary hack, see TODO below
code = CoffeeScript.compile(code, {filename: file, bare: true})
cb(null, vm.runInContext(code, context, file))
catch err
cb(err)
# Config
REPL_PROMPT = 'coffee> '
REPL_PROMPT_MULTILINE = '------> '
REPL_PROMPT_CONTINUATION = '......> '
enableColours = no
unless process.platform is 'win32'
enableColours = not process.env.NODE_DISABLE_COLORS
# TODO: how to test?
addMultilineHandler = (repl) ->
{rli, inputStream, outputStream} = repl
# Log an error.
error = (err) ->
stdout.write (err.stack or err.toString()) + '\n'
multiline =
enabled: off
prompt: new Array(repl.prompt.length).join('.') + ' '
buffer: ''
## Autocompletion
# Proxy node's line listener
nodeLineListener = rli.listeners('line')[0]
rli.removeListener 'line', nodeLineListener
rli.on 'line', (cmd) ->
if multiline.enabled is on
multiline.buffer += "#{cmd}\n"
rli.prompt true
else
nodeLineListener(cmd)
# Regexes to match complete-able bits of text.
ACCESSOR = /\s*([\w\.]+)(?:\.(\w*))$/
SIMPLEVAR = /(\w+)$/i
# 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'
multiline.enabled = !multiline.enabled
if multiline.enabled is off
unless multiline.buffer.match /\n/
rli.setPrompt repl.prompt
rli.prompt true
return
# TODO: how to encode line breaks so the node repl will pass the complete multiline to our eval?
multiline.buffer = multiline.buffer.replace /\n/mg, ''
rli.emit 'line', multiline.buffer
multiline.buffer = ''
else
rli.setPrompt multiline.prompt
rli.prompt true
# 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
_ = 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
addMultilineHandler(repl)
repl

View File

@ -1,4 +1,46 @@
# REPL
# ----
# TODO: add tests
# TODO: add more tests
{spawn} = require 'child_process'
PROMPT = 'coffee> '
testOutput = (expected, actual) ->
eq expected, actual.slice(0, expected.length)
actual.substr expected.length
testCommands = (input, expectedOutput) ->
input = [input] if typeof input is 'string'
expectedOutput = [expectedOutput] if typeof expectedOutput in ['string', 'undefined']
output = ''
coffee = spawn 'bin/coffee'
input.push 'process.exit()'
coffee.stdout.on 'data', (data) ->
output += data.toString().replace(/\u001b\[\d{0,2}m/g, '')
coffee.stdin.write "#{input.shift()}\n"
coffee.on 'exit', ->
output = testOutput PROMPT, output
while expectedOutput.length > 0
output = testOutput "#{expectedOutput.shift()}\n#{PROMPT}", output
eq '', output
test "comments are ignored", ->
testCommands "1 + 1 #foo", "2"
test "output in inspect mode", ->
testCommands '"1 + 1\\n"', "'1 + 1\\n'"
test "variables are saved", ->
input = [
"foo = 'foo'"
'foobar = "#{foo}bar"'
]
testCommands input, [
"'foo'"
"'foobar'"
]
test "empty command evaluates to undefined", ->
testCommands "", undefined

View File

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