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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc55b4c0d67bcf2d51cfece504e53483ce6cc744fa482042bf8c9cdc5b6713e6
4
- data.tar.gz: aa657a320f52d7b97dff961045d2e8144e04a5390889c00c41ba65ab5b9c8112
3
+ metadata.gz: 45452e2ccad431e725e146db37fb0120542f809624e349d3aef928e2dc86ebe0
4
+ data.tar.gz: 71d467b0bc3909928f963c82b668974d3854de1520b8e477c8b2774fa4c63ee2
5
5
  SHA512:
6
- metadata.gz: 6b9e0856f031b24d5d97569db824d7781c10d0243099614d1bace814e46f326e280342ef451cef46ac1a9d28f9974ea8487b3dd35cae353ad79be7147fca726e
7
- data.tar.gz: e1d0e2840d2b41d3af0a139f45a1c4546dcd87130812764f1a0051285ff148c2b6ff5cbcbdfed7944e6e89f9ffd6e11267140fd208ec7669195fae5f351c6404
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.index, dispatcher)
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
- #: (RunnerClient client, RubyLsp::ResponseBuilders::CollectionResponseBuilder[(Interface::Location | Interface::LocationLink)] response_builder, NodeContext node_context, RubyIndexer::Index index, Prism::Dispatcher dispatcher) -> void
34
- def initialize(client, response_builder, node_context, index, dispatcher)
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
- @index = index
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
- #: ((Prism::SymbolNode | Prism::StringNode) node) -> void
55
- def handle_possible_dsl(node)
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 Support::Associations::ALL.include?(message)
65
- handle_association(node)
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
- #: (Prism::CallNode node) -> void
72
- def on_call_node_enter(node)
73
- return unless self_receiver?(node)
73
+ private
74
74
 
75
- message = node.message
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
- if message.end_with?("_path") || message.end_with?("_url")
80
- handle_route(node)
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
- private
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
- #: (Prism::CallNode node) -> void
87
- def handle_callback(node)
88
- arguments = node.arguments&.arguments
89
- return unless arguments&.any?
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
- arguments.each do |argument|
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
- next unless name
112
+ collect_definitions(name)
113
+ end
100
114
 
101
- collect_definitions(name)
102
- end
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.association_target_location(
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
- methods = @index.resolve_method(name, @nesting.join("::"))
135
- return unless methods
136
-
137
- methods.each do |target_method|
138
- @response_builder << Interface::Location.new(
139
- uri: target_method.uri.to_s,
140
- range: range_from_location(target_method.location),
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
- @index = global_state.index #: RubyIndexer::Index
26
- dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter)
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
- entries = @index.resolve(node.slice, @nesting)
32
- item = entries&.first
33
- return unless item
39
+ name = constant_name(node)
40
+ return unless name
41
+
42
+ declaration = @graph.resolve_constant(name, @nesting)
43
+ return unless declaration
34
44
 
35
- name = item.name
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
- entries = @index.resolve(node.name.to_s, @nesting)
42
- item = entries&.first
43
- return unless item
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
- generate_column_content(item.name)
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
- "- **#{index[:name]}** (#{index[:columns].join(",")})#{uniqueness}"
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 = "#{RbConfig.ruby} bin/rails test" #: String
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(Dir.glob(
30
- "#{path}/**/{*_test,test_*}.rb",
31
- File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
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
- name,
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] attached_ancestors, String fully_qualified_name) -> bool
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
- # ActiveSupport::Testing::Declarative
141
- name_parts = fully_qualified_name.split("::")
142
- singleton_name = "#{name_parts.join("::")}::<Class:#{name_parts.last}>"
143
- @index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative")
144
- rescue RubyIndexer::Index::NonExistingNamespaceError
145
- false
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 node, String test_name) -> void
149
- def add_test_item(node, test_name)
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}##{test_name}",
155
- test_name,
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
- until @stderr.closed?
111
- notification = read_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 association_target_location(model_name:, association_name:)
123
+ def association_target(model_name:, association_name:)
148
124
  make_request(
149
- "association_target_location",
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
- sleep(0.5) # give the server a bit of time to shutdown
247
- [@stdin, @stdout, @stderr].each(&:close)
248
- rescue IOError
249
- # The server connection may have died
250
- force_kill
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") #: as String
293
- json = @stdin.read(headers[/Content-Length: (\d+)/i, 1].to_i) #: as String
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 "association_target_location"
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
- ALL = (MODELS + CONTROLLERS + JOBS).freeze #: Array[String]
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
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Rails
6
- VERSION = "0.4.7"
6
+ VERSION = "0.5.0.beta1"
7
7
  end
8
8
  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.7
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.25.0
18
+ version: 0.27.0.beta2
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
- version: 0.26.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.25.0
28
+ version: 0.27.0.beta2
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
- version: 0.26.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: 3.6.9
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