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