diff --git a/Cakefile b/Cakefile index b148ee54..0ffa9762 100644 --- a/Cakefile +++ b/Cakefile @@ -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. diff --git a/lib/coffee-script/command.js b/lib/coffee-script/command.js index 10ae763a..01667867 100644 --- a/lib/coffee-script/command.js +++ b/lib/coffee-script/command.js @@ -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); diff --git a/lib/coffee-script/repl.js b/lib/coffee-script/repl.js index c14b8d1b..06f7f321 100644 --- a/lib/coffee-script/repl.js +++ b/lib/coffee-script/repl.js @@ -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); diff --git a/src/command.coffee b/src/command.coffee index 8be84a41..cc10c06d 100644 --- a/src/command.coffee +++ b/src/command.coffee @@ -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' diff --git a/src/repl.coffee b/src/repl.coffee index d94c63eb..c0855b43 100644 --- a/src/repl.coffee +++ b/src/repl.coffee @@ -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 diff --git a/test/repl.coffee b/test/repl.coffee index c683b32a..c08dc6c0 100644 --- a/test/repl.coffee +++ b/test/repl.coffee @@ -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() diff --git a/test/test.html b/test/test.html index 256b2988..a09692c9 100644 --- a/test/test.html +++ b/test/test.html @@ -106,7 +106,6 @@ 'option_parser' 'ranges' 'regexps' - 'repl' 'scope' 'slicing_and_splicing' 'soaks'