1use crate::buffer::{BufferAPI, SortOrder, SortState};
2use crate::hybrid_parser::HybridParser;
3use chrono::Local;
4use serde_json::Value;
5
6pub struct DebugInfo;
8
9impl DebugInfo {
10 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 debug_info.push_str(&hybrid_parser.get_detailed_debug_info(input_text, cursor_pos));
27
28 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 test_case.push_str(&format!(
266 " query: \"{}\".to_string(),\n",
267 current_query.replace('"', "\\\"")
268 ));
269
270 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 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 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 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 #[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
378pub 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; }
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) }
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}