@@ -5,7 +5,7 @@ use std::cmp;
55const MAX_SAFE_CHAR : u32 = std:: u32:: MAX ; // You can adjust this to mimic your TS MAX_SAFE_VALUE_i32
66
77/// Returns the full range of the document, from the beginning (line 0, character 0)
8- /// to the “ end” (last line with a very large character position).
8+ /// to the " end" (last line with a very large character position).
99pub fn full_document_range ( contents : & str ) -> Range {
1010 // Compute the number of lines. If the text is empty, assume one line.
1111 let line_count = if contents. is_empty ( ) {
@@ -42,15 +42,23 @@ pub fn get_current_line<'a>(contents: &'a str, line: u32) -> &'a str {
4242/// The logic is as follows:
4343/// - If the trimmed current line is empty, return true.
4444/// - Otherwise, take the substring _up to_ the given position and check if the last
45- /// “ word” (using a `\w+` search) ends exactly at the position.
45+ /// " word" (using a `\w+` search) ends exactly at the position.
4646pub fn is_first_inside_block ( position : & Position , current_line : & str ) -> bool {
4747 if current_line. trim ( ) . is_empty ( ) {
4848 return true ;
4949 }
5050
51- // Ensure we don’t slice past the length of the current line.
52- let pos = cmp:: min ( position. character as usize , current_line. len ( ) ) ;
53- let string_til_position = & current_line[ ..pos] ;
51+ // Ensure we don't slice past the length of the current line, using character counts.
52+ let char_count_in_line = current_line. chars ( ) . count ( ) ;
53+ let clamped_char_pos = cmp:: min ( position. character as usize , char_count_in_line) ;
54+
55+ // Convert character position to byte offset for slicing.
56+ let byte_offset_at_clamped_char_pos = current_line
57+ . char_indices ( )
58+ . nth ( clamped_char_pos)
59+ . map_or ( current_line. len ( ) , |( idx, _) | idx) ;
60+
61+ let string_til_position = & current_line[ ..byte_offset_at_clamped_char_pos] ;
5462
5563 // Find the first occurrence of a word.
5664 let re = Regex :: new ( r"\w+" ) . unwrap ( ) ;
@@ -71,21 +79,37 @@ pub fn is_first_inside_block(position: &Position, current_line: &str) -> bool {
7179/// If no non-word boundary is found after the position, an empty string is returned.
7280pub fn get_word_at_position ( contents : & str , position : & Position ) -> String {
7381 let current_line = get_current_line ( contents, position. line ) ;
74- let line_len = current_line. len ( ) ;
82+ if current_line. is_empty ( ) {
83+ return "" . to_string ( ) ;
84+ }
7585
76- // Clamp position.character to the current line length.
77- let pos = cmp:: min ( position. character as usize , line_len) ;
86+ let char_count_in_line = current_line. chars ( ) . count ( ) ;
87+ // `position.character` is a 0-indexed character offset. Clamp it.
88+ let clamped_char_idx = cmp:: min ( position. character as usize , char_count_in_line) ;
89+
90+ // Part 1: Search backward to find the start of the word.
91+ // Slice for backward search extends up to character `clamped_char_idx + 1` (exclusive).
92+ let char_len_for_backward_search = cmp:: min ( clamped_char_idx + 1 , char_count_in_line) ;
93+ let byte_len_for_backward_search_slice = current_line
94+ . char_indices ( )
95+ . nth ( char_len_for_backward_search)
96+ . map_or ( current_line. len ( ) , |( idx, _) | idx) ;
97+ let text_for_backward_search = & current_line[ ..byte_len_for_backward_search_slice] ;
7898
7999 // Search backward from position.character + 1 using a regex for the last non-whitespace sequence.
80100 let re_begin = Regex :: new ( r"\S+$" ) . unwrap ( ) ;
81- let slice_end = cmp:: min ( pos + 1 , line_len) ;
82- let substring_before = & current_line[ ..slice_end] ;
83- let beginning = if let Some ( mat) = re_begin. find ( substring_before) {
84- mat. start ( )
85- } else {
86- return "" . to_string ( ) ;
101+ let word_start_byte_idx = match re_begin. find ( text_for_backward_search) {
102+ Some ( mat) => mat. start ( ) ,
103+ None => return "" . to_string ( ) ,
87104 } ;
88105
106+ // Convert `clamped_char_idx` (character index) to a byte index for the LSP `position.character`.
107+ // This `pos` will be used as the starting point for the forward search.
108+ let pos = current_line
109+ . char_indices ( )
110+ . nth ( clamped_char_idx)
111+ . map_or ( current_line. len ( ) , |( idx, _) | idx) ;
112+
89113 // Search forward from position.character for the first non-word character.
90114 let re_end = Regex :: new ( r"\W" ) . unwrap ( ) ;
91115 let substring_after = & current_line[ pos..] ;
@@ -97,8 +121,8 @@ pub fn get_word_at_position(contents: &str, position: &Position) -> String {
97121
98122 let word_end = pos + end;
99123
100- if beginning <= word_end && word_end <= current_line. len ( ) {
101- current_line[ beginning ..word_end] . to_string ( )
124+ if word_start_byte_idx <= word_end && word_end <= current_line. len ( ) {
125+ current_line[ word_start_byte_idx ..word_end] . to_string ( )
102126 } else {
103127 "" . to_string ( )
104128 }
@@ -107,19 +131,44 @@ pub fn get_word_at_position(contents: &str, position: &Position) -> String {
107131/// Returns the symbol (a single character) immediately preceding the given position.
108132/// If the position is at the start of the line, an empty string is returned.
109133pub fn get_symbol_before_position ( contents : & str , position : & Position ) -> String {
110- if position. character == 0 {
111- return "" . to_string ( ) ;
112- }
113134 let current_line = get_current_line ( contents, position. line ) ;
114- let pos = cmp:: min ( position. character as usize , current_line. len ( ) ) ;
115- if pos == 0 {
135+ // position.character is a 0-indexed character offset.
136+ let char_cursor_pos = position. character as usize ;
137+
138+ if char_cursor_pos == 0 {
116139 return "" . to_string ( ) ;
117140 }
118- // This simple slicing works correctly if the text is ASCII.
119- current_line[ pos - 1 ..pos] . to_string ( )
141+
142+ // Clamp the character cursor position against the actual number of characters in the line.
143+ // This ensures that if position.character is, for example, 5, but the line only has 3 chars,
144+ // we don't panic. We want the character at index (clamped_char_cursor_pos - 1).
145+ let num_chars_in_line = current_line. chars ( ) . count ( ) ;
146+
147+ // If the effective cursor position is beyond the line's character length,
148+ // or if it's at the very beginning (char_cursor_pos == 0, handled above),
149+ // there's no valid preceding character to get by simple indexing from char_cursor_pos.
150+ // We are interested in the character at `char_cursor_pos - 1`.
151+ if char_cursor_pos > num_chars_in_line {
152+ // If cursor is effectively beyond the line, the "preceding" character would be the last one.
153+ // So we try to get char at num_chars_in_line - 1.
154+ if num_chars_in_line == 0 {
155+ return "" . to_string ( ) ; // Empty line
156+ }
157+ return current_line
158+ . chars ( )
159+ . nth ( num_chars_in_line - 1 )
160+ . map_or ( "" . to_string ( ) , |ch| ch. to_string ( ) ) ;
161+ }
162+
163+ // At this point, 0 < char_cursor_pos <= num_chars_in_line.
164+ // We want the character at index (char_cursor_pos - 1).
165+ current_line
166+ . chars ( )
167+ . nth ( char_cursor_pos - 1 )
168+ . map_or ( "" . to_string ( ) , |ch| ch. to_string ( ) )
120169}
121170
122- /// Computes the Position (line and character) corresponding to a given index in the document’ s text.
171+ /// Computes the Position (line and character) corresponding to a given index in the document' s text.
123172/// This mimics the TS implementation by iterating over each character up to the given index.
124173pub fn get_position_from_index ( document : & TextDocumentItem , index : usize ) -> Position {
125174 let mut line: u32 = 0 ;
0 commit comments