diff --git a/sql-cli/integration_tests/test_scripts/test_pin_columns.sh b/sql-cli/integration_tests/test_scripts/test_pin_columns.sh new file mode 100755 index 00000000..bafb0763 --- /dev/null +++ b/sql-cli/integration_tests/test_scripts/test_pin_columns.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Test script for pin columns feature +# Tests that pinned columns stay fixed when scrolling horizontally + +set -e + +echo "======================================" +echo "Testing Pin Columns Feature" +echo "======================================" + +# Create test CSV with many columns to force horizontal scrolling +cat > /tmp/test_pin_columns.csv << 'EOF' +id,name,category,price,quantity,status,date,location,vendor,sku,warehouse,region,country,notes +1,Apple,Fruit,2.50,100,Available,2024-01-01,Store A,FreshCo,SKU001,W1,North,USA,Fresh +2,Banana,Fruit,1.25,150,Available,2024-01-02,Store B,FruitMart,SKU002,W2,South,USA,Ripe +3,Carrot,Vegetable,0.75,200,Available,2024-01-03,Store C,VeggieCo,SKU003,W1,East,USA,Organic +4,Desk,Furniture,150.00,10,Available,2024-01-04,Store D,OfficePro,SKU004,W3,West,USA,Wooden +5,Eggs,Dairy,3.99,50,Low Stock,2024-01-05,Store A,DairyFarm,SKU005,W2,North,USA,Dozen +EOF + +echo "Test 1: Basic pin column functionality" +echo "--------------------------------------" +# The test would normally be interactive, but we can verify the feature is working +# by checking that the ViewportManager correctly handles pinned columns + +# Run with debug logging to verify pin functionality +RUST_LOG=sql_cli::ui::viewport_manager=debug timeout 2 ./target/release/sql-cli /tmp/test_pin_columns.csv -e "select * from data" 2>&1 | grep -E "pinned|calculate_visible_column" | head -5 || true + +echo "" +echo "Test 2: Verify pinned columns in debug output" +echo "--------------------------------------------" +# Test that F5 debug mode shows pinned columns correctly +echo -e "llp\n" | timeout 2 ./target/release/sql-cli /tmp/test_pin_columns.csv -e "select * from data" 2>&1 | grep -i "pinned" | head -3 || true + +echo "" +echo "✅ Pin columns feature tests completed" +echo "" +echo "Manual verification steps:" +echo "1. Run: ./target/release/sql-cli /tmp/test_pin_columns.csv" +echo "2. Navigate to 'name' column (press 'l')" +echo "3. Pin the column (press 'p')" +echo "4. Scroll right (press 'l' multiple times)" +echo "5. Verify 'name' column stays visible on the left with 📌 indicator" +echo "6. Press 'P' to clear all pins" +echo "" + +# Clean up +rm -f /tmp/test_pin_columns.csv + +echo "Test script completed successfully!" \ No newline at end of file diff --git a/sql-cli/src/ui/enhanced_tui.rs b/sql-cli/src/ui/enhanced_tui.rs index 2e1c3a7a..56b717f7 100644 --- a/sql-cli/src/ui/enhanced_tui.rs +++ b/sql-cli/src/ui/enhanced_tui.rs @@ -5734,13 +5734,28 @@ impl EnhancedTuiApp { } // Get structured column information from ViewportManager - let (pinned_indices, crosshair_column_position, _) = { + let (pinned_visual_positions, crosshair_column_position, _) = { let mut viewport_manager_borrow = self.viewport_manager.borrow_mut(); let viewport_manager = viewport_manager_borrow .as_mut() .expect("ViewportManager must exist for rendering"); let info = viewport_manager.get_visible_columns_info(available_width); + // info.0 = visible_indices (all visible column source indices) + // info.1 = pinned_visible (pinned column source indices) + // info.2 = scrollable_visible (scrollable column source indices) + let visible_indices = &info.0; + let pinned_source_indices = &info.1; + + // Convert pinned source indices to visual positions + // The TableRenderContext expects visual positions (0-based positions in the visible array) + let mut pinned_visual_positions = Vec::new(); + for &source_idx in pinned_source_indices { + if let Some(visual_pos) = visible_indices.iter().position(|&x| x == source_idx) { + pinned_visual_positions.push(visual_pos); + } + } + // Get the crosshair's viewport-relative position for rendering // The viewport manager stores crosshair in absolute coordinates // but we need viewport-relative for rendering @@ -5754,7 +5769,11 @@ impl EnhancedTuiApp { let crosshair_visual = viewport_manager.get_crosshair_col(); - (info.1, crosshair_column_position, crosshair_visual) + ( + pinned_visual_positions, + crosshair_column_position, + crosshair_visual, + ) }; // Calculate row viewport @@ -5811,7 +5830,7 @@ impl EnhancedTuiApp { .row_count(row_count) .visible_rows(visible_row_indices.clone(), data_to_display) .columns(column_headers, column_widths_visual) - .pinned_columns(pinned_indices) + .pinned_columns(pinned_visual_positions) .selection( selected_row, selected_col, diff --git a/sql-cli/src/ui/table_renderer.rs b/sql-cli/src/ui/table_renderer.rs index bd757f61..d654b9a1 100644 --- a/sql-cli/src/ui/table_renderer.rs +++ b/sql-cli/src/ui/table_renderer.rs @@ -64,49 +64,62 @@ fn build_header_row(ctx: &TableRenderContext) -> Row<'static> { ); } - // Add data headers - header_cells.extend( - ctx.column_headers - .iter() - .enumerate() - .map(|(visual_pos, header)| { - // Get sort indicator - let sort_indicator = ctx.get_sort_indicator(visual_pos); - - // Check if this is the current column - let is_crosshair = ctx.is_selected_column(visual_pos); - let column_indicator = if is_crosshair { " [*]" } else { "" }; - - // Check if this column is pinned - let is_pinned = ctx.is_pinned_column(visual_pos); - - // Determine styling - let mut style = if is_pinned { - // Pinned columns get blue background + // Add data headers with separator between pinned and scrollable columns + let mut last_was_pinned = false; + for (visual_pos, header) in ctx.column_headers.iter().enumerate() { + let is_pinned = ctx.is_pinned_column(visual_pos); + + // Add separator if transitioning from pinned to non-pinned + if last_was_pinned && !is_pinned && ctx.pinned_count > 0 { + // Add a visual separator column + header_cells.push( + Cell::from("│").style( Style::default() - .bg(Color::Blue) - .fg(Color::White) - .add_modifier(Modifier::BOLD) - } else { - // Regular columns - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD) - }; + .fg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + ); + } - if is_crosshair { - // Current column gets yellow text - style = if is_pinned { - style.fg(Color::Yellow).add_modifier(Modifier::UNDERLINED) - } else { - style.fg(Color::Yellow).add_modifier(Modifier::UNDERLINED) - }; - } + // Get sort indicator + let sort_indicator = ctx.get_sort_indicator(visual_pos); + + // Check if this is the current column + let is_crosshair = ctx.is_selected_column(visual_pos); + let column_indicator = if is_crosshair { " [*]" } else { "" }; + + // Add pin indicator for pinned columns + let pin_indicator = if is_pinned { "📌 " } else { "" }; + + // Determine styling + let mut style = if is_pinned { + // Pinned columns get special styling + Style::default() + .bg(Color::Rgb(40, 40, 80)) // Darker blue background + .fg(Color::White) + .add_modifier(Modifier::BOLD) + } else { + // Regular columns + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + }; + + if is_crosshair { + // Current column gets yellow text + style = style.fg(Color::Yellow).add_modifier(Modifier::UNDERLINED); + } + + header_cells.push( + Cell::from(format!( + "{}{}{}{}", + pin_indicator, header, sort_indicator, column_indicator + )) + .style(style), + ); - Cell::from(format!("{}{}{}", header, sort_indicator, column_indicator)).style(style) - }) - .collect::>(), - ); + last_was_pinned = is_pinned; + } Row::new(header_cells) } @@ -130,11 +143,17 @@ fn build_data_rows(ctx: &TableRenderContext) -> Vec> { // Check if this is the current row let is_current_row = ctx.is_selected_row(row_idx); - // Add data cells with appropriate styling - cells.extend(row_data.iter().enumerate().map(|(col_idx, val)| { - let is_selected_column = ctx.is_selected_column(col_idx); + // Add data cells with separator between pinned and scrollable + let mut last_was_pinned = false; + for (col_idx, val) in row_data.iter().enumerate() { let is_pinned = ctx.is_pinned_column(col_idx); + // Add separator if transitioning from pinned to non-pinned + if last_was_pinned && !is_pinned && ctx.pinned_count > 0 { + cells.push(Cell::from("│").style(Style::default().fg(Color::DarkGray))); + } + + let is_selected_column = ctx.is_selected_column(col_idx); let mut cell = Cell::from(val.clone()); // Apply fuzzy filter highlighting @@ -142,6 +161,11 @@ fn build_data_rows(ctx: &TableRenderContext) -> Vec> { cell = cell.style(Style::default().fg(Color::Magenta)); } + // Apply background for pinned columns + if is_pinned && !is_current_row { + cell = cell.style(Style::default().bg(Color::Rgb(20, 20, 40))); + } + // Apply selection styling based on mode cell = match ctx.selection_mode { SelectionMode::Cell if is_current_row && is_selected_column => { @@ -183,8 +207,9 @@ fn build_data_rows(ctx: &TableRenderContext) -> Vec> { _ => cell, }; - cell - })); + cells.push(cell); + last_was_pinned = is_pinned; + } // Apply row highlighting let row_style = if is_current_row { @@ -209,9 +234,18 @@ fn calculate_column_widths(ctx: &TableRenderContext) -> Vec { widths.push(Constraint::Length(8)); // Fixed width for row numbers } - // Add widths for visible data columns - for &width in &ctx.column_widths { + // Add widths for visible data columns with separator + let mut last_was_pinned = false; + for (idx, &width) in ctx.column_widths.iter().enumerate() { + let is_pinned = ctx.is_pinned_column(idx); + + // Add separator width if transitioning from pinned to non-pinned + if last_was_pinned && !is_pinned && ctx.pinned_count > 0 { + widths.push(Constraint::Length(1)); // Separator column + } + widths.push(Constraint::Length(width)); + last_was_pinned = is_pinned; } widths diff --git a/sql-cli/src/ui/traits/column_ops.rs b/sql-cli/src/ui/traits/column_ops.rs index 0fc477d0..add32d65 100644 --- a/sql-cli/src/ui/traits/column_ops.rs +++ b/sql-cli/src/ui/traits/column_ops.rs @@ -21,20 +21,29 @@ pub trait ColumnBehavior { // Helper method to apply column navigation results fn apply_column_navigation_result(&mut self, result: NavigationResult, direction: &str) { - // Get the visual position from ViewportManager after navigation - let visual_position = { - let viewport_borrow = self.viewport_manager().borrow(); - viewport_borrow - .as_ref() - .map(|vm| vm.get_crosshair_col()) - .unwrap_or(0) - }; + // Use the column position from the navigation result - this is the visual/display index + // IMPORTANT: Don't re-query ViewportManager as that may have stale state + let visual_position = result.column_position; + + tracing::debug!( + "[COLUMN_OPS] apply_column_navigation_result: direction={}, visual_position={}", + direction, + visual_position + ); // Update Buffer's current column self.buffer_mut().set_current_column(visual_position); + tracing::debug!( + "[COLUMN_OPS] apply_column_navigation_result: set buffer column to {}", + visual_position + ); // Update navigation state self.state_container().navigation_mut().selected_column = visual_position; + tracing::debug!( + "[COLUMN_OPS] apply_column_navigation_result: set navigation column to {}", + visual_position + ); // Update scroll offset if viewport changed if result.viewport_changed { @@ -179,9 +188,22 @@ pub trait ColumnBehavior { .as_mut() .expect("ViewportManager must exist for navigation"); let current_visual = vm.get_crosshair_col(); - vm.navigate_column_right(current_visual) + tracing::debug!( + "[COLUMN_OPS] move_column_right: current_visual from VM = {}", + current_visual + ); + let result = vm.navigate_column_right(current_visual); + tracing::debug!( + "[COLUMN_OPS] move_column_right: navigation result column_position = {}", + result.column_position + ); + result }; + tracing::debug!( + "[COLUMN_OPS] move_column_right: applying result with column_position = {}", + nav_result.column_position + ); self.apply_column_navigation_result(nav_result, "right"); } diff --git a/sql-cli/src/ui/viewport_manager.rs b/sql-cli/src/ui/viewport_manager.rs index 70b38671..acc589ca 100644 --- a/sql-cli/src/ui/viewport_manager.rs +++ b/sql-cli/src/ui/viewport_manager.rs @@ -277,18 +277,38 @@ impl ViewportManager { /// Returns (row_offset, col_offset) within the viewport, or None if outside pub fn get_crosshair_viewport_position(&self) -> Option<(usize, usize)> { // Check if crosshair is within the current viewport - if self.crosshair_row >= self.viewport_rows.start - && self.crosshair_row < self.viewport_rows.end - && self.crosshair_col >= self.viewport_cols.start - && self.crosshair_col < self.viewport_cols.end + // For rows, standard check + if self.crosshair_row < self.viewport_rows.start + || self.crosshair_row >= self.viewport_rows.end { - Some(( + return None; + } + + // For columns, we need to account for pinned columns + let pinned_count = self.dataview.get_pinned_columns().len(); + + // If crosshair is on a pinned column, it's always visible + if self.crosshair_col < pinned_count { + return Some(( self.crosshair_row - self.viewport_rows.start, - self.crosshair_col - self.viewport_cols.start, - )) - } else { - None + self.crosshair_col, // Pinned columns are always at the start + )); + } + + // For scrollable columns, check if it's in the viewport + // Convert visual column to scrollable column index + let scrollable_col = self.crosshair_col - pinned_count; + if scrollable_col >= self.viewport_cols.start && scrollable_col < self.viewport_cols.end { + // Calculate the visual position in the rendered output + // Pinned columns come first, then the visible scrollable columns + let visual_col_in_viewport = pinned_count + (scrollable_col - self.viewport_cols.start); + return Some(( + self.crosshair_row - self.viewport_rows.start, + visual_col_in_viewport, + )); } + + None } /// Navigate up one row @@ -953,19 +973,75 @@ impl ViewportManager { return Vec::new(); } + // Get pinned columns - they're always visible + let pinned_columns = self.dataview.get_pinned_columns(); + let pinned_count = pinned_columns.len(); + let mut used_width = 0u16; let separator_width = 1u16; + let mut result = Vec::new(); - // Work in visual coordinate space! - // Visual indices are 0, 1, 2, 3... (contiguous, no gaps) - let visual_start = self.viewport_cols.start.min(total_visual_columns); - let mut visual_end = visual_start; + tracing::debug!("[PIN_DEBUG] === calculate_visible_column_indices ==="); + tracing::debug!( + "[PIN_DEBUG] available_width={}, total_visual_columns={}", + available_width, + total_visual_columns + ); + tracing::debug!( + "[PIN_DEBUG] pinned_columns={:?} (count={})", + pinned_columns, + pinned_count + ); + tracing::debug!("[PIN_DEBUG] viewport_cols={:?}", self.viewport_cols); + tracing::debug!("[PIN_DEBUG] display_columns={:?}", display_columns); debug!(target: "viewport_manager", - "calculate_visible_column_indices: available_width={}, total_visual_columns={}, viewport_start={}", - available_width, total_visual_columns, visual_start); + "calculate_visible_column_indices: available_width={}, total_visual_columns={}, pinned_count={}, viewport_start={}", + available_width, total_visual_columns, pinned_count, self.viewport_cols.start); + + // First, always add all pinned columns (they're at the beginning of display_columns) + for visual_idx in 0..pinned_count { + if visual_idx >= display_columns.len() { + break; + } + + let datatable_idx = display_columns[visual_idx]; + let width = self + .column_widths + .get(datatable_idx) + .copied() + .unwrap_or(DEFAULT_COL_WIDTH); + + // Always include pinned columns, even if they exceed available width + used_width += width + separator_width; + result.push(datatable_idx); + tracing::debug!( + "[PIN_DEBUG] Added pinned column: visual_idx={}, datatable_idx={}, width={}", + visual_idx, + datatable_idx, + width + ); + } + + // IMPORTANT FIX: viewport_cols represents SCROLLABLE column indices (0-based, excluding pinned) + // To get the visual column index, we need to add pinned_count to the scrollable index + let scrollable_start = self.viewport_cols.start; + let visual_start = scrollable_start + pinned_count; + + tracing::debug!( + "[PIN_DEBUG] viewport_cols.start={} is SCROLLABLE index", + self.viewport_cols.start + ); + tracing::debug!( + "[PIN_DEBUG] visual_start={} (scrollable_start {} + pinned_count {})", + visual_start, + scrollable_start, + pinned_count + ); + + let visual_start = visual_start.min(total_visual_columns); - // Calculate how many visual columns we can fit starting from visual_start + // Calculate how many columns we can fit from the viewport for visual_idx in visual_start..total_visual_columns { // Get the DataTable index for this visual position let datatable_idx = display_columns[visual_idx]; @@ -978,33 +1054,30 @@ impl ViewportManager { if used_width + width + separator_width <= available_width { used_width += width + separator_width; - visual_end = visual_idx + 1; + result.push(datatable_idx); + tracing::debug!("[PIN_DEBUG] Added scrollable column: visual_idx={}, datatable_idx={}, width={}", visual_idx, datatable_idx, width); } else { + tracing::debug!( + "[PIN_DEBUG] Stopped at visual_idx={} - would exceed width", + visual_idx + ); break; } } - // If we couldn't fit anything, ensure we show at least one column - if visual_end == visual_start && visual_start < total_visual_columns { - visual_end = visual_start + 1; - } - - // Now we need to return DataTable indices for compatibility with the renderer - // (until we fully refactor the renderer to work in visual space) - let mut result = Vec::new(); - for visual_idx in visual_start..visual_end { - if visual_idx < display_columns.len() { - result.push(display_columns[visual_idx]); - } + // If we couldn't fit any scrollable columns but have pinned columns, that's okay + // If we have no columns at all, ensure we show at least one column + if result.is_empty() && total_visual_columns > 0 { + result.push(display_columns[0]); } + tracing::debug!("[PIN_DEBUG] Final result: {:?}", result); + tracing::debug!("[PIN_DEBUG] === End calculate_visible_column_indices ==="); debug!(target: "viewport_manager", - "calculate_visible_column_indices RESULT: visual range {}..{} -> DataTable indices {:?}", - visual_start, visual_end, result); + "calculate_visible_column_indices RESULT: pinned={}, viewport_start={}, visual_start={} -> DataTable indices {:?}", + pinned_count, self.viewport_cols.start, visual_start, result); result - - // Removed the complex optimization logic - we now work with simple ranges } /// Calculate how many columns we can fit starting from a given column index @@ -1674,24 +1747,37 @@ impl ViewportManager { "get_visual_display: Using viewport_rows {:?} -> row_indices: {:?} (first 5)", self.viewport_rows, row_indices.iter().take(5).collect::>()); + // IMPORTANT: Use calculate_visible_column_indices to get the correct columns + // This properly handles pinned columns that should always be visible + let visible_column_indices = self.calculate_visible_column_indices(available_width); + + tracing::debug!( + "[RENDER_DEBUG] visible_column_indices from calculate: {:?}", + visible_column_indices + ); + // Get ALL visual columns from DataView (already filtered for hidden columns) let all_headers = self.dataview.get_display_column_names(); + let display_columns = self.dataview.get_display_columns(); let total_visual_columns = all_headers.len(); debug!(target: "viewport_manager", "get_visual_display: {} total visual columns, viewport: {:?}", total_visual_columns, self.viewport_cols); - // Determine visual range to display - let visual_start = self.viewport_cols.start.min(total_visual_columns); - let visual_end = self.viewport_cols.end.min(total_visual_columns); - - debug!(target: "viewport_manager", - "Showing visual columns {}..{} (of {})", - visual_start, visual_end, total_visual_columns); + // Build headers from the visible column indices (DataTable indices) + let headers: Vec = visible_column_indices + .iter() + .filter_map(|&dt_idx| { + // Find the visual position for this DataTable index + display_columns + .iter() + .position(|&x| x == dt_idx) + .and_then(|visual_idx| all_headers.get(visual_idx).cloned()) + }) + .collect(); - // Get headers for the visual range - let headers: Vec = all_headers[visual_start..visual_end].to_vec(); + tracing::debug!("[RENDER_DEBUG] headers: {:?}", headers); // Get data from DataView in visual column order // IMPORTANT: row_indices contains display row indices (0-based positions in the result set) @@ -1711,20 +1797,29 @@ impl ViewportManager { } } row_data.map(|full_row| { - // Slice to just the visible columns - full_row[visual_start..visual_end.min(full_row.len())].to_vec() + // Extract the columns we need based on visible_column_indices + visible_column_indices + .iter() + .filter_map(|&dt_idx| { + // Find the visual position for this DataTable index + display_columns + .iter() + .position(|&x| x == dt_idx) + .and_then(|visual_idx| full_row.get(visual_idx).cloned()) + }) + .collect() }) }) .collect(); // Get the actual calculated widths for the visible columns - let widths: Vec = (visual_start..visual_end) - .map(|idx| { - if idx < self.column_widths.len() { - self.column_widths[idx] - } else { - DEFAULT_COL_WIDTH - } + let widths: Vec = visible_column_indices + .iter() + .map(|&dt_idx| { + self.column_widths + .get(dt_idx) + .copied() + .unwrap_or(DEFAULT_COL_WIDTH) }) .collect(); @@ -2099,9 +2194,9 @@ impl ViewportManager { // Vim-like behavior: don't wrap, stay at boundary if current_display_index == 0 { // Already at first column, don't move - let first_display_column = display_columns.get(0).copied().unwrap_or(0); + // Already at first column, return visual index 0 return NavigationResult { - column_position: first_display_column, + column_position: 0, // Visual position, not DataTable index scroll_offset: self.viewport_cols.start, description: "Already at first column".to_string(), viewport_changed: false, @@ -2133,8 +2228,7 @@ impl ViewportManager { // Use set_current_column to handle viewport adjustment automatically (this takes DataTable index) let viewport_changed = self.set_current_column(new_display_index); - // Update crosshair to the new visual position - self.crosshair_col = new_display_index; + // crosshair_col is already updated by set_current_column, no need to set it again let column_names = self.dataview.column_names(); let column_name = display_columns @@ -2154,7 +2248,7 @@ impl ViewportManager { old_scroll_offset, self.viewport_cols.start, viewport_changed); NavigationResult { - column_position: new_visual_column, // Return DataTable index for Buffer + column_position: new_display_index, // Return visual/display index scroll_offset: self.viewport_cols.start, description, viewport_changed, @@ -2164,6 +2258,14 @@ impl ViewportManager { /// Navigate one column to the right with intelligent wrapping and scrolling /// IMPORTANT: current_display_position is a logical display position (0,1,2,3...), NOT a DataTable index pub fn navigate_column_right(&mut self, current_display_position: usize) -> NavigationResult { + debug!(target: "viewport_manager", + "=== CRITICAL DEBUG: navigate_column_right CALLED ==="); + debug!(target: "viewport_manager", + "Input current_display_position: {}", current_display_position); + debug!(target: "viewport_manager", + "Current crosshair_col: {}", self.crosshair_col); + debug!(target: "viewport_manager", + "Current viewport_cols: {:?}", self.viewport_cols); // Check viewport lock first - prevent scrolling entirely if self.viewport_lock { debug!(target: "viewport_manager", @@ -2192,20 +2294,52 @@ impl ViewportManager { let display_columns = self.dataview.get_display_columns(); let total_display_columns = display_columns.len(); + let column_names = self.dataview.column_names(); + // Enhanced logging to debug the external_id issue + debug!(target: "viewport_manager", + "=== navigate_column_right DETAILED DEBUG ==="); debug!(target: "viewport_manager", - "navigate_column_right ENTRY: current_display_pos={}, display_columns={:?}", - current_display_position, display_columns); + "ENTRY: current_display_pos={}, total_display_columns={}", + current_display_position, total_display_columns); + debug!(target: "viewport_manager", + "display_columns (DataTable indices): {:?}", display_columns); + + // Log column names at each position + if current_display_position < display_columns.len() { + let current_dt_idx = display_columns[current_display_position]; + let current_name = column_names + .get(current_dt_idx) + .map(|s| s.as_str()) + .unwrap_or("unknown"); + debug!(target: "viewport_manager", + "Current position {} -> column '{}' (dt_idx={})", + current_display_position, current_name, current_dt_idx); + } + + if current_display_position + 1 < display_columns.len() { + let next_dt_idx = display_columns[current_display_position + 1]; + let next_name = column_names + .get(next_dt_idx) + .map(|s| s.as_str()) + .unwrap_or("unknown"); + debug!(target: "viewport_manager", + "Next position {} -> column '{}' (dt_idx={})", + current_display_position + 1, next_name, next_dt_idx); + } // Validate current position let current_display_index = if current_display_position < total_display_columns { current_display_position } else { + debug!(target: "viewport_manager", + "WARNING: current_display_position {} >= total_display_columns {}, resetting to 0", + current_display_position, total_display_columns); 0 // Reset to first if out of bounds }; debug!(target: "viewport_manager", - "navigate_column_right: using display_index={}", + "Validated: current_display_index={}", current_display_index); // Calculate new display position (move right without wrapping) @@ -2213,12 +2347,11 @@ impl ViewportManager { if current_display_index + 1 >= total_display_columns { // Already at last column, don't move let last_display_index = total_display_columns.saturating_sub(1); - let last_visual_column = display_columns - .get(last_display_index) - .copied() - .unwrap_or(0); + debug!(target: "viewport_manager", + "At last column boundary: current={}, total={}, returning last_display_index={}", + current_display_index, total_display_columns, last_display_index); return NavigationResult { - column_position: last_visual_column, + column_position: last_display_index, // Return visual/display index scroll_offset: self.viewport_cols.start, description: "Already at last column".to_string(), viewport_changed: false, @@ -2232,8 +2365,17 @@ impl ViewportManager { .get(new_display_index) .copied() .unwrap_or_else(|| { - // Fallback: if something goes wrong, use first column - display_columns.get(0).copied().unwrap_or(0) + // This fallback should never be hit since we already checked bounds + tracing::error!( + "[NAV_ERROR] Failed to get display column at index {}, total={}", + new_display_index, + display_columns.len() + ); + // Return the current column instead of wrapping to first + display_columns + .get(current_display_index) + .copied() + .unwrap_or(0) }); debug!(target: "viewport_manager", @@ -2251,17 +2393,17 @@ impl ViewportManager { "navigate_column_right: moving to datatable_column={}, current viewport={:?}", new_visual_column, self.viewport_cols); - // Use set_current_column to handle viewport adjustment automatically (this takes DataTable index) + // Use set_current_column to handle viewport adjustment automatically + // IMPORTANT: set_current_column expects a VISUAL index, and we're passing new_display_index which IS a visual index debug!(target: "viewport_manager", - "navigate_column_right: before set_current_column({}), viewport={:?}", - new_visual_column, self.viewport_cols); + "navigate_column_right: before set_current_column(visual_idx={}), viewport={:?}", + new_display_index, self.viewport_cols); let viewport_changed = self.set_current_column(new_display_index); debug!(target: "viewport_manager", - "navigate_column_right: after set_current_column({}), viewport={:?}, changed={}", - new_visual_column, self.viewport_cols, viewport_changed); + "navigate_column_right: after set_current_column(visual_idx={}), viewport={:?}, changed={}", + new_display_index, self.viewport_cols, viewport_changed); - // Update crosshair to the new visual position - self.crosshair_col = new_display_index; + // crosshair_col is already updated by set_current_column, no need to set it again let column_names = self.dataview.column_names(); let column_name = display_columns @@ -2275,13 +2417,27 @@ impl ViewportManager { new_display_index + 1 ); + // Final logging with clear indication of what we're returning + debug!(target: "viewport_manager", + "=== navigate_column_right RESULT ==="); + debug!(target: "viewport_manager", + "Returning: column_position={} (visual/display index)", new_display_index); + debug!(target: "viewport_manager", + "Movement: {} -> {} (visual indices)", current_display_index, new_display_index); + debug!(target: "viewport_manager", + "Viewport: {:?}, changed={}", self.viewport_cols, viewport_changed); + debug!(target: "viewport_manager", + "Description: {}", description); + + tracing::debug!("[NAV_DEBUG] Final result: column_position={} (visual/display idx), viewport_changed={}", + new_display_index, viewport_changed); debug!(target: "viewport_manager", "navigate_column_right EXIT: display_pos {}→{}, datatable_col: {}, viewport: {:?}, scroll: {}→{}, viewport_changed={}", current_display_index, new_display_index, new_visual_column, self.viewport_cols, old_scroll_offset, self.viewport_cols.start, viewport_changed); NavigationResult { - column_position: new_visual_column, // Return DataTable index for Buffer + column_position: new_display_index, // Return visual/display index scroll_offset: self.viewport_cols.start, description, viewport_changed, @@ -3375,6 +3531,18 @@ impl ViewportManager { let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH); // Account for borders let total_visual_columns = self.dataview.get_display_columns().len(); + tracing::debug!("[PIN_DEBUG] === set_current_column ==="); + tracing::debug!( + "[PIN_DEBUG] visual_column={}, viewport_cols={:?}", + visual_column, + self.viewport_cols + ); + tracing::debug!( + "[PIN_DEBUG] terminal_width={}, total_visual_columns={}", + terminal_width, + total_visual_columns + ); + debug!(target: "viewport_manager", "set_current_column ENTRY: visual_column={}, current_viewport={:?}, terminal_width={}, total_visual={}", visual_column, self.viewport_cols, terminal_width, total_visual_columns); @@ -3382,12 +3550,18 @@ impl ViewportManager { // Validate the visual column if visual_column >= total_visual_columns { debug!(target: "viewport_manager", "Visual column {} out of bounds (max {})", visual_column, total_visual_columns); + tracing::debug!( + "[PIN_DEBUG] Column {} out of bounds (max {})", + visual_column, + total_visual_columns + ); return false; } // Update the crosshair position self.crosshair_col = visual_column; debug!(target: "viewport_manager", "Updated crosshair_col to {}", visual_column); + tracing::debug!("[PIN_DEBUG] Updated crosshair_col to {}", visual_column); // Check if we're in optimal layout mode (all columns fit) // This needs to calculate based on visual columns @@ -3406,11 +3580,36 @@ impl ViewportManager { // All columns fit - no viewport adjustment needed, all columns are visible debug!(target: "viewport_manager", "Visual column {} in optimal layout mode (all columns fit), no adjustment needed", visual_column); + tracing::debug!("[PIN_DEBUG] All columns fit, no adjustment needed"); + tracing::debug!("[PIN_DEBUG] === End set_current_column (all fit) ==="); return false; } // Check if the visual column is already visible in the viewport - let is_visible = self.viewport_cols.contains(&visual_column); + // We need to check what's ACTUALLY visible, not just what's in the viewport range + let pinned_count = self.dataview.get_pinned_columns().len(); + tracing::debug!("[PIN_DEBUG] pinned_count={}", pinned_count); + + // Calculate which columns are actually visible with the current viewport + let visible_columns = self.calculate_visible_column_indices(terminal_width); + let display_columns = self.dataview.get_display_columns(); + + // Check if the target visual column's DataTable index is in the visible set + let target_dt_idx = if visual_column < display_columns.len() { + display_columns[visual_column] + } else { + tracing::debug!("[PIN_DEBUG] Column {} out of bounds", visual_column); + return false; + }; + + let is_visible = visible_columns.contains(&target_dt_idx); + tracing::debug!( + "[PIN_DEBUG] Column {} (dt_idx={}) visible check: visible_columns={:?}, is_visible={}", + visual_column, + target_dt_idx, + visible_columns, + is_visible + ); debug!(target: "viewport_manager", "set_current_column CHECK: visual_column={}, viewport={:?}, is_visible={}", @@ -3419,6 +3618,8 @@ impl ViewportManager { if is_visible { debug!(target: "viewport_manager", "Visual column {} already visible in viewport {:?}, no adjustment needed", visual_column, self.viewport_cols); + tracing::debug!("[PIN_DEBUG] Column already visible, no adjustment"); + tracing::debug!("[PIN_DEBUG] === End set_current_column (no change) ==="); return false; } @@ -3431,13 +3632,31 @@ impl ViewportManager { new_scroll_offset, old_scroll_offset); if new_scroll_offset != old_scroll_offset { - // Calculate how many columns fit from the new offset + // Calculate how many scrollable columns fit from the new offset + // This is similar logic to calculate_visible_column_indices let display_columns = self.dataview.get_display_columns(); - let mut new_end = new_scroll_offset; + let pinned_count = self.dataview.get_pinned_columns().len(); let mut used_width = 0u16; let separator_width = 1u16; - for visual_idx in new_scroll_offset..display_columns.len() { + // First account for pinned column widths + for visual_idx in 0..pinned_count { + if visual_idx < display_columns.len() { + let dt_idx = display_columns[visual_idx]; + let width = self + .column_widths + .get(dt_idx) + .copied() + .unwrap_or(DEFAULT_COL_WIDTH); + used_width += width + separator_width; + } + } + + // Now calculate how many scrollable columns fit + let mut scrollable_columns_that_fit = 0; + let visual_start = pinned_count + new_scroll_offset; + + for visual_idx in visual_start..display_columns.len() { let dt_idx = display_columns[visual_idx]; let width = self .column_widths @@ -3446,12 +3665,14 @@ impl ViewportManager { .unwrap_or(DEFAULT_COL_WIDTH); if used_width + width + separator_width <= terminal_width { used_width += width + separator_width; - new_end = visual_idx + 1; + scrollable_columns_that_fit += 1; } else { break; } } + // viewport_cols represents scrollable columns only + let new_end = new_scroll_offset + scrollable_columns_that_fit; self.viewport_cols = new_scroll_offset..new_end; self.cache_dirty = true; // Mark cache as dirty since viewport changed @@ -3486,41 +3707,121 @@ impl ViewportManager { } /// Calculate the optimal scroll offset to keep a visual column visible + /// Returns scroll offset in terms of scrollable columns (excluding pinned) fn calculate_scroll_offset_for_visual_column(&mut self, visual_column: usize) -> usize { - let current_offset = self.viewport_cols.start; - let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH); // Account for borders + debug!(target: "viewport_manager", + "=== calculate_scroll_offset_for_visual_column ENTRY ==="); + debug!(target: "viewport_manager", + "visual_column={}, current_viewport={:?}", visual_column, self.viewport_cols); + + let pinned_count = self.dataview.get_pinned_columns().len(); + debug!(target: "viewport_manager", + "pinned_count={}", pinned_count); + + // If it's a pinned column, it's always visible, no scrolling needed + if visual_column < pinned_count { + debug!(target: "viewport_manager", + "Visual column {} is pinned, returning current offset {}", + visual_column, self.viewport_cols.start); + return self.viewport_cols.start; // Keep current offset + } + + // Convert to scrollable column index + let scrollable_column = visual_column - pinned_count; + debug!(target: "viewport_manager", + "Converted to scrollable_column={}", scrollable_column); - // Calculate how many columns fit from current offset + let current_scroll_offset = self.viewport_cols.start; + let terminal_width = self.terminal_width.saturating_sub(TABLE_BORDER_WIDTH); + + // Calculate how much width pinned columns use let display_columns = self.dataview.get_display_columns(); - let mut columns_that_fit = 0; - let mut used_width = 0u16; + let mut pinned_width = 0u16; let separator_width = 1u16; - for visual_idx in current_offset..display_columns.len() { - let dt_idx = display_columns[visual_idx]; - let width = self - .column_widths - .get(dt_idx) - .copied() - .unwrap_or(DEFAULT_COL_WIDTH); - if used_width + width + separator_width <= terminal_width { - used_width += width + separator_width; - columns_that_fit += 1; - } else { - break; + for visual_idx in 0..pinned_count { + if visual_idx < display_columns.len() { + let dt_idx = display_columns[visual_idx]; + let width = self + .column_widths + .get(dt_idx) + .copied() + .unwrap_or(DEFAULT_COL_WIDTH); + pinned_width += width + separator_width; } } - // Smart scrolling logic in visual space - if visual_column < current_offset { + // Available width for scrollable columns + let available_for_scrollable = terminal_width.saturating_sub(pinned_width); + + debug!(target: "viewport_manager", + "Scroll offset calculation: target_scrollable_col={}, current_offset={}, available_width={}", + scrollable_column, current_scroll_offset, available_for_scrollable); + + // Smart scrolling logic in scrollable column space + if scrollable_column < current_scroll_offset { // Column is to the left of viewport, scroll left to show it - visual_column - } else if columns_that_fit > 0 && visual_column >= current_offset + columns_that_fit { - // Column is to the right of viewport, scroll right to show it - visual_column.saturating_sub(columns_that_fit - 1) + debug!(target: "viewport_manager", "Column {} is left of viewport, scrolling left to offset {}", + scrollable_column, scrollable_column); + scrollable_column } else { - // Column is already visible, keep current offset - current_offset + // Column is to the right or at current position, find optimal scroll offset + // Work backwards from the target column to find how many columns fit + let mut test_scroll_offset = scrollable_column; + let mut found_valid_offset = false; + + // Try different scroll offsets, working backwards from the target column + while test_scroll_offset > 0 { + let mut used_width = 0u16; + let mut columns_fit = 0; + let mut target_column_fits = false; + + // Test if we can fit columns starting from this scroll offset + for test_scrollable_idx in + test_scroll_offset..display_columns.len().saturating_sub(pinned_count) + { + let visual_idx = pinned_count + test_scrollable_idx; + if visual_idx < display_columns.len() { + let dt_idx = display_columns[visual_idx]; + let width = self + .column_widths + .get(dt_idx) + .copied() + .unwrap_or(DEFAULT_COL_WIDTH); + + if used_width + width + separator_width <= available_for_scrollable { + used_width += width + separator_width; + columns_fit += 1; + if test_scrollable_idx == scrollable_column { + target_column_fits = true; + } + } else { + break; + } + } + } + + debug!(target: "viewport_manager", + "Testing scroll_offset={}: columns_fit={}, target_fits={}, used_width={}", + test_scroll_offset, columns_fit, target_column_fits, used_width); + + if target_column_fits { + found_valid_offset = true; + break; + } + + test_scroll_offset = test_scroll_offset.saturating_sub(1); + } + + if found_valid_offset { + debug!(target: "viewport_manager", + "Found optimal scroll offset {} for column {}", test_scroll_offset, scrollable_column); + test_scroll_offset + } else { + debug!(target: "viewport_manager", + "Could not find valid offset, keeping current offset {}", current_scroll_offset); + current_scroll_offset + } } } diff --git a/sql-cli/test_exact_issue.sh b/sql-cli/test_exact_issue.sh new file mode 100755 index 00000000..709f6384 --- /dev/null +++ b/sql-cli/test_exact_issue.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +echo "Testing exact navigation issue..." +echo "1. Navigate to column 20 (col20)" +echo "2. Press 'l' to go right" +echo "3. Should go to column 21 (col21), not back to column 0" +echo "" + +# Navigate to column 20, then press l +seq="" +for i in {1..20}; do + seq="${seq}l" +done +seq="${seq}lq" # One more 'l' to trigger the issue, then quit + +echo -e "$seq" | RUST_LOG=sql_cli::ui::viewport_manager=debug timeout 2 ./target/release/sql-cli test_nav_simple.csv -e "select * from data" 2>&1 | grep -E "(navigate_column_right|Movement:|Returning:|WARNING)" | tail -10 + +echo "" +echo "Without pinned columns:" +echo -e "$seq" | timeout 2 ./target/release/sql-cli test_nav_simple.csv -e "select * from data" 2>&1 | grep -i "column" | tail -5 \ No newline at end of file diff --git a/sql-cli/test_nav_issue.sh b/sql-cli/test_nav_issue.sh new file mode 100644 index 00000000..eb83106f --- /dev/null +++ b/sql-cli/test_nav_issue.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +echo "Testing column 20 -> 21 navigation issue..." + +# Create test data +cat > test_nav_issue.csv << 'EOF' +c0,c1,c2,c3,c4,c5,c6,c7,c8,c9,c10,c11,c12,c13,c14,c15,c16,c17,c18,c19,c20,c21,c22,c23,c24,c25 +a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25 +EOF + +# Navigate to column 20 then press l once more +nav_sequence="" +for i in {1..20}; do + nav_sequence="${nav_sequence}l" +done +nav_sequence="${nav_sequence}lq" # One more l, then quit + +echo -e "$nav_sequence" | RUST_LOG=sql_cli::ui::viewport_manager=debug,sql_cli::ui::traits::column_ops=debug timeout 2 ./target/release/sql-cli test_nav_issue.csv -e "select * from data" 2>&1 | grep -E "(CRITICAL DEBUG|COLUMN_OPS|Input current_display|navigation result|applying result)" | tail -20 + +rm test_nav_issue.csv \ No newline at end of file diff --git a/sql-cli/test_nav_wrapping.sh b/sql-cli/test_nav_wrapping.sh new file mode 100755 index 00000000..d97e17fd --- /dev/null +++ b/sql-cli/test_nav_wrapping.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +echo "Testing navigation wrapping issue at specific columns..." + +# Create test data with enough columns to reproduce the issue +cat > test_wrap_issue.csv << 'EOF' +col1,col2,col3,col4,col5,col6,col7,col8,col9,col10,col11,col12,col13,col14,col15,col16,col17,col18,col19,col20,col21,col22,col23,col24,col25 +a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25 +b1,b2,b3,b4,b5,b6,b7,b8,b9,b10,b11,b12,b13,b14,b15,b16,b17,b18,b19,b20,b21,b22,b23,b24,b25 +EOF + +echo "Test 1: Navigate to column 20-21 and press right arrow" +echo "Expected: Move to column 21-22" +echo "Actual behavior will be shown in logs..." +echo "" + +# Navigate right 20 times to get to column 20/21, then press right again +navigation_sequence="" +for i in {1..21}; do + navigation_sequence="${navigation_sequence}l" +done +navigation_sequence="${navigation_sequence}q" + +echo -e "$navigation_sequence" | RUST_LOG=sql_cli::ui::viewport_manager=debug timeout 3 ./target/release/sql-cli test_wrap_issue.csv -e "select * from data" 2>&1 | grep -E "(navigate_column_right|WARNING|RESULT|Movement:)" | tail -30 + +rm test_wrap_issue.csv \ No newline at end of file diff --git a/sql-cli/test_navigation_fix.sh b/sql-cli/test_navigation_fix.sh new file mode 100644 index 00000000..c30d9a92 --- /dev/null +++ b/sql-cli/test_navigation_fix.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +echo "Testing navigation with pinned columns..." + +# Create test data with many columns to force scrolling +cat > test_nav_many_cols.csv << 'EOF' +id,name,book,category,price,quantity,status,location,description,notes,timestamp,user,external_id,fees,total +1,Alice,Book1,Fiction,19.99,5,Available,StoreA,Great story,Bestseller,2024-01-01,admin,EXT001,2.50,22.49 +2,Bob,Book2,Science,29.99,3,Available,StoreB,Educational,Popular,2024-01-02,user1,EXT002,3.00,32.99 +3,Charlie,Book3,History,24.99,7,Sold,StoreC,Interesting,Classic,2024-01-03,user2,EXT003,2.75,27.74 +EOF + +echo "Running with debug logging to trace navigation..." +echo -e "p\nlllllllllll\nq" | RUST_LOG=sql_cli::ui::viewport_manager=debug timeout 3 ./target/release/sql-cli test_nav_many_cols.csv -e "select * from data" 2>&1 | grep -E "(navigate_column_right|column_position|display_pos|datatable_col|visual/display)" | tail -20 + +echo "" +echo "Checking specific issue at external_id column..." +# Pin book column, navigate to external_id, then press l to see if it goes to fees or back to book +echo -e "p\nlllllllllll\nq" | timeout 3 ./target/release/sql-cli test_nav_many_cols.csv -e "select * from data" 2>&1 | grep -E "Navigate|column '|selected" | tail -10 + +rm test_nav_many_cols.csv \ No newline at end of file diff --git a/sql-cli/test_pin_columns_fix.sh b/sql-cli/test_pin_columns_fix.sh new file mode 100644 index 00000000..f256d04f --- /dev/null +++ b/sql-cli/test_pin_columns_fix.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +echo "Testing Pin Columns Feature" +echo "============================" +echo "" + +# Create test data with many columns +cat > test_pin_wide.csv << EOF +id,name,category,price,quantity,status,date,location,description,notes +1,Apple,Fruit,2.50,100,Available,2024-01-01,Store A,Fresh apples,Good quality +2,Banana,Fruit,1.25,150,Available,2024-01-02,Store B,Yellow bananas,Ripe +3,Carrot,Vegetable,0.75,200,Available,2024-01-03,Store C,Orange carrots,Fresh +4,Desk,Furniture,150.00,10,Available,2024-01-04,Store D,Wooden desk,Sturdy +5,Eggs,Dairy,3.99,50,Low Stock,2024-01-05,Store A,Dozen eggs,Organic +EOF + +echo "Test 1: Pin a column and verify it stays visible when scrolling" +echo "----------------------------------------------------------------" +echo "1. Load CSV with multiple columns" +echo "2. Navigate to 'name' column (press 'l' once)" +echo "3. Pin the column (press 'p')" +echo "4. Scroll right multiple times (press right arrow)" +echo "5. The 'name' column should remain visible on the left with pin indicator" +echo "" + +# Run with debug logging for pin operations +RUST_LOG=sql_cli::data::data_view=debug,sql_cli::ui::viewport_manager=debug timeout 3 ./target/release/sql-cli test_pin_wide.csv -e "select * from data" 2>&1 | grep -i "pin" | head -10 + +echo "" +echo "Test 2: Check viewport calculation with pinned columns" +echo "------------------------------------------------------" +RUST_LOG=viewport_manager=debug timeout 2 ./target/release/sql-cli test_pin_wide.csv -e "select * from data" 2>&1 | grep "calculate_visible_column" | head -5 + +echo "" +echo "Test 3: Visual indicators" +echo "-------------------------" +echo "Expected visual features:" +echo "- Pinned columns have 📌 indicator in header" +echo "- Darker blue background for pinned columns" +echo "- Vertical separator │ between pinned and scrollable columns" +echo "- Pinned columns stay fixed when scrolling horizontally" +echo "" + +echo "Manual Test Instructions:" +echo "1. Run: ./target/release/sql-cli test_pin_wide.csv" +echo "2. Press 'l' to move to 'name' column" +echo "3. Press 'p' to pin the column" +echo "4. Press right arrow multiple times to scroll" +echo "5. Verify 'name' column stays visible on left" +echo "6. Press 'P' (Shift+P) to clear all pins" +echo "" + +# Clean up +rm -f test_pin_wide.csv + +echo "Test complete!" \ No newline at end of file diff --git a/sql-cli/test_pin_debug.sh b/sql-cli/test_pin_debug.sh new file mode 100755 index 00000000..ae5438e7 --- /dev/null +++ b/sql-cli/test_pin_debug.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo "Testing pin columns with debug output..." + +# Test with a simpler approach - just send 'p' key and look for any response +echo -e "llp\nq" | RUST_LOG=debug timeout 2 ./target/release/sql-cli test_pin_columns.csv -e "select * from data" 2>&1 | grep -E "(pin|Pin|toggle|Toggle|column action|handle_column)" | head -20 + +echo "" +echo "Checking viewport manager for pinned columns handling..." +grep -n "pinned" src/ui/viewport_manager.rs | head -10 \ No newline at end of file diff --git a/sql-cli/test_pin_interactive.exp b/sql-cli/test_pin_interactive.exp new file mode 100755 index 00000000..5c4c7e11 --- /dev/null +++ b/sql-cli/test_pin_interactive.exp @@ -0,0 +1,32 @@ +#!/usr/bin/expect -f +set timeout 5 + +# Start the application with debug logging +spawn env RUST_LOG=sql_cli::data::data_view=debug,sql_cli::ui::viewport_manager=debug ./target/release/sql-cli test_pin_columns.csv -e "select * from data" + +# Wait for TUI to load +sleep 0.5 + +# Navigate to the second column (name) +send "l" +sleep 0.2 + +# Pin the current column +send "p" +sleep 0.5 + +# Navigate right a few columns +send "lll" +sleep 0.2 + +# Try to scroll right to see if pinned column stays +send "\[C\[C\[C" +sleep 0.5 + +# Clear all pins +send "P" +sleep 0.5 + +# Exit +send "q" +expect eof \ No newline at end of file diff --git a/sql-cli/test_pin_simple.sh b/sql-cli/test_pin_simple.sh new file mode 100755 index 00000000..bd634c05 --- /dev/null +++ b/sql-cli/test_pin_simple.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo "Testing pin columns feature..." + +# Create a simple test with debug logging +echo "p" | RUST_LOG=sql_cli::data::data_view=debug,sql_cli::ui::viewport_manager=debug timeout 2 ./target/release/sql-cli test_pin_columns.csv -e "select * from data" 2>&1 | grep -i "pin" + +echo "" +echo "Checking if pin action is registered..." +grep -r "pin_column" src/handlers/ src/action.rs src/ui/enhanced_tui.rs | head -10 \ No newline at end of file