@@ -38,7 +38,9 @@ pub(crate) fn is_sanitized_token_with_quote(node_under_cursor_txt: &str) -> bool
3838 // Node under cursor text will be "REPLACED_TOKEN_WITH_QUOTE".
3939 // The SANITIZED_TOKEN_WITH_QUOTE does not have the leading ".
4040 // We need to omit it from the txt.
41- & node_under_cursor_txt[ 1 ..] == SANITIZED_TOKEN_WITH_QUOTE
41+ node_under_cursor_txt
42+ . strip_prefix ( '"' )
43+ . is_some_and ( |it| it == SANITIZED_TOKEN_WITH_QUOTE )
4244}
4345
4446impl < ' larger , ' smaller > From < CompletionParams < ' larger > > for SanitizedCompletionParams < ' smaller >
4749{
4850 fn from ( mut params : CompletionParams < ' larger > ) -> Self {
4951 params. text = params. text . to_ascii_lowercase ( ) ;
52+
53+ while !params. text . is_char_boundary ( params. position . into ( ) )
54+ && params. position < TextSize :: new ( params. text . len ( ) . try_into ( ) . unwrap ( ) )
55+ {
56+ params. position = params. position . checked_add ( 1 . into ( ) ) . unwrap ( ) ;
57+ }
58+
5059 if cursor_inbetween_nodes ( & params. text , params. position )
5160 || cursor_prepared_to_write_token_after_last_node ( & params. text , params. position )
5261 || cursor_before_semicolon ( params. tree , params. position )
@@ -275,7 +284,11 @@ fn cursor_between_parentheses(sql: &str, position: TextSize) -> bool {
275284 . unwrap_or_default ( ) ;
276285
277286 // (.. and |)
278- let after_and_keyword = & sql[ position. saturating_sub ( 4 ) ..position] == "and " && after == ')' ;
287+ let and_check_position = position. saturating_sub ( 4 ) ;
288+ let after_and_keyword = sql. is_char_boundary ( and_check_position)
289+ && & sql[ and_check_position..position] == "and "
290+ && after == ')' ;
291+
279292 let after_eq_sign = before == '=' && after == ')' ;
280293
281294 let head_of_list = before == '(' && after == ',' ;
@@ -312,7 +325,7 @@ mod tests {
312325 use pgls_text_size:: TextSize ;
313326
314327 use crate :: {
315- CompletionParams , SanitizedCompletionParams ,
328+ CompletionParams , SanitizedCompletionParams , is_sanitized_token_with_quote ,
316329 sanitization:: {
317330 cursor_after_opened_quote, cursor_before_semicolon, cursor_between_parentheses,
318331 cursor_inbetween_nodes, cursor_on_a_dot,
@@ -635,4 +648,49 @@ mod tests {
635648 TextSize :: new( 20 )
636649 ) ) ;
637650 }
651+
652+ #[ test]
653+ fn multibyte_characters ( ) {
654+ is_sanitized_token_with_quote ( "é" ) ; // should not panic
655+
656+ {
657+ // cursor in the middle of multi-byte char
658+ // select * from "auth"."é|é; <-- cursor in the middle of the é multi-byte char
659+ let input = r#"select * from "auth"."é;"# ;
660+ let position = TextSize :: new ( 23 ) ;
661+
662+ let params = get_test_params ( input, position) ;
663+
664+ let sanitized = SanitizedCompletionParams :: from ( params) ;
665+
666+ assert_eq ! ( sanitized. text, r#"select * from "auth"."é;"# ) ;
667+ assert_eq ! ( sanitized. position, TextSize :: new( 24 ) ) ; // adjusts the cursor position to end of multibyte char
668+ }
669+
670+ {
671+ // cursor in front of multibyte char
672+ // select * from "auth"."|é; <-- cursor front of the é multi-byte char
673+ let input = r#"select * from "auth"."é;"# ;
674+ let position = TextSize :: new ( 22 ) ;
675+
676+ let params = get_test_params ( input, position) ;
677+
678+ let sanitized = SanitizedCompletionParams :: from ( params) ;
679+
680+ assert_eq ! (
681+ sanitized. text,
682+ r#"select * from "auth"."REPLACED_TOKEN_WITH_QUOTE"é; "#
683+ ) ;
684+ assert_eq ! ( sanitized. position, TextSize :: new( 22 ) ) ; // leaves cursor position as is
685+ }
686+
687+ {
688+ let input = "insert into instruments (name, id, €, )" ;
689+ let position = TextSize :: new ( 36 ) ;
690+
691+ let params = get_test_params ( input, position) ;
692+
693+ let _ = SanitizedCompletionParams :: from ( params) ; // should not panic
694+ }
695+ }
638696}
0 commit comments