Skip to content

Commit d879724

Browse files
committed
Move locate to RubyDocument and ERBDocument
1 parent d193290 commit d879724

File tree

5 files changed

+126
-112
lines changed

5 files changed

+126
-112
lines changed

lib/ruby_lsp/document.rb

-109
Original file line numberDiff line numberDiff line change
@@ -107,115 +107,6 @@ def create_scanner
107107
Scanner.new(@source, @encoding)
108108
end
109109

110-
sig do
111-
params(
112-
position: T::Hash[Symbol, T.untyped],
113-
node_types: T::Array[T.class_of(Prism::Node)],
114-
).returns(NodeContext)
115-
end
116-
def locate_node(position, node_types: [])
117-
locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
118-
end
119-
120-
sig do
121-
params(
122-
node: Prism::Node,
123-
char_position: Integer,
124-
node_types: T::Array[T.class_of(Prism::Node)],
125-
).returns(NodeContext)
126-
end
127-
def locate(node, char_position, node_types: [])
128-
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
129-
closest = node
130-
parent = T.let(nil, T.nilable(Prism::Node))
131-
nesting_nodes = T.let(
132-
[],
133-
T::Array[T.any(
134-
Prism::ClassNode,
135-
Prism::ModuleNode,
136-
Prism::SingletonClassNode,
137-
Prism::DefNode,
138-
Prism::BlockNode,
139-
Prism::LambdaNode,
140-
Prism::ProgramNode,
141-
)],
142-
)
143-
144-
nesting_nodes << node if node.is_a?(Prism::ProgramNode)
145-
call_node = T.let(nil, T.nilable(Prism::CallNode))
146-
147-
until queue.empty?
148-
candidate = queue.shift
149-
150-
# Skip nil child nodes
151-
next if candidate.nil?
152-
153-
# Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
154-
# same order as the visiting mechanism, which means searching the child nodes before moving on to the next
155-
# sibling
156-
T.unsafe(queue).unshift(*candidate.child_nodes)
157-
158-
# Skip if the current node doesn't cover the desired position
159-
loc = candidate.location
160-
next unless (loc.start_offset...loc.end_offset).cover?(char_position)
161-
162-
# If the node's start character is already past the position, then we should've found the closest node
163-
# already
164-
break if char_position < loc.start_offset
165-
166-
# If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and
167-
# need to pop the stack
168-
previous_level = nesting_nodes.last
169-
nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset
170-
171-
# Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
172-
# target when it is a constant
173-
case candidate
174-
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode, Prism::BlockNode,
175-
Prism::LambdaNode
176-
nesting_nodes << candidate
177-
end
178-
179-
if candidate.is_a?(Prism::CallNode)
180-
arg_loc = candidate.arguments&.location
181-
blk_loc = candidate.block&.location
182-
if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) ||
183-
(blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position))
184-
call_node = candidate
185-
end
186-
end
187-
188-
# If there are node types to filter by, and the current node is not one of those types, then skip it
189-
next if node_types.any? && node_types.none? { |type| candidate.class == type }
190-
191-
# If the current node is narrower than or equal to the previous closest node, then it is more precise
192-
closest_loc = closest.location
193-
if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset
194-
parent = closest
195-
closest = candidate
196-
end
197-
end
198-
199-
# When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated. That
200-
# is, when targeting Bar in the following example:
201-
#
202-
# ```ruby
203-
# class Foo::Bar; end
204-
# ```
205-
# The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, even
206-
# though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
207-
if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode)
208-
last_level = nesting_nodes.last
209-
210-
if (last_level.is_a?(Prism::ModuleNode) || last_level.is_a?(Prism::ClassNode)) &&
211-
last_level.constant_path == closest
212-
nesting_nodes.pop
213-
end
214-
end
215-
216-
NodeContext.new(closest, parent, nesting_nodes, call_node)
217-
end
218-
219110
class Scanner
220111
extend T::Sig
221112

lib/ruby_lsp/erb_document.rb

+10
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ def language_id
2828
LanguageId::ERB
2929
end
3030

31+
sig do
32+
params(
33+
position: T::Hash[Symbol, T.untyped],
34+
node_types: T::Array[T.class_of(Prism::Node)],
35+
).returns(NodeContext)
36+
end
37+
def locate_node(position, node_types: [])
38+
RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
39+
end
40+
3141
class ERBScanner
3242
extend T::Sig
3343

lib/ruby_lsp/requests/code_action_resolve.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def refactor_variable
112112
extracted_source = T.must(@document.source[start_index...end_index])
113113

114114
# Find the closest statements node, so that we place the refactor in a valid position
115-
node_context = @document
115+
node_context = RubyDocument
116116
.locate(@document.parse_result.value, start_index, node_types: [Prism::StatementsNode, Prism::BlockNode])
117117

118118
closest_statements = node_context.node
@@ -206,7 +206,7 @@ def refactor_method
206206
extracted_source = T.must(@document.source[start_index...end_index])
207207

208208
# Find the closest method declaration node, so that we place the refactor in a valid position
209-
node_context = @document.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode])
209+
node_context = RubyDocument.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode])
210210
closest_def = T.cast(node_context.node, Prism::DefNode)
211211
return Error::InvalidTargetRange if closest_def.nil?
212212

lib/ruby_lsp/requests/completion.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def initialize(document, global_state, params, sorbet_level, dispatcher)
6060
# Completion always receives the position immediately after the character that was just typed. Here we adjust it
6161
# back by 1, so that we find the right node
6262
char_position = document.create_scanner.find_char_position(params[:position]) - 1
63-
node_context = document.locate(
63+
node_context = RubyDocument.locate(
6464
document.parse_result.value,
6565
char_position,
6666
node_types: [

lib/ruby_lsp/ruby_document.rb

+113
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,109 @@ class SorbetLevel < T::Enum
1818
end
1919
end
2020

21+
class << self
22+
extend T::Sig
23+
24+
sig do
25+
params(
26+
node: Prism::Node,
27+
char_position: Integer,
28+
node_types: T::Array[T.class_of(Prism::Node)],
29+
).returns(NodeContext)
30+
end
31+
def locate(node, char_position, node_types: [])
32+
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
33+
closest = node
34+
parent = T.let(nil, T.nilable(Prism::Node))
35+
nesting_nodes = T.let(
36+
[],
37+
T::Array[T.any(
38+
Prism::ClassNode,
39+
Prism::ModuleNode,
40+
Prism::SingletonClassNode,
41+
Prism::DefNode,
42+
Prism::BlockNode,
43+
Prism::LambdaNode,
44+
Prism::ProgramNode,
45+
)],
46+
)
47+
48+
nesting_nodes << node if node.is_a?(Prism::ProgramNode)
49+
call_node = T.let(nil, T.nilable(Prism::CallNode))
50+
51+
until queue.empty?
52+
candidate = queue.shift
53+
54+
# Skip nil child nodes
55+
next if candidate.nil?
56+
57+
# Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
58+
# same order as the visiting mechanism, which means searching the child nodes before moving on to the next
59+
# sibling
60+
T.unsafe(queue).unshift(*candidate.child_nodes)
61+
62+
# Skip if the current node doesn't cover the desired position
63+
loc = candidate.location
64+
next unless (loc.start_offset...loc.end_offset).cover?(char_position)
65+
66+
# If the node's start character is already past the position, then we should've found the closest node
67+
# already
68+
break if char_position < loc.start_offset
69+
70+
# If the candidate starts after the end of the previous nesting level, then we've exited that nesting level
71+
# and need to pop the stack
72+
previous_level = nesting_nodes.last
73+
nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset
74+
75+
# Keep track of the nesting where we found the target. This is used to determine the fully qualified name of
76+
# the target when it is a constant
77+
case candidate
78+
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode, Prism::BlockNode,
79+
Prism::LambdaNode
80+
nesting_nodes << candidate
81+
end
82+
83+
if candidate.is_a?(Prism::CallNode)
84+
arg_loc = candidate.arguments&.location
85+
blk_loc = candidate.block&.location
86+
if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) ||
87+
(blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position))
88+
call_node = candidate
89+
end
90+
end
91+
92+
# If there are node types to filter by, and the current node is not one of those types, then skip it
93+
next if node_types.any? && node_types.none? { |type| candidate.class == type }
94+
95+
# If the current node is narrower than or equal to the previous closest node, then it is more precise
96+
closest_loc = closest.location
97+
if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset
98+
parent = closest
99+
closest = candidate
100+
end
101+
end
102+
103+
# When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated.
104+
# That is, when targeting Bar in the following example:
105+
#
106+
# ```ruby
107+
# class Foo::Bar; end
108+
# ```
109+
# The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack,
110+
# even though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
111+
if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode)
112+
last_level = nesting_nodes.last
113+
114+
if (last_level.is_a?(Prism::ModuleNode) || last_level.is_a?(Prism::ClassNode)) &&
115+
last_level.constant_path == closest
116+
nesting_nodes.pop
117+
end
118+
end
119+
120+
NodeContext.new(closest, parent, nesting_nodes, call_node)
121+
end
122+
end
123+
21124
sig { override.returns(ParseResultType) }
22125
def parse
23126
return @parse_result unless @needs_parsing
@@ -89,5 +192,15 @@ def locate_first_within_range(range, node_types: [])
89192
end
90193
end
91194
end
195+
196+
sig do
197+
params(
198+
position: T::Hash[Symbol, T.untyped],
199+
node_types: T::Array[T.class_of(Prism::Node)],
200+
).returns(NodeContext)
201+
end
202+
def locate_node(position, node_types: [])
203+
RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
204+
end
92205
end
93206
end

0 commit comments

Comments
 (0)