ruby-lsp-rails 0.4.7 → 0.5.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ruby_lsp/ruby_lsp_rails/addon.rb +18 -5
- data/lib/ruby_lsp/ruby_lsp_rails/definition.rb +105 -41
- data/lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb +60 -3
- data/lib/ruby_lsp/ruby_lsp_rails/hover.rb +122 -12
- data/lib/ruby_lsp/ruby_lsp_rails/rails_test_style.rb +28 -23
- data/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +44 -47
- data/lib/ruby_lsp/ruby_lsp_rails/server.rb +46 -5
- data/lib/ruby_lsp/ruby_lsp_rails/support/callbacks.rb +7 -1
- data/lib/ruby_lsp/ruby_lsp_rails/support/validations.rb +29 -0
- data/lib/ruby_lsp_rails/version.rb +1 -1
- metadata +7 -7
- data/lib/ruby_lsp/ruby_lsp_rails/indexing_enhancement.rb +0 -96
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45452e2ccad431e725e146db37fb0120542f809624e349d3aef928e2dc86ebe0
|
|
4
|
+
data.tar.gz: 71d467b0bc3909928f963c82b668974d3854de1520b8e477c8b2774fa4c63ee2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d13a3bf8c9c1d54f78afae535b852e8158426a0df64adb6eedfca0b35e6f06c2ca3ab5b97df1faac4173d48c8aba836b476e87d38d9d2f7ba82ea2d5a834f52d
|
|
7
|
+
data.tar.gz: 3b08291bbb3696ed7a266b7e6d763f03a04ae1ac1ffc05d67733b44d6b74bb6c510d8e26d1a792e44dad7868f6a08aff8ffa356ed2789871eb942476b279510c
|
|
@@ -7,6 +7,7 @@ require_relative "../../ruby_lsp_rails/version"
|
|
|
7
7
|
require_relative "support/active_support_test_case_helper"
|
|
8
8
|
require_relative "support/associations"
|
|
9
9
|
require_relative "support/callbacks"
|
|
10
|
+
require_relative "support/validations"
|
|
10
11
|
require_relative "support/location_builder"
|
|
11
12
|
require_relative "runner_client"
|
|
12
13
|
require_relative "hover"
|
|
@@ -15,7 +16,6 @@ require_relative "document_symbol"
|
|
|
15
16
|
require_relative "definition"
|
|
16
17
|
require_relative "rails_test_style"
|
|
17
18
|
require_relative "completion"
|
|
18
|
-
require_relative "indexing_enhancement"
|
|
19
19
|
|
|
20
20
|
module RubyLsp
|
|
21
21
|
module Rails
|
|
@@ -39,7 +39,7 @@ module RubyLsp
|
|
|
39
39
|
@client_mutex = Mutex.new #: Mutex
|
|
40
40
|
@client_mutex.lock
|
|
41
41
|
|
|
42
|
-
Thread.new do
|
|
42
|
+
@boot_thread = Thread.new do
|
|
43
43
|
@addon_mutex.synchronize do
|
|
44
44
|
# We need to ensure the Rails client is fully loaded before we activate the server addons
|
|
45
45
|
@client_mutex.synchronize do
|
|
@@ -50,7 +50,7 @@ module RubyLsp
|
|
|
50
50
|
end
|
|
51
51
|
offer_to_run_pending_migrations
|
|
52
52
|
end
|
|
53
|
-
end
|
|
53
|
+
end #: Thread
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
#: -> RunnerClient
|
|
@@ -77,6 +77,7 @@ module RubyLsp
|
|
|
77
77
|
# @override
|
|
78
78
|
#: -> void
|
|
79
79
|
def deactivate
|
|
80
|
+
@boot_thread.join
|
|
80
81
|
@rails_runner_client.shutdown
|
|
81
82
|
end
|
|
82
83
|
|
|
@@ -128,7 +129,7 @@ module RubyLsp
|
|
|
128
129
|
def create_definition_listener(response_builder, uri, node_context, dispatcher)
|
|
129
130
|
return unless @global_state
|
|
130
131
|
|
|
131
|
-
Definition.new(@rails_runner_client, response_builder, node_context, @global_state.
|
|
132
|
+
Definition.new(@rails_runner_client, response_builder, node_context, @global_state.graph, dispatcher)
|
|
132
133
|
end
|
|
133
134
|
|
|
134
135
|
# @override
|
|
@@ -149,6 +150,10 @@ module RubyLsp
|
|
|
149
150
|
|
|
150
151
|
offer_to_run_pending_migrations
|
|
151
152
|
end
|
|
153
|
+
|
|
154
|
+
if changes.any? { |c| %r{config/locales/.*\.yml}.match?(c[:uri]) }
|
|
155
|
+
@rails_runner_client.trigger_i18n_reload
|
|
156
|
+
end
|
|
152
157
|
end
|
|
153
158
|
|
|
154
159
|
# @override
|
|
@@ -230,7 +235,7 @@ module RubyLsp
|
|
|
230
235
|
id: "workspace/didChangeWatchedFilesRails",
|
|
231
236
|
method: "workspace/didChangeWatchedFiles",
|
|
232
237
|
register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
|
|
233
|
-
watchers: [structure_sql_file_watcher, fixture_file_watcher],
|
|
238
|
+
watchers: [structure_sql_file_watcher, fixture_file_watcher, i18n_file_watcher],
|
|
234
239
|
),
|
|
235
240
|
),
|
|
236
241
|
],
|
|
@@ -254,6 +259,14 @@ module RubyLsp
|
|
|
254
259
|
)
|
|
255
260
|
end
|
|
256
261
|
|
|
262
|
+
#: -> Interface::FileSystemWatcher
|
|
263
|
+
def i18n_file_watcher
|
|
264
|
+
Interface::FileSystemWatcher.new(
|
|
265
|
+
glob_pattern: "**/config/locales/**/*.{yml,yaml}",
|
|
266
|
+
kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
257
270
|
#: -> void
|
|
258
271
|
def offer_to_run_pending_migrations
|
|
259
272
|
return unless @outgoing_queue
|
|
@@ -30,13 +30,19 @@ module RubyLsp
|
|
|
30
30
|
class Definition
|
|
31
31
|
include Requests::Support::Common
|
|
32
32
|
|
|
33
|
-
#: (
|
|
34
|
-
|
|
33
|
+
#: (
|
|
34
|
+
#| RunnerClient,
|
|
35
|
+
#| RubyLsp::ResponseBuilders::CollectionResponseBuilder[(Interface::Location | Interface::LocationLink)],
|
|
36
|
+
#| NodeContext,
|
|
37
|
+
#| Rubydex::Graph,
|
|
38
|
+
#| Prism::Dispatcher
|
|
39
|
+
#| ) -> void
|
|
40
|
+
def initialize(client, response_builder, node_context, graph, dispatcher)
|
|
35
41
|
@client = client
|
|
36
42
|
@response_builder = response_builder
|
|
37
43
|
@node_context = node_context
|
|
38
44
|
@nesting = node_context.nesting #: Array[String]
|
|
39
|
-
@
|
|
45
|
+
@graph = graph
|
|
40
46
|
|
|
41
47
|
dispatcher.register(self, :on_call_node_enter, :on_symbol_node_enter, :on_string_node_enter)
|
|
42
48
|
end
|
|
@@ -51,55 +57,78 @@ module RubyLsp
|
|
|
51
57
|
handle_possible_dsl(node)
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
#: (
|
|
55
|
-
def
|
|
56
|
-
node = @node_context.call_node
|
|
57
|
-
return unless node
|
|
60
|
+
#: (Prism::CallNode node) -> void
|
|
61
|
+
def on_call_node_enter(node)
|
|
58
62
|
return unless self_receiver?(node)
|
|
59
63
|
|
|
60
64
|
message = node.message
|
|
61
65
|
|
|
62
66
|
return unless message
|
|
63
67
|
|
|
64
|
-
if
|
|
65
|
-
|
|
66
|
-
elsif Support::Callbacks::ALL.include?(message)
|
|
67
|
-
handle_callback(node)
|
|
68
|
+
if message.end_with?("_path") || message.end_with?("_url")
|
|
69
|
+
handle_route(node)
|
|
68
70
|
end
|
|
69
71
|
end
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
def on_call_node_enter(node)
|
|
73
|
-
return unless self_receiver?(node)
|
|
73
|
+
private
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
#: ((Prism::SymbolNode | Prism::StringNode) node) -> void
|
|
76
|
+
def handle_possible_dsl(node)
|
|
77
|
+
call_node = @node_context.call_node
|
|
78
|
+
return unless call_node
|
|
79
|
+
return unless self_receiver?(call_node)
|
|
80
|
+
|
|
81
|
+
message = call_node.message
|
|
76
82
|
|
|
77
83
|
return unless message
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
arguments = call_node.arguments&.arguments
|
|
86
|
+
return unless arguments
|
|
87
|
+
|
|
88
|
+
if Support::Associations::ALL.include?(message)
|
|
89
|
+
handle_association(call_node)
|
|
90
|
+
elsif Support::Callbacks::ALL.include?(message)
|
|
91
|
+
handle_callback(node, call_node, arguments)
|
|
92
|
+
handle_if_unless_conditional(node, call_node, arguments)
|
|
93
|
+
elsif Support::Validations::ALL.include?(message)
|
|
94
|
+
handle_validation(node, call_node, arguments)
|
|
95
|
+
handle_if_unless_conditional(node, call_node, arguments)
|
|
81
96
|
end
|
|
82
97
|
end
|
|
83
98
|
|
|
84
|
-
|
|
99
|
+
#: ((Prism::SymbolNode | Prism::StringNode) node, Prism::CallNode call_node, Array[Prism::Node] arguments) -> void
|
|
100
|
+
def handle_callback(node, call_node, arguments)
|
|
101
|
+
focus_argument = arguments.find { |argument| argument == node }
|
|
85
102
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
name = case focus_argument
|
|
104
|
+
when Prism::SymbolNode
|
|
105
|
+
focus_argument.value
|
|
106
|
+
when Prism::StringNode
|
|
107
|
+
focus_argument.content
|
|
108
|
+
end
|
|
90
109
|
|
|
91
|
-
|
|
92
|
-
name = case argument
|
|
93
|
-
when Prism::SymbolNode
|
|
94
|
-
argument.value
|
|
95
|
-
when Prism::StringNode
|
|
96
|
-
argument.content
|
|
97
|
-
end
|
|
110
|
+
return unless name
|
|
98
111
|
|
|
99
|
-
|
|
112
|
+
collect_definitions(name)
|
|
113
|
+
end
|
|
100
114
|
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
#: ((Prism::SymbolNode | Prism::StringNode) node, Prism::CallNode call_node, Array[Prism::Node] arguments) -> void
|
|
116
|
+
def handle_validation(node, call_node, arguments)
|
|
117
|
+
message = call_node.message
|
|
118
|
+
return unless message
|
|
119
|
+
|
|
120
|
+
focus_argument = arguments.find { |argument| argument == node }
|
|
121
|
+
return unless focus_argument
|
|
122
|
+
|
|
123
|
+
return unless node.is_a?(Prism::SymbolNode)
|
|
124
|
+
|
|
125
|
+
name = node.value
|
|
126
|
+
return unless name
|
|
127
|
+
|
|
128
|
+
# validates_with uses constants, not symbols - skip (handled by constant resolution)
|
|
129
|
+
return if message == "validates_with"
|
|
130
|
+
|
|
131
|
+
collect_definitions(name)
|
|
103
132
|
end
|
|
104
133
|
|
|
105
134
|
#: (Prism::CallNode node) -> void
|
|
@@ -109,7 +138,7 @@ module RubyLsp
|
|
|
109
138
|
|
|
110
139
|
association_name = first_argument.unescaped
|
|
111
140
|
|
|
112
|
-
result = @client.
|
|
141
|
+
result = @client.association_target(
|
|
113
142
|
model_name: @nesting.join("::"),
|
|
114
143
|
association_name: association_name,
|
|
115
144
|
)
|
|
@@ -131,16 +160,51 @@ module RubyLsp
|
|
|
131
160
|
|
|
132
161
|
#: (String name) -> void
|
|
133
162
|
def collect_definitions(name)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
@
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
return if @nesting.empty?
|
|
164
|
+
|
|
165
|
+
owner = @graph.resolve_constant(
|
|
166
|
+
@nesting.last, #: as !nil
|
|
167
|
+
@nesting[0...-1], #: as !nil
|
|
168
|
+
)
|
|
169
|
+
return unless owner.is_a?(Rubydex::Namespace)
|
|
170
|
+
|
|
171
|
+
declaration = owner.find_member("#{name}()")
|
|
172
|
+
return unless declaration
|
|
173
|
+
|
|
174
|
+
declaration.definitions.each do |definition|
|
|
175
|
+
@response_builder << definition.to_lsp_selection_location
|
|
142
176
|
end
|
|
143
177
|
end
|
|
178
|
+
|
|
179
|
+
#: ((Prism::SymbolNode | Prism::StringNode) node, Prism::CallNode call_node, Array[Prism::Node] arguments) -> void
|
|
180
|
+
def handle_if_unless_conditional(node, call_node, arguments)
|
|
181
|
+
keyword_arguments = arguments.find { |argument| argument.is_a?(Prism::KeywordHashNode) } #: as Prism::KeywordHashNode?
|
|
182
|
+
return unless keyword_arguments
|
|
183
|
+
|
|
184
|
+
element = keyword_arguments.elements.find do |element|
|
|
185
|
+
next false unless element.is_a?(Prism::AssocNode)
|
|
186
|
+
|
|
187
|
+
key = element.key
|
|
188
|
+
next false unless key.is_a?(Prism::SymbolNode)
|
|
189
|
+
|
|
190
|
+
key_value = key.value
|
|
191
|
+
next false unless key_value == "if" || key_value == "unless"
|
|
192
|
+
|
|
193
|
+
value = element.value
|
|
194
|
+
next false unless value.is_a?(Prism::SymbolNode)
|
|
195
|
+
|
|
196
|
+
value == node
|
|
197
|
+
end #: as Prism::AssocNode?
|
|
198
|
+
|
|
199
|
+
return unless element
|
|
200
|
+
|
|
201
|
+
value = element.value #: as Prism::SymbolNode
|
|
202
|
+
method_name = value.value
|
|
203
|
+
|
|
204
|
+
return unless method_name
|
|
205
|
+
|
|
206
|
+
collect_definitions(method_name)
|
|
207
|
+
end
|
|
144
208
|
end
|
|
145
209
|
end
|
|
146
210
|
end
|
|
@@ -16,10 +16,12 @@ module RubyLsp
|
|
|
16
16
|
def initialize(response_builder, dispatcher)
|
|
17
17
|
@response_builder = response_builder
|
|
18
18
|
@namespace_stack = [] #: Array[String]
|
|
19
|
+
@inside_schema = false #: bool
|
|
19
20
|
|
|
20
21
|
dispatcher.register(
|
|
21
22
|
self,
|
|
22
23
|
:on_call_node_enter,
|
|
24
|
+
:on_call_node_leave,
|
|
23
25
|
:on_class_node_enter,
|
|
24
26
|
:on_class_node_leave,
|
|
25
27
|
:on_module_node_enter,
|
|
@@ -29,6 +31,13 @@ module RubyLsp
|
|
|
29
31
|
|
|
30
32
|
#: (Prism::CallNode node) -> void
|
|
31
33
|
def on_call_node_enter(node)
|
|
34
|
+
message = node.message
|
|
35
|
+
return unless message
|
|
36
|
+
|
|
37
|
+
@inside_schema = true if node_is_schema_define?(node)
|
|
38
|
+
|
|
39
|
+
handle_schema_table(node)
|
|
40
|
+
|
|
32
41
|
return if @namespace_stack.empty?
|
|
33
42
|
|
|
34
43
|
content = extract_test_case_name(node)
|
|
@@ -44,9 +53,6 @@ module RubyLsp
|
|
|
44
53
|
receiver = node.receiver
|
|
45
54
|
return if receiver && !receiver.is_a?(Prism::SelfNode)
|
|
46
55
|
|
|
47
|
-
message = node.message
|
|
48
|
-
return unless message
|
|
49
|
-
|
|
50
56
|
case message
|
|
51
57
|
when *Support::Callbacks::ALL, "validate"
|
|
52
58
|
handle_all_arg_types(node, message)
|
|
@@ -58,6 +64,11 @@ module RubyLsp
|
|
|
58
64
|
end
|
|
59
65
|
end
|
|
60
66
|
|
|
67
|
+
#: (Prism::CallNode node) -> void
|
|
68
|
+
def on_call_node_leave(node)
|
|
69
|
+
@inside_schema = false if node_is_schema_define?(node)
|
|
70
|
+
end
|
|
71
|
+
|
|
61
72
|
#: (Prism::ClassNode node) -> void
|
|
62
73
|
def on_class_node_enter(node)
|
|
63
74
|
add_to_namespace_stack(node)
|
|
@@ -213,6 +224,39 @@ module RubyLsp
|
|
|
213
224
|
end
|
|
214
225
|
end
|
|
215
226
|
|
|
227
|
+
#: (Prism::CallNode node) -> void
|
|
228
|
+
def handle_schema_table(node)
|
|
229
|
+
return unless @inside_schema
|
|
230
|
+
return unless node.message == "create_table"
|
|
231
|
+
|
|
232
|
+
table_name_argument = node.arguments&.arguments&.first
|
|
233
|
+
|
|
234
|
+
return unless table_name_argument
|
|
235
|
+
|
|
236
|
+
case table_name_argument
|
|
237
|
+
when Prism::SymbolNode
|
|
238
|
+
name = table_name_argument.value
|
|
239
|
+
return unless name
|
|
240
|
+
|
|
241
|
+
append_document_symbol(
|
|
242
|
+
name: name,
|
|
243
|
+
range: range_from_location(table_name_argument.location),
|
|
244
|
+
selection_range: range_from_location(
|
|
245
|
+
table_name_argument.value_loc, #: as !nil
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
when Prism::StringNode
|
|
249
|
+
name = table_name_argument.content
|
|
250
|
+
return if name.empty?
|
|
251
|
+
|
|
252
|
+
append_document_symbol(
|
|
253
|
+
name: name,
|
|
254
|
+
range: range_from_location(table_name_argument.location),
|
|
255
|
+
selection_range: range_from_location(table_name_argument.content_loc),
|
|
256
|
+
)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
216
260
|
#: (name: String, range: RubyLsp::Interface::Range, selection_range: RubyLsp::Interface::Range) -> void
|
|
217
261
|
def append_document_symbol(name:, range:, selection_range:)
|
|
218
262
|
@response_builder.last.children << RubyLsp::Interface::DocumentSymbol.new(
|
|
@@ -222,6 +266,19 @@ module RubyLsp
|
|
|
222
266
|
selection_range: selection_range,
|
|
223
267
|
)
|
|
224
268
|
end
|
|
269
|
+
|
|
270
|
+
#: (Prism::CallNode node) -> bool
|
|
271
|
+
def node_is_schema_define?(node)
|
|
272
|
+
return false if node.message != "define"
|
|
273
|
+
|
|
274
|
+
schema_node = node.receiver
|
|
275
|
+
return false unless schema_node.is_a?(Prism::CallNode)
|
|
276
|
+
|
|
277
|
+
active_record_node = schema_node.receiver
|
|
278
|
+
return false unless active_record_node.is_a?(Prism::ConstantPathNode)
|
|
279
|
+
|
|
280
|
+
constant_name(active_record_node) == "ActiveRecord::Schema"
|
|
281
|
+
end
|
|
225
282
|
end
|
|
226
283
|
end
|
|
227
284
|
end
|
|
@@ -21,28 +21,46 @@ module RubyLsp
|
|
|
21
21
|
def initialize(client, response_builder, node_context, global_state, dispatcher)
|
|
22
22
|
@client = client
|
|
23
23
|
@response_builder = response_builder
|
|
24
|
+
@node_context = node_context
|
|
24
25
|
@nesting = node_context.nesting #: Array[String]
|
|
25
|
-
@
|
|
26
|
-
|
|
26
|
+
@graph = global_state.graph #: Rubydex::Graph
|
|
27
|
+
|
|
28
|
+
dispatcher.register(
|
|
29
|
+
self,
|
|
30
|
+
:on_constant_path_node_enter,
|
|
31
|
+
:on_constant_read_node_enter,
|
|
32
|
+
:on_symbol_node_enter,
|
|
33
|
+
:on_string_node_enter,
|
|
34
|
+
)
|
|
27
35
|
end
|
|
28
36
|
|
|
29
37
|
#: (Prism::ConstantPathNode node) -> void
|
|
30
38
|
def on_constant_path_node_enter(node)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
name = constant_name(node)
|
|
40
|
+
return unless name
|
|
41
|
+
|
|
42
|
+
declaration = @graph.resolve_constant(name, @nesting)
|
|
43
|
+
return unless declaration
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
generate_column_content(name)
|
|
45
|
+
generate_column_content(declaration.name)
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
#: (Prism::ConstantReadNode node) -> void
|
|
40
49
|
def on_constant_read_node_enter(node)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
declaration = @graph.resolve_constant(node.name.to_s, @nesting)
|
|
51
|
+
return unless declaration
|
|
52
|
+
|
|
53
|
+
generate_column_content(declaration.name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#: (Prism::SymbolNode node) -> void
|
|
57
|
+
def on_symbol_node_enter(node)
|
|
58
|
+
handle_possible_dsl(node)
|
|
59
|
+
end
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
#: (Prism::StringNode node) -> void
|
|
62
|
+
def on_string_node_enter(node)
|
|
63
|
+
handle_possible_i18n(node)
|
|
46
64
|
end
|
|
47
65
|
|
|
48
66
|
private
|
|
@@ -86,7 +104,15 @@ module RubyLsp
|
|
|
86
104
|
@response_builder.push(
|
|
87
105
|
model[:indexes].map do |index|
|
|
88
106
|
uniqueness = index[:unique] ? " (unique)" : ""
|
|
89
|
-
|
|
107
|
+
columns = case index[:columns]
|
|
108
|
+
when Array
|
|
109
|
+
index[:columns].join(",")
|
|
110
|
+
when String
|
|
111
|
+
index[:columns]
|
|
112
|
+
else
|
|
113
|
+
index[:name]
|
|
114
|
+
end
|
|
115
|
+
"- **#{index[:name]}** (#{columns})#{uniqueness}"
|
|
90
116
|
end.join("\n"),
|
|
91
117
|
category: :documentation,
|
|
92
118
|
)
|
|
@@ -104,6 +130,90 @@ module RubyLsp
|
|
|
104
130
|
default_value
|
|
105
131
|
end
|
|
106
132
|
end
|
|
133
|
+
|
|
134
|
+
#: (Prism::SymbolNode node) -> void
|
|
135
|
+
def handle_possible_dsl(node)
|
|
136
|
+
node = @node_context.call_node
|
|
137
|
+
return unless node
|
|
138
|
+
return unless self_receiver?(node)
|
|
139
|
+
|
|
140
|
+
message = node.message
|
|
141
|
+
|
|
142
|
+
return unless message
|
|
143
|
+
|
|
144
|
+
if Support::Associations::ALL.include?(message)
|
|
145
|
+
handle_association(node)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
#: (Prism::CallNode node) -> void
|
|
150
|
+
def handle_association(node)
|
|
151
|
+
first_argument = node.arguments&.arguments&.first
|
|
152
|
+
return unless first_argument.is_a?(Prism::SymbolNode)
|
|
153
|
+
|
|
154
|
+
association_name = first_argument.unescaped
|
|
155
|
+
|
|
156
|
+
result = @client.association_target(
|
|
157
|
+
model_name: @nesting.join("::"),
|
|
158
|
+
association_name: association_name,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return unless result
|
|
162
|
+
|
|
163
|
+
generate_hover(result[:name])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
#: (Prism::StringNode node) -> void
|
|
167
|
+
def handle_possible_i18n(node)
|
|
168
|
+
call_node = @node_context.call_node
|
|
169
|
+
return unless i18n_translate?(call_node)
|
|
170
|
+
|
|
171
|
+
first_argument = call_node #: as !nil
|
|
172
|
+
.arguments&.arguments&.first
|
|
173
|
+
return unless first_argument == node
|
|
174
|
+
|
|
175
|
+
i18n_key = first_argument.unescaped
|
|
176
|
+
return if i18n_key.empty?
|
|
177
|
+
|
|
178
|
+
result = @client.i18n(i18n_key)
|
|
179
|
+
return unless result
|
|
180
|
+
|
|
181
|
+
generate_i18n_hover(result)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
#: (Prism::CallNode? call_node) -> bool
|
|
185
|
+
def i18n_translate?(call_node)
|
|
186
|
+
return false unless call_node
|
|
187
|
+
|
|
188
|
+
receiver = call_node.receiver
|
|
189
|
+
return false unless receiver.is_a?(Prism::ConstantReadNode)
|
|
190
|
+
return false unless receiver.name == :I18n
|
|
191
|
+
|
|
192
|
+
message = call_node.message
|
|
193
|
+
message == "t" || message == "translate"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
#: (String name) -> void
|
|
197
|
+
def generate_hover(name)
|
|
198
|
+
declaration = @graph.resolve_constant(name, @node_context.nesting)
|
|
199
|
+
return unless declaration
|
|
200
|
+
|
|
201
|
+
# [RUBYDEX] TODO: once we have visibility exposed from Rubydex, we should only show hover for private constants
|
|
202
|
+
# if the constant is defined in the same ancestor chain as the reference
|
|
203
|
+
categorized_markdown_from_definitions(declaration.name, declaration.definitions).each do |category, content|
|
|
204
|
+
@response_builder.push(content, category: category)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
#: (Hash[Symbol, String] translations) -> void
|
|
209
|
+
def generate_i18n_hover(translations)
|
|
210
|
+
content = translations.map { |lang, translation| "#{lang}: #{translation}" }.join("\n")
|
|
211
|
+
content = "```yaml\n#{content}\n```"
|
|
212
|
+
@response_builder.push(
|
|
213
|
+
content,
|
|
214
|
+
category: :documentation,
|
|
215
|
+
)
|
|
216
|
+
end
|
|
107
217
|
end
|
|
108
218
|
end
|
|
109
219
|
end
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
module RubyLsp
|
|
5
5
|
module Rails
|
|
6
6
|
class RailsTestStyle < Listeners::TestDiscovery
|
|
7
|
-
BASE_COMMAND = "#{
|
|
7
|
+
BASE_COMMAND = "bundle exec ruby -r#{Listeners::TestStyle::MINITEST_REPORTER_PATH} bin/rails test" #: String
|
|
8
8
|
|
|
9
9
|
class << self
|
|
10
10
|
#: (Array[Hash[Symbol, untyped]]) -> Array[String]
|
|
@@ -26,17 +26,19 @@ module RubyLsp
|
|
|
26
26
|
|
|
27
27
|
if tags.include?("test_dir")
|
|
28
28
|
if children.empty?
|
|
29
|
-
full_files.concat(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
full_files.concat(
|
|
30
|
+
Dir.glob(
|
|
31
|
+
"#{path}/**/{*_test,test_*}.rb",
|
|
32
|
+
File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
|
|
33
|
+
).map! { |f| Shellwords.escape(f) },
|
|
34
|
+
)
|
|
33
35
|
end
|
|
34
36
|
elsif tags.include?("test_file")
|
|
35
|
-
full_files << path if children.empty?
|
|
37
|
+
full_files << Shellwords.escape(path) if children.empty?
|
|
36
38
|
elsif tags.include?("test_group")
|
|
37
|
-
commands << "#{BASE_COMMAND} #{path} --name \"/#{Shellwords.escape(item[:id])}(#|::)/\""
|
|
39
|
+
commands << "#{BASE_COMMAND} #{Shellwords.escape(path)} --name \"/#{Shellwords.escape(item[:id])}(#|::)/\""
|
|
38
40
|
else
|
|
39
|
-
full_files << "#{path}:#{item.dig(:range, :start, :line) + 1}"
|
|
41
|
+
full_files << "#{Shellwords.escape(path)}:#{item.dig(:range, :start, :line) + 1}"
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
queue.concat(children)
|
|
@@ -67,9 +69,11 @@ module RubyLsp
|
|
|
67
69
|
def on_class_node_enter(node)
|
|
68
70
|
with_test_ancestor_tracking(node) do |name, ancestors|
|
|
69
71
|
if declarative_minitest?(ancestors, name)
|
|
72
|
+
label = constant_name(node.constant_path) || name_with_dynamic_reference(node.constant_path)
|
|
73
|
+
|
|
70
74
|
test_item = Requests::Support::TestItem.new(
|
|
71
75
|
name,
|
|
72
|
-
|
|
76
|
+
label,
|
|
73
77
|
@uri,
|
|
74
78
|
range_from_node(node),
|
|
75
79
|
framework: :rails,
|
|
@@ -119,7 +123,7 @@ module RubyLsp
|
|
|
119
123
|
# Rails uses at runtime, ensuring proper test discovery and execution.
|
|
120
124
|
rails_normalized_name = "test_#{test_name.gsub(/\s+/, "_")}"
|
|
121
125
|
|
|
122
|
-
add_test_item(node, rails_normalized_name)
|
|
126
|
+
add_test_item(node, rails_normalized_name, test_name)
|
|
123
127
|
end
|
|
124
128
|
|
|
125
129
|
#: (Prism::DefNode node) -> void
|
|
@@ -129,30 +133,31 @@ module RubyLsp
|
|
|
129
133
|
name = node.name.to_s
|
|
130
134
|
return unless name.start_with?("test_")
|
|
131
135
|
|
|
132
|
-
add_test_item(node, name)
|
|
136
|
+
add_test_item(node, name, name)
|
|
133
137
|
end
|
|
134
138
|
|
|
135
139
|
private
|
|
136
140
|
|
|
137
|
-
#: (Array[String]
|
|
141
|
+
#: (Array[String], String) -> bool
|
|
138
142
|
def declarative_minitest?(attached_ancestors, fully_qualified_name)
|
|
139
|
-
# The declarative test style is present as long as the class extends
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
# The declarative test style is present as long as the class extends ActiveSupport::Testing::Declarative
|
|
144
|
+
declaration = @graph[fully_qualified_name]
|
|
145
|
+
return false unless declaration.is_a?(Rubydex::Namespace)
|
|
146
|
+
|
|
147
|
+
singleton = declaration.singleton_class
|
|
148
|
+
return attached_ancestors.include?("ActiveSupport::TestCase") unless singleton
|
|
149
|
+
|
|
150
|
+
singleton.ancestors.map(&:name).include?("ActiveSupport::Testing::Declarative")
|
|
146
151
|
end
|
|
147
152
|
|
|
148
|
-
#: (Prism::Node
|
|
149
|
-
def add_test_item(node,
|
|
153
|
+
#: (Prism::Node, String, String) -> void
|
|
154
|
+
def add_test_item(node, test_id, label)
|
|
150
155
|
parent = @parent_stack.last
|
|
151
156
|
return unless parent.is_a?(Requests::Support::TestItem)
|
|
152
157
|
|
|
153
158
|
example_item = Requests::Support::TestItem.new(
|
|
154
|
-
"#{parent.id}##{
|
|
155
|
-
|
|
159
|
+
"#{parent.id}##{test_id}",
|
|
160
|
+
label,
|
|
156
161
|
@uri,
|
|
157
162
|
range_from_node(node),
|
|
158
163
|
framework: :rails,
|
|
@@ -51,23 +51,12 @@ module RubyLsp
|
|
|
51
51
|
def initialize(outgoing_queue, global_state)
|
|
52
52
|
@outgoing_queue = outgoing_queue #: Thread::Queue
|
|
53
53
|
@mutex = Mutex.new #: Mutex
|
|
54
|
-
# Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
|
|
55
|
-
# parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
|
|
56
|
-
# set its own session ID
|
|
57
|
-
begin
|
|
58
|
-
Process.setpgrp
|
|
59
|
-
Process.setsid
|
|
60
|
-
rescue Errno::EPERM
|
|
61
|
-
# If we can't set the session ID, continue
|
|
62
|
-
rescue NotImplementedError
|
|
63
|
-
# setpgrp() may be unimplemented on some platform
|
|
64
|
-
# https://github.com/Shopify/ruby-lsp-rails/issues/348
|
|
65
|
-
end
|
|
66
54
|
|
|
67
55
|
log_message("Ruby LSP Rails booting server")
|
|
68
56
|
|
|
69
57
|
stdin, stdout, stderr, wait_thread = Bundler.with_original_env do
|
|
70
58
|
Open3.popen3(
|
|
59
|
+
{ "RUBY_LSP_RAILS_RUNNER" => "true" },
|
|
71
60
|
"bundle",
|
|
72
61
|
"exec",
|
|
73
62
|
"rails",
|
|
@@ -95,27 +84,14 @@ module RubyLsp
|
|
|
95
84
|
@rails_root = initialize_response[:root] #: String
|
|
96
85
|
log_message("Finished booting Ruby LSP Rails server")
|
|
97
86
|
|
|
98
|
-
unless ENV["RAILS_ENV"] == "test"
|
|
99
|
-
at_exit do
|
|
100
|
-
if @wait_thread.alive?
|
|
101
|
-
sleep(0.5) # give the server a bit of time if we already issued a shutdown notification
|
|
102
|
-
force_kill
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
87
|
# Responsible for transmitting notifications coming from the server to the outgoing queue, so that we can do
|
|
108
|
-
# things such as showing progress notifications initiated by the server
|
|
88
|
+
# things such as showing progress notifications initiated by the server. The loop exits naturally when the
|
|
89
|
+
# server closes its stderr write end (i.e., when the server process exits), at which point `read_notification`
|
|
90
|
+
# returns nil.
|
|
109
91
|
@notifier_thread = Thread.new do
|
|
110
|
-
|
|
111
|
-
notification
|
|
112
|
-
|
|
113
|
-
unless @outgoing_queue.closed? || !notification
|
|
114
|
-
@outgoing_queue << notification
|
|
115
|
-
end
|
|
92
|
+
while (notification = read_notification)
|
|
93
|
+
@outgoing_queue << notification unless @outgoing_queue.closed?
|
|
116
94
|
end
|
|
117
|
-
rescue IOError
|
|
118
|
-
# The server was shutdown and stderr is already closed
|
|
119
95
|
end #: Thread
|
|
120
96
|
rescue StandardError
|
|
121
97
|
raise InitializationError, @stderr.read
|
|
@@ -144,9 +120,9 @@ module RubyLsp
|
|
|
144
120
|
end
|
|
145
121
|
|
|
146
122
|
#: (model_name: String, association_name: String) -> Hash[Symbol, untyped]?
|
|
147
|
-
def
|
|
123
|
+
def association_target(model_name:, association_name:)
|
|
148
124
|
make_request(
|
|
149
|
-
"
|
|
125
|
+
"association_target",
|
|
150
126
|
model_name: model_name,
|
|
151
127
|
association_name: association_name,
|
|
152
128
|
)
|
|
@@ -180,6 +156,17 @@ module RubyLsp
|
|
|
180
156
|
nil
|
|
181
157
|
end
|
|
182
158
|
|
|
159
|
+
#: (String key) -> Hash[Symbol, untyped]?
|
|
160
|
+
def i18n(key)
|
|
161
|
+
make_request("i18n", key: key)
|
|
162
|
+
rescue MessageError
|
|
163
|
+
log_message(
|
|
164
|
+
"Ruby LSP Rails failed to get i18n information",
|
|
165
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
|
166
|
+
)
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
183
170
|
# Delegates a notification to a server add-on
|
|
184
171
|
#: (server_addon_name: String, request_name: String, **untyped params) -> void
|
|
185
172
|
def delegate_notification(server_addon_name:, request_name:, **params)
|
|
@@ -239,20 +226,39 @@ module RubyLsp
|
|
|
239
226
|
nil
|
|
240
227
|
end
|
|
241
228
|
|
|
229
|
+
#: -> void
|
|
230
|
+
def trigger_i18n_reload
|
|
231
|
+
log_message("Reloading I18n translations")
|
|
232
|
+
send_notification("reload_i18n")
|
|
233
|
+
rescue MessageError
|
|
234
|
+
log_message(
|
|
235
|
+
"Ruby LSP Rails failed to trigger I18n reload",
|
|
236
|
+
type: RubyLsp::Constant::MessageType::ERROR,
|
|
237
|
+
)
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
|
|
242
241
|
#: -> void
|
|
243
242
|
def shutdown
|
|
243
|
+
return if stopped?
|
|
244
|
+
|
|
244
245
|
log_message("Ruby LSP Rails shutting down server")
|
|
245
246
|
send_message("shutdown")
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
#
|
|
250
|
-
|
|
247
|
+
|
|
248
|
+
@stdin.close unless @stdin.closed?
|
|
249
|
+
|
|
250
|
+
# Wait for the server to exit. Once it does, all handles it inherited (including its stderr write end) are
|
|
251
|
+
# released, which lets the notifier thread drain remaining bytes and observe EOF.
|
|
252
|
+
@wait_thread.join
|
|
253
|
+
@notifier_thread.join
|
|
254
|
+
|
|
255
|
+
@stdout.close unless @stdout.closed?
|
|
256
|
+
@stderr.close unless @stderr.closed?
|
|
251
257
|
end
|
|
252
258
|
|
|
253
259
|
#: -> bool
|
|
254
260
|
def stopped?
|
|
255
|
-
[@stdin, @stdout, @stderr].all?(&:closed?) && !@wait_thread.alive?
|
|
261
|
+
[@stdin, @stdout, @stderr].all?(&:closed?) && !@wait_thread.alive? && !@notifier_thread.alive?
|
|
256
262
|
end
|
|
257
263
|
|
|
258
264
|
#: -> bool
|
|
@@ -313,15 +319,6 @@ module RubyLsp
|
|
|
313
319
|
nil
|
|
314
320
|
end
|
|
315
321
|
|
|
316
|
-
#: -> void
|
|
317
|
-
def force_kill
|
|
318
|
-
# Windows does not support the `TERM` signal, so we're forced to use `KILL` here
|
|
319
|
-
Process.kill(
|
|
320
|
-
Signal.list["KILL"], #: as !nil
|
|
321
|
-
@wait_thread.pid,
|
|
322
|
-
)
|
|
323
|
-
end
|
|
324
|
-
|
|
325
322
|
#: (::String message, ?type: ::Integer) -> void
|
|
326
323
|
def log_message(message, type: RubyLsp::Constant::MessageType::LOG)
|
|
327
324
|
return if @outgoing_queue.closed?
|
|
@@ -289,11 +289,18 @@ module RubyLsp
|
|
|
289
289
|
send_result({ message: "ok", root: ::Rails.root.to_s })
|
|
290
290
|
|
|
291
291
|
while @running
|
|
292
|
-
headers = @stdin.gets("\r\n\r\n")
|
|
293
|
-
|
|
292
|
+
headers = @stdin.gets("\r\n\r\n")
|
|
293
|
+
break unless headers
|
|
294
|
+
|
|
295
|
+
length = headers[/Content-Length: (\d+)/i, 1]
|
|
296
|
+
break unless length
|
|
297
|
+
|
|
298
|
+
json = @stdin.read(length.to_i)
|
|
299
|
+
break unless json
|
|
294
300
|
|
|
295
301
|
request = JSON.parse(json, symbolize_names: true)
|
|
296
302
|
execute(request.fetch(:method), request[:params])
|
|
303
|
+
disconnect_from_database
|
|
297
304
|
end
|
|
298
305
|
end
|
|
299
306
|
|
|
@@ -306,7 +313,7 @@ module RubyLsp
|
|
|
306
313
|
with_request_error_handling(request) do
|
|
307
314
|
send_result(resolve_database_info_from_model(params.fetch(:name)))
|
|
308
315
|
end
|
|
309
|
-
when "
|
|
316
|
+
when "association_target"
|
|
310
317
|
with_request_error_handling(request) do
|
|
311
318
|
send_result(resolve_association_target(params))
|
|
312
319
|
end
|
|
@@ -332,6 +339,17 @@ module RubyLsp
|
|
|
332
339
|
with_request_error_handling(request) do
|
|
333
340
|
send_result(resolve_route_info(params))
|
|
334
341
|
end
|
|
342
|
+
when "i18n"
|
|
343
|
+
with_request_error_handling(request) do
|
|
344
|
+
result = resolve_i18n_key(params.fetch(:key))
|
|
345
|
+
send_result(result)
|
|
346
|
+
end
|
|
347
|
+
when "reload_i18n"
|
|
348
|
+
with_progress("rails-reload-i18n", "Reloading Ruby LSP Rails I18n") do
|
|
349
|
+
with_notification_error_handling(request) do
|
|
350
|
+
I18n.reload! if defined?(I18n) && I18n.respond_to?(:reload!)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
335
353
|
when "server_addon/register"
|
|
336
354
|
with_notification_error_handling(request) do
|
|
337
355
|
require params[:server_addon_path]
|
|
@@ -431,12 +449,12 @@ module RubyLsp
|
|
|
431
449
|
source_location = Object.const_source_location(association_klass.to_s)
|
|
432
450
|
return unless source_location
|
|
433
451
|
|
|
434
|
-
{ location: "#{source_location[0]}:#{source_location[1]}" }
|
|
452
|
+
{ location: "#{source_location[0]}:#{source_location[1]}", name: association_klass.name }
|
|
435
453
|
rescue NameError
|
|
436
454
|
nil
|
|
437
455
|
end
|
|
438
456
|
|
|
439
|
-
#: (Module?) -> bool
|
|
457
|
+
#: (Module[top]?) -> bool
|
|
440
458
|
def active_record_model?(const)
|
|
441
459
|
!!(
|
|
442
460
|
const &&
|
|
@@ -493,6 +511,18 @@ module RubyLsp
|
|
|
493
511
|
end
|
|
494
512
|
end
|
|
495
513
|
|
|
514
|
+
# Keeping a connection to the database prevents it from being dropped in development. We don't actually need to
|
|
515
|
+
# to reuse database connections for the LSP server, the performance benefit of doing so only matters in production
|
|
516
|
+
# where there is latency, locally we're fine with the small overhead of establishing a new connection on each request.
|
|
517
|
+
#: -> void
|
|
518
|
+
def disconnect_from_database
|
|
519
|
+
return unless defined?(::ActiveRecord::Base)
|
|
520
|
+
|
|
521
|
+
with_notification_error_handling("disconnect_from_database") do
|
|
522
|
+
ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
496
526
|
#: (singleton(ActiveRecord::Base)) -> Array[String]
|
|
497
527
|
def collect_model_foreign_keys(model)
|
|
498
528
|
return [] unless model.connection.respond_to?(:supports_foreign_keys?) &&
|
|
@@ -525,10 +555,21 @@ module RubyLsp
|
|
|
525
555
|
rescue NotImplementedError
|
|
526
556
|
@database_supports_indexing = false
|
|
527
557
|
end
|
|
558
|
+
|
|
559
|
+
#: (String) -> Hash[Symbol, String]
|
|
560
|
+
def resolve_i18n_key(key)
|
|
561
|
+
I18n.available_locales.each_with_object({}) do |locale, result|
|
|
562
|
+
result[locale] = I18n.t(key, locale: locale, default: "⚠️ translation missing")
|
|
563
|
+
end
|
|
564
|
+
end
|
|
528
565
|
end
|
|
529
566
|
end
|
|
530
567
|
end
|
|
531
568
|
|
|
532
569
|
if ARGV.first == "start"
|
|
533
570
|
RubyLsp::Rails::Server.new(capabilities: JSON.parse(ARGV[1], symbolize_names: true)).start
|
|
571
|
+
|
|
572
|
+
# Ensure that we exit the process after finishing the server loop. This prevents child processes that may have been
|
|
573
|
+
# spawned by the user's Rails application from keeping this process alive
|
|
574
|
+
exit!(0)
|
|
534
575
|
end
|
|
@@ -55,7 +55,13 @@ module RubyLsp
|
|
|
55
55
|
"before_perform",
|
|
56
56
|
].freeze
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
MAILBOX = [
|
|
59
|
+
"after_processing",
|
|
60
|
+
"before_processing",
|
|
61
|
+
"around_processing",
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
ALL = (MODELS + CONTROLLERS + JOBS + MAILBOX).freeze #: Array[String]
|
|
59
65
|
end
|
|
60
66
|
end
|
|
61
67
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module RubyLsp
|
|
5
|
+
module Rails
|
|
6
|
+
module Support
|
|
7
|
+
module Validations
|
|
8
|
+
ALL = [
|
|
9
|
+
"validate",
|
|
10
|
+
"validates",
|
|
11
|
+
"validates!",
|
|
12
|
+
"validates_each",
|
|
13
|
+
"validates_with",
|
|
14
|
+
"validates_absence_of",
|
|
15
|
+
"validates_acceptance_of",
|
|
16
|
+
"validates_comparison_of",
|
|
17
|
+
"validates_confirmation_of",
|
|
18
|
+
"validates_exclusion_of",
|
|
19
|
+
"validates_format_of",
|
|
20
|
+
"validates_inclusion_of",
|
|
21
|
+
"validates_length_of",
|
|
22
|
+
"validates_numericality_of",
|
|
23
|
+
"validates_presence_of",
|
|
24
|
+
"validates_size_of",
|
|
25
|
+
].freeze
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-lsp-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0.beta1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify
|
|
@@ -15,20 +15,20 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.27.0.beta2
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: 0.
|
|
21
|
+
version: 0.28.0
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
25
|
requirements:
|
|
26
26
|
- - ">="
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: 0.
|
|
28
|
+
version: 0.27.0.beta2
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: 0.
|
|
31
|
+
version: 0.28.0
|
|
32
32
|
description: A Ruby LSP addon that adds extra editor functionality for Rails applications
|
|
33
33
|
email:
|
|
34
34
|
- ruby@shopify.com
|
|
@@ -46,7 +46,6 @@ files:
|
|
|
46
46
|
- lib/ruby_lsp/ruby_lsp_rails/definition.rb
|
|
47
47
|
- lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb
|
|
48
48
|
- lib/ruby_lsp/ruby_lsp_rails/hover.rb
|
|
49
|
-
- lib/ruby_lsp/ruby_lsp_rails/indexing_enhancement.rb
|
|
50
49
|
- lib/ruby_lsp/ruby_lsp_rails/rails_test_style.rb
|
|
51
50
|
- lib/ruby_lsp/ruby_lsp_rails/runner_client.rb
|
|
52
51
|
- lib/ruby_lsp/ruby_lsp_rails/server.rb
|
|
@@ -54,6 +53,7 @@ files:
|
|
|
54
53
|
- lib/ruby_lsp/ruby_lsp_rails/support/associations.rb
|
|
55
54
|
- lib/ruby_lsp/ruby_lsp_rails/support/callbacks.rb
|
|
56
55
|
- lib/ruby_lsp/ruby_lsp_rails/support/location_builder.rb
|
|
56
|
+
- lib/ruby_lsp/ruby_lsp_rails/support/validations.rb
|
|
57
57
|
- lib/ruby_lsp_rails/version.rb
|
|
58
58
|
- lib/tasks/ruby_lsp_rails_tasks.rake
|
|
59
59
|
homepage: https://github.com/Shopify/ruby-lsp-rails
|
|
@@ -79,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
80
|
version: '0'
|
|
81
81
|
requirements: []
|
|
82
|
-
rubygems_version:
|
|
82
|
+
rubygems_version: 4.0.3
|
|
83
83
|
specification_version: 4
|
|
84
84
|
summary: A Ruby LSP addon for Rails
|
|
85
85
|
test_files: []
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# typed: strict
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
module RubyLsp
|
|
5
|
-
module Rails
|
|
6
|
-
class IndexingEnhancement < RubyIndexer::Enhancement
|
|
7
|
-
# @override
|
|
8
|
-
#: (Prism::CallNode call_node) -> void
|
|
9
|
-
def on_call_node_enter(call_node)
|
|
10
|
-
owner = @listener.current_owner
|
|
11
|
-
return unless owner
|
|
12
|
-
|
|
13
|
-
case call_node.name
|
|
14
|
-
when :extend
|
|
15
|
-
handle_concern_extend(owner, call_node)
|
|
16
|
-
when :has_one, :has_many, :belongs_to, :has_and_belongs_to_many
|
|
17
|
-
handle_association(owner, call_node)
|
|
18
|
-
# for `class_methods do` blocks within concerns
|
|
19
|
-
when :class_methods
|
|
20
|
-
handle_class_methods(owner, call_node)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# @override
|
|
25
|
-
#: (Prism::CallNode call_node) -> void
|
|
26
|
-
def on_call_node_leave(call_node)
|
|
27
|
-
if call_node.name == :class_methods && call_node.block
|
|
28
|
-
@listener.pop_namespace_stack
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
#: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
|
|
35
|
-
def handle_association(owner, call_node)
|
|
36
|
-
arguments = call_node.arguments&.arguments
|
|
37
|
-
return unless arguments
|
|
38
|
-
|
|
39
|
-
name_arg = arguments.first
|
|
40
|
-
|
|
41
|
-
name = case name_arg
|
|
42
|
-
when Prism::StringNode
|
|
43
|
-
name_arg.content
|
|
44
|
-
when Prism::SymbolNode
|
|
45
|
-
name_arg.value
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
return unless name
|
|
49
|
-
|
|
50
|
-
loc = name_arg.location
|
|
51
|
-
|
|
52
|
-
# Reader
|
|
53
|
-
reader_signatures = [RubyIndexer::Entry::Signature.new([])]
|
|
54
|
-
@listener.add_method(name, loc, reader_signatures)
|
|
55
|
-
|
|
56
|
-
# Writer
|
|
57
|
-
writer_signatures = [
|
|
58
|
-
RubyIndexer::Entry::Signature.new([RubyIndexer::Entry::RequiredParameter.new(name: name.to_sym)]),
|
|
59
|
-
]
|
|
60
|
-
@listener.add_method("#{name}=", loc, writer_signatures)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
#: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
|
|
64
|
-
def handle_concern_extend(owner, call_node)
|
|
65
|
-
arguments = call_node.arguments&.arguments
|
|
66
|
-
return unless arguments
|
|
67
|
-
|
|
68
|
-
arguments.each do |node|
|
|
69
|
-
next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
70
|
-
|
|
71
|
-
module_name = node.full_name
|
|
72
|
-
next unless module_name == "ActiveSupport::Concern"
|
|
73
|
-
|
|
74
|
-
@listener.register_included_hook do |index, base|
|
|
75
|
-
class_methods_name = "#{owner.name}::ClassMethods"
|
|
76
|
-
|
|
77
|
-
if index.indexed?(class_methods_name)
|
|
78
|
-
singleton = index.existing_or_new_singleton_class(base.name)
|
|
79
|
-
singleton.mixin_operations << RubyIndexer::Entry::Include.new(class_methods_name)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError,
|
|
83
|
-
Prism::ConstantPathNode::MissingNodesInConstantPathError
|
|
84
|
-
# Do nothing
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
#: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
|
|
89
|
-
def handle_class_methods(owner, call_node)
|
|
90
|
-
return unless call_node.block
|
|
91
|
-
|
|
92
|
-
@listener.add_module("ClassMethods", call_node.location, call_node.location)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|