sql_cli/
cursor_operations.rs

1/// Cursor and text manipulation operations for SQL input
2/// These operations use the lexer to understand SQL syntax and provide
3/// intelligent cursor movement and text editing
4use crate::recursive_parser::Lexer;
5
6pub struct CursorOperations;
7
8impl CursorOperations {
9    /// Move cursor to the previous word boundary
10    pub fn find_word_boundary_backward(text: &str, cursor_pos: usize) -> usize {
11        if cursor_pos == 0 {
12            return 0;
13        }
14
15        // Use lexer to tokenize and find word boundaries
16        let mut lexer = Lexer::new(text);
17        let tokens = lexer.tokenize_all_with_positions();
18
19        // Find the token boundary before the cursor
20        let mut target_pos = 0;
21        for (start, end, _) in tokens.iter().rev() {
22            if *end <= cursor_pos {
23                // If we're at the start of a token, go to the previous one
24                if *end == cursor_pos && start < &cursor_pos {
25                    target_pos = *start;
26                } else {
27                    // Otherwise go to the start of this token
28                    for (s, e, _) in tokens.iter().rev() {
29                        if *e <= cursor_pos && *s < cursor_pos {
30                            target_pos = *s;
31                            break;
32                        }
33                    }
34                }
35                break;
36            }
37        }
38
39        target_pos
40    }
41
42    /// Move cursor to the next word boundary
43    pub fn find_word_boundary_forward(text: &str, cursor_pos: usize) -> usize {
44        // Use lexer to tokenize
45        let mut lexer = Lexer::new(text);
46        let tokens = lexer.tokenize_all_with_positions();
47
48        // Find the next token boundary after cursor
49        for (start, _, _) in &tokens {
50            if *start > cursor_pos {
51                return *start;
52            }
53        }
54
55        // If no token found, go to end
56        text.len()
57    }
58
59    /// Delete from cursor to previous word boundary
60    pub fn delete_word_backward(text: &str, cursor_pos: usize) -> (String, usize) {
61        if cursor_pos == 0 {
62            return (text.to_string(), cursor_pos);
63        }
64
65        let word_start = Self::find_word_boundary_backward(text, cursor_pos);
66
67        // Delete from word_start to cursor_pos
68        let mut new_text = String::new();
69        new_text.push_str(&text[..word_start]);
70        new_text.push_str(&text[cursor_pos..]);
71
72        (new_text, word_start)
73    }
74
75    /// Delete from cursor to next word boundary
76    pub fn delete_word_forward(text: &str, cursor_pos: usize) -> (String, usize) {
77        if cursor_pos >= text.len() {
78            return (text.to_string(), cursor_pos);
79        }
80
81        let word_end = Self::find_word_boundary_forward(text, cursor_pos);
82
83        // Delete from cursor_pos to word_end
84        let mut new_text = String::new();
85        new_text.push_str(&text[..cursor_pos]);
86        new_text.push_str(&text[word_end..]);
87
88        (new_text, cursor_pos)
89    }
90
91    /// Kill line from cursor to end
92    pub fn kill_line(text: &str, cursor_pos: usize) -> (String, String) {
93        let killed = text[cursor_pos..].to_string();
94        let new_text = text[..cursor_pos].to_string();
95        (new_text, killed)
96    }
97
98    /// Kill line from start to cursor
99    pub fn kill_line_backward(text: &str, cursor_pos: usize) -> (String, String, usize) {
100        let killed = text[..cursor_pos].to_string();
101        let new_text = text[cursor_pos..].to_string();
102        (new_text, killed, 0) // New cursor position is 0
103    }
104
105    /// Jump to previous SQL token
106    pub fn jump_to_prev_token(text: &str, cursor_pos: usize) -> usize {
107        let mut lexer = Lexer::new(text);
108        let tokens = lexer.tokenize_all_with_positions();
109
110        // Find the previous significant token (skip whitespace/punctuation)
111        let mut target_pos = cursor_pos;
112        for (start, _, _) in tokens.iter().rev() {
113            if *start < cursor_pos {
114                target_pos = *start;
115                break;
116            }
117        }
118
119        target_pos
120    }
121
122    /// Jump to next SQL token
123    pub fn jump_to_next_token(text: &str, cursor_pos: usize) -> usize {
124        let mut lexer = Lexer::new(text);
125        let tokens = lexer.tokenize_all_with_positions();
126
127        // Find the next significant token
128        for (start, _, _) in &tokens {
129            if *start > cursor_pos {
130                return *start;
131            }
132        }
133
134        text.len()
135    }
136
137    /// Find position of matching bracket/parenthesis
138    pub fn find_matching_bracket(text: &str, cursor_pos: usize) -> Option<usize> {
139        let chars: Vec<char> = text.chars().collect();
140        if cursor_pos >= chars.len() {
141            return None;
142        }
143
144        let ch = chars[cursor_pos];
145        let (open, close, direction) = match ch {
146            '(' => ('(', ')', 1),
147            ')' => ('(', ')', -1),
148            '[' => ('[', ']', 1),
149            ']' => ('[', ']', -1),
150            '{' => ('{', '}', 1),
151            '}' => ('{', '}', -1),
152            _ => return None,
153        };
154
155        let mut count = 1;
156        let mut pos = cursor_pos as isize;
157
158        while count > 0 {
159            pos += direction;
160            if pos < 0 || pos >= chars.len() as isize {
161                return None;
162            }
163
164            let current = chars[pos as usize];
165            if current == open {
166                count += if direction > 0 { 1 } else { -1 };
167            } else if current == close {
168                count -= if direction > 0 { 1 } else { -1 };
169            }
170        }
171
172        Some(pos as usize)
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_word_boundary_backward() {
182        let text = "SELECT * FROM users WHERE id = 1";
183        assert_eq!(CursorOperations::find_word_boundary_backward(text, 14), 9); // FROM -> *
184        assert_eq!(CursorOperations::find_word_boundary_backward(text, 7), 0); // SELECT start
185    }
186
187    #[test]
188    fn test_delete_word_backward() {
189        let text = "SELECT * FROM users";
190        let (new_text, cursor) = CursorOperations::delete_word_backward(text, 19); // At end
191        assert_eq!(new_text, "SELECT * FROM ");
192        assert_eq!(cursor, 14);
193    }
194
195    #[test]
196    fn test_kill_line() {
197        let text = "SELECT * FROM users WHERE id = 1";
198        let (new_text, killed) = CursorOperations::kill_line(text, 19); // After "users"
199        assert_eq!(new_text, "SELECT * FROM users");
200        assert_eq!(killed, " WHERE id = 1");
201    }
202
203    #[test]
204    fn test_matching_bracket() {
205        let text = "SELECT * FROM (SELECT id FROM users)";
206        assert_eq!(CursorOperations::find_matching_bracket(text, 14), Some(35)); // ( -> )
207        assert_eq!(CursorOperations::find_matching_bracket(text, 35), Some(14));
208        // ) -> (
209    }
210}