From 4ee6becf493d467eeb913033e314ee1ad3d3f621 Mon Sep 17 00:00:00 2001 From: tompng Date: Mon, 19 Jun 2023 01:11:12 +0900 Subject: [PATCH] Completion using Ripper.sexp --- lib/irb/completion.rb | 546 ++++++++++++++++++------------------ lib/irb/input-method.rb | 10 +- lib/irb/nesting_parser.rb | 29 ++ test/irb/test_completion.rb | 245 ++++++++++------ 4 files changed, 459 insertions(+), 371 deletions(-) diff --git a/lib/irb/completion.rb b/lib/irb/completion.rb index a143d1b3e..752184daa 100644 --- a/lib/irb/completion.rb +++ b/lib/irb/completion.rb @@ -24,11 +24,25 @@ def eval_instance_variables end def eval_global_variables - ::Kernel.instance_method(:global_variables).bind(eval("self")).call + ::Kernel.global_variables end - def eval_class_constants - ::Module.instance_method(:constants).bind(eval("self.class")).call + def eval_constants + [Object, *eval('::Module.nesting')].flat_map(&:constants).uniq.sort rescue [] + end + + def eval_class_variables + mod = eval('::Module.nesting').first + mod&.class_variables || [] rescue [] + end + + def eval_instance_variable_get(name) + ::Kernel.instance_method(:instance_variable_get).bind_call(eval('self'), name) rescue nil + end + + def eval_class_variable_get(name) + mod = eval('::Module.nesting').first + mod&.class_variable_get(name) rescue nil end end } @@ -109,287 +123,289 @@ def self.retrieve_files_to_require_relative_from_current_dir } end - CompletionRequireProc = lambda { |target, preposing = nil, postposing = nil| - if target =~ /\A(['"])([^'"]+)\Z/ - quote = $1 - actual_target = $2 + def self.retrieve_completion_sexp_nodes(code) + tokens = RubyLex.ripper_lex_without_warning(code) + + # remove error tokens + tokens.pop while tokens&.last&.tok&.empty? + + event = tokens.last&.event + tok = tokens.last&.tok + + if (event == :on_ignored_by_ripper || event == :on_op || event == :on_period) && (tok == '.' || tok == '::' || tok == '&.') + suffix = tok == '::' ? 'Const' : 'method' + tok = '' + elsif event == :on_symbeg + suffix = 'symbol' + tok = '' + elsif event == :on_ident || event == :on_kw + suffix = 'method' + elsif event == :on_const + suffix = 'Const' + elsif event == :on_tstring_content + suffix = 'string' + elsif event == :on_gvar + suffix = '$gvar' + elsif event == :on_ivar + suffix = '@ivar' + elsif event == :on_cvar + suffix = '@@cvar' 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 + return 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 - } + + code = code.delete_suffix(tok) + last_opens = IRB::NestingParser.open_tokens(tokens) + closing_code = IRB::NestingParser.closing_code(last_opens) + sexp = Ripper.sexp("#{code}#{suffix}#{closing_code}") + return unless sexp + + lines = code.split("\n", -1) + row = lines.empty? ? 1 : lines.size + col = lines.last&.bytesize || 0 + matched_nodes = find_target_from_sexp(sexp, row, col) + [matched_nodes, tok] if matched_nodes + end + + def self.find_target_from_sexp(sexp, row, col) + return unless sexp.is_a? Array + + sexp.each do |child| + event, tok, pos = child + if event.is_a?(Symbol) && tok.is_a?(String) && pos == [row, col] + return [child] + else + result = find_target_from_sexp(child, row, col) + if result + result.unshift child + return result + end end end - result - } + nil + end - CompletionProc = lambda { |target, preposing = nil, postposing = nil| - if preposing && postposing - result = CompletionRequireProc.(target, preposing, postposing) - unless result - result = retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) } + def self.evaluate_receiver_with_visibility(receiver_node, bind) + event, *data = receiver_node + case event + when :var_ref + if (value, visibility = evaluate_var_ref_with_visibility(data[0], bind)) + [value.nil? ? NilClass : nil, value, visibility] end - result - else - retrieve_completion_data(target).compact.map{ |i| i.encode(Encoding.default_external) } + when :const_path_ref + _reciever_class, reciever, _visibility = evaluate_receiver_with_visibility(data[0], bind) + if reciever + [nil, reciever.const_get(data[1][1]), false] rescue nil + end + when :top_const_ref + [nil, Object.const_get(data[0][1]), false] rescue nil + when :array + [Array, nil, false] + when :hash + [Hash, nil, false] + when :lambda + [Proc, nil, false] + when :symbol_literal, :dyna_symbol, :def + [Symbol, nil, false] + when :string_literal, :xstring_literal + [String, nil, false] + when :@int + [Integer, nil, false] + when :@float + [Float, nil, false] + when :@rational + [Rational, nil, false] + when :@imaginary + [Complex, nil, false] + when :regexp_literal + [Regexp, nil, false] end - } + end - def self.retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace.binding, doc_namespace: false) - 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) + def self.evaluate_var_ref_with_visibility(node, bind) + type, value = node + case type + when :@kw + case value + when 'self' + [bind.eval('self'), true] + when 'true' + [true, false] + when 'false' + [false, false] + when 'nil' + [nil, false] end + when :@ident + [bind.local_variable_get(value), false] + when :@gvar + [eval(value), false] if global_variables.include? value + when :@ivar + [bind.eval_instance_variable_get(value), false] + when :@cvar + [bind.class_variable_get(value), false] + when :@const + [bind.eval(value), false] rescue nil + end + 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 + def self.retrieve_completion_target(code) + matched_nodes, name = retrieve_completion_sexp_nodes(code) + return unless matched_nodes - if doc_namespace - "Regexp.#{message}" - else - candidates = Regexp.instance_methods.collect{|m| m.to_s} - select_message(receiver, message, candidates) - end + *parents, expression, (target_event,) = matched_nodes - when /^([^\]]*\])\.([^.]*)$/ - # Array - receiver = $1 - message = $2 + case target_event + when :@gvar + return [:gvar, name] + when :@ivar + return [:ivar, name] + when :@cvar + return [:cvar, name] + end + return unless expression - if doc_namespace - "Array.#{message}" - else - candidates = Array.instance_methods.collect{|m| m.to_s} - select_message(receiver, message, candidates) + if target_event == :@tstring_content + req_event, (ident_event, ident_name) = parents[-4] + if req_event == :command && ident_event == :@ident && (ident_name == 'require' || ident_name == 'require_relative') + return [ident_name.to_sym, name.rstrip] end + 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) + expression_event = expression[0] + case expression_event + when :symbol + [:symbol, name] + when :vcall + [:lvar_or_method, name] + when :var_ref + if target_event == :@ident + [:lvar_or_method, name] + elsif target_event == :@const + [:const_or_method, name] end + when :const_ref + [:const, name] + when :const_path_ref + [:const, name, expression[1]] + when :top_const_ref + [:top_const, name] + when :def + [:const_or_lvar_or_method, name] + when :call, :defs + [:call, name, expression[1]] + end + end + + def self.retrieve_completion_data(code, bind:) + lvars_code = RubyLex.generate_local_variables_assign_code(bind.local_variables) + type, name, receiver_node = retrieve_completion_target("#{lvars_code}\n#{code}") + receiver_class, receiver_object, public_visibility = evaluate_receiver_with_visibility(receiver_node, bind) if receiver_node + [type, name, receiver_class, receiver_object, public_visibility] + end - when /^(:[^:.]+)$/ - # Symbol - if doc_namespace - nil + def self.completion_candidates(completion_data, bind:) + type, name, receiver_class, receiver_object, public_visibility = completion_data + return [] unless type + + case type + when :require + retrieve_files_to_require_from_load_path + when :require_relative + retrieve_files_to_require_relative_from_current_dir + when :symbol + if name.empty? + [] else - sym = $1 - candidates = Symbol.all_symbols.collect do |s| - s.inspect + Symbol.all_symbols.filter_map do |s| + s.inspect[1..] rescue EncodingError - # ignore + # ignore for truffleruby 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}" + when :gvar + global_variables + when :ivar + bind.eval_instance_variables + when :cvar + bind.eval_class_variables + when :call + if receiver_class + public_visibility ? receiver_class.public_instance_methods : receiver_class.instance_methods + elsif receiver_object.nil? + [] 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, "::") + public_visibility ? receiver_object.public_methods | receiver_object.private_methods : receiver_object.public_methods end - - when /^(:[^:.]+)(\.|::)([^.]*)$/ - # Symbol - receiver = $1 - sep = $2 - message = $3 - - if doc_namespace - "Symbol.#{message}" + when :top_const + Object.constants.sort + when :const + if receiver_object + receiver_object.constants.sort else - candidates = Symbol.instance_methods.collect{|m| m.to_s} - select_message(receiver, message, candidates, sep) - end - - when /^(?-?(?:0[dbo])?[0-9_]+(?:\.[0-9_]+)?(?:(?:[eE][+-]?[0-9]+)?i?|r)?)(?\.|::)(?[^.]*)$/ - # 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 + bind.eval_constants.sort end + when :const_or_method + (bind.eval_constants | bind.eval_methods | bind.eval_private_methods | ReservedWords).map(&:to_s).sort + when :const_or_lvar_or_method + (bind.eval_constants | bind.local_variables | bind.eval_methods | bind.eval_private_methods | ReservedWords).map(&:to_s).sort + when :lvar_or_method + (bind.local_variables | bind.eval_methods | bind.eval_private_methods | ReservedWords).map(&:to_s).sort + else + [] + end + end - when /^(\$[^.]*)$/ - # global var - gvar = $1 - all_gvars = global_variables.collect{|m| m.to_s} + @@previous_completion_data = nil - if doc_namespace - all_gvars.find{ |i| i == gvar } - else - all_gvars.grep(Regexp.new(Regexp.quote(gvar))) - end + def self.previous_completion_data + @@previous_completion_data + 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 + CompletionProc = lambda { |target, preposing, postposing| + context = IRB.conf[:MAIN_CONTEXT] + bind = context.workspace.binding + completion_data = retrieve_completion_data("#{preposing}#{target}", bind: bind) + candidates = completion_candidates(completion_data, bind: bind) - 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 + # Hack to use completion_data from SHOW_DOC_DIALOG and from PerfectMatchedProc + @@previous_completion_data = completion_data - when /^\.([^.]*)$/ - # unknown(maybe String) + type, name, = completion_data + return [] unless type - receiver = "" - message = $1 + prefix = target.delete_suffix name + candidates.map(&:to_s).select { |s| s.start_with? name }.map do |s| + prefix + s + end + } - candidates = String.instance_methods(true).collect{|m| m.to_s} + def self.retrieve_doc_namespace(target, completion_data, bind:) + name = target[/(\$|@|@@)?[a-zA-Z_0-9]+[?=!]?\z/] + return unless name - if doc_namespace - "String.#{candidates.find{ |i| i == message }}" + type, _name, receiver_class, receiver_object, _public_visibility = completion_data + receiver_class ||= receiver_object.class + case type + when :call + if receiver_object.is_a?(Module) + "#{receiver_object}.#{name}" else - select_message(receiver, message, candidates.sort) + "#{receiver_class}.#{name}" 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 + when :top_const + name + when :const + "#{receiver_object}::#{name}" if receiver_object.is_a?(Module) + when :const_or_method + name + when :ivar + bind.eval_instance_variable_get(name).class.to_s rescue nil + when :lvar_or_method + if bind.local_variables.include?(name.to_sym) + bind.local_variable_get(name).class.to_s 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 + "#{bind.eval('self').class}.#{name}" end end end @@ -408,39 +424,13 @@ def self.retrieve_completion_data(input, bind: IRB.conf[:MAIN_CONTEXT].workspace return end - namespace = retrieve_completion_data(matched, bind: bind, doc_namespace: true) + namespace = retrieve_doc_namespace(matched, previous_completion_data, bind: bind) 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 + begin + RDocRIDriver.display_names([namespace]) + rescue RDoc::RI::Driver::NotFoundError end } - - # Set of available operators in Ruby - Operators = %w[% & * ** + - / < << <= <=> == === =~ > >= >> [] []= ^ ! != !~] - - def self.select_message(receiver, message, candidates, sep = ".") - candidates.grep(/^#{Regexp.quote(message)}/).collect do |e| - case e - when /^[a-zA-Z_]/ - receiver + sep + e - when /^[0-9]/ - when *Operators - #receiver + " " + e - end - end - end end end diff --git a/lib/irb/input-method.rb b/lib/irb/input-method.rb index 4e049b22d..88a8b2a74 100644 --- a/lib/irb/input-method.rb +++ b/lib/irb/input-method.rb @@ -196,7 +196,12 @@ def initialize Readline.basic_word_break_characters = IRB::InputCompletor::BASIC_WORD_BREAK_CHARACTERS end Readline.completion_append_character = nil - Readline.completion_proc = IRB::InputCompletor::CompletionProc + Readline.completion_proc = ->(target) { + line_buffer = Readline.line_buffer || '' + preposing_target = line_buffer[...Readline.point] + postposing = line_buffer[Readline.point..] + IRB::InputCompletor::CompletionProc.call(target, preposing_target.delete_suffix(target), postposing) + } end # Reads the next line from this input method. @@ -326,8 +331,9 @@ def auto_indent(&block) end cursor_pos_to_render, result, pointer, autocomplete_dialog = context.pop(4) return nil if result.nil? or pointer.nil? or pointer < 0 + bind = IRB.conf[:MAIN_CONTEXT].workspace.binding name = result[pointer] - name = IRB::InputCompletor.retrieve_completion_data(name, doc_namespace: true) + name = IRB::InputCompletor.retrieve_doc_namespace(name, IRB::InputCompletor.previous_completion_data, bind: bind) options = {} options[:extra_doc_dirs] = IRB.conf[:EXTRA_DOC_DIRS] unless IRB.conf[:EXTRA_DOC_DIRS].empty? diff --git a/lib/irb/nesting_parser.rb b/lib/irb/nesting_parser.rb index 3d4db8244..9d7f6a8a0 100644 --- a/lib/irb/nesting_parser.rb +++ b/lib/irb/nesting_parser.rb @@ -223,5 +223,34 @@ def self.parse_by_line(tokens) output << [line_tokens, prev_opens, last_opens, min_depth] if line_tokens.any? output end + + def self.closing_code(opens) + closing_tokens = opens.map do |t| + case t.tok + when /\A%.[<>]\z/ + '>' + when '{', '#{', /\A%.?[{}]\z/ + '}' + when '(', /\A%.?[()]\z/ + # do not insert \n before closing paren. workaround to avoid syntax error of "a in ^(b\n)" + ')' + when '[', /\A%.?[\[\]]\z/ + ']' + when /\A%.?(.)\z/ + $1 + when '"', "'", '/', '`' + t.tok + when /\A<<[~-]?(?:"(?.+)"|'(?.+)'|(?.+))/ + "\n#{s}\n" + when ':"', ":'", ':' + t.tok[1] + when 'case' + "\nwhen true\nend" + else + "\nend" + end + end + closing_tokens.reverse.join + end end end diff --git a/test/irb/test_completion.rb b/test/irb/test_completion.rb index 2a659818e..92eab82bd 100644 --- a/test/irb/test_completion.rb +++ b/test/irb/test_completion.rb @@ -11,68 +11,103 @@ def setup IRB::InputCompletor.class_variable_set(:@@files_from_load_path, nil) end + TestCompletionProcContext = Struct.new(:workspace) + + def call_completion_proc(target, preposing, postposing, bind: nil) + main_context = IRB.conf[:MAIN_CONTEXT] + IRB.conf[:MAIN_CONTEXT] = TestCompletionProcContext.new(IRB::WorkSpace.new(bind || Object.new)) + candidates = IRB::InputCompletor::CompletionProc.call target, preposing, postposing + yield if block_given? + candidates + ensure + IRB::InputCompletor.class_variable_set :@@previous_completion_data, nil + IRB.conf[:MAIN_CONTEXT] = main_context + end + + def completion_candidates(code, bind:) + call_completion_proc(code, '', '', bind: bind) + end + + def doc_namespace(code, bind:) + completion_data = IRB::InputCompletor.retrieve_completion_data(code, bind: bind) + IRB::InputCompletor.retrieve_doc_namespace(code, completion_data, bind: bind) + end + class MethodCompletionTest < CompletionTest def test_complete_string - assert_include(IRB::InputCompletor.retrieve_completion_data("'foo'.up", bind: 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(call_completion_proc("'foo'.up", "", "", bind: binding), "'foo'.upcase") + assert_include(call_completion_proc("bar'.up", "'foo ", "", bind: binding), "bar'.upcase") + assert_equal("String.upcase", doc_namespace("'foo'.upcase", bind: binding)) end def test_complete_regexp - assert_include(IRB::InputCompletor.retrieve_completion_data("/foo/.ma", bind: 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(call_completion_proc("/foo/.ma", "" ,"", bind: binding), "/foo/.match") + assert_include(call_completion_proc("bar/.ma", "/foo ", "", bind: binding), "bar/.match") + assert_equal("Regexp.match", doc_namespace("/foo/.match", bind: 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", bind: binding), "[].any?") + assert_include(completion_candidates("[a].an", bind: binding), "[a].any?") + assert_include(completion_candidates("[*a].an", bind: binding), "[*a].any?") + assert_equal("Array.any?", doc_namespace("[].any?", bind: 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)) + def test_complete_hash + assert_include(completion_candidates("{}.an", bind: binding), "{}.any?") + assert_include(completion_candidates("{a:1}.an", bind: binding), "{a:1}.any?") + assert_include(completion_candidates("{**a}.an", bind: binding), "{**a}.any?") + assert_equal("Hash.any?", doc_namespace("{}.any?", bind: binding)) + end - # 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)) + def test_complete_proc + assert_include(completion_candidates("->{}.bin", bind: binding), "->{}.binding") + assert_equal("Proc.binding", doc_namespace("->{}.binding", bind: binding)) + end + + def test_complete_keywords + assert_include(completion_candidates("nil.to_", bind: binding), "nil.to_a") + assert_equal("NilClass.to_a", doc_namespace("nil.to_a", bind: binding)) + + assert_include(completion_candidates("true.to_", bind: binding), "true.to_s") + assert_equal("TrueClass.to_s", doc_namespace("true.to_s", bind: binding)) + + assert_include(completion_candidates("false.to_", bind: binding), "false.to_s") + assert_equal("FalseClass.to_s", doc_namespace("false.to_s", bind: 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", bind: binding), "1.positive?") + assert_equal("Integer.positive?", doc_namespace("1.positive?", bind: 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", bind: binding), "1r.positive?") + assert_equal("Rational.positive?", doc_namespace("1r.positive?", bind: 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", bind: binding), "0xFFFF.positive?") + assert_equal("Integer.positive?", doc_namespace("0xFFFF.positive?", bind: binding)) - assert_empty(IRB::InputCompletor.retrieve_completion_data("1i.positi", bind: binding)) + assert_empty(completion_candidates("1i.positi", bind: 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", bind: binding), ":foo.to_proc") + assert_equal("Symbol.to_proc", doc_namespace(":foo.to_proc", bind: 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", bind: binding), "String.new") + assert_equal("String.new", doc_namespace("String.new", bind: binding)) end end class RequireComepletionTest < CompletionTest def test_complete_require - candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") + candidates = call_completion_proc("'irb", "require ", "") %w['irb/init 'irb/ruby-lex].each do |word| assert_include candidates, word end # Test cache - candidates = IRB::InputCompletor::CompletionProc.("'irb", "require ", "") + candidates = call_completion_proc("'irb", "require ", "") %w['irb/init 'irb/ruby-lex].each do |word| assert_include candidates, word end @@ -84,7 +119,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 = call_completion_proc("'foo", "require ", "") assert_include candidates, "'foo" ensure $LOAD_PATH.pop if test_path @@ -98,7 +133,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 = call_completion_proc("'foo", "require ", "") assert_include candidates, "'foo" ensure $LOAD_PATH.pop if object @@ -111,27 +146,27 @@ def object.to_s; raise; end $LOAD_PATH << object assert_nothing_raised do - IRB::InputCompletor::CompletionProc.("'foo", "require ", "") + call_completion_proc("'foo", "require ", "") end ensure $LOAD_PATH.pop if object end def test_complete_require_library_name_first - candidates = IRB::InputCompletor::CompletionProc.("'csv", "require ", "") + candidates = call_completion_proc("'csv", "require ", "") assert_equal "'csv", candidates.first end def test_complete_require_relative candidates = Dir.chdir(__dir__ + "/../..") do - IRB::InputCompletor::CompletionProc.("'lib/irb", "require_relative ", "") + call_completion_proc("'lib/irb", "require_relative ", "") 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 ", "") + call_completion_proc("'lib/irb", "require_relative ", "") end %w['lib/irb/init 'lib/irb/ruby-lex].each do |word| assert_include candidates, word @@ -160,13 +195,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", bind: binding), "str_example") + assert_equal("String", doc_namespace("str_example", bind: binding)) + assert_equal("String.to_s", doc_namespace("str_example.to_s", bind: 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", bind: binding), "@str_example") + assert_equal("String", doc_namespace("@str_example", bind: binding)) + assert_equal("String.to_s", doc_namespace("@str_example.to_s", bind: binding)) end def test_complete_sort_variables @@ -176,9 +211,15 @@ 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", bind: binding) assert_equal(%w[xzy xzy2 xzy_1], candidates) end + + def test_localvar_dependent + bind = eval('lvar = 1; binding') + assert_include(call_completion_proc('lvar&.', 'non_lvar /%i&i/i; ', '', bind: bind), 'lvar&.abs') + assert_include(call_completion_proc('lvar&.', 'lvar /%i&i/i; ', '', bind: bind), 'lvar&.sort') + end end class ConstantCompletionTest < CompletionTest @@ -189,12 +230,61 @@ 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_include(completion_candidates("IRB::Input", bind: binding), "IRB::InputCompletor") + assert_not_include(completion_candidates("Input", bind: binding), "InputCompletor") + + assert_include(completion_candidates("Fo", bind: binding), "Foo") + assert_include(completion_candidates("Fo", bind: binding), "Forwardable") + assert_include(completion_candidates("Con", bind: binding), "ConstantCompletionTest") + assert_include(completion_candidates("Var", bind: binding), "VariableCompletionTest") + + assert_equal(["Foo::B1", "Foo::B2", "Foo::B3"], completion_candidates("Foo::B", bind: binding)) + assert_equal(["Foo::B1.positive?"], completion_candidates("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)) + assert_equal(["::Forwardable"], completion_candidates("::Fo", bind: binding)) + assert_equal("Forwardable", doc_namespace("::Forwardable", bind: binding)) + end + end + + class NestedCompletionTest < CompletionTest + def assert_preposing_completable(preposing) + assert_include(call_completion_proc('1.', preposing, ''), '1.abs') + end + + def test_paren_bracket_brace + assert_preposing_completable('(') + assert_preposing_completable('[') + assert_preposing_completable('a(') + assert_preposing_completable('a[') + assert_preposing_completable('a{') + assert_preposing_completable('{x:') + assert_preposing_completable('[([{x:([(') + end + + def test_embexpr + assert_preposing_completable('"#{') + assert_preposing_completable('/#{') + assert_preposing_completable('`#{') + assert_preposing_completable('%(#{') + assert_preposing_completable('%)#{') + assert_preposing_completable('%!#{') + assert_preposing_completable('%r[#{') + assert_preposing_completable('%I]#{') + assert_preposing_completable('%W!#{') + assert_preposing_completable('%Q@#{') + end + + def test_control_syntax + assert_preposing_completable('if true;') + assert_preposing_completable('def f;') + assert_preposing_completable('def f(a =') + assert_preposing_completable('case ') + assert_preposing_completable('case a; when ') + assert_preposing_completable('case a; when b;') + assert_preposing_completable('p do;') + assert_preposing_completable('begin;') + assert_preposing_completable('p do; rescue;') + assert_preposing_completable('1 rescue ') end end @@ -216,7 +306,9 @@ def test_perfectly_matched_namespace_triggers_document_display omit unless has_rdoc_content? out, err = capture_output do - IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding) + call_completion_proc("St", '', '', bind: binding) do + IRB::InputCompletor::PerfectMatchedProc.("String", bind: binding) + end end assert_empty(err) @@ -224,32 +316,12 @@ def test_perfectly_matched_namespace_triggers_document_display 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) + call_completion_proc("St", '', '', bind: binding) do + result = IRB::InputCompletor::PerfectMatchedProc.("Stri", bind: binding) + end end assert_empty(err) @@ -294,34 +366,25 @@ def test_complete_symbol rescue end symbols += [:aiueo, :"aiu eo"] - candidates = IRB::InputCompletor.retrieve_completion_data(":a", bind: binding) + candidates = completion_candidates(":a", bind: binding) assert_include(candidates, ":aiueo") assert_not_include(candidates, ":aiu eo") - assert_empty(IRB::InputCompletor.retrieve_completion_data(":irb_unknown_symbol_abcdefg", bind: binding)) # Do not complete empty symbol for performance reason - assert_empty(IRB::InputCompletor.retrieve_completion_data(":", bind: binding)) + assert_empty(completion_candidates(":", bind: 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)) - 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", bind: binding)) + assert_empty(completion_candidates(":::", bind: binding)) end def test_complete_reserved_words - candidates = IRB::InputCompletor.retrieve_completion_data("de", bind: binding) + candidates = completion_candidates("de", bind: binding) %w[def defined?].each do |word| assert_include candidates, word end - candidates = IRB::InputCompletor.retrieve_completion_data("__", bind: binding) + candidates = completion_candidates("__", bind: binding) %w[__ENCODING__ __LINE__ __FILE__].each do |word| assert_include candidates, word end @@ -342,11 +405,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: bind), "public_hoge") + assert_include(doc_namespace("public_hoge", bind: 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: bind), "private_hoge") + assert_include(doc_namespace("private_hoge", bind: bind), "private_hoge") end end end