lib/byebug/processors/command_processor.rb



module Byebug

  class CommandProcessor < Processor
    attr_reader :display

    def initialize(interface = LocalInterface.new)
      super(interface)

      @display = []
      @mutex = Mutex.new
      @last_cmd         = nil   # To allow empty (just <RET>) commands
      @last_file        = nil   # Filename the last time we stopped
      @last_line        = nil   # Line number the last time we stopped
      @context_was_dead = false # Assume we haven't started.
    end

    def interface=(interface)
      @mutex.synchronize do
        @interface.close if @interface
        @interface = interface
      end
    end

    require 'pathname'  # For cleanpath

    #
    # Regularize file name.
    #
    # This is also used as a common funnel place if basename is desired or if we
    # are working remotely and want to change the basename. Or we are eliding
    # filenames.
    def self.canonic_file(filename)
      return filename if ['(irb)', '-e'].include?(filename)

      # For now we want resolved filenames
      if Setting[:basename]
        File.basename(filename)
      else
        Pathname.new(filename).cleanpath.to_s
      end
    end

    def self.protect(mname)
      alias_method "__#{mname}", mname
      module_eval <<-END, __FILE__, __LINE__+1
        def #{mname}(*args)
          @mutex.synchronize do
            return unless @interface
            __#{mname}(*args)
          end
        rescue IOError, SystemCallError
          @interface.close
        rescue SignalException
          raise
        rescue
          print "INTERNAL ERROR!!! #\{$!\}\n" rescue nil
          print $!.backtrace.map{|l| "\t#\{l\}"}.join("\n") rescue nil
        end
      END
    end

    def at_breakpoint(context, breakpoint)
      n = Byebug.breakpoints.index(breakpoint) + 1
      file = CommandProcessor.canonic_file(breakpoint.source)
      line = breakpoint.pos
      print "Stopped by breakpoint #{n} at #{file}:#{line}\n"
    end
    protect :at_breakpoint

    def at_catchpoint(context, excpt)
      file = CommandProcessor.canonic_file(context.frame_file(0))
      line = context.frame_line(0)
      print "Catchpoint at %s:%d: `%s' (%s)\n", file, line, excpt, excpt.class
    end
    protect :at_catchpoint

    def at_tracing(context, file, line)
      if file != @last_file || line != @last_line || Setting[:tracing_plus]
        @last_file, @last_line = file, line
        print "Tracing: #{CommandProcessor.canonic_file(file)}:#{line} " \
              "#{Byebug.line_at(file,line)}\n"
      end
      always_run(context, file, line, 2)
    end
    protect :at_tracing

    def at_line(context, file, line)
      Byebug.source_reload if Setting[:autoreload]
      process_commands(context, file, line)
    end
    protect :at_line

    def at_return(context, file, line)
      process_commands(context, file, line)
    end
    protect :at_return

    private
      #
      # Prompt shown before reading a command.
      #
      def prompt(context)
        return "(byebug#{context.dead?  ? ':post-mortem' : ''}) "
      end

      #
      # Run commands everytime.
      #
      # For example display commands or possibly the list or irb in an
      # "autolist" or "autoirb".
      #
      # @return List of commands acceptable to run bound to the current state
      #
      def always_run(context, file, line, run_level)
        cmds = Command.commands

        state = State.new(cmds, context, @display, file, @interface, line)

        # Change default when in irb or code included in command line
        Setting[:autolist] = false if ['(irb)', '-e'].include?(file)

        # Bind commands to the current state.
        commands = cmds.map { |cmd| cmd.new(state) }

        commands.select { |cmd| cmd.class.always_run >= run_level }
                .each { |cmd| cmd.execute }

        return state, commands
      end

      #
      # Splits a command line of the form "cmd1 ; cmd2 ; ... ; cmdN" into an
      # array of commands: [cmd1, cmd2, ..., cmdN]
      #
      def split_commands(cmd_line)
        cmd_line.split(/;/).inject([]) do |m, v|
          if m.empty?
            m << v
          else
            if m.last[-1] == ?\\
              m.last[-1,1] = ''
              m.last << ';' << v
            else
              m << v
            end
          end
          m
        end
      end

      #
      # Handle byebug commands.
      #
      def process_commands(context, file, line)
        state, commands = always_run(context, file, line, 1)

        if Setting[:testing]
          Thread.current.thread_variable_set('state', state)
        else
          Thread.current.thread_variable_set('state', nil)
        end

        preloop(commands, context)
        print state.location if Setting[:autolist] == 0

        while !state.proceed?
          input = @interface.command_queue.empty? ?
                  @interface.read_command(prompt(context)) :
                  @interface.command_queue.shift
          break unless input
          catch(:debug_error) do
            if input == ""
              next unless @last_cmd
              input = @last_cmd
            else
              @last_cmd = input
            end
            split_commands(input).each do |cmd|
              one_cmd(commands, context, cmd)
            end
          end
        end
      end

      #
      # Executes a single byebug command
      #
      def one_cmd(commands, context, input)
        if cmd = commands.find { |c| c.match(input) }
          if context.dead? && !cmd.class.allow_in_post_mortem
            errmsg "Command unavailable in post mortem mode.\n"
          else
            cmd.execute
          end
        else
          unknown_cmd = commands.find { |c| c.class.unknown }
          if unknown_cmd
            unknown_cmd.execute
          else
            errmsg "Unknown command: \"#{input}\". Try \"help\".\n"
          end
        end
      end

      #
      # Tasks to do before processor loop
      #
      def preloop(commands, context)
        @context_was_dead = true if context.dead? and not @context_was_dead

        if @context_was_dead
          print "The program finished.\n"
          @context_was_dead = false
        end
      end

      class State
        attr_accessor :commands, :context, :display, :file, :frame_pos
        attr_accessor :interface, :line, :previous_line

        def initialize(commands, context, display, file, interface, line)
          @commands, @context, @display = commands, context, display
          @file, @interface, @line = file, interface, line
          @frame_pos, @previous_line, @proceed = 0, nil, false
        end

        extend Forwardable
        def_delegators :@interface, :errmsg, :print, :confirm

        def proceed?
          @proceed
        end

        def proceed
          @proceed = true
        end

        def location
          loc = "#{CommandProcessor.canonic_file(@file)} @ #{@line}\n"
          loc += "#{Byebug.line_at(@file, @line)}\n" unless
            ['(irb)', '-e'].include? @file
          loc
        end
      end

  end # class CommandProcessor

end