sql_cli/ui/
viewport_manager.rs

1/// ViewportManager - A window into the DataView
2///
3/// This manages the visible portion of data for rendering, handling:
4/// - Column width calculations based on visible data
5/// - Row/column windowing for virtual scrolling
6/// - Caching of expensive calculations
7/// - Rendering optimizations
8///
9/// Architecture:
10/// DataTable (immutable storage)
11///     → DataView (filtered/sorted/projected data)
12///         → ViewportManager (visible window)
13///             → Renderer (pixels on screen)
14use std::ops::Range;
15use std::sync::Arc;
16use tracing::debug;
17
18use crate::data::data_view::DataView;
19use crate::data::datatable::DataRow;
20use crate::ui::viewport::column_width_calculator::{
21    COLUMN_PADDING, DEFAULT_COL_WIDTH, MAX_COL_WIDTH, MAX_COL_WIDTH_DATA_FOCUS, MIN_COL_WIDTH,
22};
23use crate::ui::viewport::{ColumnPackingMode, ColumnWidthCalculator};
24
25/// Result of a navigation operation
26#[derive(Debug, Clone)]
27pub struct NavigationResult {
28    /// The new column position
29    pub column_position: usize,
30    /// The new scroll offset
31    pub scroll_offset: usize,
32    /// Human-readable description of the operation
33    pub description: String,
34    /// Whether the operation changed the viewport
35    pub viewport_changed: bool,
36}
37
38/// Result of a row navigation operation (Page Up/Down, etc.)
39#[derive(Debug, Clone)]
40pub struct RowNavigationResult {
41    /// The new row position
42    pub row_position: usize,
43    /// The new viewport scroll offset for rows
44    pub row_scroll_offset: usize,
45    /// Human-readable description of the operation
46    pub description: String,
47    /// Whether the operation changed the viewport
48    pub viewport_changed: bool,
49}
50
51/// Result of a column reordering operation
52#[derive(Debug, Clone)]
53pub struct ColumnReorderResult {
54    /// The new column position after reordering
55    pub new_column_position: usize,
56    /// Human-readable description of the operation
57    pub description: String,
58    /// Whether the reordering was successful
59    pub success: bool,
60}
61
62/// Unified result for all column operations
63#[derive(Debug, Clone)]
64pub struct ColumnOperationResult {
65    /// Whether the operation was successful
66    pub success: bool,
67    /// Human-readable description for status message
68    pub description: String,
69    /// Updated DataView after the operation (if changed)
70    pub updated_dataview: Option<DataView>,
71    /// New column position (for move/navigation operations)
72    pub new_column_position: Option<usize>,
73    /// New viewport range (if changed)
74    pub new_viewport: Option<std::ops::Range<usize>>,
75    /// Number of columns affected (for hide/unhide operations)
76    pub affected_count: Option<usize>,
77}
78
79impl ColumnOperationResult {
80    /// Create a failure result with a description
81    pub fn failure(description: impl Into<String>) -> Self {
82        Self {
83            success: false,
84            description: description.into(),
85            updated_dataview: None,
86            new_column_position: None,
87            new_viewport: None,
88            affected_count: None,
89        }
90    }
91
92    /// Create a success result with a description
93    pub fn success(description: impl Into<String>) -> Self {
94        Self {
95            success: true,
96            description: description.into(),
97            updated_dataview: None,
98            new_column_position: None,
99            new_viewport: None,
100            affected_count: None,
101        }
102    }
103}
104
105/// Column packing mode for optimizing data display
106/// Default column width if no data (used as fallback)
107
108/// Number of rows used by the table widget chrome (header + borders)
109/// This includes:
110/// - 1 row for the header
111/// - 1 row for the top border  
112/// - 1 row for the bottom border
113const TABLE_CHROME_ROWS: usize = 3;
114
115/// Number of columns used by table borders (left + right + padding)
116const TABLE_BORDER_WIDTH: u16 = 4;
117
118/// Manages the visible viewport into a DataView
119pub struct ViewportManager {
120    /// The underlying data view
121    dataview: Arc<DataView>,
122
123    /// Current viewport bounds
124    viewport_rows: Range<usize>,
125    viewport_cols: Range<usize>,
126
127    /// Terminal dimensions
128    terminal_width: u16,
129    terminal_height: u16,
130
131    /// Column width calculator (extracted subsystem)
132    width_calculator: ColumnWidthCalculator,
133
134    /// Cache of visible row indices (for efficient scrolling)
135    visible_row_cache: Vec<usize>,
136
137    /// Hash of current state for cache invalidation
138    cache_signature: u64,
139
140    /// Whether cache needs recalculation
141    cache_dirty: bool,
142
143    /// Crosshair position in visual coordinates (row, col)
144    /// This is the single source of truth for crosshair position
145    crosshair_row: usize,
146    crosshair_col: usize,
147
148    /// Cursor lock state - when true, crosshair stays at same viewport position while scrolling
149    cursor_lock: bool,
150    /// The relative position of crosshair within viewport when locked (0 = top, viewport_height-1 = bottom)
151    cursor_lock_position: Option<usize>,
152
153    /// Viewport lock state - when true, prevents scrolling and constrains cursor to current viewport
154    viewport_lock: bool,
155    /// The viewport boundaries when locked (prevents scrolling beyond these)
156    viewport_lock_boundaries: Option<std::ops::Range<usize>>,
157}
158
159impl ViewportManager {
160    /// Get the current viewport column range
161    pub fn get_viewport_range(&self) -> std::ops::Range<usize> {
162        self.viewport_cols.clone()
163    }
164
165    /// Get the current viewport row range
166    pub fn get_viewport_rows(&self) -> std::ops::Range<usize> {
167        self.viewport_rows.clone()
168    }
169
170    /// Set crosshair position in visual coordinates
171    pub fn set_crosshair(&mut self, row: usize, col: usize) {
172        self.crosshair_row = row;
173        self.crosshair_col = col;
174        debug!(target: "viewport_manager", 
175               "Crosshair set to visual position: row={}, col={}", row, col);
176    }
177
178    /// Set crosshair row position in visual coordinates with automatic viewport adjustment
179    pub fn set_crosshair_row(&mut self, row: usize) {
180        let total_rows = self.dataview.row_count();
181
182        // Clamp row to valid range
183        let clamped_row = row.min(total_rows.saturating_sub(1));
184        self.crosshair_row = clamped_row;
185
186        // Don't adjust viewport if viewport is locked
187        if self.viewport_lock {
188            debug!(target: "viewport_manager", 
189                   "Crosshair row set to: {} (viewport locked, no scroll adjustment)", 
190                   clamped_row);
191            return;
192        }
193
194        // Adjust viewport if crosshair is outside current viewport
195        let viewport_height = self.viewport_rows.len();
196        let mut viewport_changed = false;
197
198        if clamped_row < self.viewport_rows.start {
199            // Crosshair is above current viewport - scroll up
200            self.viewport_rows = clamped_row..(clamped_row + viewport_height).min(total_rows);
201            viewport_changed = true;
202        } else if clamped_row >= self.viewport_rows.end {
203            // Crosshair is below current viewport - scroll down
204            let new_start = clamped_row.saturating_sub(viewport_height.saturating_sub(1));
205            self.viewport_rows = new_start..(new_start + viewport_height).min(total_rows);
206            viewport_changed = true;
207        }
208
209        if viewport_changed {
210            debug!(target: "viewport_manager", 
211                   "Crosshair row set to: {}, adjusted viewport to: {:?}", 
212                   clamped_row, self.viewport_rows);
213        } else {
214            debug!(target: "viewport_manager", 
215                   "Crosshair row set to: {}", clamped_row);
216        }
217    }
218
219    /// Set crosshair column position in visual coordinates with automatic viewport adjustment  
220    pub fn set_crosshair_column(&mut self, col: usize) {
221        let total_columns = self.dataview.get_display_columns().len();
222
223        // Clamp column to valid range
224        let clamped_col = col.min(total_columns.saturating_sub(1));
225        self.crosshair_col = clamped_col;
226
227        // Don't adjust viewport if viewport is locked
228        if self.viewport_lock {
229            debug!(target: "viewport_manager", 
230                   "Crosshair column set to: {} (viewport locked, no scroll adjustment)", 
231                   clamped_col);
232            return;
233        }
234
235        // Use the existing smart column adjustment logic
236        let terminal_width = self.terminal_width.saturating_sub(4); // Account for borders
237        if self.set_current_column(clamped_col) {
238            debug!(target: "viewport_manager", 
239                   "Crosshair column set to: {} with viewport adjustment", clamped_col);
240        } else {
241            debug!(target: "viewport_manager", 
242                   "Crosshair column set to: {}", clamped_col);
243        }
244    }
245
246    /// Get crosshair column position in visual coordinates
247    pub fn get_crosshair_col(&self) -> usize {
248        self.crosshair_col
249    }
250
251    /// Get crosshair row position in visual coordinates  
252    pub fn get_crosshair_row(&self) -> usize {
253        self.crosshair_row
254    }
255
256    /// Get selected row (alias for crosshair_row for compatibility)
257    pub fn get_selected_row(&self) -> usize {
258        self.crosshair_row
259    }
260
261    /// Get selected column (alias for crosshair_col for compatibility)
262    pub fn get_selected_column(&self) -> usize {
263        self.crosshair_col
264    }
265
266    /// Get crosshair position as (row, column) tuple in visual coordinates
267    pub fn get_crosshair_position(&self) -> (usize, usize) {
268        (self.crosshair_row, self.crosshair_col)
269    }
270
271    /// Get scroll offset as (row_offset, col_offset)
272    pub fn get_scroll_offset(&self) -> (usize, usize) {
273        (self.viewport_rows.start, self.viewport_cols.start)
274    }
275
276    /// Set scroll offset and update viewport accordingly
277    pub fn set_scroll_offset(&mut self, row_offset: usize, col_offset: usize) {
278        let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
279        let viewport_width = self.viewport_cols.end - self.viewport_cols.start;
280
281        // Update viewport ranges based on new scroll offset
282        self.viewport_rows = row_offset..(row_offset + viewport_height);
283        self.viewport_cols = col_offset..(col_offset + viewport_width);
284
285        // Ensure crosshair stays within new viewport
286        if self.crosshair_row < self.viewport_rows.start {
287            self.crosshair_row = self.viewport_rows.start;
288        } else if self.crosshair_row >= self.viewport_rows.end {
289            self.crosshair_row = self.viewport_rows.end.saturating_sub(1);
290        }
291
292        if self.crosshair_col < self.viewport_cols.start {
293            self.crosshair_col = self.viewport_cols.start;
294        } else if self.crosshair_col >= self.viewport_cols.end {
295            self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
296        }
297
298        self.cache_dirty = true;
299    }
300
301    /// Get crosshair position relative to current viewport for rendering
302    /// Returns (row_offset, col_offset) within the viewport, or None if outside
303    pub fn get_crosshair_viewport_position(&self) -> Option<(usize, usize)> {
304        // Check if crosshair is within the current viewport
305        // For rows, standard check
306        if self.crosshair_row < self.viewport_rows.start
307            || self.crosshair_row >= self.viewport_rows.end
308        {
309            return None;
310        }
311
312        // For columns, we need to account for pinned columns
313        let pinned_count = self.dataview.get_pinned_columns().len();
314
315        // If crosshair is on a pinned column, it's always visible
316        if self.crosshair_col < pinned_count {
317            return Some((
318                self.crosshair_row - self.viewport_rows.start,
319                self.crosshair_col, // Pinned columns are always at the start
320            ));
321        }
322
323        // For scrollable columns, check if it's in the viewport
324        // Convert visual column to scrollable column index
325        let scrollable_col = self.crosshair_col - pinned_count;
326        if scrollable_col >= self.viewport_cols.start && scrollable_col < self.viewport_cols.end {
327            // Calculate the visual position in the rendered output
328            // Pinned columns come first, then the visible scrollable columns
329            let visual_col_in_viewport = pinned_count + (scrollable_col - self.viewport_cols.start);
330            return Some((
331                self.crosshair_row - self.viewport_rows.start,
332                visual_col_in_viewport,
333            ));
334        }
335
336        None
337    }
338
339    /// Navigate up one row
340    pub fn navigate_row_up(&mut self) -> RowNavigationResult {
341        let total_rows = self.dataview.row_count();
342
343        // Check viewport lock first - prevent scrolling entirely
344        if self.viewport_lock {
345            debug!(target: "viewport_manager", 
346                   "navigate_row_up: Viewport locked, crosshair={}, viewport={:?}",
347                   self.crosshair_row, self.viewport_rows);
348            // In viewport lock mode, just move cursor up within current viewport
349            if self.crosshair_row > self.viewport_rows.start {
350                self.crosshair_row -= 1;
351                return RowNavigationResult {
352                    row_position: self.crosshair_row,
353                    row_scroll_offset: self.viewport_rows.start,
354                    description: "Moved within locked viewport".to_string(),
355                    viewport_changed: false,
356                };
357            } else {
358                // Already at top of locked viewport
359                return RowNavigationResult {
360                    row_position: self.crosshair_row,
361                    row_scroll_offset: self.viewport_rows.start,
362                    description: "Moved within locked viewport".to_string(),
363                    viewport_changed: false,
364                };
365            }
366        }
367
368        // Handle cursor lock mode
369        if self.cursor_lock {
370            if let Some(lock_position) = self.cursor_lock_position {
371                // In cursor lock mode, scroll the viewport but keep crosshair at same relative position
372                if self.viewport_rows.start == 0 {
373                    // Can't scroll further up
374                    return RowNavigationResult {
375                        row_position: self.crosshair_row,
376                        row_scroll_offset: self.viewport_rows.start,
377                        description: "At top of data".to_string(),
378                        viewport_changed: false,
379                    };
380                }
381
382                let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
383                let new_viewport_start = self.viewport_rows.start.saturating_sub(1);
384
385                // Update viewport
386                self.viewport_rows =
387                    new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
388
389                // Update crosshair to maintain relative position
390                self.crosshair_row = (self.viewport_rows.start + lock_position)
391                    .min(self.viewport_rows.end.saturating_sub(1));
392
393                return RowNavigationResult {
394                    row_position: self.crosshair_row,
395                    row_scroll_offset: self.viewport_rows.start,
396                    description: format!(
397                        "Scrolled up (locked at viewport row {})",
398                        lock_position + 1
399                    ),
400                    viewport_changed: true,
401                };
402            }
403        }
404
405        // Normal navigation (not locked)
406        // Vim-like behavior: don't wrap, stay at boundary
407        if self.crosshair_row == 0 {
408            // Already at first row, don't move
409            return RowNavigationResult {
410                row_position: 0,
411                row_scroll_offset: self.viewport_rows.start,
412                description: "Already at first row".to_string(),
413                viewport_changed: false,
414            };
415        }
416
417        let new_row = self.crosshair_row - 1;
418        self.crosshair_row = new_row;
419
420        // Adjust viewport if needed
421        let viewport_changed = if new_row < self.viewport_rows.start {
422            self.viewport_rows = new_row..self.viewport_rows.end.saturating_sub(1);
423            true
424        } else {
425            false
426        };
427
428        RowNavigationResult {
429            row_position: new_row,
430            row_scroll_offset: self.viewport_rows.start,
431            description: format!("Move to row {}", new_row + 1),
432            viewport_changed,
433        }
434    }
435
436    /// Navigate down one row
437    pub fn navigate_row_down(&mut self) -> RowNavigationResult {
438        let total_rows = self.dataview.row_count();
439
440        // Check viewport lock first - prevent scrolling entirely
441        if self.viewport_lock {
442            debug!(target: "viewport_manager", 
443                   "navigate_row_down: Viewport locked, crosshair={}, viewport={:?}",
444                   self.crosshair_row, self.viewport_rows);
445            // In viewport lock mode, just move cursor down within current viewport
446            if self.crosshair_row < self.viewport_rows.end - 1
447                && self.crosshair_row < total_rows - 1
448            {
449                self.crosshair_row += 1;
450                return RowNavigationResult {
451                    row_position: self.crosshair_row,
452                    row_scroll_offset: self.viewport_rows.start,
453                    description: "Moved within locked viewport".to_string(),
454                    viewport_changed: false,
455                };
456            } else {
457                // Already at bottom of locked viewport or end of data
458                return RowNavigationResult {
459                    row_position: self.crosshair_row,
460                    row_scroll_offset: self.viewport_rows.start,
461                    description: "Moved within locked viewport".to_string(),
462                    viewport_changed: false,
463                };
464            }
465        }
466
467        // Handle cursor lock mode
468        if self.cursor_lock {
469            if let Some(lock_position) = self.cursor_lock_position {
470                // In cursor lock mode, scroll the viewport but keep crosshair at same relative position
471                let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
472                let new_viewport_start =
473                    (self.viewport_rows.start + 1).min(total_rows.saturating_sub(viewport_height));
474
475                if new_viewport_start == self.viewport_rows.start {
476                    // Can't scroll further
477                    return RowNavigationResult {
478                        row_position: self.crosshair_row,
479                        row_scroll_offset: self.viewport_rows.start,
480                        description: "At bottom of data".to_string(),
481                        viewport_changed: false,
482                    };
483                }
484
485                // Update viewport
486                self.viewport_rows =
487                    new_viewport_start..(new_viewport_start + viewport_height).min(total_rows);
488
489                // Update crosshair to maintain relative position
490                self.crosshair_row = (self.viewport_rows.start + lock_position)
491                    .min(self.viewport_rows.end.saturating_sub(1));
492
493                return RowNavigationResult {
494                    row_position: self.crosshair_row,
495                    row_scroll_offset: self.viewport_rows.start,
496                    description: format!(
497                        "Scrolled down (locked at viewport row {})",
498                        lock_position + 1
499                    ),
500                    viewport_changed: true,
501                };
502            }
503        }
504
505        // Normal navigation (not locked)
506        // Vim-like behavior: don't wrap, stay at boundary
507        if self.crosshair_row + 1 >= total_rows {
508            // Already at last row, don't move
509            let last_row = total_rows.saturating_sub(1);
510            return RowNavigationResult {
511                row_position: last_row,
512                row_scroll_offset: self.viewport_rows.start,
513                description: "Already at last row".to_string(),
514                viewport_changed: false,
515            };
516        }
517
518        let new_row = self.crosshair_row + 1;
519        self.crosshair_row = new_row;
520
521        // Adjust viewport if needed
522        // viewport_rows now correctly represents only data rows (no table chrome)
523        let viewport_changed = if new_row >= self.viewport_rows.end {
524            // Need to scroll - cursor is at or past the end of viewport
525            let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
526            self.viewport_rows = (new_row + 1).saturating_sub(viewport_height)..(new_row + 1);
527            true
528        } else {
529            false
530        };
531
532        RowNavigationResult {
533            row_position: new_row,
534            row_scroll_offset: self.viewport_rows.start,
535            description: format!("Move to row {}", new_row + 1),
536            viewport_changed,
537        }
538    }
539
540    /// Create a new ViewportManager for a DataView
541    pub fn new(dataview: Arc<DataView>) -> Self {
542        // Get the actual visible column count (after hiding)
543        let display_columns = dataview.get_display_columns();
544        let visible_col_count = display_columns.len();
545        let total_col_count = dataview.source().column_count(); // Total DataTable columns for width array
546        let total_rows = dataview.row_count();
547
548        // Initialize viewport in visual coordinate space
549        let initial_viewport_cols = if visible_col_count > 0 {
550            0..visible_col_count.min(20) // Show up to 20 visual columns initially
551        } else {
552            0..0
553        };
554
555        // Initialize viewport rows to show first page of data
556        // Start with a reasonable default that will be updated when terminal size is known
557        let default_visible_rows = 50usize; // Start larger, will be adjusted by update_terminal_size
558        let initial_viewport_rows = if total_rows > 0 {
559            0..total_rows.min(default_visible_rows)
560        } else {
561            0..0
562        };
563
564        Self {
565            dataview,
566            viewport_rows: initial_viewport_rows,
567            viewport_cols: initial_viewport_cols,
568            terminal_width: 80,
569            terminal_height: 24,
570            width_calculator: ColumnWidthCalculator::new(),
571            visible_row_cache: Vec::new(),
572            cache_signature: 0,
573            cache_dirty: true,
574            crosshair_row: 0,
575            crosshair_col: 0,
576            cursor_lock: false,
577            cursor_lock_position: None,
578            viewport_lock: false,
579            viewport_lock_boundaries: None,
580        }
581    }
582
583    /// Update the underlying DataView
584    pub fn set_dataview(&mut self, dataview: Arc<DataView>) {
585        self.dataview = dataview;
586        self.invalidate_cache();
587    }
588
589    /// Reset crosshair position to origin (0, 0)
590    pub fn reset_crosshair(&mut self) {
591        self.crosshair_row = 0;
592        self.crosshair_col = 0;
593        self.cursor_lock = false;
594        self.cursor_lock_position = None;
595    }
596
597    /// Get the current column packing mode
598    pub fn get_packing_mode(&self) -> ColumnPackingMode {
599        self.width_calculator.get_packing_mode()
600    }
601
602    /// Set the column packing mode and recalculate widths
603    pub fn set_packing_mode(&mut self, mode: ColumnPackingMode) {
604        self.width_calculator.set_packing_mode(mode);
605        self.invalidate_cache();
606    }
607
608    /// Cycle to the next packing mode
609    pub fn cycle_packing_mode(&mut self) -> ColumnPackingMode {
610        self.width_calculator.cycle_packing_mode();
611        self.invalidate_cache();
612        self.width_calculator.get_packing_mode()
613    }
614
615    /// Update viewport position and size
616    pub fn set_viewport(&mut self, row_offset: usize, col_offset: usize, width: u16, height: u16) {
617        let new_rows = row_offset
618            ..row_offset
619                .saturating_add(height as usize)
620                .min(self.dataview.row_count());
621
622        // For columns, we need to calculate how many columns actually fit in the width
623        // Don't use width directly as column count - it's terminal width in characters!
624        let display_columns = self.dataview.get_display_columns();
625        let visual_column_count = display_columns.len();
626
627        // Calculate how many columns we can actually fit in the available width
628        let columns_that_fit = self.calculate_columns_that_fit(col_offset, width);
629        let new_cols = col_offset
630            ..col_offset
631                .saturating_add(columns_that_fit)
632                .min(visual_column_count);
633
634        // Check if viewport actually changed
635        if new_rows != self.viewport_rows || new_cols != self.viewport_cols {
636            self.viewport_rows = new_rows;
637            self.viewport_cols = new_cols;
638            self.terminal_width = width;
639            self.terminal_height = height;
640            self.cache_dirty = true;
641        }
642    }
643
644    /// Update viewport size based on terminal dimensions
645    /// Returns the calculated visible rows for the results area
646    pub fn update_terminal_size(&mut self, terminal_width: u16, terminal_height: u16) -> usize {
647        // The terminal_height passed here should already be the number of data rows available
648        // The caller should have already accounted for any UI chrome
649        let visible_rows = (terminal_height as usize).max(10);
650
651        debug!(target: "viewport_manager",
652            "update_terminal_size: terminal_height={}, calculated visible_rows={}",
653            terminal_height, visible_rows
654        );
655
656        let old_viewport = self.viewport_rows.clone();
657
658        // Update our stored terminal dimensions
659        self.terminal_width = terminal_width;
660        self.terminal_height = terminal_height;
661
662        // Only adjust viewport if terminal size actually changed AND we need to
663        // Don't reset the viewport on every render!
664        let total_rows = self.dataview.row_count();
665
666        // Check if viewport needs adjustment for the new terminal size
667        let viewport_size = self.viewport_rows.end - self.viewport_rows.start;
668        if viewport_size != visible_rows && total_rows > 0 {
669            // Terminal size changed - adjust viewport to maintain crosshair position
670            // Make sure crosshair stays visible in the viewport
671            if self.crosshair_row < self.viewport_rows.start {
672                // Crosshair is above viewport - scroll up
673                self.viewport_rows =
674                    self.crosshair_row..(self.crosshair_row + visible_rows).min(total_rows);
675            } else if self.crosshair_row >= self.viewport_rows.start + visible_rows {
676                // Crosshair is below viewport - scroll down
677                let start = self.crosshair_row.saturating_sub(visible_rows - 1);
678                self.viewport_rows = start..(start + visible_rows).min(total_rows);
679            } else {
680                // Crosshair is in viewport - just resize the viewport
681                self.viewport_rows = self.viewport_rows.start
682                    ..(self.viewport_rows.start + visible_rows).min(total_rows);
683            }
684        }
685
686        // Also update column viewport based on new terminal width
687        // This is crucial for showing all columns that fit when first loading
688        let visible_column_count = self.dataview.get_display_columns().len();
689        if visible_column_count > 0 {
690            // Calculate how many columns we can fit with the new terminal width
691            // Calculate how many columns we can fit with the new terminal width
692            // Subtract 2 for left and right table borders
693            let columns_that_fit = self.calculate_columns_that_fit(
694                self.viewport_cols.start,
695                terminal_width.saturating_sub(2), // Left + right table borders
696            );
697
698            let new_col_viewport_end = self
699                .viewport_cols
700                .start
701                .saturating_add(columns_that_fit)
702                .min(visible_column_count);
703
704            let old_col_viewport = self.viewport_cols.clone();
705            self.viewport_cols = self.viewport_cols.start..new_col_viewport_end;
706
707            if old_col_viewport != self.viewport_cols {
708                debug!(target: "viewport_manager",
709                    "update_terminal_size - column viewport changed from {:?} to {:?}, terminal_width={}",
710                    old_col_viewport, self.viewport_cols, terminal_width
711                );
712                self.cache_dirty = true;
713            }
714        }
715
716        if old_viewport != self.viewport_rows {
717            debug!(target: "navigation",
718                "ViewportManager::update_terminal_size - viewport changed from {:?} to {:?}, crosshair={}, visible_rows={}",
719                old_viewport, self.viewport_rows, self.crosshair_row, visible_rows
720            );
721        }
722
723        visible_rows
724    }
725
726    /// Scroll viewport by relative amount
727    pub fn scroll_by(&mut self, row_delta: isize, col_delta: isize) {
728        let new_row_start = (self.viewport_rows.start as isize + row_delta).max(0) as usize;
729        let new_col_start = (self.viewport_cols.start as isize + col_delta).max(0) as usize;
730
731        self.set_viewport(
732            new_row_start,
733            new_col_start,
734            self.terminal_width,
735            self.terminal_height,
736        );
737    }
738
739    /// Get calculated column widths for current viewport
740    pub fn get_column_widths(&mut self) -> &[u16] {
741        self.width_calculator
742            .get_all_column_widths(&self.dataview, &self.viewport_rows)
743    }
744
745    /// Get column width for a specific column
746    pub fn get_column_width(&mut self, col_idx: usize) -> u16 {
747        self.width_calculator
748            .get_column_width(&self.dataview, &self.viewport_rows, col_idx)
749    }
750
751    /// Get visible rows in the current viewport
752    pub fn get_visible_rows(&self) -> Vec<DataRow> {
753        let mut rows = Vec::with_capacity(self.viewport_rows.len());
754
755        for row_idx in self.viewport_rows.clone() {
756            if let Some(row) = self.dataview.get_row(row_idx) {
757                rows.push(row);
758            }
759        }
760
761        rows
762    }
763
764    /// Get a specific visible row by viewport-relative index
765    pub fn get_visible_row(&self, viewport_row: usize) -> Option<DataRow> {
766        let absolute_row = self.viewport_rows.start + viewport_row;
767        if absolute_row < self.viewport_rows.end {
768            self.dataview.get_row(absolute_row)
769        } else {
770            None
771        }
772    }
773
774    /// Get visible column headers (only non-hidden columns that are in current viewport)
775    pub fn get_visible_columns(&self) -> Vec<String> {
776        // Get display column names (excludes hidden columns)
777        let display_column_names = self.dataview.get_display_column_names();
778
779        // Map viewport column indices to display column names
780        let mut visible = Vec::new();
781        for col_idx in self.viewport_cols.clone() {
782            if col_idx < display_column_names.len() {
783                visible.push(display_column_names[col_idx].clone());
784            }
785        }
786
787        visible
788    }
789
790    /// Get the current viewport row range
791    pub fn viewport_rows(&self) -> Range<usize> {
792        self.viewport_rows.clone()
793    }
794
795    /// Get the current viewport column range
796    pub fn viewport_cols(&self) -> Range<usize> {
797        self.viewport_cols.clone()
798    }
799
800    /// Check if a row is visible in the viewport
801    pub fn is_row_visible(&self, row_idx: usize) -> bool {
802        self.viewport_rows.contains(&row_idx)
803    }
804
805    /// Check if a column is visible in the viewport
806    pub fn is_column_visible(&self, col_idx: usize) -> bool {
807        self.viewport_cols.contains(&col_idx)
808    }
809
810    /// Get total row count from underlying view
811    pub fn total_rows(&self) -> usize {
812        self.dataview.row_count()
813    }
814
815    /// Get total column count from underlying view
816    pub fn total_columns(&self) -> usize {
817        self.dataview.column_count()
818    }
819
820    /// Get terminal width in characters
821    pub fn get_terminal_width(&self) -> u16 {
822        self.terminal_width
823    }
824
825    /// Get terminal height in rows
826    pub fn get_terminal_height(&self) -> usize {
827        self.terminal_height as usize
828    }
829
830    /// Force cache recalculation on next access
831    pub fn invalidate_cache(&mut self) {
832        self.cache_dirty = true;
833        self.width_calculator.mark_dirty();
834    }
835
836    /// Calculate optimal column layout for available width
837    /// Returns a RANGE of visual column indices (0..n) that should be displayed
838    /// This works entirely in visual coordinate space - no DataTable indices!
839    pub fn calculate_visible_column_indices(&mut self, available_width: u16) -> Vec<usize> {
840        // Width calculation is now handled by ColumnWidthCalculator
841
842        // Get the display columns from DataView (these are DataTable indices for visible columns)
843        let display_columns = self.dataview.get_display_columns();
844        let total_visual_columns = display_columns.len();
845
846        if total_visual_columns == 0 {
847            return Vec::new();
848        }
849
850        // Get pinned columns - they're always visible
851        let pinned_columns = self.dataview.get_pinned_columns();
852        let pinned_count = pinned_columns.len();
853
854        let mut used_width = 0u16;
855        let separator_width = 1u16;
856        let mut result = Vec::new();
857
858        tracing::debug!("[PIN_DEBUG] === calculate_visible_column_indices ===");
859        tracing::debug!(
860            "[PIN_DEBUG] available_width={}, total_visual_columns={}",
861            available_width,
862            total_visual_columns
863        );
864        tracing::debug!(
865            "[PIN_DEBUG] pinned_columns={:?} (count={})",
866            pinned_columns,
867            pinned_count
868        );
869        tracing::debug!("[PIN_DEBUG] viewport_cols={:?}", self.viewport_cols);
870        tracing::debug!("[PIN_DEBUG] display_columns={:?}", display_columns);
871
872        debug!(target: "viewport_manager",
873               "calculate_visible_column_indices: available_width={}, total_visual_columns={}, pinned_count={}, viewport_start={}",
874               available_width, total_visual_columns, pinned_count, self.viewport_cols.start);
875
876        // First, always add all pinned columns (they're at the beginning of display_columns)
877        for visual_idx in 0..pinned_count {
878            if visual_idx >= display_columns.len() {
879                break;
880            }
881
882            let datatable_idx = display_columns[visual_idx];
883            let width = self.width_calculator.get_column_width(
884                &self.dataview,
885                &self.viewport_rows,
886                datatable_idx,
887            );
888
889            // Always include pinned columns, even if they exceed available width
890            used_width += width + separator_width;
891            result.push(datatable_idx);
892            tracing::debug!(
893                "[PIN_DEBUG] Added pinned column: visual_idx={}, datatable_idx={}, width={}",
894                visual_idx,
895                datatable_idx,
896                width
897            );
898        }
899
900        // IMPORTANT FIX: viewport_cols represents SCROLLABLE column indices (0-based, excluding pinned)
901        // To get the visual column index, we need to add pinned_count to the scrollable index
902        let scrollable_start = self.viewport_cols.start;
903        let visual_start = scrollable_start + pinned_count;
904
905        tracing::debug!(
906            "[PIN_DEBUG] viewport_cols.start={} is SCROLLABLE index",
907            self.viewport_cols.start
908        );
909        tracing::debug!(
910            "[PIN_DEBUG] visual_start={} (scrollable_start {} + pinned_count {})",
911            visual_start,
912            scrollable_start,
913            pinned_count
914        );
915
916        let visual_start = visual_start.min(total_visual_columns);
917
918        // Calculate how many columns we can fit from the viewport
919        for visual_idx in visual_start..total_visual_columns {
920            // Get the DataTable index for this visual position
921            let datatable_idx = display_columns[visual_idx];
922
923            let width = self.width_calculator.get_column_width(
924                &self.dataview,
925                &self.viewport_rows,
926                datatable_idx,
927            );
928
929            if used_width + width + separator_width <= available_width {
930                used_width += width + separator_width;
931                result.push(datatable_idx);
932                tracing::debug!("[PIN_DEBUG] Added scrollable column: visual_idx={}, datatable_idx={}, width={}", visual_idx, datatable_idx, width);
933            } else {
934                tracing::debug!(
935                    "[PIN_DEBUG] Stopped at visual_idx={} - would exceed width",
936                    visual_idx
937                );
938                break;
939            }
940        }
941
942        // If we couldn't fit any scrollable columns but have pinned columns, that's okay
943        // If we have no columns at all, ensure we show at least one column
944        if result.is_empty() && total_visual_columns > 0 {
945            result.push(display_columns[0]);
946        }
947
948        tracing::debug!("[PIN_DEBUG] Final result: {:?}", result);
949        tracing::debug!("[PIN_DEBUG] === End calculate_visible_column_indices ===");
950        debug!(target: "viewport_manager",
951               "calculate_visible_column_indices RESULT: pinned={}, viewport_start={}, visual_start={} -> DataTable indices {:?}",
952               pinned_count, self.viewport_cols.start, visual_start, result);
953
954        result
955    }
956
957    /// Calculate how many columns we can fit starting from a given column index
958    /// This helps determine optimal scrolling positions
959    pub fn calculate_columns_that_fit(&mut self, start_col: usize, available_width: u16) -> usize {
960        // Width calculation is now handled by ColumnWidthCalculator
961
962        let mut used_width = 0u16;
963        let mut column_count = 0usize;
964        let separator_width = 1u16;
965
966        for col_idx in start_col..self.dataview.column_count() {
967            let width = self.width_calculator.get_column_width(
968                &self.dataview,
969                &self.viewport_rows,
970                col_idx,
971            );
972            if used_width + width + separator_width <= available_width {
973                used_width += width + separator_width;
974                column_count += 1;
975            } else {
976                break;
977            }
978        }
979
980        column_count.max(1) // Always show at least one column
981    }
982
983    /// Get calculated widths for specific columns
984    /// This is useful for rendering when we know which columns will be displayed
985    pub fn get_column_widths_for(&mut self, column_indices: &[usize]) -> Vec<u16> {
986        column_indices
987            .iter()
988            .map(|&idx| {
989                self.width_calculator
990                    .get_column_width(&self.dataview, &self.viewport_rows, idx)
991            })
992            .collect()
993    }
994
995    /// Update viewport for column scrolling
996    /// This recalculates column widths based on newly visible columns
997    pub fn update_column_viewport(&mut self, start_col: usize, available_width: u16) {
998        let col_count = self.calculate_columns_that_fit(start_col, available_width);
999        let end_col = (start_col + col_count).min(self.dataview.column_count());
1000
1001        if self.viewport_cols.start != start_col || self.viewport_cols.end != end_col {
1002            self.viewport_cols = start_col..end_col;
1003            self.cache_dirty = true;
1004        }
1005    }
1006
1007    /// Get a reference to the underlying DataView
1008    pub fn dataview(&self) -> &DataView {
1009        &self.dataview
1010    }
1011
1012    /// Get a cloned copy of the underlying DataView (for syncing with Buffer)
1013    /// This is a temporary solution until we refactor Buffer to use Arc<DataView>
1014    pub fn clone_dataview(&self) -> DataView {
1015        (*self.dataview).clone()
1016    }
1017
1018    /// Calculate the optimal scroll offset to show the last column
1019    /// This backtracks from the end to find the best viewport position
1020    /// Returns the scroll offset in terms of scrollable columns (excluding pinned)
1021    pub fn calculate_optimal_offset_for_last_column(&mut self, available_width: u16) -> usize {
1022        // Width calculation is now handled by ColumnWidthCalculator
1023
1024        // Get the display columns (visible columns only, in display order)
1025        let display_columns = self.dataview.get_display_columns();
1026        if display_columns.is_empty() {
1027            return 0;
1028        }
1029
1030        let pinned = self.dataview.get_pinned_columns();
1031        let pinned_count = pinned.len();
1032
1033        // Calculate how much width is used by pinned columns
1034        let mut pinned_width = 0u16;
1035        let separator_width = 1u16;
1036        for &col_idx in pinned {
1037            let width = self.width_calculator.get_column_width(
1038                &self.dataview,
1039                &self.viewport_rows,
1040                col_idx,
1041            );
1042            pinned_width += width + separator_width;
1043        }
1044
1045        // Available width for scrollable columns
1046        let available_for_scrollable = available_width.saturating_sub(pinned_width);
1047
1048        // Get scrollable columns only (display columns minus pinned)
1049        let scrollable_columns: Vec<usize> = display_columns
1050            .iter()
1051            .filter(|&&col| !pinned.contains(&col))
1052            .copied()
1053            .collect();
1054
1055        if scrollable_columns.is_empty() {
1056            return 0;
1057        }
1058
1059        // Get the last scrollable column
1060        let last_col_idx = *scrollable_columns.last().unwrap();
1061        let last_col_width = self.width_calculator.get_column_width(
1062            &self.dataview,
1063            &self.viewport_rows,
1064            last_col_idx,
1065        );
1066
1067        tracing::debug!(
1068            "Starting calculation: last_col_idx={}, width={}w, available={}w, scrollable_cols={}",
1069            last_col_idx,
1070            last_col_width,
1071            available_for_scrollable,
1072            scrollable_columns.len()
1073        );
1074
1075        let mut accumulated_width = last_col_width + separator_width;
1076        let mut best_offset = scrollable_columns.len() - 1; // Start with last scrollable column
1077
1078        // Now work backwards through scrollable columns to find how many more we can fit
1079        for (idx, &col_idx) in scrollable_columns.iter().enumerate().rev().skip(1) {
1080            let width = self.width_calculator.get_column_width(
1081                &self.dataview,
1082                &self.viewport_rows,
1083                col_idx,
1084            );
1085
1086            let width_with_separator = width + separator_width;
1087
1088            if accumulated_width + width_with_separator <= available_for_scrollable {
1089                // This column fits, keep going backwards
1090                accumulated_width += width_with_separator;
1091                best_offset = idx; // Use the index in scrollable_columns
1092                tracing::trace!(
1093                    "Column {} (idx {}) fits ({}w), accumulated={}w, new offset={}",
1094                    col_idx,
1095                    idx,
1096                    width,
1097                    accumulated_width,
1098                    best_offset
1099                );
1100            } else {
1101                // This column doesn't fit, we found our optimal offset
1102                // The offset should be the next column (since this one doesn't fit)
1103                best_offset = idx + 1;
1104                tracing::trace!(
1105                    "Column {} doesn't fit ({}w would make {}w total), stopping at offset {}",
1106                    col_idx,
1107                    width,
1108                    accumulated_width + width_with_separator,
1109                    best_offset
1110                );
1111                break;
1112            }
1113        }
1114
1115        // best_offset is now the index within scrollable_columns
1116        // We need to return it as is (it's already the scroll offset for scrollable columns)
1117
1118        // Now verify that starting from best_offset, we can actually see the last column
1119        // This is the critical check we were missing!
1120        let mut test_width = 0u16;
1121        let mut can_see_last = false;
1122        for idx in best_offset..scrollable_columns.len() {
1123            let col_idx = scrollable_columns[idx];
1124            let width = self.width_calculator.get_column_width(
1125                &self.dataview,
1126                &self.viewport_rows,
1127                col_idx,
1128            );
1129            test_width += width + separator_width;
1130
1131            if test_width > available_for_scrollable {
1132                // We can't fit all columns from best_offset to last
1133                // Need to adjust offset forward
1134                tracing::warn!(
1135                    "Offset {} doesn't show last column! Need {}w but have {}w",
1136                    best_offset,
1137                    test_width,
1138                    available_for_scrollable
1139                );
1140                // Move offset forward until last column fits
1141                best_offset = best_offset + 1;
1142                can_see_last = false;
1143                break;
1144            }
1145            if idx == scrollable_columns.len() - 1 {
1146                can_see_last = true;
1147            }
1148        }
1149
1150        // If we still can't see the last column, keep adjusting
1151        while !can_see_last && best_offset < scrollable_columns.len() {
1152            test_width = 0;
1153            for idx in best_offset..scrollable_columns.len() {
1154                let col_idx = scrollable_columns[idx];
1155                let width = self.width_calculator.get_column_width(
1156                    &self.dataview,
1157                    &self.viewport_rows,
1158                    col_idx,
1159                );
1160                test_width += width + separator_width;
1161
1162                if test_width > available_for_scrollable {
1163                    best_offset = best_offset + 1;
1164                    break;
1165                }
1166                if idx == scrollable_columns.len() - 1 {
1167                    can_see_last = true;
1168                }
1169            }
1170        }
1171
1172        // best_offset is already in terms of scrollable columns
1173        tracing::debug!(
1174            "Final offset for last column: scrollable_offset={}, fits {} columns, last col width: {}w, verified last col visible: {}",
1175            best_offset,
1176            scrollable_columns.len() - best_offset,
1177            last_col_width,
1178            can_see_last
1179        );
1180
1181        best_offset
1182    }
1183
1184    /// Debug dump of ViewportManager state for F5 diagnostics
1185    pub fn debug_dump(&mut self, available_width: u16) -> String {
1186        // Width calculation is now handled by ColumnWidthCalculator
1187
1188        let mut output = String::new();
1189        output.push_str("========== VIEWPORT MANAGER DEBUG ==========\n");
1190
1191        let total_cols = self.dataview.column_count();
1192        let pinned = self.dataview.get_pinned_columns();
1193        let pinned_count = pinned.len();
1194
1195        output.push_str(&format!("Total columns: {}\n", total_cols));
1196        output.push_str(&format!("Pinned columns: {:?}\n", pinned));
1197        output.push_str(&format!("Available width: {}w\n", available_width));
1198        output.push_str(&format!("Current viewport: {:?}\n", self.viewport_cols));
1199        output.push_str(&format!(
1200            "Packing mode: {} (Alt+S to cycle)\n",
1201            self.width_calculator.get_packing_mode().display_name()
1202        ));
1203        output.push_str("\n");
1204
1205        // Show detailed column width calculations
1206        output.push_str("=== COLUMN WIDTH CALCULATIONS ===\n");
1207        output.push_str(&format!(
1208            "Mode: {}\n",
1209            self.width_calculator.get_packing_mode().display_name()
1210        ));
1211
1212        // Show debug info for visible columns in viewport
1213        let debug_info = self.width_calculator.get_debug_info();
1214        if !debug_info.is_empty() {
1215            output.push_str("Visible columns in viewport:\n");
1216
1217            // Only show columns that are currently visible
1218            let mut visible_count = 0;
1219            for col_idx in self.viewport_cols.clone() {
1220                if col_idx < debug_info.len() {
1221                    let (ref col_name, header_width, max_data_width, final_width, sample_count) =
1222                        debug_info[col_idx];
1223
1224                    // Determine why this width was chosen
1225                    let reason = match self.width_calculator.get_packing_mode() {
1226                        ColumnPackingMode::DataFocus => {
1227                            if max_data_width <= 3 {
1228                                format!("Ultra aggressive (data:{}≤3 chars)", max_data_width)
1229                            } else if max_data_width <= 10 && header_width > max_data_width * 2 {
1230                                format!(
1231                                    "Aggressive truncate (data:{}≤10, header:{}>{} )",
1232                                    max_data_width,
1233                                    header_width,
1234                                    max_data_width * 2
1235                                )
1236                            } else if final_width == MAX_COL_WIDTH_DATA_FOCUS {
1237                                "Max width reached".to_string()
1238                            } else {
1239                                "Data-based width".to_string()
1240                            }
1241                        }
1242                        ColumnPackingMode::HeaderFocus => {
1243                            if final_width == header_width + COLUMN_PADDING {
1244                                "Full header shown".to_string()
1245                            } else if final_width == MAX_COL_WIDTH {
1246                                "Max width reached".to_string()
1247                            } else {
1248                                "Header priority".to_string()
1249                            }
1250                        }
1251                        ColumnPackingMode::Balanced => {
1252                            if header_width > max_data_width && final_width < header_width {
1253                                "Header constrained by ratio".to_string()
1254                            } else {
1255                                "Balanced".to_string()
1256                            }
1257                        }
1258                    };
1259
1260                    output.push_str(&format!(
1261                        "  [{}] \"{}\":\n    Header: {}w, Data: {}w → Final: {}w ({}, {} samples)\n",
1262                        col_idx, col_name, header_width, max_data_width, final_width, reason, sample_count
1263                    ));
1264
1265                    visible_count += 1;
1266
1267                    // Stop after showing 10 columns to avoid clutter
1268                    if visible_count >= 10 {
1269                        let remaining = self.viewport_cols.end - self.viewport_cols.start - 10;
1270                        if remaining > 0 {
1271                            output.push_str(&format!("  ... and {} more columns\n", remaining));
1272                        }
1273                        break;
1274                    }
1275                }
1276            }
1277        }
1278
1279        output.push_str("\n");
1280
1281        // Show column widths summary
1282        output.push_str("Column width summary (all columns):\n");
1283        let all_widths = self
1284            .width_calculator
1285            .get_all_column_widths(&self.dataview, &self.viewport_rows);
1286        for (idx, &width) in all_widths.iter().enumerate() {
1287            if idx >= 20 && idx < total_cols - 10 {
1288                if idx == 20 {
1289                    output.push_str("  ... (showing only first 20 and last 10)\n");
1290                }
1291                continue;
1292            }
1293            output.push_str(&format!("  [{}] {}w\n", idx, width));
1294        }
1295        output.push_str("\n");
1296
1297        // Test optimal offset calculation step by step
1298        output.push_str("=== OPTIMAL OFFSET CALCULATION ===\n");
1299        let last_col_idx = total_cols - 1;
1300        let last_col_width = self.width_calculator.get_column_width(
1301            &self.dataview,
1302            &self.viewport_rows,
1303            last_col_idx,
1304        );
1305
1306        // Calculate available width for scrollable columns
1307        let separator_width = 1u16;
1308        let mut pinned_width = 0u16;
1309        for &col_idx in pinned {
1310            let width = self.width_calculator.get_column_width(
1311                &self.dataview,
1312                &self.viewport_rows,
1313                col_idx,
1314            );
1315            pinned_width += width + separator_width;
1316        }
1317        let available_for_scrollable = available_width.saturating_sub(pinned_width);
1318
1319        output.push_str(&format!(
1320            "Last column: {} (width: {}w)\n",
1321            last_col_idx, last_col_width
1322        ));
1323        output.push_str(&format!("Pinned width: {}w\n", pinned_width));
1324        output.push_str(&format!(
1325            "Available for scrollable: {}w\n",
1326            available_for_scrollable
1327        ));
1328        output.push_str("\n");
1329
1330        // Simulate the calculation
1331        let mut accumulated_width = last_col_width + separator_width;
1332        let mut best_offset = last_col_idx;
1333
1334        output.push_str("Backtracking from last column:\n");
1335        output.push_str(&format!(
1336            "  Start: column {} = {}w (accumulated: {}w)\n",
1337            last_col_idx, last_col_width, accumulated_width
1338        ));
1339
1340        for col_idx in (pinned_count..last_col_idx).rev() {
1341            let width = self.width_calculator.get_column_width(
1342                &self.dataview,
1343                &self.viewport_rows,
1344                col_idx,
1345            );
1346            let width_with_sep = width + separator_width;
1347
1348            if accumulated_width + width_with_sep <= available_for_scrollable {
1349                accumulated_width += width_with_sep;
1350                best_offset = col_idx;
1351                output.push_str(&format!(
1352                    "  Column {} fits: {}w (accumulated: {}w, offset: {})\n",
1353                    col_idx, width, accumulated_width, best_offset
1354                ));
1355            } else {
1356                output.push_str(&format!(
1357                    "  Column {} doesn't fit: {}w (would make {}w > {}w)\n",
1358                    col_idx,
1359                    width,
1360                    accumulated_width + width_with_sep,
1361                    available_for_scrollable
1362                ));
1363                best_offset = col_idx + 1;
1364                break;
1365            }
1366        }
1367
1368        output.push_str(&format!(
1369            "\nCalculated offset: {} (absolute)\n",
1370            best_offset
1371        ));
1372
1373        // Now verify this offset actually works
1374        output.push_str("\n=== VERIFICATION ===\n");
1375        let mut verify_width = 0u16;
1376        let mut can_show_last = true;
1377
1378        for test_idx in best_offset..=last_col_idx {
1379            let width = self.width_calculator.get_column_width(
1380                &self.dataview,
1381                &self.viewport_rows,
1382                test_idx,
1383            );
1384            verify_width += width + separator_width;
1385
1386            output.push_str(&format!(
1387                "  Column {}: {}w (running total: {}w)\n",
1388                test_idx, width, verify_width
1389            ));
1390
1391            if verify_width > available_for_scrollable {
1392                output.push_str(&format!(
1393                    "    ❌ EXCEEDS LIMIT! {}w > {}w\n",
1394                    verify_width, available_for_scrollable
1395                ));
1396                if test_idx == last_col_idx {
1397                    can_show_last = false;
1398                    output.push_str("    ❌ LAST COLUMN NOT VISIBLE!\n");
1399                }
1400                break;
1401            }
1402
1403            if test_idx == last_col_idx {
1404                output.push_str("    ✅ LAST COLUMN VISIBLE!\n");
1405            }
1406        }
1407
1408        output.push_str(&format!(
1409            "\nVerification result: last column visible = {}\n",
1410            can_show_last
1411        ));
1412
1413        // Show what the current viewport actually shows
1414        output.push_str("\n=== CURRENT VIEWPORT RESULT ===\n");
1415        let visible_indices = self.calculate_visible_column_indices(available_width);
1416        output.push_str(&format!("Visible columns: {:?}\n", visible_indices));
1417        output.push_str(&format!(
1418            "Last visible column: {}\n",
1419            visible_indices.last().copied().unwrap_or(0)
1420        ));
1421        output.push_str(&format!(
1422            "Shows last column ({}): {}\n",
1423            last_col_idx,
1424            visible_indices.contains(&last_col_idx)
1425        ));
1426
1427        output.push_str("============================================\n");
1428        output
1429    }
1430
1431    /// Get column names in DataView's preferred order (pinned first, then display order)
1432    /// This should be the single source of truth for column ordering from TUI perspective
1433    pub fn get_column_names_ordered(&self) -> Vec<String> {
1434        self.dataview.column_names()
1435    }
1436
1437    /// Get structured information about visible columns for rendering
1438    /// Returns (visible_indices, pinned_indices, scrollable_indices)
1439    pub fn get_visible_columns_info(
1440        &mut self,
1441        available_width: u16,
1442    ) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
1443        debug!(target: "viewport_manager", 
1444               "get_visible_columns_info CALLED with width={}, current_viewport={:?}", 
1445               available_width, self.viewport_cols);
1446
1447        // Get all visible column indices - use viewport-aware method
1448        let viewport_indices = self.calculate_visible_column_indices(available_width);
1449
1450        // Sort visible indices according to DataView's display order (pinned first)
1451        let display_order = self.dataview.get_display_columns();
1452        let mut visible_indices = Vec::new();
1453
1454        // Add columns in DataView's preferred order, but only if they're in the viewport
1455        for &col_idx in &display_order {
1456            if viewport_indices.contains(&col_idx) {
1457                visible_indices.push(col_idx);
1458            }
1459        }
1460
1461        // Get pinned column indices from DataView
1462        let pinned_columns = self.dataview.get_pinned_columns();
1463
1464        // Split visible columns into pinned and scrollable
1465        let mut pinned_visible = Vec::new();
1466        let mut scrollable_visible = Vec::new();
1467
1468        for &idx in &visible_indices {
1469            if pinned_columns.contains(&idx) {
1470                pinned_visible.push(idx);
1471            } else {
1472                scrollable_visible.push(idx);
1473            }
1474        }
1475
1476        debug!(target: "viewport_manager", 
1477               "get_visible_columns_info: viewport={:?} -> ordered={:?} ({} pinned, {} scrollable)",
1478               viewport_indices, visible_indices, pinned_visible.len(), scrollable_visible.len());
1479
1480        debug!(target: "viewport_manager", 
1481               "RENDERER DEBUG: viewport_indices={:?}, display_order={:?}, visible_indices={:?}",
1482               viewport_indices, display_order, visible_indices);
1483
1484        (visible_indices, pinned_visible, scrollable_visible)
1485    }
1486
1487    /// Calculate the actual X positions in terminal coordinates for visible columns
1488    /// Returns (column_indices, x_positions) where x_positions[i] is the starting x position for column_indices[i]
1489    pub fn calculate_column_x_positions(&mut self, available_width: u16) -> (Vec<usize>, Vec<u16>) {
1490        let visible_indices = self.calculate_visible_column_indices(available_width);
1491        let mut x_positions = Vec::new();
1492        let mut current_x = 0u16;
1493        let separator_width = 1u16;
1494
1495        for &col_idx in &visible_indices {
1496            x_positions.push(current_x);
1497            let width = self.width_calculator.get_column_width(
1498                &self.dataview,
1499                &self.viewport_rows,
1500                col_idx,
1501            );
1502            current_x += width + separator_width;
1503        }
1504
1505        (visible_indices, x_positions)
1506    }
1507
1508    /// Get the X position in terminal coordinates for a specific column (if visible)
1509    pub fn get_column_x_position(&mut self, column: usize, available_width: u16) -> Option<u16> {
1510        let (indices, positions) = self.calculate_column_x_positions(available_width);
1511        indices
1512            .iter()
1513            .position(|&idx| idx == column)
1514            .and_then(|pos| positions.get(pos).copied())
1515    }
1516
1517    /// Get visible column indices that fit in available width, preserving DataView's order
1518    pub fn calculate_visible_column_indices_ordered(&mut self, available_width: u16) -> Vec<usize> {
1519        // Width calculation is now handled by ColumnWidthCalculator
1520
1521        // Get DataView's preferred column order (pinned first)
1522        let ordered_columns = self.dataview.get_display_columns();
1523        let mut visible_indices = Vec::new();
1524        let mut used_width = 0u16;
1525        let separator_width = 1u16;
1526
1527        tracing::trace!(
1528            "ViewportManager: Starting ordered column layout. Available width: {}w, DataView order: {:?}",
1529            available_width,
1530            ordered_columns
1531        );
1532
1533        // Process columns in DataView's order (pinned first, then display order)
1534        for &col_idx in &ordered_columns {
1535            let width = self.width_calculator.get_column_width(
1536                &self.dataview,
1537                &self.viewport_rows,
1538                col_idx,
1539            );
1540
1541            if used_width + width + separator_width <= available_width {
1542                visible_indices.push(col_idx);
1543                used_width += width + separator_width;
1544                tracing::trace!(
1545                    "Added column {} in DataView order: {}w (total used: {}w)",
1546                    col_idx,
1547                    width,
1548                    used_width
1549                );
1550            } else {
1551                tracing::trace!(
1552                    "Skipped column {} ({}w) - would exceed available width",
1553                    col_idx,
1554                    width
1555                );
1556                break; // Stop when we run out of space, maintaining order
1557            }
1558        }
1559
1560        tracing::trace!(
1561            "Final ordered layout: {} columns visible {:?}, {}w used of {}w",
1562            visible_indices.len(),
1563            visible_indices,
1564            used_width,
1565            available_width
1566        );
1567
1568        visible_indices
1569    }
1570
1571    /// Convert a DataTable column index to its display position within the current visible columns
1572    /// Returns None if the column is not currently visible
1573    pub fn get_display_position_for_datatable_column(
1574        &mut self,
1575        datatable_column: usize,
1576        available_width: u16,
1577    ) -> Option<usize> {
1578        let visible_columns_info = self.get_visible_columns_info(available_width);
1579        let visible_indices = visible_columns_info.0;
1580
1581        // Find the position of the datatable column in the visible columns list
1582        let position = visible_indices
1583            .iter()
1584            .position(|&col| col == datatable_column);
1585
1586        debug!(target: "viewport_manager",
1587               "get_display_position_for_datatable_column: datatable_column={}, visible_indices={:?}, position={:?}",
1588               datatable_column, visible_indices, position);
1589
1590        position
1591    }
1592
1593    /// Get the exact crosshair column position for rendering
1594    /// This is the single source of truth for which column should be highlighted
1595    /// For now, current_column is still a DataTable index (due to Buffer storing DataTable indices)
1596    /// This converts it to the correct display position
1597    pub fn get_crosshair_column(
1598        &mut self,
1599        current_datatable_column: usize,
1600        available_width: u16,
1601    ) -> Option<usize> {
1602        // Get visible columns
1603        let visible_columns_info = self.get_visible_columns_info(available_width);
1604        let visible_indices = visible_columns_info.0;
1605
1606        // Find where this DataTable column appears in the visible columns
1607        let position = visible_indices
1608            .iter()
1609            .position(|&col| col == current_datatable_column);
1610
1611        debug!(target: "viewport_manager",
1612               "CROSSHAIR: current_datatable_column={}, visible_indices={:?}, crosshair_position={:?}",
1613               current_datatable_column, visible_indices, position);
1614
1615        position
1616    }
1617
1618    /// Get the complete visual display data for rendering
1619    /// Returns (headers, rows, widths) where everything is in visual order with no gaps
1620    /// This method works entirely in visual coordinate space
1621    pub fn get_visual_display(
1622        &mut self,
1623        available_width: u16,
1624        _row_indices: &[usize], // DEPRECATED - using internal viewport_rows instead
1625    ) -> (Vec<String>, Vec<Vec<String>>, Vec<u16>) {
1626        // Use our internal viewport_rows to determine what rows to display
1627        let row_indices: Vec<usize> = (self.viewport_rows.start..self.viewport_rows.end).collect();
1628
1629        debug!(target: "viewport_manager",
1630               "get_visual_display: Using viewport_rows {:?} -> row_indices: {:?} (first 5)",
1631               self.viewport_rows,
1632               row_indices.iter().take(5).collect::<Vec<_>>());
1633        // IMPORTANT: Use calculate_visible_column_indices to get the correct columns
1634        // This properly handles pinned columns that should always be visible
1635        let visible_column_indices = self.calculate_visible_column_indices(available_width);
1636
1637        tracing::debug!(
1638            "[RENDER_DEBUG] visible_column_indices from calculate: {:?}",
1639            visible_column_indices
1640        );
1641
1642        // Get ALL visual columns from DataView (already filtered for hidden columns)
1643        let all_headers = self.dataview.get_display_column_names();
1644        let display_columns = self.dataview.get_display_columns();
1645        let total_visual_columns = all_headers.len();
1646
1647        debug!(target: "viewport_manager",
1648               "get_visual_display: {} total visual columns, viewport: {:?}",
1649               total_visual_columns, self.viewport_cols);
1650
1651        // Build headers from the visible column indices (DataTable indices)
1652        let headers: Vec<String> = visible_column_indices
1653            .iter()
1654            .filter_map(|&dt_idx| {
1655                // Find the visual position for this DataTable index
1656                display_columns
1657                    .iter()
1658                    .position(|&x| x == dt_idx)
1659                    .and_then(|visual_idx| all_headers.get(visual_idx).cloned())
1660            })
1661            .collect();
1662
1663        tracing::debug!("[RENDER_DEBUG] headers: {:?}", headers);
1664
1665        // Get data from DataView in visual column order
1666        // IMPORTANT: row_indices contains display row indices (0-based positions in the result set)
1667        let visual_rows: Vec<Vec<String>> = row_indices
1668            .iter()
1669            .filter_map(|&display_row_idx| {
1670                // Get the full row in visual column order from DataView
1671                // display_row_idx is the position in the filtered/sorted result set
1672                let row_data = self.dataview.get_row_visual_values(display_row_idx);
1673                if let Some(ref full_row) = row_data {
1674                    // Debug first few and last few rows to track what we're actually getting
1675                    if display_row_idx < 5 || display_row_idx >= 19900 {
1676                        debug!(target: "viewport_manager",
1677                               "DATAVIEW FETCH: display_row_idx {} -> data: {:?} (first 3 cols)",
1678                               display_row_idx,
1679                               full_row.iter().take(3).collect::<Vec<_>>());
1680                    }
1681                }
1682                row_data.map(|full_row| {
1683                    // Extract the columns we need based on visible_column_indices
1684                    visible_column_indices
1685                        .iter()
1686                        .filter_map(|&dt_idx| {
1687                            // Find the visual position for this DataTable index
1688                            display_columns
1689                                .iter()
1690                                .position(|&x| x == dt_idx)
1691                                .and_then(|visual_idx| full_row.get(visual_idx).cloned())
1692                        })
1693                        .collect()
1694                })
1695            })
1696            .collect();
1697
1698        // Get the actual calculated widths for the visible columns
1699        let widths: Vec<u16> = visible_column_indices
1700            .iter()
1701            .map(|&dt_idx| {
1702                Some(self.width_calculator.get_column_width(
1703                    &self.dataview,
1704                    &self.viewport_rows,
1705                    dt_idx,
1706                ))
1707                .unwrap_or(DEFAULT_COL_WIDTH)
1708            })
1709            .collect();
1710
1711        debug!(target: "viewport_manager",
1712               "get_visual_display RESULT: {} headers, {} rows",
1713               headers.len(), visual_rows.len());
1714        if let Some(first_row) = visual_rows.first() {
1715            debug!(target: "viewport_manager",
1716                   "Alignment check (FIRST ROW): {:?}",
1717                   headers.iter().zip(first_row).take(5)
1718                       .map(|(h, v)| format!("{}: {}", h, v)).collect::<Vec<_>>());
1719        }
1720        if let Some(last_row) = visual_rows.last() {
1721            debug!(target: "viewport_manager",
1722                   "Alignment check (LAST ROW): {:?}",
1723                   headers.iter().zip(last_row).take(5)
1724                       .map(|(h, v)| format!("{}: {}", h, v)).collect::<Vec<_>>());
1725        }
1726
1727        (headers, visual_rows, widths)
1728    }
1729
1730    /// Get the column headers for the visible columns in the correct order
1731    /// This ensures headers align with the data columns when columns are hidden
1732    pub fn get_visible_column_headers(&self, visible_indices: &[usize]) -> Vec<String> {
1733        let mut headers = Vec::new();
1734
1735        // Get the column names directly from the DataTable source
1736        // The visible_indices are DataTable column indices, so we can use them directly
1737        let source = self.dataview.source();
1738        let all_column_names = source.column_names();
1739
1740        for &visual_idx in visible_indices {
1741            if visual_idx < all_column_names.len() {
1742                headers.push(all_column_names[visual_idx].clone());
1743            } else {
1744                // Fallback for invalid indices
1745                headers.push(format!("Column_{}", visual_idx));
1746            }
1747        }
1748
1749        debug!(target: "viewport_manager", 
1750               "get_visible_column_headers: indices={:?} -> headers={:?}", 
1751               visible_indices, headers);
1752
1753        headers
1754    }
1755
1756    /// Get crosshair column position for rendering when given a display position
1757    /// This is for the new architecture where Buffer stores display positions
1758    pub fn get_crosshair_column_for_display(
1759        &mut self,
1760        current_display_position: usize,
1761        available_width: u16,
1762    ) -> Option<usize> {
1763        // Get the display columns order from DataView
1764        let display_columns = self.dataview.get_display_columns();
1765
1766        // Validate the display position
1767        if current_display_position >= display_columns.len() {
1768            debug!(target: "viewport_manager",
1769                   "CROSSHAIR DISPLAY: display_position {} out of bounds (max {})",
1770                   current_display_position, display_columns.len());
1771            return None;
1772        }
1773
1774        // Get the DataTable column index for this display position
1775        let datatable_column = display_columns[current_display_position];
1776
1777        // Get visible columns for rendering
1778        let visible_columns_info = self.get_visible_columns_info(available_width);
1779        let visible_indices = visible_columns_info.0;
1780
1781        // Find where this DataTable column appears in the visible columns
1782        let position = visible_indices
1783            .iter()
1784            .position(|&col| col == datatable_column);
1785
1786        debug!(target: "viewport_manager",
1787               "CROSSHAIR DISPLAY: display_pos={} -> datatable_col={} -> visible_indices={:?} -> crosshair_pos={:?}",
1788               current_display_position, datatable_column, visible_indices, position);
1789
1790        position
1791    }
1792
1793    /// Calculate viewport efficiency metrics
1794    pub fn calculate_efficiency_metrics(&mut self, available_width: u16) -> ViewportEfficiency {
1795        // Get the visible columns
1796        let visible_indices = self.calculate_visible_column_indices(available_width);
1797
1798        // Calculate total width used
1799        let mut used_width = 0u16;
1800        let separator_width = 1u16;
1801
1802        for &col_idx in &visible_indices {
1803            let width = self.width_calculator.get_column_width(
1804                &self.dataview,
1805                &self.viewport_rows,
1806                col_idx,
1807            );
1808            used_width += width + separator_width;
1809        }
1810
1811        // Remove the last separator since it's not needed after the last column
1812        if !visible_indices.is_empty() {
1813            used_width = used_width.saturating_sub(separator_width);
1814        }
1815
1816        let wasted_space = available_width.saturating_sub(used_width);
1817
1818        // Find the next column that didn't fit
1819        let next_column_width = if !visible_indices.is_empty() {
1820            let last_visible = *visible_indices.last().unwrap();
1821            if last_visible + 1 < self.dataview.column_count() {
1822                Some(self.width_calculator.get_column_width(
1823                    &self.dataview,
1824                    &self.viewport_rows,
1825                    last_visible + 1,
1826                ))
1827            } else {
1828                None
1829            }
1830        } else {
1831            None
1832        };
1833
1834        // Find ALL columns that COULD fit in the wasted space
1835        let mut columns_that_could_fit = Vec::new();
1836        if wasted_space > MIN_COL_WIDTH + separator_width {
1837            let all_widths = self
1838                .width_calculator
1839                .get_all_column_widths(&self.dataview, &self.viewport_rows);
1840            for (idx, &width) in all_widths.iter().enumerate() {
1841                // Skip already visible columns
1842                if !visible_indices.contains(&idx) {
1843                    if width + separator_width <= wasted_space {
1844                        columns_that_could_fit.push((idx, width));
1845                    }
1846                }
1847            }
1848        }
1849
1850        let efficiency_percent = if available_width > 0 {
1851            ((used_width as f32 / available_width as f32) * 100.0) as u8
1852        } else {
1853            0
1854        };
1855
1856        ViewportEfficiency {
1857            available_width,
1858            used_width,
1859            wasted_space,
1860            efficiency_percent,
1861            visible_columns: visible_indices.len(),
1862            column_widths: visible_indices
1863                .iter()
1864                .map(|&idx| {
1865                    Some(self.width_calculator.get_column_width(
1866                        &self.dataview,
1867                        &self.viewport_rows,
1868                        idx,
1869                    ))
1870                    .unwrap_or(DEFAULT_COL_WIDTH)
1871                })
1872                .collect(),
1873            next_column_width,
1874            columns_that_could_fit,
1875        }
1876    }
1877
1878    /// Navigate to the first column (first scrollable column after pinned columns)
1879    /// This centralizes the logic for first column navigation
1880    pub fn navigate_to_first_column(&mut self) -> NavigationResult {
1881        // Check viewport lock - prevent scrolling
1882        if self.viewport_lock {
1883            // In viewport lock mode, just move to leftmost visible column
1884            self.crosshair_col = self.viewport_cols.start;
1885            return NavigationResult {
1886                column_position: self.crosshair_col,
1887                scroll_offset: self.viewport_cols.start,
1888                description: "Moved to first visible column (viewport locked)".to_string(),
1889                viewport_changed: false,
1890            };
1891        }
1892        // Get pinned column count from dataview
1893        let pinned_count = self.dataview.get_pinned_columns().len();
1894        let pinned_names = self.dataview.get_pinned_column_names();
1895
1896        // First scrollable column is at index = pinned_count
1897        let first_scrollable_column = pinned_count;
1898
1899        // Reset viewport to beginning (scroll offset = 0)
1900        let new_scroll_offset = 0;
1901        let old_scroll_offset = self.viewport_cols.start;
1902
1903        // Recalculate the entire viewport to show columns starting from new_scroll_offset
1904        let visible_indices = self
1905            .calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
1906        let viewport_end = if let Some(&last_idx) = visible_indices.last() {
1907            last_idx + 1
1908        } else {
1909            new_scroll_offset + 1
1910        };
1911
1912        // Update our internal viewport state
1913        self.viewport_cols = new_scroll_offset..viewport_end;
1914
1915        // Update crosshair to first scrollable column
1916        self.crosshair_col = first_scrollable_column;
1917
1918        // Create description
1919        let description = if pinned_count > 0 {
1920            format!(
1921                "First scrollable column selected (after {} pinned: {:?})",
1922                pinned_count, pinned_names
1923            )
1924        } else {
1925            "First column selected".to_string()
1926        };
1927
1928        let viewport_changed = old_scroll_offset != new_scroll_offset;
1929
1930        debug!(target: "viewport_manager", 
1931               "navigate_to_first_column: pinned={}, first_scrollable={}, crosshair_col={}, scroll_offset={}->{}",
1932               pinned_count, first_scrollable_column, self.crosshair_col, old_scroll_offset, new_scroll_offset);
1933
1934        NavigationResult {
1935            column_position: first_scrollable_column,
1936            scroll_offset: new_scroll_offset,
1937            description,
1938            viewport_changed,
1939        }
1940    }
1941
1942    /// Navigate to the last column (rightmost visible column)
1943    /// This centralizes the logic for last column navigation
1944    pub fn navigate_to_last_column(&mut self) -> NavigationResult {
1945        // Check viewport lock - prevent scrolling
1946        if self.viewport_lock {
1947            // In viewport lock mode, just move to rightmost visible column
1948            self.crosshair_col = self.viewport_cols.end.saturating_sub(1);
1949            return NavigationResult {
1950                column_position: self.crosshair_col,
1951                scroll_offset: self.viewport_cols.start,
1952                description: "Moved to last visible column (viewport locked)".to_string(),
1953                viewport_changed: false,
1954            };
1955        }
1956        // Get the display columns (visual order)
1957        let display_columns = self.dataview.get_display_columns();
1958        let total_visual_columns = display_columns.len();
1959
1960        if total_visual_columns == 0 {
1961            return NavigationResult {
1962                column_position: 0,
1963                scroll_offset: 0,
1964                description: "No columns available".to_string(),
1965                viewport_changed: false,
1966            };
1967        }
1968
1969        // Last column is at visual index total_visual_columns - 1
1970        let last_visual_column = total_visual_columns - 1;
1971
1972        // Update crosshair to last visual column
1973        self.crosshair_col = last_visual_column;
1974
1975        // Calculate the appropriate scroll offset to make the last column visible
1976        // We need to ensure the last column fits within the viewport
1977        let available_width = self.terminal_width;
1978        let pinned_count = self.dataview.get_pinned_columns().len();
1979
1980        // Calculate pinned width
1981        let mut pinned_width = 0u16;
1982        for i in 0..pinned_count {
1983            let col_idx = display_columns[i];
1984            let width = self.width_calculator.get_column_width(
1985                &self.dataview,
1986                &self.viewport_rows,
1987                col_idx,
1988            );
1989            pinned_width += width + 3; // separator width
1990        }
1991
1992        let available_for_scrollable = available_width.saturating_sub(pinned_width);
1993
1994        // Calculate the optimal scroll offset to show the last column
1995        let mut accumulated_width = 0u16;
1996        let mut new_scroll_offset = last_visual_column;
1997
1998        // Work backwards from the last column to find the best scroll position
1999        for visual_idx in (pinned_count..=last_visual_column).rev() {
2000            let col_idx = display_columns[visual_idx];
2001            let width = self.width_calculator.get_column_width(
2002                &self.dataview,
2003                &self.viewport_rows,
2004                col_idx,
2005            );
2006            accumulated_width += width + 3; // separator width
2007
2008            if accumulated_width > available_for_scrollable {
2009                // We've exceeded available width, use the next column as scroll start
2010                new_scroll_offset = visual_idx + 1;
2011                break;
2012            }
2013            new_scroll_offset = visual_idx;
2014        }
2015
2016        // Ensure scroll offset doesn't go below pinned columns
2017        new_scroll_offset = new_scroll_offset.max(pinned_count);
2018
2019        let old_scroll_offset = self.viewport_cols.start;
2020        let viewport_changed = old_scroll_offset != new_scroll_offset;
2021
2022        // Recalculate the entire viewport to show columns starting from new_scroll_offset
2023        let visible_indices = self
2024            .calculate_visible_column_indices_with_offset(self.terminal_width, new_scroll_offset);
2025        let viewport_end = if let Some(&last_idx) = visible_indices.last() {
2026            last_idx + 1
2027        } else {
2028            new_scroll_offset + 1
2029        };
2030
2031        // Update our internal viewport state
2032        self.viewport_cols = new_scroll_offset..viewport_end;
2033
2034        debug!(target: "viewport_manager", 
2035               "navigate_to_last_column: last_visual={}, scroll_offset={}->{}",
2036               last_visual_column, old_scroll_offset, new_scroll_offset);
2037
2038        NavigationResult {
2039            column_position: last_visual_column,
2040            scroll_offset: new_scroll_offset,
2041            description: format!("Last column selected (column {})", last_visual_column + 1),
2042            viewport_changed,
2043        }
2044    }
2045
2046    /// Navigate one column to the left with intelligent wrapping and scrolling
2047    /// This method handles everything: column movement, viewport tracking, and scrolling
2048    /// IMPORTANT: current_display_position is a logical display position (0,1,2,3...), NOT a DataTable index
2049    pub fn navigate_column_left(&mut self, current_display_position: usize) -> NavigationResult {
2050        // Check viewport lock first - prevent scrolling entirely
2051        if self.viewport_lock {
2052            debug!(target: "viewport_manager", 
2053                   "navigate_column_left: Viewport locked, crosshair_col={}, viewport={:?}",
2054                   self.crosshair_col, self.viewport_cols);
2055
2056            // In viewport lock mode, just move cursor left within current viewport
2057            if self.crosshair_col > self.viewport_cols.start {
2058                self.crosshair_col -= 1;
2059                return NavigationResult {
2060                    column_position: self.crosshair_col,
2061                    scroll_offset: self.viewport_cols.start,
2062                    description: "Moved within locked viewport".to_string(),
2063                    viewport_changed: false,
2064                };
2065            } else {
2066                // Already at left edge of locked viewport
2067                return NavigationResult {
2068                    column_position: self.crosshair_col,
2069                    scroll_offset: self.viewport_cols.start,
2070                    description: "At left edge of locked viewport".to_string(),
2071                    viewport_changed: false,
2072                };
2073            }
2074        }
2075
2076        // Get the DataView's display order (pinned columns first, then others)
2077        let display_columns = self.dataview.get_display_columns();
2078        let total_display_columns = display_columns.len();
2079
2080        debug!(target: "viewport_manager", 
2081               "navigate_column_left: current_display_pos={}, total_display={}, display_order={:?}", 
2082               current_display_position, total_display_columns, display_columns);
2083
2084        // Validate current position
2085        let current_display_index = if current_display_position < total_display_columns {
2086            current_display_position
2087        } else {
2088            0 // Reset to first if out of bounds
2089        };
2090
2091        debug!(target: "viewport_manager", 
2092               "navigate_column_left: using display_index={}", 
2093               current_display_index);
2094
2095        // Calculate new display position (move left in display order)
2096        // Vim-like behavior: don't wrap, stay at boundary
2097        if current_display_index == 0 {
2098            // Already at first column, don't move
2099            // Already at first column, return visual index 0
2100            return NavigationResult {
2101                column_position: 0, // Visual position, not DataTable index
2102                scroll_offset: self.viewport_cols.start,
2103                description: "Already at first column".to_string(),
2104                viewport_changed: false,
2105            };
2106        }
2107
2108        let new_display_index = current_display_index - 1;
2109
2110        // Get the actual DataTable column index from display order for internal operations
2111        let new_visual_column = display_columns
2112            .get(new_display_index)
2113            .copied()
2114            .unwrap_or_else(|| {
2115                display_columns
2116                    .get(current_display_index)
2117                    .copied()
2118                    .unwrap_or(0)
2119            });
2120
2121        let old_scroll_offset = self.viewport_cols.start;
2122
2123        // Don't pre-extend viewport - let set_current_column handle all viewport adjustments
2124        // This avoids the issue where we extend the viewport, then set_current_column thinks
2125        // the column is already visible and doesn't scroll
2126        debug!(target: "viewport_manager", 
2127               "navigate_column_left: moving to datatable_column={}, current viewport={:?}", 
2128               new_visual_column, self.viewport_cols);
2129
2130        // Use set_current_column to handle viewport adjustment automatically (this takes DataTable index)
2131        let viewport_changed = self.set_current_column(new_display_index);
2132
2133        // crosshair_col is already updated by set_current_column, no need to set it again
2134
2135        let column_names = self.dataview.column_names();
2136        let column_name = display_columns
2137            .get(new_display_index)
2138            .and_then(|&dt_idx| column_names.get(dt_idx))
2139            .map(|s| s.as_str())
2140            .unwrap_or("unknown");
2141        let description = format!(
2142            "Navigate left to column '{}' ({})",
2143            column_name,
2144            new_display_index + 1
2145        );
2146
2147        debug!(target: "viewport_manager", 
2148               "navigate_column_left: display_pos {}→{}, datatable_col: {}, scroll: {}→{}, viewport_changed={}", 
2149               current_display_index, new_display_index, new_visual_column,
2150               old_scroll_offset, self.viewport_cols.start, viewport_changed);
2151
2152        NavigationResult {
2153            column_position: new_display_index, // Return visual/display index
2154            scroll_offset: self.viewport_cols.start,
2155            description,
2156            viewport_changed,
2157        }
2158    }
2159
2160    /// Navigate one column to the right with intelligent wrapping and scrolling
2161    /// IMPORTANT: current_display_position is a logical display position (0,1,2,3...), NOT a DataTable index
2162    pub fn navigate_column_right(&mut self, current_display_position: usize) -> NavigationResult {
2163        debug!(target: "viewport_manager",
2164               "=== CRITICAL DEBUG: navigate_column_right CALLED ===");
2165        debug!(target: "viewport_manager",
2166               "Input current_display_position: {}", current_display_position);
2167        debug!(target: "viewport_manager",
2168               "Current crosshair_col: {}", self.crosshair_col);
2169        debug!(target: "viewport_manager",
2170               "Current viewport_cols: {:?}", self.viewport_cols);
2171        // Check viewport lock first - prevent scrolling entirely
2172        if self.viewport_lock {
2173            debug!(target: "viewport_manager", 
2174                   "navigate_column_right: Viewport locked, crosshair_col={}, viewport={:?}",
2175                   self.crosshair_col, self.viewport_cols);
2176
2177            // In viewport lock mode, just move cursor right within current viewport
2178            if self.crosshair_col < self.viewport_cols.end - 1 {
2179                self.crosshair_col += 1;
2180                return NavigationResult {
2181                    column_position: self.crosshair_col,
2182                    scroll_offset: self.viewport_cols.start,
2183                    description: "Moved within locked viewport".to_string(),
2184                    viewport_changed: false,
2185                };
2186            } else {
2187                // Already at right edge of locked viewport
2188                return NavigationResult {
2189                    column_position: self.crosshair_col,
2190                    scroll_offset: self.viewport_cols.start,
2191                    description: "At right edge of locked viewport".to_string(),
2192                    viewport_changed: false,
2193                };
2194            }
2195        }
2196
2197        let display_columns = self.dataview.get_display_columns();
2198        let total_display_columns = display_columns.len();
2199        let column_names = self.dataview.column_names();
2200
2201        // Enhanced logging to debug the external_id issue
2202        debug!(target: "viewport_manager", 
2203               "=== navigate_column_right DETAILED DEBUG ===");
2204        debug!(target: "viewport_manager", 
2205               "ENTRY: current_display_pos={}, total_display_columns={}", 
2206               current_display_position, total_display_columns);
2207        debug!(target: "viewport_manager",
2208               "display_columns (DataTable indices): {:?}", display_columns);
2209
2210        // Log column names at each position
2211        if current_display_position < display_columns.len() {
2212            let current_dt_idx = display_columns[current_display_position];
2213            let current_name = column_names
2214                .get(current_dt_idx)
2215                .map(|s| s.as_str())
2216                .unwrap_or("unknown");
2217            debug!(target: "viewport_manager",
2218                   "Current position {} -> column '{}' (dt_idx={})", 
2219                   current_display_position, current_name, current_dt_idx);
2220        }
2221
2222        if current_display_position + 1 < display_columns.len() {
2223            let next_dt_idx = display_columns[current_display_position + 1];
2224            let next_name = column_names
2225                .get(next_dt_idx)
2226                .map(|s| s.as_str())
2227                .unwrap_or("unknown");
2228            debug!(target: "viewport_manager",
2229                   "Next position {} -> column '{}' (dt_idx={})", 
2230                   current_display_position + 1, next_name, next_dt_idx);
2231        }
2232
2233        // Validate current position
2234        let current_display_index = if current_display_position < total_display_columns {
2235            current_display_position
2236        } else {
2237            debug!(target: "viewport_manager",
2238                   "WARNING: current_display_position {} >= total_display_columns {}, resetting to 0",
2239                   current_display_position, total_display_columns);
2240            0 // Reset to first if out of bounds
2241        };
2242
2243        debug!(target: "viewport_manager", 
2244               "Validated: current_display_index={}", 
2245               current_display_index);
2246
2247        // Calculate new display position (move right without wrapping)
2248        // Vim-like behavior: don't wrap, stay at boundary
2249        if current_display_index + 1 >= total_display_columns {
2250            // Already at last column, don't move
2251            let last_display_index = total_display_columns.saturating_sub(1);
2252            debug!(target: "viewport_manager",
2253                   "At last column boundary: current={}, total={}, returning last_display_index={}",
2254                   current_display_index, total_display_columns, last_display_index);
2255            return NavigationResult {
2256                column_position: last_display_index, // Return visual/display index
2257                scroll_offset: self.viewport_cols.start,
2258                description: "Already at last column".to_string(),
2259                viewport_changed: false,
2260            };
2261        }
2262
2263        let new_display_index = current_display_index + 1;
2264
2265        // Get the actual DataTable column index for the new position (for internal operations)
2266        let new_visual_column = display_columns
2267            .get(new_display_index)
2268            .copied()
2269            .unwrap_or_else(|| {
2270                // This fallback should never be hit since we already checked bounds
2271                tracing::error!(
2272                    "[NAV_ERROR] Failed to get display column at index {}, total={}",
2273                    new_display_index,
2274                    display_columns.len()
2275                );
2276                // Return the current column instead of wrapping to first
2277                display_columns
2278                    .get(current_display_index)
2279                    .copied()
2280                    .unwrap_or(0)
2281            });
2282
2283        debug!(target: "viewport_manager", 
2284               "navigate_column_right: display_pos {}→{}, new_visual_column={}",
2285               current_display_index, new_display_index, new_visual_column);
2286
2287        let old_scroll_offset = self.viewport_cols.start;
2288
2289        // Ensure the viewport includes the target column before checking visibility
2290        // This fixes the range issue where column N is not included in range start..N
2291        // Don't pre-extend viewport - let set_current_column handle all viewport adjustments
2292        // This avoids the issue where we extend the viewport, then set_current_column thinks
2293        // the column is already visible and doesn't scroll
2294        debug!(target: "viewport_manager", 
2295               "navigate_column_right: moving to datatable_column={}, current viewport={:?}", 
2296               new_visual_column, self.viewport_cols);
2297
2298        // Use set_current_column to handle viewport adjustment automatically
2299        // IMPORTANT: set_current_column expects a VISUAL index, and we're passing new_display_index which IS a visual index
2300        debug!(target: "viewport_manager", 
2301               "navigate_column_right: before set_current_column(visual_idx={}), viewport={:?}", 
2302               new_display_index, self.viewport_cols);
2303        let viewport_changed = self.set_current_column(new_display_index);
2304        debug!(target: "viewport_manager", 
2305               "navigate_column_right: after set_current_column(visual_idx={}), viewport={:?}, changed={}", 
2306               new_display_index, self.viewport_cols, viewport_changed);
2307
2308        // crosshair_col is already updated by set_current_column, no need to set it again
2309
2310        let column_names = self.dataview.column_names();
2311        let column_name = display_columns
2312            .get(new_display_index)
2313            .and_then(|&dt_idx| column_names.get(dt_idx))
2314            .map(|s| s.as_str())
2315            .unwrap_or("unknown");
2316        let description = format!(
2317            "Navigate right to column '{}' ({})",
2318            column_name,
2319            new_display_index + 1
2320        );
2321
2322        // Final logging with clear indication of what we're returning
2323        debug!(target: "viewport_manager", 
2324               "=== navigate_column_right RESULT ===");
2325        debug!(target: "viewport_manager",
2326               "Returning: column_position={} (visual/display index)", new_display_index);
2327        debug!(target: "viewport_manager",
2328               "Movement: {} -> {} (visual indices)", current_display_index, new_display_index);
2329        debug!(target: "viewport_manager",
2330               "Viewport: {:?}, changed={}", self.viewport_cols, viewport_changed);
2331        debug!(target: "viewport_manager",
2332               "Description: {}", description);
2333
2334        tracing::debug!("[NAV_DEBUG] Final result: column_position={} (visual/display idx), viewport_changed={}", 
2335                       new_display_index, viewport_changed);
2336        debug!(target: "viewport_manager", 
2337               "navigate_column_right EXIT: display_pos {}→{}, datatable_col: {}, viewport: {:?}, scroll: {}→{}, viewport_changed={}", 
2338               current_display_index, new_display_index, new_visual_column,
2339               self.viewport_cols, old_scroll_offset, self.viewport_cols.start, viewport_changed);
2340
2341        NavigationResult {
2342            column_position: new_display_index, // Return visual/display index
2343            scroll_offset: self.viewport_cols.start,
2344            description,
2345            viewport_changed,
2346        }
2347    }
2348
2349    /// Navigate one page down in the data
2350    pub fn page_down(&mut self) -> RowNavigationResult {
2351        let total_rows = self.dataview.row_count();
2352        // Calculate visible rows (viewport height)
2353        let visible_rows = self.terminal_height.saturating_sub(6) as usize; // Account for headers, borders, status
2354
2355        debug!(target: "viewport_manager", 
2356               "page_down: crosshair_row={}, total_rows={}, visible_rows={}, current_viewport_rows={:?}", 
2357               self.crosshair_row, total_rows, visible_rows, self.viewport_rows);
2358
2359        // Check viewport lock first - prevent scrolling entirely
2360        if self.viewport_lock {
2361            debug!(target: "viewport_manager", 
2362                   "page_down: Viewport locked, moving within current viewport");
2363            // In viewport lock mode, move to bottom of current viewport
2364            let new_row = self
2365                .viewport_rows
2366                .end
2367                .saturating_sub(1)
2368                .min(total_rows.saturating_sub(1));
2369            self.crosshair_row = new_row;
2370            return RowNavigationResult {
2371                row_position: new_row,
2372                row_scroll_offset: self.viewport_rows.start,
2373                description: format!(
2374                    "Page down within locked viewport: row {} → {}",
2375                    self.crosshair_row + 1,
2376                    new_row + 1
2377                ),
2378                viewport_changed: false,
2379            };
2380        }
2381
2382        // Check cursor lock - scroll viewport but keep cursor at same relative position
2383        if self.cursor_lock {
2384            if let Some(lock_position) = self.cursor_lock_position {
2385                debug!(target: "viewport_manager", 
2386                       "page_down: Cursor locked at position {}", lock_position);
2387
2388                // Calculate new viewport position
2389                let old_scroll_offset = self.viewport_rows.start;
2390                let max_scroll = total_rows.saturating_sub(visible_rows);
2391                let new_scroll_offset = (old_scroll_offset + visible_rows).min(max_scroll);
2392
2393                if new_scroll_offset == old_scroll_offset {
2394                    // Can't scroll further
2395                    return RowNavigationResult {
2396                        row_position: self.crosshair_row,
2397                        row_scroll_offset: old_scroll_offset,
2398                        description: "Already at bottom".to_string(),
2399                        viewport_changed: false,
2400                    };
2401                }
2402
2403                // Update viewport
2404                self.viewport_rows =
2405                    new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2406
2407                // Keep crosshair at same relative position
2408                self.crosshair_row =
2409                    (new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
2410
2411                return RowNavigationResult {
2412                    row_position: self.crosshair_row,
2413                    row_scroll_offset: new_scroll_offset,
2414                    description: format!(
2415                        "Page down with cursor lock (viewport {} → {})",
2416                        old_scroll_offset + 1,
2417                        new_scroll_offset + 1
2418                    ),
2419                    viewport_changed: true,
2420                };
2421            }
2422        }
2423
2424        // Normal page down behavior
2425        // Calculate new row position (move down by one page) using ViewportManager's crosshair
2426        let new_row = (self.crosshair_row + visible_rows).min(total_rows.saturating_sub(1));
2427        self.crosshair_row = new_row;
2428
2429        // Calculate new scroll offset to keep new position visible
2430        let old_scroll_offset = self.viewport_rows.start;
2431        let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
2432            // Need to scroll down
2433            (new_row + 1).saturating_sub(visible_rows)
2434        } else {
2435            // Keep current scroll
2436            old_scroll_offset
2437        };
2438
2439        // Update viewport
2440        self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2441        let viewport_changed = new_scroll_offset != old_scroll_offset;
2442
2443        let description = format!(
2444            "Page down: row {} → {} (of {})",
2445            self.crosshair_row + 1,
2446            new_row + 1,
2447            total_rows
2448        );
2449
2450        debug!(target: "viewport_manager", 
2451               "page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}", 
2452               new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2453
2454        RowNavigationResult {
2455            row_position: new_row,
2456            row_scroll_offset: new_scroll_offset,
2457            description,
2458            viewport_changed,
2459        }
2460    }
2461
2462    /// Navigate one page up in the data
2463    pub fn page_up(&mut self) -> RowNavigationResult {
2464        let total_rows = self.dataview.row_count();
2465        // Calculate visible rows (viewport height)
2466        let visible_rows = self.terminal_height.saturating_sub(6) as usize; // Account for headers, borders, status
2467
2468        debug!(target: "viewport_manager", 
2469               "page_up: crosshair_row={}, visible_rows={}, current_viewport_rows={:?}", 
2470               self.crosshair_row, visible_rows, self.viewport_rows);
2471
2472        // Check viewport lock first - prevent scrolling entirely
2473        if self.viewport_lock {
2474            debug!(target: "viewport_manager", 
2475                   "page_up: Viewport locked, moving within current viewport");
2476            // In viewport lock mode, move to top of current viewport
2477            let new_row = self.viewport_rows.start;
2478            self.crosshair_row = new_row;
2479            return RowNavigationResult {
2480                row_position: new_row,
2481                row_scroll_offset: self.viewport_rows.start,
2482                description: format!(
2483                    "Page up within locked viewport: row {} → {}",
2484                    self.crosshair_row + 1,
2485                    new_row + 1
2486                ),
2487                viewport_changed: false,
2488            };
2489        }
2490
2491        // Check cursor lock - scroll viewport but keep cursor at same relative position
2492        if self.cursor_lock {
2493            if let Some(lock_position) = self.cursor_lock_position {
2494                debug!(target: "viewport_manager", 
2495                       "page_up: Cursor locked at position {}", lock_position);
2496
2497                // Calculate new viewport position
2498                let old_scroll_offset = self.viewport_rows.start;
2499                let new_scroll_offset = old_scroll_offset.saturating_sub(visible_rows);
2500
2501                if new_scroll_offset == old_scroll_offset {
2502                    // Can't scroll further
2503                    return RowNavigationResult {
2504                        row_position: self.crosshair_row,
2505                        row_scroll_offset: old_scroll_offset,
2506                        description: "Already at top".to_string(),
2507                        viewport_changed: false,
2508                    };
2509                }
2510
2511                // Update viewport
2512                self.viewport_rows =
2513                    new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2514
2515                // Keep crosshair at same relative position
2516                self.crosshair_row = new_scroll_offset + lock_position;
2517
2518                return RowNavigationResult {
2519                    row_position: self.crosshair_row,
2520                    row_scroll_offset: new_scroll_offset,
2521                    description: format!(
2522                        "Page up with cursor lock (viewport {} → {})",
2523                        old_scroll_offset + 1,
2524                        new_scroll_offset + 1
2525                    ),
2526                    viewport_changed: true,
2527                };
2528            }
2529        }
2530
2531        // Normal page up behavior
2532        // Calculate new row position (move up by one page) using ViewportManager's crosshair
2533        let new_row = self.crosshair_row.saturating_sub(visible_rows);
2534        self.crosshair_row = new_row;
2535
2536        // Calculate new scroll offset to keep new position visible
2537        let old_scroll_offset = self.viewport_rows.start;
2538        let new_scroll_offset = if new_row < self.viewport_rows.start {
2539            // Need to scroll up
2540            new_row
2541        } else {
2542            // Keep current scroll
2543            old_scroll_offset
2544        };
2545
2546        // Update viewport
2547        self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2548        let viewport_changed = new_scroll_offset != old_scroll_offset;
2549
2550        let description = format!("Page up: row {} → {}", self.crosshair_row + 1, new_row + 1);
2551
2552        debug!(target: "viewport_manager", 
2553               "page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}", 
2554               new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2555
2556        RowNavigationResult {
2557            row_position: new_row,
2558            row_scroll_offset: new_scroll_offset,
2559            description,
2560            viewport_changed,
2561        }
2562    }
2563
2564    /// Navigate half page down in the data
2565    pub fn half_page_down(&mut self) -> RowNavigationResult {
2566        let total_rows = self.dataview.row_count();
2567        // Calculate visible rows (viewport height)
2568        let visible_rows = self.terminal_height.saturating_sub(6) as usize; // Account for headers, borders, status
2569        let half_page = visible_rows / 2;
2570
2571        debug!(target: "viewport_manager", 
2572               "half_page_down: crosshair_row={}, total_rows={}, half_page={}, current_viewport_rows={:?}", 
2573               self.crosshair_row, total_rows, half_page, self.viewport_rows);
2574
2575        // Check viewport lock first - prevent scrolling entirely
2576        if self.viewport_lock {
2577            debug!(target: "viewport_manager", 
2578                   "half_page_down: Viewport locked, moving within current viewport");
2579            // In viewport lock mode, move to bottom of current viewport
2580            let new_row = self
2581                .viewport_rows
2582                .end
2583                .saturating_sub(1)
2584                .min(total_rows.saturating_sub(1));
2585            self.crosshair_row = new_row;
2586            return RowNavigationResult {
2587                row_position: new_row,
2588                row_scroll_offset: self.viewport_rows.start,
2589                description: format!(
2590                    "Half page down within locked viewport: row {} → {}",
2591                    self.crosshair_row + 1,
2592                    new_row + 1
2593                ),
2594                viewport_changed: false,
2595            };
2596        }
2597
2598        // Check cursor lock - scroll viewport but keep cursor at same relative position
2599        if self.cursor_lock {
2600            if let Some(lock_position) = self.cursor_lock_position {
2601                debug!(target: "viewport_manager", 
2602                       "half_page_down: Cursor locked at position {}", lock_position);
2603
2604                // Calculate new viewport position
2605                let old_scroll_offset = self.viewport_rows.start;
2606                let max_scroll = total_rows.saturating_sub(visible_rows);
2607                let new_scroll_offset = (old_scroll_offset + half_page).min(max_scroll);
2608
2609                if new_scroll_offset == old_scroll_offset {
2610                    // Can't scroll further
2611                    return RowNavigationResult {
2612                        row_position: self.crosshair_row,
2613                        row_scroll_offset: old_scroll_offset,
2614                        description: "Already at bottom".to_string(),
2615                        viewport_changed: false,
2616                    };
2617                }
2618
2619                // Update viewport
2620                self.viewport_rows =
2621                    new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2622
2623                // Keep crosshair at same relative position
2624                self.crosshair_row =
2625                    (new_scroll_offset + lock_position).min(total_rows.saturating_sub(1));
2626
2627                return RowNavigationResult {
2628                    row_position: self.crosshair_row,
2629                    row_scroll_offset: new_scroll_offset,
2630                    description: format!(
2631                        "Half page down with cursor lock (viewport {} → {})",
2632                        old_scroll_offset + 1,
2633                        new_scroll_offset + 1
2634                    ),
2635                    viewport_changed: true,
2636                };
2637            }
2638        }
2639
2640        // Normal half page down behavior
2641        // Calculate new row position (move down by half page) using ViewportManager's crosshair
2642        let new_row = (self.crosshair_row + half_page).min(total_rows.saturating_sub(1));
2643        self.crosshair_row = new_row;
2644
2645        // Calculate new scroll offset to keep new position visible
2646        let old_scroll_offset = self.viewport_rows.start;
2647        let new_scroll_offset = if new_row >= self.viewport_rows.start + visible_rows {
2648            // Need to scroll down
2649            (new_row + 1).saturating_sub(visible_rows)
2650        } else {
2651            // Keep current scroll
2652            old_scroll_offset
2653        };
2654
2655        // Update viewport
2656        self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2657        let viewport_changed = new_scroll_offset != old_scroll_offset;
2658
2659        let description = format!(
2660            "Half page down: row {} → {} (of {})",
2661            self.crosshair_row + 1 - half_page.min(self.crosshair_row),
2662            new_row + 1,
2663            total_rows
2664        );
2665
2666        debug!(target: "viewport_manager", 
2667               "half_page_down result: new_row={}, scroll_offset={}→{}, viewport_changed={}", 
2668               new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2669
2670        RowNavigationResult {
2671            row_position: new_row,
2672            row_scroll_offset: new_scroll_offset,
2673            description,
2674            viewport_changed,
2675        }
2676    }
2677
2678    /// Navigate half page up in the data
2679    pub fn half_page_up(&mut self) -> RowNavigationResult {
2680        let total_rows = self.dataview.row_count();
2681        // Calculate visible rows (viewport height)
2682        let visible_rows = self.terminal_height.saturating_sub(6) as usize; // Account for headers, borders, status
2683        let half_page = visible_rows / 2;
2684
2685        debug!(target: "viewport_manager", 
2686               "half_page_up: crosshair_row={}, half_page={}, current_viewport_rows={:?}", 
2687               self.crosshair_row, half_page, self.viewport_rows);
2688
2689        // Check viewport lock first - prevent scrolling entirely
2690        if self.viewport_lock {
2691            debug!(target: "viewport_manager", 
2692                   "half_page_up: Viewport locked, moving within current viewport");
2693            // In viewport lock mode, move to top of current viewport
2694            let new_row = self.viewport_rows.start;
2695            self.crosshair_row = new_row;
2696            return RowNavigationResult {
2697                row_position: new_row,
2698                row_scroll_offset: self.viewport_rows.start,
2699                description: format!(
2700                    "Half page up within locked viewport: row {} → {}",
2701                    self.crosshair_row + 1,
2702                    new_row + 1
2703                ),
2704                viewport_changed: false,
2705            };
2706        }
2707
2708        // Check cursor lock - scroll viewport but keep cursor at same relative position
2709        if self.cursor_lock {
2710            if let Some(lock_position) = self.cursor_lock_position {
2711                debug!(target: "viewport_manager", 
2712                       "half_page_up: Cursor locked at position {}", lock_position);
2713
2714                // Calculate new viewport position
2715                let old_scroll_offset = self.viewport_rows.start;
2716                let new_scroll_offset = old_scroll_offset.saturating_sub(half_page);
2717
2718                if new_scroll_offset == old_scroll_offset {
2719                    // Can't scroll further
2720                    return RowNavigationResult {
2721                        row_position: self.crosshair_row,
2722                        row_scroll_offset: old_scroll_offset,
2723                        description: "Already at top".to_string(),
2724                        viewport_changed: false,
2725                    };
2726                }
2727
2728                // Update viewport
2729                self.viewport_rows =
2730                    new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2731
2732                // Keep crosshair at same relative position
2733                self.crosshair_row = new_scroll_offset + lock_position;
2734
2735                return RowNavigationResult {
2736                    row_position: self.crosshair_row,
2737                    row_scroll_offset: new_scroll_offset,
2738                    description: format!(
2739                        "Half page up with cursor lock (viewport {} → {})",
2740                        old_scroll_offset + 1,
2741                        new_scroll_offset + 1
2742                    ),
2743                    viewport_changed: true,
2744                };
2745            }
2746        }
2747
2748        // Normal half page up behavior
2749        // Calculate new row position (move up by half page) using ViewportManager's crosshair
2750        let new_row = self.crosshair_row.saturating_sub(half_page);
2751        self.crosshair_row = new_row;
2752
2753        // Calculate new scroll offset to keep new position visible
2754        let old_scroll_offset = self.viewport_rows.start;
2755        let new_scroll_offset = if new_row < self.viewport_rows.start {
2756            // Need to scroll up
2757            new_row
2758        } else {
2759            // Keep current scroll
2760            old_scroll_offset
2761        };
2762
2763        // Update viewport
2764        self.viewport_rows = new_scroll_offset..(new_scroll_offset + visible_rows).min(total_rows);
2765        let viewport_changed = new_scroll_offset != old_scroll_offset;
2766
2767        let description = format!(
2768            "Half page up: row {} → {}",
2769            self.crosshair_row + half_page + 1,
2770            new_row + 1
2771        );
2772
2773        debug!(target: "viewport_manager", 
2774               "half_page_up result: new_row={}, scroll_offset={}→{}, viewport_changed={}", 
2775               new_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2776
2777        RowNavigationResult {
2778            row_position: new_row,
2779            row_scroll_offset: new_scroll_offset,
2780            description,
2781            viewport_changed,
2782        }
2783    }
2784
2785    /// Navigate to the last row in the data (like vim 'G' command)
2786    pub fn navigate_to_last_row(&mut self, total_rows: usize) -> RowNavigationResult {
2787        // Check viewport lock - prevent scrolling
2788        if self.viewport_lock {
2789            // In viewport lock mode, just move to bottom of current viewport
2790            let last_visible = self
2791                .viewport_rows
2792                .end
2793                .saturating_sub(1)
2794                .min(total_rows.saturating_sub(1));
2795            self.crosshair_row = last_visible;
2796            return RowNavigationResult {
2797                row_position: self.crosshair_row,
2798                row_scroll_offset: self.viewport_rows.start,
2799                description: "Moved to last visible row (viewport locked)".to_string(),
2800                viewport_changed: false,
2801            };
2802        }
2803        if total_rows == 0 {
2804            return RowNavigationResult {
2805                row_position: 0,
2806                row_scroll_offset: 0,
2807                description: "No rows to navigate".to_string(),
2808                viewport_changed: false,
2809            };
2810        }
2811
2812        // Get the actual visible rows from our current viewport
2813        // terminal_height should already account for UI chrome
2814        let visible_rows = (self.terminal_height as usize).max(10);
2815
2816        // The last row index
2817        let last_row = total_rows - 1;
2818
2819        // Calculate scroll offset to show the last row at the bottom of the viewport
2820        // We want the last row visible at the bottom, so start the viewport such that
2821        // the last row appears at the last position
2822        let new_scroll_offset = if total_rows > visible_rows {
2823            total_rows - visible_rows
2824        } else {
2825            0
2826        };
2827
2828        debug!(target: "viewport_manager", 
2829               "navigate_to_last_row: total_rows={}, last_row={}, visible_rows={}, new_scroll_offset={}", 
2830               total_rows, last_row, visible_rows, new_scroll_offset);
2831
2832        // Check if viewport actually changed
2833        let old_scroll_offset = self.viewport_rows.start;
2834        let viewport_changed = new_scroll_offset != old_scroll_offset;
2835
2836        // Update viewport to show the last rows
2837        self.viewport_rows = new_scroll_offset..total_rows.min(new_scroll_offset + visible_rows);
2838
2839        // Update crosshair to be at the last row
2840        // The crosshair position is the absolute row in the data
2841        self.crosshair_row = last_row;
2842
2843        let description = format!("Jumped to last row ({}/{})", last_row + 1, total_rows);
2844
2845        debug!(target: "viewport_manager", 
2846               "navigate_to_last_row result: row={}, crosshair_row={}, scroll_offset={}→{}, viewport_changed={}", 
2847               last_row, self.crosshair_row, old_scroll_offset, new_scroll_offset, viewport_changed);
2848
2849        RowNavigationResult {
2850            row_position: last_row,
2851            row_scroll_offset: new_scroll_offset,
2852            description,
2853            viewport_changed,
2854        }
2855    }
2856
2857    /// Navigate to the first row in the data (like vim 'gg' command)
2858    pub fn navigate_to_first_row(&mut self, total_rows: usize) -> RowNavigationResult {
2859        // Check viewport lock - prevent scrolling
2860        if self.viewport_lock {
2861            // In viewport lock mode, just move to top of current viewport
2862            self.crosshair_row = self.viewport_rows.start;
2863            return RowNavigationResult {
2864                row_position: self.crosshair_row,
2865                row_scroll_offset: self.viewport_rows.start,
2866                description: "Moved to first visible row (viewport locked)".to_string(),
2867                viewport_changed: false,
2868            };
2869        }
2870        if total_rows == 0 {
2871            return RowNavigationResult {
2872                row_position: 0,
2873                row_scroll_offset: 0,
2874                description: "No rows to navigate".to_string(),
2875                viewport_changed: false,
2876            };
2877        }
2878
2879        // Get the actual visible rows from our current viewport
2880        // terminal_height should already account for UI chrome
2881        let visible_rows = (self.terminal_height as usize).max(10);
2882
2883        // First row is always 0
2884        let first_row = 0;
2885
2886        // Scroll offset should be 0 to show the first row at the top
2887        let new_scroll_offset = 0;
2888
2889        debug!(target: "viewport_manager", 
2890               "navigate_to_first_row: total_rows={}, visible_rows={}", 
2891               total_rows, visible_rows);
2892
2893        // Check if viewport actually changed
2894        let old_scroll_offset = self.viewport_rows.start;
2895        let viewport_changed = new_scroll_offset != old_scroll_offset;
2896
2897        // Update viewport to show the first rows
2898        self.viewport_rows = 0..visible_rows.min(total_rows);
2899
2900        // Update crosshair to be at the first row
2901        self.crosshair_row = first_row;
2902
2903        let description = format!("Jumped to first row (1/{})", total_rows);
2904
2905        debug!(target: "viewport_manager", 
2906               "navigate_to_first_row result: row=0, crosshair_row={}, scroll_offset={}→0, viewport_changed={}", 
2907               self.crosshair_row, old_scroll_offset, viewport_changed);
2908
2909        RowNavigationResult {
2910            row_position: first_row,
2911            row_scroll_offset: new_scroll_offset,
2912            description,
2913            viewport_changed,
2914        }
2915    }
2916
2917    /// Navigate to the top of the current viewport (H in vim)
2918    pub fn navigate_to_viewport_top(&mut self) -> RowNavigationResult {
2919        let top_row = self.viewport_rows.start;
2920        let old_row = self.crosshair_row;
2921
2922        // Move crosshair to top of viewport
2923        self.crosshair_row = top_row;
2924
2925        let description = format!("Moved to viewport top (row {})", top_row + 1);
2926
2927        debug!(target: "viewport_manager", 
2928               "navigate_to_viewport_top: crosshair {} -> {}", 
2929               old_row, self.crosshair_row);
2930
2931        RowNavigationResult {
2932            row_position: self.crosshair_row,
2933            row_scroll_offset: self.viewport_rows.start,
2934            description,
2935            viewport_changed: false, // Viewport doesn't change, only crosshair moves
2936        }
2937    }
2938
2939    /// Navigate to the middle of the current viewport (M in vim)
2940    pub fn navigate_to_viewport_middle(&mut self) -> RowNavigationResult {
2941        // Calculate the middle of the viewport (viewport now only contains data rows)
2942        let viewport_height = self.viewport_rows.end - self.viewport_rows.start;
2943        let middle_offset = viewport_height / 2;
2944        let middle_row = self.viewport_rows.start + middle_offset;
2945        let old_row = self.crosshair_row;
2946
2947        // Move crosshair to middle of viewport
2948        self.crosshair_row = middle_row;
2949
2950        let description = format!("Moved to viewport middle (row {})", middle_row + 1);
2951
2952        debug!(target: "viewport_manager", 
2953               "navigate_to_viewport_middle: crosshair {} -> {}", 
2954               old_row, self.crosshair_row);
2955
2956        RowNavigationResult {
2957            row_position: self.crosshair_row,
2958            row_scroll_offset: self.viewport_rows.start,
2959            description,
2960            viewport_changed: false, // Viewport doesn't change, only crosshair moves
2961        }
2962    }
2963
2964    /// Navigate to the bottom of the current viewport (L in vim)
2965    pub fn navigate_to_viewport_bottom(&mut self) -> RowNavigationResult {
2966        // Bottom row is the last visible row in the viewport
2967        // viewport_rows now represents only data rows (no table chrome)
2968        let bottom_row = self.viewport_rows.end.saturating_sub(1);
2969        let old_row = self.crosshair_row;
2970
2971        // Move crosshair to bottom of viewport
2972        self.crosshair_row = bottom_row;
2973
2974        let description = format!("Moved to viewport bottom (row {})", bottom_row + 1);
2975
2976        debug!(target: "viewport_manager", 
2977               "navigate_to_viewport_bottom: crosshair {} -> {}", 
2978               old_row, self.crosshair_row);
2979
2980        RowNavigationResult {
2981            row_position: self.crosshair_row,
2982            row_scroll_offset: self.viewport_rows.start,
2983            description,
2984            viewport_changed: false, // Viewport doesn't change, only crosshair moves
2985        }
2986    }
2987
2988    /// Toggle viewport lock - when locked, crosshair stays at same viewport position while scrolling
2989    /// Toggle cursor lock - cursor stays at same viewport position while scrolling
2990    pub fn toggle_cursor_lock(&mut self) -> (bool, String) {
2991        self.cursor_lock = !self.cursor_lock;
2992
2993        if self.cursor_lock {
2994            // Calculate and store the relative position within viewport
2995            let relative_position = self.crosshair_row.saturating_sub(self.viewport_rows.start);
2996            self.cursor_lock_position = Some(relative_position);
2997
2998            let description = format!(
2999                "Cursor lock: ON (locked at viewport position {})",
3000                relative_position + 1
3001            );
3002            debug!(target: "viewport_manager", 
3003                   "Cursor lock enabled: crosshair at viewport position {}", 
3004                   relative_position);
3005            (true, description)
3006        } else {
3007            self.cursor_lock_position = None;
3008            let description = "Cursor lock: OFF".to_string();
3009            debug!(target: "viewport_manager", "Cursor lock disabled");
3010            (false, description)
3011        }
3012    }
3013
3014    /// Toggle viewport lock - prevents scrolling and constrains cursor to current viewport
3015    pub fn toggle_viewport_lock(&mut self) -> (bool, String) {
3016        self.viewport_lock = !self.viewport_lock;
3017
3018        if self.viewport_lock {
3019            // Store current viewport boundaries
3020            self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
3021
3022            let description = format!(
3023                "Viewport lock: ON (no scrolling, cursor constrained to rows {}-{})",
3024                self.viewport_rows.start + 1,
3025                self.viewport_rows.end
3026            );
3027            debug!(target: "viewport_manager", 
3028                   "VIEWPORT LOCK ENABLED: boundaries {:?}, crosshair={}, viewport={:?}", 
3029                   self.viewport_lock_boundaries, self.crosshair_row, self.viewport_rows);
3030            (true, description)
3031        } else {
3032            self.viewport_lock_boundaries = None;
3033            let description = "Viewport lock: OFF (normal scrolling)".to_string();
3034            debug!(target: "viewport_manager", "VIEWPORT LOCK DISABLED");
3035            (false, description)
3036        }
3037    }
3038
3039    /// Check if cursor is locked
3040    pub fn is_cursor_locked(&self) -> bool {
3041        self.cursor_lock
3042    }
3043
3044    /// Check if viewport is locked
3045    pub fn is_viewport_locked(&self) -> bool {
3046        self.viewport_lock
3047    }
3048
3049    /// Lock the viewport to prevent scrolling
3050    pub fn lock_viewport(&mut self) {
3051        if !self.viewport_lock {
3052            self.viewport_lock = true;
3053            self.viewport_lock_boundaries = Some(self.viewport_rows.clone());
3054            debug!(target: "viewport_manager", "Viewport locked: rows {}-{}", 
3055                   self.viewport_rows.start + 1, self.viewport_rows.end);
3056        }
3057    }
3058
3059    /// Unlock the viewport to allow scrolling
3060    pub fn unlock_viewport(&mut self) {
3061        if self.viewport_lock {
3062            self.viewport_lock = false;
3063            self.viewport_lock_boundaries = None;
3064            debug!(target: "viewport_manager", "Viewport unlocked");
3065        }
3066    }
3067
3068    /// Move the current column left in the display order (swap with previous column)
3069    pub fn reorder_column_left(&mut self, current_column: usize) -> ColumnReorderResult {
3070        debug!(target: "viewport_manager",
3071            "reorder_column_left: current_column={}, viewport={:?}",
3072            current_column, self.viewport_cols
3073        );
3074
3075        // Get the current column count
3076        let column_count = self.dataview.column_count();
3077
3078        if current_column >= column_count {
3079            return ColumnReorderResult {
3080                new_column_position: current_column,
3081                description: "Invalid column position".to_string(),
3082                success: false,
3083            };
3084        }
3085
3086        // Get pinned columns count to respect boundaries
3087        let pinned_count = self.dataview.get_pinned_columns().len();
3088
3089        debug!(target: "viewport_manager",
3090            "Before move: column_count={}, pinned_count={}, current_column={}",
3091            column_count, pinned_count, current_column
3092        );
3093
3094        // Clone the DataView, modify it, and replace the Arc
3095        let mut new_dataview = (*self.dataview).clone();
3096
3097        // Delegate to DataView's move_column_left - it handles pinned column logic
3098        let success = new_dataview.move_column_left(current_column);
3099
3100        if success {
3101            // Replace the Arc with the modified DataView
3102            self.dataview = Arc::new(new_dataview);
3103        }
3104
3105        if success {
3106            self.invalidate_cache(); // Column order changed, need to recalculate widths
3107
3108            // Determine new cursor position based on the move operation
3109            let wrapped_to_end =
3110                current_column == 0 || (current_column == pinned_count && pinned_count > 0);
3111            let new_position = if wrapped_to_end {
3112                // Column wrapped to end
3113                column_count - 1
3114            } else {
3115                // Normal swap with previous
3116                current_column - 1
3117            };
3118
3119            let column_names = self.dataview.column_names();
3120            let column_name = column_names
3121                .get(new_position)
3122                .map(|s| s.as_str())
3123                .unwrap_or("?");
3124
3125            debug!(target: "viewport_manager",
3126                "After move: new_position={}, wrapped_to_end={}, column_name={}",
3127                new_position, wrapped_to_end, column_name
3128            );
3129
3130            // Adjust viewport to keep the moved column visible
3131            if wrapped_to_end {
3132                // Calculate optimal offset to show the last column
3133                let optimal_offset = self.calculate_optimal_offset_for_last_column(
3134                    self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH),
3135                );
3136                debug!(target: "viewport_manager",
3137                    "Column wrapped to end! Adjusting viewport from {:?} to {}..{}",
3138                    self.viewport_cols, optimal_offset, self.dataview.column_count()
3139                );
3140                self.viewport_cols = optimal_offset..self.dataview.column_count();
3141            } else {
3142                // Check if the new position is outside the current viewport
3143                if !self.viewport_cols.contains(&new_position) {
3144                    // Column moved outside viewport, adjust to show it
3145                    let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3146
3147                    // Calculate how many columns we can fit starting from the new position
3148                    let columns_that_fit =
3149                        self.calculate_columns_that_fit(new_position, terminal_width);
3150
3151                    // Adjust viewport to show the column at its new position
3152                    let new_start = if new_position < self.viewport_cols.start {
3153                        // Column moved to the left, scroll left
3154                        new_position
3155                    } else {
3156                        // Column moved to the right (shouldn't happen in move_left, but handle it)
3157                        new_position.saturating_sub(columns_that_fit - 1)
3158                    };
3159
3160                    let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
3161                    self.viewport_cols = new_start..new_end;
3162
3163                    debug!(target: "viewport_manager",
3164                        "Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
3165                        new_start, new_end, column_name, new_position
3166                    );
3167                }
3168            }
3169
3170            // Update crosshair to follow the moved column
3171            self.crosshair_col = new_position;
3172
3173            ColumnReorderResult {
3174                new_column_position: new_position,
3175                description: format!("Moved column '{}' left", column_name),
3176                success: true,
3177            }
3178        } else {
3179            ColumnReorderResult {
3180                new_column_position: current_column,
3181                description: "Cannot move column left".to_string(),
3182                success: false,
3183            }
3184        }
3185    }
3186
3187    /// Move the current column right in the display order (swap with next column)
3188    pub fn reorder_column_right(&mut self, current_column: usize) -> ColumnReorderResult {
3189        // Get the current column count
3190        let column_count = self.dataview.column_count();
3191
3192        if current_column >= column_count {
3193            return ColumnReorderResult {
3194                new_column_position: current_column,
3195                description: "Invalid column position".to_string(),
3196                success: false,
3197            };
3198        }
3199
3200        // Get pinned columns count to respect boundaries
3201        let pinned_count = self.dataview.get_pinned_columns().len();
3202
3203        // Clone the DataView, modify it, and replace the Arc
3204        let mut new_dataview = (*self.dataview).clone();
3205
3206        // Delegate to DataView's move_column_right - it handles pinned column logic
3207        let success = new_dataview.move_column_right(current_column);
3208
3209        if success {
3210            // Replace the Arc with the modified DataView
3211            self.dataview = Arc::new(new_dataview);
3212        }
3213
3214        if success {
3215            self.invalidate_cache(); // Column order changed, need to recalculate widths
3216
3217            // Determine new cursor position and if wrapping occurred
3218            let wrapped_to_beginning = current_column == column_count - 1
3219                || (pinned_count > 0 && current_column == pinned_count - 1);
3220
3221            let new_position = if current_column == column_count - 1 {
3222                // Column wrapped to beginning
3223                if pinned_count > 0 {
3224                    pinned_count // First non-pinned column
3225                } else {
3226                    0 // No pinned columns, go to start
3227                }
3228            } else if pinned_count > 0 && current_column == pinned_count - 1 {
3229                // Last pinned column wrapped to first pinned
3230                0
3231            } else {
3232                // Normal swap with next
3233                current_column + 1
3234            };
3235
3236            let column_names = self.dataview.column_names();
3237            let column_name = column_names
3238                .get(new_position)
3239                .map(|s| s.as_str())
3240                .unwrap_or("?");
3241
3242            // Adjust viewport to keep the moved column visible
3243            if wrapped_to_beginning {
3244                // Reset viewport to start
3245                self.viewport_cols = 0..self.dataview.column_count().min(20); // Show first ~20 columns or all if less
3246                debug!(target: "viewport_manager",
3247                    "Column wrapped to beginning, resetting viewport to show column {} at position {}",
3248                    column_name, new_position
3249                );
3250            } else {
3251                // Check if the new position is outside the current viewport
3252                if !self.viewport_cols.contains(&new_position) {
3253                    // Column moved outside viewport, adjust to show it
3254                    let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3255
3256                    // Calculate how many columns we can fit
3257                    let columns_that_fit =
3258                        self.calculate_columns_that_fit(new_position, terminal_width);
3259
3260                    // Adjust viewport to show the column at its new position
3261                    let new_start = if new_position > self.viewport_cols.end {
3262                        // Column moved to the right, scroll right
3263                        new_position.saturating_sub(columns_that_fit - 1)
3264                    } else {
3265                        // Column moved to the left (shouldn't happen in move_right, but handle it)
3266                        new_position
3267                    };
3268
3269                    let new_end = (new_start + columns_that_fit).min(self.dataview.column_count());
3270                    self.viewport_cols = new_start..new_end;
3271
3272                    debug!(target: "viewport_manager",
3273                        "Column moved outside viewport! Adjusting viewport to {}..{} to show column {} at position {}",
3274                        new_start, new_end, column_name, new_position
3275                    );
3276                }
3277            }
3278
3279            // Update crosshair to follow the moved column
3280            self.crosshair_col = new_position;
3281
3282            ColumnReorderResult {
3283                new_column_position: new_position,
3284                description: format!("Moved column '{}' right", column_name),
3285                success: true,
3286            }
3287        } else {
3288            ColumnReorderResult {
3289                new_column_position: current_column,
3290                description: "Cannot move column right".to_string(),
3291                success: false,
3292            }
3293        }
3294    }
3295
3296    /// Hide the specified column
3297    /// Returns true if the column was hidden, false if it couldn't be hidden
3298    pub fn hide_column(&mut self, column_index: usize) -> bool {
3299        debug!(target: "viewport_manager", "hide_column: column_index={}", column_index);
3300
3301        // Clone the DataView, modify it, and replace the Arc
3302        let mut new_dataview = (*self.dataview).clone();
3303
3304        // Hide the column in the cloned DataView
3305        let success = new_dataview.hide_column(column_index);
3306
3307        if success {
3308            // Replace the Arc with the modified DataView
3309            self.dataview = Arc::new(new_dataview);
3310            self.invalidate_cache(); // Column visibility changed, need to recalculate widths
3311
3312            // Adjust viewport if necessary
3313            let column_count = self.dataview.column_count();
3314            if self.viewport_cols.end > column_count {
3315                self.viewport_cols.end = column_count;
3316            }
3317            if self.viewport_cols.start >= column_count && column_count > 0 {
3318                self.viewport_cols.start = column_count - 1;
3319            }
3320
3321            // Adjust crosshair if necessary
3322            // If we hid the column the crosshair was on, or a column before it, adjust
3323            if column_index == self.crosshair_col {
3324                // We hid the current column
3325                if column_count > 0 {
3326                    // If we were at the last column and it's now hidden, move to the new last column
3327                    // Otherwise, stay at the same index (which now points to the next column)
3328                    if self.crosshair_col >= column_count {
3329                        self.crosshair_col = column_count - 1;
3330                    }
3331                    // Note: if crosshair_col < column_count, we keep the same index,
3332                    // which naturally moves us to the next column
3333                } else {
3334                    self.crosshair_col = 0;
3335                }
3336                debug!(target: "viewport_manager", "Crosshair was on hidden column, moved to {}", self.crosshair_col);
3337            } else if column_index < self.crosshair_col {
3338                // We hid a column before the crosshair - decrement crosshair position
3339                self.crosshair_col = self.crosshair_col.saturating_sub(1);
3340                debug!(target: "viewport_manager", "Hidden column was before crosshair, adjusted crosshair to {}", self.crosshair_col);
3341            }
3342
3343            debug!(target: "viewport_manager", "Column {} hidden successfully", column_index);
3344        } else {
3345            debug!(target: "viewport_manager", "Failed to hide column {} (might be pinned)", column_index);
3346        }
3347
3348        success
3349    }
3350
3351    /// Hide a column by name
3352    /// Returns true if the column was hidden, false if it couldn't be hidden
3353    pub fn hide_column_by_name(&mut self, column_name: &str) -> bool {
3354        debug!(target: "viewport_manager", "hide_column_by_name: column_name={}", column_name);
3355
3356        // Clone the DataView, modify it, and replace the Arc
3357        let mut new_dataview = (*self.dataview).clone();
3358
3359        // Hide the column in DataView
3360        let success = new_dataview.hide_column_by_name(column_name);
3361
3362        if success {
3363            // Replace the Arc with the modified DataView
3364            self.dataview = Arc::new(new_dataview);
3365        }
3366
3367        if success {
3368            self.invalidate_cache(); // Column visibility changed, need to recalculate widths
3369
3370            // Adjust viewport if necessary
3371            let column_count = self.dataview.column_count();
3372            if self.viewport_cols.end > column_count {
3373                self.viewport_cols.end = column_count;
3374            }
3375            if self.viewport_cols.start >= column_count && column_count > 0 {
3376                self.viewport_cols.start = column_count - 1;
3377            }
3378
3379            // Ensure crosshair stays within bounds after hiding
3380            if self.crosshair_col >= column_count && column_count > 0 {
3381                self.crosshair_col = column_count - 1;
3382                debug!(target: "viewport_manager", "Adjusted crosshair to {} after hiding column", self.crosshair_col);
3383            }
3384
3385            debug!(target: "viewport_manager", "Column '{}' hidden successfully", column_name);
3386        } else {
3387            debug!(target: "viewport_manager", "Failed to hide column '{}' (might be pinned or not found)", column_name);
3388        }
3389
3390        success
3391    }
3392
3393    /// Hide all empty columns
3394    /// Returns the number of columns hidden
3395    pub fn hide_empty_columns(&mut self) -> usize {
3396        debug!(target: "viewport_manager", "hide_empty_columns called");
3397
3398        // Clone the DataView, modify it, and replace the Arc
3399        let mut new_dataview = (*self.dataview).clone();
3400
3401        // Hide empty columns in DataView
3402        let count = new_dataview.hide_empty_columns();
3403
3404        if count > 0 {
3405            // Replace the Arc with the modified DataView
3406            self.dataview = Arc::new(new_dataview);
3407        }
3408
3409        if count > 0 {
3410            self.invalidate_cache(); // Column visibility changed, need to recalculate widths
3411
3412            // Adjust viewport if necessary
3413            let column_count = self.dataview.column_count();
3414            if self.viewport_cols.end > column_count {
3415                self.viewport_cols.end = column_count;
3416            }
3417            if self.viewport_cols.start >= column_count && column_count > 0 {
3418                self.viewport_cols.start = column_count - 1;
3419            }
3420
3421            debug!(target: "viewport_manager", "Hidden {} empty columns", count);
3422        }
3423
3424        count
3425    }
3426
3427    /// Unhide all columns
3428    pub fn unhide_all_columns(&mut self) {
3429        debug!(target: "viewport_manager", "unhide_all_columns called");
3430
3431        // Clone the DataView, modify it, and replace the Arc
3432        let mut new_dataview = (*self.dataview).clone();
3433
3434        // Unhide all columns in the cloned DataView
3435        new_dataview.unhide_all_columns();
3436
3437        // Replace the Arc with the modified DataView
3438        self.dataview = Arc::new(new_dataview);
3439
3440        self.invalidate_cache(); // Column visibility changed, need to recalculate widths
3441
3442        // Reset viewport to show first columns
3443        let column_count = self.dataview.column_count();
3444        self.viewport_cols = 0..column_count.min(20); // Show first ~20 columns or all if less
3445
3446        debug!(target: "viewport_manager", "All columns unhidden, viewport reset to {:?}", self.viewport_cols);
3447    }
3448
3449    /// Pin the specified column
3450    /// Returns true if the column was pinned successfully, false otherwise
3451    pub fn pin_column(&mut self, column_index: usize) -> bool {
3452        debug!(target: "viewport_manager", "pin_column: column_index={}", column_index);
3453
3454        // Clone the DataView, modify it, and replace the Arc
3455        let mut new_dataview = (*self.dataview).clone();
3456
3457        // Try to pin the column in the cloned DataView
3458        let success = new_dataview.pin_column(column_index).is_ok();
3459
3460        if success {
3461            // Replace the Arc with the modified DataView
3462            self.dataview = Arc::new(new_dataview);
3463            self.invalidate_cache(); // Column pinning affects layout, need to recalculate
3464
3465            debug!(target: "viewport_manager", "Column {} pinned successfully", column_index);
3466        } else {
3467            debug!(target: "viewport_manager", "Failed to pin column {}", column_index);
3468        }
3469
3470        success
3471    }
3472
3473    /// Update the current column position and automatically adjust viewport if needed
3474    /// This takes a VISUAL column index (0, 1, 2... in display order)
3475    pub fn set_current_column(&mut self, visual_column: usize) -> bool {
3476        let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH); // Account for borders
3477        let total_visual_columns = self.dataview.get_display_columns().len();
3478
3479        tracing::debug!("[PIN_DEBUG] === set_current_column ===");
3480        tracing::debug!(
3481            "[PIN_DEBUG] visual_column={}, viewport_cols={:?}",
3482            visual_column,
3483            self.viewport_cols
3484        );
3485        tracing::debug!(
3486            "[PIN_DEBUG] terminal_width={}, total_visual_columns={}",
3487            terminal_width,
3488            total_visual_columns
3489        );
3490
3491        debug!(target: "viewport_manager", 
3492               "set_current_column ENTRY: visual_column={}, current_viewport={:?}, terminal_width={}, total_visual={}", 
3493               visual_column, self.viewport_cols, terminal_width, total_visual_columns);
3494
3495        // Validate the visual column
3496        if visual_column >= total_visual_columns {
3497            debug!(target: "viewport_manager", "Visual column {} out of bounds (max {})", visual_column, total_visual_columns);
3498            tracing::debug!(
3499                "[PIN_DEBUG] Column {} out of bounds (max {})",
3500                visual_column,
3501                total_visual_columns
3502            );
3503            return false;
3504        }
3505
3506        // Update the crosshair position
3507        self.crosshair_col = visual_column;
3508        debug!(target: "viewport_manager", "Updated crosshair_col to {}", visual_column);
3509        tracing::debug!("[PIN_DEBUG] Updated crosshair_col to {}", visual_column);
3510
3511        // Check if we're in optimal layout mode (all columns fit)
3512        // This needs to calculate based on visual columns
3513        let display_columns = self.dataview.get_display_columns();
3514        let mut total_width_needed = 0u16;
3515        for &dt_idx in &display_columns {
3516            let width =
3517                self.width_calculator
3518                    .get_column_width(&self.dataview, &self.viewport_rows, dt_idx);
3519            total_width_needed += width + 1; // +1 for separator
3520        }
3521
3522        if total_width_needed <= terminal_width {
3523            // All columns fit - no viewport adjustment needed, all columns are visible
3524            debug!(target: "viewport_manager", 
3525                   "Visual column {} in optimal layout mode (all columns fit), no adjustment needed", visual_column);
3526            tracing::debug!("[PIN_DEBUG] All columns fit, no adjustment needed");
3527            tracing::debug!("[PIN_DEBUG] === End set_current_column (all fit) ===");
3528            return false;
3529        }
3530
3531        // Check if the visual column is already visible in the viewport
3532        // We need to check what's ACTUALLY visible, not just what's in the viewport range
3533        let pinned_count = self.dataview.get_pinned_columns().len();
3534        tracing::debug!("[PIN_DEBUG] pinned_count={}", pinned_count);
3535
3536        // Calculate which columns are actually visible with the current viewport
3537        let visible_columns = self.calculate_visible_column_indices(terminal_width);
3538        let display_columns = self.dataview.get_display_columns();
3539
3540        // Check if the target visual column's DataTable index is in the visible set
3541        let target_dt_idx = if visual_column < display_columns.len() {
3542            display_columns[visual_column]
3543        } else {
3544            tracing::debug!("[PIN_DEBUG] Column {} out of bounds", visual_column);
3545            return false;
3546        };
3547
3548        let is_visible = visible_columns.contains(&target_dt_idx);
3549        tracing::debug!(
3550            "[PIN_DEBUG] Column {} (dt_idx={}) visible check: visible_columns={:?}, is_visible={}",
3551            visual_column,
3552            target_dt_idx,
3553            visible_columns,
3554            is_visible
3555        );
3556
3557        debug!(target: "viewport_manager", 
3558               "set_current_column CHECK: visual_column={}, viewport={:?}, is_visible={}", 
3559               visual_column, self.viewport_cols, is_visible);
3560
3561        if is_visible {
3562            debug!(target: "viewport_manager", "Visual column {} already visible in viewport {:?}, no adjustment needed", 
3563                   visual_column, self.viewport_cols);
3564            tracing::debug!("[PIN_DEBUG] Column already visible, no adjustment");
3565            tracing::debug!("[PIN_DEBUG] === End set_current_column (no change) ===");
3566            return false;
3567        }
3568
3569        // Column is not visible, need to adjust viewport
3570        debug!(target: "viewport_manager", "Visual column {} NOT visible, calculating new offset", visual_column);
3571        let new_scroll_offset = self.calculate_scroll_offset_for_visual_column(visual_column);
3572        let old_scroll_offset = self.viewport_cols.start;
3573
3574        debug!(target: "viewport_manager", "Calculated new_scroll_offset={}, old_scroll_offset={}", 
3575               new_scroll_offset, old_scroll_offset);
3576
3577        if new_scroll_offset != old_scroll_offset {
3578            // Calculate how many scrollable columns fit from the new offset
3579            // This is similar logic to calculate_visible_column_indices
3580            let display_columns = self.dataview.get_display_columns();
3581            let pinned_count = self.dataview.get_pinned_columns().len();
3582            let mut used_width = 0u16;
3583            let separator_width = 1u16;
3584
3585            // First account for pinned column widths
3586            for visual_idx in 0..pinned_count {
3587                if visual_idx < display_columns.len() {
3588                    let dt_idx = display_columns[visual_idx];
3589                    let width = self.width_calculator.get_column_width(
3590                        &self.dataview,
3591                        &self.viewport_rows,
3592                        dt_idx,
3593                    );
3594                    used_width += width + separator_width;
3595                }
3596            }
3597
3598            // Now calculate how many scrollable columns fit
3599            let mut scrollable_columns_that_fit = 0;
3600            let visual_start = pinned_count + new_scroll_offset;
3601
3602            for visual_idx in visual_start..display_columns.len() {
3603                let dt_idx = display_columns[visual_idx];
3604                let width = self.width_calculator.get_column_width(
3605                    &self.dataview,
3606                    &self.viewport_rows,
3607                    dt_idx,
3608                );
3609                if used_width + width + separator_width <= terminal_width {
3610                    used_width += width + separator_width;
3611                    scrollable_columns_that_fit += 1;
3612                } else {
3613                    break;
3614                }
3615            }
3616
3617            // viewport_cols represents scrollable columns only
3618            let new_end = new_scroll_offset + scrollable_columns_that_fit;
3619            self.viewport_cols = new_scroll_offset..new_end;
3620            self.cache_dirty = true; // Mark cache as dirty since viewport changed
3621
3622            debug!(target: "viewport_manager", 
3623                   "Adjusted viewport for visual column {}: offset {}→{} (viewport: {:?})", 
3624                   visual_column, old_scroll_offset, new_scroll_offset, self.viewport_cols);
3625
3626            return true;
3627        }
3628
3629        false
3630    }
3631
3632    /// Calculate visible columns with a specific scroll offset (for viewport tracking)
3633    /// Returns visual column indices that would be visible with the given offset
3634    fn calculate_visible_column_indices_with_offset(
3635        &mut self,
3636        available_width: u16,
3637        scroll_offset: usize,
3638    ) -> Vec<usize> {
3639        // Temporarily update viewport to calculate with new offset
3640        let original_viewport = self.viewport_cols.clone();
3641        let total_visual_columns = self.dataview.get_display_columns().len();
3642        self.viewport_cols = scroll_offset..(scroll_offset + 50).min(total_visual_columns);
3643
3644        let visible_columns = self.calculate_visible_column_indices(available_width);
3645
3646        // Restore original viewport
3647        self.viewport_cols = original_viewport;
3648
3649        visible_columns
3650    }
3651
3652    /// Calculate the optimal scroll offset to keep a visual column visible
3653    /// Returns scroll offset in terms of scrollable columns (excluding pinned)
3654    fn calculate_scroll_offset_for_visual_column(&mut self, visual_column: usize) -> usize {
3655        debug!(target: "viewport_manager",
3656               "=== calculate_scroll_offset_for_visual_column ENTRY ===");
3657        debug!(target: "viewport_manager",
3658               "visual_column={}, current_viewport={:?}", visual_column, self.viewport_cols);
3659
3660        let pinned_count = self.dataview.get_pinned_columns().len();
3661        debug!(target: "viewport_manager",
3662               "pinned_count={}", pinned_count);
3663
3664        // If it's a pinned column, it's always visible, no scrolling needed
3665        if visual_column < pinned_count {
3666            debug!(target: "viewport_manager",
3667                   "Visual column {} is pinned, returning current offset {}", 
3668                   visual_column, self.viewport_cols.start);
3669            return self.viewport_cols.start; // Keep current offset
3670        }
3671
3672        // Convert to scrollable column index
3673        let scrollable_column = visual_column - pinned_count;
3674        debug!(target: "viewport_manager",
3675               "Converted to scrollable_column={}", scrollable_column);
3676
3677        let current_scroll_offset = self.viewport_cols.start;
3678        let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH);
3679
3680        // Calculate how much width pinned columns use
3681        let display_columns = self.dataview.get_display_columns();
3682        let mut pinned_width = 0u16;
3683        let separator_width = 1u16;
3684
3685        for visual_idx in 0..pinned_count {
3686            if visual_idx < display_columns.len() {
3687                let dt_idx = display_columns[visual_idx];
3688                let width = self.width_calculator.get_column_width(
3689                    &self.dataview,
3690                    &self.viewport_rows,
3691                    dt_idx,
3692                );
3693                pinned_width += width + separator_width;
3694            }
3695        }
3696
3697        // Available width for scrollable columns
3698        let available_for_scrollable = terminal_width.saturating_sub(pinned_width);
3699
3700        debug!(target: "viewport_manager",
3701               "Scroll offset calculation: target_scrollable_col={}, current_offset={}, available_width={}", 
3702               scrollable_column, current_scroll_offset, available_for_scrollable);
3703
3704        // Smart scrolling logic in scrollable column space
3705        if scrollable_column < current_scroll_offset {
3706            // Column is to the left of viewport, scroll left to show it
3707            debug!(target: "viewport_manager", "Column {} is left of viewport, scrolling left to offset {}", 
3708                   scrollable_column, scrollable_column);
3709            scrollable_column
3710        } else {
3711            // Column is to the right of viewport, use MINIMAL scrolling to make it visible
3712            // Strategy: Try the current offset first, then increment by small steps
3713
3714            debug!(target: "viewport_manager",
3715                   "Checking if column {} can be made visible with minimal scrolling from offset {}",
3716                   scrollable_column, current_scroll_offset);
3717
3718            // Try starting from current offset and incrementing until target column fits
3719            let mut test_scroll_offset = current_scroll_offset;
3720            let max_scrollable_columns = display_columns.len().saturating_sub(pinned_count);
3721
3722            while test_scroll_offset <= scrollable_column
3723                && test_scroll_offset < max_scrollable_columns
3724            {
3725                let mut used_width = 0u16;
3726                let mut target_column_fits = false;
3727
3728                // Test columns from this scroll offset
3729                for test_scrollable_idx in test_scroll_offset..max_scrollable_columns {
3730                    let visual_idx = pinned_count + test_scrollable_idx;
3731                    if visual_idx < display_columns.len() {
3732                        let dt_idx = display_columns[visual_idx];
3733                        let width = self.width_calculator.get_column_width(
3734                            &self.dataview,
3735                            &self.viewport_rows,
3736                            dt_idx,
3737                        );
3738
3739                        if used_width + width + separator_width <= available_for_scrollable {
3740                            used_width += width + separator_width;
3741                            if test_scrollable_idx == scrollable_column {
3742                                target_column_fits = true;
3743                                break; // Found it, no need to check more columns
3744                            }
3745                        } else {
3746                            break; // No more columns fit
3747                        }
3748                    }
3749                }
3750
3751                debug!(target: "viewport_manager", 
3752                       "Testing scroll_offset={}: target_fits={}, used_width={}", 
3753                       test_scroll_offset, target_column_fits, used_width);
3754
3755                if target_column_fits {
3756                    debug!(target: "viewport_manager", 
3757                           "Found minimal scroll offset {} for column {} (current was {})", 
3758                           test_scroll_offset, scrollable_column, current_scroll_offset);
3759                    return test_scroll_offset;
3760                }
3761
3762                // If target column doesn't fit, try next offset (scroll one column right)
3763                test_scroll_offset += 1;
3764            }
3765
3766            // If we couldn't find a minimal scroll, fall back to placing target column at start
3767            debug!(target: "viewport_manager", 
3768                   "Could not find minimal scroll, placing column {} at scroll offset {}", 
3769                   scrollable_column, scrollable_column);
3770            scrollable_column
3771        }
3772    }
3773
3774    /// Jump to a specific line (row) with centering
3775    pub fn goto_line(&mut self, target_row: usize) -> RowNavigationResult {
3776        let total_rows = self.dataview.row_count();
3777
3778        // Clamp target row to valid range
3779        let target_row = target_row.min(total_rows.saturating_sub(1));
3780
3781        // Calculate visible rows
3782        let visible_rows = (self.terminal_height as usize).saturating_sub(6);
3783
3784        // Calculate scroll offset to center the target row
3785        let centered_scroll_offset = if visible_rows > 0 {
3786            // Try to center the row in the viewport
3787            let half_viewport = visible_rows / 2;
3788            if target_row > half_viewport {
3789                // Can scroll up to center
3790                (target_row - half_viewport).min(total_rows.saturating_sub(visible_rows))
3791            } else {
3792                // Target is near the top, can't center
3793                0
3794            }
3795        } else {
3796            target_row
3797        };
3798
3799        // Update viewport
3800        let old_scroll_offset = self.viewport_rows.start;
3801        self.viewport_rows =
3802            centered_scroll_offset..(centered_scroll_offset + visible_rows).min(total_rows);
3803        let viewport_changed = centered_scroll_offset != old_scroll_offset;
3804
3805        // Update crosshair position
3806        self.crosshair_row = target_row;
3807
3808        let description = format!(
3809            "Jumped to row {} (centered at viewport {})",
3810            target_row + 1,
3811            centered_scroll_offset + 1
3812        );
3813
3814        debug!(target: "viewport_manager", 
3815               "goto_line: target_row={}, crosshair_row={}, scroll_offset={}→{}, viewport={:?}", 
3816               target_row, self.crosshair_row, old_scroll_offset, centered_scroll_offset, self.viewport_rows);
3817
3818        RowNavigationResult {
3819            row_position: target_row,
3820            row_scroll_offset: centered_scroll_offset,
3821            description,
3822            viewport_changed,
3823        }
3824    }
3825
3826    // ========== Column Operation Methods with Unified Results ==========
3827
3828    /// Hide the current column (using crosshair position) and return unified result
3829    pub fn hide_current_column_with_result(&mut self) -> ColumnOperationResult {
3830        let visual_col_idx = self.get_crosshair_col();
3831        let columns = self.dataview.column_names();
3832
3833        if visual_col_idx >= columns.len() {
3834            return ColumnOperationResult::failure("Invalid column position");
3835        }
3836
3837        let col_name = columns[visual_col_idx].clone();
3838        let visible_count = columns.len();
3839
3840        // Don't hide if it's the last visible column
3841        if visible_count <= 1 {
3842            return ColumnOperationResult::failure("Cannot hide the last visible column");
3843        }
3844
3845        // Hide the column
3846        let success = self.hide_column(visual_col_idx);
3847
3848        if success {
3849            let mut result =
3850                ColumnOperationResult::success(format!("Column '{}' hidden", col_name));
3851            result.updated_dataview = Some(self.clone_dataview());
3852            result.new_column_position = Some(self.get_crosshair_col());
3853            result.new_viewport = Some(self.viewport_cols.clone());
3854            result
3855        } else {
3856            ColumnOperationResult::failure(format!(
3857                "Cannot hide column '{}' (may be pinned)",
3858                col_name
3859            ))
3860        }
3861    }
3862
3863    /// Unhide all columns and return unified result
3864    pub fn unhide_all_columns_with_result(&mut self) -> ColumnOperationResult {
3865        let hidden_columns = self.dataview.get_hidden_column_names();
3866        let count = hidden_columns.len();
3867
3868        if count == 0 {
3869            return ColumnOperationResult::success("No hidden columns");
3870        }
3871
3872        self.unhide_all_columns();
3873
3874        let mut result = ColumnOperationResult::success(format!("Unhidden {} column(s)", count));
3875        result.updated_dataview = Some(self.clone_dataview());
3876        result.affected_count = Some(count);
3877        result.new_viewport = Some(self.viewport_cols.clone());
3878        result
3879    }
3880
3881    /// Reorder column left and return unified result
3882    pub fn reorder_column_left_with_result(&mut self) -> ColumnOperationResult {
3883        let current_col = self.get_crosshair_col();
3884        let reorder_result = self.reorder_column_left(current_col);
3885
3886        if reorder_result.success {
3887            let mut result = ColumnOperationResult::success(reorder_result.description);
3888            result.updated_dataview = Some(self.clone_dataview());
3889            result.new_column_position = Some(reorder_result.new_column_position);
3890            result.new_viewport = Some(self.viewport_cols.clone());
3891            result
3892        } else {
3893            ColumnOperationResult::failure(reorder_result.description)
3894        }
3895    }
3896
3897    /// Reorder column right and return unified result
3898    pub fn reorder_column_right_with_result(&mut self) -> ColumnOperationResult {
3899        let current_col = self.get_crosshair_col();
3900        let reorder_result = self.reorder_column_right(current_col);
3901
3902        if reorder_result.success {
3903            let mut result = ColumnOperationResult::success(reorder_result.description);
3904            result.updated_dataview = Some(self.clone_dataview());
3905            result.new_column_position = Some(reorder_result.new_column_position);
3906            result.new_viewport = Some(self.viewport_cols.clone());
3907            result
3908        } else {
3909            ColumnOperationResult::failure(reorder_result.description)
3910        }
3911    }
3912
3913    // ========== COLUMN WIDTH CALCULATIONS ==========
3914
3915    /// Calculate optimal column widths based on visible viewport rows
3916    /// This is a performance-optimized version that only examines visible data
3917    pub fn calculate_viewport_column_widths(
3918        &mut self,
3919        viewport_start: usize,
3920        viewport_end: usize,
3921        compact_mode: bool,
3922    ) -> Vec<u16> {
3923        let headers = self.dataview.column_names();
3924        let mut widths = Vec::with_capacity(headers.len());
3925
3926        // Use compact mode settings
3927        let min_width = if compact_mode { 4 } else { 6 };
3928        let padding = if compact_mode { 1 } else { 2 };
3929
3930        // Calculate dynamic max_width based on terminal size and column count
3931        let available_width = self.terminal_width.saturating_sub(10) as usize;
3932        let visible_cols = headers.len().min(12); // Estimate visible columns
3933
3934        // Allow columns to use more space on wide terminals
3935        let dynamic_max = if visible_cols > 0 {
3936            (available_width / visible_cols).max(30).min(80)
3937        } else {
3938            30
3939        };
3940
3941        let max_width = if compact_mode {
3942            dynamic_max.min(40)
3943        } else {
3944            dynamic_max
3945        };
3946
3947        // PERF: Only convert viewport rows to strings, not entire table!
3948        let mut rows_to_check = Vec::new();
3949        let source_table = self.dataview.source();
3950        for i in viewport_start..viewport_end.min(source_table.row_count()) {
3951            if let Some(row_strings) = source_table.get_row_as_strings(i) {
3952                rows_to_check.push(row_strings);
3953            }
3954        }
3955
3956        // Calculate width for each column
3957        for (col_idx, header) in headers.iter().enumerate() {
3958            // Start with header width
3959            let mut max_col_width = header.len();
3960
3961            // Check only visible rows for this column
3962            for row in &rows_to_check {
3963                if let Some(value) = row.get(col_idx) {
3964                    let display_value = if value.is_empty() {
3965                        "NULL"
3966                    } else {
3967                        value.as_str()
3968                    };
3969                    max_col_width = max_col_width.max(display_value.len());
3970                }
3971            }
3972
3973            // Apply min/max constraints and padding
3974            let width = (max_col_width + padding).clamp(min_width, max_width) as u16;
3975            widths.push(width);
3976        }
3977
3978        widths
3979    }
3980
3981    /// Calculate optimal column widths using smart viewport-based calculations
3982    /// Returns the calculated widths without modifying any state
3983    pub fn calculate_optimal_column_widths(&mut self) -> Vec<u16> {
3984        // Use the column width calculator with terminal width awareness
3985        self.width_calculator.calculate_with_terminal_width(
3986            &self.dataview,
3987            &self.viewport_rows,
3988            self.terminal_width,
3989        );
3990
3991        // Return all calculated widths
3992        let col_count = self.dataview.column_count();
3993        let mut widths = Vec::with_capacity(col_count);
3994        for idx in 0..col_count {
3995            widths.push(self.width_calculator.get_column_width(
3996                &self.dataview,
3997                &self.viewport_rows,
3998                idx,
3999            ));
4000        }
4001        widths
4002    }
4003
4004    /// Ensure the specified column is visible by adjusting the viewport if necessary
4005    pub fn ensure_column_visible(&mut self, column_index: usize, available_width: u16) {
4006        debug!(target: "viewport_manager", "ensure_column_visible: column_index={}, available_width={}", column_index, available_width);
4007
4008        let total_columns = self.dataview.get_display_columns().len();
4009
4010        // Clamp column_index to valid range
4011        if column_index >= total_columns {
4012            debug!(target: "viewport_manager", "Column index {} out of range (max {})", column_index, total_columns.saturating_sub(1));
4013            return;
4014        }
4015
4016        // Check if column is already visible
4017        let visible_columns = self.calculate_visible_column_indices(available_width);
4018        let dt_columns = self.dataview.get_display_columns();
4019
4020        // Find the DataTable index for the visual column
4021        if let Some(&dt_index) = dt_columns.get(column_index) {
4022            if visible_columns.contains(&dt_index) {
4023                debug!(target: "viewport_manager", "Column {} already visible", column_index);
4024                return;
4025            }
4026        }
4027
4028        // Column is not visible, need to adjust viewport
4029        // Use set_current_column which has the smart viewport adjustment logic
4030        if self.set_current_column(column_index) {
4031            self.crosshair_col = column_index;
4032            debug!(target: "viewport_manager", "Ensured column {} is visible and set crosshair", column_index);
4033        } else {
4034            debug!(target: "viewport_manager", "Failed to make column {} visible", column_index);
4035        }
4036    }
4037
4038    /// Reorder a column from one position to another
4039    pub fn reorder_column(&mut self, from_index: usize, to_index: usize) -> bool {
4040        debug!(target: "viewport_manager", "reorder_column: from_index={}, to_index={}", from_index, to_index);
4041
4042        if from_index == to_index {
4043            return true; // No move needed
4044        }
4045
4046        // Clone the DataView, modify it, and replace the Arc
4047        let mut new_dataview = (*self.dataview).clone();
4048
4049        let mut current_pos = from_index;
4050        let mut success = true;
4051
4052        // Move the column step by step to the target position
4053        if from_index < to_index {
4054            // Moving right - use move_column_right repeatedly
4055            while current_pos < to_index && success {
4056                success = new_dataview.move_column_right(current_pos);
4057                if success {
4058                    current_pos += 1;
4059                }
4060            }
4061        } else {
4062            // Moving left - use move_column_left repeatedly
4063            while current_pos > to_index && success {
4064                success = new_dataview.move_column_left(current_pos);
4065                if success {
4066                    current_pos -= 1;
4067                }
4068            }
4069        }
4070
4071        if success {
4072            // Replace the Arc with the modified DataView
4073            self.dataview = Arc::new(new_dataview);
4074            self.invalidate_cache(); // Column order changed, need to recalculate
4075
4076            debug!(target: "viewport_manager", "Column moved from {} to {} successfully", from_index, to_index);
4077        } else {
4078            debug!(target: "viewport_manager", "Failed to move column from {} to {}", from_index, to_index);
4079        }
4080
4081        success
4082    }
4083
4084    /// Calculate column widths for the given available width
4085    /// This is a convenience method that returns the calculated widths for all columns
4086    pub fn calculate_column_widths(&mut self, available_width: u16) -> Vec<u16> {
4087        // Calculate visible column indices first to trigger width calculations
4088        let _visible_indices = self.calculate_visible_column_indices(available_width);
4089
4090        // Return the cached column widths
4091        self.get_column_widths().to_vec()
4092    }
4093}
4094
4095/// Viewport efficiency metrics
4096#[derive(Debug, Clone)]
4097pub struct ViewportEfficiency {
4098    pub available_width: u16,
4099    pub used_width: u16,
4100    pub wasted_space: u16,
4101    pub efficiency_percent: u8,
4102    pub visible_columns: usize,
4103    pub column_widths: Vec<u16>,
4104    pub next_column_width: Option<u16>, // Width of the next column that didn't fit
4105    pub columns_that_could_fit: Vec<(usize, u16)>, // Columns that could fit in wasted space
4106}
4107
4108impl ViewportEfficiency {
4109    /// Format as a compact status line message
4110    pub fn to_status_string(&self) -> String {
4111        format!(
4112            "Viewport: {}w used of {}w ({}% efficient, {} cols, {}w wasted)",
4113            self.used_width,
4114            self.available_width,
4115            self.efficiency_percent,
4116            self.visible_columns,
4117            self.wasted_space
4118        )
4119    }
4120
4121    /// Format as detailed debug info
4122    pub fn to_debug_string(&self) -> String {
4123        let avg_width = if !self.column_widths.is_empty() {
4124            self.column_widths.iter().sum::<u16>() / self.column_widths.len() as u16
4125        } else {
4126            0
4127        };
4128
4129        // Show what efficiency we could get by fitting more columns
4130        let mut efficiency_analysis = String::new();
4131        if let Some(next_width) = self.next_column_width {
4132            efficiency_analysis.push_str(&format!(
4133                "\n\n  Next column needs: {}w (+1 separator)",
4134                next_width
4135            ));
4136            if next_width < self.wasted_space {
4137                efficiency_analysis.push_str(" ✓ FITS!");
4138            } else {
4139                efficiency_analysis.push_str(&format!(" ✗ Too wide (have {}w)", self.wasted_space));
4140            }
4141        }
4142
4143        if !self.columns_that_could_fit.is_empty() {
4144            efficiency_analysis.push_str(&format!(
4145                "\n  Columns that COULD fit in {}w:",
4146                self.wasted_space
4147            ));
4148            for (idx, width) in
4149                &self.columns_that_could_fit[..self.columns_that_could_fit.len().min(5)]
4150            {
4151                efficiency_analysis.push_str(&format!("\n    - Column {}: {}w", idx, width));
4152            }
4153            if self.columns_that_could_fit.len() > 5 {
4154                efficiency_analysis.push_str(&format!(
4155                    "\n    ... and {} more",
4156                    self.columns_that_could_fit.len() - 5
4157                ));
4158            }
4159        }
4160
4161        // Calculate hypothetical efficiencies
4162        efficiency_analysis.push_str("\n\n  Hypothetical efficiencies:");
4163        for extra in 1..=3 {
4164            let hypothetical_used =
4165                self.used_width + (extra * (avg_width + 1)).min(self.wasted_space);
4166            let hypothetical_eff =
4167                ((hypothetical_used as f32 / self.available_width as f32) * 100.0) as u8;
4168            let hypothetical_wasted = self.available_width.saturating_sub(hypothetical_used);
4169            efficiency_analysis.push_str(&format!(
4170                "\n    +{} cols ({}w each): {}% efficiency, {}w wasted",
4171                extra, avg_width, hypothetical_eff, hypothetical_wasted
4172            ));
4173        }
4174
4175        format!(
4176            "Viewport Efficiency:\n  Available: {}w\n  Used: {}w\n  Wasted: {}w\n  Efficiency: {}%\n  Columns: {} visible\n  Widths: {:?}\n  Avg Width: {}w{}",
4177            self.available_width,
4178            self.used_width,
4179            self.wasted_space,
4180            self.efficiency_percent,
4181            self.visible_columns,
4182            self.column_widths.clone(),
4183            avg_width,
4184            efficiency_analysis
4185        )
4186    }
4187}
4188
4189#[cfg(test)]
4190mod tests {
4191    use super::*;
4192    use crate::data::datatable::{DataColumn, DataRow, DataTable, DataValue};
4193
4194    fn create_test_dataview() -> Arc<DataView> {
4195        let mut table = DataTable::new("test");
4196        table.add_column(DataColumn::new("id"));
4197        table.add_column(DataColumn::new("name"));
4198        table.add_column(DataColumn::new("amount"));
4199
4200        for i in 0..100 {
4201            table
4202                .add_row(DataRow::new(vec![
4203                    DataValue::Integer(i),
4204                    DataValue::String(format!("Item {}", i)),
4205                    DataValue::Float(i as f64 * 10.5),
4206                ]))
4207                .unwrap();
4208        }
4209
4210        Arc::new(DataView::new(Arc::new(table)))
4211    }
4212
4213    #[test]
4214    fn test_viewport_basic() {
4215        let dataview = create_test_dataview();
4216        let mut viewport = ViewportManager::new(dataview);
4217
4218        viewport.set_viewport(0, 0, 80, 24);
4219
4220        assert_eq!(viewport.viewport_rows(), 0..24);
4221        assert_eq!(viewport.viewport_cols(), 0..3);
4222
4223        let visible_rows = viewport.get_visible_rows();
4224        assert_eq!(visible_rows.len(), 24);
4225    }
4226
4227    #[test]
4228    fn test_column_width_calculation() {
4229        let dataview = create_test_dataview();
4230        let mut viewport = ViewportManager::new(dataview);
4231
4232        viewport.set_viewport(0, 0, 80, 10);
4233
4234        let widths = viewport.get_column_widths();
4235        assert_eq!(widths.len(), 3);
4236
4237        // "id" column should be narrow
4238        assert!(widths[0] < 10);
4239        // "name" column should be wider
4240        assert!(widths[1] > widths[0]);
4241    }
4242
4243    #[test]
4244    fn test_viewport_scrolling() {
4245        let dataview = create_test_dataview();
4246        let mut viewport = ViewportManager::new(dataview);
4247
4248        viewport.set_viewport(0, 0, 80, 24);
4249        viewport.scroll_by(10, 0);
4250
4251        assert_eq!(viewport.viewport_rows(), 10..34);
4252
4253        viewport.scroll_by(-5, 1);
4254        assert_eq!(viewport.viewport_rows(), 5..29);
4255        assert_eq!(viewport.viewport_cols(), 1..3);
4256    }
4257
4258    // Comprehensive navigation and column operation tests
4259
4260    #[test]
4261    fn test_navigate_to_last_and_first_column() {
4262        let dataview = create_test_dataview();
4263        let mut vm = ViewportManager::new(dataview);
4264        vm.update_terminal_size(120, 40);
4265
4266        // Navigate to last column
4267        let result = vm.navigate_to_last_column();
4268        assert_eq!(vm.get_crosshair_col(), 2); // We have 3 columns (0-2)
4269        assert_eq!(result.column_position, 2);
4270
4271        // Navigate back to first column
4272        let result = vm.navigate_to_first_column();
4273        assert_eq!(vm.get_crosshair_col(), 0);
4274        assert_eq!(result.column_position, 0);
4275    }
4276
4277    #[test]
4278    fn test_column_reorder_right_with_crosshair() {
4279        let dataview = create_test_dataview();
4280        let mut vm = ViewportManager::new(dataview);
4281        vm.update_terminal_size(120, 40);
4282
4283        // Start at column 0 (id)
4284        vm.crosshair_col = 0;
4285
4286        // Move column right (swap id with name)
4287        let result = vm.reorder_column_right(0);
4288        assert!(result.success);
4289        assert_eq!(result.new_column_position, 1);
4290        assert_eq!(vm.get_crosshair_col(), 1); // Crosshair follows the moved column
4291
4292        // Verify column order changed
4293        let headers = vm.dataview.column_names();
4294        assert_eq!(headers[0], "name"); // name is now at position 0
4295        assert_eq!(headers[1], "id"); // id is now at position 1
4296    }
4297
4298    #[test]
4299    fn test_column_reorder_left_with_crosshair() {
4300        let dataview = create_test_dataview();
4301        let mut vm = ViewportManager::new(dataview);
4302        vm.update_terminal_size(120, 40);
4303
4304        // Start at column 1 (name)
4305        vm.crosshair_col = 1;
4306
4307        // Move column left (swap name with id)
4308        let result = vm.reorder_column_left(1);
4309        assert!(result.success);
4310        assert_eq!(result.new_column_position, 0);
4311        assert_eq!(vm.get_crosshair_col(), 0); // Crosshair follows the moved column
4312    }
4313
4314    #[test]
4315    fn test_hide_column_adjusts_crosshair() {
4316        let dataview = create_test_dataview();
4317        let mut vm = ViewportManager::new(dataview);
4318        vm.update_terminal_size(120, 40);
4319
4320        // Test hiding column that crosshair is on
4321        vm.crosshair_col = 1; // On "name" column
4322        let success = vm.hide_column(1);
4323        assert!(success);
4324        // Crosshair stays at index 1, which now points to "amount"
4325        assert_eq!(vm.get_crosshair_col(), 1);
4326        assert_eq!(vm.dataview.column_count(), 2); // Only 2 visible columns now
4327
4328        // Test hiding last column when crosshair is on it
4329        vm.crosshair_col = 1; // On last visible column now
4330        let success = vm.hide_column(1);
4331        assert!(success);
4332        assert_eq!(vm.get_crosshair_col(), 0); // Moved to previous column
4333        assert_eq!(vm.dataview.column_count(), 1); // Only 1 visible column
4334    }
4335
4336    #[test]
4337    fn test_goto_line_centers_viewport() {
4338        let dataview = create_test_dataview();
4339        let mut vm = ViewportManager::new(dataview);
4340        vm.update_terminal_size(120, 40);
4341
4342        // Jump to row 50
4343        let result = vm.goto_line(50);
4344        assert_eq!(result.row_position, 50);
4345        assert_eq!(vm.get_crosshair_row(), 50);
4346
4347        // Verify viewport is centered around target row
4348        let visible_rows = 34; // 40 - 6 for headers/status
4349        let expected_offset = 50 - (visible_rows / 2);
4350        assert_eq!(result.row_scroll_offset, expected_offset);
4351    }
4352
4353    #[test]
4354    fn test_page_navigation() {
4355        let dataview = create_test_dataview();
4356        let mut vm = ViewportManager::new(dataview);
4357        vm.update_terminal_size(120, 40);
4358
4359        // Test page down
4360        let initial_row = vm.get_crosshair_row();
4361        let result = vm.page_down();
4362        assert!(result.row_position > initial_row);
4363        assert_eq!(vm.get_crosshair_row(), result.row_position);
4364
4365        // Test page up to return
4366        vm.page_down(); // Go down more
4367        vm.page_down();
4368        let prev_position = vm.get_crosshair_row();
4369        let result = vm.page_up();
4370        assert!(result.row_position < prev_position); // Should have moved up
4371    }
4372
4373    #[test]
4374    fn test_cursor_lock_mode() {
4375        let dataview = create_test_dataview();
4376        let mut vm = ViewportManager::new(dataview);
4377        vm.update_terminal_size(120, 40);
4378
4379        // Enable cursor lock
4380        vm.toggle_cursor_lock();
4381        assert!(vm.is_cursor_locked());
4382
4383        // Move down with cursor lock - viewport position should stay same
4384        let initial_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
4385        let result = vm.navigate_row_down();
4386
4387        // With cursor lock, viewport should scroll but cursor stays at same viewport position
4388        if result.viewport_changed {
4389            let new_viewport_position = vm.get_crosshair_row() - vm.viewport_rows.start;
4390            assert_eq!(initial_viewport_position, new_viewport_position);
4391        }
4392    }
4393
4394    #[test]
4395    fn test_viewport_lock_prevents_scrolling() {
4396        let dataview = create_test_dataview();
4397        let mut vm = ViewportManager::new(dataview);
4398        vm.update_terminal_size(120, 40);
4399
4400        // Enable viewport lock
4401        vm.toggle_viewport_lock();
4402        assert!(vm.is_viewport_locked());
4403
4404        // Try to navigate - viewport should not change
4405        let initial_viewport = vm.viewport_rows.clone();
4406        let result = vm.navigate_row_down();
4407
4408        // Viewport should remain the same
4409        assert_eq!(vm.viewport_rows, initial_viewport);
4410        // Viewport lock should prevent scrolling
4411        assert!(!result.viewport_changed);
4412    }
4413
4414    #[test]
4415    fn test_h_m_l_viewport_navigation() {
4416        let dataview = create_test_dataview();
4417        let mut vm = ViewportManager::new(dataview);
4418        vm.update_terminal_size(120, 40);
4419
4420        // Move down to establish a viewport
4421        for _ in 0..20 {
4422            vm.navigate_row_down();
4423        }
4424
4425        // Test H (top of viewport)
4426        let result = vm.navigate_to_viewport_top();
4427        assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.start);
4428
4429        // Test L (bottom of viewport)
4430        let result = vm.navigate_to_viewport_bottom();
4431        assert_eq!(vm.get_crosshair_row(), vm.viewport_rows.end - 1);
4432
4433        // Test M (middle of viewport)
4434        let result = vm.navigate_to_viewport_middle();
4435        let expected_middle =
4436            vm.viewport_rows.start + (vm.viewport_rows.end - vm.viewport_rows.start) / 2;
4437        assert_eq!(vm.get_crosshair_row(), expected_middle);
4438    }
4439
4440    #[test]
4441    fn test_out_of_order_column_navigation() {
4442        // Create a test dataview with 12 columns
4443        let mut table = DataTable::new("test");
4444        for i in 0..12 {
4445            table.add_column(DataColumn::new(&format!("col{}", i)));
4446        }
4447
4448        // Add some test data
4449        for row in 0..10 {
4450            let mut values = Vec::new();
4451            for col in 0..12 {
4452                values.push(DataValue::String(format!("r{}c{}", row, col)));
4453            }
4454            table.add_row(DataRow::new(values)).unwrap();
4455        }
4456
4457        // Create DataView with columns selected out of order
4458        // Select columns in order: col11, col0, col5, col3, col8, col1, col10, col2, col7, col4, col9, col6
4459        // This simulates a SQL query like: SELECT col11, col0, col5, ... FROM table
4460        let dataview =
4461            DataView::new(Arc::new(table)).with_columns(vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6]);
4462
4463        let mut vm = ViewportManager::new(Arc::new(dataview));
4464        vm.update_terminal_size(200, 40); // Wide terminal to see all columns
4465
4466        // Test that columns appear in the order we selected them
4467        let column_names = vm.dataview.column_names();
4468        assert_eq!(
4469            column_names[0], "col11",
4470            "First visual column should be col11"
4471        );
4472        assert_eq!(
4473            column_names[1], "col0",
4474            "Second visual column should be col0"
4475        );
4476        assert_eq!(
4477            column_names[2], "col5",
4478            "Third visual column should be col5"
4479        );
4480
4481        // Start at first visual column (col11)
4482        vm.crosshair_col = 0;
4483
4484        // Navigate right through all columns and verify crosshair moves sequentially
4485        let mut visual_positions = vec![0];
4486        let mut datatable_positions = vec![];
4487
4488        // Record initial position
4489        let display_cols = vm.dataview.get_display_columns();
4490        datatable_positions.push(display_cols[0]);
4491
4492        // Navigate right through all columns
4493        for i in 0..11 {
4494            let current_visual = vm.get_crosshair_col();
4495            let result = vm.navigate_column_right(current_visual);
4496
4497            // Crosshair should move to next visual position
4498            let new_visual = vm.get_crosshair_col();
4499            assert_eq!(
4500                new_visual,
4501                current_visual + 1,
4502                "Crosshair should move from visual position {} to {}, but got {}",
4503                current_visual,
4504                current_visual + 1,
4505                new_visual
4506            );
4507
4508            visual_positions.push(new_visual);
4509            // Get the actual DataTable index at this visual position
4510            let display_cols = vm.dataview.get_display_columns();
4511            datatable_positions.push(display_cols[new_visual]);
4512        }
4513
4514        // Verify we visited columns in sequential visual order (0,1,2,3...11)
4515        assert_eq!(
4516            visual_positions,
4517            vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
4518            "Crosshair should move through visual positions sequentially"
4519        );
4520
4521        // Verify DataTable indices match our selection order
4522        assert_eq!(
4523            datatable_positions,
4524            vec![11, 0, 5, 3, 8, 1, 10, 2, 7, 4, 9, 6],
4525            "DataTable indices should match our column selection order"
4526        );
4527
4528        // Navigate back left and verify sequential movement
4529        for _i in (0..11).rev() {
4530            let current_visual = vm.get_crosshair_col();
4531            let _result = vm.navigate_column_left(current_visual);
4532
4533            // Crosshair should move to previous visual position
4534            let new_visual = vm.get_crosshair_col();
4535            assert_eq!(
4536                new_visual,
4537                current_visual - 1,
4538                "Crosshair should move from visual position {} to {}, but got {}",
4539                current_visual,
4540                current_visual - 1,
4541                new_visual
4542            );
4543        }
4544
4545        // Should be back at first column
4546        assert_eq!(
4547            vm.get_crosshair_col(),
4548            0,
4549            "Should be back at first visual column"
4550        );
4551
4552        // Test hiding a column and verifying navigation still works
4553        vm.hide_column(2); // Hide col5 (at visual position 2)
4554
4555        // Navigate from position 0 to what was position 3 (now position 2)
4556        vm.crosshair_col = 0;
4557        let _result1 = vm.navigate_column_right(0);
4558        assert_eq!(vm.get_crosshair_col(), 1, "Should be at visual position 1");
4559
4560        let _result2 = vm.navigate_column_right(1);
4561        assert_eq!(
4562            vm.get_crosshair_col(),
4563            2,
4564            "Should be at visual position 2 after hiding"
4565        );
4566
4567        // The column at position 2 should now be what was at position 3 (col3)
4568        let visible_cols = vm.dataview.column_names();
4569        assert_eq!(
4570            visible_cols[2], "col3",
4571            "Column at position 2 should be col3 after hiding col5"
4572        );
4573    }
4574}