1use crate::recursive_parser::{Lexer, Token};
2
3pub struct TextNavigator;
6
7impl TextNavigator {
8 #[must_use]
10 pub fn get_cursor_token_position(query: &str, cursor_pos: usize) -> (usize, usize) {
11 if query.is_empty() {
12 return (0, 0);
13 }
14
15 let mut lexer = Lexer::new(query);
17 let tokens = lexer.tokenize_all_with_positions();
18
19 if tokens.is_empty() {
20 return (0, 0);
21 }
22
23 if cursor_pos == 0 {
25 return (0, tokens.len());
26 }
27
28 let mut current_token = 0;
30 for (i, (start, end, _)) in tokens.iter().enumerate() {
31 if cursor_pos >= *start && cursor_pos <= *end {
32 current_token = i + 1;
33 break;
34 } else if cursor_pos < *start {
35 current_token = i;
37 break;
38 }
39 }
40
41 if current_token == 0 && cursor_pos > 0 {
43 current_token = tokens.len();
44 }
45
46 (current_token, tokens.len())
47 }
48
49 #[must_use]
51 pub fn get_token_at_cursor(query: &str, cursor_pos: usize) -> Option<String> {
52 if query.is_empty() {
53 return None;
54 }
55
56 let mut lexer = Lexer::new(query);
58 let tokens = lexer.tokenize_all_with_positions();
59
60 for (start, end, token) in &tokens {
62 if cursor_pos >= *start && cursor_pos <= *end {
63 let token_str = Self::format_token(token);
65 return Some(token_str.to_string());
66 }
67 }
68
69 None
70 }
71
72 #[must_use]
74 pub fn calculate_prev_token_position(query: &str, cursor_pos: usize) -> Option<usize> {
75 if cursor_pos == 0 {
76 return None;
77 }
78
79 let mut lexer = Lexer::new(query);
80 let tokens = lexer.tokenize_all_with_positions();
81
82 let mut in_token = false;
84 let mut current_token_start = 0;
85 for (start, end, _) in &tokens {
86 if cursor_pos > *start && cursor_pos <= *end {
87 in_token = true;
88 current_token_start = *start;
89 break;
90 }
91 }
92
93 let target_pos = if in_token && cursor_pos > current_token_start {
95 current_token_start
97 } else {
98 let mut prev_start = 0;
100 for (start, _, _) in tokens.iter().rev() {
101 if *start < cursor_pos {
102 prev_start = *start;
103 break;
104 }
105 }
106 prev_start
107 };
108
109 if target_pos < cursor_pos {
110 Some(target_pos)
111 } else {
112 None
113 }
114 }
115
116 #[must_use]
118 pub fn calculate_next_token_position(query: &str, cursor_pos: usize) -> Option<usize> {
119 let query_len = query.len();
120 if cursor_pos >= query_len {
121 return None;
122 }
123
124 let mut lexer = Lexer::new(query);
125 let tokens = lexer.tokenize_all_with_positions();
126
127 let mut in_token = false;
129 let mut current_token_end = query_len;
130 for (start, end, _) in &tokens {
131 if cursor_pos >= *start && cursor_pos < *end {
132 in_token = true;
133 current_token_end = *end;
134 break;
135 }
136 }
137
138 let target_pos = if in_token && cursor_pos < current_token_end {
140 let mut next_start = query_len;
142 for (start, _, _) in &tokens {
143 if *start > current_token_end {
144 next_start = *start;
145 break;
146 }
147 }
148 next_start
149 } else {
150 let mut next_start = query_len;
152 for (start, _, _) in &tokens {
153 if *start > cursor_pos {
154 next_start = *start;
155 break;
156 }
157 }
158 next_start
159 };
160
161 if target_pos > cursor_pos && target_pos <= query_len {
162 Some(target_pos)
163 } else {
164 None
165 }
166 }
167
168 fn format_token(token: &Token) -> &str {
170 match token {
171 Token::Select => "SELECT",
172 Token::From => "FROM",
173 Token::Where => "WHERE",
174 Token::With => "WITH",
175 Token::GroupBy => "GROUP BY",
176 Token::OrderBy => "ORDER BY",
177 Token::Having => "HAVING",
178 Token::As => "AS",
179 Token::Asc => "ASC",
180 Token::Desc => "DESC",
181 Token::And => "AND",
182 Token::Or => "OR",
183 Token::In => "IN",
184 Token::DateTime => "DateTime",
185 Token::Case => "CASE",
186 Token::When => "WHEN",
187 Token::Then => "THEN",
188 Token::Else => "ELSE",
189 Token::End => "END",
190 Token::Distinct => "DISTINCT",
191 Token::Exclude => "EXCLUDE",
192 Token::Pivot => "PIVOT",
193 Token::Unpivot => "UNPIVOT",
194 Token::For => "FOR",
195 Token::Over => "OVER",
196 Token::Partition => "PARTITION",
197 Token::By => "BY",
198 Token::Rows => "ROWS",
200 Token::Range => "RANGE",
201 Token::Unbounded => "UNBOUNDED",
202 Token::Preceding => "PRECEDING",
203 Token::Following => "FOLLOWING",
204 Token::Current => "CURRENT",
205 Token::Row => "ROW",
206 Token::Union => "UNION",
208 Token::Intersect => "INTERSECT",
209 Token::Except => "EXCEPT",
210 Token::Web => "WEB",
212 Token::File => "FILE",
213 Token::Unnest => "UNNEST",
215 Token::Identifier(s) => s,
216 Token::QuotedIdentifier(s) => s,
217 Token::StringLiteral(s) => s,
218 Token::JsonBlock(s) => s,
219 Token::NumberLiteral(s) => s,
220 Token::Star => "*",
221 Token::Comma => ",",
222 Token::Semicolon => ";",
223 Token::Colon => ":",
224 Token::Dot => ".",
225 Token::LeftParen => "(",
226 Token::RightParen => ")",
227 Token::Equal => "=",
228 Token::NotEqual => "!=",
229 Token::LessThan => "<",
230 Token::LessThanOrEqual => "<=",
231 Token::GreaterThan => ">",
232 Token::GreaterThanOrEqual => ">=",
233 Token::Like => "LIKE",
234 Token::ILike => "ILIKE",
235 Token::Not => "NOT",
236 Token::Is => "IS",
237 Token::Null => "NULL",
238 Token::Between => "BETWEEN",
239 Token::Limit => "LIMIT",
240 Token::Offset => "OFFSET",
241 Token::Into => "INTO",
242 Token::Plus => "+",
243 Token::Minus => "-",
244 Token::Divide => "/",
245 Token::Modulo => "%",
246 Token::Concat => "||",
247 Token::Join => "JOIN",
248 Token::Inner => "INNER",
249 Token::Left => "LEFT",
250 Token::Right => "RIGHT",
251 Token::Full => "FULL",
252 Token::Cross => "CROSS",
253 Token::Outer => "OUTER",
254 Token::On => "ON",
255 Token::LineComment(text) => text,
256 Token::BlockComment(text) => text,
257 Token::Eof => "EOF",
258 Token::Qualify => "QUALIFY",
259 }
260 }
261}
262
263pub struct TextEditor;
265
266impl TextEditor {
267 #[must_use]
270 pub fn kill_line_backward(text: &str, cursor_pos: usize) -> Option<(String, String)> {
271 if cursor_pos == 0 {
272 return None;
273 }
274
275 let killed_text = text.chars().take(cursor_pos).collect::<String>();
276 let remaining_text = text.chars().skip(cursor_pos).collect::<String>();
277
278 Some((killed_text, remaining_text))
279 }
280
281 #[must_use]
284 pub fn kill_line_forward(text: &str, cursor_pos: usize) -> Option<(String, String)> {
285 if cursor_pos >= text.len() {
286 return None;
287 }
288
289 let remaining_text = text.chars().take(cursor_pos).collect::<String>();
290 let killed_text = text.chars().skip(cursor_pos).collect::<String>();
291
292 Some((killed_text, remaining_text))
293 }
294
295 #[must_use]
298 pub fn delete_word_backward(text: &str, cursor_pos: usize) -> Option<(String, String, usize)> {
299 if cursor_pos == 0 {
300 return None;
301 }
302
303 let before_cursor = &text[..cursor_pos];
304 let after_cursor = &text[cursor_pos..];
305
306 let mut word_start = before_cursor.len();
308 let mut chars = before_cursor.chars().rev().peekable();
309
310 while let Some(&ch) = chars.peek() {
312 if ch.is_whitespace() {
313 word_start -= ch.len_utf8();
314 chars.next();
315 } else {
316 break;
317 }
318 }
319
320 while let Some(&ch) = chars.peek() {
322 if !ch.is_alphanumeric() && ch != '_' {
323 break;
324 }
325 word_start -= ch.len_utf8();
326 chars.next();
327 }
328
329 while let Some(&ch) = chars.peek() {
331 if ch.is_whitespace() {
332 word_start -= ch.len_utf8();
333 chars.next();
334 } else {
335 break;
336 }
337 }
338
339 let deleted_text = text[word_start..cursor_pos].to_string();
340 let remaining_text = format!("{}{}", &text[..word_start], after_cursor);
341
342 Some((deleted_text, remaining_text, word_start))
343 }
344
345 #[must_use]
348 pub fn delete_word_forward(text: &str, cursor_pos: usize) -> Option<(String, String)> {
349 if cursor_pos >= text.len() {
350 return None;
351 }
352
353 let before_cursor = &text[..cursor_pos];
354 let after_cursor = &text[cursor_pos..];
355
356 let mut chars = after_cursor.chars();
358 let mut word_end = 0;
359
360 while let Some(ch) = chars.next() {
362 word_end += ch.len_utf8();
363 if ch.is_alphanumeric() || ch == '_' {
364 for ch in chars.by_ref() {
366 if !ch.is_alphanumeric() && ch != '_' {
367 break;
368 }
369 word_end += ch.len_utf8();
370 }
371 break;
372 }
373 }
374
375 let deleted_text = text[cursor_pos..cursor_pos + word_end].to_string();
376 let remaining_text = format!("{}{}", before_cursor, &after_cursor[word_end..]);
377
378 Some((deleted_text, remaining_text))
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn test_cursor_token_position() {
388 let query = "SELECT * FROM users WHERE id = 1";
389
390 assert_eq!(TextNavigator::get_cursor_token_position(query, 0), (0, 8));
392
393 assert_eq!(TextNavigator::get_cursor_token_position(query, 3), (1, 8));
395
396 assert_eq!(TextNavigator::get_cursor_token_position(query, 7), (2, 8));
398 }
399
400 #[test]
401 fn test_kill_line_backward() {
402 let text = "SELECT * FROM users";
403
404 let result = TextEditor::kill_line_backward(text, 8);
406 assert_eq!(
407 result,
408 Some(("SELECT *".to_string(), " FROM users".to_string()))
409 );
410
411 let result = TextEditor::kill_line_backward(text, 0);
413 assert_eq!(result, None);
414 }
415
416 #[test]
417 fn test_delete_word_backward() {
418 let text = "SELECT * FROM users";
419
420 let result = TextEditor::delete_word_backward(text, 13);
422 assert_eq!(
423 result,
424 Some((" FROM".to_string(), "SELECT * users".to_string(), 8))
425 );
426 }
427}