Skip to content

Commit a09ac2d

Browse files
committed
Add hover support for :through associations
1 parent afa840e commit a09ac2d

File tree

2 files changed

+126
-8
lines changed

2 files changed

+126
-8
lines changed

lib/ruby_lsp/ruby_lsp_rails/hover.rb

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,26 +118,44 @@ def format_default(default_value, type)
118118

119119
#: (Prism::SymbolNode node) -> void
120120
def handle_possible_dsl(node)
121-
node = @node_context.call_node
122-
return unless node
123-
return unless self_receiver?(node)
121+
call_node = @node_context.call_node
122+
return unless call_node
123+
return unless self_receiver?(call_node)
124124

125-
message = node.message
125+
message = call_node.message
126126

127127
return unless message
128128

129129
if Support::Associations::ALL.include?(message)
130-
handle_association(node)
130+
handle_association(node, call_node)
131131
end
132132
end
133133

134-
#: (Prism::CallNode node) -> void
135-
def handle_association(node)
136-
first_argument = node.arguments&.arguments&.first
134+
#: ((Prism::SymbolNode | Prism::StringNode) node, Prism::CallNode call_node) -> void
135+
def handle_association(node, call_node)
136+
arguments = call_node.arguments&.arguments
137+
return unless arguments
138+
139+
first_argument = arguments.first
137140
return unless first_argument.is_a?(Prism::SymbolNode)
138141

139142
association_name = first_argument.unescaped
140143

144+
through_element = arguments
145+
.filter_map { |arg| arg.elements if arg.is_a?(Prism::KeywordHashNode) }
146+
.flatten
147+
.find { |elem| elem.key.value == "through" }
148+
149+
if node == first_argument
150+
handle_association_name(association_name)
151+
elsif through_element && node == through_element.value
152+
through_association_name = through_element.value.unescaped
153+
handle_association_name(through_association_name)
154+
end
155+
end
156+
157+
#: (String association_name) -> void
158+
def handle_association_name(association_name)
141159
result = @client.association_target(
142160
model_name: @nesting.join("::"),
143161
association_name: association_name,

test/ruby_lsp_rails/hover_test.rb

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,106 @@ class Bar < ApplicationRecord
321321
CONTENT
322322
end
323323

324+
test "returns main association on has_many :through association" do
325+
expected_response = {
326+
location: "#{dummy_root}/app/models/user.rb:2",
327+
name: "User",
328+
}
329+
RunnerClient.any_instance.stubs(association_target: expected_response)
330+
331+
response = hover_on_source(<<~RUBY, { line: 4, character: 12 })
332+
# typed: false
333+
334+
class Organization < ActiveRecord::Base
335+
has_many :memberships
336+
has_many :users, through: :memberships
337+
end
338+
RUBY
339+
340+
assert_equal(<<~CONTENT.chomp, response.contents.value)
341+
```ruby
342+
User
343+
```
344+
345+
**Definitions**: [fake.rb](file:///fake.rb#L2,1-2,4)
346+
CONTENT
347+
end
348+
349+
test "returns main association on has_one :through association" do
350+
expected_response = {
351+
location: "#{dummy_root}/app/models/flag.rb:2",
352+
name: "Flag",
353+
}
354+
RunnerClient.any_instance.stubs(association_target: expected_response)
355+
356+
response = hover_on_source(<<~RUBY, { line: 4, character: 19 })
357+
# typed: false
358+
359+
class User < ActiveRecord::Base
360+
belongs_to :location, class_name: "Country"
361+
has_one :country_flag, through: :location, source: :flag
362+
end
363+
RUBY
364+
365+
assert_equal(<<~CONTENT.chomp, response.contents.value)
366+
```ruby
367+
Flag
368+
```
369+
370+
**Definitions**: [fake.rb](file:///fake.rb#L2,1-2,4)
371+
CONTENT
372+
end
373+
374+
test "returns through association on has_many :through association" do
375+
expected_response = {
376+
location: "#{dummy_root}/app/models/membership.rb:3",
377+
name: "Membership",
378+
}
379+
RunnerClient.any_instance.stubs(association_target: expected_response)
380+
381+
response = hover_on_source(<<~RUBY, { line: 4, character: 29 })
382+
# typed: false
383+
384+
class Organization < ActiveRecord::Base
385+
has_many :memberships
386+
has_many :users, through: :memberships
387+
end
388+
RUBY
389+
390+
assert_equal(<<~CONTENT.chomp, response.contents.value)
391+
```ruby
392+
Membership
393+
```
394+
395+
**Definitions**: [fake.rb](file:///fake.rb#L3,1-3,4)
396+
CONTENT
397+
end
398+
399+
test "returns through association on has_one :through association" do
400+
expected_response = {
401+
location: "#{dummy_root}/app/models/country.rb:2",
402+
name: "Country",
403+
}
404+
RunnerClient.any_instance.stubs(association_target: expected_response)
405+
406+
response = hover_on_source(<<~RUBY, { line: 4, character: 35 })
407+
# typed: false
408+
409+
class User < ActiveRecord::Base
410+
belongs_to :location, class_name: "Country"
411+
has_one :country_flag, through: :location, source: :flag
412+
end
413+
RUBY
414+
415+
assert_equal(<<~CONTENT.chomp, response.contents.value)
416+
```ruby
417+
Country
418+
```
419+
420+
**Definitions**: [fake.rb](file:///fake.rb#L2,1-2,4)
421+
CONTENT
422+
end
423+
324424
private
325425

326426
def hover_on_source(source, position)

0 commit comments

Comments
 (0)