lib/pry/commands/edit.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

class Pry
  class Command
    class Edit < Pry::ClassCommand
      match 'edit'
      group 'Editing'
      description 'Invoke the default editor on a file.'

      banner <<-'BANNER'
        Usage: edit [--no-reload|--reload|--patch] [--line LINE] [--temp|--ex|FILE[:LINE]|OBJECT|--in N]

        Open a text editor. When no FILE is given, edits the pry input buffer.
        When a method/module/command is given, the code is opened in an editor.
        Ensure `Pry.config.editor` or `pry_instance.config.editor` is set to your editor of choice.

        edit sample.rb                edit -p MyClass#my_method
        edit sample.rb --line 105     edit MyClass
        edit MyClass#my_method        edit --ex
        edit --method                 edit --ex -p

        https://github.com/pry/pry/wiki/Editor-integration#wiki-Edit_command
      BANNER

      command_options state: %i[dynamical_ex_file]

      def options(opt)
        opt.on :e, :ex, "Open the file that raised the most recent exception " \
                        "(_ex_.file)",
               optional_argument: true, as: Integer
        opt.on :i, :in, "Open a temporary file containing the Nth input " \
                        "expression. N may be a range",
               optional_argument: true, as: Range, default: -1..-1
        opt.on :t, :temp, "Open an empty temporary file"
        opt.on :l, :line, "Jump to this line in the opened file",
               argument: true, as: Integer
        opt.on :n, :"no-reload", "Don't automatically reload the edited file"
        opt.on :c, :current, "Open the current __FILE__ and at __LINE__ (as " \
                             "returned by `whereami`)"
        opt.on :r, :reload, "Reload the edited code immediately (default for " \
                            "ruby files)"
        opt.on :p, :patch, "Instead of editing the object's file, try to edit " \
                           "in a tempfile and apply as a monkey patch"
        opt.on :m, :method, "Explicitly edit the _current_ method (when " \
                            "inside a method context)."
      end

      def process
        if bad_option_combination?
          raise CommandError, "Only one of --ex, --temp, --in, --method and " \
                              "FILE may be specified."
        end

        if repl_edit?
          # code defined in pry, eval'd within pry.
          repl_edit
        elsif runtime_patch?
          # patch code without persisting changes, implies future changes are patches
          apply_runtime_patch
        else
          # code stored in actual files, eval'd at top-level
          file_edit
        end
      end

      def repl_edit?
        !opts.present?(:ex) && !opts.present?(:current) && !opts.present?(:method) &&
          filename_argument.empty?
      end

      def repl_edit
        content = Pry::Editor.new(pry_instance).edit_tempfile_with_content(
          initial_temp_file_content,
          initial_temp_file_content.lines.count
        )
        pry_instance.eval_string = content
        Pry.history.push(content)
      end

      def file_based_exception?
        opts.present?(:ex) && !opts.present?(:patch)
      end

      def runtime_patch?
        !file_based_exception? &&
          (opts.present?(:patch) ||
           previously_patched?(code_object) ||
           pry_method?(code_object))
      end

      def apply_runtime_patch
        if patch_exception?
          ExceptionPatcher.new(
            pry_instance, state, file_and_line_for_current_exception
          ).perform_patch
        elsif code_object.is_a?(Pry::Method)
          code_object.redefine(
            Pry::Editor.new(pry_instance).edit_tempfile_with_content(
              code_object.source
            )
          )
        else
          raise NotImplementedError, "Cannot yet patch #{code_object} objects!"
        end
      end

      def ensure_file_name_is_valid(file_name)
        unless file_name
          raise CommandError, "Cannot find a valid file for #{filename_argument}"
        end

        return unless not_a_real_file?(file_name)

        raise CommandError, "#{file_name} is not a valid file name, cannot edit!"
      end

      def file_and_line_for_current_exception
        FileAndLineLocator.from_exception(pry_instance.last_exception, opts[:ex].to_i)
      end

      def file_and_line
        file_name, line =
          if opts.present?(:current)
            FileAndLineLocator.from_binding(target)
          elsif opts.present?(:ex)
            file_and_line_for_current_exception
          elsif code_object
            FileAndLineLocator.from_code_object(code_object, filename_argument)
          else
            # when file and line are passed as a single arg, e.g my_file.rb:30
            FileAndLineLocator.from_filename_argument(filename_argument)
          end

        [file_name, opts.present?(:line) ? opts[:l].to_i : line]
      end

      def file_edit
        file_name, line = file_and_line

        ensure_file_name_is_valid(file_name)

        Pry::Editor.new(pry_instance).invoke_editor(file_name, line, reload?(file_name))
        set_file_and_dir_locals(file_name)

        return unless reload?(file_name)

        silence_warnings { load(file_name) }
      end

      def filename_argument
        args.join(' ')
      end

      def code_object
        @code_object ||=
          !probably_a_file?(filename_argument) &&
          Pry::CodeObject.lookup(filename_argument, pry_instance)
      end

      def pry_method?(code_object)
        code_object.is_a?(Pry::Method) &&
          code_object.pry_method?
      end

      def previously_patched?(code_object)
        code_object.is_a?(Pry::Method) &&
          Pry::Method::Patcher.code_for(code_object.source_location.first)
      end

      def patch_exception?
        opts.present?(:ex) && opts.present?(:patch)
      end

      def bad_option_combination?
        [
          opts.present?(:ex), opts.present?(:temp),
          opts.present?(:in), opts.present?(:method),
          !filename_argument.empty?
        ].count(true) > 1
      end

      def input_expression
        case opts[:i]
        when Range
          (pry_instance.input_ring[opts[:i]] || []).join
        when Integer
          pry_instance.input_ring[opts[:i]] || ""
        else
          raise Pry::CommandError, "Not a valid range: #{opts[:i]}"
        end
      end

      def reloadable?
        opts.present?(:reload) || opts.present?(:ex)
      end

      def never_reload?
        opts.present?(:'no-reload') || pry_instance.config.disable_auto_reload
      end

      def reload?(file_name = "")
        (reloadable? || file_name.end_with?(".rb")) && !never_reload?
      end

      def initial_temp_file_content
        if opts.present?(:temp)
          ""
        elsif opts.present?(:in)
          input_expression
        elsif eval_string.strip != ""
          eval_string
        else
          pry_instance.input_ring.to_a.reverse_each.find { |x| x && x.strip != "" } || ""
        end
      end

      def probably_a_file?(str)
        [".rb", ".c", ".py", ".yml", ".gemspec"].include?(File.extname(str)) ||
          str =~ %r{/|\\}
      end
    end

    Pry::Commands.add_command(Pry::Command::Edit)
  end
end