Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ruby_lsp/internal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
require "set"

# Rubydex LSP additions
require "ruby_lsp/rubydex/declaration"
require "ruby_lsp/rubydex/definition"
require "ruby_lsp/rubydex/reference"

Expand Down
88 changes: 66 additions & 22 deletions lib/ruby_lsp/requests/prepare_type_hierarchy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ module RubyLsp
module Requests
# The [prepare type hierarchy
# request](https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareTypeHierarchy)
# displays the list of ancestors (supertypes) and descendants (subtypes) for the selected type.
#
# Currently only supports supertypes due to a limitation of the index.
# displays the list of direct ancestors (supertypes) and descendants (subtypes) for the selected type.
class PrepareTypeHierarchy < Request
include Support::Common

Expand All @@ -18,12 +16,12 @@ def provider
end
end

#: ((RubyDocument | ERBDocument) document, RubyIndexer::Index index, Hash[Symbol, untyped] position) -> void
def initialize(document, index, position)
#: ((RubyDocument | ERBDocument) document, GlobalState global_state, Hash[Symbol, untyped] position) -> void
def initialize(document, global_state, position)
super()

@document = document
@index = index
@graph = global_state.graph #: Rubydex::Graph
@position = position
end

Expand All @@ -36,32 +34,78 @@ def perform
Prism::ConstantReadNode,
Prism::ConstantWriteNode,
Prism::ConstantPathNode,
Prism::SingletonClassNode,
],
)

node = context.node
parent = context.parent
return unless node && parent
node = context.node #: as (Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantWriteNode | Prism::SingletonClassNode)?
return unless node

pair = name_and_nesting(node, context)
return unless pair

target = determine_target(node, parent, @position)
entries = @index.resolve(target.slice, context.nesting)
return unless entries
declaration = @graph.resolve_constant(pair.first, pair.last)
return unless declaration.is_a?(Rubydex::Namespace)

# While the spec allows for multiple entries, VSCode seems to only support one
# We'll just return the first one for now
first_entry = entries.first #: as !nil
range = range_from_location(first_entry.location)
primary = declaration.definitions.first
return unless primary

[
Interface::TypeHierarchyItem.new(
name: first_entry.name,
kind: kind_for_entry(first_entry),
uri: first_entry.uri.to_s,
range: range,
selection_range: range,
primary.to_lsp_type_hierarchy_item(
declaration.name,
detail: declaration.lsp_type_hierarchy_detail,
),
]
end

private

# Returns the `(name, nesting)` pair to pass to `Rubydex::Graph#resolve_constant`, covering three cases:
#
#: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantWriteNode | Prism::SingletonClassNode), NodeContext) -> [String, Array[String]]?
def name_and_nesting(node, context)
parent = context.parent
nesting = context.nesting

singleton_node = singleton_class_node_for(node, parent)
return singleton_lookup(singleton_node, nesting) if singleton_node

target = parent ? determine_target(node, parent, @position) : node
[target.slice, nesting]
end

# Ensures that we're returning the target of the singleton class block regardless of whether the cursor is on the
# `class` keyword or the constant reference for the target
#: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantWriteNode | Prism::SingletonClassNode), Prism::Node?) -> Prism::SingletonClassNode?
def singleton_class_node_for(node, parent)
return node if node.is_a?(Prism::SingletonClassNode)
return unless parent.is_a?(Prism::SingletonClassNode) && parent.expression == node

parent
end

# Builds the synthesized singleton class name (e.g. `Foo::<Foo>`) for a `class << X` block, together with the
# outer lexical nesting. `NodeContext` already appends a `<ClassName>` marker as the last element of the nesting
# whenever the cursor sits inside (or on) a `SingletonClassNode`, so we drop that marker to obtain the scope in
# which the singleton should be resolved.
#: (Prism::SingletonClassNode, Array[String]) -> [String, Array[String]]?
def singleton_lookup(singleton_node, nesting)
outer = nesting[0...-1] || []

case expression = singleton_node.expression
when Prism::SelfNode
name = nesting.last
return unless name

[name, outer]
when Prism::ConstantReadNode, Prism::ConstantPathNode
name = constant_name(expression)
return unless name

unqualified = name.split("::").last #: as !nil
["#{name}::<#{unqualified}>", outer]
end
end
end
end
end
128 changes: 82 additions & 46 deletions lib/ruby_lsp/requests/type_hierarchy_supertypes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,65 +9,101 @@ module Requests
class TypeHierarchySupertypes < Request
include Support::Common

#: (RubyIndexer::Index index, Hash[Symbol, untyped] item) -> void
def initialize(index, item)
#: (GlobalState, Hash[Symbol, untyped]) -> void
def initialize(global_state, item)
super()

@index = index
@graph = global_state.graph #: Rubydex::Graph
@item = item
end

# @override
#: -> Array[Interface::TypeHierarchyItem]?
def perform
name = @item[:name]
entries = @index[name]

parents = Set.new #: Set[RubyIndexer::Entry::Namespace]
return unless entries&.any?

entries.each do |entry|
next unless entry.is_a?(RubyIndexer::Entry::Namespace)

if entry.is_a?(RubyIndexer::Entry::Class)
parent_class_name = entry.parent_class
if parent_class_name
resolved_parent_entries = @index.resolve(parent_class_name, entry.nesting)
resolved_parent_entries&.each do |entry|
next unless entry.is_a?(RubyIndexer::Entry::Class)

parents << entry
end
end
end

entry.mixin_operations.each do |mixin_operation|
mixin_name = mixin_operation.module_name
resolved_mixin_entries = @index.resolve(mixin_name, entry.nesting)
next unless resolved_mixin_entries

resolved_mixin_entries.each do |mixin_entry|
next unless mixin_entry.is_a?(RubyIndexer::Entry::Module)

parents << mixin_entry
end
end
end
fully_qualified_name = @item.dig(:data, :fully_qualified_name) || @item[:name] #: String?
return unless fully_qualified_name

declaration = @graph[fully_qualified_name]
return unless declaration.is_a?(Rubydex::Namespace)

parents.map { |entry| hierarchy_item(entry) }
compute_supertypes(declaration).filter_map { |name, backing| hierarchy_item(name, backing) }
end

private

#: (RubyIndexer::Entry entry) -> Interface::TypeHierarchyItem
def hierarchy_item(entry)
Interface::TypeHierarchyItem.new(
name: entry.name,
kind: kind_for_entry(entry),
uri: entry.uri.to_s,
range: range_from_location(entry.location),
selection_range: range_from_location(entry.name_location),
detail: entry.file_name,
# Returns an array of `[display_name, backing_declaration]` pairs. `display_name` is the name shown in the type
# hierarchy item (which may be a synthesized singleton class name like `Object::<Object>`). `backing_declaration`
# is the namespace whose primary definition provides the location for the hierarchy item — it may differ from the
# display name when the singleton class is implicit and has no definitions of its own, in which case we fall back
# to the attached object's definition so the user still lands somewhere useful.
#
#: (Rubydex::Namespace) -> Array[[String, Rubydex::Namespace]]
def compute_supertypes(declaration)
case declaration
when Rubydex::SingletonClass
singleton_supertypes(declaration)
when Rubydex::Class
class_supertypes(declaration)
else
explicit_supertypes(declaration)
end
end

#: (Rubydex::Class) -> Array[[String, Rubydex::Namespace]]
def class_supertypes(declaration)
# `BasicObject` is the root of the Ruby class hierarchy
supertypes = explicit_supertypes(declaration)
return supertypes if declaration.name == "BasicObject"

# If the class has any superclass reference (resolved or unresolved), don't re-add the implicit `Object`.
has_superclass = declaration.definitions.any? do |d|
d.is_a?(Rubydex::ClassDefinition) && !d.superclass.nil?
end
return supertypes if has_superclass

object = @graph["Object"] #: as Rubydex::Namespace
supertypes << ["Object", object]
supertypes
end

#: (Rubydex::Namespace) -> Array[[String, Rubydex::Namespace]]
def explicit_supertypes(declaration)
declaration.direct_supertypes.map { |s| [s.name, s] }
end

# Singleton classes don't have their own superclass references. Their direct supertype is the singleton class of
# the attached object's superclass, computed recursively so that nested singleton classes (e.g.
# `Foo::<Foo>::<<Foo>>`) still resolve to the matching depth on the parent chain. When the synthesized singleton
# class name has no backing declaration with definitions (implicit singleton), we fall back to the attached
# supertype's backing so the user is still navigated to a meaningful location.
#
#: (Rubydex::SingletonClass) -> Array[[String, Rubydex::Namespace]]
def singleton_supertypes(declaration)
attached = declaration.owner
return [] unless attached.is_a?(Rubydex::Namespace)

compute_supertypes(attached).map do |parent_name, parent_backing|
singleton_name = singleton_name_of(parent_name)
found = @graph[singleton_name]
backing = found.is_a?(Rubydex::Namespace) && found.definitions.any? ? found : parent_backing
[singleton_name, backing]
end
end

#: (String) -> String
def singleton_name_of(name)
unqualified = name.split("::").last || name
"#{name}::<#{unqualified}>"
end

#: (String, Rubydex::Namespace) -> Interface::TypeHierarchyItem?
def hierarchy_item(name, declaration)
primary = declaration.definitions.first #: Rubydex::Definition?
return unless primary

primary.to_lsp_type_hierarchy_item(
name,
detail: declaration.lsp_type_hierarchy_detail,
)
end
end
Expand Down
48 changes: 48 additions & 0 deletions lib/ruby_lsp/rubydex/declaration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# typed: strict
# frozen_string_literal: true

module Rubydex
class Declaration
# Detail text shown on a `TypeHierarchyItem` for this declaration. Hints at multiplicity
# when the declaration spans more than one re-open; otherwise falls back to the primary
# definition's file name so users can quickly see where the type comes from.
#
#: () -> String?
def lsp_type_hierarchy_detail
defs = definitions
count = defs.count
return "#{count} definitions" if count > 1

primary = defs.first
return unless primary

uri = URI(primary.location.uri)
path = uri.full_path
path ? File.basename(path) : uri.to_s
end
end

class Namespace
# Resolved, deduplicated direct supertypes across every re-open of this declaration.
# Aggregates each definition's own `superclass`/`include`/`prepend` references and drops
# unresolved ones. Order is stable (first-seen across definitions).
#: () -> Array[Rubydex::Namespace]
def direct_supertypes
seen = {} #: Hash[String, Rubydex::Namespace]

definitions.each do |definition|
definition.direct_supertype_references.each do |ref|
next unless ref.is_a?(ResolvedConstantReference)

target = ref.declaration
next unless target.is_a?(Namespace)
next if seen.key?(target.name)

seen[target.name] = target
end
end

seen.values
end
end
end
Loading
Loading