Skip to content

Commit 87d97d7

Browse files
committed
Migrate type hierarchy to use Rubydex
1 parent d1435ac commit 87d97d7

9 files changed

Lines changed: 837 additions & 149 deletions

lib/ruby_lsp/internal.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
require "set"
3333

3434
# Rubydex LSP additions
35+
require "ruby_lsp/rubydex/declaration"
3536
require "ruby_lsp/rubydex/definition"
3637
require "ruby_lsp/rubydex/reference"
3738

lib/ruby_lsp/requests/prepare_type_hierarchy.rb

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ module RubyLsp
55
module Requests
66
# The [prepare type hierarchy
77
# request](https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareTypeHierarchy)
8-
# displays the list of ancestors (supertypes) and descendants (subtypes) for the selected type.
9-
#
10-
# Currently only supports supertypes due to a limitation of the index.
8+
# displays the list of direct ancestors (supertypes) and descendants (subtypes) for the selected type.
119
class PrepareTypeHierarchy < Request
1210
include Support::Common
1311

@@ -18,12 +16,12 @@ def provider
1816
end
1917
end
2018

21-
#: ((RubyDocument | ERBDocument) document, RubyIndexer::Index index, Hash[Symbol, untyped] position) -> void
22-
def initialize(document, index, position)
19+
#: ((RubyDocument | ERBDocument) document, GlobalState global_state, Hash[Symbol, untyped] position) -> void
20+
def initialize(document, global_state, position)
2321
super()
2422

2523
@document = document
26-
@index = index
24+
@graph = global_state.graph #: Rubydex::Graph
2725
@position = position
2826
end
2927

@@ -36,32 +34,78 @@ def perform
3634
Prism::ConstantReadNode,
3735
Prism::ConstantWriteNode,
3836
Prism::ConstantPathNode,
37+
Prism::SingletonClassNode,
3938
],
4039
)
4140

42-
node = context.node
43-
parent = context.parent
44-
return unless node && parent
41+
node = context.node #: as (Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantWriteNode | Prism::SingletonClassNode)?
42+
return unless node
43+
44+
pair = name_and_nesting(node, context)
45+
return unless pair
4546

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

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

5553
[
56-
Interface::TypeHierarchyItem.new(
57-
name: first_entry.name,
58-
kind: kind_for_entry(first_entry),
59-
uri: first_entry.uri.to_s,
60-
range: range,
61-
selection_range: range,
54+
primary.to_lsp_type_hierarchy_item(
55+
declaration.name,
56+
detail: declaration.lsp_type_hierarchy_detail,
6257
),
6358
]
6459
end
60+
61+
private
62+
63+
# Returns the `(name, nesting)` pair to pass to `Rubydex::Graph#resolve_constant`, covering three cases:
64+
#
65+
#: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantWriteNode | Prism::SingletonClassNode), NodeContext) -> [String, Array[String]]?
66+
def name_and_nesting(node, context)
67+
parent = context.parent
68+
nesting = context.nesting
69+
70+
singleton_node = singleton_class_node_for(node, parent)
71+
return singleton_lookup(singleton_node, nesting) if singleton_node
72+
73+
target = parent ? determine_target(node, parent, @position) : node
74+
[target.slice, nesting]
75+
end
76+
77+
# Ensures that we're returning the target of the singleton class block regardless of whether the cursor is on the
78+
# `class` keyword or the constant reference for the target
79+
#: ((Prism::ConstantReadNode | Prism::ConstantPathNode | Prism::ConstantWriteNode | Prism::SingletonClassNode), Prism::Node?) -> Prism::SingletonClassNode?
80+
def singleton_class_node_for(node, parent)
81+
return node if node.is_a?(Prism::SingletonClassNode)
82+
return unless parent.is_a?(Prism::SingletonClassNode) && parent.expression == node
83+
84+
parent
85+
end
86+
87+
# Builds the synthesized singleton class name (e.g. `Foo::<Foo>`) for a `class << X` block, together with the
88+
# outer lexical nesting. `NodeContext` already appends a `<ClassName>` marker as the last element of the nesting
89+
# whenever the cursor sits inside (or on) a `SingletonClassNode`, so we drop that marker to obtain the scope in
90+
# which the singleton should be resolved.
91+
#: (Prism::SingletonClassNode, Array[String]) -> [String, Array[String]]?
92+
def singleton_lookup(singleton_node, nesting)
93+
outer = nesting[0...-1] || []
94+
95+
case expression = singleton_node.expression
96+
when Prism::SelfNode
97+
name = nesting.last
98+
return unless name
99+
100+
[name, outer]
101+
when Prism::ConstantReadNode, Prism::ConstantPathNode
102+
name = constant_name(expression)
103+
return unless name
104+
105+
unqualified = name.split("::").last #: as !nil
106+
["#{name}::<#{unqualified}>", outer]
107+
end
108+
end
65109
end
66110
end
67111
end

lib/ruby_lsp/requests/type_hierarchy_supertypes.rb

Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,65 +9,101 @@ module Requests
99
class TypeHierarchySupertypes < Request
1010
include Support::Common
1111

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

16-
@index = index
16+
@graph = global_state.graph #: Rubydex::Graph
1717
@item = item
1818
end
1919

2020
# @override
2121
#: -> Array[Interface::TypeHierarchyItem]?
2222
def perform
23-
name = @item[:name]
24-
entries = @index[name]
25-
26-
parents = Set.new #: Set[RubyIndexer::Entry::Namespace]
27-
return unless entries&.any?
28-
29-
entries.each do |entry|
30-
next unless entry.is_a?(RubyIndexer::Entry::Namespace)
31-
32-
if entry.is_a?(RubyIndexer::Entry::Class)
33-
parent_class_name = entry.parent_class
34-
if parent_class_name
35-
resolved_parent_entries = @index.resolve(parent_class_name, entry.nesting)
36-
resolved_parent_entries&.each do |entry|
37-
next unless entry.is_a?(RubyIndexer::Entry::Class)
38-
39-
parents << entry
40-
end
41-
end
42-
end
43-
44-
entry.mixin_operations.each do |mixin_operation|
45-
mixin_name = mixin_operation.module_name
46-
resolved_mixin_entries = @index.resolve(mixin_name, entry.nesting)
47-
next unless resolved_mixin_entries
48-
49-
resolved_mixin_entries.each do |mixin_entry|
50-
next unless mixin_entry.is_a?(RubyIndexer::Entry::Module)
51-
52-
parents << mixin_entry
53-
end
54-
end
55-
end
23+
fully_qualified_name = @item.dig(:data, :fully_qualified_name) || @item[:name] #: String?
24+
return unless fully_qualified_name
25+
26+
declaration = @graph[fully_qualified_name]
27+
return unless declaration.is_a?(Rubydex::Namespace)
5628

57-
parents.map { |entry| hierarchy_item(entry) }
29+
compute_supertypes(declaration).filter_map { |name, backing| hierarchy_item(name, backing) }
5830
end
5931

6032
private
6133

62-
#: (RubyIndexer::Entry entry) -> Interface::TypeHierarchyItem
63-
def hierarchy_item(entry)
64-
Interface::TypeHierarchyItem.new(
65-
name: entry.name,
66-
kind: kind_for_entry(entry),
67-
uri: entry.uri.to_s,
68-
range: range_from_location(entry.location),
69-
selection_range: range_from_location(entry.name_location),
70-
detail: entry.file_name,
34+
# Returns an array of `[display_name, backing_declaration]` pairs. `display_name` is the name shown in the type
35+
# hierarchy item (which may be a synthesized singleton class name like `Object::<Object>`). `backing_declaration`
36+
# is the namespace whose primary definition provides the location for the hierarchy item — it may differ from the
37+
# display name when the singleton class is implicit and has no definitions of its own, in which case we fall back
38+
# to the attached object's definition so the user still lands somewhere useful.
39+
#
40+
#: (Rubydex::Namespace) -> Array[[String, Rubydex::Namespace]]
41+
def compute_supertypes(declaration)
42+
case declaration
43+
when Rubydex::SingletonClass
44+
singleton_supertypes(declaration)
45+
when Rubydex::Class
46+
class_supertypes(declaration)
47+
else
48+
explicit_supertypes(declaration)
49+
end
50+
end
51+
52+
#: (Rubydex::Class) -> Array[[String, Rubydex::Namespace]]
53+
def class_supertypes(declaration)
54+
# `BasicObject` is the root of the Ruby class hierarchy
55+
supertypes = explicit_supertypes(declaration)
56+
return supertypes if declaration.name == "BasicObject"
57+
58+
# If the class has any superclass reference (resolved or unresolved), don't re-add the implicit `Object`.
59+
has_superclass = declaration.definitions.any? do |d|
60+
d.is_a?(Rubydex::ClassDefinition) && !d.superclass.nil?
61+
end
62+
return supertypes if has_superclass
63+
64+
object = @graph["Object"] #: as Rubydex::Namespace
65+
supertypes << ["Object", object]
66+
supertypes
67+
end
68+
69+
#: (Rubydex::Namespace) -> Array[[String, Rubydex::Namespace]]
70+
def explicit_supertypes(declaration)
71+
declaration.direct_supertypes.map { |s| [s.name, s] }
72+
end
73+
74+
# Singleton classes don't have their own superclass references. Their direct supertype is the singleton class of
75+
# the attached object's superclass, computed recursively so that nested singleton classes (e.g.
76+
# `Foo::<Foo>::<<Foo>>`) still resolve to the matching depth on the parent chain. When the synthesized singleton
77+
# class name has no backing declaration with definitions (implicit singleton), we fall back to the attached
78+
# supertype's backing so the user is still navigated to a meaningful location.
79+
#
80+
#: (Rubydex::SingletonClass) -> Array[[String, Rubydex::Namespace]]
81+
def singleton_supertypes(declaration)
82+
attached = declaration.owner
83+
return [] unless attached.is_a?(Rubydex::Namespace)
84+
85+
compute_supertypes(attached).map do |parent_name, parent_backing|
86+
singleton_name = singleton_name_of(parent_name)
87+
found = @graph[singleton_name]
88+
backing = found.is_a?(Rubydex::Namespace) && found.definitions.any? ? found : parent_backing
89+
[singleton_name, backing]
90+
end
91+
end
92+
93+
#: (String) -> String
94+
def singleton_name_of(name)
95+
unqualified = name.split("::").last || name
96+
"#{name}::<#{unqualified}>"
97+
end
98+
99+
#: (String, Rubydex::Namespace) -> Interface::TypeHierarchyItem?
100+
def hierarchy_item(name, declaration)
101+
primary = declaration.definitions.first #: Rubydex::Definition?
102+
return unless primary
103+
104+
primary.to_lsp_type_hierarchy_item(
105+
name,
106+
detail: declaration.lsp_type_hierarchy_detail,
71107
)
72108
end
73109
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module Rubydex
5+
class Declaration
6+
# Detail text shown on a `TypeHierarchyItem` for this declaration. Hints at multiplicity
7+
# when the declaration spans more than one re-open; otherwise falls back to the primary
8+
# definition's file name so users can quickly see where the type comes from.
9+
#
10+
#: () -> String?
11+
def lsp_type_hierarchy_detail
12+
defs = definitions
13+
count = defs.count
14+
return "#{count} definitions" if count > 1
15+
16+
primary = defs.first
17+
return unless primary
18+
19+
uri = URI(primary.location.uri)
20+
path = uri.full_path
21+
path ? File.basename(path) : uri.to_s
22+
end
23+
end
24+
25+
class Namespace
26+
# Resolved, deduplicated direct supertypes across every re-open of this declaration.
27+
# Aggregates each definition's own `superclass`/`include`/`prepend` references and drops
28+
# unresolved ones. Order is stable (first-seen across definitions).
29+
#: () -> Array[Rubydex::Namespace]
30+
def direct_supertypes
31+
seen = {} #: Hash[String, Rubydex::Namespace]
32+
33+
definitions.each do |definition|
34+
definition.direct_supertype_references.each do |ref|
35+
next unless ref.is_a?(ResolvedConstantReference)
36+
37+
target = ref.declaration
38+
next unless target.is_a?(Namespace)
39+
next if seen.key?(target.name)
40+
41+
seen[target.name] = target
42+
end
43+
end
44+
45+
seen.values
46+
end
47+
end
48+
end

0 commit comments

Comments
 (0)