sql_cli/ui/utils/
text_utils.rs1#[must_use]
5pub fn extract_partial_word_at_cursor(query: &str, cursor_pos: usize) -> Option<String> {
6 if cursor_pos == 0 || cursor_pos > query.len() {
7 return None;
8 }
9
10 let chars: Vec<char> = query.chars().collect();
11 let mut start = cursor_pos;
12 let end = cursor_pos;
13
14 let mut in_quote = false;
16
17 for i in (0..cursor_pos).rev() {
19 if i < chars.len() && chars[i] == '"' {
20 let mut found_closing = false;
23 for j in cursor_pos..chars.len() {
24 if chars[j] == '"' {
25 found_closing = true;
26 break;
27 }
28 }
29 if !found_closing || cursor_pos <= chars.len() {
31 in_quote = true;
32 start = i;
33 break;
34 }
35 }
36 }
37
38 if in_quote {
40 let start_byte = chars[..start].iter().map(|c| c.len_utf8()).sum();
42 let end_byte = chars[..end].iter().map(|c| c.len_utf8()).sum();
43
44 if start_byte < end_byte {
45 return Some(query[start_byte..end_byte].to_string());
46 }
47 }
48
49 while start > 0 {
51 let prev_char = chars[start - 1];
52 if prev_char.is_alphanumeric() || prev_char == '_' {
53 start -= 1;
54 } else {
55 break;
56 }
57 }
58
59 let start_byte = chars[..start].iter().map(|c| c.len_utf8()).sum();
61 let end_byte = chars[..end].iter().map(|c| c.len_utf8()).sum();
62
63 if start_byte < end_byte {
64 Some(query[start_byte..end_byte].to_string())
65 } else {
66 None
67 }
68}
69
70#[must_use]
72pub fn get_token_at_cursor(sql_text: &str, cursor_pos: usize) -> Option<String> {
73 if sql_text.is_empty() || cursor_pos > sql_text.len() {
74 return None;
75 }
76
77 let chars: Vec<char> = sql_text.chars().collect();
78 if cursor_pos > chars.len() {
79 return None;
80 }
81
82 let mut start = cursor_pos;
84 let mut end = cursor_pos;
85
86 while start > 0 {
88 let idx = start - 1;
89 if idx < chars.len() && (chars[idx].is_alphanumeric() || chars[idx] == '_') {
90 start -= 1;
91 } else {
92 break;
93 }
94 }
95
96 while end < chars.len() {
98 if chars[end].is_alphanumeric() || chars[end] == '_' {
99 end += 1;
100 } else {
101 break;
102 }
103 }
104
105 if start < end {
106 let token: String = chars[start..end].iter().collect();
107 Some(token)
108 } else {
109 None
110 }
111}
112
113#[must_use]
115pub fn get_cursor_token_position(sql_text: &str, cursor_pos: usize) -> (usize, usize) {
116 if let Some(token) = get_token_at_cursor(sql_text, cursor_pos) {
117 let before_cursor = &sql_text[..cursor_pos.min(sql_text.len())];
119 if let Some(rev_pos) = before_cursor.rfind(&token) {
120 let token_start = rev_pos;
121 let pos_in_token = cursor_pos.saturating_sub(token_start);
122 return (token_start, pos_in_token);
123 }
124 }
125 (cursor_pos, 0)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_extract_partial_word() {
134 assert_eq!(
135 extract_partial_word_at_cursor("SELECT coun", 11),
136 Some("coun".to_string())
137 );
138
139 assert_eq!(
140 extract_partial_word_at_cursor("SELECT \"quoted col", 18),
141 Some("\"quoted col".to_string())
142 );
143
144 assert_eq!(extract_partial_word_at_cursor("", 0), None);
145 }
146
147 #[test]
148 fn test_get_token_at_cursor() {
149 assert_eq!(
150 get_token_at_cursor("SELECT column_name FROM", 10),
151 Some("column_name".to_string())
152 );
153
154 assert_eq!(
155 get_token_at_cursor("WHERE id = 123", 7),
156 Some("id".to_string())
157 );
158 }
159}