Skip to content

Commit

Permalink
RegexpCompletor extends BaseCompletor, move back definition to comple…
Browse files Browse the repository at this point in the history
…tion.rb
  • Loading branch information
tompng committed Oct 10, 2023
1 parent a92c2a2 commit 9bc85a0
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 371 deletions.
369 changes: 368 additions & 1 deletion lib/irb/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@
require_relative 'ruby-lex'

module IRB
module InputCompletor # :nodoc:
class BaseCompletor # :nodoc:
def initialize(target, preposing = nil, postposing = nil)
raise NotImplementedError
end

def completion_candidates
raise NotImplementedError
end

def doc_namespace(matched)
raise NotImplementedError
end

GEM_PATHS =
if defined?(Gem::Specification)
Expand Down Expand Up @@ -61,4 +72,360 @@ def self.retrieve_files_to_require_relative_from_current_dir
}
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
}

# 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
]

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
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 = BaseCompletor.retrieve_files_to_require_from_load_path.select { |path|
path.start_with?(actual_target)
}.map { |path|
quote + path
}
when 'require_relative'
result = BaseCompletor.retrieve_files_to_require_relative_from_current_dir.select { |path|
path.start_with?(actual_target)
}.map { |path|
quote + path
}
end
end
result
end

def initialize(target, preposing, postposing, bind:)
@target = target
@preposing = preposing
@postposing = postposing
@binding = bind
end

def completion_candidates
if @preposing && @postposing
result = complete_require_path(@target, @preposing, @postposing)
return result if result
end
retrieve_completion_data(@target, bind: @binding, doc_namespace: false).compact.map{ |i| i.encode(Encoding.default_external) }
end

def doc_namespace(target)
retrieve_completion_data(target, bind: @binding, 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 /^([^\}]*\})\.([^.]*)$/
# 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.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 /^(?<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 = []
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

# Set of available operators in Ruby
Operators = %w[% & * ** + - / < << <= <=> == === =~ > >= >> [] []= ^ ! != !~]

def 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
end
end
Loading

0 comments on commit 9bc85a0

Please sign in to comment.