summaryrefslogtreecommitdiff
path: root/lib/pry/commands/edit.rb
blob: 0e13852601cc179127d88cc48322f652d4a6b0c4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
class Pry
  class Command::Edit < Pry::ClassCommand
    require 'pry/commands/edit/exception_patcher'
    require 'pry/commands/edit/file_and_line_locator'

    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_.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

    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_).edit_tempfile_with_content(initial_temp_file_content,
                                                                  initial_temp_file_content.lines.count)
      silence_warnings do
        eval_string.replace content
      end
      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_, state, file_and_line_for_current_exception).perform_patch
      else
        if code_object.is_a?(Pry::Method)
          code_object.redefine Pry::Editor.new(_pry_).edit_tempfile_with_content(code_object.source)
        else
          raise NotImplementedError, "Cannot yet patch #{code_object} objects!"
        end
      end
    end

    def ensure_file_name_is_valid(file_name)
      raise CommandError, "Cannot find a valid file for #{filename_argument}" if !file_name
      raise CommandError, "#{file_name} is not a valid file name, cannot edit!" if not_a_real_file?(file_name)
    end

    def file_and_line_for_current_exception
      FileAndLineLocator.from_exception(_pry_.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_).invoke_editor(file_name, line, reload?(file_name))
      set_file_and_dir_locals(file_name)

      if reload?(file_name)
        silence_warnings do
          load file_name
        end
      end
    end

    def filename_argument
      args.join(' ')
    end

    def code_object
      @code_object ||=
        !probably_a_file?(filename_argument) &&
        Pry::CodeObject.lookup(filename_argument, _pry_)
    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_.input_ring[opts[:i]] || []).join
      when Integer
        _pry_.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_.config.disable_auto_reload
    end

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

    def initial_temp_file_content
      case
      when opts.present?(:temp)
        ""
      when opts.present?(:in)
        input_expression
      when eval_string.strip != ""
        eval_string
      else
        _pry_.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 =~ /\/|\\/
    end
  end

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