lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.9.3 vs lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.9.4

- old
+ new

@@ -15,46 +15,96 @@ # "Foo" => [#<Entry::Class>, #<Entry::Class>], # "Foo::Bar" => [#<Entry::Class>], # } @entries = T.let({}, T::Hash[String, T::Array[Entry]]) + # Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion + @entries_tree = T.let(PrefixTree[T::Array[Entry]].new, PrefixTree[T::Array[Entry]]) + # Holds references to where entries where discovered so that we can easily delete them # { # "/my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>], # "/my/project/bar.rb" => [#<Entry::Class>], # } @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[String].new, PrefixTree[String]) end - sig { params(path: String).void } - def delete(path) + sig { params(indexable: IndexablePath).void } + def delete(indexable) # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries # left, delete the constant from the index. - @files_to_entries[path]&.each do |entry| - entries = @entries[entry.name] + @files_to_entries[indexable.full_path]&.each do |entry| + name = entry.name + entries = @entries[name] next unless entries # Delete the specific entry from the list for this name entries.delete(entry) - # If all entries were deleted, then remove the name from the hash - @entries.delete(entry.name) if entries.empty? + + # If all entries were deleted, then remove the name from the hash and from the prefix tree. Otherwise, update + # the prefix tree with the current entries + if entries.empty? + @entries.delete(name) + @entries_tree.delete(name) + else + @entries_tree.insert(name, entries) + end end - @files_to_entries.delete(path) + @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 } def <<(entry) - (@entries[entry.name] ||= []) << entry + name = entry.name + + (@entries[name] ||= []) << entry (@files_to_entries[entry.file_path] ||= []) << entry + @entries_tree.insert(name, T.must(@entries[name])) end sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) } 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 + + # Searches entries in the index based on an exact prefix, intended for providing autocomplete. All possible matches + # to the prefix are returned. The return is an array of arrays, where each entry is the array of entries for a given + # name match. For example: + # ## Example + # ```ruby + # # If the index has two entries for `Foo::Bar` and one for `Foo::Baz`, then: + # index.prefix_search("Foo::B") + # # Will return: + # [ + # [#<Entry::Class name="Foo::Bar">, #<Entry::Class name="Foo::Bar">], + # [#<Entry::Class name="Foo::Baz">], + # ] + # ``` + sig { params(query: String, nesting: T::Array[String]).returns(T::Array[T::Array[Entry]]) } + def prefix_search(query, nesting) + results = (nesting.length + 1).downto(0).flat_map do |i| + prefix = T.must(nesting[0...i]).join("::") + namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}" + @entries_tree.search(namespaced_query) + end + + results.uniq! + results + 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) return @entries.flat_map { |_name, entries| entries } unless query @@ -83,19 +133,22 @@ end nil end - sig { params(paths: T::Array[String]).void } - def index_all(paths: RubyIndexer.configuration.files_to_index) - paths.each { |path| index_single(path) } + sig { params(indexable_paths: T::Array[IndexablePath]).void } + def index_all(indexable_paths: RubyIndexer.configuration.indexables) + indexable_paths.each { |path| index_single(path) } end - sig { params(path: String, source: T.nilable(String)).void } - def index_single(path, source = nil) - content = source || File.read(path) - visitor = IndexVisitor.new(self, YARP.parse(content), path) + sig { params(indexable_path: IndexablePath, source: T.nilable(String)).void } + def index_single(indexable_path, source = nil) + content = source || File.read(indexable_path.full_path) + visitor = IndexVisitor.new(self, YARP.parse(content), indexable_path.full_path) visitor.run + + require_path = indexable_path.require_path + @require_paths_tree.insert(require_path, require_path) if require_path rescue Errno::EISDIR # If `path` is a directory, just ignore it and continue indexing end class Entry