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(&format!("Buffer Manager: INITIALIZED\n"));
115        debug_info.push_str(&format!("Number of Buffers: {}\n", buffer_count));
116        debug_info.push_str(&format!("Current Buffer Index: {}\n", buffer_index));
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 - {} ===\n\n", timestamp));
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            &query
146        } else {
147            &last_query
148        };
149        context.push_str(&format!("{}\n\n", current_query));
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(|p| p.to_string_lossy().to_string())
159                .unwrap_or_else(|| "memory".to_string())
160        ));
161        context.push_str(&format!("- Mode: {:?}\n", buffer.get_mode()));
162        context.push_str(&format!(
163            "- Case Insensitive: {}\n",
164            buffer.is_case_insensitive()
165        ));
166
167        // Results info
168        if let Some(datatable) = buffer.get_datatable() {
169            context.push_str(&format!("\nRESULTS INFO:\n"));
170            context.push_str(&format!("- Total rows: {}\n", datatable.row_count()));
171            context.push_str(&format!("- Columns: {}\n", datatable.column_count()));
172            context.push_str(&format!(
173                "- Column names: {}\n",
174                datatable.column_names().join(", ")
175            ));
176
177            // Filter info
178            if buffer.is_filter_active() {
179                context.push_str(&format!("\nFILTER:\n"));
180                context.push_str(&format!("- Pattern: {}\n", buffer.get_filter_pattern()));
181                if let Some(dataview) = buffer.get_dataview() {
182                    context.push_str(&format!("- Filtered rows: {}\n", dataview.row_count()));
183                }
184            }
185
186            if buffer.is_fuzzy_filter_active() {
187                context.push_str(&format!("\nFUZZY FILTER:\n"));
188                context.push_str(&format!(
189                    "- Pattern: {}\n",
190                    buffer.get_fuzzy_filter_pattern()
191                ));
192                let indices = buffer.get_fuzzy_filter_indices();
193                context.push_str(&format!("- Matched rows: {}\n", indices.len()));
194            }
195        }
196
197        // Navigation state
198        context.push_str("\nNAVIGATION:\n");
199        context.push_str(&format!("- Current row: {:?}\n", buffer.get_selected_row()));
200        context.push_str(&format!(
201            "- Current column: {}\n",
202            buffer.get_current_column()
203        ));
204        context.push_str(&format!(
205            "- Scroll offset: ({}, {})\n",
206            buffer.get_scroll_offset().0,
207            buffer.get_scroll_offset().1
208        ));
209
210        context
211    }
212
213    /// Generate a complete test case string that can be pasted into a test file
214    pub fn generate_test_case(buffer: &dyn BufferAPI) -> String {
215        let query = buffer.get_query();
216        let last_query = buffer.get_last_query();
217        let current_query = if !query.is_empty() {
218            &query
219        } else {
220            &last_query
221        };
222
223        let mut test_case = String::new();
224        let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
225
226        // Header comment with session info
227        test_case.push_str(&format!(
228            "// Test case generated from TUI session at {}\n",
229            timestamp
230        ));
231        test_case.push_str(&format!(
232            "// Buffer: {} (ID: {})\n",
233            buffer
234                .get_file_path()
235                .map(|p| p.to_string_lossy().to_string())
236                .unwrap_or_else(|| "memory".to_string()),
237            buffer.get_id()
238        ));
239
240        if let Some(datatable) = buffer.get_datatable() {
241            test_case.push_str(&format!(
242                "// Results: {} rows, {} columns\n",
243                datatable.row_count(),
244                datatable.column_count()
245            ));
246        }
247
248        test_case.push_str("\n#[test]\n");
249        test_case.push_str("fn test_yanked_from_tui_session() -> anyhow::Result<()> {\n");
250        test_case.push_str("    let mut harness = QueryReplayHarness::new();\n\n");
251
252        test_case.push_str("    harness.add_query(CapturedQuery {\n");
253        test_case.push_str(&format!(
254            "        description: \"Captured from TUI session {}\".to_string(),\n",
255            timestamp
256        ));
257
258        // Add data file path
259        if let Some(file_path) = buffer.get_file_path() {
260            test_case.push_str(&format!(
261                "        data_file: \"{}\".to_string(),\n",
262                file_path.to_string_lossy()
263            ));
264        } else {
265            test_case.push_str("        data_file: \"data/trades.json\".to_string(),\n");
266        }
267
268        // Add query
269        test_case.push_str(&format!(
270            "        query: \"{}\".to_string(),\n",
271            current_query.replace('"', "\\\"")
272        ));
273
274        // Add expected results
275        if let Some(datatable) = buffer.get_datatable() {
276            test_case.push_str(&format!(
277                "        expected_row_count: {},\n",
278                datatable.row_count()
279            ));
280
281            // Add column names
282            test_case.push_str("        expected_columns: vec![\n");
283            for column_name in datatable.column_names() {
284                test_case.push_str(&format!("            \"{}\".to_string(), \n", column_name));
285            }
286            test_case.push_str("        ],\n");
287        } else {
288            test_case.push_str("        expected_row_count: 0,\n");
289            test_case.push_str("        expected_columns: vec![],\n");
290            test_case.push_str("        expected_first_row: None,\n");
291        }
292
293        test_case.push_str(&format!(
294            "        case_insensitive: {},\n",
295            buffer.is_case_insensitive()
296        ));
297        test_case.push_str("    });\n\n");
298
299        test_case.push_str("    // Run the test\n");
300        test_case.push_str("    harness.run_all_tests()?;\n\n");
301        test_case.push_str("    println!(\"✅ Yanked query test passed!\");\n");
302        test_case.push_str("    Ok(())\n");
303        test_case.push_str("}\n");
304
305        test_case
306    }
307
308    /// Convert a serde_json::Value to Rust code representation
309    fn value_to_rust_code(value: &Value) -> String {
310        match value {
311            Value::String(s) => format!(
312                "serde_json::Value::String(\"{}\".to_string())",
313                s.replace('"', "\\\"")
314            ),
315            Value::Number(n) => {
316                if let Some(i) = n.as_i64() {
317                    format!("serde_json::Value::Number(serde_json::Number::from({}))", i)
318                } else if let Some(f) = n.as_f64() {
319                    format!(
320                        "serde_json::Value::Number(serde_json::Number::from_f64({}).unwrap())",
321                        f
322                    )
323                } else {
324                    format!(
325                        "serde_json::Value::Number(serde_json::Number::from_str(\"{}\").unwrap())",
326                        n
327                    )
328                }
329            }
330            Value::Bool(b) => format!("serde_json::Value::Bool({})", b),
331            Value::Null => "serde_json::Value::Null".to_string(),
332            _ => format!("serde_json::json!({})", value),
333        }
334    }
335
336    /// Generate buffer state summary for status messages
337    pub fn generate_buffer_summary(buffer: &dyn BufferAPI) -> String {
338        let mut summary = Vec::new();
339
340        summary.push(format!("Buffer #{}", buffer.get_id()));
341
342        if let Some(path) = buffer.get_file_path() {
343            summary.push(format!(
344                "File: {}",
345                path.file_name().unwrap_or_default().to_string_lossy()
346            ));
347        }
348
349        if let Some(datatable) = buffer.get_datatable() {
350            summary.push(format!("{} rows", datatable.row_count()));
351
352            if buffer.is_filter_active() {
353                if let Some(dataview) = buffer.get_dataview() {
354                    summary.push(format!("{} filtered", dataview.row_count()));
355                }
356            }
357
358            if buffer.is_fuzzy_filter_active() {
359                let indices = buffer.get_fuzzy_filter_indices();
360                summary.push(format!("{} fuzzy matches", indices.len()));
361            }
362        }
363
364        summary.join(" | ")
365    }
366
367    /// Generate query execution debug info
368    pub fn generate_query_debug(query: &str, error: Option<&str>) -> String {
369        let mut debug = String::new();
370        let timestamp = Local::now().format("%H:%M:%S%.3f");
371
372        debug.push_str(&format!("[{}] Query execution:\n", timestamp));
373        debug.push_str(&format!("Query: {}\n", query));
374
375        if let Some(err) = error {
376            debug.push_str(&format!("Error: {}\n", err));
377        } else {
378            debug.push_str("Status: Success\n");
379        }
380
381        debug
382    }
383}
384
385/// Manages debug view scrolling and navigation
386pub struct DebugView {
387    pub content: String,
388    pub scroll_offset: u16,
389}
390
391impl DebugView {
392    pub fn new() -> Self {
393        Self {
394            content: String::new(),
395            scroll_offset: 0,
396        }
397    }
398
399    pub fn set_content(&mut self, content: String) {
400        self.content = content;
401        self.scroll_offset = 0; // Reset scroll when content changes
402    }
403
404    pub fn scroll_up(&mut self) {
405        self.scroll_offset = self.scroll_offset.saturating_sub(1);
406    }
407
408    pub fn scroll_down(&mut self) {
409        let max_scroll = self.get_max_scroll();
410        if (self.scroll_offset as usize) < max_scroll {
411            self.scroll_offset = self.scroll_offset.saturating_add(1);
412        }
413    }
414
415    pub fn page_up(&mut self) {
416        self.scroll_offset = self.scroll_offset.saturating_sub(10);
417    }
418
419    pub fn page_down(&mut self) {
420        let max_scroll = self.get_max_scroll();
421        self.scroll_offset = (self.scroll_offset + 10).min(max_scroll as u16);
422    }
423
424    pub fn go_to_top(&mut self) {
425        self.scroll_offset = 0;
426    }
427
428    pub fn go_to_bottom(&mut self) {
429        self.scroll_offset = self.get_max_scroll() as u16;
430    }
431
432    pub fn get_max_scroll(&self) -> usize {
433        let line_count = self.content.lines().count();
434        line_count.saturating_sub(10) // Assuming 10 visible lines
435    }
436
437    pub fn get_visible_lines(&self, height: usize) -> Vec<String> {
438        self.content
439            .lines()
440            .skip(self.scroll_offset as usize)
441            .take(height)
442            .map(|s| s.to_string())
443            .collect()
444    }
445}
446
447impl Default for DebugView {
448    fn default() -> Self {
449        Self::new()
450    }
451}