Skip to content

Commit

Permalink
Add the ability to locate the first node within a range
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Aug 7, 2024
1 parent 7c4abea commit 21139ab
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 0 deletions.
34 changes: 34 additions & 0 deletions lib/ruby_lsp/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,40 @@ def locate(node, char_position, node_types: [])
NodeContext.new(closest, parent, nesting_nodes, call_node)
end

sig do
params(
range: T::Hash[Symbol, T.untyped],
node_types: T::Array[T.class_of(Prism::Node)],
).returns(T.nilable(Prism::Node))
end
def locate_first_within_range(range, node_types: [])
scanner = create_scanner
start_position = scanner.find_char_position(range[:start])
end_position = scanner.find_char_position(range[:end])
desired_range = (start_position...end_position)
queue = T.let(@parse_result.value.child_nodes.compact, T::Array[T.nilable(Prism::Node)])

until queue.empty?
candidate = queue.shift

# Skip nil child nodes
next if candidate.nil?

# Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
# same order as the visiting mechanism, which means searching the child nodes before moving on to the next
# sibling
T.unsafe(queue).unshift(*candidate.child_nodes)

# Skip if the current node doesn't cover the desired position
loc = candidate.location

if desired_range.cover?(loc.start_offset...loc.end_offset) &&
(node_types.empty? || node_types.any? { |type| candidate.class == type })
return candidate
end
end
end

sig { returns(SorbetLevel) }
def sorbet_level
sigil = parse_result.magic_comments.find do |comment|
Expand Down
23 changes: 23 additions & 0 deletions test/ruby_document_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,29 @@ def qux
assert_equal("qux", node_context.surrounding_method)
end

def test_locate_first_within_range
document = RubyLsp::RubyDocument.new(source: +<<~RUBY, version: 1, uri: URI("file:///foo/bar.rb"))
method_call(other_call).each do |a|
nested_call(fourth_call).each do |b|
end
end
RUBY

target = document.locate_first_within_range(
{ start: { line: 0, character: 0 }, end: { line: 3, character: 3 } },
node_types: [Prism::CallNode],
)

assert_equal("each", T.cast(target, Prism::CallNode).message)

target = document.locate_first_within_range(
{ start: { line: 1, character: 2 }, end: { line: 2, character: 5 } },
node_types: [Prism::CallNode],
)

assert_equal("each", T.cast(target, Prism::CallNode).message)
end

private

def assert_error_edit(actual, error_range)
Expand Down

0 comments on commit 21139ab

Please sign in to comment.