sql_cli/widgets/
debug_widget.rs

1use crate::buffer::{AppMode, BufferAPI, SortState};
2use crate::debug_info::DebugInfo;
3use crate::hybrid_parser::HybridParser;
4use crate::where_parser;
5use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6use ratatui::{
7    layout::Rect,
8    style::{Color, Style},
9    text::{Line, Text},
10    widgets::{Block, Borders, Paragraph, Wrap},
11    Frame,
12};
13
14/// A self-contained debug widget that manages its own state and rendering
15pub struct DebugWidget {
16    /// The debug content to display
17    content: String,
18    /// Current scroll offset
19    scroll_offset: u16,
20    /// Maximum scroll position
21    max_scroll: u16,
22}
23
24impl DebugWidget {
25    pub fn new() -> Self {
26        Self {
27            content: String::new(),
28            scroll_offset: 0,
29            max_scroll: 0,
30        }
31    }
32
33    /// Generate and set debug content
34    pub fn generate_debug(
35        &mut self,
36        buffer: &dyn BufferAPI,
37        buffer_count: usize,
38        buffer_index: usize,
39        buffer_names: Vec<String>,
40        hybrid_parser: &HybridParser,
41        sort_state: &SortState,
42        input_text: &str,
43        cursor_pos: usize,
44        visual_cursor: usize,
45        api_url: &str,
46    ) {
47        // Generate full debug info
48        let mut debug_info = DebugInfo::generate_full_debug_simple(
49            buffer,
50            buffer_count,
51            buffer_index,
52            buffer_names,
53            hybrid_parser,
54            sort_state,
55            input_text,
56            cursor_pos,
57            visual_cursor,
58            api_url,
59        );
60
61        // Add WHERE clause AST if query contains WHERE
62        if input_text.to_lowercase().contains(" where ") {
63            let where_ast_info = match Self::parse_where_clause_ast(input_text) {
64                Ok(ast_str) => ast_str,
65                Err(e) => format!(
66                    "\n========== WHERE CLAUSE AST ==========\nError parsing WHERE clause: {}\n",
67                    e
68                ),
69            };
70            debug_info.push_str(&where_ast_info);
71        }
72
73        self.content = debug_info;
74        self.scroll_offset = 0;
75        self.update_max_scroll();
76    }
77
78    /// Generate pretty formatted SQL
79    pub fn generate_pretty_sql(&mut self, query: &str) {
80        if !query.trim().is_empty() {
81            let debug_text = format!(
82                "Pretty SQL Query\n{}\n\n{}",
83                "=".repeat(50),
84                crate::recursive_parser::format_sql_pretty_compact(query, 5).join("\n")
85            );
86            self.content = debug_text;
87            self.scroll_offset = 0;
88            self.update_max_scroll();
89        }
90    }
91
92    /// Generate test case content
93    pub fn generate_test_case(&mut self, buffer: &dyn BufferAPI) {
94        self.content = DebugInfo::generate_test_case(buffer);
95        self.scroll_offset = 0;
96        self.update_max_scroll();
97    }
98
99    /// Handle key events for the debug widget
100    pub fn handle_key(&mut self, key: KeyEvent) -> bool {
101        match key.code {
102            // Navigation
103            KeyCode::Up | KeyCode::Char('k') => {
104                self.scroll_up(1);
105                false
106            }
107            KeyCode::Down | KeyCode::Char('j') => {
108                self.scroll_down(1);
109                false
110            }
111            KeyCode::PageUp => {
112                self.scroll_up(10);
113                false
114            }
115            KeyCode::PageDown => {
116                self.scroll_down(10);
117                false
118            }
119            KeyCode::Home | KeyCode::Char('g') => {
120                self.scroll_to_top();
121                false
122            }
123            KeyCode::End | KeyCode::Char('G') => {
124                self.scroll_to_bottom();
125                false
126            }
127
128            // Exit debug mode
129            KeyCode::Esc | KeyCode::Char('q') => true,
130
131            // Ctrl+C to copy debug content to clipboard
132            KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
133                // This would return true to signal the main app to copy content
134                // The main app would handle the actual clipboard operation
135                true
136            }
137
138            _ => false,
139        }
140    }
141
142    /// Render the debug widget
143    pub fn render(&self, f: &mut Frame, area: Rect, mode: AppMode) {
144        let visible_height = area.height.saturating_sub(2) as usize;
145        let visible_lines = self.get_visible_lines(visible_height);
146
147        let debug_text = Text::from(visible_lines);
148        let total_lines = self.content.lines().count();
149        let start = self.scroll_offset as usize;
150        let end = (start + visible_height).min(total_lines);
151
152        // Check if there's a parse error
153        let has_parse_error = self.content.contains("❌ PARSE ERROR ❌");
154        let (border_color, title_prefix) = if has_parse_error {
155            (Color::Red, "⚠️  Parser Debug Info [PARSE ERROR] ")
156        } else {
157            (Color::Yellow, "Parser Debug Info ")
158        };
159
160        let title = match mode {
161            AppMode::Debug => format!(
162                "{}- Lines {}-{} of {} (↑↓/jk: scroll, PgUp/PgDn: page, Home/g: top, End/G: bottom, q/Esc: exit)",
163                title_prefix,
164                start + 1,
165                end,
166                total_lines
167            ),
168            AppMode::PrettyQuery => {
169                "Pretty SQL Query (F6) - ↑↓ to scroll, Esc/q to close".to_string()
170            }
171            _ => "Debug Info".to_string(),
172        };
173
174        let debug_paragraph = Paragraph::new(debug_text)
175            .block(
176                Block::default()
177                    .borders(Borders::ALL)
178                    .title(title)
179                    .border_style(Style::default().fg(border_color)),
180            )
181            .style(Style::default().fg(Color::White))
182            .wrap(Wrap { trim: false });
183
184        f.render_widget(debug_paragraph, area);
185    }
186
187    /// Get the visible lines based on scroll offset
188    pub fn get_visible_lines(&self, height: usize) -> Vec<Line<'static>> {
189        let lines: Vec<&str> = self.content.lines().collect();
190        let start = self.scroll_offset as usize;
191        let end = (start + height).min(lines.len());
192
193        lines[start..end]
194            .iter()
195            .map(|line| Line::from(line.to_string()))
196            .collect()
197    }
198
199    /// Scroll up by the specified amount
200    pub fn scroll_up(&mut self, amount: u16) {
201        self.scroll_offset = self.scroll_offset.saturating_sub(amount);
202    }
203
204    /// Scroll down by the specified amount
205    pub fn scroll_down(&mut self, amount: u16) {
206        self.scroll_offset = (self.scroll_offset + amount).min(self.max_scroll);
207    }
208
209    /// Scroll to the top
210    pub fn scroll_to_top(&mut self) {
211        self.scroll_offset = 0;
212    }
213
214    /// Scroll to the bottom
215    pub fn scroll_to_bottom(&mut self) {
216        self.scroll_offset = self.max_scroll;
217    }
218
219    /// Update the maximum scroll position based on content
220    fn update_max_scroll(&mut self) {
221        let line_count = self.content.lines().count() as u16;
222        self.max_scroll = line_count.saturating_sub(10); // Leave some visible lines
223    }
224
225    /// Get the current content (for clipboard operations)
226    pub fn get_content(&self) -> &str {
227        &self.content
228    }
229
230    /// Set custom content
231    pub fn set_content(&mut self, content: String) {
232        self.content = content;
233        self.scroll_offset = 0;
234        self.update_max_scroll();
235    }
236
237    /// Parse WHERE clause and return AST representation
238    fn parse_where_clause_ast(query: &str) -> Result<String, String> {
239        // Find WHERE clause in the query
240        let lower_query = query.to_lowercase();
241        let where_pos = lower_query.find(" where ");
242
243        if let Some(pos) = where_pos {
244            let where_start = pos + 7; // Skip " where "
245            let where_clause = &query[where_start..];
246
247            // Find the end of WHERE clause (before ORDER BY, GROUP BY, LIMIT, etc.)
248            let end_keywords = ["order by", "group by", "limit", "offset", ";"];
249            let mut where_end = where_clause.len();
250
251            for keyword in &end_keywords {
252                if let Some(keyword_pos) = where_clause.to_lowercase().find(keyword) {
253                    where_end = where_end.min(keyword_pos);
254                }
255            }
256
257            let where_only = where_clause[..where_end].trim();
258
259            match where_parser::WhereParser::parse(where_only) {
260                Ok(ast) => {
261                    let mut result = String::from("\n========== WHERE CLAUSE AST ==========\n");
262                    result.push_str(&format!("Input: {}\n", where_only));
263                    result.push_str(&format!("Parsed AST:\n{:#?}\n", ast));
264                    Ok(result)
265                }
266                Err(e) => Err(format!("Failed to parse WHERE clause: {}", e)),
267            }
268        } else {
269            Err("No WHERE clause found in query".to_string())
270        }
271    }
272}
273
274impl Default for DebugWidget {
275    fn default() -> Self {
276        Self::new()
277    }
278}