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