From 1e98521483707a9a15b0ddd502be253019fb7f08 Mon Sep 17 00:00:00 2001 From: tomoya ishida Date: Thu, 12 Oct 2023 02:08:59 +0900 Subject: [PATCH] Rename current completor to RegexpCompletor and refactored for future extension (#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 --- lib/irb/completion.rb | 174 ++++++++++++-------------- lib/irb/input-method.rb | 213 +++++++++++++++++++------------- test/irb/test_completion.rb | 222 +++++++++++----------------------- test/irb/test_input_method.rb | 104 +++++++++++++++- 4 files changed, 376 insertions(+), 337 deletions(-) diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index a143d1b3e..26215f6fe 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -8,55 +8,14 @@ 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 - ] + class BaseCompletor # :nodoc: + def completion_candidates(preposing, target, postposing, bind:) + raise NotImplementedError + end - BASIC_WORD_BREAK_CHARACTERS = " \t\n`><=;|&{(" + def doc_namespace(preposing, matched, postposing, bind:) + raise NotImplementedError + end GEM_PATHS = if defined?(Gem::Specification) @@ -73,7 +32,7 @@ def defined? do [] end.freeze - def self.retrieve_gem_and_system_load_path + def retrieve_gem_and_system_load_path candidates = (GEM_PATHS | $LOAD_PATH) candidates.map do |p| if p.respond_to?(:to_path) @@ -84,8 +43,8 @@ def self.retrieve_gem_and_system_load_path end.compact.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| @@ -103,13 +62,62 @@ def self.retrieve_files_to_require_from_load_path ) 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 - CompletionRequireProc = lambda { |target, preposing = nil, postposing = nil| + 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 @@ -142,21 +150,21 @@ def self.retrieve_files_to_require_relative_from_current_dir 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 @@ def self.retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace 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_]/ diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index bd7d4fede..454b6db00 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -11,6 +11,8 @@ 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 @@ def initialize 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 @@ def initialize 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 @@ def initialize 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 @@ def auto_indent(&block) @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 - 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) + 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) + 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 key.match?(dialog.name) + begin + driver.display_names([name]) + rescue RDoc::RI::Driver::NotFoundError + end end - end - 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 - 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 - - 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 + 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 + 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 + 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') + } + 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 - x = right_x + begin + driver.display_names([namespace]) + rescue RDoc::RI::Driver::NotFoundError + end 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) - - y = cursor_pos_to_render.y - Reline::DialogRenderInfo.new(pos: Reline::CursorPos.new(x, y), contents: contents, width: width, bg_color: '49') - } + end # Reads the next line from this input method. # diff --git a/test/irb/test_completion.rb b/test/irb/test_completion.rb index 2a659818e..2ef893826 100644 --- a/test/irb/test_completion.rb +++ b/test/irb/test_completion.rb @@ -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 @@ def test_complete_require_with_pathname_in_load_path 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 @@ def test_complete_require_with_string_convertable_in_load_path 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 @@ def object.to_s; raise; end $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 @@ def test_complete_variable 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 @@ def test_complete_sort_variables 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 @@ class Foo 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(["::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 + 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)) - 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 @@ def test_complete_symbol 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 @@ def instance_variables; end } 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 diff --git a/test/irb/test_input_method.rb b/test/irb/test_input_method.rb index b516be5e3..2d8cfadcf 100644 --- a/test/irb/test_input_method.rb +++ b/test/irb/test_input_method.rb @@ -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 @@ def teardown # 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 @@ def test_initialization_with_use_autocomplete 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 @@ def test_initialization_with_use_autocomplete_but_without_rdoc 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