mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
ca0b592673
There are a few downsides of the current approach: 1. Because gem specs are lazily retrieved, this computation happens in every irb completion test case, which is not necessary. (In tests we don't cache the result of `retrieve_files_to_require_from_load_path`) 2. Gem::Specification.latest_specs is sensible to the content of LOAD_PATH. And when combined with 1, tests fail "randomly" if they try to mutate LOAD_PATH, even though the test subject it's something else. So by pre-computing and storing the gem paths in a constant, it guarantees that the computation only happens once and it doesn't get affected by test cases. One argument could be made against the change is that, it'll store unnecessary data for users that disable autocompletion. But the counter-arguments are: 1. Since autocompletion is enabled by default, this should not be the case for most users. 2. For users with autocompletion enabled, IRB already caches the result of `retrieve_files_to_require_from_load_path` in memory, which should have a similar size of GEM_SPECS. And we currently haven't received any report about problems caused by such memory consumption. https://github.com/ruby/irb/commit/c671d39020
484 lines
14 KiB
Ruby
484 lines
14 KiB
Ruby
# frozen_string_literal: false
|
|
#
|
|
# irb/completion.rb -
|
|
# $Release Version: 0.9$
|
|
# $Revision$
|
|
# by Keiju ISHITSUKA(keiju@ishitsuka.com)
|
|
# From Original Idea of shugo@ruby-lang.org
|
|
#
|
|
|
|
require_relative 'ruby-lex'
|
|
|
|
module IRB
|
|
module InputCompletor # :nodoc:
|
|
using Module.new {
|
|
refine ::Binding do
|
|
def eval_methods
|
|
::Kernel.instance_method(:methods).bind(eval("self")).call
|
|
end
|
|
|
|
def eval_private_methods
|
|
::Kernel.instance_method(:private_methods).bind(eval("self")).call
|
|
end
|
|
|
|
def eval_instance_variables
|
|
::Kernel.instance_method(:instance_variables).bind(eval("self")).call
|
|
end
|
|
|
|
def eval_global_variables
|
|
::Kernel.instance_method(:global_variables).bind(eval("self")).call
|
|
end
|
|
|
|
def eval_class_constants
|
|
::Module.instance_method(:constants).bind(eval("self.class")).call
|
|
end
|
|
end
|
|
}
|
|
|
|
# Set of reserved words used by Ruby, you should not use these for
|
|
# constants or variables
|
|
ReservedWords = %w[
|
|
__ENCODING__ __LINE__ __FILE__
|
|
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
|
|
]
|
|
|
|
BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{("
|
|
|
|
def self.absolute_path?(p) # TODO Remove this method after 2.6 EOL.
|
|
if File.respond_to?(:absolute_path?)
|
|
File.absolute_path?(p)
|
|
else
|
|
File.absolute_path(p) == p
|
|
end
|
|
end
|
|
|
|
GEM_PATHS =
|
|
if defined?(Gem::Specification)
|
|
Gem::Specification.latest_specs(true).map { |s|
|
|
s.require_paths.map { |p|
|
|
if absolute_path?(p)
|
|
p
|
|
else
|
|
File.join(s.full_gem_path, p)
|
|
end
|
|
}
|
|
}.flatten
|
|
else
|
|
[]
|
|
end.freeze
|
|
|
|
def self.retrieve_gem_and_system_load_path
|
|
candidates = (GEM_PATHS | $LOAD_PATH)
|
|
candidates.map do |p|
|
|
if p.respond_to?(:to_path)
|
|
p.to_path
|
|
else
|
|
String(p) rescue nil
|
|
end
|
|
end.compact.sort
|
|
end
|
|
|
|
def self.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 self.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
|
|
|
|
CompletionRequireProc = lambda { |target, preposing = nil, postposing = nil|
|
|
if target =~ /\A(['"])([^'"]+)\Z/
|
|
quote = $1
|
|
actual_target = $2
|
|
else
|
|
return nil # It's not String literal
|
|
end
|
|
tokens = RubyLex.ripper_lex_without_warning(preposing.gsub(/\s*\z/, ''))
|
|
tok = nil
|
|
tokens.reverse_each do |t|
|
|
unless [:on_lparen, :on_sp, :on_ignored_sp, :on_nl, :on_ignored_nl, :on_comment].include?(t.event)
|
|
tok = t
|
|
break
|
|
end
|
|
end
|
|
result = []
|
|
if tok && tok.event == :on_ident && tok.state == Ripper::EXPR_CMDARG
|
|
case tok.tok
|
|
when 'require'
|
|
result = retrieve_files_to_require_from_load_path.select { |path|
|
|
path.start_with?(actual_target)
|
|
}.map { |path|
|
|
quote + path
|
|
}
|
|
when 'require_relative'
|
|
result = retrieve_files_to_require_relative_from_current_dir.select { |path|
|
|
path.start_with?(actual_target)
|
|
}.map { |path|
|
|
quote + path
|
|
}
|
|
end
|
|
end
|
|
result
|
|
}
|
|
|
|
CompletionProc = lambda { |target, preposing = nil, postposing = nil|
|
|
if preposing && postposing
|
|
result = CompletionRequireProc.(target, preposing, postposing)
|
|
unless result
|
|
result = retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) }
|
|
end
|
|
result
|
|
else
|
|
retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) }
|
|
end
|
|
}
|
|
|
|
def self.retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding, doc_namespace: false)
|
|
case input
|
|
when /^((["'`]).*\2)\.([^.]*)$/
|
|
# String
|
|
receiver = $1
|
|
message = $3
|
|
|
|
if doc_namespace
|
|
"String.#{message}"
|
|
else
|
|
candidates = String.instance_methods.collect{|m| m.to_s}
|
|
select_message(receiver, message, candidates)
|
|
end
|
|
|
|
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 /^([^\}]*\})\.([^.]*)$/
|
|
# Proc or Hash
|
|
receiver = $1
|
|
message = $2
|
|
|
|
if doc_namespace
|
|
["Proc.#{message}", "Hash.#{message}"]
|
|
else
|
|
proc_candidates = Proc.instance_methods.collect{|m| m.to_s}
|
|
hash_candidates = Hash.instance_methods.collect{|m| m.to_s}
|
|
select_message(receiver, message, proc_candidates | hash_candidates)
|
|
end
|
|
|
|
when /^(:[^:.]*)$/
|
|
# Symbol
|
|
if doc_namespace
|
|
nil
|
|
else
|
|
sym = $1
|
|
candidates = Symbol.all_symbols.collect do |s|
|
|
":" + s.id2name.encode(Encoding.default_external)
|
|
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(/^#{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 /^(?<num>-?(?:0[dbo])?[0-9_]+(?:\.[0-9_]+)?(?:(?:[eE][+-]?[0-9]+)?i?|r)?)(?<sep>\.|::)(?<mes>[^.]*)$/
|
|
# Numeric
|
|
receiver = $~[:num]
|
|
sep = $~[:sep]
|
|
message = $~[:mes]
|
|
|
|
begin
|
|
instance = eval(receiver, bind)
|
|
|
|
if doc_namespace
|
|
"#{instance.class.name}.#{message}"
|
|
else
|
|
candidates = instance.methods.collect{|m| m.to_s}
|
|
select_message(receiver, message, candidates, sep)
|
|
end
|
|
rescue Exception
|
|
if doc_namespace
|
|
nil
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
when /^(-?0x[0-9a-fA-F_]+)(\.|::)([^.]*)$/
|
|
# Numeric(0xFFFF)
|
|
receiver = $1
|
|
sep = $2
|
|
message = $3
|
|
|
|
begin
|
|
instance = eval(receiver, bind)
|
|
if doc_namespace
|
|
"#{instance.class.name}.#{message}"
|
|
else
|
|
candidates = instance.methods.collect{|m| m.to_s}
|
|
select_message(receiver, message, candidates, sep)
|
|
end
|
|
rescue Exception
|
|
if doc_namespace
|
|
nil
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
when /^(\$[^.]*)$/
|
|
# global var
|
|
gvar = $1
|
|
all_gvars = global_variables.collect{|m| m.to_s}
|
|
|
|
if doc_namespace
|
|
all_gvars.find{ |i| i == gvar }
|
|
else
|
|
all_gvars.grep(Regexp.new(Regexp.quote(gvar)))
|
|
end
|
|
|
|
when /^([^.:"].*)(\.|::)([^.]*)$/
|
|
# variable.func or func.func
|
|
receiver = $1
|
|
sep = $2
|
|
message = $3
|
|
|
|
gv = bind.eval_global_variables.collect{|m| m.to_s}.push("true", "false", "nil")
|
|
lv = bind.local_variables.collect{|m| m.to_s}
|
|
iv = bind.eval_instance_variables.collect{|m| m.to_s}
|
|
cv = bind.eval_class_constants.collect{|m| m.to_s}
|
|
|
|
if (gv | lv | iv | cv).include?(receiver) or /^[A-Z]/ =~ receiver && /\./ !~ receiver
|
|
# foo.func and foo is var. OR
|
|
# foo::func and foo is var. OR
|
|
# foo::Const and foo is var. OR
|
|
# Foo::Bar.func
|
|
begin
|
|
candidates = []
|
|
rec = eval(receiver, bind)
|
|
if sep == "::" and rec.kind_of?(Module)
|
|
candidates = rec.constants.collect{|m| m.to_s}
|
|
end
|
|
candidates |= rec.methods.collect{|m| m.to_s}
|
|
rescue Exception
|
|
candidates = []
|
|
end
|
|
else
|
|
# func1.func2
|
|
candidates = []
|
|
to_ignore = ignored_modules
|
|
ObjectSpace.each_object(Module){|m|
|
|
next if (to_ignore.include?(m) rescue true)
|
|
candidates.concat m.instance_methods(false).collect{|x| x.to_s}
|
|
}
|
|
candidates.sort!
|
|
candidates.uniq!
|
|
end
|
|
|
|
if doc_namespace
|
|
rec_class = rec.is_a?(Module) ? rec : rec.class
|
|
"#{rec_class.name}#{sep}#{candidates.find{ |i| i == message }}"
|
|
else
|
|
select_message(receiver, message, candidates, sep)
|
|
end
|
|
|
|
when /^\.([^.]*)$/
|
|
# unknown(maybe String)
|
|
|
|
receiver = ""
|
|
message = $1
|
|
|
|
candidates = String.instance_methods(true).collect{|m| m.to_s}
|
|
|
|
if doc_namespace
|
|
"String.#{candidates.find{ |i| i == message }}"
|
|
else
|
|
select_message(receiver, message, candidates.sort)
|
|
end
|
|
|
|
else
|
|
if doc_namespace
|
|
vars = (bind.local_variables | bind.eval_instance_variables).collect{|m| m.to_s}
|
|
perfect_match_var = vars.find{|m| m.to_s == input}
|
|
if perfect_match_var
|
|
eval("#{perfect_match_var}.class.name", bind)
|
|
else
|
|
candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s}
|
|
candidates |= ReservedWords
|
|
candidates.find{ |i| i == input }
|
|
end
|
|
else
|
|
candidates = (bind.eval_methods | bind.eval_private_methods | bind.local_variables | bind.eval_instance_variables | bind.eval_class_constants).collect{|m| m.to_s}
|
|
candidates |= ReservedWords
|
|
candidates.grep(/^#{Regexp.quote(input)}/).sort
|
|
end
|
|
end
|
|
end
|
|
|
|
PerfectMatchedProc = ->(matched, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding) {
|
|
begin
|
|
require 'rdoc'
|
|
rescue LoadError
|
|
return
|
|
end
|
|
|
|
RDocRIDriver ||= RDoc::RI::Driver.new
|
|
|
|
if matched =~ /\A(?:::)?RubyVM/ and not ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER']
|
|
IRB.__send__(:easter_egg)
|
|
return
|
|
end
|
|
|
|
namespace = retrieve_completion_data(matched, bind: bind, doc_namespace: true)
|
|
return unless namespace
|
|
|
|
if namespace.is_a?(Array)
|
|
out = RDoc::Markup::Document.new
|
|
namespace.each do |m|
|
|
begin
|
|
RDocRIDriver.add_method(out, m)
|
|
rescue RDoc::RI::Driver::NotFoundError
|
|
end
|
|
end
|
|
RDocRIDriver.display(out)
|
|
else
|
|
begin
|
|
RDocRIDriver.display_names([namespace])
|
|
rescue RDoc::RI::Driver::NotFoundError
|
|
end
|
|
end
|
|
}
|
|
|
|
# Set of available operators in Ruby
|
|
Operators = %w[% & * ** + - / < << <= <=> == === =~ > >= >> [] []= ^ ! != !~]
|
|
|
|
def self.select_message(receiver, message, candidates, sep = ".")
|
|
candidates.grep(/^#{Regexp.quote(message)}/).collect do |e|
|
|
case e
|
|
when /^[a-zA-Z_]/
|
|
receiver + sep + e
|
|
when /^[0-9]/
|
|
when *Operators
|
|
#receiver + " " + e
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.ignored_modules
|
|
# We could cache the result, but this is very fast already.
|
|
# By using this approach, we avoid Module#name calls, which are
|
|
# relatively slow when there are a lot of anonymous modules defined.
|
|
s = {}
|
|
|
|
scanner = lambda do |m|
|
|
next if s.include?(m) # IRB::ExtendCommandBundle::EXCB recurses.
|
|
s[m] = true
|
|
m.constants(false).each do |c|
|
|
value = m.const_get(c)
|
|
scanner.call(value) if value.is_a?(Module)
|
|
end
|
|
end
|
|
|
|
%i(IRB RubyLex).each do |sym|
|
|
next unless Object.const_defined?(sym)
|
|
scanner.call(Object.const_get(sym))
|
|
end
|
|
|
|
s.delete(IRB::Context) if defined?(IRB::Context)
|
|
|
|
s
|
|
end
|
|
end
|
|
end
|