pry/lib/pry/input_completer.rb

284 lines
10 KiB
Ruby

# 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