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