summaryrefslogtreecommitdiff
path: root/lib/pry/commands/code_collector.rb
blob: 5ca385f5baa90dea167e49a8e3f41bc7bc035f62 (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
# frozen_string_literal: true

class Pry
  class Command
    class CodeCollector
      include Helpers::CommandHelpers

      attr_reader :args
      attr_reader :opts
      attr_reader :pry_instance

      # The name of the explicitly given file (if any).
      attr_accessor :file

      class << self
        attr_accessor :input_expression_ranges
        attr_accessor :output_result_ranges
      end

      @input_expression_ranges = []
      @output_result_ranges = []

      def initialize(args, opts, pry_instance)
        @args = args
        @opts = opts
        @pry_instance = pry_instance
      end

      # Add the `--lines`, `-o`, `-i`, `-s`, `-d` options.
      def self.inject_options(opt)
        @input_expression_ranges = []
        @output_result_ranges = []

        opt.on :l, :lines, "Restrict to a subset of lines. Takes a line number " \
                           "or range",
               optional_argument: true, as: Range, default: 1..-1
        opt.on :o, :out, "Select lines from Pry's output result history. " \
                         "Takes an index or range",
               optional_argument: true, as: Range, default: -5..-1 do |r|
          output_result_ranges << (r || (-5..-1))
        end
        opt.on :i, :in, "Select lines from Pry's input expression history. " \
                        "Takes an index or range",
               optional_argument: true, as: Range, default: -5..-1 do |r|
          input_expression_ranges << (r || (-5..-1))
        end
        opt.on :s, :super, "Select the 'super' method. Can be repeated to " \
                           "traverse the ancestors",
               as: :count
        opt.on :d, :doc, "Select lines from the code object's documentation"
      end

      # The content (i.e code/docs) for the selected object.
      # If the user provided a bare code object, it returns the source.
      # If the user provided the `-i` or `-o` switches, it returns the
      # selected input/output lines joined as a string. If the user used
      # `-d CODE_OBJECT` it returns the docs for that code object.
      #
      # @return [String]
      def content
        @content ||=
          begin
            if bad_option_combination?
              raise CommandError,
                    "Only one of --out, --in, --doc and CODE_OBJECT may " \
                    "be specified."
            end

            content = if opts.present?(:o)
                        pry_output_content
                      elsif opts.present?(:i)
                        pry_input_content
                      elsif opts.present?(:d)
                        code_object_doc
                      else
                        code_object_source_or_file
                      end

            restrict_to_lines(content, line_range)
          end
      end

      # The code object
      #
      # @return [Pry::WrappedModule, Pry::Method, Pry::Command]
      def code_object
        Pry::CodeObject.lookup(obj_name, pry_instance, super: opts[:super])
      end

      # Given a string and a range, return the `range` lines of that
      # string.
      #
      # @param [String] content
      # @param [Range, Fixnum] range
      # @return [String] The string restricted to the given range
      def restrict_to_lines(content, range)
        Array(content.lines.to_a[range]).join
      end

      # The selected `pry_instance.output_ring` as a string, as specified by
      # the `-o` switch.
      #
      # @return [String]
      def pry_output_content
        pry_array_content_as_string(
          pry_instance.output_ring,
          self.class.output_result_ranges,
          &:pretty_inspect
        )
      end

      # The selected `pry_instance.input_ring` as a string, as specified by
      # the `-i` switch.
      #
      # @return [String]
      def pry_input_content
        pry_array_content_as_string(
          pry_instance.input_ring, self.class.input_expression_ranges
        ) { |v| v }
      end

      # The line range passed to `--lines`, converted to a 0-indexed range.
      def line_range
        opts.present?(:lines) ? one_index_range_or_number(opts[:lines]) : 0..-1
      end

      # Name of the object argument
      def obj_name
        @obj_name ||= args.empty? ? "" : args.join(" ")
      end

      private

      def bad_option_combination?
        [opts.present?(:in), opts.present?(:out),
         !args.empty?].count(true) > 1
      end

      def pry_array_content_as_string(array, ranges)
        all = ''
        ranges.each do |range|
          if convert_to_range(range).first == 0
            raise CommandError, "Minimum value for range is 1, not 0."
          end

          ranged_array = Array(array[range]) || []
          ranged_array.compact.each { |v| all += yield(v) }
        end

        all
      end

      def code_object_doc
        (code_object && code_object.doc) || could_not_locate(obj_name)
      end

      def code_object_source_or_file
        (code_object && code_object.source) || file_content
      end

      def file_content
        if File.exist?(obj_name)
          # Set the file accessor.
          self.file = obj_name
          File.read(obj_name)
        else
          could_not_locate(obj_name)
        end
      end

      def could_not_locate(name)
        raise CommandError, "Cannot locate: #{name}!"
      end

      def convert_to_range(range)
        return range if range.is_a?(Range)

        (range..range)
      end
    end
  end
end