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