summaryrefslogtreecommitdiff
path: root/lib/pry/input_completer.rb
blob: 3059d4b6782a54c1664ccbfafadb80f689877cfc (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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# frozen_string_literal: true

# taken from irb
# Implements tab completion for Readline in Pry
class Pry
  class InputCompleter
    NUMERIC_REGEXP = /^(-?(0[dbo])?[0-9_]+(\.[0-9_]+)?([eE]-?[0-9]+)?)\.([^.]*)$/.freeze
    ARRAY_REGEXP = /^([^\]]*\])\.([^.]*)$/.freeze
    SYMBOL_REGEXP = /^(:[^:.]*)$/.freeze
    SYMBOL_METHOD_CALL_REGEXP = /^(:[^:.]+)\.([^.]*)$/.freeze
    REGEX_REGEXP = %r{^(/[^/]*/)\.([^.]*)$}.freeze
    PROC_OR_HASH_REGEXP = /^([^\}]*\})\.([^.]*)$/.freeze
    TOPLEVEL_LOOKUP_REGEXP = /^::([A-Z][^:\.\(]*)$/.freeze
    CONSTANT_REGEXP = /^([A-Z][A-Za-z0-9]*)$/.freeze
    CONSTANT_OR_METHOD_REGEXP = /^([A-Z].*)::([^:.]*)$/.freeze
    HEX_REGEXP = /^(-?0x[0-9a-fA-F_]+)\.([^.]*)$/.freeze
    GLOBALVARIABLE_REGEXP = /^(\$[^.]*)$/.freeze
    VARIABLE_REGEXP = /^([^."].*)\.([^.]*)$/.freeze

    RESERVED_WORDS = %w[
      BEGIN END
      alias and
      begin break
      case class
      def defined do
      else elsif end ensure
      false for
      if in
      module
      next nil not
      or
      redo rescue retry return
      self super
      then true
      undef unless until
      when while
      yield
    ].freeze

    WORD_ESCAPE_STR = " \t\n\"\\'`><=;|&{(".freeze

    def initialize(input, pry = nil)
      @pry = pry
      @input = input
      if @input.respond_to?(:basic_word_break_characters=)
        @input.basic_word_break_characters = WORD_ESCAPE_STR
      end

      return unless @input.respond_to?(:completion_append_character=)

      @input.completion_append_character = nil
    end

    # Return a new completion proc for use by Readline.
    # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
    def call(str, options = {})
      custom_completions = options[:custom_completions] || []
      # if there are multiple contexts e.g. cd 1/2/3
      # get new target for 1/2 and find candidates for 3
      path, input = build_path(str)

      if path.call.empty?
        target = options[:target]
      else
        # Assume the user is tab-completing the 'cd' command
        begin
          target = Pry::ObjectPath.new(path.call, @pry.binding_stack).resolve.last
        # but if that doesn't work, assume they're doing division with no spaces
        rescue Pry::CommandError
          target = options[:target]
        end
      end

      begin
        bind = target
        # Complete stdlib symbols
        case input
        when REGEX_REGEXP # Regexp
          receiver = Regexp.last_match(1)
          message = Regexp.quote(Regexp.last_match(2))
          candidates = Regexp.instance_methods.collect(&:to_s)
          select_message(path, receiver, message, candidates)
        when ARRAY_REGEXP # Array
          receiver = Regexp.last_match(1)
          message = Regexp.quote(Regexp.last_match(2))
          candidates = Array.instance_methods.collect(&:to_s)
          select_message(path, receiver, message, candidates)
        when PROC_OR_HASH_REGEXP # Proc or Hash
          receiver = Regexp.last_match(1)
          message = Regexp.quote(Regexp.last_match(2))
          candidates = Proc.instance_methods.collect(&:to_s)
          candidates |= Hash.instance_methods.collect(&:to_s)
          select_message(path, receiver, message, candidates)
        when SYMBOL_REGEXP # Symbol
          if Symbol.respond_to?(:all_symbols)
            sym = Regexp.quote(Regexp.last_match(1))
            candidates = Symbol.all_symbols.collect { |s| ":" + s.id2name }
            candidates.grep(/^#{sym}/)
          else
            []
          end
        when TOPLEVEL_LOOKUP_REGEXP # Absolute Constant or class methods
          receiver = Regexp.last_match(1)
          candidates = Object.constants.collect(&:to_s)
          candidates.grep(/^#{receiver}/).collect { |e| "::" + e }
        when CONSTANT_REGEXP # Constant
          message = Regexp.last_match(1)
          begin
            context = target.eval("self")
            context = context.class unless context.respond_to? :constants
            candidates = context.constants.collect(&:to_s)
          rescue StandardError
            candidates = []
          end
          candidates = candidates.grep(/^#{message}/).collect(&path)
        when CONSTANT_OR_METHOD_REGEXP # Constant or class methods
          receiver = Regexp.last_match(1)
          message = Regexp.quote(Regexp.last_match(2))
          begin
            candidates = eval( # rubocop:disable Security/Eval
              "#{receiver}.constants.collect(&:to_s)", bind, __FILE__, __LINE__
            )
            candidates |= eval( # rubocop:disable Security/Eval
              "#{receiver}.methods.collect(&:to_s)", bind, __FILE__, __LINE__
            )
          rescue Pry::RescuableException
            candidates = []
          end
          candidates.grep(/^#{message}/).collect { |e| receiver + "::" + e }
        when SYMBOL_METHOD_CALL_REGEXP # method call on a Symbol
          receiver = Regexp.last_match(1)
          message = Regexp.quote(Regexp.last_match(2))
          candidates = Symbol.instance_methods.collect(&:to_s)
          select_message(path, receiver, message, candidates)
        when NUMERIC_REGEXP
          # Numeric
          receiver = Regexp.last_match(1)
          message = Regexp.quote(Regexp.last_match(5))
          begin
            # rubocop:disable Security/Eval
            candidates = eval(receiver, bind).methods.collect(&:to_s)
            # rubocop:enable Security/Eval
          rescue Pry::RescuableException
            candidates = []
          end
          select_message(path, receiver, message, candidates)
        when HEX_REGEXP
          # Numeric(0xFFFF)
          receiver = Regexp.last_match(1)
          message = Regexp.quote(Regexp.last_match(2))
          begin
            # rubocop:disable Security/Eval
            candidates = eval(receiver, bind).methods.collect(&:to_s)
            # rubocop:enable Security/Eval
          rescue Pry::RescuableException
            candidates = []
          end
          select_message(path, receiver, message, candidates)
        when GLOBALVARIABLE_REGEXP # global
          regmessage = Regexp.new(Regexp.quote(Regexp.last_match(1)))
          candidates = global_variables.collect(&:to_s).grep(regmessage)
        when VARIABLE_REGEXP # variable
          receiver = Regexp.last_match(1)
          message = Regexp.quote(Regexp.last_match(2))

          gv = eval("global_variables", bind, __FILE__, __LINE__).collect(&:to_s)
          lv = eval("local_variables", bind, __FILE__, __LINE__).collect(&:to_s)
          cv = eval("self.class.constants", bind, __FILE__, __LINE__).collect(&:to_s)

          if (gv | lv | cv).include?(receiver) || /^[A-Z]/ =~ receiver && /\./ !~ receiver
            # foo.func and foo is local var. OR
            # Foo::Bar.func
            begin
              candidates = eval( # rubocop:disable Security/Eval
                "#{receiver}.methods", bind, __FILE__, __LINE__
              ).collect(&:to_s)
            rescue Pry::RescuableException
              candidates = []
            end
          else
            # func1.func2
            require 'set'
            candidates = Set.new
            to_ignore = ignored_modules
            ObjectSpace.each_object(Module) do |m|
              next if begin
                        to_ignore.include?(m)
                      rescue StandardError
                        true
                      end

              # jruby doesn't always provide #instance_methods() on each
              # object.
              if m.respond_to?(:instance_methods)
                candidates.merge m.instance_methods(false).collect(&:to_s)
              end
            end
          end
          select_message(path, receiver, message, candidates.sort)
        when /^\.([^.]*)$/
          # Unknown(maybe String)
          receiver = ""
          message = Regexp.quote(Regexp.last_match(1))
          candidates = String.instance_methods(true).collect(&:to_s)
          select_message(path, receiver, message, candidates)
        else
          candidates = eval(
            "methods | private_methods | local_variables | " \
            "self.class.constants | instance_variables",
            bind, __FILE__, __LINE__ - 2
          ).collect(&:to_s)

          if eval("respond_to?(:class_variables)", bind, __FILE__, __LINE__)
            candidates += eval(
              "class_variables", bind, __FILE__, __LINE__
            ).collect(&:to_s)
          end
          candidates =
            (candidates | RESERVED_WORDS | custom_completions)
              .grep(/^#{Regexp.quote(input)}/)
          candidates.collect(&path)
        end
      rescue Pry::RescuableException
        []
      end
    end
    # rubocop:enable Metrics/AbcSize, Metrics/MethodLength

    def select_message(path, receiver, message, candidates)
      candidates.grep(/^#{message}/).collect do |e|
        next unless e =~ /^[a-zA-Z_]/

        path.call(receiver + "." + e)
      end.compact
    end

    # build_path seperates the input into two parts: path and input.
    # input is the partial string that should be completed
    # path is a proc that takes an input and builds a full path.
    def build_path(input)
      # check to see if the input is a regex
      return proc { |i| i.to_s }, input if input[%r{/\.}]

      trailing_slash = input.end_with?('/')
      contexts = input.chomp('/').split(%r{/})
      input = contexts[-1]
      path = proc do |i|
        p = contexts[0..-2].push(i).join('/')
        p += '/' if trailing_slash && !i.nil?
        p
      end
      [path, input]
    end

    def ignored_modules
      # We could cache the result, but IRB is not loaded by default.
      # And this is very fast anyway.
      # By using this approach, we avoid Module#name calls, which are
      # relatively slow when there are a lot of anonymous modules defined.
      s = Set.new

      scanner = lambda do |m|
        next if s.include?(m) # IRB::ExtendCommandBundle::EXCB recurses.

        s << m
        m.constants(false).each do |c|
          value = m.const_get(c)
          scanner.call(value) if value.is_a?(Module)
        end
      end

      # FIXME: Add Pry here as well?
      %i[IRB SLex RubyLex RubyToken].each do |module_name|
        next unless Object.const_defined?(module_name)

        scanner.call(Object.const_get(module_name))
      end

      s.delete(IRB::Context) if defined?(IRB::Context)
      s
    end
  end
end