# frozen_string_literal: true
#
# irb/completion.rb -
# by Keiju ISHITSUKA([email protected])
# From Original Idea of [email protected]
#
require_relative 'ruby-lex'
module IRB
class DocumentTarget # :nodoc:
attr_reader :name
def initialize(name)
@name = name
end
end
class CommandDocument < DocumentTarget # :nodoc:
end
# Represents a method/class documentation target. May hold multiple names
# when the receiver is ambiguous (e.g. `{}.any?` could be Hash#any? or Proc#any?).
# The dialog popup uses only the first name; the full-screen display renders all.
class MethodDocument < DocumentTarget # :nodoc:
attr_reader :names
def initialize(*names)
super(names.first)
@names = names
end
end
class BaseCompletor # :nodoc:
# Set of reserved words used by Ruby, you should not use these for
# constants or variables
HELP_COMMAND_PREPOSING = /\Ahelp\s+/
def completion_candidates(preposing, target, postposing, bind:)
raise NotImplementedError
end
def doc_namespace(preposing, matched, postposing, bind:)
raise NotImplementedError
end
GEM_PATHS =
if defined?(Gem::Specification)
Gem::Specification.latest_specs(true).map { |s|
s.require_paths.map { |p|
if File.absolute_path?(p)
p
else
File.join(s.full_gem_path, p)
end
}
}.flatten
else
[]
end.freeze
def retrieve_gem_and_system_load_path
candidates = (GEM_PATHS | $LOAD_PATH)
candidates.filter_map do |p|
if p.respond_to?(:to_path)
p.to_path
else
String(p) rescue nil
end
end.sort
end
def retrieve_files_to_require_from_load_path
@files_from_load_path ||=
(
shortest = []
rest = retrieve_gem_and_system_load_path.each_with_object([]) { |path, result|
begin
names = Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: path)
rescue Errno::ENOENT
nil
end
next if names.empty?
names.map! { |n| n.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '') }.sort!
shortest << names.shift
result.concat(names)
}
shortest.sort! | rest
)
end
def command_candidates(target)
if !target.empty?
IRB::Command.command_names.select { _1.start_with?(target) }
else
[]
end
end
def command_document_target(preposing, matched)
if preposing.empty? && IRB::Command.command_names.include?(matched)
CommandDocument.new(matched)
end
end
def retrieve_files_to_require_relative_from_current_dir
@files_from_current_dir ||= Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: '.').map { |path|
path.sub(/\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/, '')
}
end
end
class TypeCompletor < BaseCompletor # :nodoc:
def initialize(context)
@context = context
end
def inspect
ReplTypeCompletor.info
end
def completion_candidates(preposing, target, _postposing, bind:)
# When completing the argument of `help` command, only commands should be candidates
return command_candidates(target) if preposing.match?(HELP_COMMAND_PREPOSING)
commands = if preposing.empty?
command_candidates(target)
# It doesn't make sense to propose commands with other preposing
else
[]
end
result = ReplTypeCompletor.analyze(preposing + target, binding: bind, filename: @context.irb_path)
return commands unless result
encoded_candidates = result.completion_candidates.filter_map do |i|
encoded = i.encode(Encoding.default_external)
target + encoded
rescue Encoding::UndefinedConversionError
# If the string cannot be converted, we just ignore it
nil
end
commands | encoded_candidates
end
def doc_namespace(preposing, matched, _postposing, bind:)
command_document_target(preposing, matched) || begin
result = ReplTypeCompletor.analyze(preposing + matched, binding: bind, filename: @context.irb_path)
result&.doc_namespace('')
end
end
end
class RegexpCompletor < BaseCompletor # :nodoc:
KERNEL_METHODS = ::Kernel.instance_method(:methods)
KERNEL_PRIVATE_METHODS = ::Kernel.instance_method(:private_methods)
KERNEL_INSTANCE_VARIABLES = ::Kernel.instance_method(:instance_variables)
OBJECT_CLASS_INSTANCE_METHOD = ::Object.instance_method(:class)
MODULE_CONSTANTS_INSTANCE_METHOD = ::Module.instance_method(:constants)
using Module.new {
refine ::Binding do
def eval_methods
KERNEL_METHODS.bind_call(receiver)
end
def eval_private_methods
KERNEL_PRIVATE_METHODS.bind_call(receiver)
end
def eval_instance_variables
KERNEL_INSTANCE_VARIABLES.bind_call(receiver)
end
def eval_global_variables
::Kernel.global_variables
end
def eval_class_constants
klass = OBJECT_CLASS_INSTANCE_METHOD.bind_call(receiver)
MODULE_CONSTANTS_INSTANCE_METHOD.bind_call(klass)
end
end
}
def inspect
'RegexpCompletor'
end
def complete_require_path(target, preposing, postposing)
if target =~ /\A(['"])([^'"]+)\Z/
quote = $1
actual_target = $2
else
return nil # It's not String literal
end
case preposing
when /(^|[^\w])require\(? *\z/
retrieve_files_to_require_from_load_path.filter_map { |path|
quote + path if path.start_with?(actual_target)
}
when /(^|[^\w])require_relative\(? *\z/
retrieve_files_to_require_relative_from_current_dir.filter_map { |path|
quote + path if path.start_with?(actual_target)
}
end
end
def completion_candidates(preposing, target, postposing, bind:)
if result = complete_require_path(target, preposing, postposing)
return result
end
commands = command_candidates(target)
# When completing the argument of `help` command, only commands should be candidates
return commands if preposing.match?(HELP_COMMAND_PREPOSING)
# It doesn't make sense to propose commands with other preposing
commands = [] unless preposing.empty?
completion_data = retrieve_completion_data(target, bind: bind, doc_namespace: false).compact.filter_map do |i|
i.encode(Encoding.default_external)
rescue Encoding::UndefinedConversionError
# If the string cannot be converted, we just ignore it
nil
end
commands | completion_data
end
def doc_namespace(preposing, matched, _postposing, bind:)
command_document_target(preposing, matched) || retrieve_completion_data(matched, bind: bind, doc_namespace: true)
end
def retrieve_completion_data(input, bind:, doc_namespace:)
case input
# this regexp only matches the closing character because of irb's Reline.completer_quote_characters setting
# details are described in: https://github.com/ruby/irb/pull/523
when /^(.*["'`])\.([^.]*)$/
# String
receiver = $1
message = $2
if doc_namespace
"String.#{message}"
else
candidates = String.instance_methods.collect{|m| m.to_s}
select_message(receiver, message, candidates)
end
# this regexp only matches the closing character because of irb's Reline.completer_quote_characters setting
# details are described in: https://github.com/ruby/irb/pull/523
when /^(.*\/)\.([^.]*)$/
# Regexp
receiver = $1
message = $2
if doc_namespace
"Regexp.#{message}"
else
candidates = Regexp.instance_methods.collect{|m| m.to_s}
select_message(receiver, message, candidates)
end
when /^([^\]]*\])\.([^.]*)$/
# Array
receiver = $1
message = $2
if doc_namespace
"Array.#{message}"
else
candidates = Array.instance_methods.collect{|m| m.to_s}
select_message(receiver, message, candidates)
end
when /^([^\}]*\})\.([^.]*)$/
# Hash or Proc
receiver = $1
message = $2
if doc_namespace
["Hash.#{message}", "Proc.#{message}"]
else
hash_candidates = Hash.instance_methods.collect{|m| m.to_s}
proc_candidates = Proc.instance_methods.collect{|m| m.to_s}
select_message(receiver, message, hash_candidates | proc_candidates)
end
when /^(:[^:.]+)$/
# Symbol
if doc_namespace
nil
else
sym = $1
candidates = Symbol.all_symbols.filter_map do |s|
s.inspect
rescue EncodingError
# ignore
end
candidates.grep(/^#{Regexp.quote(sym)}/)
end
when /^::([A-Z][^:\.\(\)]*)$/
# Absolute Constant or class methods
receiver = $1
candidates = Object.constants.collect{|m| m.to_s}
if doc_namespace
candidates.find { |i| i == receiver }
else
candidates.grep(/^#{Regexp.quote(receiver)}/).collect{|e| "::" + e}
end
when /^([A-Z].*)::([^:.]*)$/
# Constant or class methods
receiver = $1
message = $2
if doc_namespace
"#{receiver}::#{message}"
else
begin
candidates = eval("#{receiver}.constants.collect{|m| m.to_s}", bind)
candidates |= eval("#{receiver}.methods.collect{|m| m.to_s}", bind)
rescue Exception
candidates = []
end
select_message(receiver, message, candidates.sort, "::")
end
when /^(:[^:.]+)(\.|::)([^.]*)$/
# Symbol
receiver = $1
sep = $2
message = $3
if doc_namespace
"Symbol.#{message}"
else
candidates = Symbol.instance_methods.collect{|m| m.to_s}
select_message(receiver, message, candidates, sep)
end
when /^(?