diff options
Diffstat (limited to 'lib/irb/completion.rb')
-rw-r--r-- | lib/irb/completion.rb | 353 |
1 files changed, 188 insertions, 165 deletions
diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index a8a73cce80..b3813e8939 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -1,8 +1,6 @@ -# frozen_string_literal: false +# frozen_string_literal: true # # irb/completion.rb - -# $Release Version: 0.9$ -# $Revision$ # by Keiju ISHITSUKA(keiju@ishitsuka.com) # From Original Idea of shugo@ruby-lang.org # @@ -10,8 +8,7 @@ require_relative 'ruby-lex' module IRB - module InputCompletor # :nodoc: - + class BaseCompletor # :nodoc: # Set of reserved words used by Ruby, you should not use these for # constants or variables @@ -36,35 +33,42 @@ module IRB yield ] - BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{(" + def completion_candidates(preposing, target, postposing, bind:) + raise NotImplementedError + end + + def doc_namespace(preposing, matched, postposing, bind:) + raise NotImplementedError + end - def self.absolute_path?(p) # TODO Remove this method after 2.6 EOL. - if File.respond_to?(:absolute_path?) - File.absolute_path?(p) + 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 - if File.absolute_path(p) == p - true + [] + end.freeze + + def 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 - false + String(p) rescue nil end - end + end.compact.sort end - def self.retrieve_gem_and_system_load_path - gem_paths = 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 if defined?(Gem::Specification) - (gem_paths.to_a | $LOAD_PATH).sort - end - - def self.retrieve_files_to_require_from_load_path - @@files_from_load_path ||= + 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| @@ -82,13 +86,64 @@ module IRB ) end - def self.retrieve_files_to_require_relative_from_current_dir - @@files_from_current_dir ||= Dir.glob("**/*.{rb,#{RbConfig::CONFIG['DLEXT']}}", base: '.').map { |path| + 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:) + result = ReplTypeCompletor.analyze(preposing + target, binding: bind, filename: @context.irb_path) + return [] unless result + result.completion_candidates.map { target + _1 } + end + + def doc_namespace(preposing, matched, _postposing, bind:) + result = ReplTypeCompletor.analyze(preposing + matched, binding: bind, filename: @context.irb_path) + result&.doc_namespace('') + end + end + + class RegexpCompletor < BaseCompletor # :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 + } + + def inspect + 'RegexpCompletor' + end - CompletionRequireProc = lambda { |target, preposing = nil, postposing = nil| + def complete_require_path(target, preposing, postposing) if target =~ /\A(['"])([^'"]+)\Z/ quote = $1 actual_target = $2 @@ -103,61 +158,63 @@ module IRB 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 + return unless tok&.event == :on_ident && tok.state == Ripper::EXPR_CMDARG + + case tok.tok + when 'require' + retrieve_files_to_require_from_load_path.select { |path| + path.start_with?(actual_target) + }.map { |path| + quote + path + } + when 'require_relative' + retrieve_files_to_require_relative_from_current_dir.select { |path| + path.start_with?(actual_target) + }.map { |path| + quote + path + } end - result - } + end - CompletionProc = lambda { |target, preposing = nil, postposing = nil| + def completion_candidates(preposing, target, postposing, bind:) 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) } + result = complete_require_path(target, preposing, postposing) + return result if result end - } + retrieve_completion_data(target, bind: bind, doc_namespace: false).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) + def doc_namespace(_preposing, matched, _postposing, bind:) + retrieve_completion_data(matched, bind: bind, doc_namespace: true) + end + + def retrieve_completion_data(input, bind:, doc_namespace:) case input - when /^((["'`]).*\2)\.([^.]*)$/ + # 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 = $3 + message = $2 - candidates = String.instance_methods.collect{|m| m.to_s} if doc_namespace "String.#{message}" else + candidates = String.instance_methods.collect{|m| m.to_s} select_message(receiver, message, candidates) end - when /^(\/[^\/]*\/)\.([^.]*)$/ + # 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 - candidates = Regexp.instance_methods.collect{|m| m.to_s} if doc_namespace "Regexp.#{message}" else + candidates = Regexp.instance_methods.collect{|m| m.to_s} select_message(receiver, message, candidates) end @@ -166,61 +223,67 @@ module IRB receiver = $1 message = $2 - candidates = Array.instance_methods.collect{|m| m.to_s} 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 + # Hash or Proc receiver = $1 message = $2 - proc_candidates = Proc.instance_methods.collect{|m| m.to_s} - hash_candidates = Hash.instance_methods.collect{|m| m.to_s} if doc_namespace - ["Proc.#{message}", "Hash.#{message}"] + ["Hash.#{message}", "Proc.#{message}"] else - select_message(receiver, message, proc_candidates | hash_candidates) + 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 /^(:[^:.]*)$/ + when /^(:[^:.]+)$/ # Symbol - return nil if doc_namespace - sym = $1 - candidates = Symbol.all_symbols.collect do |s| - ":" + s.id2name.encode(Encoding.default_external) - rescue EncodingError - # ignore + if doc_namespace + nil + else + sym = $1 + candidates = Symbol.all_symbols.collect do |s| + s.inspect + rescue EncodingError + # ignore + end + candidates.grep(/^#{Regexp.quote(sym)}/) end - candidates.grep(/^#{Regexp.quote(sym)}/) - - when /^::([A-Z][^:\.\(]*)$/ + 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} + candidates.grep(/^#{Regexp.quote(receiver)}/).collect{|e| "::" + e} end when /^([A-Z].*)::([^:.]*)$/ # Constant or class methods receiver = $1 message = $2 - 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 + if doc_namespace "#{receiver}::#{message}" else - select_message(receiver, message, candidates, "::") + 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 /^(:[^:.]+)(\.|::)([^.]*)$/ @@ -229,10 +292,10 @@ module IRB sep = $2 message = $3 - candidates = Symbol.instance_methods.collect{|m| m.to_s} if doc_namespace "Symbol.#{message}" else + candidates = Symbol.instance_methods.collect{|m| m.to_s} select_message(receiver, message, candidates, sep) end @@ -244,6 +307,7 @@ module IRB begin instance = eval(receiver, bind) + if doc_namespace "#{instance.class.name}.#{message}" else @@ -254,7 +318,7 @@ module IRB if doc_namespace nil else - candidates = [] + [] end end @@ -276,7 +340,7 @@ module IRB if doc_namespace nil else - candidates = [] + [] end end @@ -284,6 +348,7 @@ module IRB # global var gvar = $1 all_gvars = global_variables.collect{|m| m.to_s} + if doc_namespace all_gvars.find{ |i| i == gvar } else @@ -296,10 +361,10 @@ module IRB sep = $2 message = $3 - gv = eval("global_variables", bind).collect{|m| m.to_s}.push("true", "false", "nil") - lv = eval("local_variables", bind).collect{|m| m.to_s} - iv = eval("instance_variables", bind).collect{|m| m.to_s} - cv = eval("self.class.constants", bind).collect{|m| m.to_s} + 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 @@ -319,17 +384,11 @@ module IRB 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 }}" + "#{rec_class.name}#{sep}#{candidates.find{ |i| i == message }}" rescue nil else select_message(receiver, message, candidates, sep) end @@ -341,69 +400,42 @@ module IRB 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) + select_message(receiver, message, candidates.sort) + end + when /^\s*$/ + # empty input + if doc_namespace + nil + else + [] end - else if doc_namespace - vars = eval("local_variables | instance_variables", bind).collect{|m| m.to_s} + 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) + eval("#{perfect_match_var}.class.name", bind) rescue nil else - candidates = eval("methods | private_methods | local_variables | instance_variables | self.class.constants", bind).collect{|m| m.to_s} + 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 = eval("methods | private_methods | local_variables | instance_variables | self.class.constants", bind).collect{|m| m.to_s} + 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)}/) + 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 = ".") + def select_message(receiver, message, candidates, sep = ".") candidates.grep(/^#{Regexp.quote(message)}/).collect do |e| case e when /^[a-zA-Z_]/ @@ -414,30 +446,21 @@ module IRB end 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 + module InputCompletor + class << self + private def regexp_completor + @regexp_completor ||= RegexpCompletor.new end - %i(IRB RubyLex).each do |sym| - next unless Object.const_defined?(sym) - scanner.call(Object.const_get(sym)) + def retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding, doc_namespace: false) + regexp_completor.retrieve_completion_data(input, bind: bind, doc_namespace: doc_namespace) end - - s.delete(IRB::Context) if defined?(IRB::Context) - - s end + CompletionProc = ->(target, preposing = nil, postposing = nil) { + regexp_completor.completion_candidates(preposing, target, postposing, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding) + } end + deprecate_constant :InputCompletor end |