summaryrefslogtreecommitdiff
path: root/lib/pry/commands/edit.rb
blob: 4c31e089fa1f74aceb8dd8fe1ad715f1f7ebe23b (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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# 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

      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