Skip to content

Commit

Permalink
Faster symbol completion with cache and limit
Browse files Browse the repository at this point in the history
  • Loading branch information
tompng committed Oct 19, 2024
1 parent 3da04b9 commit b11e2dd
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 15 deletions.
44 changes: 33 additions & 11 deletions lib/irb/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ def command_candidates(target)
end
end

def clear_symbol_cache
@all_symbols = nil
end

def symbol_candidates(prefix, first: 50, last: 50)
limit = first + last
symbols = @all_symbols ||= Symbol.all_symbols.map { _1.inspect[1..] }.sort
start_index = symbols.bsearch_index { |sym| sym.to_s >= prefix }
end_index = (start_index...symbols.size).bsearch { |i| !symbols[i].start_with?(prefix) } || symbols.size
if end_index - start_index <= limit
symbols[start_index...end_index]
else
symbols[start_index, first] + symbols[end_index - last, last]
end
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/, '')
Expand All @@ -116,18 +132,27 @@ def completion_candidates(preposing, target, _postposing, bind:)
# When completing the argument of `help` command, only commands should be candidates
return command_candidates(target) if preposing.match?(HELP_COMMAND_PREPOSING)

commands = if preposing.empty?
command_candidates(target)
type_candidates = type_completion_candidates(preposing, target, bind)

if preposing.empty?
command_candidates(target) | type_candidates
# It doesn't make sense to propose commands with other preposing
else
[]
type_candidates
end
end

def type_completion_candidates(preposing, target, bind)
result = ReplTypeCompletor.analyze(preposing + target, binding: bind, filename: @context.irb_path)
return [] unless result

return commands unless result

commands | result.completion_candidates.map { target + _1 }
analyze_result = result.instance_variable_get(:@analyze_result)
if analyze_result.is_a?(Array) && analyze_result[0] == :symbol && analyze_result[1].is_a?(String)
symbol_prefix = analyze_result[1]
symbol_candidates(symbol_prefix).map { target + _1[symbol_prefix.size..] }
else
result.completion_candidates.map { target + _1 }
end
end

def doc_namespace(preposing, matched, _postposing, bind:)
Expand Down Expand Up @@ -280,12 +305,9 @@ def retrieve_completion_data(input, bind:, doc_namespace:)
nil
else
sym = $1
candidates = Symbol.all_symbols.collect do |s|
s.inspect
rescue EncodingError
# ignore
candidates = symbol_candidates(sym[1..]).filter_map do |s|
":#{s}"
end
candidates.grep(/^#{Regexp.quote(sym)}/)
end
when /^::([A-Z][^:\.\(\)]*)$/
# Absolute Constant or class methods
Expand Down
2 changes: 2 additions & 0 deletions lib/irb/input-method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ def completion_info
def gets
Readline.input = @stdin
Readline.output = @stdout
@completor.clear_symbol_cache
if l = readline(@prompt, false)
HISTORY.push(l) if !l.empty?
@line[@line_no += 1] = l + "\n"
Expand Down Expand Up @@ -473,6 +474,7 @@ def gets
Reline.output = @stdout
Reline.prompt_proc = @prompt_proc
Reline.auto_indent_proc = @auto_indent_proc if @auto_indent_proc
@completor.clear_symbol_cache
if l = Reline.readmultiline(@prompt, false, &@check_termination_proc)
Reline::HISTORY.push(l) if !l.empty?
@line[@line_no += 1] = l + "\n"
Expand Down
16 changes: 12 additions & 4 deletions test/irb/test_completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,23 @@ def test_complete_symbol
"K".force_encoding(enc).to_sym
rescue
end
symbols += [:aiueo, :"aiu eo"]
candidates = completion_candidates(":a", binding)
assert_include(candidates, ":aiueo")
assert_not_include(candidates, ":aiu eo")
symbols += [:irb_test_symbol_aiueo, :"irb_test_symbol_aiu eo"]
candidates = completion_candidates(":irb_test_symbol_a", binding)
assert_include(candidates, ":irb_test_symbol_aiueo")
assert_not_include(candidates, ":irb_test_symbol_aiu eo")
assert_empty(completion_candidates(":irb_unknown_symbol_abcdefg", binding))
# Do not complete empty symbol for performance reason
assert_empty(completion_candidates(":", binding))
end

def test_complete_symbol_limit
symbols = 200.times.map { :"irb_test_sym_limit_#{_1}" }.sort
candidates = completion_candidates(":irb_test_sym_lim", binding)
assert_include(candidates, symbols.first.inspect)
assert_include(candidates, symbols.last.inspect)
assert_equal(candidates.size, 100)
end

def test_complete_invalid_three_colons
assert_empty(completion_candidates(":::A", binding))
assert_empty(completion_candidates(":::", binding))
Expand Down
8 changes: 8 additions & 0 deletions test/irb/test_type_completor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ def test_type_completion
assert_doc_namespace('num.chr.', 'upcase', 'String#upcase', binding: bind)
end

def test_complete_symbol_limit
symbols = 200.times.map { :"irb_test_sym_limit_#{_1}" }.sort
candidates = @completor.completion_candidates('', ':irb_test_sym_lim', '', bind: binding)
assert_include(candidates, symbols.first.inspect)
assert_include(candidates, symbols.last.inspect)
assert_equal(candidates.size, 100)
end

def test_inspect
assert_match(/\AReplTypeCompletor.*\z/, @completor.inspect)
end
Expand Down

0 comments on commit b11e2dd

Please sign in to comment.