class Apricot::REPL
Constants
- COMMANDS
- COMMAND_COMPLETIONS
- HISTORY_FILE
TODO: make history more configurable
- MAX_HISTORY_LINES
- SPECIAL_COMPLETIONS
Public Class Methods
new(prompt = 'apr> ', history_file = nil)
click to toggle source
# File lib/apricot/repl.rb, line 43 def initialize(prompt = 'apr> ', history_file = nil) @prompt = prompt @history_file = File.expand_path(history_file || HISTORY_FILE) @line = 1 end
Public Instance Methods
clear_line()
click to toggle source
Clear the current line in the terminal. This snippet was stolen from Pry.
# File lib/apricot/repl.rb, line 177 def clear_line puts "\e[0A\e[0G" end
constant_completion(s)
click to toggle source
Tab-completion for constants and namespaced identifiers
# 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)
click to toggle source
Find constant Foo::Bar::Baz from [“Foo”, “Bar”, “Baz”] array. Helper for tab-completion of constants.
# 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()
click to toggle source
# 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()
click to toggle source
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
# 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()
click to toggle source
# 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.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
save_history()
click to toggle source
# 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