[ruby/irb] Rename current completor to RegexpCompletor and

refactored for future extension
(https://github.com/ruby/irb/pull/707)

* Move completion implementation to completion/regexp_completor for future extension

* Remove constant CompletionProc and PerfectMatchedProc and add a class method

* Move document display logic to InputCompletor. Each completor only need to implement `completion_caididates` and `doc_namespace`

* Move display_document logic to RelineInputMethod

* Use RegexpCompletor directly. Not through class method of InputCompletor.

* RegexpCompletor extends BaseCompletor, move back definition to completion.rb

* Move display_document test to input_method test

* Stop re-initialize completor on each completion phase

* Store completor to ReadlineInputMethod's iver

https://github.com/ruby/irb/commit/1e98521483
This commit is contained in:
tomoya ishida 2023-10-12 02:08:59 +09:00 committed by git
parent b9a6fca67d
commit 94cb5765e2
4 changed files with 378 additions and 339 deletions

View File

@ -8,7 +8,68 @@
require_relative 'ruby-lex'
module IRB
module InputCompletor # :nodoc:
class BaseCompletor # :nodoc:
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.map do |p|
if p.respond_to?(:to_path)
p.to_path
else
String(p) rescue nil
end
end.compact.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 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 RegexpCompletor < BaseCompletor # :nodoc:
using Module.new {
refine ::Binding do
def eval_methods
@ -56,60 +117,7 @@ module IRB
yield
]
BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{("
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 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|
def complete_require_path(target, preposing, postposing)
if target =~ /\A(['"])([^'"]+)\Z/
quote = $1
actual_target = $2
@ -142,21 +150,21 @@ module IRB
end
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
# 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
@ -394,44 +402,10 @@ module IRB
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_]/

View File

@ -11,6 +11,8 @@ require 'reline'
module IRB
class InputMethod
BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{("
# The irb prompt associated with this input method
attr_accessor :prompt
@ -179,12 +181,16 @@ module IRB
super
@eof = false
@completor = RegexpCompletor.new
if Readline.respond_to?("basic_word_break_characters=")
Readline.basic_word_break_characters = IRB::InputCompletor::BASIC_WORD_BREAK_CHARACTERS
Readline.basic_word_break_characters = BASIC_WORD_BREAK_CHARACTERS
end
Readline.completion_append_character = nil
Readline.completion_proc = IRB::InputCompletor::CompletionProc
Readline.completion_proc = ->(target) {
bind = IRB.conf[:MAIN_CONTEXT].workspace.binding
@completor.completion_candidates('', target, '', bind: bind)
}
end
# Reads the next line from this input method.
@ -230,11 +236,16 @@ module IRB
super
@eof = false
@completor = RegexpCompletor.new
Reline.basic_word_break_characters = IRB::InputCompletor::BASIC_WORD_BREAK_CHARACTERS
Reline.basic_word_break_characters = BASIC_WORD_BREAK_CHARACTERS
Reline.completion_append_character = nil
Reline.completer_quote_characters = ''
Reline.completion_proc = IRB::InputCompletor::CompletionProc
Reline.completion_proc = ->(target, preposing, postposing) {
bind = IRB.conf[:MAIN_CONTEXT].workspace.binding
@completion_params = [preposing, target, postposing, bind]
@completor.completion_candidates(preposing, target, postposing, bind: bind)
}
Reline.output_modifier_proc =
if IRB.conf[:USE_COLORIZE]
proc do |output, complete: |
@ -247,13 +258,13 @@ module IRB
Reline::Unicode.escape_for_print(output)
end
end
Reline.dig_perfect_match_proc = IRB::InputCompletor::PerfectMatchedProc
Reline.dig_perfect_match_proc = ->(matched) { display_document(matched) }
Reline.autocompletion = IRB.conf[:USE_AUTOCOMPLETE]
if IRB.conf[:USE_AUTOCOMPLETE]
begin
require 'rdoc'
Reline.add_dialog_proc(:show_doc, SHOW_DOC_DIALOG, Reline::DEFAULT_DIALOG_CONTEXT)
Reline.add_dialog_proc(:show_doc, show_doc_dialog_proc, Reline::DEFAULT_DIALOG_CONTEXT)
rescue LoadError
end
end
@ -271,100 +282,140 @@ module IRB
@auto_indent_proc = block
end
SHOW_DOC_DIALOG = ->() {
dialog.trap_key = nil
alt_d = [
[Reline::Key.new(nil, 0xE4, true)], # Normal Alt+d.
[27, 100], # Normal Alt+d when convert-meta isn't used.
[195, 164], # The "ä" that appears when Alt+d is pressed on xterm.
[226, 136, 130] # The "∂" that appears when Alt+d in pressed on iTerm2.
]
def show_doc_dialog_proc
doc_namespace = ->(matched) {
preposing, _target, postposing, bind = @completion_params
@completor.doc_namespace(preposing, matched, postposing, bind: bind)
}
->() {
dialog.trap_key = nil
alt_d = [
[Reline::Key.new(nil, 0xE4, true)], # Normal Alt+d.
[27, 100], # Normal Alt+d when convert-meta isn't used.
[195, 164], # The "ä" that appears when Alt+d is pressed on xterm.
[226, 136, 130] # The "∂" that appears when Alt+d in pressed on iTerm2.
]
if just_cursor_moving and completion_journey_data.nil?
return nil
end
cursor_pos_to_render, result, pointer, autocomplete_dialog = context.pop(4)
return nil if result.nil? or pointer.nil? or pointer < 0
name = result[pointer]
name = IRB::InputCompletor.retrieve_completion_data(name, doc_namespace: true)
options = {}
options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty?
driver = RDoc::RI::Driver.new(options)
if key.match?(dialog.name)
begin
driver.display_names([name])
rescue RDoc::RI::Driver::NotFoundError
if just_cursor_moving and completion_journey_data.nil?
return nil
end
end
cursor_pos_to_render, result, pointer, autocomplete_dialog = context.pop(4)
return nil if result.nil? or pointer.nil? or pointer < 0
begin
name = driver.expand_name(name)
rescue RDoc::RI::Driver::NotFoundError
return nil
rescue
return nil # unknown error
end
doc = nil
used_for_class = false
if not name =~ /#|\./
found, klasses, includes, extends = driver.classes_and_includes_and_extends_for(name)
if not found.empty?
doc = driver.class_document(name, found, klasses, includes, extends)
used_for_class = true
name = doc_namespace.call(result[pointer])
options = {}
options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty?
driver = RDoc::RI::Driver.new(options)
if key.match?(dialog.name)
begin
driver.display_names([name])
rescue RDoc::RI::Driver::NotFoundError
end
end
end
unless used_for_class
doc = RDoc::Markup::Document.new
begin
driver.add_method(doc, name)
name = driver.expand_name(name)
rescue RDoc::RI::Driver::NotFoundError
doc = nil
return nil
rescue
return nil # unknown error
end
end
return nil if doc.nil?
width = 40
doc = nil
used_for_class = false
if not name =~ /#|\./
found, klasses, includes, extends = driver.classes_and_includes_and_extends_for(name)
if not found.empty?
doc = driver.class_document(name, found, klasses, includes, extends)
used_for_class = true
end
end
unless used_for_class
doc = RDoc::Markup::Document.new
begin
driver.add_method(doc, name)
rescue RDoc::RI::Driver::NotFoundError
doc = nil
rescue
return nil # unknown error
end
end
return nil if doc.nil?
width = 40
right_x = cursor_pos_to_render.x + autocomplete_dialog.width
if right_x + width > screen_width
right_width = screen_width - (right_x + 1)
left_x = autocomplete_dialog.column - width
left_x = 0 if left_x < 0
left_width = width > autocomplete_dialog.column ? autocomplete_dialog.column : width
if right_width.positive? and left_width.positive?
if right_width >= left_width
right_x = cursor_pos_to_render.x + autocomplete_dialog.width
if right_x + width > screen_width
right_width = screen_width - (right_x + 1)
left_x = autocomplete_dialog.column - width
left_x = 0 if left_x < 0
left_width = width > autocomplete_dialog.column ? autocomplete_dialog.column : width
if right_width.positive? and left_width.positive?
if right_width >= left_width
width = right_width
x = right_x
else
width = left_width
x = left_x
end
elsif right_width.positive? and left_width <= 0
width = right_width
x = right_x
else
elsif right_width <= 0 and left_width.positive?
width = left_width
x = left_x
else # Both are negative width.
return nil
end
elsif right_width.positive? and left_width <= 0
width = right_width
else
x = right_x
elsif right_width <= 0 and left_width.positive?
width = left_width
x = left_x
else # Both are negative width.
return nil
end
else
x = right_x
end
formatter = RDoc::Markup::ToAnsi.new
formatter.width = width
dialog.trap_key = alt_d
mod_key = RUBY_PLATFORM.match?(/darwin/) ? "Option" : "Alt"
message = "Press #{mod_key}+d to read the full document"
contents = [message] + doc.accept(formatter).split("\n")
contents = contents.take(preferred_dialog_height)
formatter = RDoc::Markup::ToAnsi.new
formatter.width = width
dialog.trap_key = alt_d
mod_key = RUBY_PLATFORM.match?(/darwin/) ? "Option" : "Alt"
message = "Press #{mod_key}+d to read the full document"
contents = [message] + doc.accept(formatter).split("\n")
contents = contents.take(preferred_dialog_height)
y = cursor_pos_to_render.y
Reline::DialogRenderInfo.new(pos: Reline::CursorPos.new(x, y), contents: contents, width: width, bg_color: '49')
}
y = cursor_pos_to_render.y
Reline::DialogRenderInfo.new(pos: Reline::CursorPos.new(x, y), contents: contents, width: width, bg_color: '49')
}
end
def display_document(matched, driver: nil)
begin
require 'rdoc'
rescue LoadError
return
end
if matched =~ /\A(?:::)?RubyVM/ and not ENV['RUBY_YES_I_AM_NOT_A_NORMAL_USER']
IRB.__send__(:easter_egg)
return
end
_target, preposing, postposing, bind = @completion_params
namespace = @completor.doc_namespace(preposing, matched, postposing, bind: bind)
return unless namespace
driver ||= RDoc::RI::Driver.new
if namespace.is_a?(Array)
out = RDoc::Markup::Document.new
namespace.each do |m|
begin
driver.add_method(out, m)
rescue RDoc::RI::Driver::NotFoundError
end
end
driver.display(out)
else
begin
driver.display_names([namespace])
rescue RDoc::RI::Driver::NotFoundError
end
end
end
# Reads the next line from this input method.
#

View File

@ -8,71 +8,83 @@ module TestIRB
class CompletionTest < TestCase
def setup
# make sure require completion candidates are not cached
IRB::InputCompletor.class_variable_set(:@@files_from_load_path, nil)
IRB::BaseCompletor.class_variable_set(:@@files_from_load_path, nil)
end
def teardown
IRB.conf[:MAIN_CONTEXT] = nil
end
def completion_candidates(target, bind)
IRB::RegexpCompletor.new.completion_candidates('', target, '', bind: bind)
end
def doc_namespace(target, bind)
IRB::RegexpCompletor.new.doc_namespace('', target, '', bind: bind)
end
class MethodCompletionTest < CompletionTest
def test_complete_string
assert_include(IRB::InputCompletor.retrieve_completion_data("'foo'.up", bind: binding), "'foo'.upcase")
assert_include(completion_candidates("'foo'.up", binding), "'foo'.upcase")
# completing 'foo bar'.up
assert_include(IRB::InputCompletor.retrieve_completion_data("bar'.up", bind: binding), "bar'.upcase")
assert_equal("String.upcase", IRB::InputCompletor.retrieve_completion_data("'foo'.upcase", bind: binding, doc_namespace: true))
assert_include(completion_candidates("bar'.up", binding), "bar'.upcase")
assert_equal("String.upcase", doc_namespace("'foo'.upcase", binding))
end
def test_complete_regexp
assert_include(IRB::InputCompletor.retrieve_completion_data("/foo/.ma", bind: binding), "/foo/.match")
assert_include(completion_candidates("/foo/.ma", binding), "/foo/.match")
# completing /foo bar/.ma
assert_include(IRB::InputCompletor.retrieve_completion_data("bar/.ma", bind: binding), "bar/.match")
assert_equal("Regexp.match", IRB::InputCompletor.retrieve_completion_data("/foo/.match", bind: binding, doc_namespace: true))
assert_include(completion_candidates("bar/.ma", binding), "bar/.match")
assert_equal("Regexp.match", doc_namespace("/foo/.match", binding))
end
def test_complete_array
assert_include(IRB::InputCompletor.retrieve_completion_data("[].an", bind: binding), "[].any?")
assert_equal("Array.any?", IRB::InputCompletor.retrieve_completion_data("[].any?", bind: binding, doc_namespace: true))
assert_include(completion_candidates("[].an", binding), "[].any?")
assert_equal("Array.any?", doc_namespace("[].any?", binding))
end
def test_complete_hash_and_proc
# hash
assert_include(IRB::InputCompletor.retrieve_completion_data("{}.an", bind: binding), "{}.any?")
assert_equal(["Proc.any?", "Hash.any?"], IRB::InputCompletor.retrieve_completion_data("{}.any?", bind: binding, doc_namespace: true))
assert_include(completion_candidates("{}.an", binding), "{}.any?")
assert_equal(["Proc.any?", "Hash.any?"], doc_namespace("{}.any?", binding))
# proc
assert_include(IRB::InputCompletor.retrieve_completion_data("{}.bin", bind: binding), "{}.binding")
assert_equal(["Proc.binding", "Hash.binding"], IRB::InputCompletor.retrieve_completion_data("{}.binding", bind: binding, doc_namespace: true))
assert_include(completion_candidates("{}.bin", binding), "{}.binding")
assert_equal(["Proc.binding", "Hash.binding"], doc_namespace("{}.binding", binding))
end
def test_complete_numeric
assert_include(IRB::InputCompletor.retrieve_completion_data("1.positi", bind: binding), "1.positive?")
assert_equal("Integer.positive?", IRB::InputCompletor.retrieve_completion_data("1.positive?", bind: binding, doc_namespace: true))
assert_include(completion_candidates("1.positi", binding), "1.positive?")
assert_equal("Integer.positive?", doc_namespace("1.positive?", binding))
assert_include(IRB::InputCompletor.retrieve_completion_data("1r.positi", bind: binding), "1r.positive?")
assert_equal("Rational.positive?", IRB::InputCompletor.retrieve_completion_data("1r.positive?", bind: binding, doc_namespace: true))
assert_include(completion_candidates("1r.positi", binding), "1r.positive?")
assert_equal("Rational.positive?", doc_namespace("1r.positive?", binding))
assert_include(IRB::InputCompletor.retrieve_completion_data("0xFFFF.positi", bind: binding), "0xFFFF.positive?")
assert_equal("Integer.positive?", IRB::InputCompletor.retrieve_completion_data("0xFFFF.positive?", bind: binding, doc_namespace: true))
assert_include(completion_candidates("0xFFFF.positi", binding), "0xFFFF.positive?")
assert_equal("Integer.positive?", doc_namespace("0xFFFF.positive?", binding))
assert_empty(IRB::InputCompletor.retrieve_completion_data("1i.positi", bind: binding))
assert_empty(completion_candidates("1i.positi", binding))
end
def test_complete_symbol
assert_include(IRB::InputCompletor.retrieve_completion_data(":foo.to_p", bind: binding), ":foo.to_proc")
assert_equal("Symbol.to_proc", IRB::InputCompletor.retrieve_completion_data(":foo.to_proc", bind: binding, doc_namespace: true))
assert_include(completion_candidates(":foo.to_p", binding), ":foo.to_proc")
assert_equal("Symbol.to_proc", doc_namespace(":foo.to_proc", binding))
end
def test_complete_class
assert_include(IRB::InputCompletor.retrieve_completion_data("String.ne", bind: binding), "String.new")
assert_equal("String.new", IRB::InputCompletor.retrieve_completion_data("String.new", bind: binding, doc_namespace: true))
assert_include(completion_candidates("String.ne", binding), "String.new")
assert_equal("String.new", doc_namespace("String.new", binding))
end
end
class RequireComepletionTest < CompletionTest
def test_complete_require
candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "")
candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'irb", "", bind: binding)
%w['irb/init 'irb/ruby-lex].each do |word|
assert_include candidates, word
end
# Test cache
candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "")
candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'irb", "", bind: binding)
%w['irb/init 'irb/ruby-lex].each do |word|
assert_include candidates, word
end
@ -84,7 +96,7 @@ module TestIRB
test_path = Pathname.new(temp_dir)
$LOAD_PATH << test_path
candidates = IRB::InputCompletor::CompletionProc.("'foo", "require ", "")
candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'foo", "", bind: binding)
assert_include candidates, "'foo"
ensure
$LOAD_PATH.pop if test_path
@ -98,7 +110,7 @@ module TestIRB
object.define_singleton_method(:to_s) { temp_dir }
$LOAD_PATH << object
candidates = IRB::InputCompletor::CompletionProc.("'foo", "require ", "")
candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'foo", "", bind: binding)
assert_include candidates, "'foo"
ensure
$LOAD_PATH.pop if object
@ -111,27 +123,27 @@ module TestIRB
$LOAD_PATH << object
assert_nothing_raised do
IRB::InputCompletor::CompletionProc.("'foo", "require ", "")
IRB::RegexpCompletor.new.completion_candidates("require ", "'foo", "", bind: binding)
end
ensure
$LOAD_PATH.pop if object
end
def test_complete_require_library_name_first
candidates = IRB::InputCompletor::CompletionProc.("'csv", "require ", "")
candidates = IRB::RegexpCompletor.new.completion_candidates("require ", "'csv", "", bind: binding)
assert_equal "'csv", candidates.first
end
def test_complete_require_relative
candidates = Dir.chdir(__dir__ + "/../..") do
IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "")
IRB::RegexpCompletor.new.completion_candidates("require_relative ", "'lib/irb", "", bind: binding)
end
%w['lib/irb/init 'lib/irb/ruby-lex].each do |word|
assert_include candidates, word
end
# Test cache
candidates = Dir.chdir(__dir__ + "/../..") do
IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "")
IRB::RegexpCompletor.new.completion_candidates("require_relative ", "'lib/irb", "", bind: binding)
end
%w['lib/irb/init 'lib/irb/ruby-lex].each do |word|
assert_include candidates, word
@ -160,13 +172,13 @@ module TestIRB
local_variables.clear
instance_variables.clear
assert_include(IRB::InputCompletor.retrieve_completion_data("str_examp", bind: binding), "str_example")
assert_equal("String", IRB::InputCompletor.retrieve_completion_data("str_example", bind: binding, doc_namespace: true))
assert_equal("String.to_s", IRB::InputCompletor.retrieve_completion_data("str_example.to_s", bind: binding, doc_namespace: true))
assert_include(completion_candidates("str_examp", binding), "str_example")
assert_equal("String", doc_namespace("str_example", binding))
assert_equal("String.to_s", doc_namespace("str_example.to_s", binding))
assert_include(IRB::InputCompletor.retrieve_completion_data("@str_examp", bind: binding), "@str_example")
assert_equal("String", IRB::InputCompletor.retrieve_completion_data("@str_example", bind: binding, doc_namespace: true))
assert_equal("String.to_s", IRB::InputCompletor.retrieve_completion_data("@str_example.to_s", bind: binding, doc_namespace: true))
assert_include(completion_candidates("@str_examp", binding), "@str_example")
assert_equal("String", doc_namespace("@str_example", binding))
assert_equal("String.to_s", doc_namespace("@str_example.to_s", binding))
end
def test_complete_sort_variables
@ -176,7 +188,7 @@ module TestIRB
xzy_1.clear
xzy2.clear
candidates = IRB::InputCompletor.retrieve_completion_data("xz", bind: binding, doc_namespace: false)
candidates = completion_candidates("xz", binding)
assert_equal(%w[xzy xzy2 xzy_1], candidates)
end
end
@ -189,102 +201,12 @@ module TestIRB
end
def test_complete_constants
assert_equal(["Foo"], IRB::InputCompletor.retrieve_completion_data("Fo", bind: binding))
assert_equal(["Foo::B1", "Foo::B2", "Foo::B3"], IRB::InputCompletor.retrieve_completion_data("Foo::B", bind: binding))
assert_equal(["Foo::B1.positive?"], IRB::InputCompletor.retrieve_completion_data("Foo::B1.pos", bind: binding))
assert_equal(["Foo"], completion_candidates("Fo", binding))
assert_equal(["Foo::B1", "Foo::B2", "Foo::B3"], completion_candidates("Foo::B", binding))
assert_equal(["Foo::B1.positive?"], completion_candidates("Foo::B1.pos", binding))
assert_equal(["::Forwardable"], IRB::InputCompletor.retrieve_completion_data("::Fo", bind: binding))
assert_equal("Forwardable", IRB::InputCompletor.retrieve_completion_data("::Forwardable", bind: binding, doc_namespace: true))
end
end
class PerfectMatchingTest < CompletionTest
def setup
# trigger PerfectMatchedProc to set up RDocRIDriver constant
IRB::InputCompletor::PerfectMatchedProc.("foo", bind: binding)
@original_use_stdout = IRB::InputCompletor::RDocRIDriver.use_stdout
# force the driver to use stdout so it doesn't start a pager and interrupt tests
IRB::InputCompletor::RDocRIDriver.use_stdout = true
end
def teardown
IRB::InputCompletor::RDocRIDriver.use_stdout = @original_use_stdout
end
def test_perfectly_matched_namespace_triggers_document_display
omit unless has_rdoc_content?
out, err = capture_output do
IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding)
end
assert_empty(err)
assert_include(out, " S\bSt\btr\bri\bin\bng\bg")
end
def test_perfectly_matched_multiple_namespaces_triggers_document_display
result = nil
out, err = capture_output do
result = IRB::InputCompletor::PerfectMatchedProc.("{}.nil?", bind: binding)
end
assert_empty(err)
# check if there're rdoc contents (e.g. CI doesn't generate them)
if has_rdoc_content?
# if there's rdoc content, we can verify by checking stdout
# rdoc generates control characters for formatting method names
assert_include(out, "P\bPr\bro\boc\bc.\b.n\bni\bil\bl?\b?") # Proc.nil?
assert_include(out, "H\bHa\bas\bsh\bh.\b.n\bni\bil\bl?\b?") # Hash.nil?
else
# this is a hacky way to verify the rdoc rendering code path because CI doesn't have rdoc content
# if there are multiple namespaces to be rendered, PerfectMatchedProc renders the result with a document
# which always returns the bytes rendered, even if it's 0
assert_equal(0, result)
end
end
def test_not_matched_namespace_triggers_nothing
result = nil
out, err = capture_output do
result = IRB::InputCompletor::PerfectMatchedProc.("Stri", bind: binding)
end
assert_empty(err)
assert_empty(out)
assert_nil(result)
end
def test_perfect_matching_stops_without_rdoc
result = nil
out, err = capture_output do
without_rdoc do
result = IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding)
end
end
assert_empty(err)
assert_not_match(/from ruby core/, out)
assert_nil(result)
end
def test_perfect_matching_handles_nil_namespace
out, err = capture_output do
# symbol literal has `nil` doc namespace so it's a good test subject
assert_nil(IRB::InputCompletor::PerfectMatchedProc.(":aiueo", bind: binding))
end
assert_empty(err)
assert_empty(out)
end
private
def has_rdoc_content?
File.exist?(RDoc::RI::Paths::BASE)
assert_equal(["::Forwardable"], completion_candidates("::Fo", binding))
assert_equal("Forwardable", doc_namespace("::Forwardable", binding))
end
end
@ -294,34 +216,34 @@ module TestIRB
rescue
end
symbols += [:aiueo, :"aiu eo"]
candidates = IRB::InputCompletor.retrieve_completion_data(":a", bind: binding)
candidates = completion_candidates(":a", binding)
assert_include(candidates, ":aiueo")
assert_not_include(candidates, ":aiu eo")
assert_empty(IRB::InputCompletor.retrieve_completion_data(":irb_unknown_symbol_abcdefg", bind: binding))
assert_empty(completion_candidates(":irb_unknown_symbol_abcdefg", binding))
# Do not complete empty symbol for performance reason
assert_empty(IRB::InputCompletor.retrieve_completion_data(":", bind: binding))
assert_empty(completion_candidates(":", binding))
end
def test_complete_invalid_three_colons
assert_empty(IRB::InputCompletor.retrieve_completion_data(":::A", bind: binding))
assert_empty(IRB::InputCompletor.retrieve_completion_data(":::", bind: binding))
assert_empty(completion_candidates(":::A", binding))
assert_empty(completion_candidates(":::", binding))
end
def test_complete_absolute_constants_with_special_characters
assert_empty(IRB::InputCompletor.retrieve_completion_data("::A:", bind: binding))
assert_empty(IRB::InputCompletor.retrieve_completion_data("::A.", bind: binding))
assert_empty(IRB::InputCompletor.retrieve_completion_data("::A(", bind: binding))
assert_empty(IRB::InputCompletor.retrieve_completion_data("::A)", bind: binding))
assert_empty(IRB::InputCompletor.retrieve_completion_data("::A[", bind: binding))
assert_empty(completion_candidates("::A:", binding))
assert_empty(completion_candidates("::A.", binding))
assert_empty(completion_candidates("::A(", binding))
assert_empty(completion_candidates("::A)", binding))
assert_empty(completion_candidates("::A[", binding))
end
def test_complete_reserved_words
candidates = IRB::InputCompletor.retrieve_completion_data("de", bind: binding)
candidates = completion_candidates("de", binding)
%w[def defined?].each do |word|
assert_include candidates, word
end
candidates = IRB::InputCompletor.retrieve_completion_data("__", bind: binding)
candidates = completion_candidates("__", binding)
%w[__ENCODING__ __LINE__ __FILE__].each do |word|
assert_include candidates, word
end
@ -342,11 +264,11 @@ module TestIRB
}
bind = obj.instance_exec { binding }
assert_include(IRB::InputCompletor.retrieve_completion_data("public_hog", bind: bind), "public_hoge")
assert_include(IRB::InputCompletor.retrieve_completion_data("public_hoge", bind: bind, doc_namespace: true), "public_hoge")
assert_include(completion_candidates("public_hog", bind), "public_hoge")
assert_include(doc_namespace("public_hoge", bind), "public_hoge")
assert_include(IRB::InputCompletor.retrieve_completion_data("private_hog", bind: bind), "private_hoge")
assert_include(IRB::InputCompletor.retrieve_completion_data("private_hoge", bind: bind, doc_namespace: true), "private_hoge")
assert_include(completion_candidates("private_hog", bind), "private_hoge")
assert_include(doc_namespace("private_hoge", bind), "private_hoge")
end
end
end

View File

@ -1,11 +1,11 @@
# frozen_string_literal: false
require "irb"
require "rdoc"
require_relative "helper"
module TestIRB
class RelineInputMethodTest < TestCase
class InputMethodTest < TestCase
def setup
@conf_backup = IRB.conf.dup
IRB.conf[:LC_MESSAGES] = IRB::Locale.new
@ -18,15 +18,19 @@ module TestIRB
# Reset Reline configuration overrided by RelineInputMethod.
Reline.instance_variable_set(:@core, nil)
end
end
class RelineInputMethodTest < InputMethodTest
def test_initialization
Reline.completion_proc = nil
Reline.dig_perfect_match_proc = nil
IRB::RelineInputMethod.new
assert_nil Reline.completion_append_character
assert_equal '', Reline.completer_quote_characters
assert_equal IRB::InputCompletor::BASIC_WORD_BREAK_CHARACTERS, Reline.basic_word_break_characters
assert_equal IRB::InputCompletor::CompletionProc, Reline.completion_proc
assert_equal IRB::InputCompletor::PerfectMatchedProc, Reline.dig_perfect_match_proc
assert_equal IRB::InputMethod::BASIC_WORD_BREAK_CHARACTERS, Reline.basic_word_break_characters
assert_not_nil Reline.completion_proc
assert_not_nil Reline.dig_perfect_match_proc
end
def test_initialization_without_use_autocomplete
@ -54,7 +58,7 @@ module TestIRB
IRB::RelineInputMethod.new
assert Reline.autocompletion
assert_equal IRB::RelineInputMethod::SHOW_DOC_DIALOG, Reline.dialog_proc(:show_doc).dialog_proc
assert_not_equal empty_proc, Reline.dialog_proc(:show_doc).dialog_proc
ensure
Reline.add_dialog_proc(:show_doc, original_show_doc_proc, Reline::DEFAULT_DIALOG_CONTEXT)
end
@ -77,5 +81,93 @@ module TestIRB
Reline.add_dialog_proc(:show_doc, original_show_doc_proc, Reline::DEFAULT_DIALOG_CONTEXT)
end
end
class DisplayDocumentTest < InputMethodTest
def setup
super
@driver = RDoc::RI::Driver.new(use_stdout: true)
end
def display_document(target, bind)
input_method = IRB::RelineInputMethod.new
input_method.instance_variable_set(:@completion_params, [target, '', '', bind])
input_method.display_document(target, driver: @driver)
end
def test_perfectly_matched_namespace_triggers_document_display
omit unless has_rdoc_content?
out, err = capture_output do
display_document("String", binding)
end
assert_empty(err)
assert_include(out, " S\bSt\btr\bri\bin\bng\bg")
end
def test_perfectly_matched_multiple_namespaces_triggers_document_display
result = nil
out, err = capture_output do
result = display_document("{}.nil?", binding)
end
assert_empty(err)
# check if there're rdoc contents (e.g. CI doesn't generate them)
if has_rdoc_content?
# if there's rdoc content, we can verify by checking stdout
# rdoc generates control characters for formatting method names
assert_include(out, "P\bPr\bro\boc\bc.\b.n\bni\bil\bl?\b?") # Proc.nil?
assert_include(out, "H\bHa\bas\bsh\bh.\b.n\bni\bil\bl?\b?") # Hash.nil?
else
# this is a hacky way to verify the rdoc rendering code path because CI doesn't have rdoc content
# if there are multiple namespaces to be rendered, PerfectMatchedProc renders the result with a document
# which always returns the bytes rendered, even if it's 0
assert_equal(0, result)
end
end
def test_not_matched_namespace_triggers_nothing
result = nil
out, err = capture_output do
result = display_document("Stri", binding)
end
assert_empty(err)
assert_empty(out)
assert_nil(result)
end
def test_perfect_matching_stops_without_rdoc
result = nil
out, err = capture_output do
without_rdoc do
result = display_document("String", binding)
end
end
assert_empty(err)
assert_not_match(/from ruby core/, out)
assert_nil(result)
end
def test_perfect_matching_handles_nil_namespace
out, err = capture_output do
# symbol literal has `nil` doc namespace so it's a good test subject
assert_nil(display_document(":aiueo", binding))
end
assert_empty(err)
assert_empty(out)
end
private
def has_rdoc_content?
File.exist?(RDoc::RI::Paths::BASE)
end
end
end