ruby-lsp 0.16.7 → 0.17.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +139 -18
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +101 -12
  6. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +183 -10
  7. data/lib/ruby_indexer/test/classes_and_modules_test.rb +55 -9
  8. data/lib/ruby_indexer/test/configuration_test.rb +4 -5
  9. data/lib/ruby_indexer/test/constant_test.rb +8 -8
  10. data/lib/ruby_indexer/test/index_test.rb +528 -0
  11. data/lib/ruby_indexer/test/instance_variables_test.rb +131 -0
  12. data/lib/ruby_indexer/test/method_test.rb +37 -0
  13. data/lib/ruby_indexer/test/test_case.rb +3 -1
  14. data/lib/ruby_lsp/addon.rb +8 -8
  15. data/lib/ruby_lsp/document.rb +26 -3
  16. data/lib/ruby_lsp/internal.rb +1 -0
  17. data/lib/ruby_lsp/listeners/completion.rb +74 -17
  18. data/lib/ruby_lsp/listeners/definition.rb +82 -24
  19. data/lib/ruby_lsp/listeners/hover.rb +62 -6
  20. data/lib/ruby_lsp/listeners/signature_help.rb +4 -4
  21. data/lib/ruby_lsp/node_context.rb +39 -0
  22. data/lib/ruby_lsp/requests/code_action_resolve.rb +73 -2
  23. data/lib/ruby_lsp/requests/code_actions.rb +16 -15
  24. data/lib/ruby_lsp/requests/completion.rb +21 -13
  25. data/lib/ruby_lsp/requests/completion_resolve.rb +9 -0
  26. data/lib/ruby_lsp/requests/definition.rb +25 -5
  27. data/lib/ruby_lsp/requests/document_highlight.rb +2 -2
  28. data/lib/ruby_lsp/requests/hover.rb +5 -6
  29. data/lib/ruby_lsp/requests/on_type_formatting.rb +8 -4
  30. data/lib/ruby_lsp/requests/signature_help.rb +3 -3
  31. data/lib/ruby_lsp/requests/support/common.rb +4 -3
  32. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +23 -6
  33. data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +5 -1
  34. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
  35. data/lib/ruby_lsp/requests/workspace_symbol.rb +7 -4
  36. data/lib/ruby_lsp/server.rb +11 -5
  37. data/lib/ruby_lsp/test_helper.rb +1 -0
  38. metadata +5 -3
@@ -15,7 +15,7 @@ module RubyIndexer
15
15
  @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), source)
16
16
  end
17
17
 
18
- def assert_entry(expected_name, type, expected_location)
18
+ def assert_entry(expected_name, type, expected_location, visibility: nil)
19
19
  entries = @index[expected_name]
20
20
  refute_empty(entries, "Expected #{expected_name} to be indexed")
21
21
 
@@ -28,6 +28,8 @@ module RubyIndexer
28
28
  ":#{location.end_line - 1}-#{location.end_column}"
29
29
 
30
30
  assert_equal(expected_location, location_string)
31
+
32
+ assert_equal(visibility, entry.visibility) if visibility
31
33
  end
32
34
 
33
35
  def refute_entry(expected_name)
@@ -99,8 +99,8 @@ module RubyLsp
99
99
  end
100
100
 
101
101
  sig { returns(String) }
102
- def backtraces
103
- @errors.filter_map(&:backtrace).join("\n\n")
102
+ def errors_details
103
+ @errors.map(&:full_message).join("\n\n")
104
104
  end
105
105
 
106
106
  # Each addon should implement `MyAddon#activate` and use to perform any sort of initialization, such as
@@ -131,11 +131,11 @@ module RubyLsp
131
131
  sig do
132
132
  overridable.params(
133
133
  response_builder: ResponseBuilders::Hover,
134
- nesting: T::Array[String],
134
+ node_context: NodeContext,
135
135
  dispatcher: Prism::Dispatcher,
136
136
  ).void
137
137
  end
138
- def create_hover_listener(response_builder, nesting, dispatcher); end
138
+ def create_hover_listener(response_builder, node_context, dispatcher); end
139
139
 
140
140
  # Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request
141
141
  sig do
@@ -159,21 +159,21 @@ module RubyLsp
159
159
  overridable.params(
160
160
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
161
161
  uri: URI::Generic,
162
- nesting: T::Array[String],
162
+ node_context: NodeContext,
163
163
  dispatcher: Prism::Dispatcher,
164
164
  ).void
165
165
  end
166
- def create_definition_listener(response_builder, uri, nesting, dispatcher); end
166
+ def create_definition_listener(response_builder, uri, node_context, dispatcher); end
167
167
 
168
168
  # Creates a new Completion listener. This method is invoked on every Completion request
169
169
  sig do
170
170
  overridable.params(
171
171
  response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
172
- nesting: T::Array[String],
172
+ node_context: NodeContext,
173
173
  dispatcher: Prism::Dispatcher,
174
174
  uri: URI::Generic,
175
175
  ).void
176
176
  end
177
- def create_completion_listener(response_builder, nesting, dispatcher, uri); end
177
+ def create_completion_listener(response_builder, node_context, dispatcher, uri); end
178
178
  end
179
179
  end
@@ -110,7 +110,7 @@ module RubyLsp
110
110
  params(
111
111
  position: T::Hash[Symbol, T.untyped],
112
112
  node_types: T::Array[T.class_of(Prism::Node)],
113
- ).returns([T.nilable(Prism::Node), T.nilable(Prism::Node), T::Array[String]])
113
+ ).returns(NodeContext)
114
114
  end
115
115
  def locate_node(position, node_types: [])
116
116
  locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
@@ -121,13 +121,14 @@ module RubyLsp
121
121
  node: Prism::Node,
122
122
  char_position: Integer,
123
123
  node_types: T::Array[T.class_of(Prism::Node)],
124
- ).returns([T.nilable(Prism::Node), T.nilable(Prism::Node), T::Array[String]])
124
+ ).returns(NodeContext)
125
125
  end
126
126
  def locate(node, char_position, node_types: [])
127
127
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
128
128
  closest = node
129
129
  parent = T.let(nil, T.nilable(Prism::Node))
130
130
  nesting = T.let([], T::Array[T.any(Prism::ClassNode, Prism::ModuleNode)])
131
+ call_node = T.let(nil, T.nilable(Prism::CallNode))
131
132
 
132
133
  until queue.empty?
133
134
  candidate = queue.shift
@@ -159,6 +160,15 @@ module RubyLsp
159
160
  nesting << candidate
160
161
  end
161
162
 
163
+ if candidate.is_a?(Prism::CallNode)
164
+ arg_loc = candidate.arguments&.location
165
+ blk_loc = candidate.block&.location
166
+ if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) ||
167
+ (blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position))
168
+ call_node = candidate
169
+ end
170
+ end
171
+
162
172
  # If there are node types to filter by, and the current node is not one of those types, then skip it
163
173
  next if node_types.any? && node_types.none? { |type| candidate.class == type }
164
174
 
@@ -170,7 +180,20 @@ module RubyLsp
170
180
  end
171
181
  end
172
182
 
173
- [closest, parent, nesting.map { |n| n.constant_path.location.slice }]
183
+ # When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated. That
184
+ # is, when targeting Bar in the following example:
185
+ #
186
+ # ```ruby
187
+ # class Foo::Bar; end
188
+ # ```
189
+ # The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, even
190
+ # though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
191
+ if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode)
192
+ last_level = nesting.last
193
+ nesting.pop if last_level && last_level.constant_path == closest
194
+ end
195
+
196
+ NodeContext.new(closest, parent, nesting.map { |n| n.constant_path.location.slice }, call_node)
174
197
  end
175
198
 
176
199
  sig { returns(T::Boolean) }
@@ -29,6 +29,7 @@ require "ruby_lsp/global_state"
29
29
  require "ruby_lsp/server"
30
30
  require "ruby_lsp/requests"
31
31
  require "ruby_lsp/response_builders"
32
+ require "ruby_lsp/node_context"
32
33
  require "ruby_lsp/document"
33
34
  require "ruby_lsp/ruby_document"
34
35
  require "ruby_lsp/store"
@@ -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