ruby-lsp 0.16.6 → 0.17.0

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 +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