Skip to content

Commit eca9613

Browse files
authored
Handle multibyte characters in RubyDocument (#2669)
1 parent 4b2709e commit eca9613

14 files changed

Lines changed: 101 additions & 16 deletions

lib/ruby_lsp/erb_document.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ def language_id
5050
).returns(NodeContext)
5151
end
5252
def locate_node(position, node_types: [])
53-
RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
53+
RubyDocument.locate(
54+
@parse_result.value,
55+
create_scanner.find_char_position(position),
56+
node_types: node_types,
57+
encoding: @encoding,
58+
)
5459
end
5560

5661
sig { params(char_position: Integer).returns(T.nilable(T::Boolean)) }

lib/ruby_lsp/requests/code_action_resolve.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ class Error < ::T::Enum
2323
end
2424
end
2525

26-
sig { params(document: RubyDocument, code_action: T::Hash[Symbol, T.untyped]).void }
27-
def initialize(document, code_action)
26+
sig { params(document: RubyDocument, global_state: GlobalState, code_action: T::Hash[Symbol, T.untyped]).void }
27+
def initialize(document, global_state, code_action)
2828
super()
2929
@document = document
30+
@global_state = global_state
3031
@code_action = code_action
3132
end
3233

@@ -191,7 +192,12 @@ def refactor_method
191192
extracted_source = T.must(@document.source[start_index...end_index])
192193

193194
# Find the closest method declaration node, so that we place the refactor in a valid position
194-
node_context = RubyDocument.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode])
195+
node_context = RubyDocument.locate(
196+
@document.parse_result.value,
197+
start_index,
198+
node_types: [Prism::DefNode],
199+
encoding: @global_state.encoding,
200+
)
195201
closest_node = node_context.node
196202
return Error::InvalidTargetRange unless closest_node
197203

lib/ruby_lsp/requests/completion.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def initialize(document, global_state, params, sorbet_level, dispatcher)
5757
Prism::InstanceVariableTargetNode,
5858
Prism::InstanceVariableWriteNode,
5959
],
60+
encoding: global_state.encoding,
6061
)
6162
@response_builder = T.let(
6263
ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem].new,

lib/ruby_lsp/requests/definition.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def initialize(document, global_state, position, dispatcher, sorbet_level)
5757
Prism::SuperNode,
5858
Prism::ForwardingSuperNode,
5959
],
60+
encoding: global_state.encoding,
6061
)
6162

6263
target = node_context.node

lib/ruby_lsp/requests/document_highlight.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def initialize(global_state, document, position, dispatcher)
2828
char_position = document.create_scanner.find_char_position(position)
2929
delegate_request_if_needed!(global_state, document, char_position)
3030

31-
node_context = RubyDocument.locate(document.parse_result.value, char_position)
31+
node_context = RubyDocument.locate(document.parse_result.value, char_position, encoding: global_state.encoding)
3232

3333
@response_builder = T.let(
3434
ResponseBuilders::CollectionResponseBuilder[Interface::DocumentHighlight].new,

lib/ruby_lsp/requests/hover.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def initialize(document, global_state, position, dispatcher, sorbet_level)
4141
document.parse_result.value,
4242
char_position,
4343
node_types: Listeners::Hover::ALLOWED_TARGETS,
44+
encoding: global_state.encoding,
4445
)
4546
target = node_context.node
4647
parent = node_context.parent

lib/ruby_lsp/requests/references.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def perform
4242
Prism::CallNode,
4343
Prism::DefNode,
4444
],
45+
encoding: @global_state.encoding,
4546
)
4647
target = node_context.node
4748
parent = node_context.parent

lib/ruby_lsp/requests/rename.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def perform
3737
@document.parse_result.value,
3838
char_position,
3939
node_types: [Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode],
40+
encoding: @global_state.encoding,
4041
)
4142
target = node_context.node
4243
parent = node_context.parent

lib/ruby_lsp/requests/signature_help.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ def initialize(document, global_state, position, context, dispatcher, sorbet_lev
3939
char_position = document.create_scanner.find_char_position(position)
4040
delegate_request_if_needed!(global_state, document, char_position)
4141

42-
node_context = RubyDocument.locate(document.parse_result.value, char_position, node_types: [Prism::CallNode])
42+
node_context = RubyDocument.locate(
43+
document.parse_result.value,
44+
char_position,
45+
node_types: [Prism::CallNode],
46+
encoding: global_state.encoding,
47+
)
4348

4449
target = adjust_for_nested_target(node_context.node, node_context.parent, position)
4550

lib/ruby_lsp/ruby_document.rb

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ class << self
2626
node: Prism::Node,
2727
char_position: Integer,
2828
node_types: T::Array[T.class_of(Prism::Node)],
29+
encoding: Encoding,
2930
).returns(NodeContext)
3031
end
31-
def locate(node, char_position, node_types: [])
32+
def locate(node, char_position, node_types: [], encoding: Encoding::UTF_8)
3233
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
3334
closest = node
3435
parent = T.let(nil, T.nilable(Prism::Node))
@@ -61,16 +62,21 @@ def locate(node, char_position, node_types: [])
6162

6263
# Skip if the current node doesn't cover the desired position
6364
loc = candidate.location
64-
next unless (loc.start_offset...loc.end_offset).cover?(char_position)
65+
loc_start_offset = loc.start_code_units_offset(encoding)
66+
loc_end_offset = loc.end_code_units_offset(encoding)
67+
next unless (loc_start_offset...loc_end_offset).cover?(char_position)
6568

6669
# If the node's start character is already past the position, then we should've found the closest node
6770
# already
68-
break if char_position < loc.start_offset
71+
break if char_position < loc_start_offset
6972

7073
# If the candidate starts after the end of the previous nesting level, then we've exited that nesting level
7174
# and need to pop the stack
7275
previous_level = nesting_nodes.last
73-
nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset
76+
if previous_level &&
77+
(loc_start_offset > previous_level.location.end_code_units_offset(encoding))
78+
nesting_nodes.pop
79+
end
7480

7581
# Keep track of the nesting where we found the target. This is used to determine the fully qualified name of
7682
# the target when it is a constant
@@ -83,8 +89,10 @@ def locate(node, char_position, node_types: [])
8389
if candidate.is_a?(Prism::CallNode)
8490
arg_loc = candidate.arguments&.location
8591
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))
92+
if (arg_loc && (arg_loc.start_code_units_offset(encoding)...
93+
arg_loc.end_code_units_offset(encoding)).cover?(char_position)) ||
94+
(blk_loc && (blk_loc.start_code_units_offset(encoding)...
95+
blk_loc.end_code_units_offset(encoding)).cover?(char_position))
8896
call_node = candidate
8997
end
9098
end
@@ -94,7 +102,9 @@ def locate(node, char_position, node_types: [])
94102

95103
# If the current node is narrower than or equal to the previous closest node, then it is more precise
96104
closest_loc = closest.location
97-
if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset
105+
closest_node_start_offset = closest_loc.start_code_units_offset(encoding)
106+
closest_node_end_offset = closest_loc.end_code_units_offset(encoding)
107+
if loc_end_offset - loc_start_offset <= closest_node_end_offset - closest_node_start_offset
98108
parent = closest
99109
closest = candidate
100110
end
@@ -201,7 +211,12 @@ def locate_first_within_range(range, node_types: [])
201211
).returns(NodeContext)
202212
end
203213
def locate_node(position, node_types: [])
204-
RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
214+
RubyDocument.locate(
215+
@parse_result.value,
216+
create_scanner.find_char_position(position),
217+
node_types: node_types,
218+
encoding: @encoding,
219+
)
205220
end
206221
end
207222
end

0 commit comments

Comments
 (0)