Skip to content

Commit 2d67e09

Browse files
authored
Merge pull request #799 from Shopify/vs/add_nesting_to_locate
Keep track of nesting when locating nodes
2 parents a446a97 + 1346771 commit 2d67e09

File tree

2 files changed

+51
-8
lines changed

2 files changed

+51
-8
lines changed

lib/ruby_lsp/document.rb

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ def create_scanner
114114
params(
115115
position: PositionShape,
116116
node_types: T::Array[T.class_of(SyntaxTree::Node)],
117-
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
117+
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node), T::Array[String]])
118118
end
119119
def locate_node(position, node_types: [])
120-
return [nil, nil] unless parsed?
120+
return [nil, nil, []] unless parsed?
121121

122122
locate(T.must(@tree), create_scanner.find_char_position(position), node_types: node_types)
123123
end
@@ -127,21 +127,24 @@ def locate_node(position, node_types: [])
127127
node: SyntaxTree::Node,
128128
char_position: Integer,
129129
node_types: T::Array[T.class_of(SyntaxTree::Node)],
130-
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
130+
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node), T::Array[String]])
131131
end
132132
def locate(node, char_position, node_types: [])
133133
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
134134
closest = node
135135
parent = T.let(nil, T.nilable(SyntaxTree::Node))
136+
nesting = T.let([], T::Array[T.any(SyntaxTree::ClassDeclaration, SyntaxTree::ModuleDeclaration)])
136137

137138
until queue.empty?
138139
candidate = queue.shift
139140

140141
# Skip nil child nodes
141142
next if candidate.nil?
142143

143-
# Add the next child_nodes to the queue to be processed
144-
queue.concat(candidate.child_nodes)
144+
# Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
145+
# same order as the visiting mechanism, which means searching the child nodes before moving on to the next
146+
# sibling
147+
queue.unshift(*candidate.child_nodes)
145148

146149
# Skip if the current node doesn't cover the desired position
147150
loc = candidate.location
@@ -160,9 +163,20 @@ def locate(node, char_position, node_types: [])
160163
parent = closest
161164
closest = candidate
162165
end
166+
167+
# If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and
168+
# need to pop the stack
169+
previous_level = nesting.last
170+
nesting.pop if previous_level && candidate.start_char > previous_level.end_char
171+
172+
# Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
173+
# target when it is a constant
174+
if candidate.is_a?(SyntaxTree::ClassDeclaration) || candidate.is_a?(SyntaxTree::ModuleDeclaration)
175+
nesting << candidate
176+
end
163177
end
164178

165-
[closest, parent]
179+
[closest, parent, nesting.map { |n| n.constant.constant.value }]
166180
end
167181

168182
class Scanner

test/document_test.rb

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ class Post < ActiveRecord::Base
456456
# Locate the `Base` class
457457
found, parent = T.cast(
458458
document.locate_node({ line: 0, character: 27 }),
459-
[SyntaxTree::Const, SyntaxTree::ConstPathRef],
459+
[SyntaxTree::Const, SyntaxTree::ConstPathRef, T::Array[String]],
460460
)
461461
assert_instance_of(SyntaxTree::Const, found)
462462
assert_equal("Base", found.value)
@@ -466,13 +466,42 @@ class Post < ActiveRecord::Base
466466
assert_equal("ActiveRecord", T.cast(parent.parent, SyntaxTree::VarRef).value.value)
467467

468468
# Locate the `where` invocation
469-
found, parent = T.cast(document.locate_node({ line: 3, character: 4 }), [SyntaxTree::Ident, SyntaxTree::CallNode])
469+
found, parent = T.cast(
470+
document.locate_node({ line: 3, character: 4 }),
471+
[SyntaxTree::Ident, SyntaxTree::CallNode, T::Array[String]],
472+
)
470473
assert_instance_of(SyntaxTree::Ident, found)
471474
assert_equal("where", found.value)
472475

473476
assert_instance_of(SyntaxTree::CallNode, parent)
474477
end
475478

479+
def test_locate_returns_nesting
480+
document = RubyLsp::Document.new(source: <<~RUBY, version: 1, uri: "file:///foo/bar.rb")
481+
module Foo
482+
class Other
483+
def do_it
484+
Hello
485+
end
486+
end
487+
488+
class Bar
489+
def baz
490+
Qux
491+
end
492+
end
493+
end
494+
RUBY
495+
496+
found, _parent, nesting = document.locate_node({ line: 9, character: 6 })
497+
assert_equal("Qux", T.cast(found, SyntaxTree::Const).value)
498+
assert_equal(["Foo", "Bar"], nesting)
499+
500+
found, _parent, nesting = document.locate_node({ line: 3, character: 6 })
501+
assert_equal("Hello", T.cast(found, SyntaxTree::Const).value)
502+
assert_equal(["Foo", "Other"], nesting)
503+
end
504+
476505
def test_reparsing_without_new_edits_does_nothing
477506
document = RubyLsp::Document.new(source: +"", version: 1, uri: "file:///foo/bar.rb")
478507
document.push_edits(

0 commit comments

Comments
 (0)