ruby-lsp 0.16.7 → 0.17.3

Sign up to get free protection for your applications and to get access to all the features.
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) }