sql_cli/utils/
debug_info.rs

1use crate::buffer::{BufferAPI, SortOrder, SortState};
2use crate::hybrid_parser::HybridParser;
3use chrono::Local;
4use serde_json::Value;
5
6/// Handles debug information generation and management
7pub struct DebugInfo;
8
9impl DebugInfo {
10    /// Generate full debug information including parser state, buffer state, etc.
11    pub fn generate_full_debug_simple(
12        buffer: &dyn BufferAPI,
13        buffer_count: usize,
14        buffer_index: usize,
15        buffer_names: Vec<String>,
16        hybrid_parser: &HybridParser,
17        sort_state: &SortState,
18        input_text: &str,
19        cursor_pos: usize,
20        visual_cursor: usize,
21        api_url: &str,
22    ) -> String {
23        let mut debug_info = String::new();
24
25        // Get parser debug info
26        debug_info.push_str(&hybrid_parser.get_detailed_debug_info(input_text, cursor_pos));
27
28        // Add input state information
29        let input_state = format!(
30            "\n========== INPUT STATE ==========\n\
31            Input Value Length: {}\n\
32            Cursor Position: {}\n\
33            Visual Cursor: {}\n\
34            Input Mode: Command\n",
35            input_text.len(),
36            cursor_pos,
37            visual_cursor
38        );
39        debug_info.push_str(&input_state);
40
41        // Add dataset information
42        let dataset_info = if let Some(dataview) = buffer.get_dataview() {
43            let table_name = dataview.source().name.clone();
44            let columns = dataview.column_names();
45            format!(
46                "\n========== DATASET INFO ==========\n\
47                Table Name: {}\n\
48                Visible Columns ({}): {}\n\
49                Hidden Columns: {}\n",
50                table_name,
51                columns.len(),
52                columns.join(", "),
53                dataview.get_hidden_column_names().len()
54            )
55        } else {
56            "\n========== DATASET INFO ==========\nNo DataView available\n".to_string()
57        };
58        debug_info.push_str(&dataset_info);
59
60        // Add current data statistics
61        let data_stats = if let Some(dataview) = buffer.get_dataview() {
62            let total_rows = dataview.source().row_count();
63            let filtered_rows = dataview.row_count();
64            format!(
65                "\n========== CURRENT DATA ==========\n\
66                Total Rows Loaded: {}\n\
67                Filtered Rows: {}\n\
68                Has Filter: {}\n\
69                Current Column: {}\n\
70                Sort State: {}\n",
71                total_rows,
72                filtered_rows,
73                dataview.has_filter(),
74                buffer.get_current_column(),
75                match sort_state {
76                    SortState {
77                        column: Some(col),
78                        order,
79                    } => format!(
80                        "Column {} - {}",
81                        col,
82                        match order {
83                            SortOrder::Ascending => "Ascending",
84                            SortOrder::Descending => "Descending",
85                            SortOrder::None => "None",
86                        }
87                    ),
88                    _ => "None".to_string(),
89                }
90            )
91        } else {
92            "\n========== CURRENT DATA ==========\nNo DataView available\n".to_string()
93        };
94        debug_info.push_str(&data_stats);
95
96        // Add status line info
97        let status_line_info = format!(
98            "\n========== STATUS LINE INFO ==========\n\
99            Current Mode: {:?}\n\
100            Case Insensitive: {}\n\
101            Compact Mode: {}\n\
102            Viewport Lock: {}\n\
103            Data Source: {}\n",
104            buffer.get_mode(),
105            buffer.is_case_insensitive(),
106            buffer.is_compact_mode(),
107            buffer.is_viewport_lock(),
108            buffer.get_last_query_source().unwrap_or("None".to_string()),
109        );
110        debug_info.push_str(&status_line_info);
111
112        // Add buffer manager debug info
113        debug_info.push_str("\n========== BUFFER MANAGER STATE ==========\n");
114        debug_info.push_str("Buffer Manager: INITIALIZED\n");
115        debug_info.push_str(&format!("Number of Buffers: {buffer_count}\n"));
116        debug_info.push_str(&format!("Current Buffer Index: {buffer_index}\n"));
117        debug_info.push_str(&format!("Has Multiple Buffers: {}\n", buffer_count > 1));
118
119        // Add info about all buffers
120        for (i, name) in buffer_names.iter().enumerate() {
121            let is_current = i == buffer_index;
122            debug_info.push_str(&format!(
123                "Buffer {}: {} {}\n",
124                i + 1,
125                name,
126                if is_current { "[CURRENT]" } else { "" }
127            ));
128        }
129
130        debug_info
131    }
132
133    /// Generate complete debug context for current state
134    pub fn generate_debug_context(buffer: &dyn BufferAPI) -> String {
135        let mut context = String::new();
136        let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
137
138        context.push_str(&format!("=== TUI Debug Context - {timestamp} ===\n\n"));
139
140        // Current query info
141        context.push_str("CURRENT QUERY:\n");
142        let query = buffer.get_query();
143        let last_query = buffer.get_last_query();
144        let current_query = if query.is_empty() {
145            &last_query
146        } else {
147            &query
148        };
149        context.push_str(&format!("{current_query}\n\n"));
150
151        // Buffer state
152        context.push_str("BUFFER STATE:\n");
153        context.push_str(&format!("- ID: {}\n", buffer.get_id()));
154        context.push_str(&format!(
155            "- File: {}\n",
156            buffer
157                .get_file_path()
158                .map_or_else(|| "memory".to_string(), |p| p.to_string_lossy().to_string())
159        ));
160        context.push_str(&format!("- Mode: {:?}\n", buffer.get_mode()));
161        context.push_str(&format!(
162            "- Case Insensitive: {}\n",
163            buffer.is_case_insensitive()
164        ));
165
166        // Results info
167        if let Some(datatable) = buffer.get_datatable() {
168            context.push_str("\nRESULTS INFO:\n");
169            context.push_str(&format!("- Total rows: {}\n", datatable.row_count()));
170            context.push_str(&format!("- Columns: {}\n", datatable.column_count()));
171            context.push_str(&format!(
172                "- Column names: {}\n",
173                datatable.column_names().join(", ")
174            ));
175
176            // Filter info
177            if buffer.is_filter_active() {
178                context.push_str("\nFILTER:\n");
179                context.push_str(&format!("- Pattern: {}\n", buffer.get_filter_pattern()));
180                if let Some(dataview) = buffer.get_dataview() {
181                    context.push_str(&format!("- Filtered rows: {}\n", dataview.row_count()));
182                }
183            }
184
185            if buffer.is_fuzzy_filter_active() {
186                context.push_str("\nFUZZY FILTER:\n");
187                context.push_str(&format!(
188                    "- Pattern: {}\n",
189                    buffer.get_fuzzy_filter_pattern()
190                ));
191                let indices = buffer.get_fuzzy_filter_indices();
192                context.push_str(&format!("- Matched rows: {}\n", indices.len()));
193            }
194        }
195
196        // Navigation state
197        context.push_str("\nNAVIGATION:\n");
198        context.push_str(&format!("- Current row: {:?}\n", buffer.get_selected_row()));
199        context.push_str(&format!(
200            "- Current column: {}\n",
201            buffer.get_current_column()
202        ));
203        context.push_str(&format!(
204            "- Scroll offset: ({}, {})\n",
205            buffer.get_scroll_offset().0,
206            buffer.get_scroll_offset().1
207        ));
208
209        context
210    }
211
212    /// Generate a complete test case string that can be pasted into a test file
213    pub fn generate_test_case(buffer: &dyn BufferAPI) -> String {
214        let query = buffer.get_query();
215        let last_query = buffer.get_last_query();
216        let current_query = if query.is_empty() {
217            &last_query
218        } else {
219            &query
220        };
221
222        let mut test_case = String::new();
223        let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
224
225        // Header comment with session info
226        test_case.push_str(&format!(
227            "// Test case generated from TUI session at {timestamp}\n"
228        ));
229        test_case.push_str(&format!(
230            "// Buffer: {} (ID: {})\n",
231            buffer
232                .get_file_path()
233                .map_or_else(|| "memory".to_string(), |p| p.to_string_lossy().to_string()),
234            buffer.get_id()
235        ));
236
237        if let Some(datatable) = buffer.get_datatable() {
238            test_case.push_str(&format!(
239                "// Results: {} rows, {} columns\n",
240                datatable.row_count(),
241                datatable.column_count()
242            ));
243        }
244
245        test_case.push_str("\n#[test]\n");
246        test_case.push_str("fn test_yanked_from_tui_session() -> anyhow::Result<()> {\n");
247        test_case.push_str("    let mut harness = QueryReplayHarness::new();\n\n");
248
249        test_case.push_str("    harness.add_query(CapturedQuery {\n");
250        test_case.push_str(&format!(
251            "        description: \"Captured from TUI session {timestamp}\".to_string(),\n"
252        ));
253
254        // Add data file path
255        if let Some(file_path) = buffer.get_file_path() {
256            test_case.push_str(&format!(
257                "        data_file: \"{}\".to_string(),\n",
258                file_path.to_string_lossy()
259            ));
260        } else {
261            test_case.push_str("        data_file: \"data/trades.json\".to_string(),\n");
262        }
263
264        // Add query
265        test_case.push_str(&format!(
266            "        query: \"{}\".to_string(),\n",
267            current_query.replace('"', "\\\"")
268        ));
269
270        // Add expected results
271        if let Some(datatable) = buffer.get_datatable() {
272            test_case.push_str(&format!(
273                "        expected_row_count: {},\n",
274                datatable.row_count()
275            ));
276
277            // Add column names
278            test_case.push_str("        expected_columns: vec![\n");
279            for column_name in datatable.column_names() {
280                test_case.push_str(&format!("            \"{column_name}\".to_string(), \n"));
281            }
282            test_case.push_str("        ],\n");
283        } else {
284            test_case.push_str("        expected_row_count: 0,\n");
285            test_case.push_str("        expected_columns: vec![],\n");
286            test_case.push_str("        expected_first_row: None,\n");
287        }
288
289        test_case.push_str(&format!(
290            "        case_insensitive: {},\n",
291            buffer.is_case_insensitive()
292        ));
293        test_case.push_str("    });\n\n");
294
295        test_case.push_str("    // Run the test\n");
296        test_case.push_str("    harness.run_all_tests()?;\n\n");
297        test_case.push_str("    println!(\"✅ Yanked query test passed!\");\n");
298        test_case.push_str("    Ok(())\n");
299        test_case.push_str("}\n");
300
301        test_case
302    }
303
304    /// Convert a `serde_json::Value` to Rust code representation
305    fn value_to_rust_code(value: &Value) -> String {
306        match value {
307            Value::String(s) => format!(
308                "serde_json::Value::String(\"{}\".to_string())",
309                s.replace('"', "\\\"")
310            ),
311            Value::Number(n) => {
312                if let Some(i) = n.as_i64() {
313                    format!("serde_json::Value::Number(serde_json::Number::from({i}))")
314                } else if let Some(f) = n.as_f64() {
315                    format!("serde_json::Value::Number(serde_json::Number::from_f64({f}).unwrap())")
316                } else {
317                    format!(
318                        "serde_json::Value::Number(serde_json::Number::from_str(\"{n}\").unwrap())"
319                    )
320                }
321            }
322            Value::Bool(b) => format!("serde_json::Value::Bool({b})"),
323            Value::Null => "serde_json::Value::Null".to_string(),
324            _ => format!("serde_json::json!({value})"),
325        }
326    }
327
328    /// Generate buffer state summary for status messages
329    pub fn generate_buffer_summary(buffer: &dyn BufferAPI) -> String {
330        let mut summary = Vec::new();
331
332        summary.push(format!("Buffer #{}", buffer.get_id()));
333
334        if let Some(path) = buffer.get_file_path() {
335            summary.push(format!(
336                "File: {}",
337                path.file_name().unwrap_or_default().to_string_lossy()
338            ));
339        }
340
341        if let Some(datatable) = buffer.get_datatable() {
342            summary.push(format!("{} rows", datatable.row_count()));
343
344            if buffer.is_filter_active() {
345                if let Some(dataview) = buffer.get_dataview() {
346                    summary.push(format!("{} filtered", dataview.row_count()));
347                }
348            }
349
350            if buffer.is_fuzzy_filter_active() {
351                let indices = buffer.get_fuzzy_filter_indices();
352                summary.push(format!("{} fuzzy matches", indices.len()));
353            }
354        }
355
356        summary.join(" | ")
357    }
358
359    /// Generate query execution debug info
360    #[must_use]
361    pub fn generate_query_debug(query: &str, error: Option<&str>) -> String {
362        let mut debug = String::new();
363        let timestamp = Local::now().format("%H:%M:%S%.3f");
364
365        debug.push_str(&format!("[{timestamp}] Query execution:\n"));
366        debug.push_str(&format!("Query: {query}\n"));
367
368        if let Some(err) = error {
369            debug.push_str(&format!("Error: {err}\n"));
370        } else {
371            debug.push_str("Status: Success\n");
372        }
373
374        debug
375    }
376}
377
378/// Manages debug view scrolling and navigation
379pub struct DebugView {
380    pub content: String,
381    pub scroll_offset: u16,
382}
383
384impl DebugView {
385    #[must_use]
386    pub fn new() -> Self {
387        Self {
388            content: String::new(),
389            scroll_offset: 0,
390        }
391    }
392
393    pub fn set_content(&mut self, content: String) {
394        self.content = content;
395        self.scroll_offset = 0; // Reset scroll when content changes
396    }
397
398    pub fn scroll_up(&mut self) {
399        self.scroll_offset = self.scroll_offset.saturating_sub(1);
400    }
401
402    pub fn scroll_down(&mut self) {
403        let max_scroll = self.get_max_scroll();
404        if (self.scroll_offset as usize) < max_scroll {
405            self.scroll_offset = self.scroll_offset.saturating_add(1);
406        }
407    }
408
409    pub fn page_up(&mut self) {
410        self.scroll_offset = self.scroll_offset.saturating_sub(10);
411    }
412
413    pub fn page_down(&mut self) {
414        let max_scroll = self.get_max_scroll();
415        self.scroll_offset = (self.scroll_offset + 10).min(max_scroll as u16);
416    }
417
418    pub fn go_to_top(&mut self) {
419        self.scroll_offset = 0;
420    }
421
422    pub fn go_to_bottom(&mut self) {
423        self.scroll_offset = self.get_max_scroll() as u16;
424    }
425
426    #[must_use]
427    pub fn get_max_scroll(&self) -> usize {
428        let line_count = self.content.lines().count();
429        line_count.saturating_sub(10) // Assuming 10 visible lines
430    }
431
432    #[must_use]
433    pub fn get_visible_lines(&self, height: usize) -> Vec<String> {
434        self.content
435            .lines()
436            .skip(self.scroll_offset as usize)
437            .take(height)
438            .map(std::string::ToString::to_string)
439            .collect()
440    }
441}
442
443impl Default for DebugView {
444    fn default() -> Self {
445        Self::new()
446    }
447}