diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb index ea6a4d1b13..a614b44324 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/index.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/index.rb @@ -23,6 +23,9 @@ def initialize # "/my/project/bar.rb" => [#], # } @files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]]) + + # Holds all require paths for every indexed item so that we can provide autocomplete for requires + @require_paths_tree = T.let(PrefixTree.new([]), PrefixTree) end sig { params(indexable: Indexable).void } @@ -40,6 +43,9 @@ def delete(indexable) end @files_to_entries.delete(indexable.full_path) + + require_path = indexable.require_path + @require_paths_tree.delete(require_path) if require_path end sig { params(entry: Entry).void } @@ -53,6 +59,11 @@ def [](fully_qualified_name) @entries[fully_qualified_name.delete_prefix("::")] end + sig { params(query: String).returns(T::Array[String]) } + def search_require_paths(query) + @require_paths_tree.search(query) + end + # Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned sig { params(query: T.nilable(String)).returns(T::Array[Entry]) } def fuzzy_search(query) @@ -95,6 +106,9 @@ def index_single(indexable, source = nil) content = source || File.read(indexable.full_path) visitor = IndexVisitor.new(self, YARP.parse(content), indexable.full_path) visitor.run + + require_path = indexable.require_path + @require_paths_tree.insert(require_path) if require_path rescue Errno::EISDIR # If `path` is a directory, just ignore it and continue indexing end diff --git a/lib/ruby_indexer/test/index_test.rb b/lib/ruby_indexer/test/index_test.rb index 68d47e30f6..076f441fd2 100644 --- a/lib/ruby_indexer/test/index_test.rb +++ b/lib/ruby_indexer/test/index_test.rb @@ -125,5 +125,18 @@ def test_index_single_ignores_directories ensure FileUtils.rm_r("lib/this_is_a_dir.rb") end + + def test_searching_for_require_paths + @index.index_single(Indexable.new("/fake", "/fake/path/foo.rb"), <<~RUBY) + class Foo + end + RUBY + @index.index_single(Indexable.new("/fake", "/fake/path/other_foo.rb"), <<~RUBY) + class Foo + end + RUBY + + assert_equal(["path/foo", "path/other_foo"], @index.search_require_paths("path")) + end end end diff --git a/lib/ruby_lsp/executor.rb b/lib/ruby_lsp/executor.rb index 52db7ab625..cc0a9b4fab 100644 --- a/lib/ruby_lsp/executor.rb +++ b/lib/ruby_lsp/executor.rb @@ -504,7 +504,7 @@ def completion(uri, position) return unless target emitter = EventEmitter.new - listener = Requests::PathCompletion.new(emitter, @message_queue) + listener = Requests::PathCompletion.new(@index, emitter, @message_queue) emitter.emit_for_target(target) listener.response end diff --git a/lib/ruby_lsp/requests/path_completion.rb b/lib/ruby_lsp/requests/path_completion.rb index c7d56466ba..dec34beea9 100644 --- a/lib/ruby_lsp/requests/path_completion.rb +++ b/lib/ruby_lsp/requests/path_completion.rb @@ -22,33 +22,24 @@ class PathCompletion < Listener sig { override.returns(ResponseType) } attr_reader :response - sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void } - def initialize(emitter, message_queue) - super + sig { params(index: RubyIndexer::Index, emitter: EventEmitter, message_queue: Thread::Queue).void } + def initialize(index, emitter, message_queue) + super(emitter, message_queue) @response = T.let([], ResponseType) - @tree = T.let(RubyIndexer::PrefixTree.new(collect_load_path_files), RubyIndexer::PrefixTree) + @index = index emitter.register(self, :on_tstring_content) end sig { params(node: SyntaxTree::TStringContent).void } def on_tstring_content(node) - @tree.search(node.value).sort.each do |path| + @index.search_require_paths(node.value).sort!.each do |path| @response << build_completion(path, node) end end private - sig { returns(T::Array[String]) } - def collect_load_path_files - $LOAD_PATH.flat_map do |p| - Dir.glob("**/*.rb", base: p) - end.map! do |result| - result.delete_suffix!(".rb") - end - end - sig { params(label: String, node: SyntaxTree::TStringContent).returns(Interface::CompletionItem) } def build_completion(label, node) Interface::CompletionItem.new( diff --git a/test/requests/path_completion_test.rb b/test/requests/path_completion_test.rb index 2ee5807d81..a718f52e0d 100644 --- a/test/requests/path_completion_test.rb +++ b/test/requests/path_completion_test.rb @@ -8,6 +8,7 @@ def setup @message_queue = Thread::Queue.new @uri = URI("file:///fake.rb") @store = RubyLsp::Store.new + @executor = RubyLsp::Executor.new(@store, @message_queue) end def teardown @@ -31,7 +32,6 @@ def test_completion_command } result = with_file_structure do - @store = RubyLsp::Store.new @store.set(uri: @uri, source: document.source, version: 1) run_request( method: "textDocument/completion", @@ -196,7 +196,6 @@ def test_completion_is_not_triggered_if_argument_is_not_a_string private def run_request(method:, params: {}) - @executor = RubyLsp::Executor.new(@store, @message_queue) result = @executor.execute({ method: method, params: params }) error = result.error raise error if error @@ -228,6 +227,13 @@ def with_file_structure(&block) tmpdir + "/foo/support/quux.rb", ]) + index = @executor.instance_variable_get(:@index) + indexables = Dir.glob(File.join(tmpdir, "**", "*.rb")).map! do |path| + RubyIndexer::Indexable.new(tmpdir, path) + end + + index.index_all(indexables: indexables) + return block.call ensure $LOAD_PATH.delete(tmpdir)