Skip to content

Commit 679a494

Browse files
author
Aryan Soni
committed
Enable definitions for dsl method arguments
1 parent 46b97ce commit 679a494

File tree

5 files changed

+296
-1
lines changed

5 files changed

+296
-1
lines changed

lib/ruby_lsp/ruby_lsp_rails/addon.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require_relative "hover"
99
require_relative "code_lens"
1010
require_relative "document_symbol"
11+
require_relative "definition"
1112

1213
module RubyLsp
1314
module Rails
@@ -68,6 +69,19 @@ def create_document_symbol_listener(response_builder, dispatcher)
6869
DocumentSymbol.new(response_builder, dispatcher)
6970
end
7071

72+
sig do
73+
override.params(
74+
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
75+
uri: URI::Generic,
76+
nesting: T::Array[String],
77+
index: RubyIndexer::Index,
78+
dispatcher: Prism::Dispatcher,
79+
).void
80+
end
81+
def create_definition_listener(response_builder, uri, nesting, index, dispatcher)
82+
Definition.new(response_builder, nesting, index, dispatcher)
83+
end
84+
7185
sig { params(changes: T::Array[{ uri: String, type: Integer }]).void }
7286
def workspace_did_change_watched_files(changes)
7387
if changes.any? do |change|
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module RubyLsp
5+
module Rails
6+
# ![Definition demo](../../definition.gif)
7+
#
8+
# The [definition
9+
# request](https://microsoft.github.io/language-server-protocol/specification#textDocument_definition) jumps to the
10+
# definition of the symbol under the cursor.
11+
#
12+
# Currently supported targets:
13+
# - Classes
14+
# - Modules
15+
# - Constants
16+
# - Require paths
17+
# - Methods invoked on self only
18+
#
19+
# # Example
20+
#
21+
# ```ruby
22+
# require "some_gem/file" # <- Request go to definition on this string will take you to the file
23+
# Product.new # <- Request go to definition on this class name will take you to its declaration.
24+
# ```
25+
class Definition
26+
extend T::Sig
27+
include Requests::Support::Common
28+
include ActiveSupportTestCaseHelper
29+
30+
MODEL_CALLBACKS = T.let(
31+
[
32+
"before_validation",
33+
"after_validation",
34+
"before_save",
35+
"around_save",
36+
"after_save",
37+
"before_create",
38+
"around_create",
39+
"after_create",
40+
"after_commit",
41+
"after_rollback",
42+
"before_update",
43+
"around_update",
44+
"after_update",
45+
"before_destroy",
46+
"around_destroy",
47+
"after_destroy",
48+
"after_initialize",
49+
"after_find",
50+
"after_touch",
51+
].freeze,
52+
T::Array[String],
53+
)
54+
55+
CONTROLLER_CALLBACKS = T.let(
56+
[
57+
"after_action",
58+
"append_after_action",
59+
"append_around_action",
60+
"append_before_action",
61+
"around_action",
62+
"before_action",
63+
"prepend_after_action",
64+
"prepend_around_action",
65+
"prepend_before_action",
66+
"skip_after_action",
67+
"skip_around_action",
68+
"skip_before_action",
69+
].freeze,
70+
T::Array[String],
71+
)
72+
73+
JOB_CALLBACKS = T.let(
74+
[
75+
"after_enqueue",
76+
"after_perform",
77+
"around_enqueue",
78+
"around_perform",
79+
"before_enqueue",
80+
"before_perform",
81+
].freeze,
82+
T::Array[String],
83+
)
84+
85+
CALLBACKS = T.let((MODEL_CALLBACKS + CONTROLLER_CALLBACKS + JOB_CALLBACKS).freeze, T::Array[String])
86+
87+
sig do
88+
params(
89+
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
90+
nesting: T::Array[String],
91+
index: RubyIndexer::Index,
92+
dispatcher: Prism::Dispatcher,
93+
).void
94+
end
95+
def initialize(response_builder, nesting, index, dispatcher)
96+
@response_builder = response_builder
97+
@nesting = nesting
98+
@index = index
99+
100+
dispatcher.register(self, :on_call_node_enter)
101+
end
102+
103+
sig { params(node: Prism::CallNode).void }
104+
def on_call_node_enter(node)
105+
return unless self_receiver?(node)
106+
107+
message = node.message
108+
109+
return unless message && CALLBACKS.include?(message)
110+
111+
arguments = node.arguments&.arguments
112+
return unless arguments&.any?
113+
114+
arguments.each do |argument|
115+
name = case argument
116+
when Prism::SymbolNode
117+
argument.value
118+
when Prism::StringNode
119+
argument.content
120+
end
121+
122+
next unless name
123+
124+
collect_definitions(name)
125+
end
126+
end
127+
128+
private
129+
130+
sig { params(name: String).void }
131+
def collect_definitions(name)
132+
methods = @index.resolve_method(name, @nesting.join("::"))
133+
return unless methods
134+
135+
methods.each do |target_method|
136+
location = target_method.location
137+
file_path = target_method.file_path
138+
139+
@response_builder << Interface::Location.new(
140+
uri: URI::Generic.from_path(path: file_path).to_s,
141+
range: Interface::Range.new(
142+
start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
143+
end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
144+
),
145+
)
146+
end
147+
end
148+
end
149+
end
150+
end

misc/definition.gif

756 KB
Loading

test/dummy/app/models/user.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
# frozen_string_literal: true
33

44
class User < ApplicationRecord
5-
before_create :foo_arg, -> () {}
5+
before_create :foo, -> () {}
66
validates :name, presence: true
7+
8+
private
9+
10+
def foo
11+
puts "test"
12+
end
713
end
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
require "test_helper"
5+
6+
module RubyLsp
7+
module Rails
8+
class DefinitionTest < ActiveSupport::TestCase
9+
setup do
10+
@message_queue = Thread::Queue.new
11+
12+
# Build the Rails documents index ahead of time
13+
capture_io do
14+
Support::RailsDocumentClient.send(:search_index)
15+
end
16+
end
17+
18+
def teardown
19+
T.must(@message_queue).close
20+
end
21+
22+
test "recognizes model callback with multiple symbol arguments" do
23+
response = generate_definitions_for_source(<<~RUBY, { line: 3, character: 10 })
24+
# typed: false
25+
26+
class TestModel
27+
before_create :foo, :baz
28+
29+
def foo; end
30+
def baz; end
31+
end
32+
RUBY
33+
34+
assert_equal(2, response.size)
35+
36+
assert_equal("file:///fake.rb", response[0].uri)
37+
assert_equal(5, response[0].range.start.line)
38+
assert_equal(2, response[0].range.start.character)
39+
assert_equal(5, response[0].range.end.line)
40+
assert_equal(14, response[0].range.end.character)
41+
42+
assert_equal("file:///fake.rb", response[1].uri)
43+
assert_equal(6, response[1].range.start.line)
44+
assert_equal(2, response[1].range.start.character)
45+
assert_equal(6, response[1].range.end.line)
46+
assert_equal(14, response[1].range.end.character)
47+
end
48+
49+
test "recognizes controller callback with string argument" do
50+
response = generate_definitions_for_source(<<~RUBY, { line: 3, character: 10 })
51+
# typed: false
52+
53+
class TestController
54+
before_action "foo"
55+
56+
def foo; end
57+
end
58+
RUBY
59+
60+
assert_equal(1, response.size)
61+
62+
assert_equal("file:///fake.rb", response[0].uri)
63+
assert_equal(5, response[0].range.start.line)
64+
assert_equal(2, response[0].range.start.character)
65+
assert_equal(5, response[0].range.end.line)
66+
assert_equal(14, response[0].range.end.character)
67+
end
68+
69+
test "recognizes job callback with string and symbol arguments" do
70+
response = generate_definitions_for_source(<<~RUBY, { line: 3, character: 10 })
71+
# typed: false
72+
73+
class TestJob
74+
before_perform :foo, "baz"
75+
76+
def foo; end
77+
def baz; end
78+
end
79+
RUBY
80+
81+
assert_equal(2, response.size)
82+
83+
assert_equal("file:///fake.rb", response[0].uri)
84+
assert_equal(5, response[0].range.start.line)
85+
assert_equal(2, response[0].range.start.character)
86+
assert_equal(5, response[0].range.end.line)
87+
assert_equal(14, response[0].range.end.character)
88+
89+
assert_equal("file:///fake.rb", response[1].uri)
90+
assert_equal(6, response[1].range.start.line)
91+
assert_equal(2, response[1].range.start.character)
92+
assert_equal(6, response[1].range.end.line)
93+
assert_equal(14, response[1].range.end.character)
94+
end
95+
96+
private
97+
98+
def generate_definitions_for_source(source, position)
99+
uri = URI("file:///fake.rb")
100+
store = RubyLsp::Store.new
101+
store.set(uri: uri, source: source, version: 1)
102+
103+
executor = RubyLsp::Executor.new(store, @message_queue)
104+
executor.instance_variable_get(:@index).index_single(
105+
RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)), source
106+
)
107+
108+
capture_subprocess_io do
109+
RubyLsp::Executor.new(store, @message_queue).execute({
110+
method: "initialized",
111+
params: {},
112+
})
113+
end
114+
115+
response = executor.execute({
116+
method: "textDocument/definition",
117+
params: { textDocument: { uri: uri }, position: position },
118+
})
119+
120+
assert_nil(response.error)
121+
response.response
122+
end
123+
end
124+
end
125+
end

0 commit comments

Comments
 (0)