ruby-lsp 0.16.6 → 0.17.0

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 +2 -2
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +21 -4
  5. data/exe/ruby-lsp-check +1 -3
  6. data/exe/ruby-lsp-doctor +1 -4
  7. data/lib/core_ext/uri.rb +3 -0
  8. data/lib/ruby_indexer/lib/ruby_indexer/{collector.rb → declaration_listener.rb} +258 -140
  9. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +101 -12
  10. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +187 -12
  11. data/lib/ruby_indexer/ruby_indexer.rb +1 -1
  12. data/lib/ruby_indexer/test/classes_and_modules_test.rb +106 -10
  13. data/lib/ruby_indexer/test/configuration_test.rb +4 -5
  14. data/lib/ruby_indexer/test/constant_test.rb +11 -8
  15. data/lib/ruby_indexer/test/index_test.rb +528 -0
  16. data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
  17. data/lib/ruby_indexer/test/method_test.rb +93 -0
  18. data/lib/ruby_indexer/test/test_case.rb +3 -1
  19. data/lib/ruby_lsp/addon.rb +8 -8
  20. data/lib/ruby_lsp/document.rb +3 -3
  21. data/lib/ruby_lsp/internal.rb +1 -0
  22. data/lib/ruby_lsp/listeners/code_lens.rb +11 -0
  23. data/lib/ruby_lsp/listeners/completion.rb +144 -51
  24. data/lib/ruby_lsp/listeners/definition.rb +77 -12
  25. data/lib/ruby_lsp/listeners/document_highlight.rb +1 -1
  26. data/lib/ruby_lsp/listeners/document_link.rb +1 -1
  27. data/lib/ruby_lsp/listeners/hover.rb +60 -6
  28. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +59 -3
  29. data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
  30. data/lib/ruby_lsp/node_context.rb +28 -0
  31. data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
  32. data/lib/ruby_lsp/requests/code_actions.rb +16 -15
  33. data/lib/ruby_lsp/requests/completion.rb +22 -13
  34. data/lib/ruby_lsp/requests/completion_resolve.rb +26 -10
  35. data/lib/ruby_lsp/requests/definition.rb +21 -5
  36. data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
  37. data/lib/ruby_lsp/requests/hover.rb +5 -6
  38. data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
  39. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  40. data/lib/ruby_lsp/requests/support/common.rb +20 -1
  41. data/lib/ruby_lsp/requests/workspace_symbol.rb +3 -1
  42. data/lib/ruby_lsp/server.rb +10 -4
  43. metadata +10 -8
@@ -14,24 +14,31 @@ 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(
31
31
  self,
32
32
  :on_call_node_enter,
33
+ :on_block_argument_node_enter,
33
34
  :on_constant_read_node_enter,
34
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,
35
42
  )
36
43
  end
37
44
 
@@ -42,10 +49,21 @@ module RubyLsp
42
49
  if message == :require || message == :require_relative
43
50
  handle_require_definition(node)
44
51
  else
45
- handle_method_definition(node)
52
+ handle_method_definition(message.to_s, self_receiver?(node))
46
53
  end
47
54
  end
48
55
 
56
+ sig { params(node: Prism::BlockArgumentNode).void }
57
+ def on_block_argument_node_enter(node)
58
+ expression = node.expression
59
+ return unless expression.is_a?(Prism::SymbolNode)
60
+
61
+ value = expression.value
62
+ return unless value
63
+
64
+ handle_method_definition(value, false)
65
+ end
66
+
49
67
  sig { params(node: Prism::ConstantPathNode).void }
50
68
  def on_constant_path_node_enter(node)
51
69
  name = constant_name(node)
@@ -62,15 +80,62 @@ module RubyLsp
62
80
  find_in_index(name)
63
81
  end
64
82
 
83
+ sig { params(node: Prism::InstanceVariableReadNode).void }
84
+ def on_instance_variable_read_node_enter(node)
85
+ handle_instance_variable_definition(node.name.to_s)
86
+ end
87
+
88
+ sig { params(node: Prism::InstanceVariableWriteNode).void }
89
+ def on_instance_variable_write_node_enter(node)
90
+ handle_instance_variable_definition(node.name.to_s)
91
+ end
92
+
93
+ sig { params(node: Prism::InstanceVariableAndWriteNode).void }
94
+ def on_instance_variable_and_write_node_enter(node)
95
+ handle_instance_variable_definition(node.name.to_s)
96
+ end
97
+
98
+ sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
99
+ def on_instance_variable_operator_write_node_enter(node)
100
+ handle_instance_variable_definition(node.name.to_s)
101
+ end
102
+
103
+ sig { params(node: Prism::InstanceVariableOrWriteNode).void }
104
+ def on_instance_variable_or_write_node_enter(node)
105
+ handle_instance_variable_definition(node.name.to_s)
106
+ end
107
+
108
+ sig { params(node: Prism::InstanceVariableTargetNode).void }
109
+ def on_instance_variable_target_node_enter(node)
110
+ handle_instance_variable_definition(node.name.to_s)
111
+ end
112
+
65
113
  private
66
114
 
67
- sig { params(node: Prism::CallNode).void }
68
- def handle_method_definition(node)
69
- message = node.message
70
- return unless message
115
+ sig { params(name: String).void }
116
+ def handle_instance_variable_definition(name)
117
+ entries = @index.resolve_instance_variable(name, @node_context.fully_qualified_name)
118
+ return unless entries
119
+
120
+ entries.each do |entry|
121
+ location = entry.location
122
+
123
+ @response_builder << Interface::Location.new(
124
+ uri: URI::Generic.from_path(path: entry.file_path).to_s,
125
+ range: Interface::Range.new(
126
+ start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
127
+ end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
128
+ ),
129
+ )
130
+ end
131
+ rescue RubyIndexer::Index::NonExistingNamespaceError
132
+ # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
133
+ end
71
134
 
72
- methods = if self_receiver?(node)
73
- @index.resolve_method(message, @nesting.join("::"))
135
+ sig { params(message: String, self_receiver: T::Boolean).void }
136
+ def handle_method_definition(message, self_receiver)
137
+ methods = if self_receiver
138
+ @index.resolve_method(message, @node_context.fully_qualified_name)
74
139
  else
75
140
  # If the method doesn't have a receiver, then we provide a few candidates to jump to
76
141
  # But we don't want to provide too many candidates, as it can be overwhelming
@@ -138,13 +203,13 @@ module RubyLsp
138
203
 
139
204
  sig { params(value: String).void }
140
205
  def find_in_index(value)
141
- entries = @index.resolve(value, @nesting)
206
+ entries = @index.resolve(value, @node_context.nesting)
142
207
  return unless entries
143
208
 
144
209
  # We should only allow jumping to the definition of private constants if the constant is defined in the same
145
210
  # namespace as the reference
146
211
  first_entry = T.must(entries.first)
147
- return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{value}"
212
+ return if first_entry.private? && first_entry.name != "#{@node_context.fully_qualified_name}::#{value}"
148
213
 
149
214
  entries.each do |entry|
150
215
  location = entry.location
@@ -271,7 +271,7 @@ module RubyLsp
271
271
  def on_constant_path_node_enter(node)
272
272
  return unless matches?(node, CONSTANT_PATH_NODES)
273
273
 
274
- add_highlight(Constant::DocumentHighlightKind::READ, node.location)
274
+ add_highlight(Constant::DocumentHighlightKind::READ, node.name_loc)
275
275
  end
276
276
 
277
277
  sig { params(node: Prism::ConstantReadNode).void }
@@ -30,7 +30,7 @@ module RubyLsp
30
30
  lookup[spec.name] = {}
31
31
  lookup[spec.name][spec.version.to_s] = {}
32
32
 
33
- Dir.glob("**/*.rb", base: "#{spec.full_gem_path}/").each do |path|
33
+ Dir.glob("**/*.rb", base: "#{spec.full_gem_path.delete_prefix("//?/")}/").each do |path|
34
34
  lookup[spec.name][spec.version.to_s][path] = "#{spec.full_gem_path}/#{path}"
35
35
  end
36
36
  end
@@ -13,6 +13,12 @@ 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,
16
22
  ],
17
23
  T::Array[T.class_of(Prism::Node)],
18
24
  )
@@ -30,17 +36,17 @@ module RubyLsp
30
36
  response_builder: ResponseBuilders::Hover,
31
37
  global_state: GlobalState,
32
38
  uri: URI::Generic,
33
- nesting: T::Array[String],
39
+ node_context: NodeContext,
34
40
  dispatcher: Prism::Dispatcher,
35
41
  typechecker_enabled: T::Boolean,
36
42
  ).void
37
43
  end
38
- def initialize(response_builder, global_state, uri, nesting, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
44
+ def initialize(response_builder, global_state, uri, node_context, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
39
45
  @response_builder = response_builder
40
46
  @global_state = global_state
41
47
  @index = T.let(global_state.index, RubyIndexer::Index)
42
48
  @path = T.let(uri.to_standardized_path, T.nilable(String))
43
- @nesting = nesting
49
+ @node_context = node_context
44
50
  @typechecker_enabled = typechecker_enabled
45
51
 
46
52
  dispatcher.register(
@@ -49,6 +55,12 @@ module RubyLsp
49
55
  :on_constant_write_node_enter,
50
56
  :on_constant_path_node_enter,
51
57
  :on_call_node_enter,
58
+ :on_instance_variable_read_node_enter,
59
+ :on_instance_variable_write_node_enter,
60
+ :on_instance_variable_and_write_node_enter,
61
+ :on_instance_variable_operator_write_node_enter,
62
+ :on_instance_variable_or_write_node_enter,
63
+ :on_instance_variable_target_node_enter,
52
64
  )
53
65
  end
54
66
 
@@ -93,7 +105,7 @@ module RubyLsp
93
105
  message = node.message
94
106
  return unless message
95
107
 
96
- methods = @index.resolve_method(message, @nesting.join("::"))
108
+ methods = @index.resolve_method(message, @node_context.fully_qualified_name)
97
109
  return unless methods
98
110
 
99
111
  categorized_markdown_from_index_entries(message, methods).each do |category, content|
@@ -101,17 +113,59 @@ module RubyLsp
101
113
  end
102
114
  end
103
115
 
116
+ sig { params(node: Prism::InstanceVariableReadNode).void }
117
+ def on_instance_variable_read_node_enter(node)
118
+ handle_instance_variable_hover(node.name.to_s)
119
+ end
120
+
121
+ sig { params(node: Prism::InstanceVariableWriteNode).void }
122
+ def on_instance_variable_write_node_enter(node)
123
+ handle_instance_variable_hover(node.name.to_s)
124
+ end
125
+
126
+ sig { params(node: Prism::InstanceVariableAndWriteNode).void }
127
+ def on_instance_variable_and_write_node_enter(node)
128
+ handle_instance_variable_hover(node.name.to_s)
129
+ end
130
+
131
+ sig { params(node: Prism::InstanceVariableOperatorWriteNode).void }
132
+ def on_instance_variable_operator_write_node_enter(node)
133
+ handle_instance_variable_hover(node.name.to_s)
134
+ end
135
+
136
+ sig { params(node: Prism::InstanceVariableOrWriteNode).void }
137
+ def on_instance_variable_or_write_node_enter(node)
138
+ handle_instance_variable_hover(node.name.to_s)
139
+ end
140
+
141
+ sig { params(node: Prism::InstanceVariableTargetNode).void }
142
+ def on_instance_variable_target_node_enter(node)
143
+ handle_instance_variable_hover(node.name.to_s)
144
+ end
145
+
104
146
  private
105
147
 
148
+ sig { params(name: String).void }
149
+ def handle_instance_variable_hover(name)
150
+ entries = @index.resolve_instance_variable(name, @node_context.fully_qualified_name)
151
+ return unless entries
152
+
153
+ categorized_markdown_from_index_entries(name, entries).each do |category, content|
154
+ @response_builder.push(content, category: category)
155
+ end
156
+ rescue RubyIndexer::Index::NonExistingNamespaceError
157
+ # If by any chance we haven't indexed the owner, then there's no way to find the right declaration
158
+ end
159
+
106
160
  sig { params(name: String, location: Prism::Location).void }
107
161
  def generate_hover(name, location)
108
- entries = @index.resolve(name, @nesting)
162
+ entries = @index.resolve(name, @node_context.nesting)
109
163
  return unless entries
110
164
 
111
165
  # We should only show hover for private constants if the constant is defined in the same namespace as the
112
166
  # reference
113
167
  first_entry = T.must(entries.first)
114
- return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{name}"
168
+ return if first_entry.private? && first_entry.name != "#{@node_context.fully_qualified_name}::#{name}"
115
169
 
116
170
  categorized_markdown_from_index_entries(name, entries).each do |category, content|
117
171
  @response_builder.push(content, category: category)
@@ -58,6 +58,7 @@ module RubyLsp
58
58
  :on_constant_operator_write_node_enter,
59
59
  :on_constant_or_write_node_enter,
60
60
  :on_constant_target_node_enter,
61
+ :on_constant_path_node_enter,
61
62
  :on_local_variable_and_write_node_enter,
62
63
  :on_local_variable_operator_write_node_enter,
63
64
  :on_local_variable_or_write_node_enter,
@@ -302,17 +303,64 @@ module RubyLsp
302
303
  def on_class_node_enter(node)
303
304
  return unless visible?(node, @range)
304
305
 
305
- @response_builder.add_token(node.constant_path.location, :class, [:declaration])
306
+ constant_path = node.constant_path
307
+
308
+ if constant_path.is_a?(Prism::ConstantReadNode)
309
+ @response_builder.add_token(constant_path.location, :class, [:declaration])
310
+ else
311
+ each_constant_path_part(constant_path) do |part|
312
+ loc = case part
313
+ when Prism::ConstantPathNode
314
+ part.name_loc
315
+ when Prism::ConstantReadNode
316
+ part.location
317
+ end
318
+ next unless loc
319
+
320
+ @response_builder.add_token(loc, :class, [:declaration])
321
+ end
322
+ end
306
323
 
307
324
  superclass = node.superclass
308
- @response_builder.add_token(superclass.location, :class) if superclass
325
+
326
+ if superclass.is_a?(Prism::ConstantReadNode)
327
+ @response_builder.add_token(superclass.location, :class)
328
+ elsif superclass
329
+ each_constant_path_part(superclass) do |part|
330
+ loc = case part
331
+ when Prism::ConstantPathNode
332
+ part.name_loc
333
+ when Prism::ConstantReadNode
334
+ part.location
335
+ end
336
+ next unless loc
337
+
338
+ @response_builder.add_token(loc, :class)
339
+ end
340
+ end
309
341
  end
310
342
 
311
343
  sig { params(node: Prism::ModuleNode).void }
312
344
  def on_module_node_enter(node)
313
345
  return unless visible?(node, @range)
314
346
 
315
- @response_builder.add_token(node.constant_path.location, :namespace, [:declaration])
347
+ constant_path = node.constant_path
348
+
349
+ if constant_path.is_a?(Prism::ConstantReadNode)
350
+ @response_builder.add_token(constant_path.location, :namespace, [:declaration])
351
+ else
352
+ each_constant_path_part(constant_path) do |part|
353
+ loc = case part
354
+ when Prism::ConstantPathNode
355
+ part.name_loc
356
+ when Prism::ConstantReadNode
357
+ part.location
358
+ end
359
+ next unless loc
360
+
361
+ @response_builder.add_token(loc, :namespace, [:declaration])
362
+ end
363
+ end
316
364
  end
317
365
 
318
366
  sig { params(node: Prism::ImplicitNode).void }
@@ -327,6 +375,14 @@ module RubyLsp
327
375
  @inside_implicit_node = false
328
376
  end
329
377
 
378
+ sig { params(node: Prism::ConstantPathNode).void }
379
+ def on_constant_path_node_enter(node)
380
+ return if @inside_implicit_node
381
+ return unless visible?(node, @range)
382
+
383
+ @response_builder.add_token(node.name_loc, :namespace)
384
+ end
385
+
330
386
  private
331
387
 
332
388
  # Textmate provides highlighting for a subset of these special Ruby-specific methods. We want to utilize that
@@ -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,28 @@
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
+ # and its namespace nesting.
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 { params(node: T.nilable(Prism::Node), parent: T.nilable(Prism::Node), nesting: T::Array[String]).void }
17
+ def initialize(node, parent, nesting)
18
+ @node = node
19
+ @parent = parent
20
+ @nesting = nesting
21
+ end
22
+
23
+ sig { returns(String) }
24
+ def fully_qualified_name
25
+ @fully_qualified_name ||= T.let(@nesting.join("::"), T.nilable(String))
26
+ end
27
+ end
28
+ 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) }
@@ -19,6 +19,9 @@ module RubyLsp
19
19
  class CodeActions < Request
20
20
  extend T::Sig
21
21
 
22
+ EXTRACT_TO_VARIABLE_TITLE = "Refactor: Extract Variable"
23
+ EXTRACT_TO_METHOD_TITLE = "Refactor: Extract Method"
24
+
22
25
  class << self
23
26
  extend T::Sig
24
27
 
@@ -52,22 +55,20 @@ module RubyLsp
52
55
  end
53
56
 
54
57
  # Only add refactor actions if there's a non empty selection in the editor
55
- code_actions << refactor_code_action(@range, @uri) unless @range.dig(:start) == @range.dig(:end)
56
- code_actions
57
- end
58
-
59
- private
58
+ unless @range.dig(:start) == @range.dig(:end)
59
+ code_actions << Interface::CodeAction.new(
60
+ title: EXTRACT_TO_VARIABLE_TITLE,
61
+ kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
62
+ data: { range: @range, uri: @uri.to_s },
63
+ )
64
+ code_actions << Interface::CodeAction.new(
65
+ title: EXTRACT_TO_METHOD_TITLE,
66
+ kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
67
+ data: { range: @range, uri: @uri.to_s },
68
+ )
69
+ end
60
70
 
61
- sig { params(range: T::Hash[Symbol, T.untyped], uri: URI::Generic).returns(Interface::CodeAction) }
62
- def refactor_code_action(range, uri)
63
- Interface::CodeAction.new(
64
- title: "Refactor: Extract Variable",
65
- kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
66
- data: {
67
- range: range,
68
- uri: uri.to_s,
69
- },
70
- )
71
+ code_actions
71
72
  end
72
73
  end
73
74
  end
@@ -11,11 +11,13 @@ module RubyLsp
11
11
  # suggests possible completions according to what the developer is typing.
12
12
  #
13
13
  # Currently supported targets:
14
+ #
14
15
  # - Classes
15
16
  # - Modules
16
17
  # - Constants
17
18
  # - Require paths
18
19
  # - Methods invoked on self only
20
+ # - Instance variables
19
21
  #
20
22
  # # Example
21
23
  #
@@ -34,7 +36,7 @@ module RubyLsp
34
36
  def provider
35
37
  Interface::CompletionOptions.new(
36
38
  resolve_provider: true,
37
- trigger_characters: ["/", "\"", "'"],
39
+ trigger_characters: ["/", "\"", "'", ":", "@"],
38
40
  completion_item: {
39
41
  labelDetailsSupport: true,
40
42
  },
@@ -58,10 +60,20 @@ module RubyLsp
58
60
  # Completion always receives the position immediately after the character that was just typed. Here we adjust it
59
61
  # back by 1, so that we find the right node
60
62
  char_position = document.create_scanner.find_char_position(position) - 1
61
- matched, parent, nesting = document.locate(
63
+ node_context = document.locate(
62
64
  document.tree,
63
65
  char_position,
64
- node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode],
66
+ node_types: [
67
+ Prism::CallNode,
68
+ Prism::ConstantReadNode,
69
+ Prism::ConstantPathNode,
70
+ Prism::InstanceVariableReadNode,
71
+ Prism::InstanceVariableAndWriteNode,
72
+ Prism::InstanceVariableOperatorWriteNode,
73
+ Prism::InstanceVariableOrWriteNode,
74
+ Prism::InstanceVariableTargetNode,
75
+ Prism::InstanceVariableWriteNode,
76
+ ],
65
77
  )
66
78
  @response_builder = T.let(
67
79
  ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem].new,
@@ -71,27 +83,24 @@ module RubyLsp
71
83
  Listeners::Completion.new(
72
84
  @response_builder,
73
85
  global_state,
74
- nesting,
86
+ node_context,
75
87
  typechecker_enabled,
76
88
  dispatcher,
77
89
  document.uri,
78
90
  )
79
91
 
80
92
  Addon.addons.each do |addon|
81
- addon.create_completion_listener(@response_builder, nesting, dispatcher, document.uri)
93
+ addon.create_completion_listener(@response_builder, node_context, dispatcher, document.uri)
82
94
  end
83
95
 
96
+ matched = node_context.node
97
+ parent = node_context.parent
84
98
  return unless matched && parent
85
99
 
86
- @target = case matched
87
- when Prism::CallNode
100
+ @target = if parent.is_a?(Prism::ConstantPathNode) && matched.is_a?(Prism::ConstantReadNode)
101
+ parent
102
+ else
88
103
  matched
89
- when Prism::ConstantReadNode, Prism::ConstantPathNode
90
- if parent.is_a?(Prism::ConstantPathNode) && matched.is_a?(Prism::ConstantReadNode)
91
- parent
92
- else
93
- matched
94
- end
95
104
  end
96
105
  end
97
106