Skip to content

Commit 79da85f

Browse files
fix(completions): multi-byte char sanitization (#764)
1 parent 708863a commit 79da85f

1 file changed

Lines changed: 61 additions & 3 deletions

File tree

crates/pgls_completions/src/sanitization.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4446
impl<'larger, 'smaller> From<CompletionParams<'larger>> for SanitizedCompletionParams<'smaller>
@@ -47,6 +49,13 @@ where
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

Comments
 (0)