Skip to content

Commit

Permalink
fixes #185: add persistent history to REPL
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelficarra committed Mar 29, 2013
1 parent c5797a5 commit 1daecc1
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 3 deletions.
58 changes: 57 additions & 1 deletion lib/repl.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions src/repl.coffee
@@ -1,3 +1,5 @@
fs = require 'fs'
path = require 'path'
vm = require 'vm'
nodeREPL = require 'repl'
CoffeeScript = require './module'
Expand Down Expand Up @@ -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'
Expand All @@ -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
2 changes: 2 additions & 0 deletions 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

Expand Down
39 changes: 37 additions & 2 deletions test/repl.coffee
Expand Up @@ -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'}

Expand Down Expand Up @@ -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

0 comments on commit 1daecc1

Please sign in to comment.