ruby-lsp 0.23.20 → 0.26.1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp +10 -4
  4. data/exe/ruby-lsp-check +0 -4
  5. data/exe/ruby-lsp-launcher +25 -11
  6. data/exe/ruby-lsp-test-exec +6 -0
  7. data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +0 -1
  8. data/lib/rubocop/cop/ruby_lsp/use_register_with_handler_method.rb +0 -1
  9. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +7 -1
  10. data/lib/ruby_indexer/lib/ruby_indexer/enhancement.rb +1 -4
  11. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +10 -19
  12. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +29 -7
  13. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +2 -2
  14. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +12 -8
  15. data/lib/ruby_indexer/test/configuration_test.rb +1 -2
  16. data/lib/ruby_indexer/test/index_test.rb +39 -0
  17. data/lib/ruby_indexer/test/instance_variables_test.rb +24 -0
  18. data/lib/ruby_indexer/test/method_test.rb +17 -0
  19. data/lib/ruby_indexer/test/rbs_indexer_test.rb +2 -2
  20. data/lib/ruby_indexer/test/reference_finder_test.rb +79 -14
  21. data/lib/ruby_lsp/addon.rb +44 -15
  22. data/lib/ruby_lsp/base_server.rb +34 -26
  23. data/lib/ruby_lsp/document.rb +162 -52
  24. data/lib/ruby_lsp/erb_document.rb +8 -3
  25. data/lib/ruby_lsp/global_state.rb +21 -0
  26. data/lib/ruby_lsp/internal.rb +0 -2
  27. data/lib/ruby_lsp/listeners/completion.rb +14 -3
  28. data/lib/ruby_lsp/listeners/hover.rb +7 -0
  29. data/lib/ruby_lsp/listeners/inlay_hints.rb +5 -3
  30. data/lib/ruby_lsp/listeners/spec_style.rb +126 -67
  31. data/lib/ruby_lsp/listeners/test_discovery.rb +18 -15
  32. data/lib/ruby_lsp/listeners/test_style.rb +56 -23
  33. data/lib/ruby_lsp/requests/code_action_resolve.rb +3 -3
  34. data/lib/ruby_lsp/requests/code_lens.rb +14 -5
  35. data/lib/ruby_lsp/requests/completion.rb +1 -1
  36. data/lib/ruby_lsp/requests/definition.rb +1 -1
  37. data/lib/ruby_lsp/requests/discover_tests.rb +2 -2
  38. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  39. data/lib/ruby_lsp/requests/hover.rb +1 -1
  40. data/lib/ruby_lsp/requests/inlay_hints.rb +3 -3
  41. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  42. data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
  43. data/lib/ruby_lsp/requests/references.rb +10 -6
  44. data/lib/ruby_lsp/requests/rename.rb +8 -6
  45. data/lib/ruby_lsp/requests/request.rb +6 -7
  46. data/lib/ruby_lsp/requests/selection_ranges.rb +1 -1
  47. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -1
  48. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  49. data/lib/ruby_lsp/requests/support/common.rb +1 -3
  50. data/lib/ruby_lsp/requests/support/formatter.rb +16 -15
  51. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
  52. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -3
  53. data/lib/ruby_lsp/response_builders/response_builder.rb +6 -8
  54. data/lib/ruby_lsp/ruby_document.rb +10 -5
  55. data/lib/ruby_lsp/server.rb +95 -110
  56. data/lib/ruby_lsp/setup_bundler.rb +59 -25
  57. data/lib/ruby_lsp/static_docs.rb +1 -0
  58. data/lib/ruby_lsp/store.rb +0 -10
  59. data/lib/ruby_lsp/test_helper.rb +1 -4
  60. data/lib/ruby_lsp/test_reporters/lsp_reporter.rb +18 -7
  61. data/lib/ruby_lsp/test_reporters/minitest_reporter.rb +54 -7
  62. data/lib/ruby_lsp/test_reporters/test_unit_reporter.rb +0 -1
  63. data/lib/ruby_lsp/utils.rb +47 -11
  64. data/static_docs/break.md +103 -0
  65. metadata +7 -19
  66. data/lib/ruby_lsp/load_sorbet.rb +0 -62
@@ -56,6 +56,17 @@ module RubyLsp
56
56
  @enabled_feature_flags = {} #: Hash[Symbol, bool]
57
57
  @mutex = Mutex.new #: Mutex
58
58
  @telemetry_machine_id = nil #: String?
59
+ @feature_configuration = {
60
+ inlayHint: RequestConfig.new({
61
+ enableAll: false,
62
+ implicitRescue: false,
63
+ implicitHashValue: false,
64
+ }),
65
+ codeLens: RequestConfig.new({
66
+ enableAll: false,
67
+ enableTestCodeLens: true,
68
+ }),
69
+ } #: Hash[Symbol, RequestConfig]
59
70
  end
60
71
 
61
72
  #: [T] { -> T } -> T
@@ -175,9 +186,19 @@ module RubyLsp
175
186
  @enabled_feature_flags = enabled_flags if enabled_flags
176
187
 
177
188
  @telemetry_machine_id = options.dig(:initializationOptions, :telemetryMachineId)
189
+
190
+ options.dig(:initializationOptions, :featuresConfiguration)&.each do |feature_name, config|
191
+ @feature_configuration[feature_name]&.merge!(config)
192
+ end
193
+
178
194
  notifications
179
195
  end
180
196
 
197
+ #: (Symbol) -> RequestConfig?
198
+ def feature_configuration(feature_name)
199
+ @feature_configuration[feature_name]
200
+ end
201
+
181
202
  #: (Symbol flag) -> bool?
182
203
  def enabled_feature?(flag)
183
204
  @enabled_feature_flags[:all] || @enabled_feature_flags[flag]
@@ -6,8 +6,6 @@
6
6
  yarp_require_paths = Gem.loaded_specs["yarp"]&.full_require_paths
7
7
  $LOAD_PATH.delete_if { |path| yarp_require_paths.include?(path) } if yarp_require_paths
8
8
 
9
- require "sorbet-runtime"
10
-
11
9
  # Set Bundler's UI level to silent as soon as possible to prevent any prints to STDOUT
12
10
  require "bundler"
13
11
  Bundler.ui.level = :silent
@@ -445,11 +445,14 @@ module RubyLsp
445
445
  return unless arguments_node
446
446
 
447
447
  path_node_to_complete = arguments_node.arguments.first
448
-
449
448
  return unless path_node_to_complete.is_a?(Prism::StringNode)
450
449
 
451
- origin_dir = Pathname.new(@uri.to_standardized_path).dirname
450
+ # If the file is unsaved (e.g.: untitled:Untitled-1), we can't provide relative completion as we don't know
451
+ # where the user intends to save it
452
+ full_path = @uri.to_standardized_path
453
+ return unless full_path
452
454
 
455
+ origin_dir = Pathname.new(full_path).dirname
453
456
  content = path_node_to_complete.content
454
457
  # if the path is not a directory, glob all possible next characters
455
458
  # for example ../somethi| (where | is the cursor position)
@@ -516,6 +519,14 @@ module RubyLsp
516
519
 
517
520
  entry_name = entry.name
518
521
  owner_name = entry.owner&.name
522
+ new_text = entry_name
523
+
524
+ if entry_name.end_with?("=")
525
+ method_name = entry_name.delete_suffix("=")
526
+
527
+ # For writer methods, format as assignment and prefix "self." when no receiver is specified
528
+ new_text = node.receiver.nil? ? "self.#{method_name} = " : "#{method_name} = "
529
+ end
519
530
 
520
531
  label_details = Interface::CompletionItemLabelDetails.new(
521
532
  description: entry.file_name,
@@ -525,7 +536,7 @@ module RubyLsp
525
536
  label: entry_name,
526
537
  filter_text: entry_name,
527
538
  label_details: label_details,
528
- text_edit: Interface::TextEdit.new(range: range, new_text: entry_name),
539
+ text_edit: Interface::TextEdit.new(range: range, new_text: new_text),
529
540
  kind: Constant::CompletionItemKind::METHOD,
530
541
  data: {
531
542
  owner_name: owner_name,
@@ -7,6 +7,7 @@ module RubyLsp
7
7
  include Requests::Support::Common
8
8
 
9
9
  ALLOWED_TARGETS = [
10
+ Prism::BreakNode,
10
11
  Prism::CallNode,
11
12
  Prism::ConstantReadNode,
12
13
  Prism::ConstantWriteNode,
@@ -54,6 +55,7 @@ module RubyLsp
54
55
 
55
56
  dispatcher.register(
56
57
  self,
58
+ :on_break_node_enter,
57
59
  :on_constant_read_node_enter,
58
60
  :on_constant_write_node_enter,
59
61
  :on_constant_path_node_enter,
@@ -84,6 +86,11 @@ module RubyLsp
84
86
  )
85
87
  end
86
88
 
89
+ #: (Prism::BreakNode node) -> void
90
+ def on_break_node_enter(node)
91
+ handle_keyword_documentation(node.keyword)
92
+ end
93
+
87
94
  #: (Prism::StringNode node) -> void
88
95
  def on_string_node_enter(node)
89
96
  if @path && File.basename(@path) == GEMFILE_NAME
@@ -8,10 +8,12 @@ module RubyLsp
8
8
 
9
9
  RESCUE_STRING_LENGTH = "rescue".length #: Integer
10
10
 
11
- #: (ResponseBuilders::CollectionResponseBuilder[Interface::InlayHint] response_builder, RequestConfig hints_configuration, Prism::Dispatcher dispatcher) -> void
12
- def initialize(response_builder, hints_configuration, dispatcher)
11
+ #: (GlobalState, ResponseBuilders::CollectionResponseBuilder[Interface::InlayHint], Prism::Dispatcher) -> void
12
+ def initialize(global_state, response_builder, dispatcher)
13
13
  @response_builder = response_builder
14
- @hints_configuration = hints_configuration
14
+ @hints_configuration = ( # rubocop:disable Style/RedundantParentheses
15
+ global_state.feature_configuration(:inlayHint) #: as !nil
16
+ ) #: RequestConfig
15
17
 
16
18
  dispatcher.register(self, :on_rescue_node_enter, :on_implicit_node_enter)
17
19
  end
@@ -4,39 +4,62 @@
4
4
  module RubyLsp
5
5
  module Listeners
6
6
  class SpecStyle < TestDiscovery
7
+ class Group
8
+ #: String
9
+ attr_reader :id
10
+
11
+ #: (String) -> void
12
+ def initialize(id)
13
+ @id = id
14
+ end
15
+ end
16
+
17
+ class ClassGroup < Group; end
18
+ class DescribeGroup < Group; end
19
+
7
20
  #: (ResponseBuilders::TestCollection, GlobalState, Prism::Dispatcher, URI::Generic) -> void
8
21
  def initialize(response_builder, global_state, dispatcher, uri)
9
- super
22
+ super(response_builder, global_state, uri)
10
23
 
11
- @describe_block_nesting = [] #: Array[String]
12
- @spec_class_stack = [] #: Array[bool]
24
+ @spec_group_id_stack = [] #: Array[Group?]
13
25
 
14
- dispatcher.register(
15
- self,
16
- # Common handlers registered in parent class
26
+ register_events(
27
+ dispatcher,
17
28
  :on_class_node_enter,
18
- :on_call_node_enter, # e.g. `describe` or `it`
29
+ :on_call_node_enter,
19
30
  :on_call_node_leave,
20
31
  )
21
32
  end
22
33
 
23
34
  #: (Prism::ClassNode) -> void
24
- def on_class_node_enter(node)
25
- with_test_ancestor_tracking(node) do |_, ancestors|
26
- is_spec = ancestors.include?("Minitest::Spec")
27
- @spec_class_stack.push(is_spec)
35
+ def on_class_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
36
+ with_test_ancestor_tracking(node) do |name, ancestors|
37
+ @spec_group_id_stack << (ancestors.include?("Minitest::Spec") ? ClassGroup.new(name) : nil)
28
38
  end
29
39
  end
30
40
 
31
41
  #: (Prism::ClassNode) -> void
32
42
  def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
43
+ @spec_group_id_stack.pop
33
44
  super
45
+ end
34
46
 
35
- @spec_class_stack.pop
47
+ #: (Prism::ModuleNode) -> void
48
+ def on_module_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
49
+ @spec_group_id_stack << nil
50
+ super
51
+ end
52
+
53
+ #: (Prism::ModuleNode) -> void
54
+ def on_module_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
55
+ @spec_group_id_stack.pop
56
+ super
36
57
  end
37
58
 
38
59
  #: (Prism::CallNode) -> void
39
- def on_call_node_enter(node)
60
+ def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
61
+ return unless in_spec_context?
62
+
40
63
  case node.name
41
64
  when :describe
42
65
  handle_describe(node)
@@ -46,87 +69,74 @@ module RubyLsp
46
69
  end
47
70
 
48
71
  #: (Prism::CallNode) -> void
49
- def on_call_node_leave(node)
72
+ def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
50
73
  return unless node.name == :describe && !node.receiver
51
74
 
52
- @describe_block_nesting.pop
75
+ current_group = @spec_group_id_stack.last
76
+ return unless current_group.is_a?(DescribeGroup)
77
+
78
+ description = extract_description(node)
79
+ return unless description && current_group.id.end_with?(description)
80
+
81
+ @spec_group_id_stack.pop
53
82
  end
54
83
 
55
84
  private
56
85
 
57
86
  #: (Prism::CallNode) -> void
58
87
  def handle_describe(node)
88
+ # Describes will include the nesting of all classes and all outer describes as part of its ID, unlike classes
89
+ # that ignore describes
59
90
  return if node.block.nil?
60
91
 
61
92
  description = extract_description(node)
62
93
  return unless description
63
94
 
64
- return unless in_spec_context?
95
+ parent = latest_group
96
+ return unless parent
65
97
 
66
- if @nesting.empty? && @describe_block_nesting.empty?
67
- test_item = Requests::Support::TestItem.new(
68
- description,
69
- description,
70
- @uri,
71
- range_from_node(node),
72
- framework: :minitest,
73
- )
74
- @response_builder.add(test_item)
75
- @response_builder.add_code_lens(test_item)
98
+ id = case parent
99
+ when Requests::Support::TestItem
100
+ "#{parent.id}::#{description}"
76
101
  else
77
- add_to_parent_test_group(description, node)
102
+ description
78
103
  end
79
104
 
80
- @describe_block_nesting << description
105
+ test_item = Requests::Support::TestItem.new(
106
+ id,
107
+ description,
108
+ @uri,
109
+ range_from_node(node),
110
+ framework: :minitest,
111
+ )
112
+
113
+ parent.add(test_item)
114
+ @response_builder.add_code_lens(test_item)
115
+ @spec_group_id_stack << DescribeGroup.new(id)
81
116
  end
82
117
 
83
118
  #: (Prism::CallNode) -> void
84
119
  def handle_example(node)
85
- return unless in_spec_context?
86
-
87
- return if @describe_block_nesting.empty? && @nesting.empty?
88
-
89
- description = extract_description(node)
90
- return unless description
120
+ # Minitest formats the descriptions into test method names by using the count of examples with the description
121
+ # We are not guaranteed to discover examples in the exact order using static analysis, so we use the line number
122
+ # instead. Note that anonymous examples mixed with meta-programming will not be handled correctly
123
+ description = extract_description(node) || "anonymous"
124
+ line = node.location.start_line - 1
125
+ parent = latest_group
126
+ return unless parent.is_a?(Requests::Support::TestItem)
91
127
 
92
- add_to_parent_test_group(description, node)
93
- end
94
-
95
- #: (String, Prism::CallNode) -> void
96
- def add_to_parent_test_group(description, node)
97
- parent_test_group = find_parent_test_group
98
- return unless parent_test_group
128
+ id = "#{parent.id}##{format("test_%04d_%s", line, description)}"
99
129
 
100
130
  test_item = Requests::Support::TestItem.new(
101
- description,
131
+ id,
102
132
  description,
103
133
  @uri,
104
134
  range_from_node(node),
105
135
  framework: :minitest,
106
136
  )
107
- parent_test_group.add(test_item)
108
- @response_builder.add_code_lens(test_item)
109
- end
110
-
111
- #: -> Requests::Support::TestItem?
112
- def find_parent_test_group
113
- root_group_name, nested_describe_groups = if @nesting.empty?
114
- [@describe_block_nesting.first, @describe_block_nesting[1..]]
115
- else
116
- [RubyIndexer::Index.actual_nesting(@nesting, nil).join("::"), @describe_block_nesting]
117
- end
118
- return unless root_group_name
119
137
 
120
- test_group = @response_builder[root_group_name] #: Requests::Support::TestItem?
121
- return unless test_group
122
-
123
- return test_group unless nested_describe_groups
124
-
125
- nested_describe_groups.each do |description|
126
- test_group = test_group[description]
127
- end
128
-
129
- test_group
138
+ parent.add(test_item)
139
+ @response_builder.add_code_lens(test_item)
130
140
  end
131
141
 
132
142
  #: (Prism::CallNode) -> String?
@@ -144,11 +154,60 @@ module RubyLsp
144
154
  end
145
155
  end
146
156
 
157
+ #: -> (Requests::Support::TestItem | ResponseBuilders::TestCollection)?
158
+ def latest_group
159
+ # If we haven't found anything yet, then return the response builder
160
+ return @response_builder if @spec_group_id_stack.compact.empty?
161
+ # If we found something that isn't a group last, then we're inside a random module or class, but not a spec
162
+ # group
163
+ return unless @spec_group_id_stack.last
164
+
165
+ # Specs using at least one class as a group require special handling
166
+ closest_class_index = @spec_group_id_stack.rindex { |i| i.is_a?(ClassGroup) }
167
+
168
+ if closest_class_index
169
+ first_class_index = @spec_group_id_stack.index { |i| i.is_a?(ClassGroup) } #: as !nil
170
+ first_class = @spec_group_id_stack[first_class_index] #: as !nil
171
+ item = @response_builder[first_class.id] #: as !nil
172
+
173
+ # Descend into child items from the beginning all the way to the latest class group, ignoring describes
174
+ @spec_group_id_stack[first_class_index + 1..closest_class_index] #: as !nil
175
+ .each do |group|
176
+ next unless group.is_a?(ClassGroup)
177
+
178
+ item = item[group.id] #: as !nil
179
+ end
180
+
181
+ # From the class forward, we must take describes into account
182
+ @spec_group_id_stack[closest_class_index + 1..] #: as !nil
183
+ .each do |group|
184
+ next unless group
185
+
186
+ item = item[group.id] #: as !nil
187
+ end
188
+
189
+ return item
190
+ end
191
+
192
+ # Specs only using describes
193
+ first_group = @spec_group_id_stack.find { |i| i.is_a?(DescribeGroup) }
194
+ return unless first_group
195
+
196
+ item = @response_builder[first_group.id] #: as !nil
197
+
198
+ @spec_group_id_stack[1..] #: as !nil
199
+ .each do |group|
200
+ next unless group.is_a?(DescribeGroup)
201
+
202
+ item = item[group.id] #: as !nil
203
+ end
204
+
205
+ item
206
+ end
207
+
147
208
  #: -> bool
148
209
  def in_spec_context?
149
- return true if @nesting.empty?
150
-
151
- @spec_class_stack.last #: as !nil
210
+ @nesting.empty? || @spec_group_id_stack.any? { |id| id }
152
211
  end
153
212
  end
154
213
  end
@@ -3,32 +3,23 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Listeners
6
+ # @abstract
6
7
  class TestDiscovery
7
- extend T::Helpers
8
- abstract!
9
-
10
8
  include Requests::Support::Common
11
9
 
12
10
  DYNAMIC_REFERENCE_MARKER = "<dynamic_reference>"
13
11
 
14
- #: (ResponseBuilders::TestCollection response_builder, GlobalState global_state, Prism::Dispatcher dispatcher, URI::Generic uri) -> void
15
- def initialize(response_builder, global_state, dispatcher, uri)
12
+ #: (ResponseBuilders::TestCollection response_builder, GlobalState global_state, URI::Generic uri) -> void
13
+ def initialize(response_builder, global_state, uri)
16
14
  @response_builder = response_builder
17
15
  @uri = uri
18
16
  @index = global_state.index #: RubyIndexer::Index
19
17
  @visibility_stack = [:public] #: Array[Symbol]
20
18
  @nesting = [] #: Array[String]
21
-
22
- dispatcher.register(
23
- self,
24
- :on_class_node_leave,
25
- :on_module_node_enter,
26
- :on_module_node_leave,
27
- )
28
19
  end
29
20
 
30
21
  #: (Prism::ModuleNode node) -> void
31
- def on_module_node_enter(node)
22
+ def on_module_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
32
23
  @visibility_stack << :public
33
24
 
34
25
  name = constant_name(node.constant_path)
@@ -38,19 +29,31 @@ module RubyLsp
38
29
  end
39
30
 
40
31
  #: (Prism::ModuleNode node) -> void
41
- def on_module_node_leave(node)
32
+ def on_module_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
42
33
  @visibility_stack.pop
43
34
  @nesting.pop
44
35
  end
45
36
 
46
37
  #: (Prism::ClassNode node) -> void
47
- def on_class_node_leave(node)
38
+ def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
48
39
  @visibility_stack.pop
49
40
  @nesting.pop
50
41
  end
51
42
 
52
43
  private
53
44
 
45
+ #: (Prism::Dispatcher, *Symbol) -> void
46
+ def register_events(dispatcher, *events)
47
+ unique_events = events.dup.push(
48
+ :on_class_node_leave,
49
+ :on_module_node_enter,
50
+ :on_module_node_leave,
51
+ )
52
+
53
+ unique_events.uniq!
54
+ dispatcher.register(self, *unique_events)
55
+ end
56
+
54
57
  #: (String? name) -> String
55
58
  def calc_fully_qualified_name(name)
56
59
  RubyIndexer::Index.actual_nesting(@nesting, name).join("::")
@@ -34,7 +34,7 @@ module RubyLsp
34
34
  if tags.include?("test_dir")
35
35
  if children.empty?
36
36
  full_files.concat(Dir.glob(
37
- "#{path}/**/{*_test,test_*}.rb",
37
+ "#{path}/**/{*_test,test_*,*_spec}.rb",
38
38
  File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
39
39
  ))
40
40
  end
@@ -44,7 +44,7 @@ module RubyLsp
44
44
  # If all of the children of the current test group are other groups, then there's no need to add it to the
45
45
  # aggregated examples
46
46
  unless children.any? && children.all? { |child| child[:tags].include?("test_group") }
47
- aggregated_tests[path][item[:label]] = { tags: tags, examples: [] }
47
+ aggregated_tests[path][item[:id]] = { tags: tags, examples: [] }
48
48
  end
49
49
  else
50
50
  class_name, method_name = item[:id].split("#")
@@ -74,7 +74,10 @@ module RubyLsp
74
74
  end
75
75
 
76
76
  unless full_files.empty?
77
- commands << "#{BASE_COMMAND} -Itest -e \"ARGV.each { |f| require f }\" #{full_files.join(" ")}"
77
+ specs, tests = full_files.partition { |path| spec?(path) }
78
+
79
+ commands << "#{COMMAND} -Itest -e \"ARGV.each { |f| require f }\" #{tests.join(" ")}" if tests.any?
80
+ commands << "#{COMMAND} -Ispec -e \"ARGV.each { |f| require f }\" #{specs.join(" ")}" if specs.any?
78
81
  end
79
82
 
80
83
  commands
@@ -82,10 +85,15 @@ module RubyLsp
82
85
 
83
86
  private
84
87
 
88
+ #: (String) -> bool
89
+ def spec?(path)
90
+ File.fnmatch?("**/spec/**/*_spec.rb", path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
91
+ end
92
+
85
93
  #: (String, Hash[String, Hash[Symbol, untyped]]) -> String
86
94
  def handle_minitest_groups(file_path, groups_and_examples)
87
95
  regexes = groups_and_examples.flat_map do |group, info|
88
- examples = info[:examples]
96
+ examples = info[:examples].map { |e| Shellwords.escape(e).gsub(/test_\d{4}/, "test_\\d{4}") }
89
97
  group_regex = Shellwords.escape(group).gsub(
90
98
  Shellwords.escape(TestDiscovery::DYNAMIC_REFERENCE_MARKER),
91
99
  ".*",
@@ -105,7 +113,8 @@ module RubyLsp
105
113
  "(#{regexes.join("|")})"
106
114
  end
107
115
 
108
- "#{BASE_COMMAND} -Itest #{file_path} --name \"/#{regex}/\""
116
+ load_path = spec?(file_path) ? "-Ispec" : "-Itest"
117
+ "#{COMMAND} #{load_path} #{file_path} --name \"/#{regex}/\""
109
118
  end
110
119
 
111
120
  #: (String, Hash[String, Hash[Symbol, untyped]]) -> Array[String]
@@ -116,7 +125,7 @@ module RubyLsp
116
125
  Shellwords.escape(TestDiscovery::DYNAMIC_REFERENCE_MARKER),
117
126
  ".*",
118
127
  )
119
- command = +"#{BASE_COMMAND} -Itest #{file_path} --testcase \"/^#{group_regex}\\$/\""
128
+ command = +"#{COMMAND} -Itest #{file_path} --testcase \"/^#{group_regex}\\$/\""
120
129
 
121
130
  unless examples.empty?
122
131
  command << if examples.length == 1
@@ -135,23 +144,24 @@ module RubyLsp
135
144
 
136
145
  MINITEST_REPORTER_PATH = File.expand_path("../test_reporters/minitest_reporter.rb", __dir__) #: String
137
146
  TEST_UNIT_REPORTER_PATH = File.expand_path("../test_reporters/test_unit_reporter.rb", __dir__) #: String
138
- ACCESS_MODIFIERS = [:public, :private, :protected].freeze
139
147
  BASE_COMMAND = begin
140
148
  Bundler.with_original_env { Bundler.default_lockfile }
141
149
  "bundle exec ruby"
142
150
  rescue Bundler::GemfileNotFound
143
151
  "ruby"
144
152
  end #: String
153
+ COMMAND = "#{BASE_COMMAND} -r#{MINITEST_REPORTER_PATH} -r#{TEST_UNIT_REPORTER_PATH}" #: String
154
+ ACCESS_MODIFIERS = [:public, :private, :protected].freeze
145
155
 
146
156
  #: (ResponseBuilders::TestCollection, GlobalState, Prism::Dispatcher, URI::Generic) -> void
147
157
  def initialize(response_builder, global_state, dispatcher, uri)
148
- super
158
+ super(response_builder, global_state, uri)
149
159
 
150
160
  @framework = :minitest #: Symbol
161
+ @parent_stack = [@response_builder] #: Array[(Requests::Support::TestItem | ResponseBuilders::TestCollection)?]
151
162
 
152
- dispatcher.register(
153
- self,
154
- # Common handlers registered in parent class
163
+ register_events(
164
+ dispatcher,
155
165
  :on_class_node_enter,
156
166
  :on_def_node_enter,
157
167
  :on_call_node_enter,
@@ -160,7 +170,7 @@ module RubyLsp
160
170
  end
161
171
 
162
172
  #: (Prism::ClassNode node) -> void
163
- def on_class_node_enter(node)
173
+ def on_class_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
164
174
  with_test_ancestor_tracking(node) do |name, ancestors|
165
175
  @framework = :test_unit if ancestors.include?("Test::Unit::TestCase")
166
176
 
@@ -173,26 +183,43 @@ module RubyLsp
173
183
  framework: @framework,
174
184
  )
175
185
 
176
- @response_builder.add(test_item)
186
+ last_test_group.add(test_item)
177
187
  @response_builder.add_code_lens(test_item)
188
+ @parent_stack << test_item
189
+ else
190
+ @parent_stack << nil
178
191
  end
179
192
  end
180
193
  end
181
194
 
195
+ #: (Prism::ClassNode node) -> void
196
+ def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
197
+ @parent_stack.pop
198
+ super
199
+ end
200
+
201
+ #: (Prism::ModuleNode node) -> void
202
+ def on_module_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
203
+ @parent_stack << nil
204
+ super
205
+ end
206
+
207
+ #: (Prism::ModuleNode node) -> void
208
+ def on_module_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
209
+ @parent_stack.pop
210
+ super
211
+ end
212
+
182
213
  #: (Prism::DefNode node) -> void
183
- def on_def_node_enter(node)
214
+ def on_def_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
184
215
  return if @visibility_stack.last != :public
185
216
 
186
217
  name = node.name.to_s
187
218
  return unless name.start_with?("test_")
188
219
 
189
220
  current_group_name = RubyIndexer::Index.actual_nesting(@nesting, nil).join("::")
190
-
191
- # If we're finding a test method, but for the wrong framework, then the group test item will not have been
192
- # previously pushed and thus we return early and avoid adding items for a framework this listener is not
193
- # interested in
194
- test_item = @response_builder[current_group_name]
195
- return unless test_item
221
+ parent = @parent_stack.last
222
+ return unless parent.is_a?(Requests::Support::TestItem)
196
223
 
197
224
  example_item = Requests::Support::TestItem.new(
198
225
  "#{current_group_name}##{name}",
@@ -201,12 +228,12 @@ module RubyLsp
201
228
  range_from_node(node),
202
229
  framework: @framework,
203
230
  )
204
- test_item.add(example_item)
231
+ parent.add(example_item)
205
232
  @response_builder.add_code_lens(example_item)
206
233
  end
207
234
 
208
235
  #: (Prism::CallNode node) -> void
209
- def on_call_node_enter(node)
236
+ def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
210
237
  name = node.name
211
238
  return unless ACCESS_MODIFIERS.include?(name)
212
239
 
@@ -214,7 +241,7 @@ module RubyLsp
214
241
  end
215
242
 
216
243
  #: (Prism::CallNode node) -> void
217
- def on_call_node_leave(node)
244
+ def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
218
245
  name = node.name
219
246
  return unless ACCESS_MODIFIERS.include?(name)
220
247
  return unless node.arguments&.arguments
@@ -224,6 +251,12 @@ module RubyLsp
224
251
 
225
252
  private
226
253
 
254
+ #: -> (Requests::Support::TestItem | ResponseBuilders::TestCollection)
255
+ def last_test_group
256
+ index = @parent_stack.rindex { |i| i } #: as !nil
257
+ @parent_stack[index] #: as Requests::Support::TestItem | ResponseBuilders::TestCollection
258
+ end
259
+
227
260
  #: (Array[String] attached_ancestors, String fully_qualified_name) -> bool
228
261
  def non_declarative_minitest?(attached_ancestors, fully_qualified_name)
229
262
  return false unless attached_ancestors.include?("Minitest::Test")