Class: Apricot::REPL
Constant Summary collapse
- HISTORY_FILE =
TODO: make history more configurable
"~/.apricot-history"
- MAX_HISTORY_LINES =
1000
- COMMANDS =
{ "!backtrace" => { doc: "Print the backtrace of the most recent exception", code: proc do puts (@exception ? @exception.awesome_backtrace : "No backtrace") end }, "!bytecode" => { doc: "Print the bytecode generated from the previous line", code: proc do puts (@compiled_code ? @compiled_code.decode : "No previous line") end }, "!exit" => {doc: "Exit the REPL", code: proc { exit }}, "!help" => { doc: "Print this message", code: proc do width = 14 puts "(doc foo)".ljust(width) + "Print the documentation for a function or macro" COMMANDS.sort.each {|name, c| puts name.ljust(width) + c[:doc] } end } }
- COMMAND_COMPLETIONS =
COMMANDS.keys.sort
- SPECIAL_COMPLETIONS =
SpecialForm::SPECIAL_FORMS.keys.map(&:to_s)
Instance Method Summary collapse
-
#clear_line ⇒ Object
Clear the current line in the terminal.
-
#constant_completion(s) ⇒ Object
Tab-completion for constants and namespaced identifiers.
-
#find_constant(const_names) ⇒ Object
Find constant Foo::Bar::Baz from [“Foo”, “Bar”, “Baz”] array.
-
#initialize(prompt = 'apr> ', history_file = nil) ⇒ REPL
constructor
A new instance of REPL.
- #load_history ⇒ Object
-
#readline_with_history ⇒ Object
Smarter Readline to prevent empty and dups 1.
- #run ⇒ Object
- #save_history ⇒ Object
Constructor Details
#initialize(prompt = 'apr> ', history_file = nil) ⇒ REPL
Returns a new instance of REPL.
43 44 45 46 47 |
# File 'lib/apricot/repl.rb', line 43 def initialize(prompt = 'apr> ', history_file = nil) @prompt = prompt @history_file = File.(history_file || HISTORY_FILE) @line = 1 end |
Instance Method Details
#clear_line ⇒ Object
Clear the current line in the terminal. This snippet was stolen from Pry.
177 178 179 |
# File 'lib/apricot/repl.rb', line 177 def clear_line puts "\e[0A\e[0G" end |
#constant_completion(s) ⇒ Object
Tab-completion for constants and namespaced identifiers
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/apricot/repl.rb', line 238 def constant_completion(s) # Split Foo/bar into Foo and bar. If there is no / then id will be nil. constant_str, id = s.split('/', 2) # If we have a Foo/bar case, complete the 'bar' part if possible. if id # Split with -1 returns an extra empty string if constant_str ends in # '::'. Then it will fail to find the constant for Foo::/ and we won't # try completing Foo::/ to Foo/whatever. const_names = constant_str.split('::', -1) const = find_constant(const_names) # If we can't find the constant the user is typing, don't return any # completions. If it isn't a Module or Namespace (subclass of Module), # we can't complete methods or vars below it. (e.g. in Math::PI/<tab> # we can't do any completions) return [] unless const && const.is_a?(Module) # Complete the vars of the namespace or the methods of the module. potential_completions = const.is_a?(Apricot::Namespace) ? const.vars.keys : const.methods # Select the matching vars or methods and format them properly as # completions. potential_completions.select do |c| c.to_s.start_with? id end.sort.map do |c| "#{constant_str}/#{c}" end # Otherwise there is no / and we complete constant names. else # Split with -1 returns an extra empty string if constant_str ends in # '::'. This allows us to differentiate Foo:: and Foo cases. const_names = constant_str.split('::', -1) curr_name = const_names.pop # The user is currently typing the last name. const = find_constant(const_names) # If we can't find the constant the user is typing, don't return any # completions. If it isn't a Module, we can't complete constants below # it. (e.g. in Math::PI::<tab> we can't do anything) return [] unless const && const.is_a?(Module) # Select the matching constants and format them properly as # completions. const.constants.select do |c| c.to_s.start_with? curr_name end.sort.map do |name| if const_names.size == 0 name.to_s else "#{const_names.join('::')}::#{name}" end end end end |
#find_constant(const_names) ⇒ Object
Find constant Foo::Bar::Baz from [“Foo”, “Bar”, “Baz”] array. Helper for tab-completion of constants.
228 229 230 231 232 233 234 235 |
# File 'lib/apricot/repl.rb', line 228 def find_constant(const_names) const_names.reduce(Object) do |mod, name| mod.const_get(name) end rescue NameError # Return nil if the constant doesn't exist. nil end |
#load_history ⇒ Object
181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/apricot/repl.rb', line 181 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 |
#readline_with_history ⇒ Object
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
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/apricot/repl.rb', line 209 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. Try to read another line. puts "^C" @line -= 1 retry end |
#run ⇒ Object
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 171 172 173 |
# File 'lib/apricot/repl.rb', line 49 def run # *1, *2, and *3 shall hold the results of the previous three # evaluations. Apricot::Core.set_var(:'*1', nil) Apricot::Core.set_var(:'*2', nil) Apricot::Core.set_var(:'*3', nil) # Set up some Readline options. Readline.completion_append_character = " " Readline.basic_word_break_characters = " \t\n\"'`~@;{[(" # Set up tab completion. Readline.completion_proc = proc do |s| if s.start_with? '!' # User is typing a REPL command COMMAND_COMPLETIONS.select {|c| c.start_with? s } elsif ('A'..'Z').include? s[0] # User is typing a constant constant_completion(s) else # User is typing a regular name 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 # Clear the current line before starting the REPL. This means the user # can begin typing before the prompt appears and it will gracefully # appear in front of their code when the REPL is ready, without any ugly # text duplication issues. clear_line while code = readline_with_history stripped = code.strip # Ignore blank lines. next if stripped.empty? # Handle REPL commands. if stripped.start_with?('!') if COMMANDS.include?(stripped) && block = COMMANDS[stripped][:code] instance_eval(&block) else puts "Unknown command: #{stripped}" end next end # Otherwise treat the input as code to evaluate. begin begin forms = Apricot::Reader.read_string(code, "(eval)", @line) rescue Apricot::SyntaxError => e # Reraise unless this is an incomplete error (meaning we can read # more on the next line). raise unless e.incomplete? begin indent = ' ' * @prompt.length more_code = Readline.readline(indent, false) if more_code code << "\n" << indent << more_code Readline::HISTORY.pop Readline::HISTORY << code retry else print "\r" # print the exception at the start of the line raise 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. puts "^C" current_code = Readline::HISTORY.pop @line -= current_code.count("\n") next end end forms.each do |form| @compiled_code = Apricot::Compiler.compile_form(form, "(eval)", @line) value = Rubinius.run_script(@compiled_code) puts "=> #{value.apricot_inspect}" # Save the result of the evaluation in *1 and shift down the older # previous values. old = Apricot::Core.get_var(:'*1') older = Apricot::Core.get_var(:'*2') Apricot::Core.set_var(:'*1', value) Apricot::Core.set_var(:'*2', old) Apricot::Core.set_var(:'*3', older) end e = nil 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.}" 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 |
#save_history ⇒ Object
195 196 197 198 199 200 201 202 203 |
# File 'lib/apricot/repl.rb', line 195 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 |