ruby-lsp 0.16.7 → 0.17.3

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +195 -18
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +129 -12
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +333 -44
  7. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +99 -0
  8. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  9. data/lib/ruby_indexer/test/classes_and_modules_test.rb +58 -11
  10. data/lib/ruby_indexer/test/configuration_test.rb +5 -6
  11. data/lib/ruby_indexer/test/constant_test.rb +9 -9
  12. data/lib/ruby_indexer/test/index_test.rb +867 -7
  13. data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
  14. data/lib/ruby_indexer/test/method_test.rb +57 -0
  15. data/lib/ruby_indexer/test/rbs_indexer_test.rb +42 -0
  16. data/lib/ruby_indexer/test/test_case.rb +10 -1
  17. data/lib/ruby_lsp/addon.rb +8 -8
  18. data/lib/ruby_lsp/document.rb +26 -3
  19. data/lib/ruby_lsp/global_state.rb +37 -16
  20. data/lib/ruby_lsp/internal.rb +2 -0
  21. data/lib/ruby_lsp/listeners/code_lens.rb +2 -2
  22. data/lib/ruby_lsp/listeners/completion.rb +74 -17
  23. data/lib/ruby_lsp/listeners/definition.rb +82 -24
  24. data/lib/ruby_lsp/listeners/hover.rb +62 -6
  25. data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
  26. data/lib/ruby_lsp/node_context.rb +39 -0
  27. data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
  28. data/lib/ruby_lsp/requests/code_actions.rb +16 -15
  29. data/lib/ruby_lsp/requests/completion.rb +21 -13
  30. data/lib/ruby_lsp/requests/completion_resolve.rb +9 -0
  31. data/lib/ruby_lsp/requests/definition.rb +25 -5
  32. data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
  33. data/lib/ruby_lsp/requests/hover.rb +5 -6
  34. data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
  35. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  36. data/lib/ruby_lsp/requests/support/common.rb +4 -3
  37. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +23 -6
  38. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +5 -1
  39. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
  40. data/lib/ruby_lsp/requests/workspace_symbol.rb +7 -4
  41. data/lib/ruby_lsp/server.rb +22 -5
  42. data/lib/ruby_lsp/test_helper.rb +1 -0
  43. metadata +29 -5
@@ -11,17 +11,17 @@ module RubyLsp
11
11
  params(
12
12
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
13
13
  global_state: GlobalState,
14
- nesting: T::Array[String],
14
+ node_context: NodeContext,
15
15
  typechecker_enabled: T::Boolean,
16
16
  dispatcher: Prism::Dispatcher,
17
17
  uri: URI::Generic,
18
18
  ).void
19
19
  end
20
- def initialize(response_builder, global_state, nesting, typechecker_enabled, dispatcher, uri) # rubocop:disable Metrics/ParameterLists
20
+ def initialize(response_builder, global_state, node_context, typechecker_enabled, dispatcher, uri) # rubocop:disable Metrics/ParameterLists
21
21
  @response_builder = response_builder
22
22
  @global_state = global_state
23
23
  @index = T.let(global_state.index, RubyIndexer::Index)
24
- @nesting = nesting
24
+ @node_context = node_context
25
25
  @typechecker_enabled = typechecker_enabled
26
26
  @uri = uri
27
27
 
@@ -30,6 +30,12 @@ module RubyLsp
30
30
  :on_constant_path_node_enter,
31
31
  :on_constant_read_node_enter,
32
32
  :on_call_node_enter,
33
+ :on_instance_variable_read_node_enter,
34
+ :on_instance_variable_write_node_enter,
35
+ :on_instance_variable_and_write_node_enter,
36
+ :on_instance_variable_operator_write_node_enter,
37
+ :on_instance_variable_or_write_node_enter,
38
+ :on_instance_variable_target_node_enter,
33
39
  )
34
40
  end
35
41
 
@@ -41,7 +47,7 @@ module RubyLsp
41
47
  name = constant_name(node)
42
48
  return if name.nil?
43
49
 
44
- candidates = @index.prefix_search(name, @nesting)
50
+ candidates = @index.prefix_search(name, @node_context.nesting)
45
51
  candidates.each do |entries|
46
52
  complete_name = T.must(entries.first).name
47
53
  @response_builder << build_entry_completion(
@@ -105,6 +111,36 @@ module RubyLsp
105
111
  end
106
112
  end
107
113
 
114
+ sig { params(node: Prism::InstanceVariableReadNode).void }
115
+ def on_instance_variable_read_node_enter(node)
116
+ handle_instance_variable_completion(node.name.to_s, node.location)
117
+ end
118
+
119
+ sig { params(node: Prism::InstanceVariableWriteNode).void }
120
+ def on_instance_variable_write_node_enter(node)
121
+ handle_instance_variable_completion(node.name.to_s, node.name_loc)
122
+ end
123
+
124
+ sig { params(node: Prism::InstanceVariableAndWriteNode).void }
125
+ def on_instance_variable_and_write_node_enter(node)
126
+ handle_instance_variable_completion(node.name.to_s, node.name_loc)
127
+ end
128
+
129
+ sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
130
+ def on_instance_variable_operator_write_node_enter(node)
131
+ handle_instance_variable_completion(node.name.to_s, node.name_loc)
132
+ end
133
+
134
+ sig { params(node: Prism::InstanceVariableOrWriteNode).void }
135
+ def on_instance_variable_or_write_node_enter(node)
136
+ handle_instance_variable_completion(node.name.to_s, node.name_loc)
137
+ end
138
+
139
+ sig { params(node: Prism::InstanceVariableTargetNode).void }
140
+ def on_instance_variable_target_node_enter(node)
141
+ handle_instance_variable_completion(node.name.to_s, node.location)
142
+ end
143
+
108
144
  private
109
145
 
110
146
  sig { params(name: String, range: Interface::Range).void }
@@ -125,20 +161,21 @@ module RubyLsp
125
161
  T.must(namespace).join("::")
126
162
  end
127
163
 
128
- namespace_entries = @index.resolve(aliased_namespace, @nesting)
164
+ nesting = @node_context.nesting
165
+ namespace_entries = @index.resolve(aliased_namespace, nesting)
129
166
  return unless namespace_entries
130
167
 
131
168
  real_namespace = @index.follow_aliased_namespace(T.must(namespace_entries.first).name)
132
169
 
133
170
  candidates = @index.prefix_search(
134
171
  "#{real_namespace}::#{incomplete_name}",
135
- top_level_reference ? [] : @nesting,
172
+ top_level_reference ? [] : nesting,
136
173
  )
137
174
  candidates.each do |entries|
138
175
  # The only time we may have a private constant reference from outside of the namespace is if we're dealing
139
176
  # with ConstantPath and the entry name doesn't start with the current nesting
140
177
  first_entry = T.must(entries.first)
141
- next if first_entry.visibility == :private && !first_entry.name.start_with?("#{@nesting}::")
178
+ next if first_entry.private? && !first_entry.name.start_with?("#{nesting}::")
142
179
 
143
180
  constant_name = first_entry.name.delete_prefix("#{real_namespace}::")
144
181
  full_name = aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}"
@@ -153,6 +190,27 @@ module RubyLsp
153
190
  end
154
191
  end
155
192
 
193
+ sig { params(name: String, location: Prism::Location).void }
194
+ def handle_instance_variable_completion(name, location)
195
+ @index.instance_variable_completion_candidates(name, @node_context.fully_qualified_name).each do |entry|
196
+ variable_name = entry.name
197
+
198
+ @response_builder << Interface::CompletionItem.new(
199
+ label: variable_name,
200
+ text_edit: Interface::TextEdit.new(
201
+ range: range_from_location(location),
202
+ new_text: variable_name,
203
+ ),
204
+ kind: Constant::CompletionItemKind::FIELD,
205
+ data: {
206
+ owner_name: entry.owner&.name,
207
+ },
208
+ )
209
+ end
210
+ rescue RubyIndexer::Index::NonExistingNamespaceError
211
+ # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
212
+ end
213
+
156
214
  sig { params(node: Prism::CallNode).void }
157
215
  def complete_require(node)
158
216
  arguments_node = node.arguments
@@ -200,15 +258,12 @@ module RubyLsp
200
258
 
201
259
  sig { params(node: Prism::CallNode, name: String).void }
202
260
  def complete_self_receiver_method(node, name)
203
- receiver_entries = @index[@nesting.join("::")]
261
+ receiver_entries = @index[@node_context.fully_qualified_name]
204
262
  return unless receiver_entries
205
263
 
206
264
  receiver = T.must(receiver_entries.first)
207
265
 
208
- @index.prefix_search(name).each do |entries|
209
- entry = entries.find { |e| e.is_a?(RubyIndexer::Entry::Member) && e.owner&.name == receiver.name }
210
- next unless entry
211
-
266
+ @index.method_completion_candidates(name, receiver.name).each do |entry|
212
267
  @response_builder << build_method_completion(T.cast(entry, RubyIndexer::Entry::Member), node)
213
268
  end
214
269
  end
@@ -297,13 +352,14 @@ module RubyLsp
297
352
  #
298
353
  # Foo::B # --> completion inserts `Bar` instead of `Foo::Bar`
299
354
  # end
300
- unless @nesting.join("::").start_with?(incomplete_name)
301
- @nesting.each do |namespace|
355
+ nesting = @node_context.nesting
356
+ unless @node_context.fully_qualified_name.start_with?(incomplete_name)
357
+ nesting.each do |namespace|
302
358
  prefix = "#{namespace}::"
303
359
  shortened_name = insertion_text.delete_prefix(prefix)
304
360
 
305
361
  # If a different entry exists for the shortened name, then there's a conflict and we should not shorten it
306
- conflict_name = "#{@nesting.join("::")}::#{shortened_name}"
362
+ conflict_name = "#{@node_context.fully_qualified_name}::#{shortened_name}"
307
363
  break if real_name != conflict_name && @index[conflict_name]
308
364
 
309
365
  insertion_text = shortened_name
@@ -345,8 +401,9 @@ module RubyLsp
345
401
  # ```
346
402
  sig { params(entry_name: String).returns(T::Boolean) }
347
403
  def top_level?(entry_name)
348
- @nesting.length.downto(0).each do |i|
349
- prefix = T.must(@nesting[0...i]).join("::")
404
+ nesting = @node_context.nesting
405
+ nesting.length.downto(0).each do |i|
406
+ prefix = T.must(nesting[0...i]).join("::")
350
407
  full_name = prefix.empty? ? entry_name : "#{prefix}::#{entry_name}"
351
408
  next if full_name == entry_name
352
409
 
@@ -14,17 +14,17 @@ module RubyLsp
14
14
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
15
15
  global_state: GlobalState,
16
16
  uri: URI::Generic,
17
- nesting: T::Array[String],
17
+ node_context: NodeContext,
18
18
  dispatcher: Prism::Dispatcher,
19
19
  typechecker_enabled: T::Boolean,
20
20
  ).void
21
21
  end
22
- def initialize(response_builder, global_state, uri, nesting, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
22
+ def initialize(response_builder, global_state, uri, node_context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
23
23
  @response_builder = response_builder
24
24
  @global_state = global_state
25
25
  @index = T.let(global_state.index, RubyIndexer::Index)
26
26
  @uri = uri
27
- @nesting = nesting
27
+ @node_context = node_context
28
28
  @typechecker_enabled = typechecker_enabled
29
29
 
30
30
  dispatcher.register(
@@ -33,18 +33,33 @@ module RubyLsp
33
33
  :on_block_argument_node_enter,
34
34
  :on_constant_read_node_enter,
35
35
  :on_constant_path_node_enter,
36
+ :on_instance_variable_read_node_enter,
37
+ :on_instance_variable_write_node_enter,
38
+ :on_instance_variable_and_write_node_enter,
39
+ :on_instance_variable_operator_write_node_enter,
40
+ :on_instance_variable_or_write_node_enter,
41
+ :on_instance_variable_target_node_enter,
42
+ :on_string_node_enter,
36
43
  )
37
44
  end
38
45
 
39
46
  sig { params(node: Prism::CallNode).void }
40
47
  def on_call_node_enter(node)
41
- message = node.name
48
+ message = node.message
49
+ return unless message
42
50
 
43
- if message == :require || message == :require_relative
44
- handle_require_definition(node)
45
- else
46
- handle_method_definition(message.to_s, self_receiver?(node))
47
- end
51
+ handle_method_definition(message, self_receiver?(node))
52
+ end
53
+
54
+ sig { params(node: Prism::StringNode).void }
55
+ def on_string_node_enter(node)
56
+ enclosing_call = @node_context.call_node
57
+ return unless enclosing_call
58
+
59
+ name = enclosing_call.name
60
+ return unless name == :require || name == :require_relative
61
+
62
+ handle_require_definition(node, name)
48
63
  end
49
64
 
50
65
  sig { params(node: Prism::BlockArgumentNode).void }
@@ -74,12 +89,62 @@ module RubyLsp
74
89
  find_in_index(name)
75
90
  end
76
91
 
92
+ sig { params(node: Prism::InstanceVariableReadNode).void }
93
+ def on_instance_variable_read_node_enter(node)
94
+ handle_instance_variable_definition(node.name.to_s)
95
+ end
96
+
97
+ sig { params(node: Prism::InstanceVariableWriteNode).void }
98
+ def on_instance_variable_write_node_enter(node)
99
+ handle_instance_variable_definition(node.name.to_s)
100
+ end
101
+
102
+ sig { params(node: Prism::InstanceVariableAndWriteNode).void }
103
+ def on_instance_variable_and_write_node_enter(node)
104
+ handle_instance_variable_definition(node.name.to_s)
105
+ end
106
+
107
+ sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
108
+ def on_instance_variable_operator_write_node_enter(node)
109
+ handle_instance_variable_definition(node.name.to_s)
110
+ end
111
+
112
+ sig { params(node: Prism::InstanceVariableOrWriteNode).void }
113
+ def on_instance_variable_or_write_node_enter(node)
114
+ handle_instance_variable_definition(node.name.to_s)
115
+ end
116
+
117
+ sig { params(node: Prism::InstanceVariableTargetNode).void }
118
+ def on_instance_variable_target_node_enter(node)
119
+ handle_instance_variable_definition(node.name.to_s)
120
+ end
121
+
77
122
  private
78
123
 
124
+ sig { params(name: String).void }
125
+ def handle_instance_variable_definition(name)
126
+ entries = @index.resolve_instance_variable(name, @node_context.fully_qualified_name)
127
+ return unless entries
128
+
129
+ entries.each do |entry|
130
+ location = entry.location
131
+
132
+ @response_builder << Interface::Location.new(
133
+ uri: URI::Generic.from_path(path: entry.file_path).to_s,
134
+ range: Interface::Range.new(
135
+ start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
136
+ end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
137
+ ),
138
+ )
139
+ end
140
+ rescue RubyIndexer::Index::NonExistingNamespaceError
141
+ # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
142
+ end
143
+
79
144
  sig { params(message: String, self_receiver: T::Boolean).void }
80
145
  def handle_method_definition(message, self_receiver)
81
146
  methods = if self_receiver
82
- @index.resolve_method(message, @nesting.join("::"))
147
+ @index.resolve_method(message, @node_context.fully_qualified_name)
83
148
  else
84
149
  # If the method doesn't have a receiver, then we provide a few candidates to jump to
85
150
  # But we don't want to provide too many candidates, as it can be overwhelming
@@ -103,19 +168,12 @@ module RubyLsp
103
168
  end
104
169
  end
105
170
 
106
- sig { params(node: Prism::CallNode).void }
107
- def handle_require_definition(node)
108
- message = node.name
109
- arguments = node.arguments
110
- return unless arguments
111
-
112
- argument = arguments.arguments.first
113
- return unless argument.is_a?(Prism::StringNode)
114
-
171
+ sig { params(node: Prism::StringNode, message: Symbol).void }
172
+ def handle_require_definition(node, message)
115
173
  case message
116
174
  when :require
117
- entry = @index.search_require_paths(argument.content).find do |indexable_path|
118
- indexable_path.require_path == argument.content
175
+ entry = @index.search_require_paths(node.content).find do |indexable_path|
176
+ indexable_path.require_path == node.content
119
177
  end
120
178
 
121
179
  if entry
@@ -130,7 +188,7 @@ module RubyLsp
130
188
  )
131
189
  end
132
190
  when :require_relative
133
- required_file = "#{argument.content}.rb"
191
+ required_file = "#{node.content}.rb"
134
192
  path = @uri.to_standardized_path
135
193
  current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
136
194
  candidate = File.expand_path(File.join(current_folder, required_file))
@@ -147,13 +205,13 @@ module RubyLsp
147
205
 
148
206
  sig { params(value: String).void }
149
207
  def find_in_index(value)
150
- entries = @index.resolve(value, @nesting)
208
+ entries = @index.resolve(value, @node_context.nesting)
151
209
  return unless entries
152
210
 
153
211
  # We should only allow jumping to the definition of private constants if the constant is defined in the same
154
212
  # namespace as the reference
155
213
  first_entry = T.must(entries.first)
156
- return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{value}"
214
+ return if first_entry.private? && first_entry.name != "#{@node_context.fully_qualified_name}::#{value}"
157
215
 
158
216
  entries.each do |entry|
159
217
  location = entry.location
@@ -13,6 +13,14 @@ module RubyLsp
13
13
  Prism::ConstantReadNode,
14
14
  Prism::ConstantWriteNode,
15
15
  Prism::ConstantPathNode,
16
+ Prism::InstanceVariableReadNode,
17
+ Prism::InstanceVariableAndWriteNode,
18
+ Prism::InstanceVariableOperatorWriteNode,
19
+ Prism::InstanceVariableOrWriteNode,
20
+ Prism::InstanceVariableTargetNode,
21
+ Prism::InstanceVariableWriteNode,
22
+ Prism::SymbolNode,
23
+ Prism::StringNode,
16
24
  ],
17
25
  T::Array[T.class_of(Prism::Node)],
18
26
  )
@@ -30,17 +38,17 @@ module RubyLsp
30
38
  response_builder: ResponseBuilders::Hover,
31
39
  global_state: GlobalState,
32
40
  uri: URI::Generic,
33
- nesting: T::Array[String],
41
+ node_context: NodeContext,
34
42
  dispatcher: Prism::Dispatcher,
35
43
  typechecker_enabled: T::Boolean,
36
44
  ).void
37
45
  end
38
- def initialize(response_builder, global_state, uri, nesting, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
46
+ def initialize(response_builder, global_state, uri, node_context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
39
47
  @response_builder = response_builder
40
48
  @global_state = global_state
41
49
  @index = T.let(global_state.index, RubyIndexer::Index)
42
50
  @path = T.let(uri.to_standardized_path, T.nilable(String))
43
- @nesting = nesting
51
+ @node_context = node_context
44
52
  @typechecker_enabled = typechecker_enabled
45
53
 
46
54
  dispatcher.register(
@@ -49,6 +57,12 @@ module RubyLsp
49
57
  :on_constant_write_node_enter,
50
58
  :on_constant_path_node_enter,
51
59
  :on_call_node_enter,
60
+ :on_instance_variable_read_node_enter,
61
+ :on_instance_variable_write_node_enter,
62
+ :on_instance_variable_and_write_node_enter,
63
+ :on_instance_variable_operator_write_node_enter,
64
+ :on_instance_variable_or_write_node_enter,
65
+ :on_instance_variable_target_node_enter,
52
66
  )
53
67
  end
54
68
 
@@ -93,7 +107,7 @@ module RubyLsp
93
107
  message = node.message
94
108
  return unless message
95
109
 
96
- methods = @index.resolve_method(message, @nesting.join("::"))
110
+ methods = @index.resolve_method(message, @node_context.fully_qualified_name)
97
111
  return unless methods
98
112
 
99
113
  categorized_markdown_from_index_entries(message, methods).each do |category, content|
@@ -101,17 +115,59 @@ module RubyLsp
101
115
  end
102
116
  end
103
117
 
118
+ sig { params(node: Prism::InstanceVariableReadNode).void }
119
+ def on_instance_variable_read_node_enter(node)
120
+ handle_instance_variable_hover(node.name.to_s)
121
+ end
122
+
123
+ sig { params(node: Prism::InstanceVariableWriteNode).void }
124
+ def on_instance_variable_write_node_enter(node)
125
+ handle_instance_variable_hover(node.name.to_s)
126
+ end
127
+
128
+ sig { params(node: Prism::InstanceVariableAndWriteNode).void }
129
+ def on_instance_variable_and_write_node_enter(node)
130
+ handle_instance_variable_hover(node.name.to_s)
131
+ end
132
+
133
+ sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
134
+ def on_instance_variable_operator_write_node_enter(node)
135
+ handle_instance_variable_hover(node.name.to_s)
136
+ end
137
+
138
+ sig { params(node: Prism::InstanceVariableOrWriteNode).void }
139
+ def on_instance_variable_or_write_node_enter(node)
140
+ handle_instance_variable_hover(node.name.to_s)
141
+ end
142
+
143
+ sig { params(node: Prism::InstanceVariableTargetNode).void }
144
+ def on_instance_variable_target_node_enter(node)
145
+ handle_instance_variable_hover(node.name.to_s)
146
+ end
147
+
104
148
  private
105
149
 
150
+ sig { params(name: String).void }
151
+ def handle_instance_variable_hover(name)
152
+ entries = @index.resolve_instance_variable(name, @node_context.fully_qualified_name)
153
+ return unless entries
154
+
155
+ categorized_markdown_from_index_entries(name, entries).each do |category, content|
156
+ @response_builder.push(content, category: category)
157
+ end
158
+ rescue RubyIndexer::Index::NonExistingNamespaceError
159
+ # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
160
+ end
161
+
106
162
  sig { params(name: String, location: Prism::Location).void }
107
163
  def generate_hover(name, location)
108
- entries = @index.resolve(name, @nesting)
164
+ entries = @index.resolve(name, @node_context.nesting)
109
165
  return unless entries
110
166
 
111
167
  # We should only show hover for private constants if the constant is defined in the same namespace as the
112
168
  # reference
113
169
  first_entry = T.must(entries.first)
114
- return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{name}"
170
+ return if first_entry.private? && first_entry.name != "#{@node_context.fully_qualified_name}::#{name}"
115
171
 
116
172
  categorized_markdown_from_index_entries(name, entries).each do |category, content|
117
173
  @response_builder.push(content, category: category)
@@ -11,17 +11,17 @@ module RubyLsp
11
11
  params(
12
12
  response_builder: ResponseBuilders::SignatureHelp,
13
13
  global_state: GlobalState,
14
- nesting: T::Array[String],
14
+ node_context: NodeContext,
15
15
  dispatcher: Prism::Dispatcher,
16
16
  typechecker_enabled: T::Boolean,
17
17
  ).void
18
18
  end
19
- def initialize(response_builder, global_state, nesting, dispatcher, typechecker_enabled)
19
+ def initialize(response_builder, global_state, node_context, dispatcher, typechecker_enabled)
20
20
  @typechecker_enabled = typechecker_enabled
21
21
  @response_builder = response_builder
22
22
  @global_state = global_state
23
23
  @index = T.let(global_state.index, RubyIndexer::Index)
24
- @nesting = nesting
24
+ @node_context = node_context
25
25
  dispatcher.register(self, :on_call_node_enter)
26
26
  end
27
27
 
@@ -33,7 +33,7 @@ module RubyLsp
33
33
  message = node.message
34
34
  return unless message
35
35
 
36
- methods = @index.resolve_method(message, @nesting.join("::"))
36
+ methods = @index.resolve_method(message, @node_context.fully_qualified_name)
37
37
  return unless methods
38
38
 
39
39
  target_method = methods.first
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ # This class allows listeners to access contextual information about a node in the AST, such as its parent,
6
+ # its namespace nesting, and the surrounding CallNode (e.g. a method call).
7
+ class NodeContext
8
+ extend T::Sig
9
+
10
+ sig { returns(T.nilable(Prism::Node)) }
11
+ attr_reader :node, :parent
12
+
13
+ sig { returns(T::Array[String]) }
14
+ attr_reader :nesting
15
+
16
+ sig { returns(T.nilable(Prism::CallNode)) }
17
+ attr_reader :call_node
18
+
19
+ sig do
20
+ params(
21
+ node: T.nilable(Prism::Node),
22
+ parent: T.nilable(Prism::Node),
23
+ nesting: T::Array[String],
24
+ call_node: T.nilable(Prism::CallNode),
25
+ ).void
26
+ end
27
+ def initialize(node, parent, nesting, call_node)
28
+ @node = node
29
+ @parent = parent
30
+ @nesting = nesting
31
+ @call_node = call_node
32
+ end
33
+
34
+ sig { returns(String) }
35
+ def fully_qualified_name
36
+ @fully_qualified_name ||= T.let(@nesting.join("::"), T.nilable(String))
37
+ end
38
+ end
39
+ end
@@ -24,6 +24,7 @@ module RubyLsp
24
24
  class CodeActionResolve < Request
25
25
  extend T::Sig
26
26
  NEW_VARIABLE_NAME = "new_variable"
27
+ NEW_METHOD_NAME = "new_method"
27
28
 
28
29
  class CodeActionError < StandardError; end
29
30
 
@@ -31,6 +32,7 @@ module RubyLsp
31
32
  enums do
32
33
  EmptySelection = new
33
34
  InvalidTargetRange = new
35
+ UnknownCodeAction = new
34
36
  end
35
37
  end
36
38
 
@@ -43,6 +45,18 @@ module RubyLsp
43
45
 
44
46
  sig { override.returns(T.any(Interface::CodeAction, Error)) }
45
47
  def perform
48
+ case @code_action[:title]
49
+ when CodeActions::EXTRACT_TO_VARIABLE_TITLE
50
+ refactor_variable
51
+ when CodeActions::EXTRACT_TO_METHOD_TITLE
52
+ refactor_method
53
+ else
54
+ Error::UnknownCodeAction
55
+ end
56
+ end
57
+
58
+ sig { returns(T.any(Interface::CodeAction, Error)) }
59
+ def refactor_variable
46
60
  return Error::EmptySelection if @document.source.empty?
47
61
 
48
62
  source_range = @code_action.dig(:data, :range)
@@ -54,9 +68,11 @@ module RubyLsp
54
68
  extracted_source = T.must(@document.source[start_index...end_index])
55
69
 
56
70
  # Find the closest statements node, so that we place the refactor in a valid position
57
- closest_statements, parent_statements = @document
71
+ node_context = @document
58
72
  .locate(@document.tree, start_index, node_types: [Prism::StatementsNode, Prism::BlockNode])
59
73
 
74
+ closest_statements = node_context.node
75
+ parent_statements = node_context.parent
60
76
  return Error::InvalidTargetRange if closest_statements.nil? || closest_statements.child_nodes.compact.empty?
61
77
 
62
78
  # Find the node with the end line closest to the requested position, so that we can place the refactor
@@ -117,7 +133,7 @@ module RubyLsp
117
133
  end
118
134
 
119
135
  Interface::CodeAction.new(
120
- title: "Refactor: Extract Variable",
136
+ title: CodeActions::EXTRACT_TO_VARIABLE_TITLE,
121
137
  edit: Interface::WorkspaceEdit.new(
122
138
  document_changes: [
123
139
  Interface::TextDocumentEdit.new(
@@ -135,6 +151,61 @@ module RubyLsp
135
151
  )
136
152
  end
137
153
 
154
+ sig { returns(T.any(Interface::CodeAction, Error)) }
155
+ def refactor_method
156
+ return Error::EmptySelection if @document.source.empty?
157
+
158
+ source_range = @code_action.dig(:data, :range)
159
+ return Error::EmptySelection if source_range[:start] == source_range[:end]
160
+
161
+ scanner = @document.create_scanner
162
+ start_index = scanner.find_char_position(source_range[:start])
163
+ end_index = scanner.find_char_position(source_range[:end])
164
+ extracted_source = T.must(@document.source[start_index...end_index])
165
+
166
+ # Find the closest method declaration node, so that we place the refactor in a valid position
167
+ node_context = @document.locate(@document.tree, start_index, node_types: [Prism::DefNode])
168
+ closest_def = T.cast(node_context.node, Prism::DefNode)
169
+ return Error::InvalidTargetRange if closest_def.nil?
170
+
171
+ end_keyword_loc = closest_def.end_keyword_loc
172
+ return Error::InvalidTargetRange if end_keyword_loc.nil?
173
+
174
+ end_line = end_keyword_loc.end_line - 1
175
+ character = end_keyword_loc.end_column
176
+ indentation = " " * end_keyword_loc.start_column
177
+ target_range = {
178
+ start: { line: end_line, character: character },
179
+ end: { line: end_line, character: character },
180
+ }
181
+
182
+ new_method_source = <<~RUBY.chomp
183
+
184
+
185
+ #{indentation}def #{NEW_METHOD_NAME}
186
+ #{indentation} #{extracted_source}
187
+ #{indentation}end
188
+ RUBY
189
+
190
+ Interface::CodeAction.new(
191
+ title: CodeActions::EXTRACT_TO_METHOD_TITLE,
192
+ edit: Interface::WorkspaceEdit.new(
193
+ document_changes: [
194
+ Interface::TextDocumentEdit.new(
195
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
196
+ uri: @code_action.dig(:data, :uri),
197
+ version: nil,
198
+ ),
199
+ edits: [
200
+ create_text_edit(target_range, new_method_source),
201
+ create_text_edit(source_range, NEW_METHOD_NAME),
202
+ ],
203
+ ),
204
+ ],
205
+ ),
206
+ )
207
+ end
208
+
138
209
  private
139
210
 
140
211
  sig { params(range: T::Hash[Symbol, T.untyped], new_text: String).returns(Interface::TextEdit) }