sql_cli/ui/operations/
simple_operations.rs

1//! Simple utility operations
2//!
3//! This module contains simple, self-contained operations extracted from the TUI
4//! that have minimal dependencies and can be easily tested.
5
6use crate::buffer::{BufferAPI, BufferManager};
7use crate::text_navigation::TextNavigator;
8
9/// Context for text navigation operations
10pub struct TextNavigationContext<'a> {
11    pub query: &'a str,
12    pub cursor_pos: usize,
13}
14
15/// Context for undo operations
16pub struct UndoContext<'a> {
17    pub buffer_manager: &'a mut BufferManager,
18}
19
20/// Get the cursor token position in the query text
21/// Returns (start, end) positions of the token at cursor
22pub fn get_cursor_token_position(ctx: &TextNavigationContext) -> (usize, usize) {
23    TextNavigator::get_cursor_token_position(ctx.query, ctx.cursor_pos)
24}
25
26/// Get the token at the cursor position
27/// Returns the token string if found
28pub fn get_token_at_cursor(ctx: &TextNavigationContext) -> Option<String> {
29    TextNavigator::get_token_at_cursor(ctx.query, ctx.cursor_pos)
30}
31
32/// Result of an undo operation
33#[derive(Debug, PartialEq)]
34pub enum UndoResult {
35    /// Undo was performed successfully
36    Success,
37    /// Nothing to undo
38    NothingToUndo,
39    /// No buffer available
40    NoBuffer,
41}
42
43impl UndoResult {
44    /// Get the status message for this result
45    pub fn status_message(&self) -> &'static str {
46        match self {
47            UndoResult::Success => "Undo performed",
48            UndoResult::NothingToUndo => "Nothing to undo",
49            UndoResult::NoBuffer => "No buffer available for undo",
50        }
51    }
52}
53
54/// Perform an undo operation on the current buffer
55pub fn perform_undo(ctx: &mut UndoContext) -> UndoResult {
56    if let Some(buffer) = ctx.buffer_manager.current_mut() {
57        if buffer.perform_undo() {
58            UndoResult::Success
59        } else {
60            UndoResult::NothingToUndo
61        }
62    } else {
63        UndoResult::NoBuffer
64    }
65}
66
67/// Check for common SQL parser errors in a query string
68/// Returns an error message if issues are found, None if the query looks valid
69pub fn check_parser_error(query: &str) -> Option<String> {
70    // Quick check for common parser errors
71    let mut paren_depth = 0;
72    let mut in_string = false;
73    let mut escape_next = false;
74
75    for ch in query.chars() {
76        if escape_next {
77            escape_next = false;
78            continue;
79        }
80
81        match ch {
82            '\\' if in_string => escape_next = true,
83            '\'' => in_string = !in_string,
84            '(' if !in_string => paren_depth += 1,
85            ')' if !in_string => {
86                paren_depth -= 1;
87                if paren_depth < 0 {
88                    return Some("Extra )".to_string());
89                }
90            }
91            _ => {}
92        }
93    }
94
95    if paren_depth > 0 {
96        return Some(format!("Missing {} )", paren_depth));
97    }
98
99    // Could add more checks here (unclosed strings, etc.)
100    if in_string {
101        return Some("Unclosed string".to_string());
102    }
103
104    None
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_get_cursor_token_position() {
113        let ctx = TextNavigationContext {
114            query: "SELECT name FROM users",
115            cursor_pos: 2, // Inside "SELECT"
116        };
117
118        let (start, end) = get_cursor_token_position(&ctx);
119        // This test assumes TextNavigator works correctly
120        // The exact values depend on TextNavigator's implementation
121        assert!(start <= ctx.cursor_pos);
122        assert!(end >= ctx.cursor_pos);
123    }
124
125    #[test]
126    fn test_get_token_at_cursor() {
127        let ctx = TextNavigationContext {
128            query: "SELECT name FROM users",
129            cursor_pos: 2, // Inside "SELECT"
130        };
131
132        let token = get_token_at_cursor(&ctx);
133        // The exact token depends on TextNavigator's implementation
134        // We just verify that we get some result
135        assert!(token.is_some() || token.is_none()); // Always true, just ensures it compiles
136    }
137
138    #[test]
139    fn test_get_token_at_cursor_empty_query() {
140        let ctx = TextNavigationContext {
141            query: "",
142            cursor_pos: 0,
143        };
144
145        let token = get_token_at_cursor(&ctx);
146        // Should handle empty query gracefully
147        assert!(token.is_none() || token.is_some()); // Always true, just ensures it compiles
148    }
149
150    #[test]
151    fn test_undo_result_status_messages() {
152        assert_eq!(UndoResult::Success.status_message(), "Undo performed");
153        assert_eq!(
154            UndoResult::NothingToUndo.status_message(),
155            "Nothing to undo"
156        );
157        assert_eq!(
158            UndoResult::NoBuffer.status_message(),
159            "No buffer available for undo"
160        );
161    }
162
163    #[test]
164    fn test_check_parser_error_valid_queries() {
165        assert_eq!(check_parser_error("SELECT * FROM users"), None);
166        assert_eq!(
167            check_parser_error("SELECT name FROM users WHERE id = 1"),
168            None
169        );
170        assert_eq!(
171            check_parser_error("SELECT (column1 + column2) FROM table"),
172            None
173        );
174        assert_eq!(check_parser_error("SELECT 'hello world' FROM dual"), None);
175    }
176
177    #[test]
178    fn test_check_parser_error_mismatched_parens() {
179        assert_eq!(
180            check_parser_error("SELECT (column FROM table"),
181            Some("Missing 1 )".to_string())
182        );
183        assert_eq!(
184            check_parser_error("SELECT ((column FROM table"),
185            Some("Missing 2 )".to_string())
186        );
187        assert_eq!(
188            check_parser_error("SELECT column) FROM table"),
189            Some("Extra )".to_string())
190        );
191    }
192
193    #[test]
194    fn test_check_parser_error_unclosed_string() {
195        assert_eq!(
196            check_parser_error("SELECT 'unclosed FROM table"),
197            Some("Unclosed string".to_string())
198        );
199        assert_eq!(
200            check_parser_error("SELECT name FROM users WHERE name = 'test"),
201            Some("Unclosed string".to_string())
202        );
203    }
204
205    #[test]
206    fn test_check_parser_error_escaped_quotes() {
207        // Escaped quotes should not be treated as string terminators
208        assert_eq!(
209            check_parser_error("SELECT 'it\\'s a test' FROM table"),
210            None
211        );
212    }
213
214    #[test]
215    fn test_check_parser_error_parens_in_strings() {
216        // Parentheses inside strings should not affect parentheses counting
217        assert_eq!(
218            check_parser_error("SELECT 'text with (parens)' FROM table"),
219            None
220        );
221        assert_eq!(
222            check_parser_error("SELECT 'text with (unclosed FROM table"),
223            Some("Unclosed string".to_string())
224        );
225    }
226
227    #[test]
228    fn test_check_parser_error_empty_query() {
229        assert_eq!(check_parser_error(""), None);
230    }
231
232    // Note: Testing perform_undo would require setting up a full BufferManager with buffers
233    // and undo state, which is complex. The function is simple enough that integration
234    // testing through the TUI is sufficient for now.
235}