From 849669fab79660b86329d3c971c4d1a2ab394629 Mon Sep 17 00:00:00 2001 From: Andrey Samsonov Date: Wed, 15 Jul 2026 17:32:45 +0200 Subject: [PATCH 1/3] Sort completion candidates Method candidates were returned in method table order, which looks random in the completion dialog. Sort all candidates in one place at the end of completion_candidates and drop the per-branch sorts. Candidates starting with an underscore go last, so that completing an empty method name does not begin with __id__, __send__ and other internals. --- lib/repl_type_completor/result.rb | 19 +++++++------ .../test_repl_type_completor.rb | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/lib/repl_type_completor/result.rb b/lib/repl_type_completor/result.rb index edfaa64..138e529 100644 --- a/lib/repl_type_completor/result.rb +++ b/lib/repl_type_completor/result.rb @@ -52,13 +52,13 @@ def completion_candidates scope_constants = type.types.flat_map do |t| scope.table_module_constants(t.module_or_class) if t.is_a?(Types::SingletonType) end - (scope_constants.compact | type.constants.map(&:to_s)).sort + scope_constants.compact | type.constants.map(&:to_s) else - scope.constants.sort | RESERVED_WORDS + scope.constants | RESERVED_WORDS end in [:ivar, name, scope] - ivars = scope.instance_variables.sort - name == '@' ? ivars + scope.class_variables.sort : ivars + ivars = scope.instance_variables + name == '@' ? ivars + scope.class_variables : ivars in [:cvar, name, scope] scope.class_variables in [:gvar, name, scope] @@ -70,13 +70,13 @@ def completion_candidates keys = receiver_type.types.grep(Types::InstanceType).select do |t| Hash == t.klass end.flat_map do |t| - t.instances.flat_map(&:keys).grep(key_type).uniq.sort + t.instances.flat_map(&:keys).grep(key_type).uniq end if key_type == Symbol keys = Symbol.all_symbols if keys.empty? && name.size >= 1 filter_symbol_candidates(keys, name, limit: 100) else - keys.select { _1.start_with?(name) }.sort + keys end in [:call, name, type, self_call] (self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS @@ -87,9 +87,12 @@ def completion_candidates end candidates.filter_map do - _1[name.size..] if _1.start_with?(name) + _1 if _1.start_with?(name) rescue EncodingError - end + end.sort_by do + # Alphabetical order, but internal methods (leading underscore) last + [_1.start_with?('_') ? 1 : 0, _1] + end.map { _1[name.size..] } rescue Exception => e ReplTypeCompletor.handle_exception(e) [] diff --git a/test/repl_type_completor/test_repl_type_completor.rb b/test/repl_type_completor/test_repl_type_completor.rb index 23d76fd..80016a4 100644 --- a/test/repl_type_completor/test_repl_type_completor.rb +++ b/test/repl_type_completor/test_repl_type_completor.rb @@ -293,6 +293,33 @@ def with_failing_method(klass, method_name, message) klass.define_method(method_name, original_method) end + def test_completion_candidates_are_sorted + xzyb, xzya, xzyc = 'b', 'a', 'c' + lvar_binding = binding + + { + '1.' => empty_binding, # [:call] with empty method name + "''.s" => empty_binding, # [:call] with method name prefix + 'Math::' => empty_binding, # [:call_or_const] + '$std' => empty_binding, # [:gvar] + 'xzy' => lvar_binding # [:lvar_or_method] + }.each do |code, bind| + candidates = ReplTypeCompletor.analyze(code, binding: bind).completion_candidates + refute_empty candidates, "Expected completion candidates of #{code.inspect} not to be empty" + underscore, regular = candidates.partition { _1.start_with?('_') } + assert_equal regular.sort + underscore.sort, candidates, "Expected completion candidates of #{code.inspect} to be sorted with underscore methods last" + end + + # Internal methods (leading underscore, e.g. __id__ and __send__) are listed last + candidates = ReplTypeCompletor.analyze('1.', binding: empty_binding).completion_candidates + assert candidates.any? { _1.start_with?('_') } + refute candidates.first.start_with?('_') + assert candidates.last.start_with?('_') + + # Local variables xzyb, xzya, xzyc are defined in non-alphabetical order + assert_equal [xzya, xzyb, xzyc], ReplTypeCompletor.analyze('xzy', binding: lvar_binding).completion_candidates + end + def test_analyze_error with_failing_method(ReplTypeCompletor.singleton_class, :analyze_code, 'error_in_analyze_code') do assert_nil ReplTypeCompletor.analyze('1.', binding: binding) From 93ca6a1290783b0838c14ddd93861a3df8a5be96 Mon Sep 17 00:00:00 2001 From: Andrey Samsonov Date: Fri, 17 Jul 2026 09:36:51 +0200 Subject: [PATCH 2/3] Preserve completion candidate groups when sorting --- lib/repl_type_completor/result.rb | 27 +++++---- .../test_repl_type_completor.rb | 56 +++++++++++-------- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/lib/repl_type_completor/result.rb b/lib/repl_type_completor/result.rb index 138e529..37f4279 100644 --- a/lib/repl_type_completor/result.rb +++ b/lib/repl_type_completor/result.rb @@ -46,23 +46,23 @@ def completion_candidates in [:require_relative, name] RequirePaths.require_relative_completions(name, @source_file) in [:call_or_const, name, type, self_call] - ((self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS) | type.constants + ((self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS).sort | type.constants.sort in [:const, name, type, scope] if type scope_constants = type.types.flat_map do |t| scope.table_module_constants(t.module_or_class) if t.is_a?(Types::SingletonType) end - scope_constants.compact | type.constants.map(&:to_s) + (scope_constants.compact | type.constants.map(&:to_s)).sort else - scope.constants | RESERVED_WORDS + scope.constants.sort | RESERVED_WORDS end in [:ivar, name, scope] - ivars = scope.instance_variables - name == '@' ? ivars + scope.class_variables : ivars + ivars = scope.instance_variables.sort + name == '@' ? ivars + scope.class_variables.sort : ivars in [:cvar, name, scope] - scope.class_variables + scope.class_variables.sort in [:gvar, name, scope] - scope.global_variables + scope.global_variables.sort in [:symbol, name] filter_symbol_candidates(Symbol.all_symbols, name, limit: 100) in [:aref, key_type, name, receiver_type] @@ -76,23 +76,22 @@ def completion_candidates keys = Symbol.all_symbols if keys.empty? && name.size >= 1 filter_symbol_candidates(keys, name, limit: 100) else - keys + keys.sort end in [:call, name, type, self_call] (self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS in [:lvar_or_method, name, scope] - scope.self_type.all_methods.map(&:to_s) | scope.local_variables | RESERVED_WORDS + scope.local_variables.sort | scope.self_type.all_methods.map(&:to_s).sort | RESERVED_WORDS else [] end - candidates.filter_map do + candidates = candidates.filter_map do _1 if _1.start_with?(name) rescue EncodingError - end.sort_by do - # Alphabetical order, but internal methods (leading underscore) last - [_1.start_with?('_') ? 1 : 0, _1] - end.map { _1[name.size..] } + end + candidates.sort_by! { [_1.start_with?('_') ? 1 : 0, _1] } if @analyze_result.first == :call + candidates.map { _1[name.size..] } rescue Exception => e ReplTypeCompletor.handle_exception(e) [] diff --git a/test/repl_type_completor/test_repl_type_completor.rb b/test/repl_type_completor/test_repl_type_completor.rb index 80016a4..e689820 100644 --- a/test/repl_type_completor/test_repl_type_completor.rb +++ b/test/repl_type_completor/test_repl_type_completor.rb @@ -293,31 +293,41 @@ def with_failing_method(klass, method_name, message) klass.define_method(method_name, original_method) end - def test_completion_candidates_are_sorted - xzyb, xzya, xzyc = 'b', 'a', 'c' - lvar_binding = binding - - { - '1.' => empty_binding, # [:call] with empty method name - "''.s" => empty_binding, # [:call] with method name prefix - 'Math::' => empty_binding, # [:call_or_const] - '$std' => empty_binding, # [:gvar] - 'xzy' => lvar_binding # [:lvar_or_method] - }.each do |code, bind| - candidates = ReplTypeCompletor.analyze(code, binding: bind).completion_candidates - refute_empty candidates, "Expected completion candidates of #{code.inspect} not to be empty" - underscore, regular = candidates.partition { _1.start_with?('_') } - assert_equal regular.sort + underscore.sort, candidates, "Expected completion candidates of #{code.inspect} to be sorted with underscore methods last" - end + def test_call_candidates_are_sorted_with_underscore_methods_last + type = Struct.new(:methods).new(%i[_zeta beta _alpha alpha]) + result = ReplTypeCompletor::Result.new([:call, '', type, false], binding, __FILE__) + + assert_equal %w[alpha beta _alpha _zeta], result.completion_candidates + end + + def test_call_or_const_candidates_are_sorted_within_groups + type = Struct.new(:methods, :constants).new(%i[z_method a_method], %i[ZConst AConst]) + result = ReplTypeCompletor::Result.new([:call_or_const, '', type, false], binding, __FILE__) + + assert_equal %w[a_method z_method AConst ZConst], result.completion_candidates + end + + def test_const_candidates_are_sorted_before_reserved_words + scope = Struct.new(:constants).new(%w[ZConst AConst]) + result = ReplTypeCompletor::Result.new([:const, '', nil, scope], binding, __FILE__) + + assert_equal %w[AConst ZConst] + ReplTypeCompletor::Result::RESERVED_WORDS, result.completion_candidates + end + + def test_ivar_candidates_are_sorted_within_groups + scope = Struct.new(:instance_variables, :class_variables).new(%w[@z @a], %w[@@z @@a]) + result = ReplTypeCompletor::Result.new([:ivar, '@', scope], binding, __FILE__) + + assert_equal %w[a z @a @z], result.completion_candidates + end - # Internal methods (leading underscore, e.g. __id__ and __send__) are listed last - candidates = ReplTypeCompletor.analyze('1.', binding: empty_binding).completion_candidates - assert candidates.any? { _1.start_with?('_') } - refute candidates.first.start_with?('_') - assert candidates.last.start_with?('_') + def test_lvar_or_method_candidates_are_sorted_within_groups + type = Struct.new(:all_methods).new(%i[z_method a_method]) + scope = Struct.new(:self_type, :local_variables).new(type, %w[z_local a_local]) + result = ReplTypeCompletor::Result.new([:lvar_or_method, '', scope], binding, __FILE__) - # Local variables xzyb, xzya, xzyc are defined in non-alphabetical order - assert_equal [xzya, xzyb, xzyc], ReplTypeCompletor.analyze('xzy', binding: lvar_binding).completion_candidates + expected = %w[a_local z_local a_method z_method] + ReplTypeCompletor::Result::RESERVED_WORDS + assert_equal expected, result.completion_candidates end def test_analyze_error From 788a0b9739e6f19267fc9f97f41f8479cc16eaa3 Mon Sep 17 00:00:00 2001 From: Andrey Samsonov Date: Fri, 17 Jul 2026 19:35:46 +0200 Subject: [PATCH 3/3] Move method candidates sort into the :call branch --- lib/repl_type_completor/result.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/repl_type_completor/result.rb b/lib/repl_type_completor/result.rb index 37f4279..562e2d5 100644 --- a/lib/repl_type_completor/result.rb +++ b/lib/repl_type_completor/result.rb @@ -79,19 +79,21 @@ def completion_candidates keys.sort end in [:call, name, type, self_call] - (self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS + methods = (self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS + # Alphabetical order, but internal methods (leading underscore) last. + # `_1[0] == '_'` because `start_with?('_')` raises EncodingError if the + # method name's encoding is incompatible with US-ASCII. + methods.sort_by { [_1[0] == '_' ? 1 : 0, _1] } in [:lvar_or_method, name, scope] scope.local_variables.sort | scope.self_type.all_methods.map(&:to_s).sort | RESERVED_WORDS else [] end - candidates = candidates.filter_map do - _1 if _1.start_with?(name) + candidates.filter_map do + _1[name.size..] if _1.start_with?(name) rescue EncodingError end - candidates.sort_by! { [_1.start_with?('_') ? 1 : 0, _1] } if @analyze_result.first == :call - candidates.map { _1[name.size..] } rescue Exception => e ReplTypeCompletor.handle_exception(e) []