Skip to content

Commit

Permalink
Changed to calculate position by converting it to byte offset
Browse files Browse the repository at this point in the history
  • Loading branch information
NotFounds committed May 23, 2024
1 parent 895dff7 commit be648a6
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 28 deletions.
27 changes: 17 additions & 10 deletions lib/ruby_lsp/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,17 @@ def locate(node, char_position, node_types: [])

# Skip if the current node doesn't cover the desired position
loc = candidate.location
next unless (loc.start_code_units_offset(encoding)...loc.end_code_units_offset(encoding)).cover?(char_position)
char_byte_offset = convert_to_byte_offset(char_position)
next unless (loc.start_offset...loc.end_offset).cover?(char_byte_offset)

# If the node's start character is already past the position, then we should've found the closest node
# already
break if char_position < loc.start_code_units_offset(encoding)
break if char_byte_offset < loc.start_offset

# If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and
# need to pop the stack
previous_level = nesting.last
if previous_level
start_offset = loc.start_code_units_offset(encoding)
end_offset = previous_level.location.end_code_units_offset(encoding)
nesting.pop if start_offset > end_offset
end
nesting.pop if previous_level && loc.start_offset > previous_level.location.end_offset

# Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
# target when it is a constant
Expand All @@ -168,9 +165,7 @@ def locate(node, char_position, node_types: [])

# If the current node is narrower than or equal to the previous closest node, then it is more precise
closest_loc = closest.location
target_length = loc.end_code_units_offset(encoding) - loc.start_code_units_offset(encoding)
closest_length = closest_loc.end_code_units_offset(encoding) - closest_loc.start_code_units_offset(encoding)
if target_length <= closest_length
if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset
parent = closest
closest = candidate
end
Expand All @@ -186,6 +181,18 @@ def sorbet_sigil_is_true_or_higher
end
end

sig { params(char_position: Integer).returns(Integer) }
def convert_to_byte_offset(char_position)
case encoding
when Encoding::UTF_8
source.slice(0, char_position).bytesize
when Encoding::UTF_16, Encoding::UTF_16LE, Encoding::UTF_16BE
char_position * 2
when Encoding::UTF_32
char_position * 4
end
end

class Scanner
extend T::Sig

Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_lsp/requests/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ def initialize(document, global_state, position, dispatcher, typechecker_enabled
node_types: [Prism::CallNode, Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::BlockArgumentNode],
)

byte_offset_position = convert_to_byte_offset_position(document.source, position, global_state.encoding)
if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
# If the target is part of a constant path node, we need to find the exact portion of the constant that the
# user is requesting to go to definition for
target = determine_target(
target,
parent,
position,
global_state.encoding,
byte_offset_position,
)
elsif target.is_a?(Prism::CallNode) && target.name != :require && target.name != :require_relative &&
!covers_position?(target.message_loc, position, global_state.encoding)
!covers_position?(target.message_loc, byte_offset_position)
# If the target is a method call, we need to ensure that the requested position is exactly on top of the
# method identifier. Otherwise, we risk showing definitions for unrelated things
target = nil
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_lsp/requests/hover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,17 @@ def initialize(document, global_state, position, dispatcher, typechecker_enabled
node_types: Listeners::Hover::ALLOWED_TARGETS,
)

byte_offset_position = convert_to_byte_offset_position(document.source, position, global_state.encoding)
if (Listeners::Hover::ALLOWED_TARGETS.include?(parent.class) &&
!Listeners::Hover::ALLOWED_TARGETS.include?(target.class)) ||
(parent.is_a?(Prism::ConstantPathNode) && target.is_a?(Prism::ConstantReadNode))
target = determine_target(
T.must(target),
T.must(parent),
position,
global_state.encoding,
byte_offset_position,
)
elsif target.is_a?(Prism::CallNode) && target.name != :require && target.name != :require_relative &&
!covers_position?(target.message_loc, position, global_state.encoding)
!covers_position?(target.message_loc, byte_offset_position)

target = nil
end
Expand Down
44 changes: 32 additions & 12 deletions lib/ruby_lsp/requests/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,45 @@ def perform; end
private

# Checks if a location covers a position
sig { params(location: Prism::Location, position: T.untyped, encoding: Encoding).returns(T::Boolean) }
def cover?(location, position, encoding)
sig { params(location: Prism::Location, position: T.untyped).returns(T::Boolean) }
def cover?(location, position)
start_covered =
location.start_line - 1 < position[:line] ||
(
location.start_line - 1 == position[:line] &&
location.start_code_units_column(encoding) <= position[:character]
location.start_line - 1 == position[:line] && location.start_column <= position[:character]
)
end_covered =
location.end_line - 1 > position[:line] ||
(
location.end_line - 1 == position[:line] &&
location.end_code_units_column(encoding) >= position[:character]
location.end_column >= position[:character]
)
start_covered && end_covered
end

sig do
params(source: String, position: T::Hash[Symbol, Integer], encoding: Encoding).returns(T::Hash[Symbol, Integer])
end
def convert_to_byte_offset_position(source, position, encoding)
char_position = position[:character]

byte_offset_pos = case encoding
when Encoding::UTF_8
source.slice(0, char_position).bytesize
when Encoding::UTF_16, Encoding::UTF_16LE, Encoding::UTF_16BE
char_position * 2
when Encoding::UTF_32
char_position * 4
else
raise InvalidFormatter, "Unsupported encoding: #{encoding}"
end

{
line: position[:line],
character: byte_offset_pos,
}
end

# Based on a constant node target, a constant path node parent and a position, this method will find the exact
# portion of the constant path that matches the requested position, for higher precision in hover and
# definition. For example:
Expand All @@ -50,16 +72,15 @@ def cover?(location, position, encoding)
target: Prism::Node,
parent: Prism::Node,
position: T::Hash[Symbol, Integer],
encoding: Encoding,
).returns(Prism::Node)
end
def determine_target(target, parent, position, encoding)
def determine_target(target, parent, position)
return target unless parent.is_a?(Prism::ConstantPathNode)

target = T.let(parent, Prism::Node)
parent = T.let(T.cast(target, Prism::ConstantPathNode).parent, T.nilable(Prism::Node))

while parent && cover?(parent.location, position, encoding)
while parent && cover?(parent.location, position)
target = parent
parent = target.is_a?(Prism::ConstantPathNode) ? target.parent : nil
end
Expand All @@ -72,19 +93,18 @@ def determine_target(target, parent, position, encoding)
params(
location: T.nilable(Prism::Location),
position: T::Hash[Symbol, T.untyped],
encoding: Encoding,
).returns(T::Boolean)
end
def covers_position?(location, position, encoding)
def covers_position?(location, position)
return false unless location

start_line = location.start_line - 1
end_line = location.end_line - 1
line = position[:line]
character = position[:character]

(start_line < line || (start_line == line && location.start_code_units_column(encoding) <= character)) &&
(end_line > line || (end_line == line && location.end_code_units_column(encoding) >= character))
(start_line < line || (start_line == line && location.start_column <= character)) &&
(end_line > line || (end_line == line && location.end_column >= character))
end
end
end
Expand Down

0 comments on commit be648a6

Please sign in to comment.