/
repl.rb
170 lines (147 loc) · 4.78 KB
/
repl.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
require 'readline'
require 'yaml'
module Apricot
class REPL
# TODO: make history more configurable
HISTORY_FILE = "~/.apricot-history"
MAX_HISTORY_LINES = 1000
COMMANDS = {
"!backtrace" => [
"Print the backtrace of the most recent exception",
proc do
puts (@exception ? @exception.awesome_backtrace : "No backtrace")
end
],
"!bytecode" => [
"Print the bytecode generated from the previous line",
proc do
puts (@compiled_code ? @compiled_code.decode : "No previous line")
end
],
"!exit" => ["Exit the REPL", proc { exit }],
"!help" => [
"Print this message",
proc do
COMMANDS.sort.each {|name, a| puts name.ljust(14) + a[0] }
end
]
}
COMMAND_COMPLETIONS = COMMANDS.keys.sort
SPECIAL_COMPLETIONS = SpecialForm::Specials.keys.map(&:to_s)
def initialize(prompt = 'apr> ', bytecode = false, history_file = nil)
@prompt = prompt
@bytecode = bytecode
@history_file = File.expand_path(history_file || HISTORY_FILE)
@line = 1
end
def run
Readline.completion_append_character = " "
Readline.completion_proc = proc do |s|
if s.start_with? '!'
COMMAND_COMPLETIONS.select {|c| c.start_with? s }
else
comps = SPECIAL_COMPLETIONS +
Apricot.current_namespace.vars.keys.map(&:to_s)
comps.select {|c| c.start_with? s }.sort
end
end
load_history
terminal_state = `stty -g`.chomp
while code = readline_with_history
stripped = code.strip
if stripped.empty?
next
elsif stripped.start_with?('!')
if COMMANDS.include?(stripped) && block = COMMANDS[stripped][1]
instance_eval(&block)
else
puts "Unknown command: #{stripped}"
end
next
end
begin
@compiled_code = Apricot::Compiler.compile_string(code, "(eval)",
@line, @bytecode)
value = Rubinius.run_script @compiled_code
puts "=> #{value.apricot_inspect}"
e = nil
rescue Apricot::SyntaxError => e
if e.incomplete?
begin
more_code = Readline.readline(' ' * @prompt.length, false)
if more_code
code << "\n" << more_code
Readline::HISTORY << Readline::HISTORY.pop + "\n" + more_code
retry
else
print "\r" # print the exception at the start of the line
end
rescue Interrupt
# This is raised by Ctrl-C. Stop trying to read more code and
# just give up. Remove the current input from history.
current_code = Readline::HISTORY.pop
@line -= current_code.count "\n"
e = nil # ignore the syntax error since the code was Ctrl-C'd
end
end
rescue Interrupt => e
# Raised by Ctrl-C. Print a newline so the error message is on the
# next line.
puts
rescue SystemExit, SignalException
raise
rescue Exception => e
end
if e
@exception = e
puts "#{e.class}: #{e.message}"
end
@line += 1 + code.count("\n")
end
puts # Print a newline after Ctrl-D (EOF)
ensure
save_history
system('stty', terminal_state) if terminal_state # Restore the terminal
end
def load_history
if File.exist?(@history_file)
hist = YAML.load_file @history_file
if hist.is_a? Array
hist.each {|x| Readline::HISTORY << x }
else
File.open(@history_file) do |f|
f.each {|line| Readline::HISTORY << line.chomp }
end
end
end
end
def save_history
return if Readline::HISTORY.empty?
File.open(@history_file, "w") do |f|
hist = Readline::HISTORY.to_a
hist.shift(hist.size - MAX_HISTORY_LINES) if hist.size > MAX_HISTORY_LINES
YAML.dump(hist, f, header: true)
end
end
# Smarter Readline to prevent empty and dups
# 1. Read a line and append to history
# 2. Quick Break on nil
# 3. Remove from history if empty or dup
def readline_with_history
line = Readline.readline(@prompt, true)
return nil if line.nil?
if line =~ /\A\s*\z/ || (Readline::HISTORY.size > 1 &&
Readline::HISTORY[-2] == line)
Readline::HISTORY.pop
end
line
rescue Interrupt
# This is raised by Ctrl-C. Remove the line from history then try to
# read another line.
puts "^C"
Readline::HISTORY.pop
@line -= 1
retry
end
end
end