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    #[must_use]
26    pub fn new() -> Self {
27        Self {
28            content: String::new(),
29            scroll_offset: 0,
30            max_scroll: 0,
31        }
32    }
33
34    /// Generate and set debug content
35    pub fn generate_debug(
36        &mut self,
37        buffer: &dyn BufferAPI,
38        buffer_count: usize,
39        buffer_index: usize,
40        buffer_names: Vec<String>,
41        hybrid_parser: &HybridParser,
42        sort_state: &SortState,
43        input_text: &str,
44        cursor_pos: usize,
45        visual_cursor: usize,
46        api_url: &str,
47    ) {
48        // Generate full debug info
49        let mut debug_info = DebugInfo::generate_full_debug_simple(
50            buffer,
51            buffer_count,
52            buffer_index,
53            buffer_names,
54            hybrid_parser,
55            sort_state,
56            input_text,
57            cursor_pos,
58            visual_cursor,
59            api_url,
60        );
61
62        // Add WHERE clause AST if query contains WHERE
63        if input_text.to_lowercase().contains(" where ") {
64            let where_ast_info = match Self::parse_where_clause_ast(input_text) {
65                Ok(ast_str) => ast_str,
66                Err(e) => format!(
67                    "\n========== WHERE CLAUSE AST ==========\nError parsing WHERE clause: {e}\n"
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    #[must_use]
189    pub fn get_visible_lines(&self, height: usize) -> Vec<Line<'static>> {
190        let lines: Vec<&str> = self.content.lines().collect();
191        let start = self.scroll_offset as usize;
192        let end = (start + height).min(lines.len());
193
194        lines[start..end]
195            .iter()
196            .map(|line| Line::from((*line).to_string()))
197            .collect()
198    }
199
200    /// Scroll up by the specified amount
201    pub fn scroll_up(&mut self, amount: u16) {
202        self.scroll_offset = self.scroll_offset.saturating_sub(amount);
203    }
204
205    /// Scroll down by the specified amount
206    pub fn scroll_down(&mut self, amount: u16) {
207        self.scroll_offset = (self.scroll_offset + amount).min(self.max_scroll);
208    }
209
210    /// Scroll to the top
211    pub fn scroll_to_top(&mut self) {
212        self.scroll_offset = 0;
213    }
214
215    /// Scroll to the bottom
216    pub fn scroll_to_bottom(&mut self) {
217        self.scroll_offset = self.max_scroll;
218    }
219
220    /// Update the maximum scroll position based on content
221    fn update_max_scroll(&mut self) {
222        let line_count = self.content.lines().count() as u16;
223        self.max_scroll = line_count.saturating_sub(10); // Leave some visible lines
224    }
225
226    /// Get the current content (for clipboard operations)
227    #[must_use]
228    pub fn get_content(&self) -> &str {
229        &self.content
230    }
231
232    /// Set custom content
233    pub fn set_content(&mut self, content: String) {
234        self.content = content;
235        self.scroll_offset = 0;
236        self.update_max_scroll();
237    }
238
239    /// Parse WHERE clause and return AST representation
240    fn parse_where_clause_ast(query: &str) -> Result<String, String> {
241        // Find WHERE clause in the query
242        let lower_query = query.to_lowercase();
243        let where_pos = lower_query.find(" where ");
244
245        if let Some(pos) = where_pos {
246            let where_start = pos + 7; // Skip " where "
247            let where_clause = &query[where_start..];
248
249            // Find the end of WHERE clause (before ORDER BY, GROUP BY, LIMIT, etc.)
250            let end_keywords = ["order by", "group by", "limit", "offset", ";"];
251            let mut where_end = where_clause.len();
252
253            for keyword in &end_keywords {
254                if let Some(keyword_pos) = where_clause.to_lowercase().find(keyword) {
255                    where_end = where_end.min(keyword_pos);
256                }
257            }
258
259            let where_only = where_clause[..where_end].trim();
260
261            match where_parser::WhereParser::parse(where_only) {
262                Ok(ast) => {
263                    let mut result = String::from("\n========== WHERE CLAUSE AST ==========\n");
264                    result.push_str(&format!("Input: {where_only}\n"));
265                    result.push_str(&format!("Parsed AST:\n{ast:#?}\n"));
266                    Ok(result)
267                }
268                Err(e) => Err(format!("Failed to parse WHERE clause: {e}")),
269            }
270        } else {
271            Err("No WHERE clause found in query".to_string())
272        }
273    }
274}
275
276impl Default for DebugWidget {
277    fn default() -> Self {
278        Self::new()
279    }
280}