diff --git a/lib/ruby_indexer/lib/ruby_indexer/collector.rb b/lib/ruby_indexer/lib/ruby_indexer/collector.rb index cc3913fbb8..f091fd57d1 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/collector.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/collector.rb @@ -163,25 +163,23 @@ def handle_call_node(node) def handle_def_node(node) method_name = node.name.to_s comments = collect_comments(node) + declaration = Entry::MemberDeclaration.new(list_params(node.parameters), @file_path, node.location, comments) + case node.receiver when nil - @index << Entry::InstanceMethod.new( + entry = (@current_owner && @index.resolve_method( method_name, - @file_path, - node.location, - comments, - node.parameters, - @current_owner, - ) + @current_owner.name, + )) || Entry::InstanceMethod.new(method_name, @current_owner) + @index.add(entry, @file_path) + entry.add_declaration(declaration) when Prism::SelfNode - @index << Entry::SingletonMethod.new( + entry = (@current_owner && @index.resolve_method( method_name, - @file_path, - node.location, - comments, - node.parameters, - @current_owner, - ) + @current_owner.name, + )) || Entry::SingletonMethod.new(method_name, @current_owner) + @index.add(entry, @file_path) + entry.add_declaration(declaration) end end @@ -206,8 +204,8 @@ def handle_private_constant(node) # The private_constant method does not resolve the constant name. It always points to a constant that needs to # exist in the current namespace - entries = @index.get_constant(fully_qualify_name(name)) - entries&.each { |entry| entry.visibility = :private } + entry = @index.get_constant(fully_qualify_name(name)) + entry&.visibility = :private end sig do @@ -231,25 +229,31 @@ def handle_private_constant(node) def add_constant(node, name, value = nil) value = node.value unless node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode) comments = collect_comments(node) + entry = @index.get_constant(name) - @index << case value - when Prism::ConstantReadNode, Prism::ConstantPathNode - Entry::UnresolvedAlias.new(value.slice, @stack.dup, name, @file_path, node.location, comments) - when Prism::ConstantWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode, + unless entry + entry = case value + when Prism::ConstantReadNode, Prism::ConstantPathNode + Entry::UnresolvedAlias.new(value.slice, @stack.dup, name) + when Prism::ConstantWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantOperatorWriteNode - # If the right hand side is another constant assignment, we need to visit it because that constant has to be - # indexed too - @queue.prepend(value) - Entry::UnresolvedAlias.new(value.name.to_s, @stack.dup, name, @file_path, node.location, comments) - when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode, + # If the right hand side is another constant assignment, we need to visit it because that constant has to be + # indexed too + @queue.prepend(value) + Entry::UnresolvedAlias.new(value.name.to_s, @stack.dup, name) + when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathAndWriteNode - @queue.prepend(value) - Entry::UnresolvedAlias.new(value.target.slice, @stack.dup, name, @file_path, node.location, comments) - else - Entry::Constant.new(name, @file_path, node.location, comments) + @queue.prepend(value) + Entry::UnresolvedAlias.new(value.target.slice, @stack.dup, name) + else + Entry::Constant.new(name) + end end + + @index.add(entry, @file_path) + entry.add_declaration(Entry::Declaration.new(@file_path, node.location, comments)) end sig { params(node: Prism::ModuleNode).void } @@ -261,8 +265,16 @@ def add_module_entry(node) end comments = collect_comments(node) - @current_owner = Entry::Module.new(fully_qualify_name(name), @file_path, node.location, comments) - @index << @current_owner + + fully_qualified_name = fully_qualify_name(name) + existing_entry = @index.get_constant(fully_qualified_name) + + # If the user has defined the same constant as a namespace and a constant, then we end up losing the original + # definition. This is an error in Ruby, but we should still try to handle it gracefully + @current_owner = existing_entry.is_a?(Entry::Namespace) ? existing_entry : Entry::Module.new(fully_qualified_name) + + @index.add(@current_owner, @file_path) + @current_owner.add_declaration(Entry::Declaration.new(@file_path, node.location, comments)) @stack << name @queue.prepend(node.body, LEAVE_EVENT) end @@ -284,14 +296,22 @@ def add_class_entry(node) superclass.slice end - @current_owner = Entry::Class.new( - fully_qualify_name(name), - @file_path, - node.location, - comments, - parent_class, - ) - @index << @current_owner + fully_qualified_name = fully_qualify_name(name) + existing_entry = @index.get_constant(fully_qualified_name) + + # If the user has defined the same constant as a namespace and a constant, then we end up losing the original + # definition. This is an error in Ruby, but we should still try to handle it gracefully + @current_owner = if existing_entry.is_a?(Entry::Namespace) + existing_entry + else + Entry::Class.new( + fully_qualified_name, + parent_class, + ) + end + @index.add(@current_owner, @file_path) + @current_owner.add_declaration(Entry::Declaration.new(@file_path, node.location, comments)) + @stack << name @queue.prepend(node.body, LEAVE_EVENT) end @@ -350,8 +370,31 @@ def handle_attribute(node, reader:, writer:) next unless name && loc - @index << Entry::Accessor.new(name, @file_path, loc, comments, @current_owner) if reader - @index << Entry::Accessor.new("#{name}=", @file_path, loc, comments, @current_owner) if writer + if reader + entry = (@current_owner && @index.resolve_method( + name, + @current_owner.name, + )) || Entry::Accessor.new(name, @current_owner) + + @index.add(entry, @file_path) + entry.add_declaration(Entry::MemberDeclaration.new([], @file_path, loc, comments)) + end + + next unless writer + + writer_name = "#{name}=" + entry = (@current_owner && @index.resolve_method( + writer_name, + @current_owner.name, + )) || Entry::Accessor.new(writer_name, @current_owner) + + @index.add(entry, @file_path) + entry.add_declaration(Entry::MemberDeclaration.new( + [Entry::RequiredParameter.new(name: name.to_sym)], + @file_path, + loc, + comments, + )) end end @@ -384,5 +427,89 @@ def handle_module_operation(node, operation) collection = operation == :included_modules ? @current_owner.included_modules : @current_owner.prepended_modules collection.concat(names) end + + sig { params(parameters_node: T.nilable(Prism::ParametersNode)).returns(T::Array[Entry::Parameter]) } + def list_params(parameters_node) + return [] unless parameters_node + + parameters = [] + + parameters_node.requireds.each do |required| + name = parameter_name(required) + next unless name + + parameters << Entry::RequiredParameter.new(name: name) + end + + parameters_node.optionals.each do |optional| + name = parameter_name(optional) + next unless name + + parameters << Entry::OptionalParameter.new(name: name) + end + + parameters_node.keywords.each do |keyword| + name = parameter_name(keyword) + next unless name + + case keyword + when Prism::RequiredKeywordParameterNode + parameters << Entry::KeywordParameter.new(name: name) + when Prism::OptionalKeywordParameterNode + parameters << Entry::OptionalKeywordParameter.new(name: name) + end + end + + rest = parameters_node.rest + + if rest.is_a?(Prism::RestParameterNode) + rest_name = rest.name || Entry::RestParameter::DEFAULT_NAME + parameters << Entry::RestParameter.new(name: rest_name) + end + + keyword_rest = parameters_node.keyword_rest + + if keyword_rest.is_a?(Prism::KeywordRestParameterNode) + keyword_rest_name = parameter_name(keyword_rest) || Entry::KeywordRestParameter::DEFAULT_NAME + parameters << Entry::KeywordRestParameter.new(name: keyword_rest_name) + end + + parameters_node.posts.each do |post| + name = parameter_name(post) + next unless name + + parameters << Entry::RequiredParameter.new(name: name) + end + + block = parameters_node.block + parameters << Entry::BlockParameter.new(name: block.name || Entry::BlockParameter::DEFAULT_NAME) if block + + parameters + end + + sig { params(node: T.nilable(Prism::Node)).returns(T.nilable(Symbol)) } + def parameter_name(node) + case node + when Prism::RequiredParameterNode, Prism::OptionalParameterNode, + Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode, + Prism::RestParameterNode, Prism::KeywordRestParameterNode + node.name + when Prism::MultiTargetNode + names = node.lefts.map { |parameter_node| parameter_name(parameter_node) } + + rest = node.rest + if rest.is_a?(Prism::SplatNode) + name = rest.expression&.slice + names << (rest.operator == "*" ? "*#{name}".to_sym : name&.to_sym) + end + + names << nil if rest.is_a?(Prism::ImplicitRestNode) + + names.concat(node.rights.map { |parameter_node| parameter_name(parameter_node) }) + + names_with_commas = names.join(", ") + :"(#{names_with_commas})" + end + end end end diff --git a/lib/ruby_indexer/lib/ruby_indexer/entry.rb b/lib/ruby_indexer/lib/ruby_indexer/entry.rb index f5e7a36876..6a6ea58cc9 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/entry.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/entry.rb @@ -8,74 +8,112 @@ class Entry sig { returns(String) } attr_reader :name - sig { returns(String) } - attr_reader :file_path - - sig { returns(RubyIndexer::Location) } - attr_reader :location - - sig { returns(T::Array[String]) } - attr_reader :comments - sig { returns(Symbol) } attr_accessor :visibility - sig do - params( - name: String, - file_path: String, - location: T.any(Prism::Location, RubyIndexer::Location), - comments: T::Array[String], - ).void - end - def initialize(name, file_path, location, comments) + sig { returns(T::Array[Declaration]) } + attr_reader :declarations + + sig { params(name: String).void } + def initialize(name) @name = name - @file_path = file_path - @comments = comments @visibility = T.let(:public, Symbol) + @declarations = T.let([], T::Array[Declaration]) + end - @location = T.let( - if location.is_a?(Prism::Location) - Location.new( - location.start_line, - location.end_line, - location.start_column, - location.end_column, - ) - else - location - end, - RubyIndexer::Location, - ) + sig { params(declaration: Declaration).void } + def add_declaration(declaration) + @declarations << declaration end - sig { returns(String) } - def file_name - File.basename(@file_path) + sig { returns(T::Array[String]) } + def comments + @declarations.flat_map(&:comments) end - class Namespace < Entry + # A declaration represents a single place in the code where a declaration for the given entry exists. For example, + # if a class is re-opened multiple times, there will be a single entry for the class with multiple declarations. + # This base declaration class can be used to track any general identifiers if the only information they contain is a + # file path, a location and some comments + class Declaration extend T::Sig - extend T::Helpers - abstract! + sig { returns(String) } + attr_reader :file_path - sig { returns(T::Array[String]) } - attr_accessor :included_modules + sig { returns(RubyIndexer::Location) } + attr_reader :location sig { returns(T::Array[String]) } - attr_accessor :prepended_modules + attr_reader :comments sig do params( - name: String, file_path: String, location: T.any(Prism::Location, RubyIndexer::Location), comments: T::Array[String], ).void end - def initialize(name, file_path, location, comments) - super(name, file_path, location, comments) + def initialize(file_path, location, comments) + @file_path = file_path + @comments = comments + @location = T.let( + if location.is_a?(Prism::Location) + Location.new( + location.start_line, + location.end_line, + location.start_column, + location.end_column, + ) + else + location + end, + RubyIndexer::Location, + ) + end + + sig { returns(String) } + def file_name + File.basename(@file_path) + end + end + + class MemberDeclaration < Declaration + extend T::Sig + + sig { returns(T::Array[Entry::Parameter]) } + attr_reader :parameters + + sig do + params( + parameters: T::Array[Entry::Parameter], + file_path: String, + location: T.any(Prism::Location, RubyIndexer::Location), + comments: T::Array[String], + ).void + end + def initialize(parameters, file_path, location, comments) + super(file_path, location, comments) + + @parameters = parameters + end + end + + class Namespace < Entry + extend T::Sig + extend T::Helpers + + abstract! + + sig { returns(T::Array[String]) } + attr_accessor :included_modules + + sig { returns(T::Array[String]) } + attr_accessor :prepended_modules + + sig { params(name: String).void } + def initialize(name) + super(name) @included_modules = T.let([], T::Array[String]) @prepended_modules = T.let([], T::Array[String]) end @@ -97,17 +135,9 @@ class Class < Namespace sig { returns(T.nilable(String)) } attr_reader :parent_class - sig do - params( - name: String, - file_path: String, - location: T.any(Prism::Location, RubyIndexer::Location), - comments: T::Array[String], - parent_class: T.nilable(String), - ).void - end - def initialize(name, file_path, location, comments, parent_class) - super(name, file_path, location, comments) + sig { params(name: String, parent_class: T.nilable(String)).void } + def initialize(name, parent_class) + super(name) @parent_class = T.let(parent_class, T.nilable(String)) end end @@ -189,153 +219,25 @@ def decorated_name end class Member < Entry - extend T::Sig extend T::Helpers - abstract! sig { returns(T.nilable(Entry::Namespace)) } attr_reader :owner - sig do - params( - name: String, - file_path: String, - location: T.any(Prism::Location, RubyIndexer::Location), - comments: T::Array[String], - owner: T.nilable(Entry::Namespace), - ).void - end - def initialize(name, file_path, location, comments, owner) - super(name, file_path, location, comments) + sig { params(name: String, owner: T.nilable(Entry::Namespace)).void } + def initialize(name, owner) + super(name) @owner = owner end - - sig { abstract.returns(T::Array[Parameter]) } - def parameters; end end class Accessor < Member - extend T::Sig - - sig { override.returns(T::Array[Parameter]) } - def parameters - params = [] - params << RequiredParameter.new(name: name.delete_suffix("=").to_sym) if name.end_with?("=") - params - end end class Method < Member - extend T::Sig extend T::Helpers - abstract! - - sig { override.returns(T::Array[Parameter]) } - attr_reader :parameters - - sig do - params( - name: String, - file_path: String, - location: T.any(Prism::Location, RubyIndexer::Location), - comments: T::Array[String], - parameters_node: T.nilable(Prism::ParametersNode), - owner: T.nilable(Entry::Namespace), - ).void - end - def initialize(name, file_path, location, comments, parameters_node, owner) # rubocop:disable Metrics/ParameterLists - super(name, file_path, location, comments, owner) - - @parameters = T.let(list_params(parameters_node), T::Array[Parameter]) - end - - private - - sig { params(parameters_node: T.nilable(Prism::ParametersNode)).returns(T::Array[Parameter]) } - def list_params(parameters_node) - return [] unless parameters_node - - parameters = [] - - parameters_node.requireds.each do |required| - name = parameter_name(required) - next unless name - - parameters << RequiredParameter.new(name: name) - end - - parameters_node.optionals.each do |optional| - name = parameter_name(optional) - next unless name - - parameters << OptionalParameter.new(name: name) - end - - parameters_node.keywords.each do |keyword| - name = parameter_name(keyword) - next unless name - - case keyword - when Prism::RequiredKeywordParameterNode - parameters << KeywordParameter.new(name: name) - when Prism::OptionalKeywordParameterNode - parameters << OptionalKeywordParameter.new(name: name) - end - end - - rest = parameters_node.rest - - if rest.is_a?(Prism::RestParameterNode) - rest_name = rest.name || RestParameter::DEFAULT_NAME - parameters << RestParameter.new(name: rest_name) - end - - keyword_rest = parameters_node.keyword_rest - - if keyword_rest.is_a?(Prism::KeywordRestParameterNode) - keyword_rest_name = parameter_name(keyword_rest) || KeywordRestParameter::DEFAULT_NAME - parameters << KeywordRestParameter.new(name: keyword_rest_name) - end - - parameters_node.posts.each do |post| - name = parameter_name(post) - next unless name - - parameters << RequiredParameter.new(name: name) - end - - block = parameters_node.block - parameters << BlockParameter.new(name: block.name || BlockParameter::DEFAULT_NAME) if block - - parameters - end - - sig { params(node: T.nilable(Prism::Node)).returns(T.nilable(Symbol)) } - def parameter_name(node) - case node - when Prism::RequiredParameterNode, Prism::OptionalParameterNode, - Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode, - Prism::RestParameterNode, Prism::KeywordRestParameterNode - node.name - when Prism::MultiTargetNode - names = node.lefts.map { |parameter_node| parameter_name(parameter_node) } - - rest = node.rest - if rest.is_a?(Prism::SplatNode) - name = rest.expression&.slice - names << (rest.operator == "*" ? "*#{name}".to_sym : name&.to_sym) - end - - names << nil if rest.is_a?(Prism::ImplicitRestNode) - - names.concat(node.rights.map { |parameter_node| parameter_name(parameter_node) }) - - names_with_commas = names.join(", ") - :"(#{names_with_commas})" - end - end end class SingletonMethod < Method @@ -368,13 +270,10 @@ class UnresolvedAlias < Entry target: String, nesting: T::Array[String], name: String, - file_path: String, - location: T.any(Prism::Location, RubyIndexer::Location), - comments: T::Array[String], ).void end - def initialize(target, nesting, name, file_path, location, comments) # rubocop:disable Metrics/ParameterLists - super(name, file_path, location, comments) + def initialize(target, nesting, name) + super(name) @target = target @nesting = nesting @@ -390,9 +289,10 @@ class Alias < Entry sig { params(target: String, unresolved_alias: UnresolvedAlias).void } def initialize(target, unresolved_alias) - super(unresolved_alias.name, unresolved_alias.file_path, unresolved_alias.location, unresolved_alias.comments) + super(unresolved_alias.name) @target = target + @declarations = unresolved_alias.declarations end end end diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb index 2568c5d40f..4232e0fb0b 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/index.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/index.rb @@ -7,6 +7,10 @@ class Index class UnresolvableAliasError < StandardError; end + ConstantType = T.type_alias do + T.any(Entry::Class, Entry::Module, Entry::Constant, Entry::Alias, Entry::UnresolvedAlias) + end + # The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query ENTRY_SIMILARITY_THRESHOLD = 0.7 @@ -14,28 +18,28 @@ class UnresolvableAliasError < StandardError; end def initialize # Holds all constant entries in the index using the following format: # { - # "Foo" => [#, #], - # "Foo::Bar" => [#], + # "Foo" => #, #]>, + # "Foo::Bar" => #]>, # } - @constant_entries = T.let({}, T::Hash[String, T::Array[Entry]]) + @constant_entries = T.let({}, T::Hash[String, ConstantType]) # Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion - @constant_entries_tree = T.let(PrefixTree[T::Array[Entry]].new, PrefixTree[T::Array[Entry]]) + @constant_entries_tree = T.let(PrefixTree[ConstantType].new, PrefixTree[ConstantType]) # Holds all method entries in the index using the following format: # { # "method_name" => [#, #], # "foo" => [#], # } - @method_entries = T.let({}, T::Hash[String, T::Array[Entry]]) + @method_entries = T.let({}, T::Hash[String, T::Array[Entry::Member]]) # Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion - @method_entries_tree = T.let(PrefixTree[T::Array[Entry]].new, PrefixTree[T::Array[Entry]]) + @method_entries_tree = T.let(PrefixTree[T::Array[Entry::Member]].new, PrefixTree[T::Array[Entry::Member]]) # Holds references to where entries where discovered so that we can easily delete them # { - # "/my/project/foo.rb" => [#, #], - # "/my/project/bar.rb" => [#], + # "/my/project/foo.rb" => [#, #]>, #], + # "/my/project/bar.rb" => #]>, # } @files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]]) @@ -47,62 +51,59 @@ def initialize 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[indexable.full_path]&.each do |entry| - if entry.is_a?(Entry::Member) - entry_set = @method_entries - entry_tree = @method_entries_tree - else - entry_set = @constant_entries - entry_tree = @constant_entries_tree - end - - name = entry.name - entries = entry_set[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 and from the prefix tree. Otherwise, update - # the prefix tree with the current entries - if entries.empty? - entry_set.delete(name) - entry_tree.delete(name) - else - entry_tree.insert(name, entries) + full_path = indexable.full_path + + @files_to_entries[full_path]&.each do |file_entry| + name = file_entry.name + declarations = file_entry.declarations + deleted_declaration = T.must(declarations.find { |declaration| declaration.file_path == full_path }) + declarations.delete(deleted_declaration) + + if declarations.empty? + if file_entry.is_a?(Entry::Member) + @method_entries.delete(name) + @method_entries_tree.delete(name) + else + @constant_entries.delete(name) + @constant_entries_tree.delete(name) + end end end - @files_to_entries.delete(indexable.full_path) - + @files_to_entries.delete(full_path) require_path = indexable.require_path @require_paths_tree.delete(require_path) if require_path end - sig { params(entry: Entry).void } - def <<(entry) + # Add a new entry to the index. Must include the file path where that entry declaration was discovered + sig { params(entry: Entry, file_path: String).void } + def add(entry, file_path) name = entry.name - (@files_to_entries[entry.file_path] ||= []) << entry - - if entry.is_a?(Entry::Member) - entry_set = @method_entries - entry_tree = @method_entries_tree - else - entry_set = @constant_entries - entry_tree = @constant_entries_tree - end + (@files_to_entries[file_path] ||= []) << entry + + case entry + when Entry::Member + # If an entry for this method already exists under the same owner, then we don't want to add a duplicate entry + existing_entry = @method_entries[name] + return if existing_entry&.any? { |e| e.owner == entry.owner } + + (@method_entries[name] ||= []) << entry + @method_entries_tree.insert(name, T.must(@method_entries[name])) + when Entry::Class, Entry::Module, Entry::Constant, Entry::Alias, Entry::UnresolvedAlias + return if @constant_entries.key?(name) - (entry_set[name] ||= []) << entry - entry_tree.insert(name, T.must(entry_set[name])) + @constant_entries[name] = entry + @constant_entries_tree.insert(name, entry) + end end - sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) } + sig { params(fully_qualified_name: String).returns(T.nilable(ConstantType)) } def get_constant(fully_qualified_name) @constant_entries[fully_qualified_name.delete_prefix("::")] end - sig { params(name: String).returns(T.nilable(T::Array[Entry])) } - def get_method(name) + sig { params(name: String).returns(T.nilable(T::Array[Entry::Member])) } + def get_methods(name) @method_entries[name] end @@ -124,7 +125,7 @@ def search_require_paths(query) # [#], # ] # ``` - sig { params(query: String, nesting: T.nilable(T::Array[String])).returns(T::Array[T::Array[Entry]]) } + sig { params(query: String, nesting: T.nilable(T::Array[String])).returns(T::Array[ConstantType]) } def prefix_search_constants(query, nesting = nil) unless nesting results = @constant_entries_tree.search(query) @@ -142,7 +143,7 @@ def prefix_search_constants(query, nesting = nil) results end - sig { params(query: String, nesting: T.nilable(T::Array[String])).returns(T::Array[T::Array[Entry]]) } + sig { params(query: String, nesting: T.nilable(T::Array[String])).returns(T::Array[T::Array[Entry::Member]]) } def prefix_search_methods(query, nesting = nil) unless nesting results = @method_entries_tree.search(query) @@ -163,10 +164,7 @@ def prefix_search_methods(query, nesting = nil) # 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) - unless query - constants = @constant_entries.flat_map { |_name, entries| entries } - return constants + @method_entries.flat_map { |_name, entries| entries } - end + return @constant_entries.values + @method_entries.values.flatten unless query normalized_query = query.gsub("::", "").downcase @@ -183,12 +181,12 @@ def fuzzy_search(query) # 1. Foo::Bar::Baz # 2. Foo::Baz # 3. Baz - sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(T::Array[Entry])) } + sig { params(name: String, nesting: T::Array[String]).returns(T.nilable(ConstantType)) } def resolve_constant(name, nesting) if name.start_with?("::") name = name.delete_prefix("::") - results = @constant_entries[name] || @constant_entries[follow_aliased_namespace(name)] - return results&.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } + target_entry = @constant_entries[name] || @constant_entries[follow_aliased_namespace(name)] + return target_entry.is_a?(Entry::UnresolvedAlias) ? resolve_alias(target_entry) : target_entry end nesting.length.downto(0).each do |i| @@ -202,8 +200,8 @@ def resolve_constant(name, nesting) # the LSP itself we alias `RubyLsp::Interface` to `LanguageServer::Protocol::Interface`, which means doing # `RubyLsp::Interface::Location` is allowed. For these cases, we need some way to realize that the # `RubyLsp::Interface` part is an alias, that has to be resolved - entries = @constant_entries[full_name] || @constant_entries[follow_aliased_namespace(full_name)] - return entries.map { |e| e.is_a?(Entry::UnresolvedAlias) ? resolve_alias(e) : e } if entries + entry = @constant_entries[full_name] || @constant_entries[follow_aliased_namespace(full_name)] + return entry.is_a?(Entry::UnresolvedAlias) ? resolve_alias(entry) : entry if entry end nil @@ -267,7 +265,7 @@ def follow_aliased_namespace(name) (parts.length - 1).downto(0).each do |i| current_name = T.must(parts[0..i]).join("::") - entry = @constant_entries[current_name]&.first + entry = @constant_entries[current_name] case entry when Entry::Alias @@ -292,19 +290,14 @@ def follow_aliased_namespace(name) # Attempts to find methods for a resolved fully qualified receiver name. # Returns `nil` if the method does not exist on that receiver - sig { params(method_name: String, receiver_name: String).returns(T.nilable(T::Array[Entry::Member])) } + sig { params(method_name: String, receiver_name: String).returns(T.nilable(Entry::Member)) } def resolve_method(method_name, receiver_name) - method_entries = get_method(method_name) - owner_entries = get_constant(receiver_name) - return unless owner_entries && method_entries - - owner_name = T.must(owner_entries.first).name - T.cast( - method_entries.grep(Entry::Member).select do |entry| - T.cast(entry, Entry::Member).owner&.name == owner_name - end, - T::Array[Entry::Member], - ) + method_entries = get_methods(method_name) + owner_entry = get_constant(receiver_name) + return unless method_entries && owner_entry + + owner_name = owner_entry.name + method_entries.find { |entry| entry.owner&.name == owner_name } end private @@ -316,15 +309,12 @@ def resolve_alias(entry) target = resolve_constant(entry.target, entry.nesting) return entry unless target - target_name = T.must(target.first).name + target_name = target.name resolved_alias = Entry::Alias.new(target_name, entry) # Replace the UnresolvedAlias by a resolved one so that we don't have to do this again later - original_entries = T.must(@constant_entries[entry.name]) - original_entries.delete(entry) - original_entries << resolved_alias - - @constant_entries_tree.insert(entry.name, original_entries) + @constant_entries[entry.name] = resolved_alias + @constant_entries_tree.insert(entry.name, resolved_alias) resolved_alias end diff --git a/lib/ruby_indexer/test/classes_and_modules_test.rb b/lib/ruby_indexer/test/classes_and_modules_test.rb index 14d5bd38e5..82f863d217 100644 --- a/lib/ruby_indexer/test/classes_and_modules_test.rb +++ b/lib/ruby_indexer/test/classes_and_modules_test.rb @@ -161,10 +161,10 @@ class Foo class Bar; end RUBY - foo_entry = @index.get_constant("Foo").first + foo_entry = @index.get_constant("Foo") assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n")) - bar_entry = @index.get_constant("Bar").first + bar_entry = @index.get_constant("Bar") assert_equal("This Bar comment has 1 line padding", bar_entry.comments.join("\n")) end @@ -174,7 +174,7 @@ def test_skips_comments_containing_invalid_encodings class Foo end RUBY - assert(@index.get_constant("Foo").first) + assert(@index.get_constant("Foo")) end def test_comments_can_be_attached_to_a_namespaced_class @@ -187,10 +187,10 @@ class Bar; end end RUBY - foo_entry = @index.get_constant("Foo").first + foo_entry = @index.get_constant("Foo") assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n")) - bar_entry = @index.get_constant("Foo::Bar").first + bar_entry = @index.get_constant("Foo::Bar") assert_equal("This is a Bar comment", bar_entry.comments.join("\n")) end @@ -203,11 +203,12 @@ class Foo; end class Foo; end RUBY - first_foo_entry = @index.get_constant("Foo")[0] - assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n")) + foo_entry = @index.get_constant("Foo") + declaration = T.must(foo_entry.declarations[0]) + assert_equal("This is a Foo comment", declaration.comments.join("\n")) - second_foo_entry = @index.get_constant("Foo")[1] - assert_equal("This is another Foo comment", second_foo_entry.comments.join("\n")) + declaration = T.must(foo_entry.declarations[1]) + assert_equal("This is another Foo comment", declaration.comments.join("\n")) end def test_comments_removes_the_leading_pound_and_space @@ -219,11 +220,13 @@ class Foo; end class Bar; end RUBY - first_foo_entry = @index.get_constant("Foo")[0] - assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n")) + foo_entry = @index.get_constant("Foo") + declaration = T.must(foo_entry.declarations[0]) + assert_equal("This is a Foo comment", declaration.comments.join("\n")) - second_foo_entry = @index.get_constant("Bar")[0] - assert_equal("This is a Bar comment", second_foo_entry.comments.join("\n")) + bar_entry = @index.get_constant("Bar") + declaration = T.must(bar_entry.declarations[0]) + assert_equal("This is a Bar comment", declaration.comments.join("\n")) end def test_private_class_and_module_indexing @@ -239,13 +242,13 @@ class D; end end RUBY - b_const = @index.get_constant("A::B").first + b_const = @index.get_constant("A::B") assert_equal(:private, b_const.visibility) - c_const = @index.get_constant("A::C").first + c_const = @index.get_constant("A::C") assert_equal(:private, c_const.visibility) - d_const = @index.get_constant("A::D").first + d_const = @index.get_constant("A::D") assert_equal(:public, d_const.visibility) end @@ -269,16 +272,16 @@ class FinalThing < Something::Baz end RUBY - foo = T.must(@index.get_constant("Foo").first) + foo = T.must(@index.get_constant("Foo")) assert_equal("Bar", foo.parent_class) - baz = T.must(@index.get_constant("Baz").first) + baz = T.must(@index.get_constant("Baz")) assert_nil(baz.parent_class) - qux = T.must(@index.get_constant("Something::Qux").first) + qux = T.must(@index.get_constant("Something::Qux")) assert_equal("::Baz", qux.parent_class) - final_thing = T.must(@index.get_constant("FinalThing").first) + final_thing = T.must(@index.get_constant("FinalThing")) assert_equal("Something::Baz", final_thing.parent_class) end @@ -318,13 +321,13 @@ class ConstantPathReferences end RUBY - foo = T.must(@index.get_constant("Foo")[0]) + foo = T.must(@index.get_constant("Foo")) assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.included_modules) - qux = T.must(@index.get_constant("Foo::Qux")[0]) + qux = T.must(@index.get_constant("Foo::Qux")) assert_equal(["Corge", "Corge", "Baz"], qux.included_modules) - constant_path_references = T.must(@index.get_constant("ConstantPathReferences")[0]) + constant_path_references = T.must(@index.get_constant("ConstantPathReferences")) assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules) end @@ -364,13 +367,13 @@ class ConstantPathReferences end RUBY - foo = T.must(@index.get_constant("Foo")[0]) + foo = T.must(@index.get_constant("Foo")) assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.prepended_modules) - qux = T.must(@index.get_constant("Foo::Qux")[0]) + qux = T.must(@index.get_constant("Foo::Qux")) assert_equal(["Corge", "Corge", "Baz"], qux.prepended_modules) - constant_path_references = T.must(@index.get_constant("ConstantPathReferences")[0]) + constant_path_references = T.must(@index.get_constant("ConstantPathReferences")) assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.prepended_modules) end end diff --git a/lib/ruby_indexer/test/constant_test.rb b/lib/ruby_indexer/test/constant_test.rb index 08912b8873..90c0c7d6a3 100644 --- a/lib/ruby_indexer/test/constant_test.rb +++ b/lib/ruby_indexer/test/constant_test.rb @@ -83,16 +83,16 @@ class A A::BAZ = 1 RUBY - foo_comment = @index.get_constant("FOO").first.comments.join("\n") + foo_comment = @index.get_constant("FOO").comments.join("\n") assert_equal("FOO comment", foo_comment) - a_foo_comment = @index.get_constant("A::FOO").first.comments.join("\n") + a_foo_comment = @index.get_constant("A::FOO").comments.join("\n") assert_equal("A::FOO comment", a_foo_comment) - bar_comment = @index.get_constant("BAR").first.comments.join("\n") + bar_comment = @index.get_constant("BAR").comments.join("\n") assert_equal("::BAR comment", bar_comment) - a_baz_comment = @index.get_constant("A::BAZ").first.comments.join("\n") + a_baz_comment = @index.get_constant("A::BAZ").comments.join("\n") assert_equal("A::BAZ comment", a_baz_comment) end @@ -118,13 +118,13 @@ class A end RUBY - b_const = @index.get_constant("A::B").first + b_const = @index.get_constant("A::B") assert_equal(:private, b_const.visibility) - c_const = @index.get_constant("A::C").first + c_const = @index.get_constant("A::C") assert_equal(:private, c_const.visibility) - d_const = @index.get_constant("A::D").first + d_const = @index.get_constant("A::D") assert_equal(:public, d_const.visibility) end @@ -151,13 +151,13 @@ module B end RUBY - a_const = @index.get_constant("A::B::CONST_A").first + a_const = @index.get_constant("A::B::CONST_A") assert_equal(:private, a_const.visibility) - b_const = @index.get_constant("A::B::CONST_B").first + b_const = @index.get_constant("A::B::CONST_B") assert_equal(:private, b_const.visibility) - c_const = @index.get_constant("A::B::CONST_C").first + c_const = @index.get_constant("A::B::CONST_C") assert_equal(:private, c_const.visibility) end @@ -175,10 +175,10 @@ module B A::B.private_constant(:CONST_B) RUBY - a_const = @index.get_constant("A::B::CONST_A").first + a_const = @index.get_constant("A::B::CONST_A") assert_equal(:private, a_const.visibility) - b_const = @index.get_constant("A::B::CONST_B").first + b_const = @index.get_constant("A::B::CONST_B") assert_equal(:private, b_const.visibility) end @@ -196,12 +196,12 @@ module C SECOND = A::FIRST RUBY - unresolve_entry = @index.get_constant("A::FIRST").first + unresolve_entry = @index.get_constant("A::FIRST") assert_instance_of(Entry::UnresolvedAlias, unresolve_entry) assert_equal(["A"], unresolve_entry.nesting) assert_equal("B::C", unresolve_entry.target) - resolved_entry = @index.resolve_constant("A::FIRST", []).first + resolved_entry = @index.resolve_constant("A::FIRST", []) assert_instance_of(Entry::Alias, resolved_entry) assert_equal("A::B::C", resolved_entry.target) end @@ -222,25 +222,25 @@ module Other end RUBY - unresolve_entry = @index.get_constant("A::ALIAS").first + unresolve_entry = @index.get_constant("A::ALIAS") assert_instance_of(Entry::UnresolvedAlias, unresolve_entry) assert_equal(["A"], unresolve_entry.nesting) assert_equal("B", unresolve_entry.target) - resolved_entry = @index.resolve_constant("ALIAS", ["A"]).first + resolved_entry = @index.resolve_constant("ALIAS", ["A"]) assert_instance_of(Entry::Alias, resolved_entry) assert_equal("A::B", resolved_entry.target) - resolved_entry = @index.resolve_constant("ALIAS::C", ["A"]).first + resolved_entry = @index.resolve_constant("ALIAS::C", ["A"]) assert_instance_of(Entry::Module, resolved_entry) assert_equal("A::B::C", resolved_entry.name) - unresolve_entry = @index.get_constant("Other::ONE_MORE").first + unresolve_entry = @index.get_constant("Other::ONE_MORE") assert_instance_of(Entry::UnresolvedAlias, unresolve_entry) assert_equal(["Other"], unresolve_entry.nesting) assert_equal("A::ALIAS", unresolve_entry.target) - resolved_entry = @index.resolve_constant("Other::ONE_MORE::C", []).first + resolved_entry = @index.resolve_constant("Other::ONE_MORE::C", []) assert_instance_of(Entry::Module, resolved_entry) end @@ -255,55 +255,55 @@ module A RUBY # B and C - unresolve_entry = @index.get_constant("A::B").first + unresolve_entry = @index.get_constant("A::B") assert_instance_of(Entry::UnresolvedAlias, unresolve_entry) assert_equal(["A"], unresolve_entry.nesting) assert_equal("C", unresolve_entry.target) - resolved_entry = @index.resolve_constant("A::B", []).first + resolved_entry = @index.resolve_constant("A::B", []) assert_instance_of(Entry::Alias, resolved_entry) assert_equal("A::C", resolved_entry.target) - constant = @index.get_constant("A::C").first + constant = @index.get_constant("A::C") assert_instance_of(Entry::Constant, constant) # D and E - unresolve_entry = @index.get_constant("A::D").first + unresolve_entry = @index.get_constant("A::D") assert_instance_of(Entry::UnresolvedAlias, unresolve_entry) assert_equal(["A"], unresolve_entry.nesting) assert_equal("E", unresolve_entry.target) - resolved_entry = @index.resolve_constant("A::D", []).first + resolved_entry = @index.resolve_constant("A::D", []) assert_instance_of(Entry::Alias, resolved_entry) assert_equal("A::E", resolved_entry.target) # F and G::H - unresolve_entry = @index.get_constant("A::F").first + unresolve_entry = @index.get_constant("A::F") assert_instance_of(Entry::UnresolvedAlias, unresolve_entry) assert_equal(["A"], unresolve_entry.nesting) assert_equal("G::H", unresolve_entry.target) - resolved_entry = @index.resolve_constant("A::F", []).first + resolved_entry = @index.resolve_constant("A::F", []) assert_instance_of(Entry::Alias, resolved_entry) assert_equal("A::G::H", resolved_entry.target) # I::J, K::L and M - unresolve_entry = @index.get_constant("A::I::J").first + unresolve_entry = @index.get_constant("A::I::J") assert_instance_of(Entry::UnresolvedAlias, unresolve_entry) assert_equal(["A"], unresolve_entry.nesting) assert_equal("K::L", unresolve_entry.target) - resolved_entry = @index.resolve_constant("A::I::J", []).first + resolved_entry = @index.resolve_constant("A::I::J", []) assert_instance_of(Entry::Alias, resolved_entry) assert_equal("A::K::L", resolved_entry.target) # When we are resolving A::I::J, we invoke `resolve("K::L", ["A"])`, which recursively resolves A::K::L too. # Therefore, both A::I::J and A::K::L point to A::M by the end of the previous resolve invocation - resolved_entry = @index.get_constant("A::K::L").first + resolved_entry = @index.get_constant("A::K::L") assert_instance_of(Entry::Alias, resolved_entry) assert_equal("A::M", resolved_entry.target) - constant = @index.get_constant("A::M").first + constant = @index.get_constant("A::M") assert_instance_of(Entry::Constant, constant) end diff --git a/lib/ruby_indexer/test/index_test.rb b/lib/ruby_indexer/test/index_test.rb index 1dac110bd6..634d920f86 100644 --- a/lib/ruby_indexer/test/index_test.rb +++ b/lib/ruby_indexer/test/index_test.rb @@ -15,12 +15,12 @@ class Foo end RUBY - entries = @index.get_constant("Foo") - assert_equal(2, entries.length) + entry = @index.get_constant("Foo") + declarations = entry.declarations + assert_equal(2, declarations.length) @index.delete(IndexablePath.new(nil, "/fake/path/other_foo.rb")) - entries = @index.get_constant("Foo") - assert_equal(1, entries.length) + assert_equal(1, declarations.length) end def test_deleting_all_entries_for_a_class @@ -29,12 +29,12 @@ class Foo end RUBY - entries = @index.get_constant("Foo") - assert_equal(1, entries.length) + entry = @index.get_constant("Foo") + declarations = entry.declarations + assert_equal(1, declarations.length) @index.delete(IndexablePath.new(nil, "/fake/path/foo.rb")) - entries = @index.get_constant("Foo") - assert_nil(entries) + assert_nil(@index.get_constant("Foo")) end def test_index_resolve @@ -52,21 +52,17 @@ class Something end RUBY - entries = @index.resolve_constant("Something", ["Foo", "Baz"]) - refute_empty(entries) - assert_equal("Foo::Baz::Something", entries.first.name) + entry = T.must(@index.resolve_constant("Something", ["Foo", "Baz"])) + assert_equal("Foo::Baz::Something", entry.name) - entries = @index.resolve_constant("Bar", ["Foo"]) - refute_empty(entries) - assert_equal("Foo::Bar", entries.first.name) + entry = T.must(@index.resolve_constant("Bar", ["Foo"])) + assert_equal("Foo::Bar", entry.name) - entries = @index.resolve_constant("Bar", ["Foo", "Baz"]) - refute_empty(entries) - assert_equal("Foo::Bar", entries.first.name) + entry = T.must(@index.resolve_constant("Bar", ["Foo", "Baz"])) + assert_equal("Foo::Bar", entry.name) - entries = @index.resolve_constant("Foo::Bar", ["Foo", "Baz"]) - refute_empty(entries) - assert_equal("Foo::Bar", entries.first.name) + entry = T.must(@index.resolve_constant("Foo::Bar", ["Foo", "Baz"])) + assert_equal("Foo::Bar", entry.name) assert_nil(@index.resolve_constant("DoesNotExist", ["Foo"])) end @@ -86,9 +82,9 @@ class Something end RUBY - entries = @index.get_constant("::Foo::Baz::Something") - refute_empty(entries) - assert_equal("Foo::Baz::Something", entries.first.name) + entry = @index.get_constant("::Foo::Baz::Something") + refute_nil(entry) + assert_equal("Foo::Baz::Something", T.must(entry).name) end def test_fuzzy_search @@ -108,7 +104,7 @@ class Something result = @index.fuzzy_search("Bar") assert_equal(1, result.length) - assert_equal(@index.get_constant("Bar").first, result.first) + assert_equal(@index.get_constant("Bar"), result.first) result = @index.fuzzy_search("foobarsomeking") assert_equal(5, result.length) @@ -152,11 +148,11 @@ class Foo::Baz end RUBY - results = @index.prefix_search_constants("Foo", []).map { |entries| entries.map(&:name) } - assert_equal([["Foo::Bar", "Foo::Bar"], ["Foo::Baz"]], results) + results = @index.prefix_search_constants("Foo", []).map(&:name) + assert_equal(["Foo::Bar", "Foo::Baz"], results) - results = @index.prefix_search_constants("Ba", ["Foo"]).map { |entries| entries.map(&:name) } - assert_equal([["Foo::Bar", "Foo::Bar"], ["Foo::Baz"]], results) + results = @index.prefix_search_constants("Ba", ["Foo"]).map(&:name) + assert_equal(["Foo::Bar", "Foo::Baz"], results) end def test_resolve_normalizes_top_level_names @@ -168,15 +164,11 @@ class Bar; end end RUBY - entries = @index.resolve_constant("::Foo::Bar", []) - refute_nil(entries) - - assert_equal("Foo::Bar", entries.first.name) - - entries = @index.resolve_constant("::Bar", ["Foo"]) - refute_nil(entries) + entry = T.must(@index.resolve_constant("::Foo::Bar", [])) + assert_equal("Foo::Bar", entry.name) - assert_equal("Bar", entries.first.name) + entry = T.must(@index.resolve_constant("::Bar", ["Foo"])) + assert_equal("Bar", entry.name) end def test_resolving_aliases_to_non_existing_constants_with_conflicting_names @@ -188,7 +180,7 @@ class Float < self end RUBY - entry = @index.resolve_constant("INFINITY", ["Foo", "Float"]).first + entry = @index.resolve_constant("INFINITY", ["Foo", "Float"]) refute_nil(entry) assert_instance_of(Entry::UnresolvedAlias, entry) @@ -226,9 +218,9 @@ def baz; end end RUBY - entries = T.must(@index.resolve_method("baz", "Foo::Bar")) - assert_equal("baz", entries.first.name) - assert_equal("Foo::Bar", T.must(entries.first.owner).name) + entry = T.must(@index.resolve_method("baz", "Foo::Bar")) + assert_equal("baz", entry.name) + assert_equal("Foo::Bar", T.must(entry.owner).name) end def test_resolve_method_with_class_name_conflict @@ -241,9 +233,9 @@ def Array(*args); end end RUBY - entries = T.must(@index.resolve_method("Array", "Foo")) - assert_equal("Array", entries.first.name) - assert_equal("Foo", T.must(entries.first.owner).name) + entry = T.must(@index.resolve_method("Array", "Foo")) + assert_equal("Array", entry.name) + assert_equal("Foo", T.must(entry.owner).name) end def test_resolve_method_attribute @@ -253,9 +245,9 @@ class Foo end RUBY - entries = T.must(@index.resolve_method("bar", "Foo")) - assert_equal("bar", entries.first.name) - assert_equal("Foo", T.must(entries.first.owner).name) + entry = T.must(@index.resolve_method("bar", "Foo")) + assert_equal("bar", entry.name) + assert_equal("Foo", T.must(entry.owner).name) end def test_resolve_method_with_two_definitions @@ -271,15 +263,12 @@ def bar; end end RUBY - first_entry, second_entry = T.must(@index.resolve_method("bar", "Foo")) - - assert_equal("bar", first_entry.name) - assert_equal("Foo", T.must(first_entry.owner).name) - assert_includes(first_entry.comments, "Hello from first `bar`") + entry = T.must(@index.resolve_method("bar", "Foo")) + first_declaration, second_declaration = entry.declarations - assert_equal("bar", second_entry.name) - assert_equal("Foo", T.must(second_entry.owner).name) - assert_includes(second_entry.comments, "Hello from second `bar`") + assert_equal("Foo", T.must(entry.owner).name) + assert_includes(first_declaration.comments, "Hello from first `bar`") + assert_includes(second_declaration.comments, "Hello from second `bar`") end def test_prefix_search_for_methods @@ -294,7 +283,7 @@ def baz; end entries = @index.prefix_search_methods("ba") refute_empty(entries) - entry = T.must(entries.first).first + entry = T.must(T.must(entries.first).first) assert_equal("baz", entry.name) end diff --git a/lib/ruby_indexer/test/method_test.rb b/lib/ruby_indexer/test/method_test.rb index d174e2a5a7..db0bf175f8 100644 --- a/lib/ruby_indexer/test/method_test.rb +++ b/lib/ruby_indexer/test/method_test.rb @@ -47,9 +47,10 @@ def bar(a) RUBY assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") - entry = T.must(@index.get_method("bar").first) - assert_equal(1, entry.parameters.length) - parameter = entry.parameters.first + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(1, declaration.parameters.length) + parameter = declaration.parameters.first assert_equal(:a, parameter.name) assert_instance_of(Entry::RequiredParameter, parameter) end @@ -63,9 +64,10 @@ def bar((a, (b, ))) RUBY assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") - entry = T.must(@index.get_method("bar").first) - assert_equal(1, entry.parameters.length) - parameter = entry.parameters.first + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(1, entry.declarations.length) + parameter = declaration.parameters.first assert_equal(:"(a, (b, ))", parameter.name) assert_instance_of(Entry::RequiredParameter, parameter) end @@ -79,9 +81,10 @@ def bar(a = 123) RUBY assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") - entry = T.must(@index.get_method("bar").first) - assert_equal(1, entry.parameters.length) - parameter = entry.parameters.first + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(1, declaration.parameters.length) + parameter = declaration.parameters.first assert_equal(:a, parameter.name) assert_instance_of(Entry::OptionalParameter, parameter) end @@ -95,9 +98,10 @@ def bar(a:, b: 123) RUBY assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") - entry = T.must(@index.get_method("bar").first) - assert_equal(2, entry.parameters.length) - a, b = entry.parameters + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(2, declaration.parameters.length) + a, b = declaration.parameters assert_equal(:a, a.name) assert_instance_of(Entry::KeywordParameter, a) @@ -115,9 +119,10 @@ def bar(*a, **b) RUBY assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") - entry = T.must(@index.get_method("bar").first) - assert_equal(2, entry.parameters.length) - a, b = entry.parameters + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(2, declaration.parameters.length) + a, b = declaration.parameters assert_equal(:a, a.name) assert_instance_of(Entry::RestParameter, a) @@ -140,9 +145,10 @@ def qux(*a, (b, c)) RUBY assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") - entry = T.must(@index.get_method("bar").first) - assert_equal(2, entry.parameters.length) - a, b = entry.parameters + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(2, declaration.parameters.length) + a, b = declaration.parameters assert_equal(:a, a.name) assert_instance_of(Entry::RestParameter, a) @@ -150,9 +156,10 @@ def qux(*a, (b, c)) assert_equal(:b, b.name) assert_instance_of(Entry::RequiredParameter, b) - entry = T.must(@index.get_method("baz").first) - assert_equal(2, entry.parameters.length) - a, b = entry.parameters + entry = T.must(@index.resolve_method("baz", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(2, declaration.parameters.length) + a, b = declaration.parameters assert_equal(:a, a.name) assert_instance_of(Entry::KeywordRestParameter, a) @@ -160,9 +167,10 @@ def qux(*a, (b, c)) assert_equal(:b, b.name) assert_instance_of(Entry::RequiredParameter, b) - entry = T.must(@index.get_method("qux").first) - assert_equal(2, entry.parameters.length) - _a, second = entry.parameters + entry = T.must(@index.resolve_method("qux", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(2, declaration.parameters.length) + _a, second = declaration.parameters assert_equal(:"(b, c)", second.name) assert_instance_of(Entry::RequiredParameter, second) @@ -177,9 +185,10 @@ def bar((a, *b)) RUBY assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") - entry = T.must(@index.get_method("bar").first) - assert_equal(1, entry.parameters.length) - param = entry.parameters.first + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(1, declaration.parameters.length) + param = declaration.parameters.first assert_equal(:"(a, *b)", param.name) assert_instance_of(Entry::RequiredParameter, param) @@ -196,15 +205,17 @@ def baz(&) end RUBY - entry = T.must(@index.get_method("bar").first) - param = entry.parameters.first + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + param = declaration.parameters.first assert_equal(:block, param.name) assert_instance_of(Entry::BlockParameter, param) - entry = T.must(@index.get_method("baz").first) - assert_equal(1, entry.parameters.length) + entry = T.must(@index.resolve_method("baz", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(1, declaration.parameters.length) - param = entry.parameters.first + param = declaration.parameters.first assert_equal(Entry::BlockParameter::DEFAULT_NAME, param.name) assert_instance_of(Entry::BlockParameter, param) end @@ -218,9 +229,10 @@ def bar(*, **) RUBY assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") - entry = T.must(@index.get_method("bar").first) - assert_equal(2, entry.parameters.length) - first, second = entry.parameters + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + assert_equal(2, declaration.parameters.length) + first, second = declaration.parameters assert_equal(Entry::RestParameter::DEFAULT_NAME, first.name) assert_instance_of(Entry::RestParameter, first) @@ -238,8 +250,9 @@ def bar(**nil) RUBY assert_entry("bar", Entry::InstanceMethod, "/fake/path/foo.rb:1-2:2-5") - entry = T.must(@index.get_method("bar").first) - assert_empty(entry.parameters) + entry = T.must(@index.resolve_method("bar", "Foo")) + declaration = T.must(entry.declarations.first) + assert_empty(declaration.parameters) end def test_keeps_track_of_method_owner @@ -250,9 +263,8 @@ def bar end RUBY - entry = T.must(@index.get_method("bar").first) + entry = T.must(@index.resolve_method("bar", "Foo")) owner_name = T.must(entry.owner).name - assert_equal("Foo", owner_name) end @@ -265,11 +277,10 @@ class Foo attr_accessor :qux end RUBY - assert_entry("bar", Entry::Accessor, "/fake/path/foo.rb:2-15:2-18") - assert_equal("Hello there", @index.get_method("bar").first.comments.join("\n")) + assert_equal("Hello there", T.must(@index.resolve_method("bar", "Foo")).comments.join("\n")) assert_entry("other", Entry::Accessor, "/fake/path/foo.rb:2-21:2-26") - assert_equal("Hello there", @index.get_method("other").first.comments.join("\n")) + assert_equal("Hello there", T.must(@index.resolve_method("other", "Foo")).comments.join("\n")) assert_entry("baz=", Entry::Accessor, "/fake/path/foo.rb:3-15:3-18") assert_entry("qux", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20") assert_entry("qux=", Entry::Accessor, "/fake/path/foo.rb:4-17:4-20") diff --git a/lib/ruby_indexer/test/test_case.rb b/lib/ruby_indexer/test/test_case.rb index 1a7d2eb404..051914d824 100644 --- a/lib/ruby_indexer/test/test_case.rb +++ b/lib/ruby_indexer/test/test_case.rb @@ -16,23 +16,23 @@ def index(source) end def assert_entry(expected_name, type, expected_location) - entries = @index.get_constant(expected_name) || @index.get_method(expected_name) - refute_empty(entries, "Expected #{expected_name} to be indexed") + entry = @index.get_constant(expected_name) || @index.get_methods(expected_name)&.first - entry = entries.first + refute_nil(entry, "Expected #{expected_name} to be indexed") assert_instance_of(type, entry, "Expected #{expected_name} to be a #{type}") - location = entry.location + first_declaration = T.must(entry.declarations.first) + location = first_declaration.location location_string = - "#{entry.file_path}:#{location.start_line - 1}-#{location.start_column}" \ + "#{first_declaration.file_path}:#{location.start_line - 1}-#{location.start_column}" \ ":#{location.end_line - 1}-#{location.end_column}" assert_equal(expected_location, location_string) end def refute_entry(expected_name) - entries = @index.get_constant(expected_name) || @index.get_method(expected_name) - assert_nil(entries, "Expected #{expected_name} to not be indexed") + entry = @index.get_constant(expected_name) || @index.get_methods(expected_name)&.first + assert_nil(entry, "Expected #{expected_name} to not be indexed") end def assert_no_entries diff --git a/lib/ruby_lsp/listeners/completion.rb b/lib/ruby_lsp/listeners/completion.rb index b836e0cc30..6cfc490b96 100644 --- a/lib/ruby_lsp/listeners/completion.rb +++ b/lib/ruby_lsp/listeners/completion.rb @@ -42,13 +42,13 @@ def on_constant_read_node_enter(node) return if name.nil? candidates = @index.prefix_search_constants(name, @nesting) - candidates.each do |entries| - complete_name = T.must(entries.first).name + candidates.each do |entry| + complete_name = entry.name @response_builder << build_entry_completion( complete_name, name, node, - entries, + entry, top_level?(complete_name), ) end @@ -73,22 +73,21 @@ def on_constant_path_node_enter(node) # order to find which possible constants match the desired search *namespace, incomplete_name = name.split("::") aliased_namespace = T.must(namespace).join("::") - namespace_entries = @index.resolve_constant(aliased_namespace, @nesting) - return unless namespace_entries + namespace_entry = @index.resolve_constant(aliased_namespace, @nesting) + return unless namespace_entry - real_namespace = @index.follow_aliased_namespace(T.must(namespace_entries.first).name) + real_namespace = @index.follow_aliased_namespace(namespace_entry.name) candidates = @index.prefix_search_constants( "#{real_namespace}::#{incomplete_name}", top_level_reference ? [] : @nesting, ) - candidates.each do |entries| + candidates.each do |entry| # The only time we may have a private constant reference from outside of the namespace is if we're dealing # with ConstantPath and the entry name doesn't start with the current nesting - first_entry = T.must(entries.first) - next if first_entry.visibility == :private && !first_entry.name.start_with?("#{@nesting}::") + next if entry.visibility == :private && !entry.name.start_with?("#{@nesting}::") - constant_name = T.must(first_entry.name.split("::").last) + constant_name = T.must(entry.name.split("::").last) full_name = aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}" @@ -96,8 +95,8 @@ def on_constant_path_node_enter(node) full_name, name, node, - entries, - top_level_reference || top_level?(T.must(entries.first).name), + entry, + top_level_reference || top_level?(entry.name), ) end end @@ -166,16 +165,14 @@ def complete_require_relative(node) sig { params(node: Prism::CallNode, name: String).void } def complete_self_receiver_method(node, name) - receiver_entries = @index.get_constant(@nesting.join("::")) - return unless receiver_entries - - receiver = T.must(receiver_entries.first) + receiver = @index.get_constant(@nesting.join("::")) + return unless receiver @index.prefix_search_methods(name).each do |entries| - entry = entries.find { |e| e.is_a?(RubyIndexer::Entry::Member) && e.owner&.name == receiver.name } + entry = entries.find { |e| e.owner&.name == receiver.name } next unless entry - @response_builder << build_method_completion(T.cast(entry, RubyIndexer::Entry::Member), node) + @response_builder << build_method_completion(entry, node) end end @@ -187,6 +184,7 @@ def complete_self_receiver_method(node, name) end def build_method_completion(entry, node) name = entry.name + declarations = T.cast(entry.declarations, T::Array[RubyIndexer::Entry::MemberDeclaration]) Interface::CompletionItem.new( label: name, @@ -194,13 +192,16 @@ def build_method_completion(entry, node) text_edit: Interface::TextEdit.new(range: range_from_location(T.must(node.message_loc)), new_text: name), kind: Constant::CompletionItemKind::METHOD, label_details: Interface::CompletionItemLabelDetails.new( - detail: "(#{entry.parameters.map(&:decorated_name).join(", ")})", - description: entry.file_name, + detail: "(#{T.must(declarations.first).parameters.map(&:decorated_name).join(", ")})", + description: declarations.map(&:file_name).join(","), ), documentation: Interface::MarkupContent.new( kind: "markdown", value: markdown_from_index_entries(name, entry), ), + data: { + owner: entry.owner&.name, + }, ) end @@ -224,13 +225,12 @@ def build_completion(label, node) real_name: String, incomplete_name: String, node: Prism::Node, - entries: T::Array[RubyIndexer::Entry], + entry: RubyIndexer::Entry, top_level: T::Boolean, ).returns(Interface::CompletionItem) end - def build_entry_completion(real_name, incomplete_name, node, entries, top_level) - first_entry = T.must(entries.first) - kind = case first_entry + def build_entry_completion(real_name, incomplete_name, node, entry, top_level) + kind = case entry when RubyIndexer::Entry::Class Constant::CompletionItemKind::CLASS when RubyIndexer::Entry::Module diff --git a/lib/ruby_lsp/listeners/definition.rb b/lib/ruby_lsp/listeners/definition.rb index 153d6aaf65..0d7e15243f 100644 --- a/lib/ruby_lsp/listeners/definition.rb +++ b/lib/ruby_lsp/listeners/definition.rb @@ -70,27 +70,30 @@ def handle_method_definition(node) return unless message methods = if self_receiver?(node) - @index.resolve_method(message, @nesting.join("::")) + resolved_method = @index.resolve_method(message, @nesting.join("::")) + [resolved_method] if resolved_method else # If the method doesn't have a receiver, then we provide a few candidates to jump to # But we don't want to provide too many candidates, as it can be overwhelming - @index.get_method(message)&.take(MAX_NUMBER_OF_DEFINITION_CANDIDATES_WITHOUT_RECEIVER) + @index.get_methods(message)&.take(MAX_NUMBER_OF_DEFINITION_CANDIDATES_WITHOUT_RECEIVER) end return unless methods methods.each do |target_method| - location = target_method.location - file_path = target_method.file_path - next if @typechecker_enabled && not_in_dependencies?(file_path) + target_method.declarations.each do |declaration| + location = declaration.location + file_path = declaration.file_path + next if @typechecker_enabled && not_in_dependencies?(file_path) - @response_builder << Interface::Location.new( - uri: URI::Generic.from_path(path: file_path).to_s, - range: Interface::Range.new( - start: Interface::Position.new(line: location.start_line - 1, character: location.start_column), - end: Interface::Position.new(line: location.end_line - 1, character: location.end_column), - ), - ) + @response_builder << Interface::Location.new( + uri: URI::Generic.from_path(path: file_path).to_s, + range: Interface::Range.new( + start: Interface::Position.new(line: location.start_line - 1, character: location.start_column), + end: Interface::Position.new(line: location.end_line - 1, character: location.end_column), + ), + ) + end end end @@ -138,20 +141,19 @@ def handle_require_definition(node) sig { params(value: String).void } def find_in_index(value) - entries = @index.resolve_constant(value, @nesting) - return unless entries + entry = @index.resolve_constant(value, @nesting) + return unless entry # We should only allow jumping to the definition of private constants if the constant is defined in the same # namespace as the reference - first_entry = T.must(entries.first) - return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{value}" + return if entry.visibility == :private && entry.name != "#{@nesting.join("::")}::#{value}" - entries.each do |entry| - location = entry.location + entry.declarations.each do |declaration| + location = declaration.location # If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an # additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants # in the project, even if the files are typed false - file_path = entry.file_path + file_path = declaration.file_path next if @typechecker_enabled && not_in_dependencies?(file_path) @response_builder << Interface::Location.new( diff --git a/lib/ruby_lsp/listeners/hover.rb b/lib/ruby_lsp/listeners/hover.rb index 64149f8584..86c699d7c3 100644 --- a/lib/ruby_lsp/listeners/hover.rb +++ b/lib/ruby_lsp/listeners/hover.rb @@ -93,10 +93,10 @@ def on_call_node_enter(node) message = node.message return unless message - methods = @index.resolve_method(message, @nesting.join("::")) - return unless methods + target_method = @index.resolve_method(message, @nesting.join("::")) + return unless target_method - categorized_markdown_from_index_entries(message, methods).each do |category, content| + categorized_markdown_from_index_entries(message, target_method).each do |category, content| @response_builder.push(content, category: category) end end @@ -105,15 +105,14 @@ def on_call_node_enter(node) sig { params(name: String, location: Prism::Location).void } def generate_hover(name, location) - entries = @index.resolve_constant(name, @nesting) - return unless entries + entry = @index.resolve_constant(name, @nesting) + return unless entry # We should only show hover for private constants if the constant is defined in the same namespace as the # reference - first_entry = T.must(entries.first) - return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{name}" + return if entry.visibility == :private && entry.name != "#{@nesting.join("::")}::#{name}" - categorized_markdown_from_index_entries(name, entries).each do |category, content| + categorized_markdown_from_index_entries(name, entry).each do |category, content| @response_builder.push(content, category: category) end end diff --git a/lib/ruby_lsp/listeners/signature_help.rb b/lib/ruby_lsp/listeners/signature_help.rb index 413a79f024..695ab72d23 100644 --- a/lib/ruby_lsp/listeners/signature_help.rb +++ b/lib/ruby_lsp/listeners/signature_help.rb @@ -33,13 +33,12 @@ def on_call_node_enter(node) message = node.message return unless message - methods = @index.resolve_method(message, @nesting.join("::")) - return unless methods - - target_method = methods.first + target_method = @index.resolve_method(message, @nesting.join("::")) return unless target_method - parameters = target_method.parameters + declarations = T.cast(target_method.declarations, T::Array[RubyIndexer::Entry::MemberDeclaration]) + # TODO: this is currently only showing the first declaration parameters, but a method can be overridden + parameters = T.must(declarations.first).parameters name = target_method.name # If the method doesn't have any parameters, there's no need to show signature help @@ -65,7 +64,7 @@ def on_call_node_enter(node) parameters: parameters.map { |param| Interface::ParameterInformation.new(label: param.name) }, documentation: Interface::MarkupContent.new( kind: "markdown", - value: markdown_from_index_entries("", methods), + value: markdown_from_index_entries("", target_method), ), ), ], diff --git a/lib/ruby_lsp/requests/completion_resolve.rb b/lib/ruby_lsp/requests/completion_resolve.rb index 6b37802365..e1f5a53943 100644 --- a/lib/ruby_lsp/requests/completion_resolve.rb +++ b/lib/ruby_lsp/requests/completion_resolve.rb @@ -25,6 +25,15 @@ class CompletionResolve < Request extend T::Sig include Requests::Support::Common + CONSTANT_KINDS = T.let( + [ + Constant::CompletionItemKind::CLASS, + Constant::CompletionItemKind::MODULE, + Constant::CompletionItemKind::CONSTANT, + ].freeze, + T::Array[Integer], + ) + # set a limit on the number of documentation entries returned, to avoid rendering performance issues # https://github.com/Shopify/ruby-lsp/pull/1798 MAX_DOCUMENTATION_ENTRIES = 10 @@ -36,25 +45,50 @@ def initialize(global_state, item) @item = item end - sig { override.returns(Interface::CompletionItem) } + sig { override.returns(T.nilable(Interface::CompletionItem)) } def perform label = @item[:label] - entries = case @item[:kind] - when Constant::CompletionItemKind::CLASS, Constant::CompletionItemKind::MODULE, - Constant::CompletionItemKind::CONSTANT - @index.get_constant(label) || [] - else - @index.get_method(label) || [] + owner = @item.dig(:data, :owner) + + if CONSTANT_KINDS.include?(@item[:kind]) + constant_item_documentation(label, T.must(@index.get_constant(label))) + elsif owner + known_method_item_documentation(label, T.must(@index.resolve_method(label, owner))) end + end + + private + + sig { params(label: String, entry: RubyIndexer::Entry::Member).returns(Interface::CompletionItem) } + def known_method_item_documentation(label, entry) + declarations = T.cast( + entry.declarations.take(MAX_DOCUMENTATION_ENTRIES), + T::Array[RubyIndexer::Entry::MemberDeclaration], + ) Interface::CompletionItem.new( label: label, label_details: Interface::CompletionItemLabelDetails.new( - description: entries.take(MAX_DOCUMENTATION_ENTRIES).map(&:file_name).join(","), + detail: "(#{T.must(declarations.first).parameters.map(&:decorated_name).join(", ")})", + description: declarations.map(&:file_name).join(","), ), documentation: Interface::MarkupContent.new( kind: "markdown", - value: markdown_from_index_entries(label, entries, MAX_DOCUMENTATION_ENTRIES), + value: markdown_from_index_entries(label, entry, MAX_DOCUMENTATION_ENTRIES), + ), + ) + end + + sig { params(label: String, entry: RubyIndexer::Entry).returns(Interface::CompletionItem) } + def constant_item_documentation(label, entry) + file_names = entry.declarations.take(MAX_DOCUMENTATION_ENTRIES).map(&:file_name) + + Interface::CompletionItem.new( + label: label, + label_details: Interface::CompletionItemLabelDetails.new(description: file_names.join(",")), + documentation: Interface::MarkupContent.new( + kind: "markdown", + value: markdown_from_index_entries(label, entry, MAX_DOCUMENTATION_ENTRIES), ), ) end diff --git a/lib/ruby_lsp/requests/support/common.rb b/lib/ruby_lsp/requests/support/common.rb index 135bd469b4..1244294b40 100644 --- a/lib/ruby_lsp/requests/support/common.rb +++ b/lib/ruby_lsp/requests/support/common.rb @@ -85,17 +85,16 @@ def self_receiver?(node) sig do params( title: String, - entries: T.any(T::Array[RubyIndexer::Entry], RubyIndexer::Entry), + entry: RubyIndexer::Entry, max_entries: T.nilable(Integer), ).returns(T::Hash[Symbol, String]) end - def categorized_markdown_from_index_entries(title, entries, max_entries = nil) + def categorized_markdown_from_index_entries(title, entry, max_entries = nil) markdown_title = "```ruby\n#{title}\n```" definitions = [] content = +"" - entries = Array(entries) - entries_to_format = max_entries ? entries.take(max_entries) : entries - entries_to_format.each do |entry| + declarations = max_entries ? entry.declarations.take(max_entries) : entry.declarations + declarations.each do |entry| loc = entry.location # We always handle locations as zero based. However, for file links in Markdown we need them to be one @@ -111,8 +110,8 @@ def categorized_markdown_from_index_entries(title, entries, max_entries = nil) content << "\n\n#{entry.comments.join("\n")}" unless entry.comments.empty? end - additional_entries_text = if max_entries && entries.length > max_entries - additional = entries.length - max_entries + additional_entries_text = if max_entries && declarations.length > max_entries + additional = declarations.length - max_entries " | #{additional} other#{additional > 1 ? "s" : ""}" else "" @@ -125,15 +124,9 @@ def categorized_markdown_from_index_entries(title, entries, max_entries = nil) } end - sig do - params( - title: String, - entries: T.any(T::Array[RubyIndexer::Entry], RubyIndexer::Entry), - max_entries: T.nilable(Integer), - ).returns(String) - end - def markdown_from_index_entries(title, entries, max_entries = nil) - categorized_markdown = categorized_markdown_from_index_entries(title, entries, max_entries) + sig { params(title: String, entry: RubyIndexer::Entry, max_entries: T.nilable(Integer)).returns(String) } + def markdown_from_index_entries(title, entry, max_entries = nil) + categorized_markdown = categorized_markdown_from_index_entries(title, entry, max_entries) <<~MARKDOWN.chomp #{categorized_markdown[:title]} diff --git a/lib/ruby_lsp/requests/workspace_symbol.rb b/lib/ruby_lsp/requests/workspace_symbol.rb index b3217a8df8..c5a6f38746 100644 --- a/lib/ruby_lsp/requests/workspace_symbol.rb +++ b/lib/ruby_lsp/requests/workspace_symbol.rb @@ -32,35 +32,37 @@ def initialize(global_state, query) sig { override.returns(T::Array[Interface::WorkspaceSymbol]) } def perform - @index.fuzzy_search(@query).filter_map do |entry| - # If the project is using Sorbet, we let Sorbet handle symbols defined inside the project itself and RBIs, but - # we still return entries defined in gems to allow developers to jump directly to the source - file_path = entry.file_path - next if @global_state.typechecker && not_in_dependencies?(file_path) + @index.fuzzy_search(@query).flat_map do |entry| + entry.declarations.filter_map do |declaration| + # If the project is using Sorbet, we let Sorbet handle symbols defined inside the project itself and RBIs, + # but we still return entries defined in gems to allow developers to jump directly to the source + file_path = declaration.file_path + next if @global_state.typechecker && not_in_dependencies?(file_path) - # We should never show private symbols when searching the entire workspace - next if entry.visibility == :private + # We should never show private symbols when searching the entire workspace + next if entry.visibility == :private - kind = kind_for_entry(entry) - loc = entry.location + kind = kind_for_entry(entry) + loc = declaration.location - # We use the namespace as the container name, but we also use the full name as the regular name. The reason we - # do this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included the - # short name `Bar`, then searching for `Foo::Bar` would not return any results - *container, _short_name = entry.name.split("::") + # We use the namespace as the container name, but we also use the full name as the regular name. The reason + # we do this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included + # the short name `Bar`, then searching for `Foo::Bar` would not return any results + *container, _short_name = entry.name.split("::") - Interface::WorkspaceSymbol.new( - name: entry.name, - container_name: T.must(container).join("::"), - kind: kind, - location: Interface::Location.new( - uri: URI::Generic.from_path(path: file_path).to_s, - range: Interface::Range.new( - start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column), - end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column), + Interface::WorkspaceSymbol.new( + name: entry.name, + container_name: T.must(container).join("::"), + kind: kind, + location: Interface::Location.new( + uri: URI::Generic.from_path(path: file_path).to_s, + range: Interface::Range.new( + start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column), + end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column), + ), ), - ), - ) + ) + end end end diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index 6c53c48255..3d13a10b04 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -554,10 +554,16 @@ def text_document_completion(message) sig { params(message: T::Hash[Symbol, T.untyped]).void } def text_document_completion_item_resolve(message) - send_message(Result.new( - id: message[:id], - response: Requests::CompletionResolve.new(@global_state, message[:params]).perform, - )) + response = Requests::CompletionResolve.new(@global_state, message[:params]).perform + send_message(Result.new(id: message[:id], response: response)) + + unless response + send_message( + Notification.log_trace( + "Tried to resolve a completion item for an unknown receiver. This shouldn't happen", + ), + ) + end end sig { params(message: T::Hash[Symbol, T.untyped]).void } diff --git a/lib/ruby_lsp/utils.rb b/lib/ruby_lsp/utils.rb index 8fb6758f15..b59512f474 100644 --- a/lib/ruby_lsp/utils.rb +++ b/lib/ruby_lsp/utils.rb @@ -52,6 +52,7 @@ def to_hash; end class Notification < Message class << self extend T::Sig + sig { params(message: String).returns(Notification) } def window_show_error(message) new( @@ -62,6 +63,17 @@ def window_show_error(message) ), ) end + + sig { params(message: String, verbose: T.nilable(String)).returns(Notification) } + def log_trace(message, verbose = nil) + new( + method: "$/logTrace", + params: Interface::LogTraceParams.new( + message: message, + verbose: verbose, + ), + ) + end end extend T::Sig