ruby-lsp 0.23.1 → 0.23.5

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp-launcher +18 -9
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +89 -58
  5. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +21 -10
  6. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +36 -0
  7. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  8. data/lib/ruby_indexer/test/index_test.rb +2 -1
  9. data/lib/ruby_indexer/test/instance_variables_test.rb +20 -0
  10. data/lib/ruby_indexer/test/method_test.rb +86 -8
  11. data/lib/ruby_lsp/base_server.rb +11 -21
  12. data/lib/ruby_lsp/document.rb +62 -9
  13. data/lib/ruby_lsp/erb_document.rb +5 -3
  14. data/lib/ruby_lsp/global_state.rb +9 -0
  15. data/lib/ruby_lsp/listeners/definition.rb +7 -2
  16. data/lib/ruby_lsp/rbs_document.rb +2 -2
  17. data/lib/ruby_lsp/requests/code_action_resolve.rb +2 -6
  18. data/lib/ruby_lsp/requests/completion.rb +2 -1
  19. data/lib/ruby_lsp/requests/definition.rb +1 -1
  20. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  21. data/lib/ruby_lsp/requests/hover.rb +1 -1
  22. data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
  23. data/lib/ruby_lsp/requests/references.rb +1 -1
  24. data/lib/ruby_lsp/requests/rename.rb +3 -3
  25. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -4
  26. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  27. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +3 -3
  28. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -13
  29. data/lib/ruby_lsp/requests/workspace_symbol.rb +2 -2
  30. data/lib/ruby_lsp/ruby_document.rb +75 -6
  31. data/lib/ruby_lsp/server.rb +69 -49
  32. data/lib/ruby_lsp/store.rb +7 -7
  33. data/lib/ruby_lsp/type_inferrer.rb +39 -17
  34. data/lib/ruby_lsp/utils.rb +43 -0
  35. metadata +3 -2
@@ -107,7 +107,7 @@ module RubyLsp
107
107
  ),
108
108
  )
109
109
  when "$/cancelRequest"
110
- @mutex.synchronize { @cancelled_requests << message[:params][:id] }
110
+ @global_state.synchronize { @cancelled_requests << message[:params][:id] }
111
111
  when nil
112
112
  process_response(message) if message[:result]
113
113
  end
@@ -298,29 +298,11 @@ module RubyLsp
298
298
 
299
299
  # Not every client supports dynamic registration or file watching
300
300
  if @global_state.client_capabilities.supports_watching_files
301
- send_message(
302
- Request.new(
303
- id: @current_request_id,
304
- method: "client/registerCapability",
305
- params: Interface::RegistrationParams.new(
306
- registrations: [
307
- # Register watching Ruby files
308
- Interface::Registration.new(
309
- id: "workspace/didChangeWatchedFiles",
310
- method: "workspace/didChangeWatchedFiles",
311
- register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
312
- watchers: [
313
- Interface::FileSystemWatcher.new(
314
- glob_pattern: "**/*.rb",
315
- kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
316
- ),
317
- ],
318
- ),
319
- ),
320
- ],
321
- ),
322
- ),
323
- )
301
+ send_message(Request.register_watched_files(@current_request_id, "**/*.rb"))
302
+ send_message(Request.register_watched_files(
303
+ @current_request_id,
304
+ Interface::RelativePattern.new(base_uri: @global_state.workspace_uri.to_s, pattern: ".rubocop.yml"),
305
+ ))
324
306
  end
325
307
 
326
308
  process_indexing_configuration(options.dig(:initializationOptions, :indexing))
@@ -357,7 +339,7 @@ module RubyLsp
357
339
  if defined?(Requests::Support::RuboCopFormatter)
358
340
  begin
359
341
  @global_state.register_formatter("rubocop", Requests::Support::RuboCopFormatter.new)
360
- rescue RuboCop::Error => e
342
+ rescue ::RuboCop::Error => e
361
343
  # The user may have provided unknown config switches in .rubocop or
362
344
  # is trying to load a non-existent config file.
363
345
  send_message(Notification.window_show_message(
@@ -377,7 +359,7 @@ module RubyLsp
377
359
 
378
360
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
379
361
  def text_document_did_open(message)
380
- @mutex.synchronize do
362
+ @global_state.synchronize do
381
363
  text_document = message.dig(:params, :textDocument)
382
364
  language_id = case text_document[:languageId]
383
365
  when "erb", "eruby"
@@ -392,7 +374,6 @@ module RubyLsp
392
374
  uri: text_document[:uri],
393
375
  source: text_document[:text],
394
376
  version: text_document[:version],
395
- encoding: @global_state.encoding,
396
377
  language_id: language_id,
397
378
  )
398
379
 
@@ -417,17 +398,12 @@ module RubyLsp
417
398
 
418
399
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
419
400
  def text_document_did_close(message)
420
- @mutex.synchronize do
401
+ @global_state.synchronize do
421
402
  uri = message.dig(:params, :textDocument, :uri)
422
403
  @store.delete(uri)
423
404
 
424
405
  # Clear diagnostics for the closed file, so that they no longer appear in the problems tab
425
- send_message(
426
- Notification.new(
427
- method: "textDocument/publishDiagnostics",
428
- params: Interface::PublishDiagnosticsParams.new(uri: uri.to_s, diagnostics: []),
429
- ),
430
- )
406
+ send_message(Notification.publish_diagnostics(uri.to_s, []))
431
407
  end
432
408
  end
433
409
 
@@ -436,7 +412,7 @@ module RubyLsp
436
412
  params = message[:params]
437
413
  text_document = params[:textDocument]
438
414
 
439
- @mutex.synchronize do
415
+ @global_state.synchronize do
440
416
  @store.push_edits(uri: text_document[:uri], edits: params[:contentChanges], version: text_document[:version])
441
417
  end
442
418
  end
@@ -493,7 +469,22 @@ module RubyLsp
493
469
  document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
494
470
  code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
495
471
  inlay_hint = Requests::InlayHints.new(document, T.must(@store.features_configuration.dig(:inlayHint)), dispatcher)
496
- dispatcher.dispatch(parse_result.value)
472
+
473
+ if document.is_a?(RubyDocument) && document.last_edit_may_change_declarations?
474
+ # Re-index the file as it is modified. This mode of indexing updates entries only. Require path trees are only
475
+ # updated on save
476
+ @global_state.synchronize do
477
+ send_log_message("Detected that last edit may have modified declarations. Re-indexing #{uri}")
478
+
479
+ @global_state.index.handle_change(uri) do |index|
480
+ index.delete(uri, skip_require_paths_tree: true)
481
+ RubyIndexer::DeclarationListener.new(index, dispatcher, parse_result, uri, collect_comments: true)
482
+ dispatcher.dispatch(parse_result.value)
483
+ end
484
+ end
485
+ else
486
+ dispatcher.dispatch(parse_result.value)
487
+ end
497
488
 
498
489
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
499
490
  # we actually received
@@ -1011,26 +1002,55 @@ module RubyLsp
1011
1002
  uri = URI(change[:uri])
1012
1003
  file_path = uri.to_standardized_path
1013
1004
  next if file_path.nil? || File.directory?(file_path)
1014
- next unless file_path.end_with?(".rb")
1015
1005
 
1016
- load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) }
1017
- uri.add_require_path_from_load_entry(load_path_entry) if load_path_entry
1006
+ if file_path.end_with?(".rb")
1007
+ handle_ruby_file_change(index, file_path, change[:type])
1008
+ next
1009
+ end
1018
1010
 
1019
- content = File.read(file_path)
1011
+ file_name = File.basename(file_path)
1020
1012
 
1021
- case change[:type]
1022
- when Constant::FileChangeType::CREATED
1023
- index.index_single(uri, content)
1024
- when Constant::FileChangeType::CHANGED
1025
- index.handle_change(uri, content)
1026
- when Constant::FileChangeType::DELETED
1027
- index.delete(uri)
1013
+ if file_name == ".rubocop.yml" || file_name == ".rubocop"
1014
+ handle_rubocop_config_change(uri)
1028
1015
  end
1029
1016
  end
1030
1017
 
1031
1018
  Addon.file_watcher_addons.each { |addon| T.unsafe(addon).workspace_did_change_watched_files(changes) }
1032
1019
  end
1033
1020
 
1021
+ sig { params(index: RubyIndexer::Index, file_path: String, change_type: Integer).void }
1022
+ def handle_ruby_file_change(index, file_path, change_type)
1023
+ load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) }
1024
+ uri = URI::Generic.from_path(load_path_entry: load_path_entry, path: file_path)
1025
+
1026
+ content = File.read(file_path)
1027
+
1028
+ case change_type
1029
+ when Constant::FileChangeType::CREATED
1030
+ index.index_single(uri, content)
1031
+ when Constant::FileChangeType::CHANGED
1032
+ index.handle_change(uri, content)
1033
+ when Constant::FileChangeType::DELETED
1034
+ index.delete(uri)
1035
+ end
1036
+ end
1037
+
1038
+ sig { params(uri: URI::Generic).void }
1039
+ def handle_rubocop_config_change(uri)
1040
+ return unless defined?(Requests::Support::RuboCopFormatter)
1041
+
1042
+ send_log_message("Reloading RuboCop since #{uri} changed")
1043
+ @global_state.register_formatter("rubocop", Requests::Support::RuboCopFormatter.new)
1044
+
1045
+ # Clear all existing diagnostics since the config changed. This has to happen under a mutex because the `state`
1046
+ # hash cannot be mutated during iteration or that will throw an error
1047
+ @global_state.synchronize do
1048
+ @store.each do |uri, _document|
1049
+ send_message(Notification.publish_diagnostics(uri.to_s, []))
1050
+ end
1051
+ end
1052
+ end
1053
+
1034
1054
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
1035
1055
  def workspace_symbol(message)
1036
1056
  send_message(
@@ -1240,10 +1260,10 @@ module RubyLsp
1240
1260
  return
1241
1261
  end
1242
1262
 
1243
- return unless indexing_options
1244
-
1245
1263
  configuration = @global_state.index.configuration
1246
1264
  configuration.workspace_path = @global_state.workspace_path
1265
+ return unless indexing_options
1266
+
1247
1267
  # The index expects snake case configurations, but VS Code standardizes on camel case settings
1248
1268
  configuration.apply_config(indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase })
1249
1269
  end
@@ -13,8 +13,9 @@ module RubyLsp
13
13
  sig { returns(String) }
14
14
  attr_accessor :client_name
15
15
 
16
- sig { void }
17
- def initialize
16
+ sig { params(global_state: GlobalState).void }
17
+ def initialize(global_state)
18
+ @global_state = global_state
18
19
  @state = T.let({}, T::Hash[String, Document[T.untyped]])
19
20
  @features_configuration = T.let(
20
21
  {
@@ -61,17 +62,16 @@ module RubyLsp
61
62
  source: String,
62
63
  version: Integer,
63
64
  language_id: Document::LanguageId,
64
- encoding: Encoding,
65
65
  ).returns(Document[T.untyped])
66
66
  end
67
- def set(uri:, source:, version:, language_id:, encoding: Encoding::UTF_8)
67
+ def set(uri:, source:, version:, language_id:)
68
68
  @state[uri.to_s] = case language_id
69
69
  when Document::LanguageId::ERB
70
- ERBDocument.new(source: source, version: version, uri: uri, encoding: encoding)
70
+ ERBDocument.new(source: source, version: version, uri: uri, global_state: @global_state)
71
71
  when Document::LanguageId::RBS
72
- RBSDocument.new(source: source, version: version, uri: uri, encoding: encoding)
72
+ RBSDocument.new(source: source, version: version, uri: uri, global_state: @global_state)
73
73
  else
74
- RubyDocument.new(source: source, version: version, uri: uri, encoding: encoding)
74
+ RubyDocument.new(source: source, version: version, uri: uri, global_state: @global_state)
75
75
  end
76
76
  end
77
77
 
@@ -91,29 +91,45 @@ module RubyLsp
91
91
  return Type.new("#{last}::<Class:#{last}>") if parts.empty?
92
92
 
93
93
  Type.new("#{parts.join("::")}::#{last}::<Class:#{last}>")
94
- else
94
+ when Prism::CallNode
95
+ raw_receiver = receiver.message
95
96
 
96
- raw_receiver = if receiver.is_a?(Prism::CallNode)
97
- receiver.message
98
- else
99
- receiver.slice
100
- end
97
+ if raw_receiver == "new"
98
+ # When invoking `new`, we recursively infer the type of the receiver to get the class type its being invoked
99
+ # on and then return the attached version of that type, since it's being instantiated.
100
+ type = infer_receiver_for_call_node(receiver, node_context)
101
101
 
102
- if raw_receiver
103
- guessed_name = raw_receiver
104
- .delete_prefix("@")
105
- .delete_prefix("@@")
106
- .split("_")
107
- .map(&:capitalize)
108
- .join
109
-
110
- entries = @index.resolve(guessed_name, node_context.nesting) || @index.first_unqualified_const(guessed_name)
111
- name = entries&.first&.name
112
- GuessedType.new(name) if name
102
+ return unless type
103
+
104
+ # If the method `new` was overridden, then we cannot assume that it will return a new instance of the class
105
+ new_method = @index.resolve_method("new", type.name)&.first
106
+ return if new_method && new_method.owner&.name != "Class"
107
+
108
+ type.attached
109
+ elsif raw_receiver
110
+ guess_type(raw_receiver, node_context.nesting)
113
111
  end
112
+ else
113
+ guess_type(receiver.slice, node_context.nesting)
114
114
  end
115
115
  end
116
116
 
117
+ sig { params(raw_receiver: String, nesting: T::Array[String]).returns(T.nilable(GuessedType)) }
118
+ def guess_type(raw_receiver, nesting)
119
+ guessed_name = raw_receiver
120
+ .delete_prefix("@")
121
+ .delete_prefix("@@")
122
+ .split("_")
123
+ .map(&:capitalize)
124
+ .join
125
+
126
+ entries = @index.resolve(guessed_name, nesting) || @index.first_unqualified_const(guessed_name)
127
+ name = entries&.first&.name
128
+ return unless name
129
+
130
+ GuessedType.new(name)
131
+ end
132
+
117
133
  sig { params(node_context: NodeContext).returns(Type) }
118
134
  def self_receiver_handling(node_context)
119
135
  nesting = node_context.nesting
@@ -176,6 +192,12 @@ module RubyLsp
176
192
  def initialize(name)
177
193
  @name = name
178
194
  end
195
+
196
+ # Returns the attached version of this type by removing the `<Class:...>` part from its name
197
+ sig { returns(Type) }
198
+ def attached
199
+ Type.new(T.must(@name.split("::")[..-2]).join("::"))
200
+ end
179
201
  end
180
202
 
181
203
  # A type that was guessed based on the receiver raw name
@@ -142,6 +142,20 @@ module RubyLsp
142
142
  ),
143
143
  )
144
144
  end
145
+
146
+ sig do
147
+ params(
148
+ uri: String,
149
+ diagnostics: T::Array[Interface::Diagnostic],
150
+ version: T.nilable(Integer),
151
+ ).returns(Notification)
152
+ end
153
+ def publish_diagnostics(uri, diagnostics, version: nil)
154
+ new(
155
+ method: "textDocument/publishDiagnostics",
156
+ params: Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: diagnostics, version: version),
157
+ )
158
+ end
145
159
  end
146
160
 
147
161
  extend T::Sig
@@ -155,6 +169,35 @@ module RubyLsp
155
169
  class Request < Message
156
170
  extend T::Sig
157
171
 
172
+ class << self
173
+ extend T::Sig
174
+
175
+ sig { params(id: Integer, pattern: T.any(Interface::RelativePattern, String), kind: Integer).returns(Request) }
176
+ def register_watched_files(
177
+ id,
178
+ pattern,
179
+ kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE
180
+ )
181
+ new(
182
+ id: id,
183
+ method: "client/registerCapability",
184
+ params: Interface::RegistrationParams.new(
185
+ registrations: [
186
+ Interface::Registration.new(
187
+ id: "workspace/didChangeWatchedFiles",
188
+ method: "workspace/didChangeWatchedFiles",
189
+ register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
190
+ watchers: [
191
+ Interface::FileSystemWatcher.new(glob_pattern: pattern, kind: kind),
192
+ ],
193
+ ),
194
+ ),
195
+ ],
196
+ ),
197
+ )
198
+ end
199
+ end
200
+
158
201
  sig { params(id: T.any(Integer, String), method: String, params: Object).void }
159
202
  def initialize(id:, method:, params:)
160
203
  @id = id
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.1
4
+ version: 0.23.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-01-06 00:00:00.000000000 Z
10
+ date: 2025-01-10 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: language_server-protocol
@@ -106,6 +106,7 @@ files:
106
106
  - lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb
107
107
  - lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb
108
108
  - lib/ruby_indexer/lib/ruby_indexer/uri.rb
109
+ - lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb
109
110
  - lib/ruby_indexer/ruby_indexer.rb
110
111
  - lib/ruby_indexer/test/class_variables_test.rb
111
112
  - lib/ruby_indexer/test/classes_and_modules_test.rb