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