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 Jul 26, 2024
1 parent a5e1775 commit 44ffa06
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 0 deletions.
42 changes: 42 additions & 0 deletions lib/ruby_lsp/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,48 @@ 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)])
closest = T.let(nil, T.nilable(Prism::Node))

until queue.empty? || closest
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
next unless desired_range.cover?(loc.start_offset...loc.end_offset)

# If the node's start character is already past the position, then we should've found the closest node
# already
break if end_position < loc.start_offset

# If there are node types to filter by, and the current node is not one of those types, then skip it
next if node_types.any? && node_types.none? { |type| candidate.class == type }

closest = candidate
end

closest
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 44ffa06

Please sign in to comment.