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
26#[must_use]
30pub fn kill_line(text: &str, cursor_position: usize) -> TextOperationResult {
31 let text_len = text.len();
32
33 if cursor_position >= text_len {
34 return TextOperationResult {
36 new_text: text.to_string(),
37 new_cursor_position: cursor_position,
38 killed_text: None,
39 description: "Nothing to kill".to_string(),
40 };
41 }
42
43 let line_end = text[cursor_position..]
45 .find('\n')
46 .map_or(text_len, |pos| cursor_position + pos);
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 {killed_len} characters"),
66 }
67}
68
69#[must_use]
71pub fn kill_line_backward(text: &str, cursor_position: usize) -> TextOperationResult {
72 if cursor_position == 0 {
73 return TextOperationResult {
75 new_text: text.to_string(),
76 new_cursor_position: 0,
77 killed_text: None,
78 description: "Nothing to kill".to_string(),
79 };
80 }
81
82 let line_start = text[..cursor_position].rfind('\n').map_or(0, |pos| pos + 1);
84
85 let killed = text[line_start..cursor_position].to_string();
86 let mut new_text = String::with_capacity(text.len());
87 new_text.push_str(&text[..line_start]);
88 new_text.push_str(&text[cursor_position..]);
89
90 let killed_len = killed.len();
91 TextOperationResult {
92 new_text,
93 new_cursor_position: line_start,
94 killed_text: Some(killed),
95 description: format!("Killed {killed_len} characters backward"),
96 }
97}
98
99#[must_use]
101pub fn delete_word_backward(text: &str, cursor_position: usize) -> TextOperationResult {
102 if cursor_position == 0 {
103 return TextOperationResult {
104 new_text: text.to_string(),
105 new_cursor_position: 0,
106 killed_text: None,
107 description: "At beginning of text".to_string(),
108 };
109 }
110
111 let mut pos = cursor_position;
113 while pos > 0 && text.chars().nth(pos - 1).is_some_and(char::is_whitespace) {
114 pos -= 1;
115 }
116
117 let word_start = if pos == 0 {
119 0
120 } else {
121 let mut start = pos;
122 while start > 0 && !text.chars().nth(start - 1).is_some_and(char::is_whitespace) {
123 start -= 1;
124 }
125 start
126 };
127
128 let killed = text[word_start..cursor_position].to_string();
129 let mut new_text = String::with_capacity(text.len());
130 new_text.push_str(&text[..word_start]);
131 new_text.push_str(&text[cursor_position..]);
132
133 let killed_trimmed = killed.trim().to_string();
134 TextOperationResult {
135 new_text,
136 new_cursor_position: word_start,
137 killed_text: Some(killed),
138 description: format!("Deleted word: '{killed_trimmed}'"),
139 }
140}
141
142#[must_use]
144pub fn delete_word_forward(text: &str, cursor_position: usize) -> TextOperationResult {
145 let text_len = text.len();
146 if cursor_position >= text_len {
147 return TextOperationResult {
148 new_text: text.to_string(),
149 new_cursor_position: cursor_position,
150 killed_text: None,
151 description: "At end of text".to_string(),
152 };
153 }
154
155 let mut pos = cursor_position;
157 while pos < text_len && text.chars().nth(pos).is_some_and(char::is_whitespace) {
158 pos += 1;
159 }
160
161 let word_end = if pos >= text_len {
163 text_len
164 } else {
165 let mut end = pos;
166 while end < text_len && !text.chars().nth(end).is_some_and(char::is_whitespace) {
167 end += 1;
168 }
169 end
170 };
171
172 let killed = text[cursor_position..word_end].to_string();
173 let mut new_text = String::with_capacity(text.len());
174 new_text.push_str(&text[..cursor_position]);
175 new_text.push_str(&text[word_end..]);
176
177 let killed_trimmed = killed.trim().to_string();
178 TextOperationResult {
179 new_text,
180 new_cursor_position: cursor_position,
181 killed_text: Some(killed),
182 description: format!("Deleted word: '{killed_trimmed}'"),
183 }
184}
185
186#[must_use]
190pub fn move_word_backward(text: &str, cursor_position: usize) -> CursorMovementResult {
191 if cursor_position == 0 {
192 return CursorMovementResult {
193 new_position: 0,
194 jumped_text: None,
195 };
196 }
197
198 let mut pos = cursor_position;
200 while pos > 0 && text.chars().nth(pos - 1).is_some_and(char::is_whitespace) {
201 pos -= 1;
202 }
203
204 let word_start = if pos == 0 {
206 0
207 } else {
208 let mut start = pos;
209 while start > 0 && !text.chars().nth(start - 1).is_some_and(char::is_whitespace) {
210 start -= 1;
211 }
212 start
213 };
214
215 let jumped = if word_start < cursor_position {
216 Some(text[word_start..cursor_position].to_string())
217 } else {
218 None
219 };
220
221 CursorMovementResult {
222 new_position: word_start,
223 jumped_text: jumped,
224 }
225}
226
227#[must_use]
229pub fn move_word_forward(text: &str, cursor_position: usize) -> CursorMovementResult {
230 let text_len = text.len();
231 if cursor_position >= text_len {
232 return CursorMovementResult {
233 new_position: cursor_position,
234 jumped_text: None,
235 };
236 }
237
238 let mut pos = cursor_position;
240 while pos < text_len && !text.chars().nth(pos).is_some_and(char::is_whitespace) {
241 pos += 1;
242 }
243
244 while pos < text_len && text.chars().nth(pos).is_some_and(char::is_whitespace) {
246 pos += 1;
247 }
248
249 let jumped = if pos > cursor_position {
250 Some(text[cursor_position..pos].to_string())
251 } else {
252 None
253 };
254
255 CursorMovementResult {
256 new_position: pos,
257 jumped_text: jumped,
258 }
259}
260
261#[must_use]
263pub fn jump_to_prev_token(text: &str, cursor_position: usize) -> CursorMovementResult {
264 if cursor_position == 0 {
265 return CursorMovementResult {
266 new_position: 0,
267 jumped_text: None,
268 };
269 }
270
271 let mut pos = cursor_position;
274
275 while pos > 0 {
277 let ch = text.chars().nth(pos - 1);
278 if let Some(c) = ch {
279 if c.is_whitespace() || "(),;=<>!+-*/".contains(c) {
280 pos -= 1;
281 } else {
282 break;
283 }
284 } else {
285 break;
286 }
287 }
288
289 let token_start = if pos == 0 {
291 0
292 } else {
293 let mut start = pos;
294 while start > 0 {
295 let ch = text.chars().nth(start - 1);
296 if let Some(c) = ch {
297 if !c.is_whitespace() && !"(),;=<>!+-*/".contains(c) {
298 start -= 1;
299 } else {
300 break;
301 }
302 } else {
303 break;
304 }
305 }
306 start
307 };
308
309 let jumped = if token_start < cursor_position {
310 Some(text[token_start..cursor_position].to_string())
311 } else {
312 None
313 };
314
315 CursorMovementResult {
316 new_position: token_start,
317 jumped_text: jumped,
318 }
319}
320
321#[must_use]
323pub fn jump_to_next_token(text: &str, cursor_position: usize) -> CursorMovementResult {
324 let text_len = text.len();
325 if cursor_position >= text_len {
326 return CursorMovementResult {
327 new_position: cursor_position,
328 jumped_text: None,
329 };
330 }
331
332 let mut pos = cursor_position;
333
334 while pos < text_len {
336 let ch = text.chars().nth(pos);
337 if let Some(c) = ch {
338 if !c.is_whitespace() && !"(),;=<>!+-*/".contains(c) {
339 pos += 1;
340 } else {
341 break;
342 }
343 } else {
344 break;
345 }
346 }
347
348 while pos < text_len {
350 let ch = text.chars().nth(pos);
351 if let Some(c) = ch {
352 if c.is_whitespace() || "(),;=<>!+-*/".contains(c) {
353 pos += 1;
354 } else {
355 break;
356 }
357 } else {
358 break;
359 }
360 }
361
362 let jumped = if pos > cursor_position {
363 Some(text[cursor_position..pos].to_string())
364 } else {
365 None
366 };
367
368 CursorMovementResult {
369 new_position: pos,
370 jumped_text: jumped,
371 }
372}
373
374#[must_use]
378pub fn clear_text() -> TextOperationResult {
379 TextOperationResult {
380 new_text: String::new(),
381 new_cursor_position: 0,
382 killed_text: None,
383 description: "Cleared all text".to_string(),
384 }
385}
386
387#[must_use]
389pub fn insert_char(text: &str, cursor_position: usize, ch: char) -> TextOperationResult {
390 let mut new_text = String::with_capacity(text.len() + 1);
391 new_text.push_str(&text[..cursor_position.min(text.len())]);
392 new_text.push(ch);
393 if cursor_position < text.len() {
394 new_text.push_str(&text[cursor_position..]);
395 }
396
397 TextOperationResult {
398 new_text,
399 new_cursor_position: cursor_position + 1,
400 killed_text: None,
401 description: format!("Inserted '{ch}'"),
402 }
403}
404
405#[must_use]
407pub fn delete_char(text: &str, cursor_position: usize) -> TextOperationResult {
408 if cursor_position >= text.len() {
409 return TextOperationResult {
410 new_text: text.to_string(),
411 new_cursor_position: cursor_position,
412 killed_text: None,
413 description: "Nothing to delete".to_string(),
414 };
415 }
416
417 let deleted = text.chars().nth(cursor_position).unwrap();
418 let mut new_text = String::with_capacity(text.len() - 1);
419 new_text.push_str(&text[..cursor_position]);
420 new_text.push_str(&text[cursor_position + 1..]);
421
422 TextOperationResult {
423 new_text,
424 new_cursor_position: cursor_position,
425 killed_text: Some(deleted.to_string()),
426 description: format!("Deleted '{deleted}'"),
427 }
428}
429
430#[must_use]
432pub fn backspace(text: &str, cursor_position: usize) -> TextOperationResult {
433 if cursor_position == 0 {
434 return TextOperationResult {
435 new_text: text.to_string(),
436 new_cursor_position: 0,
437 killed_text: None,
438 description: "At beginning".to_string(),
439 };
440 }
441
442 let deleted = text.chars().nth(cursor_position - 1).unwrap();
443 let mut new_text = String::with_capacity(text.len() - 1);
444 new_text.push_str(&text[..cursor_position - 1]);
445 new_text.push_str(&text[cursor_position..]);
446
447 TextOperationResult {
448 new_text,
449 new_cursor_position: cursor_position - 1,
450 killed_text: Some(deleted.to_string()),
451 description: format!("Deleted '{deleted}'"),
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_kill_line() {
461 let text = "SELECT * FROM table WHERE id = 1";
462 let result = kill_line(text, 7);
463 assert_eq!(result.new_text, "SELECT ");
464 assert_eq!(
465 result.killed_text,
466 Some("* FROM table WHERE id = 1".to_string())
467 );
468 assert_eq!(result.new_cursor_position, 7);
469 }
470
471 #[test]
472 fn test_kill_line_backward() {
473 let text = "SELECT * FROM table";
474 let result = kill_line_backward(text, 7);
475 assert_eq!(result.new_text, "* FROM table");
476 assert_eq!(result.killed_text, Some("SELECT ".to_string()));
477 assert_eq!(result.new_cursor_position, 0);
478 }
479
480 #[test]
481 fn test_delete_word_backward() {
482 let text = "SELECT * FROM table";
483 let result = delete_word_backward(text, 13); assert_eq!(result.new_text, "SELECT * table");
485 assert_eq!(result.killed_text, Some("FROM".to_string()));
486 assert_eq!(result.new_cursor_position, 9);
487 }
488
489 #[test]
490 fn test_move_word_forward() {
491 let text = "SELECT * FROM table";
492 let result = move_word_forward(text, 0);
493 assert_eq!(result.new_position, 7); let result2 = move_word_forward(text, 7);
496 assert_eq!(result2.new_position, 9); }
498
499 #[test]
500 fn test_move_word_backward() {
501 let text = "SELECT * FROM table";
502 let result = move_word_backward(text, 13); assert_eq!(result.new_position, 9); let result2 = move_word_backward(text, 9);
506 assert_eq!(result2.new_position, 7); }
508
509 #[test]
510 fn test_jump_to_next_token() {
511 let text = "SELECT id, name FROM users WHERE id = 1";
512 let result = jump_to_next_token(text, 0);
513 assert_eq!(result.new_position, 7); let result2 = jump_to_next_token(text, 7);
516 assert_eq!(result2.new_position, 11); }
518
519 #[test]
520 fn test_insert_and_delete() {
521 let text = "SELECT";
522 let result = insert_char(text, 6, ' ');
523 assert_eq!(result.new_text, "SELECT ");
524 assert_eq!(result.new_cursor_position, 7);
525
526 let result2 = delete_char(&result.new_text, 6);
527 assert_eq!(result2.new_text, "SELECT");
528
529 let result3 = backspace(&result.new_text, 7);
530 assert_eq!(result3.new_text, "SELECT");
531 assert_eq!(result3.new_cursor_position, 6);
532 }
533}
534
535#[must_use]
540pub fn extract_partial_word_at_cursor(query: &str, cursor_pos: usize) -> Option<String> {
541 if cursor_pos == 0 || cursor_pos > query.len() {
542 return None;
543 }
544
545 let chars: Vec<char> = query.chars().collect();
546 let mut start = cursor_pos;
547 let end = cursor_pos;
548
549 let mut in_quote = false;
551
552 while start > 0 {
554 let prev_char = chars[start - 1];
555 if prev_char == '"' {
556 start -= 1;
558 in_quote = true;
559 break;
560 } else if prev_char.is_alphanumeric() || prev_char == '_' || (prev_char == ' ' && in_quote)
561 {
562 start -= 1;
563 } else {
564 break;
565 }
566 }
567
568 if in_quote && start > 0 {
571 }
575
576 let start_byte = chars[..start].iter().map(|c| c.len_utf8()).sum();
578 let end_byte = chars[..end].iter().map(|c| c.len_utf8()).sum();
579
580 if start_byte < end_byte {
581 Some(query[start_byte..end_byte].to_string())
582 } else {
583 None
584 }
585}
586
587#[derive(Debug, Clone)]
589pub struct CompletionResult {
590 pub new_text: String,
592 pub new_cursor_position: usize,
594 pub description: String,
596}
597
598#[must_use]
601pub fn apply_completion_to_text(
602 query: &str,
603 cursor_pos: usize,
604 partial_word: &str,
605 suggestion: &str,
606) -> CompletionResult {
607 let before_partial = &query[..cursor_pos - partial_word.len()];
608 let after_cursor = &query[cursor_pos..];
609
610 let suggestion_to_use = if partial_word.starts_with('"') && suggestion.starts_with('"') {
612 if suggestion.len() > 1 {
614 suggestion[1..].to_string()
615 } else {
616 suggestion.to_string()
617 }
618 } else {
619 suggestion.to_string()
620 };
621
622 let new_query = format!("{before_partial}{suggestion_to_use}{after_cursor}");
623
624 let new_cursor_pos = if suggestion_to_use.ends_with("('')") {
626 before_partial.len() + suggestion_to_use.len() - 2
629 } else if suggestion_to_use.ends_with("()") {
630 before_partial.len() + suggestion_to_use.len()
633 } else {
634 before_partial.len() + suggestion_to_use.len()
636 };
637
638 let description = if suggestion_to_use.ends_with("('')") {
640 format!("Completed '{suggestion}' with cursor positioned for parameter input")
641 } else if suggestion_to_use.ends_with("()") {
642 format!("Completed parameterless function '{suggestion}'")
643 } else {
644 format!("Completed '{suggestion}'")
645 };
646
647 CompletionResult {
648 new_text: new_query,
649 new_cursor_position: new_cursor_pos,
650 description,
651 }
652}