1#[derive(Debug, Clone)]
6pub struct TextOperationResult {
7 pub new_text: String,
9 pub new_cursor_position: usize,
11 pub killed_text: Option<String>,
13 pub description: String,
15}
16
17#[derive(Debug, Clone)]
19pub struct CursorMovementResult {
20 pub new_position: usize,
22 pub jumped_text: Option<String>,
24}
25
26pub fn kill_line(text: &str, cursor_position: usize) -> TextOperationResult {
30 let text_len = text.len();
31
32 if cursor_position >= text_len {
33 return TextOperationResult {
35 new_text: text.to_string(),
36 new_cursor_position: cursor_position,
37 killed_text: None,
38 description: "Nothing to kill".to_string(),
39 };
40 }
41
42 let line_end = text[cursor_position..]
44 .find('\n')
45 .map(|pos| cursor_position + pos)
46 .unwrap_or(text_len);
47
48 let killed = text[cursor_position..line_end].to_string();
49 let mut new_text = String::with_capacity(text_len);
50 new_text.push_str(&text[..cursor_position]);
51
52 if line_end < text_len && text.chars().nth(line_end) == Some('\n') {
54 new_text.push('\n');
55 new_text.push_str(&text[line_end + 1..]);
56 } else {
57 new_text.push_str(&text[line_end..]);
58 }
59
60 let killed_len = killed.len();
61 TextOperationResult {
62 new_text,
63 new_cursor_position: cursor_position,
64 killed_text: Some(killed),
65 description: format!("Killed {} characters", killed_len),
66 }
67}
68
69pub fn kill_line_backward(text: &str, cursor_position: usize) -> TextOperationResult {
71 if cursor_position == 0 {
72 return TextOperationResult {
74 new_text: text.to_string(),
75 new_cursor_position: 0,
76 killed_text: None,
77 description: "Nothing to kill".to_string(),
78 };
79 }
80
81 let line_start = text[..cursor_position]
83 .rfind('\n')
84 .map(|pos| pos + 1)
85 .unwrap_or(0);
86
87 let killed = text[line_start..cursor_position].to_string();
88 let mut new_text = String::with_capacity(text.len());
89 new_text.push_str(&text[..line_start]);
90 new_text.push_str(&text[cursor_position..]);
91
92 let killed_len = killed.len();
93 TextOperationResult {
94 new_text,
95 new_cursor_position: line_start,
96 killed_text: Some(killed),
97 description: format!("Killed {} characters backward", killed_len),
98 }
99}
100
101pub fn delete_word_backward(text: &str, cursor_position: usize) -> TextOperationResult {
103 if cursor_position == 0 {
104 return TextOperationResult {
105 new_text: text.to_string(),
106 new_cursor_position: 0,
107 killed_text: None,
108 description: "At beginning of text".to_string(),
109 };
110 }
111
112 let mut pos = cursor_position;
114 while pos > 0
115 && text
116 .chars()
117 .nth(pos - 1)
118 .map_or(false, |c| c.is_whitespace())
119 {
120 pos -= 1;
121 }
122
123 let word_start = if pos == 0 {
125 0
126 } else {
127 let mut start = pos;
128 while start > 0
129 && !text
130 .chars()
131 .nth(start - 1)
132 .map_or(false, |c| c.is_whitespace())
133 {
134 start -= 1;
135 }
136 start
137 };
138
139 let killed = text[word_start..cursor_position].to_string();
140 let mut new_text = String::with_capacity(text.len());
141 new_text.push_str(&text[..word_start]);
142 new_text.push_str(&text[cursor_position..]);
143
144 let killed_trimmed = killed.trim().to_string();
145 TextOperationResult {
146 new_text,
147 new_cursor_position: word_start,
148 killed_text: Some(killed),
149 description: format!("Deleted word: '{}'", killed_trimmed),
150 }
151}
152
153pub fn delete_word_forward(text: &str, cursor_position: usize) -> TextOperationResult {
155 let text_len = text.len();
156 if cursor_position >= text_len {
157 return TextOperationResult {
158 new_text: text.to_string(),
159 new_cursor_position: cursor_position,
160 killed_text: None,
161 description: "At end of text".to_string(),
162 };
163 }
164
165 let mut pos = cursor_position;
167 while pos < text_len && text.chars().nth(pos).map_or(false, |c| c.is_whitespace()) {
168 pos += 1;
169 }
170
171 let word_end = if pos >= text_len {
173 text_len
174 } else {
175 let mut end = pos;
176 while end < text_len && !text.chars().nth(end).map_or(false, |c| c.is_whitespace()) {
177 end += 1;
178 }
179 end
180 };
181
182 let killed = text[cursor_position..word_end].to_string();
183 let mut new_text = String::with_capacity(text.len());
184 new_text.push_str(&text[..cursor_position]);
185 new_text.push_str(&text[word_end..]);
186
187 let killed_trimmed = killed.trim().to_string();
188 TextOperationResult {
189 new_text,
190 new_cursor_position: cursor_position,
191 killed_text: Some(killed),
192 description: format!("Deleted word: '{}'", killed_trimmed),
193 }
194}
195
196pub fn move_word_backward(text: &str, cursor_position: usize) -> CursorMovementResult {
200 if cursor_position == 0 {
201 return CursorMovementResult {
202 new_position: 0,
203 jumped_text: None,
204 };
205 }
206
207 let mut pos = cursor_position;
209 while pos > 0
210 && text
211 .chars()
212 .nth(pos - 1)
213 .map_or(false, |c| c.is_whitespace())
214 {
215 pos -= 1;
216 }
217
218 let word_start = if pos == 0 {
220 0
221 } else {
222 let mut start = pos;
223 while start > 0
224 && !text
225 .chars()
226 .nth(start - 1)
227 .map_or(false, |c| c.is_whitespace())
228 {
229 start -= 1;
230 }
231 start
232 };
233
234 let jumped = if word_start < cursor_position {
235 Some(text[word_start..cursor_position].to_string())
236 } else {
237 None
238 };
239
240 CursorMovementResult {
241 new_position: word_start,
242 jumped_text: jumped,
243 }
244}
245
246pub fn move_word_forward(text: &str, cursor_position: usize) -> CursorMovementResult {
248 let text_len = text.len();
249 if cursor_position >= text_len {
250 return CursorMovementResult {
251 new_position: cursor_position,
252 jumped_text: None,
253 };
254 }
255
256 let mut pos = cursor_position;
258 while pos < text_len && !text.chars().nth(pos).map_or(false, |c| c.is_whitespace()) {
259 pos += 1;
260 }
261
262 while pos < text_len && text.chars().nth(pos).map_or(false, |c| c.is_whitespace()) {
264 pos += 1;
265 }
266
267 let jumped = if pos > cursor_position {
268 Some(text[cursor_position..pos].to_string())
269 } else {
270 None
271 };
272
273 CursorMovementResult {
274 new_position: pos,
275 jumped_text: jumped,
276 }
277}
278
279pub fn jump_to_prev_token(text: &str, cursor_position: usize) -> CursorMovementResult {
281 if cursor_position == 0 {
282 return CursorMovementResult {
283 new_position: 0,
284 jumped_text: None,
285 };
286 }
287
288 let mut pos = cursor_position;
291
292 while pos > 0 {
294 let ch = text.chars().nth(pos - 1);
295 if let Some(c) = ch {
296 if c.is_whitespace() || "(),;=<>!+-*/".contains(c) {
297 pos -= 1;
298 } else {
299 break;
300 }
301 } else {
302 break;
303 }
304 }
305
306 let token_start = if pos == 0 {
308 0
309 } else {
310 let mut start = pos;
311 while start > 0 {
312 let ch = text.chars().nth(start - 1);
313 if let Some(c) = ch {
314 if !c.is_whitespace() && !"(),;=<>!+-*/".contains(c) {
315 start -= 1;
316 } else {
317 break;
318 }
319 } else {
320 break;
321 }
322 }
323 start
324 };
325
326 let jumped = if token_start < cursor_position {
327 Some(text[token_start..cursor_position].to_string())
328 } else {
329 None
330 };
331
332 CursorMovementResult {
333 new_position: token_start,
334 jumped_text: jumped,
335 }
336}
337
338pub fn jump_to_next_token(text: &str, cursor_position: usize) -> CursorMovementResult {
340 let text_len = text.len();
341 if cursor_position >= text_len {
342 return CursorMovementResult {
343 new_position: cursor_position,
344 jumped_text: None,
345 };
346 }
347
348 let mut pos = cursor_position;
349
350 while pos < text_len {
352 let ch = text.chars().nth(pos);
353 if let Some(c) = ch {
354 if !c.is_whitespace() && !"(),;=<>!+-*/".contains(c) {
355 pos += 1;
356 } else {
357 break;
358 }
359 } else {
360 break;
361 }
362 }
363
364 while pos < text_len {
366 let ch = text.chars().nth(pos);
367 if let Some(c) = ch {
368 if c.is_whitespace() || "(),;=<>!+-*/".contains(c) {
369 pos += 1;
370 } else {
371 break;
372 }
373 } else {
374 break;
375 }
376 }
377
378 let jumped = if pos > cursor_position {
379 Some(text[cursor_position..pos].to_string())
380 } else {
381 None
382 };
383
384 CursorMovementResult {
385 new_position: pos,
386 jumped_text: jumped,
387 }
388}
389
390pub fn clear_text() -> TextOperationResult {
394 TextOperationResult {
395 new_text: String::new(),
396 new_cursor_position: 0,
397 killed_text: None,
398 description: "Cleared all text".to_string(),
399 }
400}
401
402pub fn insert_char(text: &str, cursor_position: usize, ch: char) -> TextOperationResult {
404 let mut new_text = String::with_capacity(text.len() + 1);
405 new_text.push_str(&text[..cursor_position.min(text.len())]);
406 new_text.push(ch);
407 if cursor_position < text.len() {
408 new_text.push_str(&text[cursor_position..]);
409 }
410
411 TextOperationResult {
412 new_text,
413 new_cursor_position: cursor_position + 1,
414 killed_text: None,
415 description: format!("Inserted '{}'", ch),
416 }
417}
418
419pub fn delete_char(text: &str, cursor_position: usize) -> TextOperationResult {
421 if cursor_position >= text.len() {
422 return TextOperationResult {
423 new_text: text.to_string(),
424 new_cursor_position: cursor_position,
425 killed_text: None,
426 description: "Nothing to delete".to_string(),
427 };
428 }
429
430 let deleted = text.chars().nth(cursor_position).unwrap();
431 let mut new_text = String::with_capacity(text.len() - 1);
432 new_text.push_str(&text[..cursor_position]);
433 new_text.push_str(&text[cursor_position + 1..]);
434
435 TextOperationResult {
436 new_text,
437 new_cursor_position: cursor_position,
438 killed_text: Some(deleted.to_string()),
439 description: format!("Deleted '{}'", deleted),
440 }
441}
442
443pub fn backspace(text: &str, cursor_position: usize) -> TextOperationResult {
445 if cursor_position == 0 {
446 return TextOperationResult {
447 new_text: text.to_string(),
448 new_cursor_position: 0,
449 killed_text: None,
450 description: "At beginning".to_string(),
451 };
452 }
453
454 let deleted = text.chars().nth(cursor_position - 1).unwrap();
455 let mut new_text = String::with_capacity(text.len() - 1);
456 new_text.push_str(&text[..cursor_position - 1]);
457 new_text.push_str(&text[cursor_position..]);
458
459 TextOperationResult {
460 new_text,
461 new_cursor_position: cursor_position - 1,
462 killed_text: Some(deleted.to_string()),
463 description: format!("Deleted '{}'", deleted),
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn test_kill_line() {
473 let text = "SELECT * FROM table WHERE id = 1";
474 let result = kill_line(text, 7);
475 assert_eq!(result.new_text, "SELECT ");
476 assert_eq!(
477 result.killed_text,
478 Some("* FROM table WHERE id = 1".to_string())
479 );
480 assert_eq!(result.new_cursor_position, 7);
481 }
482
483 #[test]
484 fn test_kill_line_backward() {
485 let text = "SELECT * FROM table";
486 let result = kill_line_backward(text, 7);
487 assert_eq!(result.new_text, "* FROM table");
488 assert_eq!(result.killed_text, Some("SELECT ".to_string()));
489 assert_eq!(result.new_cursor_position, 0);
490 }
491
492 #[test]
493 fn test_delete_word_backward() {
494 let text = "SELECT * FROM table";
495 let result = delete_word_backward(text, 13); assert_eq!(result.new_text, "SELECT * table");
497 assert_eq!(result.killed_text, Some("FROM".to_string()));
498 assert_eq!(result.new_cursor_position, 9);
499 }
500
501 #[test]
502 fn test_move_word_forward() {
503 let text = "SELECT * FROM table";
504 let result = move_word_forward(text, 0);
505 assert_eq!(result.new_position, 7); let result2 = move_word_forward(text, 7);
508 assert_eq!(result2.new_position, 9); }
510
511 #[test]
512 fn test_move_word_backward() {
513 let text = "SELECT * FROM table";
514 let result = move_word_backward(text, 13); assert_eq!(result.new_position, 9); let result2 = move_word_backward(text, 9);
518 assert_eq!(result2.new_position, 7); }
520
521 #[test]
522 fn test_jump_to_next_token() {
523 let text = "SELECT id, name FROM users WHERE id = 1";
524 let result = jump_to_next_token(text, 0);
525 assert_eq!(result.new_position, 7); let result2 = jump_to_next_token(text, 7);
528 assert_eq!(result2.new_position, 11); }
530
531 #[test]
532 fn test_insert_and_delete() {
533 let text = "SELECT";
534 let result = insert_char(text, 6, ' ');
535 assert_eq!(result.new_text, "SELECT ");
536 assert_eq!(result.new_cursor_position, 7);
537
538 let result2 = delete_char(&result.new_text, 6);
539 assert_eq!(result2.new_text, "SELECT");
540
541 let result3 = backspace(&result.new_text, 7);
542 assert_eq!(result3.new_text, "SELECT");
543 assert_eq!(result3.new_cursor_position, 6);
544 }
545}
546
547pub fn extract_partial_word_at_cursor(query: &str, cursor_pos: usize) -> Option<String> {
552 if cursor_pos == 0 || cursor_pos > query.len() {
553 return None;
554 }
555
556 let chars: Vec<char> = query.chars().collect();
557 let mut start = cursor_pos;
558 let end = cursor_pos;
559
560 let mut in_quote = false;
562
563 while start > 0 {
565 let prev_char = chars[start - 1];
566 if prev_char == '"' {
567 start -= 1;
569 in_quote = true;
570 break;
571 } else if prev_char.is_alphanumeric() || prev_char == '_' || (prev_char == ' ' && in_quote)
572 {
573 start -= 1;
574 } else {
575 break;
576 }
577 }
578
579 if in_quote && start > 0 {
582 }
586
587 let start_byte = chars[..start].iter().map(|c| c.len_utf8()).sum();
589 let end_byte = chars[..end].iter().map(|c| c.len_utf8()).sum();
590
591 if start_byte < end_byte {
592 Some(query[start_byte..end_byte].to_string())
593 } else {
594 None
595 }
596}
597
598#[derive(Debug, Clone)]
600pub struct CompletionResult {
601 pub new_text: String,
603 pub new_cursor_position: usize,
605 pub description: String,
607}
608
609pub fn apply_completion_to_text(
612 query: &str,
613 cursor_pos: usize,
614 partial_word: &str,
615 suggestion: &str,
616) -> CompletionResult {
617 let before_partial = &query[..cursor_pos - partial_word.len()];
618 let after_cursor = &query[cursor_pos..];
619
620 let suggestion_to_use = if partial_word.starts_with('"') && suggestion.starts_with('"') {
622 if suggestion.len() > 1 {
624 suggestion[1..].to_string()
625 } else {
626 suggestion.to_string()
627 }
628 } else {
629 suggestion.to_string()
630 };
631
632 let new_query = format!("{}{}{}", before_partial, suggestion_to_use, after_cursor);
633
634 let new_cursor_pos = if suggestion_to_use.ends_with("('')") {
636 before_partial.len() + suggestion_to_use.len() - 2
639 } else if suggestion_to_use.ends_with("()") {
640 before_partial.len() + suggestion_to_use.len()
643 } else {
644 before_partial.len() + suggestion_to_use.len()
646 };
647
648 let description = if suggestion_to_use.ends_with("('')") {
650 format!(
651 "Completed '{}' with cursor positioned for parameter input",
652 suggestion
653 )
654 } else if suggestion_to_use.ends_with("()") {
655 format!("Completed parameterless function '{}'", suggestion)
656 } else {
657 format!("Completed '{}'", suggestion)
658 };
659
660 CompletionResult {
661 new_text: new_query,
662 new_cursor_position: new_cursor_pos,
663 description,
664 }
665}