Skip to content

Commit 0f1d535

Browse files
andyw8vinistock
andauthored
Introduce NodeContext for listeners (#2099)
* Introduce TargetContext * Update tests * Update ADDONS docs * Fix types * Remove accidental comment * Cleanup * Update lib/ruby_lsp/listeners/completion.rb Co-authored-by: Vinicius Stock <[email protected]> * Rename to NodeContext * Rename 'closest' to 'node' * Rename * Rename file * Lint * Fix naming * Explain --------- Co-authored-by: Vinicius Stock <[email protected]>
1 parent a5e04f7 commit 0f1d535

17 files changed

+121
-90
lines changed

ADDONS.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ module RubyLsp
143143
"Ruby LSP My Gem"
144144
end
145145

146-
def create_hover_listener(response_builder, nesting, index, dispatcher)
146+
def create_hover_listener(response_builder, node_context, index, dispatcher)
147147
# Use the listener factory methods to instantiate listeners with parameters sent by the LSP combined with any
148148
# pre-computed information in the addon. These factory methods are invoked on every request
149149
Hover.new(client, response_builder, @config, dispatcher)
@@ -259,13 +259,13 @@ module RubyLsp
259259
"Ruby LSP My Gem"
260260
end
261261

262-
def create_hover_listener(response_builder, nesting, index, dispatcher)
263-
MyHoverListener.new(@message_queue, response_builder, nesting, index, dispatcher)
262+
def create_hover_listener(response_builder, node_context, index, dispatcher)
263+
MyHoverListener.new(@message_queue, response_builder, node_context, index, dispatcher)
264264
end
265265
end
266266

267267
class MyHoverListener
268-
def initialize(message_queue, response_builder, nesting, index, dispatcher)
268+
def initialize(message_queue, response_builder, node_context, index, dispatcher)
269269
@message_queue = message_queue
270270

271271
@message_queue << Notification.new(

lib/ruby_lsp/addon.rb

+6-6
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,11 @@ def create_code_lens_listener(response_builder, uri, dispatcher); end
131131
sig do
132132
overridable.params(
133133
response_builder: ResponseBuilders::Hover,
134-
nesting: T::Array[String],
134+
node_context: NodeContext,
135135
dispatcher: Prism::Dispatcher,
136136
).void
137137
end
138-
def create_hover_listener(response_builder, nesting, dispatcher); end
138+
def create_hover_listener(response_builder, node_context, dispatcher); end
139139

140140
# Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request
141141
sig do
@@ -159,21 +159,21 @@ def create_semantic_highlighting_listener(response_builder, dispatcher); end
159159
overridable.params(
160160
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
161161
uri: URI::Generic,
162-
nesting: T::Array[String],
162+
node_context: NodeContext,
163163
dispatcher: Prism::Dispatcher,
164164
).void
165165
end
166-
def create_definition_listener(response_builder, uri, nesting, dispatcher); end
166+
def create_definition_listener(response_builder, uri, node_context, dispatcher); end
167167

168168
# Creates a new Completion listener. This method is invoked on every Completion request
169169
sig do
170170
overridable.params(
171171
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
172-
nesting: T::Array[String],
172+
node_context: NodeContext,
173173
dispatcher: Prism::Dispatcher,
174174
uri: URI::Generic,
175175
).void
176176
end
177-
def create_completion_listener(response_builder, nesting, dispatcher, uri); end
177+
def create_completion_listener(response_builder, node_context, dispatcher, uri); end
178178
end
179179
end

lib/ruby_lsp/document.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def create_scanner
110110
params(
111111
position: T::Hash[Symbol, T.untyped],
112112
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)
114114
end
115115
def locate_node(position, node_types: [])
116116
locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
@@ -121,7 +121,7 @@ def locate_node(position, node_types: [])
121121
node: Prism::Node,
122122
char_position: Integer,
123123
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)
125125
end
126126
def locate(node, char_position, node_types: [])
127127
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
@@ -170,7 +170,7 @@ def locate(node, char_position, node_types: [])
170170
end
171171
end
172172

173-
[closest, parent, nesting.map { |n| n.constant_path.location.slice }]
173+
NodeContext.new(closest, parent, nesting.map { |n| n.constant_path.location.slice })
174174
end
175175

176176
sig { returns(T::Boolean) }

lib/ruby_lsp/internal.rb

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
require "ruby_lsp/server"
3030
require "ruby_lsp/requests"
3131
require "ruby_lsp/response_builders"
32+
require "ruby_lsp/node_context"
3233
require "ruby_lsp/document"
3334
require "ruby_lsp/ruby_document"
3435
require "ruby_lsp/store"

lib/ruby_lsp/listeners/completion.rb

+17-14
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ class Completion
1111
params(
1212
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
1313
global_state: GlobalState,
14-
nesting: T::Array[String],
14+
node_context: NodeContext,
1515
typechecker_enabled: T::Boolean,
1616
dispatcher: Prism::Dispatcher,
1717
uri: URI::Generic,
1818
).void
1919
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
2121
@response_builder = response_builder
2222
@global_state = global_state
2323
@index = T.let(global_state.index, RubyIndexer::Index)
24-
@nesting = nesting
24+
@node_context = node_context
2525
@typechecker_enabled = typechecker_enabled
2626
@uri = uri
2727

@@ -47,7 +47,7 @@ def on_constant_read_node_enter(node)
4747
name = constant_name(node)
4848
return if name.nil?
4949

50-
candidates = @index.prefix_search(name, @nesting)
50+
candidates = @index.prefix_search(name, @node_context.nesting)
5151
candidates.each do |entries|
5252
complete_name = T.must(entries.first).name
5353
@response_builder << build_entry_completion(
@@ -161,20 +161,21 @@ def constant_path_completion(name, range)
161161
T.must(namespace).join("::")
162162
end
163163

164-
namespace_entries = @index.resolve(aliased_namespace, @nesting)
164+
nesting = @node_context.nesting
165+
namespace_entries = @index.resolve(aliased_namespace, nesting)
165166
return unless namespace_entries
166167

167168
real_namespace = @index.follow_aliased_namespace(T.must(namespace_entries.first).name)
168169

169170
candidates = @index.prefix_search(
170171
"#{real_namespace}::#{incomplete_name}",
171-
top_level_reference ? [] : @nesting,
172+
top_level_reference ? [] : nesting,
172173
)
173174
candidates.each do |entries|
174175
# The only time we may have a private constant reference from outside of the namespace is if we're dealing
175176
# with ConstantPath and the entry name doesn't start with the current nesting
176177
first_entry = T.must(entries.first)
177-
next if first_entry.private? && !first_entry.name.start_with?("#{@nesting}::")
178+
next if first_entry.private? && !first_entry.name.start_with?("#{nesting}::")
178179

179180
constant_name = first_entry.name.delete_prefix("#{real_namespace}::")
180181
full_name = aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}"
@@ -191,7 +192,7 @@ def constant_path_completion(name, range)
191192

192193
sig { params(name: String, location: Prism::Location).void }
193194
def handle_instance_variable_completion(name, location)
194-
@index.instance_variable_completion_candidates(name, @nesting.join("::")).each do |entry|
195+
@index.instance_variable_completion_candidates(name, @node_context.nesting.join("::")).each do |entry|
195196
variable_name = entry.name
196197

197198
@response_builder << Interface::CompletionItem.new(
@@ -257,7 +258,7 @@ def complete_require_relative(node)
257258

258259
sig { params(node: Prism::CallNode, name: String).void }
259260
def complete_self_receiver_method(node, name)
260-
receiver_entries = @index[@nesting.join("::")]
261+
receiver_entries = @index[@node_context.nesting.join("::")]
261262
return unless receiver_entries
262263

263264
receiver = T.must(receiver_entries.first)
@@ -351,13 +352,14 @@ def build_entry_completion(real_name, incomplete_name, range, entries, top_level
351352
#
352353
# Foo::B # --> completion inserts `Bar` instead of `Foo::Bar`
353354
# end
354-
unless @nesting.join("::").start_with?(incomplete_name)
355-
@nesting.each do |namespace|
355+
nesting = @node_context.nesting
356+
unless nesting.join("::").start_with?(incomplete_name)
357+
nesting.each do |namespace|
356358
prefix = "#{namespace}::"
357359
shortened_name = insertion_text.delete_prefix(prefix)
358360

359361
# If a different entry exists for the shortened name, then there's a conflict and we should not shorten it
360-
conflict_name = "#{@nesting.join("::")}::#{shortened_name}"
362+
conflict_name = "#{nesting.join("::")}::#{shortened_name}"
361363
break if real_name != conflict_name && @index[conflict_name]
362364

363365
insertion_text = shortened_name
@@ -399,8 +401,9 @@ def build_entry_completion(real_name, incomplete_name, range, entries, top_level
399401
# ```
400402
sig { params(entry_name: String).returns(T::Boolean) }
401403
def top_level?(entry_name)
402-
@nesting.length.downto(0).each do |i|
403-
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("::")
404407
full_name = prefix.empty? ? entry_name : "#{prefix}::#{entry_name}"
405408
next if full_name == entry_name
406409

lib/ruby_lsp/listeners/definition.rb

+7-7
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ class Definition
1414
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
1515
global_state: GlobalState,
1616
uri: URI::Generic,
17-
nesting: T::Array[String],
17+
node_context: NodeContext,
1818
dispatcher: Prism::Dispatcher,
1919
typechecker_enabled: T::Boolean,
2020
).void
2121
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
2323
@response_builder = response_builder
2424
@global_state = global_state
2525
@index = T.let(global_state.index, RubyIndexer::Index)
2626
@uri = uri
27-
@nesting = nesting
27+
@node_context = node_context
2828
@typechecker_enabled = typechecker_enabled
2929

3030
dispatcher.register(
@@ -114,7 +114,7 @@ def on_instance_variable_target_node_enter(node)
114114

115115
sig { params(name: String).void }
116116
def handle_instance_variable_definition(name)
117-
entries = @index.resolve_instance_variable(name, @nesting.join("::"))
117+
entries = @index.resolve_instance_variable(name, @node_context.nesting.join("::"))
118118
return unless entries
119119

120120
entries.each do |entry|
@@ -135,7 +135,7 @@ def handle_instance_variable_definition(name)
135135
sig { params(message: String, self_receiver: T::Boolean).void }
136136
def handle_method_definition(message, self_receiver)
137137
methods = if self_receiver
138-
@index.resolve_method(message, @nesting.join("::"))
138+
@index.resolve_method(message, @node_context.nesting.join("::"))
139139
else
140140
# If the method doesn't have a receiver, then we provide a few candidates to jump to
141141
# But we don't want to provide too many candidates, as it can be overwhelming
@@ -203,13 +203,13 @@ def handle_require_definition(node)
203203

204204
sig { params(value: String).void }
205205
def find_in_index(value)
206-
entries = @index.resolve(value, @nesting)
206+
entries = @index.resolve(value, @node_context.nesting)
207207
return unless entries
208208

209209
# We should only allow jumping to the definition of private constants if the constant is defined in the same
210210
# namespace as the reference
211211
first_entry = T.must(entries.first)
212-
return if first_entry.private? && first_entry.name != "#{@nesting.join("::")}::#{value}"
212+
return if first_entry.private? && first_entry.name != "#{@node_context.nesting.join("::")}::#{value}"
213213

214214
entries.each do |entry|
215215
location = entry.location

lib/ruby_lsp/listeners/hover.rb

+7-7
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,17 @@ class Hover
3636
response_builder: ResponseBuilders::Hover,
3737
global_state: GlobalState,
3838
uri: URI::Generic,
39-
nesting: T::Array[String],
39+
node_context: NodeContext,
4040
dispatcher: Prism::Dispatcher,
4141
typechecker_enabled: T::Boolean,
4242
).void
4343
end
44-
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
4545
@response_builder = response_builder
4646
@global_state = global_state
4747
@index = T.let(global_state.index, RubyIndexer::Index)
4848
@path = T.let(uri.to_standardized_path, T.nilable(String))
49-
@nesting = nesting
49+
@node_context = node_context
5050
@typechecker_enabled = typechecker_enabled
5151

5252
dispatcher.register(
@@ -105,7 +105,7 @@ def on_call_node_enter(node)
105105
message = node.message
106106
return unless message
107107

108-
methods = @index.resolve_method(message, @nesting.join("::"))
108+
methods = @index.resolve_method(message, @node_context.nesting.join("::"))
109109
return unless methods
110110

111111
categorized_markdown_from_index_entries(message, methods).each do |category, content|
@@ -147,7 +147,7 @@ def on_instance_variable_target_node_enter(node)
147147

148148
sig { params(name: String).void }
149149
def handle_instance_variable_hover(name)
150-
entries = @index.resolve_instance_variable(name, @nesting.join("::"))
150+
entries = @index.resolve_instance_variable(name, @node_context.nesting.join("::"))
151151
return unless entries
152152

153153
categorized_markdown_from_index_entries(name, entries).each do |category, content|
@@ -159,13 +159,13 @@ def handle_instance_variable_hover(name)
159159

160160
sig { params(name: String, location: Prism::Location).void }
161161
def generate_hover(name, location)
162-
entries = @index.resolve(name, @nesting)
162+
entries = @index.resolve(name, @node_context.nesting)
163163
return unless entries
164164

165165
# We should only show hover for private constants if the constant is defined in the same namespace as the
166166
# reference
167167
first_entry = T.must(entries.first)
168-
return if first_entry.private? && first_entry.name != "#{@nesting.join("::")}::#{name}"
168+
return if first_entry.private? && first_entry.name != "#{@node_context.nesting.join("::")}::#{name}"
169169

170170
categorized_markdown_from_index_entries(name, entries).each do |category, content|
171171
@response_builder.push(content, category: category)

lib/ruby_lsp/listeners/signature_help.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ class SignatureHelp
1111
params(
1212
response_builder: ResponseBuilders::SignatureHelp,
1313
global_state: GlobalState,
14-
nesting: T::Array[String],
14+
node_context: NodeContext,
1515
dispatcher: Prism::Dispatcher,
1616
typechecker_enabled: T::Boolean,
1717
).void
1818
end
19-
def initialize(response_builder, global_state, nesting, dispatcher, typechecker_enabled)
19+
def initialize(response_builder, global_state, node_context, dispatcher, typechecker_enabled)
2020
@typechecker_enabled = typechecker_enabled
2121
@response_builder = response_builder
2222
@global_state = global_state
2323
@index = T.let(global_state.index, RubyIndexer::Index)
24-
@nesting = nesting
24+
@node_context = node_context
2525
dispatcher.register(self, :on_call_node_enter)
2626
end
2727

@@ -33,7 +33,7 @@ def on_call_node_enter(node)
3333
message = node.message
3434
return unless message
3535

36-
methods = @index.resolve_method(message, @nesting.join("::"))
36+
methods = @index.resolve_method(message, @node_context.nesting.join("::"))
3737
return unless methods
3838

3939
target_method = methods.first

lib/ruby_lsp/node_context.rb

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
end
23+
end

lib/ruby_lsp/requests/code_action_resolve.rb

+5-5
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ def refactor_variable
6868
extracted_source = T.must(@document.source[start_index...end_index])
6969

7070
# Find the closest statements node, so that we place the refactor in a valid position
71-
closest_statements, parent_statements = @document
71+
node_context = @document
7272
.locate(@document.tree, start_index, node_types: [Prism::StatementsNode, Prism::BlockNode])
7373

74+
closest_statements = node_context.node
75+
parent_statements = node_context.parent
7476
return Error::InvalidTargetRange if closest_statements.nil? || closest_statements.child_nodes.compact.empty?
7577

7678
# Find the node with the end line closest to the requested position, so that we can place the refactor
@@ -162,10 +164,8 @@ def refactor_method
162164
extracted_source = T.must(@document.source[start_index...end_index])
163165

164166
# Find the closest method declaration node, so that we place the refactor in a valid position
165-
closest_def, _ = T.cast(
166-
@document.locate(@document.tree, start_index, node_types: [Prism::DefNode]),
167-
[T.nilable(Prism::DefNode), T.nilable(Prism::Node), T::Array[String]],
168-
)
167+
node_context = @document.locate(@document.tree, start_index, node_types: [Prism::DefNode])
168+
closest_def = T.cast(node_context.node, Prism::DefNode)
169169
return Error::InvalidTargetRange if closest_def.nil?
170170

171171
end_keyword_loc = closest_def.end_keyword_loc

0 commit comments

Comments
 (0)