ruby-lsp 0.17.4 → 0.17.13

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +26 -1
  5. data/exe/ruby-lsp-check +1 -1
  6. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +74 -43
  7. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +26 -0
  8. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +147 -29
  9. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +383 -79
  10. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +195 -61
  11. data/lib/ruby_indexer/ruby_indexer.rb +1 -8
  12. data/lib/ruby_indexer/test/classes_and_modules_test.rb +71 -3
  13. data/lib/ruby_indexer/test/configuration_test.rb +1 -1
  14. data/lib/ruby_indexer/test/constant_test.rb +17 -17
  15. data/lib/ruby_indexer/test/enhancements_test.rb +197 -0
  16. data/lib/ruby_indexer/test/index_test.rb +367 -17
  17. data/lib/ruby_indexer/test/method_test.rb +58 -25
  18. data/lib/ruby_indexer/test/rbs_indexer_test.rb +297 -0
  19. data/lib/ruby_indexer/test/test_case.rb +1 -5
  20. data/lib/ruby_lsp/addon.rb +22 -5
  21. data/lib/ruby_lsp/base_server.rb +8 -3
  22. data/lib/ruby_lsp/document.rb +27 -46
  23. data/lib/ruby_lsp/erb_document.rb +125 -0
  24. data/lib/ruby_lsp/global_state.rb +47 -19
  25. data/lib/ruby_lsp/internal.rb +2 -0
  26. data/lib/ruby_lsp/listeners/completion.rb +161 -57
  27. data/lib/ruby_lsp/listeners/definition.rb +91 -27
  28. data/lib/ruby_lsp/listeners/document_highlight.rb +5 -1
  29. data/lib/ruby_lsp/listeners/hover.rb +61 -19
  30. data/lib/ruby_lsp/listeners/signature_help.rb +13 -6
  31. data/lib/ruby_lsp/node_context.rb +65 -5
  32. data/lib/ruby_lsp/requests/code_action_resolve.rb +107 -9
  33. data/lib/ruby_lsp/requests/code_actions.rb +11 -2
  34. data/lib/ruby_lsp/requests/completion.rb +4 -4
  35. data/lib/ruby_lsp/requests/completion_resolve.rb +14 -9
  36. data/lib/ruby_lsp/requests/definition.rb +18 -8
  37. data/lib/ruby_lsp/requests/diagnostics.rb +6 -5
  38. data/lib/ruby_lsp/requests/document_symbol.rb +2 -7
  39. data/lib/ruby_lsp/requests/folding_ranges.rb +6 -2
  40. data/lib/ruby_lsp/requests/formatting.rb +15 -0
  41. data/lib/ruby_lsp/requests/hover.rb +5 -5
  42. data/lib/ruby_lsp/requests/on_type_formatting.rb +6 -4
  43. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  44. data/lib/ruby_lsp/requests/show_syntax_tree.rb +3 -2
  45. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  46. data/lib/ruby_lsp/requests/support/common.rb +11 -2
  47. data/lib/ruby_lsp/requests/type_hierarchy_supertypes.rb +2 -6
  48. data/lib/ruby_lsp/ruby_document.rb +74 -0
  49. data/lib/ruby_lsp/server.rb +129 -54
  50. data/lib/ruby_lsp/store.rb +33 -9
  51. data/lib/ruby_lsp/test_helper.rb +3 -1
  52. data/lib/ruby_lsp/type_inferrer.rb +61 -25
  53. data/lib/ruby_lsp/utils.rb +13 -0
  54. metadata +9 -8
  55. data/exe/ruby-lsp-doctor +0 -23
@@ -19,10 +19,10 @@ module RubyLsp
19
19
  def process_message(message)
20
20
  case message[:method]
21
21
  when "initialize"
22
- $stderr.puts("Initializing Ruby LSP v#{VERSION}...")
22
+ send_log_message("Initializing Ruby LSP v#{VERSION}...")
23
23
  run_initialize(message)
24
24
  when "initialized"
25
- $stderr.puts("Finished initializing Ruby LSP!") unless @test_mode
25
+ send_log_message("Finished initializing Ruby LSP!") unless @test_mode
26
26
  run_initialized
27
27
  when "textDocument/didOpen"
28
28
  text_document_did_open(message)
@@ -98,15 +98,43 @@ module RubyLsp
98
98
  rescue StandardError, LoadError => e
99
99
  # If an error occurred in a request, we have to return an error response or else the editor will hang
100
100
  if message[:id]
101
- send_message(Error.new(id: message[:id], code: Constant::ErrorCodes::INTERNAL_ERROR, message: e.full_message))
101
+ # If a document is deleted before we are able to process all of its enqueued requests, we will try to read it
102
+ # from disk and it raise this error. This is expected, so we don't include the `data` attribute to avoid
103
+ # reporting these to our telemetry
104
+ if e.is_a?(Store::NonExistingDocumentError)
105
+ send_message(Error.new(
106
+ id: message[:id],
107
+ code: Constant::ErrorCodes::INVALID_PARAMS,
108
+ message: e.full_message,
109
+ ))
110
+ else
111
+ send_message(Error.new(
112
+ id: message[:id],
113
+ code: Constant::ErrorCodes::INTERNAL_ERROR,
114
+ message: e.full_message,
115
+ data: {
116
+ errorClass: e.class.name,
117
+ errorMessage: e.message,
118
+ backtrace: e.backtrace&.join("\n"),
119
+ },
120
+ ))
121
+ end
102
122
  end
103
123
 
104
- $stderr.puts("Error processing #{message[:method]}: #{e.full_message}")
124
+ send_log_message("Error processing #{message[:method]}: #{e.full_message}", type: Constant::MessageType::ERROR)
105
125
  end
106
126
 
107
127
  sig { void }
108
128
  def load_addons
109
- Addon.load_addons(@global_state, @outgoing_queue)
129
+ errors = Addon.load_addons(@global_state, @outgoing_queue)
130
+
131
+ if errors.any?
132
+ send_log_message(
133
+ "Error loading addons:\n\n#{errors.map(&:full_message).join("\n\n")}",
134
+ type: Constant::MessageType::WARNING,
135
+ )
136
+ end
137
+
110
138
  errored_addons = Addon.addons.select(&:error?)
111
139
 
112
140
  if errored_addons.any?
@@ -120,7 +148,12 @@ module RubyLsp
120
148
  ),
121
149
  )
122
150
 
123
- $stderr.puts(errored_addons.map(&:errors_details).join("\n\n")) unless @test_mode
151
+ unless @test_mode
152
+ send_log_message(
153
+ errored_addons.map(&:errors_details).join("\n\n"),
154
+ type: Constant::MessageType::WARNING,
155
+ )
156
+ end
124
157
  end
125
158
  end
126
159
 
@@ -129,7 +162,7 @@ module RubyLsp
129
162
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
130
163
  def run_initialize(message)
131
164
  options = message[:params]
132
- @global_state.apply_options(options)
165
+ global_state_notifications = @global_state.apply_options(options)
133
166
 
134
167
  client_name = options.dig(:clientInfo, :name)
135
168
  @store.client_name = client_name if client_name
@@ -137,7 +170,6 @@ module RubyLsp
137
170
  progress = options.dig(:capabilities, :window, :workDoneProgress)
138
171
  @store.supports_progress = progress.nil? ? true : progress
139
172
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
140
- @store.experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
141
173
 
142
174
  configured_hints = options.dig(:initializationOptions, :featuresConfiguration, :inlayHint)
143
175
  T.must(@store.features_configuration.dig(:inlayHint)).configuration.merge!(configured_hints) if configured_hints
@@ -162,6 +194,7 @@ module RubyLsp
162
194
  hover_provider = Requests::Hover.provider if enabled_features["hover"]
163
195
  folding_ranges_provider = Requests::FoldingRanges.provider if enabled_features["foldingRanges"]
164
196
  semantic_tokens_provider = Requests::SemanticHighlighting.provider if enabled_features["semanticHighlighting"]
197
+ document_formatting_provider = Requests::Formatting.provider if enabled_features["formatting"]
165
198
  diagnostics_provider = Requests::Diagnostics.provider if enabled_features["diagnostics"]
166
199
  on_type_formatting_provider = Requests::OnTypeFormatting.provider if enabled_features["onTypeFormatting"]
167
200
  code_action_provider = Requests::CodeActions.provider if enabled_features["codeActions"]
@@ -183,7 +216,7 @@ module RubyLsp
183
216
  document_link_provider: document_link_provider,
184
217
  folding_range_provider: folding_ranges_provider,
185
218
  semantic_tokens_provider: semantic_tokens_provider,
186
- document_formatting_provider: enabled_features["formatting"] && @global_state.formatter != "none",
219
+ document_formatting_provider: document_formatting_provider && @global_state.formatter != "none",
187
220
  document_highlight_provider: enabled_features["documentHighlights"],
188
221
  code_action_provider: code_action_provider,
189
222
  document_on_type_formatting_provider: on_type_formatting_provider,
@@ -235,7 +268,11 @@ module RubyLsp
235
268
  )
236
269
  end
237
270
 
271
+ process_indexing_configuration(options.dig(:initializationOptions, :indexing))
272
+
238
273
  begin_progress("indexing-progress", "Ruby LSP: indexing files")
274
+
275
+ global_state_notifications.each { |notification| send_message(notification) }
239
276
  end
240
277
 
241
278
  sig { void }
@@ -243,28 +280,6 @@ module RubyLsp
243
280
  load_addons
244
281
  RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
245
282
 
246
- indexing_config = {}
247
-
248
- # Need to use the workspace URI, otherwise, this will fail for people working on a project that is a symlink.
249
- index_path = File.join(@global_state.workspace_path, ".index.yml")
250
-
251
- if File.exist?(index_path)
252
- begin
253
- indexing_config = YAML.parse_file(index_path).to_ruby
254
- rescue Psych::SyntaxError => e
255
- message = "Syntax error while loading configuration: #{e.message}"
256
- send_message(
257
- Notification.new(
258
- method: "window/showMessage",
259
- params: Interface::ShowMessageParams.new(
260
- type: Constant::MessageType::WARNING,
261
- message: message,
262
- ),
263
- ),
264
- )
265
- end
266
- end
267
-
268
283
  if defined?(Requests::Support::RuboCopFormatter)
269
284
  @global_state.register_formatter("rubocop", Requests::Support::RuboCopFormatter.new)
270
285
  end
@@ -272,7 +287,7 @@ module RubyLsp
272
287
  @global_state.register_formatter("syntax_tree", Requests::Support::SyntaxTreeFormatter.new)
273
288
  end
274
289
 
275
- perform_initial_indexing(indexing_config)
290
+ perform_initial_indexing
276
291
  check_formatter_is_available
277
292
  end
278
293
 
@@ -280,11 +295,18 @@ module RubyLsp
280
295
  def text_document_did_open(message)
281
296
  @mutex.synchronize do
282
297
  text_document = message.dig(:params, :textDocument)
298
+ language_id = case text_document[:languageId]
299
+ when "erb", "eruby"
300
+ Document::LanguageId::ERB
301
+ else
302
+ Document::LanguageId::Ruby
303
+ end
283
304
  @store.set(
284
305
  uri: text_document[:uri],
285
306
  source: text_document[:text],
286
307
  version: text_document[:version],
287
308
  encoding: @global_state.encoding,
309
+ language_id: language_id,
288
310
  )
289
311
  end
290
312
  end
@@ -348,15 +370,17 @@ module RubyLsp
348
370
  return
349
371
  end
350
372
 
373
+ parse_result = document.parse_result
374
+
351
375
  # Run requests for the document
352
376
  dispatcher = Prism::Dispatcher.new
353
- folding_range = Requests::FoldingRanges.new(document.parse_result.comments, dispatcher)
377
+ folding_range = Requests::FoldingRanges.new(parse_result.comments, dispatcher)
354
378
  document_symbol = Requests::DocumentSymbol.new(uri, dispatcher)
355
- document_link = Requests::DocumentLink.new(uri, document.comments, dispatcher)
379
+ document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
356
380
  code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
357
381
 
358
382
  semantic_highlighting = Requests::SemanticHighlighting.new(@global_state, dispatcher)
359
- dispatcher.dispatch(document.tree)
383
+ dispatcher.dispatch(parse_result.value)
360
384
 
361
385
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
362
386
  # we actually received
@@ -388,7 +412,7 @@ module RubyLsp
388
412
 
389
413
  dispatcher = Prism::Dispatcher.new
390
414
  request = Requests::SemanticHighlighting.new(@global_state, dispatcher, range: start_line..end_line)
391
- dispatcher.visit(document.tree)
415
+ dispatcher.visit(document.parse_result.value)
392
416
 
393
417
  response = request.perform
394
418
  send_message(Result.new(id: message[:id], response: response))
@@ -411,7 +435,9 @@ module RubyLsp
411
435
  return
412
436
  end
413
437
 
414
- response = Requests::Formatting.new(@global_state, @store.get(uri)).perform
438
+ document = @store.get(uri)
439
+
440
+ response = Requests::Formatting.new(@global_state, document).perform
415
441
  send_message(Result.new(id: message[:id], response: response))
416
442
  rescue Requests::Request::InvalidFormatter => error
417
443
  send_message(Notification.window_show_error("Configuration error: #{error.message}"))
@@ -427,19 +453,20 @@ module RubyLsp
427
453
  dispatcher = Prism::Dispatcher.new
428
454
  document = @store.get(params.dig(:textDocument, :uri))
429
455
  request = Requests::DocumentHighlight.new(document, params[:position], dispatcher)
430
- dispatcher.dispatch(document.tree)
456
+ dispatcher.dispatch(document.parse_result.value)
431
457
  send_message(Result.new(id: message[:id], response: request.perform))
432
458
  end
433
459
 
434
460
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
435
461
  def text_document_on_type_formatting(message)
436
462
  params = message[:params]
463
+ document = @store.get(params.dig(:textDocument, :uri))
437
464
 
438
465
  send_message(
439
466
  Result.new(
440
467
  id: message[:id],
441
468
  response: Requests::OnTypeFormatting.new(
442
- @store.get(params.dig(:textDocument, :uri)),
469
+ document,
443
470
  params[:position],
444
471
  params[:ch],
445
472
  @store.client_name,
@@ -462,15 +489,18 @@ module RubyLsp
462
489
  @global_state,
463
490
  params[:position],
464
491
  dispatcher,
465
- typechecker_enabled?(document),
492
+ sorbet_level(document),
466
493
  ).perform,
467
494
  ),
468
495
  )
469
496
  end
470
497
 
471
- sig { params(document: Document).returns(T::Boolean) }
472
- def typechecker_enabled?(document)
473
- @global_state.has_type_checker && document.sorbet_sigil_is_true_or_higher
498
+ sig { params(document: Document).returns(RubyDocument::SorbetLevel) }
499
+ def sorbet_level(document)
500
+ return RubyDocument::SorbetLevel::Ignore unless @global_state.has_type_checker
501
+ return RubyDocument::SorbetLevel::Ignore unless document.is_a?(RubyDocument)
502
+
503
+ document.sorbet_level
474
504
  end
475
505
 
476
506
  sig { params(message: T::Hash[Symbol, T.untyped]).void }
@@ -480,7 +510,7 @@ module RubyLsp
480
510
  dispatcher = Prism::Dispatcher.new
481
511
  document = @store.get(params.dig(:textDocument, :uri))
482
512
  request = Requests::InlayHints.new(document, params[:range], hints_configurations, dispatcher)
483
- dispatcher.visit(document.tree)
513
+ dispatcher.visit(document.parse_result.value)
484
514
  send_message(Result.new(id: message[:id], response: request.perform))
485
515
  end
486
516
 
@@ -506,6 +536,12 @@ module RubyLsp
506
536
  params = message[:params]
507
537
  uri = URI(params.dig(:data, :uri))
508
538
  document = @store.get(uri)
539
+
540
+ unless document.is_a?(RubyDocument)
541
+ send_message(Notification.window_show_error("Code actions are currently only available for Ruby documents"))
542
+ raise Requests::CodeActionResolve::CodeActionError
543
+ end
544
+
509
545
  result = Requests::CodeActionResolve.new(document, params).perform
510
546
 
511
547
  case result
@@ -542,7 +578,9 @@ module RubyLsp
542
578
  return
543
579
  end
544
580
 
545
- response = @store.cache_fetch(uri, "textDocument/diagnostic") do |document|
581
+ document = @store.get(uri)
582
+
583
+ response = document.cache_fetch("textDocument/diagnostic") do |document|
546
584
  Requests::Diagnostics.new(@global_state, document).perform
547
585
  end
548
586
 
@@ -573,7 +611,7 @@ module RubyLsp
573
611
  document,
574
612
  @global_state,
575
613
  params,
576
- typechecker_enabled?(document),
614
+ sorbet_level(document),
577
615
  dispatcher,
578
616
  ).perform,
579
617
  ),
@@ -603,7 +641,7 @@ module RubyLsp
603
641
  params[:position],
604
642
  params[:context],
605
643
  dispatcher,
606
- typechecker_enabled?(document),
644
+ sorbet_level(document),
607
645
  ).perform,
608
646
  ),
609
647
  )
@@ -623,7 +661,7 @@ module RubyLsp
623
661
  @global_state,
624
662
  params[:position],
625
663
  dispatcher,
626
- typechecker_enabled?(document),
664
+ sorbet_level(document),
627
665
  ).perform,
628
666
  ),
629
667
  )
@@ -736,16 +774,12 @@ module RubyLsp
736
774
  Addon.addons.each(&:deactivate)
737
775
  end
738
776
 
739
- sig { params(config_hash: T::Hash[String, T.untyped]).void }
740
- def perform_initial_indexing(config_hash)
777
+ sig { void }
778
+ def perform_initial_indexing
741
779
  # The begin progress invocation happens during `initialize`, so that the notification is sent before we are
742
780
  # stuck indexing files
743
- RubyIndexer.configuration.apply_config(config_hash)
744
-
745
781
  Thread.new do
746
782
  begin
747
- RubyIndexer::RBSIndexer.new(@global_state.index).index_ruby_core
748
-
749
783
  @global_state.index.index_all do |percentage|
750
784
  progress("indexing-progress", percentage)
751
785
  true
@@ -842,5 +876,46 @@ module RubyLsp
842
876
  )
843
877
  end
844
878
  end
879
+
880
+ sig { params(indexing_options: T.nilable(T::Hash[Symbol, T.untyped])).void }
881
+ def process_indexing_configuration(indexing_options)
882
+ # Need to use the workspace URI, otherwise, this will fail for people working on a project that is a symlink.
883
+ index_path = File.join(@global_state.workspace_path, ".index.yml")
884
+
885
+ if File.exist?(index_path)
886
+ begin
887
+ @global_state.index.configuration.apply_config(YAML.parse_file(index_path).to_ruby)
888
+ send_message(
889
+ Notification.new(
890
+ method: "window/showMessage",
891
+ params: Interface::ShowMessageParams.new(
892
+ type: Constant::MessageType::WARNING,
893
+ message: "The .index.yml configuration file is deprecated. " \
894
+ "Please use editor settings to configure the index",
895
+ ),
896
+ ),
897
+ )
898
+ rescue Psych::SyntaxError => e
899
+ message = "Syntax error while loading configuration: #{e.message}"
900
+ send_message(
901
+ Notification.new(
902
+ method: "window/showMessage",
903
+ params: Interface::ShowMessageParams.new(
904
+ type: Constant::MessageType::WARNING,
905
+ message: message,
906
+ ),
907
+ ),
908
+ )
909
+ end
910
+ return
911
+ end
912
+
913
+ return unless indexing_options
914
+
915
+ # The index expects snake case configurations, but VS Code standardizes on camel case settings
916
+ @global_state.index.configuration.apply_config(
917
+ indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase },
918
+ )
919
+ end
845
920
  end
846
921
  end
@@ -5,11 +5,10 @@ module RubyLsp
5
5
  class Store
6
6
  extend T::Sig
7
7
 
8
- sig { returns(T::Boolean) }
9
- attr_accessor :supports_progress
8
+ class NonExistingDocumentError < StandardError; end
10
9
 
11
10
  sig { returns(T::Boolean) }
12
- attr_accessor :experimental_features
11
+ attr_accessor :supports_progress
13
12
 
14
13
  sig { returns(T::Hash[Symbol, RequestConfig]) }
15
14
  attr_accessor :features_configuration
@@ -21,7 +20,6 @@ module RubyLsp
21
20
  def initialize
22
21
  @state = T.let({}, T::Hash[String, Document])
23
22
  @supports_progress = T.let(true, T::Boolean)
24
- @experimental_features = T.let(false, T::Boolean)
25
23
  @features_configuration = T.let(
26
24
  {
27
25
  inlayHint: RequestConfig.new({
@@ -40,14 +38,40 @@ module RubyLsp
40
38
  document = @state[uri.to_s]
41
39
  return document unless document.nil?
42
40
 
43
- path = T.must(uri.to_standardized_path)
44
- set(uri: uri, source: File.binread(path), version: 0)
41
+ # For unsaved files (`untitled:Untitled-1` uris), there's no path to read from. If we don't have the untitled file
42
+ # already present in the store, then we have to raise non existing document error
43
+ path = uri.to_standardized_path
44
+ raise NonExistingDocumentError, uri.to_s unless path
45
+
46
+ ext = File.extname(path)
47
+ language_id = if ext == ".erb" || ext == ".rhtml"
48
+ Document::LanguageId::ERB
49
+ else
50
+ Document::LanguageId::Ruby
51
+ end
52
+
53
+ set(uri: uri, source: File.binread(path), version: 0, language_id: language_id)
45
54
  T.must(@state[uri.to_s])
55
+ rescue Errno::ENOENT
56
+ raise NonExistingDocumentError, uri.to_s
46
57
  end
47
58
 
48
- sig { params(uri: URI::Generic, source: String, version: Integer, encoding: Encoding).void }
49
- def set(uri:, source:, version:, encoding: Encoding::UTF_8)
50
- document = RubyDocument.new(source: source, version: version, uri: uri, encoding: encoding)
59
+ sig do
60
+ params(
61
+ uri: URI::Generic,
62
+ source: String,
63
+ version: Integer,
64
+ language_id: Document::LanguageId,
65
+ encoding: Encoding,
66
+ ).void
67
+ end
68
+ def set(uri:, source:, version:, language_id:, encoding: Encoding::UTF_8)
69
+ document = case language_id
70
+ when Document::LanguageId::ERB
71
+ ERBDocument.new(source: source, version: version, uri: uri, encoding: encoding)
72
+ else
73
+ RubyDocument.new(source: source, version: version, uri: uri, encoding: encoding)
74
+ end
51
75
  @state[uri.to_s] = document
52
76
  end
53
77
 
@@ -21,7 +21,8 @@ module RubyLsp
21
21
  &block)
22
22
  server = RubyLsp::Server.new(test_mode: true)
23
23
  server.global_state.stubs(:has_type_checker).returns(false) if stub_no_typechecker
24
- server.global_state.apply_options({})
24
+ server.global_state.apply_options({ initializationOptions: { experimentalFeaturesEnabled: true } })
25
+ language_id = uri.to_s.end_with?(".erb") ? "erb" : "ruby"
25
26
 
26
27
  if source
27
28
  server.process_message({
@@ -31,6 +32,7 @@ module RubyLsp
31
32
  uri: uri,
32
33
  text: source,
33
34
  version: 1,
35
+ languageId: language_id,
34
36
  },
35
37
  },
36
38
  })
@@ -7,12 +7,16 @@ module RubyLsp
7
7
  class TypeInferrer
8
8
  extend T::Sig
9
9
 
10
- sig { params(index: RubyIndexer::Index).void }
11
- def initialize(index)
10
+ sig { params(experimental_features: T::Boolean).returns(T::Boolean) }
11
+ attr_writer :experimental_features
12
+
13
+ sig { params(index: RubyIndexer::Index, experimental_features: T::Boolean).void }
14
+ def initialize(index, experimental_features = true)
12
15
  @index = index
16
+ @experimental_features = experimental_features
13
17
  end
14
18
 
15
- sig { params(node_context: NodeContext).returns(T.nilable(String)) }
19
+ sig { params(node_context: NodeContext).returns(T.nilable(Type)) }
16
20
  def infer_receiver_type(node_context)
17
21
  node = node_context.node
18
22
 
@@ -20,36 +24,21 @@ module RubyLsp
20
24
  when Prism::CallNode
21
25
  infer_receiver_for_call_node(node, node_context)
22
26
  when Prism::InstanceVariableReadNode, Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableWriteNode,
23
- Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableTargetNode
24
- nesting = node_context.nesting
25
- # If we're at the top level, then the invocation is happening on `<main>`, which is a special singleton that
26
- # inherits from Object
27
- return "Object" if nesting.empty?
28
-
29
- fully_qualified_name = node_context.fully_qualified_name
30
- return fully_qualified_name if node_context.surrounding_method
31
-
32
- "#{fully_qualified_name}::<Class:#{nesting.last}>"
27
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableTargetNode,
28
+ Prism::SuperNode, Prism::ForwardingSuperNode
29
+ self_receiver_handling(node_context)
33
30
  end
34
31
  end
35
32
 
36
33
  private
37
34
 
38
- sig { params(node: Prism::CallNode, node_context: NodeContext).returns(T.nilable(String)) }
35
+ sig { params(node: Prism::CallNode, node_context: NodeContext).returns(T.nilable(Type)) }
39
36
  def infer_receiver_for_call_node(node, node_context)
40
37
  receiver = node.receiver
41
38
 
42
39
  case receiver
43
40
  when Prism::SelfNode, nil
44
- nesting = node_context.nesting
45
- # If we're at the top level, then the invocation is happening on `<main>`, which is a special singleton that
46
- # inherits from Object
47
- return "Object" if nesting.empty?
48
- return node_context.fully_qualified_name if node_context.surrounding_method
49
-
50
- # If we're not inside a method, then we're inside the body of a class or module, which is a singleton
51
- # context
52
- "#{nesting.join("::")}::<Class:#{nesting.last}>"
41
+ self_receiver_handling(node_context)
53
42
  when Prism::ConstantPathNode, Prism::ConstantReadNode
54
43
  # When the receiver is a constant reference, we have to try to resolve it to figure out the right
55
44
  # receiver. But since the invocation is directly on the constant, that's the singleton context of that
@@ -62,12 +51,42 @@ module RubyLsp
62
51
  return unless name
63
52
 
64
53
  *parts, last = name.split("::")
65
- return "#{last}::<Class:#{last}>" if parts.empty?
54
+ return Type.new("#{last}::<Class:#{last}>") if parts.empty?
55
+
56
+ Type.new("#{parts.join("::")}::#{last}::<Class:#{last}>")
57
+ else
58
+ return unless @experimental_features
66
59
 
67
- "#{parts.join("::")}::#{last}::<Class:#{last}>"
60
+ raw_receiver = node.receiver&.slice
61
+
62
+ if raw_receiver
63
+ guessed_name = raw_receiver
64
+ .delete_prefix("@")
65
+ .delete_prefix("@@")
66
+ .split("_")
67
+ .map(&:capitalize)
68
+ .join
69
+
70
+ entries = @index.resolve(guessed_name, node_context.nesting) || @index.first_unqualified_const(guessed_name)
71
+ name = entries&.first&.name
72
+ GuessedType.new(name) if name
73
+ end
68
74
  end
69
75
  end
70
76
 
77
+ sig { params(node_context: NodeContext).returns(Type) }
78
+ def self_receiver_handling(node_context)
79
+ nesting = node_context.nesting
80
+ # If we're at the top level, then the invocation is happening on `<main>`, which is a special singleton that
81
+ # inherits from Object
82
+ return Type.new("Object") if nesting.empty?
83
+ return Type.new(node_context.fully_qualified_name) if node_context.surrounding_method
84
+
85
+ # If we're not inside a method, then we're inside the body of a class or module, which is a singleton
86
+ # context
87
+ Type.new("#{nesting.join("::")}::<Class:#{nesting.last}>")
88
+ end
89
+
71
90
  sig do
72
91
  params(
73
92
  node: T.any(
@@ -82,5 +101,22 @@ module RubyLsp
82
101
  Prism::ConstantPathNode::MissingNodesInConstantPathError
83
102
  nil
84
103
  end
104
+
105
+ # A known type
106
+ class Type
107
+ extend T::Sig
108
+
109
+ sig { returns(String) }
110
+ attr_reader :name
111
+
112
+ sig { params(name: String).void }
113
+ def initialize(name)
114
+ @name = name
115
+ end
116
+ end
117
+
118
+ # A type that was guessed based on the receiver raw name
119
+ class GuessedType < Type
120
+ end
85
121
  end
86
122
  end
@@ -25,6 +25,7 @@ module RubyLsp
25
25
  end,
26
26
  String,
27
27
  )
28
+ GUESSED_TYPES_URL = "https://github.com/Shopify/ruby-lsp/blob/main/DESIGN_AND_ROADMAP.md#guessed-types"
28
29
 
29
30
  # A notification to be sent to the client
30
31
  class Message
@@ -52,6 +53,7 @@ module RubyLsp
52
53
  class Notification < Message
53
54
  class << self
54
55
  extend T::Sig
56
+
55
57
  sig { params(message: String).returns(Notification) }
56
58
  def window_show_error(message)
57
59
  new(
@@ -62,6 +64,14 @@ module RubyLsp
62
64
  ),
63
65
  )
64
66
  end
67
+
68
+ sig { params(message: String, type: Integer).returns(Notification) }
69
+ def window_log_message(message, type: Constant::MessageType::LOG)
70
+ new(
71
+ method: "window/logMessage",
72
+ params: Interface::LogMessageParams.new(type: type, message: message),
73
+ )
74
+ end
65
75
  end
66
76
 
67
77
  extend T::Sig
@@ -121,6 +131,9 @@ module RubyLsp
121
131
  sig { returns(T.untyped) }
122
132
  attr_reader :response
123
133
 
134
+ sig { returns(Integer) }
135
+ attr_reader :id
136
+
124
137
  sig { params(id: Integer, response: T.untyped).void }
125
138
  def initialize(id:, response:)
126
139
  @id = id