diff --git a/crates/pgls_completions/src/sanitization.rs b/crates/pgls_completions/src/sanitization.rs index 5c27893a5..cfba3ae5c 100644 --- a/crates/pgls_completions/src/sanitization.rs +++ b/crates/pgls_completions/src/sanitization.rs @@ -38,7 +38,9 @@ pub(crate) fn is_sanitized_token_with_quote(node_under_cursor_txt: &str) -> bool // Node under cursor text will be "REPLACED_TOKEN_WITH_QUOTE". // The SANITIZED_TOKEN_WITH_QUOTE does not have the leading ". // We need to omit it from the txt. - &node_under_cursor_txt[1..] == SANITIZED_TOKEN_WITH_QUOTE + node_under_cursor_txt + .strip_prefix('"') + .is_some_and(|it| it == SANITIZED_TOKEN_WITH_QUOTE) } impl<'larger, 'smaller> From> for SanitizedCompletionParams<'smaller> @@ -47,6 +49,13 @@ where { fn from(mut params: CompletionParams<'larger>) -> Self { params.text = params.text.to_ascii_lowercase(); + + while !params.text.is_char_boundary(params.position.into()) + && params.position < TextSize::new(params.text.len().try_into().unwrap()) + { + params.position = params.position.checked_add(1.into()).unwrap(); + } + if cursor_inbetween_nodes(¶ms.text, params.position) || cursor_prepared_to_write_token_after_last_node(¶ms.text, params.position) || cursor_before_semicolon(params.tree, params.position) @@ -275,7 +284,11 @@ fn cursor_between_parentheses(sql: &str, position: TextSize) -> bool { .unwrap_or_default(); // (.. and |) - let after_and_keyword = &sql[position.saturating_sub(4)..position] == "and " && after == ')'; + let and_check_position = position.saturating_sub(4); + let after_and_keyword = sql.is_char_boundary(and_check_position) + && &sql[and_check_position..position] == "and " + && after == ')'; + let after_eq_sign = before == '=' && after == ')'; let head_of_list = before == '(' && after == ','; @@ -312,7 +325,7 @@ mod tests { use pgls_text_size::TextSize; use crate::{ - CompletionParams, SanitizedCompletionParams, + CompletionParams, SanitizedCompletionParams, is_sanitized_token_with_quote, sanitization::{ cursor_after_opened_quote, cursor_before_semicolon, cursor_between_parentheses, cursor_inbetween_nodes, cursor_on_a_dot, @@ -635,4 +648,49 @@ mod tests { TextSize::new(20) )); } + + #[test] + fn multibyte_characters() { + is_sanitized_token_with_quote("é"); // should not panic + + { + // cursor in the middle of multi-byte char + // select * from "auth"."é|é; <-- cursor in the middle of the é multi-byte char + let input = r#"select * from "auth"."é;"#; + let position = TextSize::new(23); + + let params = get_test_params(input, position); + + let sanitized = SanitizedCompletionParams::from(params); + + assert_eq!(sanitized.text, r#"select * from "auth"."é;"#); + assert_eq!(sanitized.position, TextSize::new(24)); // adjusts the cursor position to end of multibyte char + } + + { + // cursor in front of multibyte char + // select * from "auth"."|é; <-- cursor front of the é multi-byte char + let input = r#"select * from "auth"."é;"#; + let position = TextSize::new(22); + + let params = get_test_params(input, position); + + let sanitized = SanitizedCompletionParams::from(params); + + assert_eq!( + sanitized.text, + r#"select * from "auth"."REPLACED_TOKEN_WITH_QUOTE"é; "# + ); + assert_eq!(sanitized.position, TextSize::new(22)); // leaves cursor position as is + } + + { + let input = "insert into instruments (name, id, €, )"; + let position = TextSize::new(36); + + let params = get_test_params(input, position); + + let _ = SanitizedCompletionParams::from(params); // should not panic + } + } }