From 1daecc17db7d31ee0415cdb40856fad2d7e751b1 Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Thu, 28 Mar 2013 23:12:20 -0700 Subject: [PATCH] fixes #185: add persistent history to REPL refs jashkenas/coffee-script#2886 --- lib/repl.js | 58 +++++++++++++++++++++++++++++++++++++++++++++- src/repl.coffee | 54 ++++++++++++++++++++++++++++++++++++++++++ test/_setup.coffee | 2 ++ test/repl.coffee | 39 +++++++++++++++++++++++++++++-- 4 files changed, 150 insertions(+), 3 deletions(-) diff --git a/lib/repl.js b/lib/repl.js index bd11fcd0..ad2a2819 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -1,5 +1,7 @@ // Generated by CoffeeScript 2.0.0-beta4 -var addMultilineHandler, CoffeeScript, CS, merge, nodeREPL, vm; +var addHistory, addMultilineHandler, CoffeeScript, CS, fs, merge, nodeREPL, path, vm; +fs = require('fs'); +path = require('path'); vm = require('vm'); nodeREPL = require('repl'); CoffeeScript = require('./module'); @@ -57,6 +59,50 @@ addMultilineHandler = function (repl) { } }); }; +addHistory = function (repl, filename, maxSize) { + var buffer, e, fd, lastLine, original_clear, readFd, size, stat; + try { + stat = fs.statSync(filename); + size = Math.min(maxSize, stat.size); + readFd = fs.openSync(filename, 'r'); + buffer = new Buffer(size); + if (size) + fs.readSync(readFd, buffer, 0, size, stat.size - size); + repl.rli.history = buffer.toString().split('\n').reverse(); + if (stat.size > maxSize) + repl.rli.history.pop(); + if (repl.rli.history[0] === '') + repl.rli.history.shift(); + repl.rli.historyIndex = -1; + } catch (e$) { + e = e$; + repl.rli.history = []; + } + fd = fs.openSync(filename, 'a'); + lastLine = repl.rli.history[0]; + repl.rli.addListener('line', function (code) { + if (code && code !== lastLine) { + lastLine = code; + return fs.writeSync(fd, '' + code + '\n'); + } + }); + original_clear = repl.commands['.clear'].action; + repl.commands['.clear'].action = function () { + repl.outputStream.write('Clearing history...\n'); + repl.rli.history = []; + fs.closeSync(fd); + fd = fs.openSync(filename, 'w'); + lastLine = void 0; + return original_clear.call(this); + }; + return repl.commands['.history'] = { + help: 'Show command history', + action: function () { + repl.outputStream.write('' + repl.rli.history.slice().reverse().join('\n') + '\n'); + return repl.displayPrompt(); + } + }; +}; module.exports = { start: function (opts) { var repl; @@ -67,6 +113,14 @@ module.exports = { opts.ignoreUndefined; else opts.ignoreUndefined = true; + if (null != opts.historyFile) + opts.historyFile; + else + opts.historyFile = path.join(process.env.HOME, '.coffee_history'); + if (null != opts.historyMaxInputSize) + opts.historyMaxInputSize; + else + opts.historyMaxInputSize = 10 * 1024; opts['eval'] || (opts['eval'] = function (input, context, filename, cb) { var err, inputAst, js, jsAst, transformedAst; input = input.replace(/\uFF00/g, '\n'); @@ -96,6 +150,8 @@ module.exports = { return repl.outputStream.write('\n'); }); addMultilineHandler(repl); + if (opts.historyFile) + addHistory(repl, opts.historyFile, opts.historyMaxInputSize); return repl; } }; diff --git a/src/repl.coffee b/src/repl.coffee index 70a14a5b..f8db6f16 100644 --- a/src/repl.coffee +++ b/src/repl.coffee @@ -1,3 +1,5 @@ +fs = require 'fs' +path = require 'path' vm = require 'vm' nodeREPL = require 'repl' CoffeeScript = require './module' @@ -52,11 +54,61 @@ addMultilineHandler = (repl) -> rli.prompt true return +# store and load command history from a file +addHistory = (repl, filename, maxSize) -> + try + stat = fs.statSync filename + size = Math.min maxSize, stat.size + readFd = fs.openSync filename, 'r' + buffer = new Buffer size + # read last `size` bytes from the file + fs.readSync readFd, buffer, 0, size, stat.size - size if size + repl.rli.history = (buffer.toString().split '\n').reverse() + # if the history file was truncated we should pop off a potential partial line + do repl.rli.history.pop if stat.size > maxSize + # shift off the final blank newline + do repl.rli.history.shift if repl.rli.history[0] is '' + repl.rli.historyIndex = -1 + catch e + repl.rli.history = [] + + fd = fs.openSync filename, 'a' + + # like readline's history, we do not want any adjacent duplicates + lastLine = repl.rli.history[0] + + # save new commands to the history file + repl.rli.addListener 'line', (code) -> + if code and code isnt lastLine + lastLine = code + fs.writeSync fd, "#{code}\n" + + #repl.on 'exit', -> fs.closeSync fd + + # .clear should also clear history + original_clear = repl.commands['.clear'].action + repl.commands['.clear'].action = -> + repl.outputStream.write 'Clearing history...\n' + repl.rli.history = [] + fs.closeSync fd + fd = fs.openSync filename, 'w' + lastLine = undefined + original_clear.call this + + # add a command to show the history stack + repl.commands['.history'] = + help: 'Show command history' + action: -> + repl.outputStream.write "#{repl.rli.history[..].reverse().join '\n'}\n" + do repl.displayPrompt + module.exports = start: (opts = {}) -> # REPL defaults opts.prompt or= 'coffee> ' opts.ignoreUndefined ?= yes + opts.historyFile ?= path.join process.env.HOME, '.coffee_history' + opts.historyMaxInputSize ?= 10 * 1024 # 10KiB opts.eval or= (input, context, filename, cb) -> # XXX: multiline hack input = input.replace /\uFF00/g, '\n' @@ -78,4 +130,6 @@ module.exports = repl = nodeREPL.start opts repl.on 'exit', -> repl.outputStream.write '\n' addMultilineHandler repl + if opts.historyFile + addHistory repl, opts.historyFile, opts.historyMaxInputSize repl diff --git a/test/_setup.coffee b/test/_setup.coffee index 286be7f4..cd99fdc3 100644 --- a/test/_setup.coffee +++ b/test/_setup.coffee @@ -1,3 +1,5 @@ +global.fs = require 'fs' +global.path = require 'path' util = require 'util' inspect = (o) -> util.inspect o, no, 2, yes diff --git a/test/repl.coffee b/test/repl.coffee index d2ed47ac..585279ef 100644 --- a/test/repl.coffee +++ b/test/repl.coffee @@ -24,12 +24,15 @@ suite 'REPL', -> lastWrite: (fromEnd) -> @written[@written.length - 1 - fromEnd].replace /\n$/, '' + historyFile = path.join __dirname, 'coffee_history_test' + process.on 'exit', -> fs.unlinkSync historyFile testRepl = (desc, fn) -> input = new MockInputStream output = new MockOutputStream - Repl.start {input, output} - test desc, -> fn input, output + repl = Repl.start {input, output, historyFile} + test desc, -> fn input, output, repl + repl.emit 'exit' ctrlV = { ctrl: true, name: 'v'} @@ -98,3 +101,35 @@ suite 'REPL', -> ok 0 <= (output.lastWrite 1).indexOf 'ReferenceError: a is not defined' input.emitLine '0' eq '0', output.lastWrite 1 + + test 'reads history from persistence file', -> + input = new MockInputStream + output = new MockOutputStream + fs.writeFileSync historyFile, '0\n1\n' + repl = Repl.start {input, output, historyFile} + arrayEq ['1', '0'], repl.rli.history + + testRepl 'writes history to persistence file', (input, output, repl) -> + fs.writeFileSync historyFile, '' + input.emitLine '2' + input.emitLine '3' + eq '2\n3\n', (fs.readFileSync historyFile).toString() + + testRepl '.history shows history', (input, output, repl) -> + repl.rli.history = history = ['1', '2', '3'] + fs.writeFileSync historyFile, "#{history.join '\n'}\n" + input.emitLine '.history' + eq (history.reverse().join '\n'), output.lastWrite 1 + + testRepl '.clear clears history', (input, output, repl) -> + input = new MockInputStream + output = new MockOutputStream + fs.writeFileSync historyFile, '' + repl = Repl.start {input, output, historyFile} + input.emitLine '0' + input.emitLine '1' + eq '0\n1\n', (fs.readFileSync historyFile).toString() + #arrayEq ['1', '0'], repl.rli.history + input.emitLine '.clear' + eq '.clear\n', (fs.readFileSync historyFile).toString() + #arrayEq ['.clear'], repl.rli.history