lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.18.1 vs lib/ruby_indexer/lib/ruby_indexer/index.rb in ruby-lsp-0.18.2

- old
+ new

@@ -240,10 +240,68 @@ end completion_items.values.map!(&:first) end + sig do + params( + name: String, + nesting: T::Array[String], + ).returns(T::Array[T::Array[T.any( + Entry::Constant, + Entry::ConstantAlias, + Entry::Namespace, + Entry::UnresolvedConstantAlias, + )]]) + end + def constant_completion_candidates(name, nesting) + # If we have a top level reference, then we don't need to include completions inside the current nesting + if name.start_with?("::") + return T.cast( + @entries_tree.search(name.delete_prefix("::")), + T::Array[T::Array[T.any( + Entry::Constant, + Entry::ConstantAlias, + Entry::Namespace, + Entry::UnresolvedConstantAlias, + )]], + ) + end + + # Otherwise, we have to include every possible constant the user might be referring to. This is essentially the + # same algorithm as resolve, but instead of returning early we concatenate all unique results + + # Direct constants inside this namespace + entries = @entries_tree.search(nesting.any? ? "#{nesting.join("::")}::#{name}" : name) + + # Constants defined in enclosing scopes + nesting.length.downto(1) do |i| + namespace = T.must(nesting[0...i]).join("::") + entries.concat(@entries_tree.search("#{namespace}::#{name}")) + end + + # Inherited constants + if name.end_with?("::") + entries.concat(inherited_constant_completion_candidates(nil, nesting + [name])) + else + entries.concat(inherited_constant_completion_candidates(name, nesting)) + end + + # Top level constants + entries.concat(@entries_tree.search(name)) + entries.uniq! + T.cast( + entries, + T::Array[T::Array[T.any( + Entry::Constant, + Entry::ConstantAlias, + Entry::Namespace, + Entry::UnresolvedConstantAlias, + )]], + ) + end + # Resolve a constant to its declaration based on its name and the nesting where the reference was found. Parameter # documentation: # # name: the name of the reference how it was found in the source code (qualified or not) # nesting: the nesting structure where the reference was found (e.g.: ["Foo", "Bar"]) @@ -363,16 +421,14 @@ # If we find an alias, then we want to follow its target. In the same example, if `Foo::Bar` is an alias to # `Something::Else`, then we first discover `Something::Else::Baz`. But `Something::Else::Baz` might contain other # aliases, so we have to invoke `follow_aliased_namespace` again to check until we only return a real name sig { params(name: String, seen_names: T::Array[String]).returns(String) } def follow_aliased_namespace(name, seen_names = []) - return name if @entries[name] - parts = name.split("::") real_parts = [] - (parts.length - 1).downto(0).each do |i| + (parts.length - 1).downto(0) do |i| current_name = T.must(parts[0..i]).join("::") entry = @entries[current_name]&.first case entry when Entry::ConstantAlias @@ -822,11 +878,11 @@ Entry::ConstantAlias, Entry::UnresolvedConstantAlias, )])) end def lookup_enclosing_scopes(name, nesting, seen_names) - nesting.length.downto(1).each do |i| + nesting.length.downto(1) do |i| namespace = T.must(nesting[0...i]).join("::") # If we find an entry with `full_name` directly, then we can already return it, even if it contains aliases - # because the user might be trying to jump to the alias definition. # @@ -867,9 +923,54 @@ end nil rescue NonExistingNamespaceError nil + end + + sig do + params( + name: T.nilable(String), + nesting: T::Array[String], + ).returns(T::Array[T::Array[T.any( + Entry::Namespace, + Entry::ConstantAlias, + Entry::UnresolvedConstantAlias, + Entry::Constant, + )]]) + end + def inherited_constant_completion_candidates(name, nesting) + namespace_entries = if name + *nesting_parts, constant_name = build_non_redundant_full_name(name, nesting).split("::") + return [] if nesting_parts.empty? + + resolve(nesting_parts.join("::"), []) + else + resolve(nesting.join("::"), []) + end + return [] unless namespace_entries + + ancestors = linearized_ancestors_of(T.must(namespace_entries.first).name) + candidates = ancestors.flat_map do |ancestor_name| + @entries_tree.search("#{ancestor_name}::#{constant_name}") + end + + # For candidates with the same name, we must only show the first entry in the inheritance chain, since that's the + # one the user will be referring to in completion + completion_items = candidates.each_with_object({}) do |entries, hash| + *parts, short_name = T.must(entries.first).name.split("::") + namespace_name = parts.join("::") + ancestor_index = ancestors.index(namespace_name) + existing_entry, existing_entry_index = hash[short_name] + + next unless ancestor_index && (!existing_entry || ancestor_index < existing_entry_index) + + hash[short_name] = [entries, ancestor_index] + end + + completion_items.values.map!(&:first) + rescue NonExistingNamespaceError + [] end # Removes redudancy from a constant reference's full name. For example, if we find a reference to `A::B::Foo` inside # of the ["A", "B"] nesting, then we should not concatenate the nesting with the name or else we'll end up with # `A::B::A::B::Foo`. This method will remove any redundant parts from the final name based on the reference and the