From 6ba1449bbde6ce9fc4964ceeae6d06ae90d3324e Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Tue, 17 May 2011 23:59:31 -0700 Subject: [PATCH] node 0.4 compat, extend a running repl --- lib/index.js | 335 +++++++++++++++++++++++++++++---------------------- 1 file changed, 190 insertions(+), 145 deletions(-) diff --git a/lib/index.js b/lib/index.js index e81690b..f79463c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,5 @@ // -// Copyright 2010 Sami Samhuri @_sjs +// Copyright 2010 - 2011 Sami Samhuri @_sjs // MIT license // // github.com/samsonjs/repl-edit @@ -8,164 +8,209 @@ // TODO proper error handling, better intregration with node/lib/repl.js var fs = require('fs') - , repl = require('repl') - , _repl - , _tmpdir + , spawn = require('child_process').spawn + , util = require('util') -// Find a suitable directory to store the REPL session -if (process.env['TMPDIR']) _tmpdir = process.env['TMPDIR'] -else { +module.exports = { startRepl: startRepl, extendRepl: extendRepl } + +var repl + +// Start a repl +function startRepl() { + if (repl) { + console.error('repl is already running, only one instance is allowed') + return + } + extendRepl(require('repl').start()) +} + +function extendRepl(theRepl) { + if (repl) { + console.error('repl is already running, only one instance is allowed') + return + } + + if (!theRepl || typeof theRepl.defineCommand !== function) { + console.error('bad argument, repl is not compatible') + return + } + + repl = theRepl + + var tmpfile = makeTempfile() + process.on('exit', function() { try { - fs.statSync('/tmp') - _tmpdir = '/tmp' + fs.unlinkSync(tmpfile) } catch (e) { - _tmpdir = process.cwd() + // might not exist } + }) + + console.log('Commands: .edit, .run, .stash , .unstash , .editor ') + + repl.defineCommand('edit', { + help: 'Edit the current command in your text editor', + action: function(editor) { + edit(tmpfile, editor) + } + }) + + repl.defineCommand('run', { + help: 'Run the previously edited command', + action: function() { + pause() + run(tmpfile, function() { unpause() }) + } + }) + + repl.defineCommand('stash', { + help: 'Write the current command to the named file', + action: function(dest) { + stash(tmpfile, dest) + } + }) + + repl.defineCommand('unstash', { + help: 'Replace the current command with the contents of the named file', + action: function(source) { + unstash(tmpfile, source) + } + }) + + repl.defineCommand('editor', function(editor) { + process.env['VISUAL'] = editor + }) } -exports.startRepl = function() { - console.log('Commands: edit(), run(), stash(filename), unstash(filename), setEditor(editor)') - // TODO extend the repl context instead, problem is that repl.js:resetContext() isn't exported - // so simple assignments to _repl.context can't survive .clear yet - exports.extend(global) - _repl = repl.start() - return exports -} +// Commands -exports.extend = function(obj) { - var path = require('path') - , spawn = require('child_process').spawn - , sys = require('sys') - , _tmpfile = path.join(_tmpdir, 'node-repl-' + process.pid + '.js') - - obj.edit = function(editor) { - editor = editor || process.ENV['VISUAL'] || process.ENV['EDITOR'] - // TODO seed the file with _repl.context._ if the file doesn't exist yet - pausingRepl(function(unpause) { - var fds = [process.openStdin(), process.stdout, process.stdout] - , args = [_tmpfile] - // handle things like 'mate -w' and 'emacsclient --server-file ' - if (editor.match(/\s/)) { - var words = editor.split(/\s+/) // FIXME this should do proper word splitting ... - args = words.slice(1).concat(args) - editor = words[0] - } - spawn(editor, args, {customFds: fds}).on('exit', function(code) { - // some editors change the terminal resulting in skewed output, clean up - spawn('reset').on('exit', function(_) { - if (code === 0) { - runFile(_tmpfile, function() { unpause() }) - } else { - unpause() - } - }) - }) - }) - } - - obj.run = function() { - pausingRepl(function(unpause) { - runFile(_tmpfile, function() { unpause() }) - }) - } - - obj.setEditor = function(editor) { - process.ENV['VISUAL'] = editor - } - - obj.stash = function(dest) { - try { - fs.statSync(_tmpfile) - } catch (e) { - console.log('nothing to stash') - return - } - pausingRepl(function(unpause) { - var read = fs.createReadStream(_tmpfile) - read.on('end', function() { - console.log('stashed') - unpause() - }) - // TODO confirm before overwriting an existing file - sys.pump(read, fs.createWriteStream(dest)) - }) - } - - obj.unstash = function(source) { - try { - fs.statSync(source) - } catch (e) { - console.log('no stash at ' + source) - return - } - pausingRepl(function(unpause) { - var read = fs.createReadStream(source) - read.on('end', function() { - console.log('unstashed') - unpause() - }) - sys.pump(read, fs.createWriteStream(_tmpfile)) - }) - } - - process.on('exit', function() { - try { - fs.unlinkSync(_tmpfile) - } catch (e) { - // might not exist - } +function edit(cmdfile, editor) { + editor = editor || process.env['VISUAL'] || process.env['EDITOR'] + // TODO seed the file with repl.context._ if the file doesn't exist yet + var fds = [process.openStdin(), process.stdout, process.stdout] + , args = [cmdfile] + // handle things like 'mate -w' and 'emacsclient --server-file ' + if (editor.match(/\s/)) { + var words = editor.split(/\s+/) // FIXME this should do proper word splitting ... + args = words.slice(1).concat(args) + editor = words[0] + } + pause() + spawn(editor, args, {customFds: fds}).on('exit', function(code) { + // some editors change the terminal resulting in skewed output, clean up + spawn('reset').on('exit', function(_) { + if (code === 0) { + run(cmdfile, function() { unpause() }) + } else { + unpause() + } }) - - return exports + }) } -function pausingRepl(fn) { - var origPrompt = _repl.prompt || '> ' - _repl.stream.pause() - _repl.rli.enabled = false - _repl.prompt = '' - fn(function() { - _repl.prompt = origPrompt - _repl.rli.enabled = true - _repl.stream.resume() - _repl.displayPrompt() - }) +function stash(cmdFile, dest) { + try { + fs.statSync(cmdFile) + } catch (e) { + console.log('nothing to stash') + return + } + var read = fs.createReadStream(cmdFile) + read.on('end', function() { + console.log('stashed') + unpause() + }) + // TODO confirm before overwriting an existing file + pause() + util.pump(read, fs.createWriteStream(dest)) } -function runFile(filename, callback) { - // check if file exists. might not have been saved. +function unstash(cmdFile, source) { + try { + fs.statSync(source) + } catch (e) { + console.log('no stash at ' + source) + return + } + var read = fs.createReadStream(source) + read.on('end', function() { + console.log('unstashed') + unpause() + }) + pause() + util.pump(read, fs.createWriteStream(cmdFile)) +} + +function run(filename, callback) { + // check if file exists. might not have been saved. + try { + fs.statSync(filename) + } catch (e) { + repl.stream.write('nothing to run\n') + callback() + return + } + + var evalcx = require('vm').Script.runInContext + , read = fs.createReadStream(filename) + , s = '' + read.on('data', function(d) { s += d }) + read.on('end', function() { + // The catchall for errors try { - fs.statSync(filename) + // Use evalcx to supply the global context + var ret = evalcx(s, repl.context, "repl"); + if (ret !== undefined) { + repl.context._ = ret; + repl.writer(ret) + "\n" + } } catch (e) { - _repl.stream.write('nothing to run\n') - callback() - return + // On error: Print the error and clear the buffer + if (e.stack) { + repl.stream.write(e.stack + "\n"); + } else { + repl.stream.write(e.toString() + "\n"); + } } - - var Script = process.binding('evals').Script - , evalcx = Script.runInContext - , read = fs.createReadStream(filename) - , s = '' - read.on('data', function(d) { s += d }) - read.on('end', function() { - // The catchall for errors - try { - // Use evalcx to supply the global context - var ret = evalcx(s, _repl.context, "repl"); - if (ret !== undefined) { - _repl.context._ = ret; - repl.writer(ret) + "\n" - } - } catch (e) { - // On error: Print the error and clear the buffer - if (e.stack) { - _repl.stream.write(e.stack + "\n"); - } else { - _repl.stream.write(e.toString() + "\n"); - } - } - if (callback) callback() - }) + if (callback) callback() + }) } -if (require.main === module) exports.startRepl() +// Support + +var origPrompt +function unpause() { + repl.prompt = origPrompt + repl.rli.enabled = true + repl.outputStream.resume() + repl.inputStream.resume() + repl.displayPrompt() +} + +function pause() { + repl.outputStream.pause() + repl.inputStream.pause() + repl.rli.enabled = false + origPrompt = repl.prompt || '> ' + repl.prompt = '' +} + +function makeTempfile() { + var path = require('path') + , tmpdir + + // Find a suitable directory to store the REPL session + if (process.env['TMPDIR']) tmpdir = process.env['TMPDIR'] + else { + try { + fs.statSync('/tmp') + tmpdir = '/tmp' + } catch (e) { + tmpdir = process.cwd() + } + } + return path.join(tmpdir, 'node-repl-' + process.pid + '.js') +} + +if (require.main === module) startRepl() +else if (module.parent && module.parent.id === 'repl') extendRepl(module.parent.exports.repl)