sql-cli 1.67.2

SQL query tool for CSV/JSON with both interactive TUI and non-interactive CLI modes - perfect for exploration and automation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
use crate::data::data_view::DataView;
use crate::ui::viewport_manager::ViewportManager;
use tracing::{debug, error, info, warn};

/// Represents a search match in the data
#[derive(Debug, Clone)]
pub struct SearchMatch {
    pub row: usize,
    pub col: usize,
    pub value: String,
}

/// State for vim-like search mode
#[derive(Debug, Clone)]
pub enum VimSearchState {
    /// Not in search mode
    Inactive,
    /// Typing search pattern (/ mode)
    Typing { pattern: String },
    /// Search confirmed, navigating matches (after Enter)
    Navigating {
        pattern: String,
        matches: Vec<SearchMatch>,
        current_index: usize,
    },
}

/// Manages vim-like forward search behavior
pub struct VimSearchManager {
    state: VimSearchState,
    case_sensitive: bool,
    last_search_pattern: Option<String>,
}

impl Default for VimSearchManager {
    fn default() -> Self {
        Self::new()
    }
}

impl VimSearchManager {
    #[must_use]
    pub fn new() -> Self {
        Self {
            state: VimSearchState::Inactive,
            case_sensitive: false,
            last_search_pattern: None,
        }
    }

    /// Start search mode (when / is pressed)
    pub fn start_search(&mut self) {
        info!(target: "vim_search", "Starting vim search mode");
        self.state = VimSearchState::Typing {
            pattern: String::new(),
        };
    }

    /// Update search pattern and find first match dynamically
    pub fn update_pattern(
        &mut self,
        pattern: String,
        dataview: &DataView,
        viewport: &mut ViewportManager,
    ) -> Option<SearchMatch> {
        debug!(target: "vim_search", "Updating pattern to: '{}'", pattern);

        // Update state to typing mode with new pattern
        self.state = VimSearchState::Typing {
            pattern: pattern.clone(),
        };

        if pattern.is_empty() {
            return None;
        }

        // Find all matches
        let matches = self.find_matches(&pattern, dataview);

        if let Some(first_match) = matches.first() {
            debug!(target: "vim_search", 
                "Found {} matches, navigating to first at ({}, {})", 
                matches.len(), first_match.row, first_match.col);

            // Navigate to first match and ensure it's visible
            self.navigate_to_match(first_match, viewport);
            Some(first_match.clone())
        } else {
            debug!(target: "vim_search", "No matches found for pattern: '{}'", pattern);
            None
        }
    }

    /// Confirm search (when Enter is pressed) - enter navigation mode
    pub fn confirm_search(&mut self, dataview: &DataView, viewport: &mut ViewportManager) -> bool {
        if let VimSearchState::Typing { pattern } = &self.state {
            if pattern.is_empty() {
                info!(target: "vim_search", "Empty pattern, canceling search");
                self.cancel_search();
                return false;
            }

            let pattern = pattern.clone();
            let matches = self.find_matches(&pattern, dataview);

            if matches.is_empty() {
                warn!(target: "vim_search", "No matches found for pattern: '{}'", pattern);
                self.cancel_search();
                return false;
            }

            info!(target: "vim_search", 
                "Confirming search with {} matches for pattern: '{}'", 
                matches.len(), pattern);

            // Navigate to first match
            if let Some(first_match) = matches.first() {
                self.navigate_to_match(first_match, viewport);
            }

            // Enter navigation mode
            self.state = VimSearchState::Navigating {
                pattern: pattern.clone(),
                matches,
                current_index: 0,
            };
            self.last_search_pattern = Some(pattern);
            true
        } else {
            warn!(target: "vim_search", "confirm_search called in wrong state: {:?}", self.state);
            false
        }
    }

    /// Navigate to next match (n key)
    pub fn next_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
        // First, update the index and get the match
        let match_to_navigate = if let VimSearchState::Navigating {
            matches,
            current_index,
            pattern,
        } = &mut self.state
        {
            if matches.is_empty() {
                return None;
            }

            // Log current state before moving
            info!(target: "vim_search", 
                "=== 'n' KEY PRESSED - BEFORE NAVIGATION ===");
            info!(target: "vim_search", 
                "Current match index: {}/{}, Pattern: '{}'", 
                *current_index + 1, matches.len(), pattern);
            info!(target: "vim_search", 
                "Current viewport - rows: {:?}, cols: {:?}", 
                viewport.get_viewport_rows(), viewport.viewport_cols());
            info!(target: "vim_search", 
                "Current crosshair position: row={}, col={}", 
                viewport.get_crosshair_row(), viewport.get_crosshair_col());

            // Wrap around to beginning
            *current_index = (*current_index + 1) % matches.len();
            let match_item = matches[*current_index].clone();

            info!(target: "vim_search", 
                "=== NEXT MATCH DETAILS ===");
            info!(target: "vim_search", 
                "Match {}/{}: row={}, visual_col={}, stored_value='{}'", 
                *current_index + 1, matches.len(),
                match_item.row, match_item.col, match_item.value);

            // Double-check: Does this value actually contain our pattern?
            if !match_item
                .value
                .to_lowercase()
                .contains(&pattern.to_lowercase())
            {
                error!(target: "vim_search",
                    "CRITICAL ERROR: Match value '{}' does NOT contain search pattern '{}'!",
                    match_item.value, pattern);
                error!(target: "vim_search",
                    "This indicates the search index is corrupted or stale!");
            }

            // Log what we expect to find at this position
            info!(target: "vim_search", 
                "Expected: Cell at row {} col {} should contain substring '{}'", 
                match_item.row, match_item.col, pattern);

            // Verify the stored match actually contains the pattern
            let stored_contains = match_item
                .value
                .to_lowercase()
                .contains(&pattern.to_lowercase());
            if stored_contains {
                info!(target: "vim_search",
                    "✓ Stored match '{}' contains pattern '{}'",
                    match_item.value, pattern);
            } else {
                warn!(target: "vim_search",
                    "CRITICAL: Stored match '{}' does NOT contain pattern '{}'!",
                    match_item.value, pattern);
            }

            Some(match_item)
        } else {
            debug!(target: "vim_search", "next_match called but not in navigation mode");
            None
        };

        // Then navigate to it if we have a match
        if let Some(ref match_item) = match_to_navigate {
            info!(target: "vim_search", 
                "=== NAVIGATING TO MATCH ===");
            self.navigate_to_match(match_item, viewport);

            // Log state after navigation
            info!(target: "vim_search", 
                "=== AFTER NAVIGATION ===");
            info!(target: "vim_search", 
                "New viewport - rows: {:?}, cols: {:?}", 
                viewport.get_viewport_rows(), viewport.viewport_cols());
            info!(target: "vim_search", 
                "New crosshair position: row={}, col={}", 
                viewport.get_crosshair_row(), viewport.get_crosshair_col());
            info!(target: "vim_search", 
                "Crosshair should be at: row={}, col={} (visual coordinates)", 
                match_item.row, match_item.col);
        }

        match_to_navigate
    }

    /// Navigate to previous match (N key)
    pub fn previous_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
        // First, update the index and get the match
        let match_to_navigate = if let VimSearchState::Navigating {
            matches,
            current_index,
            pattern: _,
        } = &mut self.state
        {
            if matches.is_empty() {
                return None;
            }

            // Wrap around to end
            *current_index = if *current_index == 0 {
                matches.len() - 1
            } else {
                *current_index - 1
            };

            let match_item = matches[*current_index].clone();

            info!(target: "vim_search", 
                "Navigating to previous match {}/{} at ({}, {})", 
                *current_index + 1, matches.len(), match_item.row, match_item.col);

            Some(match_item)
        } else {
            debug!(target: "vim_search", "previous_match called but not in navigation mode");
            None
        };

        // Then navigate to it if we have a match
        if let Some(ref match_item) = match_to_navigate {
            self.navigate_to_match(match_item, viewport);
        }

        match_to_navigate
    }

    /// Cancel search and return to normal mode
    pub fn cancel_search(&mut self) {
        info!(target: "vim_search", "Canceling search, returning to inactive state");
        self.state = VimSearchState::Inactive;
    }

    /// Clear all search state and return to inactive mode
    pub fn clear(&mut self) {
        info!(target: "vim_search", "Clearing all search state");
        self.state = VimSearchState::Inactive;
        self.last_search_pattern = None;
    }

    /// Exit navigation mode but keep search pattern for later
    pub fn exit_navigation(&mut self) {
        if let VimSearchState::Navigating { pattern, .. } = &self.state {
            self.last_search_pattern = Some(pattern.clone());
        }
        self.state = VimSearchState::Inactive;
    }

    /// Resume search with last pattern (for repeating search with /)
    pub fn resume_last_search(
        &mut self,
        dataview: &DataView,
        viewport: &mut ViewportManager,
    ) -> bool {
        if let Some(pattern) = &self.last_search_pattern {
            let pattern = pattern.clone();
            let matches = self.find_matches(&pattern, dataview);

            if matches.is_empty() {
                false
            } else {
                info!(target: "vim_search", 
                    "Resuming search with pattern '{}', found {} matches", 
                    pattern, matches.len());

                // Navigate to first match
                if let Some(first_match) = matches.first() {
                    self.navigate_to_match(first_match, viewport);
                }

                self.state = VimSearchState::Navigating {
                    pattern,
                    matches,
                    current_index: 0,
                };
                true
            }
        } else {
            false
        }
    }

    /// Check if currently in search mode
    #[must_use]
    pub fn is_active(&self) -> bool {
        !matches!(self.state, VimSearchState::Inactive)
    }

    /// Check if in typing mode
    #[must_use]
    pub fn is_typing(&self) -> bool {
        matches!(self.state, VimSearchState::Typing { .. })
    }

    /// Check if in navigation mode
    #[must_use]
    pub fn is_navigating(&self) -> bool {
        matches!(self.state, VimSearchState::Navigating { .. })
    }

    /// Get current pattern
    #[must_use]
    pub fn get_pattern(&self) -> Option<String> {
        match &self.state {
            VimSearchState::Typing { pattern } => Some(pattern.clone()),
            VimSearchState::Navigating { pattern, .. } => Some(pattern.clone()),
            VimSearchState::Inactive => None,
        }
    }

    /// Get current match info for status display
    #[must_use]
    pub fn get_match_info(&self) -> Option<(usize, usize)> {
        match &self.state {
            VimSearchState::Navigating {
                matches,
                current_index,
                ..
            } => Some((*current_index + 1, matches.len())),
            _ => None,
        }
    }

    /// Reset to first match (for 'g' key)
    pub fn reset_to_first_match(&mut self, viewport: &mut ViewportManager) -> Option<SearchMatch> {
        if let VimSearchState::Navigating {
            matches,
            current_index,
            ..
        } = &mut self.state
        {
            if matches.is_empty() {
                return None;
            }

            // Reset to first match
            *current_index = 0;
            let first_match = matches[0].clone();

            info!(target: "vim_search", 
                "Resetting to first match at ({}, {})", 
                first_match.row, first_match.col);

            // Navigate to the first match
            self.navigate_to_match(&first_match, viewport);
            Some(first_match)
        } else {
            debug!(target: "vim_search", "reset_to_first_match called but not in navigation mode");
            None
        }
    }

    /// Find all matches in the dataview
    fn find_matches(&self, pattern: &str, dataview: &DataView) -> Vec<SearchMatch> {
        let mut matches = Vec::new();
        let pattern_lower = if self.case_sensitive {
            pattern.to_string()
        } else {
            pattern.to_lowercase()
        };

        info!(target: "vim_search", 
            "=== FIND_MATCHES CALLED ===");
        info!(target: "vim_search", 
            "Pattern passed in: '{}', pattern_lower: '{}', case_sensitive: {}", 
            pattern, pattern_lower, self.case_sensitive);

        // Get the display column indices to map enumeration index to actual column index
        let display_columns = dataview.get_display_columns();
        debug!(target: "vim_search", 
            "Display columns mapping: {:?} (count: {})", 
            display_columns, display_columns.len());

        // Search through all visible data
        for row_idx in 0..dataview.row_count() {
            if let Some(row) = dataview.get_row(row_idx) {
                let mut first_match_in_row: Option<SearchMatch> = None;

                // The row.values are in display order
                for (enum_idx, value) in row.values.iter().enumerate() {
                    let value_str = value.to_string();
                    let search_value = if self.case_sensitive {
                        value_str.clone()
                    } else {
                        value_str.to_lowercase()
                    };

                    if search_value.contains(&pattern_lower) {
                        // For vim-like behavior, we prioritize the first match in each row
                        // This prevents jumping between columns on the same row
                        if first_match_in_row.is_none() {
                            // IMPORTANT: The enum_idx is the position in row.values array,
                            // which corresponds to the position in display_columns.
                            // Since we're searching in visual/display order, we use enum_idx directly
                            // as the visual column index for the viewport to understand.

                            // Map enum_idx back to the actual DataTable column for debugging
                            let actual_col = if enum_idx < display_columns.len() {
                                display_columns[enum_idx]
                            } else {
                                enum_idx // Fallback, shouldn't happen
                            };

                            info!(target: "vim_search", 
                                "Found first match in row {} at visual col {} (DataTable col {}, value '{}')", 
                                row_idx, enum_idx, actual_col, value_str);

                            // Extra validation - log if we find "Futures Trading"
                            if value_str.contains("Futures Trading") {
                                warn!(target: "vim_search",
                                    "SUSPICIOUS: Found 'Futures Trading' as a match for pattern '{}' (search_value='{}', pattern_lower='{}')",
                                    pattern, search_value, pattern_lower);
                            }

                            first_match_in_row = Some(SearchMatch {
                                row: row_idx,
                                col: enum_idx, // This is the visual column index in display order
                                value: value_str,
                            });
                        } else {
                            debug!(target: "vim_search", 
                                "Skipping additional match in row {} at visual col {} (enum_idx {}): '{}'", 
                                row_idx, enum_idx, enum_idx, value_str);
                        }
                    }
                }

                // Add the first match from this row if we found one
                if let Some(match_item) = first_match_in_row {
                    matches.push(match_item);
                }
            }
        }

        debug!(target: "vim_search", "Found {} total matches", matches.len());
        matches
    }

    /// Navigate viewport to ensure match is visible and set crosshair
    fn navigate_to_match(&self, match_item: &SearchMatch, viewport: &mut ViewportManager) {
        info!(target: "vim_search", 
            "=== NAVIGATE_TO_MATCH START ===");
        info!(target: "vim_search", 
            "Target match: row={} (absolute), col={} (visual), value='{}'", 
            match_item.row, match_item.col, match_item.value);

        // Get terminal dimensions to preserve width
        let terminal_width = viewport.get_terminal_width();
        let terminal_height = viewport.get_terminal_height();
        info!(target: "vim_search",
            "Terminal dimensions: width={}, height={}",
            terminal_width, terminal_height);

        // Get current viewport state BEFORE any changes
        let viewport_rows = viewport.get_viewport_rows();
        let viewport_cols = viewport.viewport_cols();
        let viewport_height = viewport_rows.end - viewport_rows.start;
        let viewport_width = viewport_cols.end - viewport_cols.start;

        info!(target: "vim_search",
            "Current viewport BEFORE changes:");
        info!(target: "vim_search",
            "  Rows: {:?} (height={})", viewport_rows, viewport_height);
        info!(target: "vim_search",
            "  Cols: {:?} (width={})", viewport_cols, viewport_width);
        info!(target: "vim_search",
            "  Current crosshair: row={}, col={}",
            viewport.get_crosshair_row(), viewport.get_crosshair_col());

        // ALWAYS center the match in the viewport for predictable behavior
        // The match should appear at viewport position (height/2, width/2)
        let new_row_start = match_item.row.saturating_sub(viewport_height / 2);
        info!(target: "vim_search", 
            "Centering row {} in viewport (height={}), new viewport start row={}", 
            match_item.row, viewport_height, new_row_start);

        // For columns, we can't just divide by 2 because columns have variable widths
        // Instead, try to position the match column reasonably in view
        // Start by trying to show a few columns before the match if possible
        let new_col_start = match_item.col.saturating_sub(3); // Show 3 columns before if possible
        info!(target: "vim_search", 
            "Positioning column {} in viewport, new viewport start col={}", 
            match_item.col, new_col_start);

        // Log what we're about to do
        info!(target: "vim_search",
            "=== VIEWPORT UPDATE ===");
        info!(target: "vim_search",
            "Will call set_viewport with: row_start={}, col_start={}, width={}, height={}",
            new_row_start, new_col_start, terminal_width, terminal_height);

        // Update viewport with preserved terminal dimensions
        viewport.set_viewport(
            new_row_start,
            new_col_start,
            terminal_width, // Use actual terminal width, not column count!
            terminal_height as u16,
        );

        // Get the updated viewport state
        let final_viewport_rows = viewport.get_viewport_rows();
        let final_viewport_cols = viewport.viewport_cols();

        info!(target: "vim_search", 
            "Viewport AFTER set_viewport: rows {:?}, cols {:?}", 
            final_viewport_rows, final_viewport_cols);

        // CRITICAL: Check if our target column is actually in the viewport!
        if match_item.col < final_viewport_cols.start || match_item.col >= final_viewport_cols.end {
            error!(target: "vim_search",
                "CRITICAL ERROR: Target column {} is NOT in viewport {:?} after set_viewport!",
                match_item.col, final_viewport_cols);
            error!(target: "vim_search",
                "We asked for col_start={}, but viewport gave us {:?}",
                new_col_start, final_viewport_cols);
        }

        // Set the crosshair to the ABSOLUTE position of the match
        // The viewport manager uses absolute coordinates internally
        info!(target: "vim_search",
            "=== CROSSHAIR POSITIONING ===");
        info!(target: "vim_search",
            "Setting crosshair to ABSOLUTE position: row={}, col={}",
            match_item.row, match_item.col);

        viewport.set_crosshair(match_item.row, match_item.col);

        // Verify the match is centered in the viewport
        let center_row =
            final_viewport_rows.start + (final_viewport_rows.end - final_viewport_rows.start) / 2;
        let center_col =
            final_viewport_cols.start + (final_viewport_cols.end - final_viewport_cols.start) / 2;

        info!(target: "vim_search",
            "Viewport center is at: row={}, col={}",
            center_row, center_col);
        info!(target: "vim_search",
            "Match is at: row={}, col={}",
            match_item.row, match_item.col);
        info!(target: "vim_search",
            "Distance from center: row_diff={}, col_diff={}",
            (match_item.row as i32 - center_row as i32).abs(),
            (match_item.col as i32 - center_col as i32).abs());

        // Get the viewport-relative position for verification
        if let Some((vp_row, vp_col)) = viewport.get_crosshair_viewport_position() {
            info!(target: "vim_search",
                "Crosshair appears at viewport position: ({}, {})",
                vp_row, vp_col);
            info!(target: "vim_search",
                "Viewport dimensions: {} rows x {} cols",
                final_viewport_rows.end - final_viewport_rows.start,
                final_viewport_cols.end - final_viewport_cols.start);
            info!(target: "vim_search",
                "Expected center position: ({}, {})",
                (final_viewport_rows.end - final_viewport_rows.start) / 2,
                (final_viewport_cols.end - final_viewport_cols.start) / 2);
        } else {
            error!(target: "vim_search",
                "CRITICAL: Crosshair is NOT visible in viewport after centering!");
        }

        // Verify the match is actually visible in the viewport after scrolling
        info!(target: "vim_search",
            "=== VERIFICATION ===");

        if match_item.row < final_viewport_rows.start || match_item.row >= final_viewport_rows.end {
            error!(target: "vim_search", 
                "ERROR: Match row {} is OUTSIDE viewport {:?} after scrolling!", 
                match_item.row, final_viewport_rows);
        } else {
            info!(target: "vim_search",
                "✓ Match row {} is within viewport {:?}",
                match_item.row, final_viewport_rows);
        }

        if match_item.col < final_viewport_cols.start || match_item.col >= final_viewport_cols.end {
            error!(target: "vim_search", 
                "ERROR: Match column {} is OUTSIDE viewport {:?} after scrolling!", 
                match_item.col, final_viewport_cols);
        } else {
            info!(target: "vim_search",
                "✓ Match column {} is within viewport {:?}",
                match_item.col, final_viewport_cols);
        }

        // Final summary
        info!(target: "vim_search", 
            "=== NAVIGATE_TO_MATCH COMPLETE ===");
        info!(target: "vim_search",
            "Match at absolute ({}, {}), crosshair at ({}, {}), viewport rows {:?} cols {:?}", 
            match_item.row, match_item.col,
            viewport.get_crosshair_row(), viewport.get_crosshair_col(),
            final_viewport_rows, final_viewport_cols);
    }

    /// Set case sensitivity for search
    pub fn set_case_sensitive(&mut self, case_sensitive: bool) {
        self.case_sensitive = case_sensitive;
        debug!(target: "vim_search", "Case sensitivity set to: {}", case_sensitive);
    }

    /// Set search state from external search (e.g., `SearchModesWidget`)
    /// This allows 'n' and 'N' to work after a regular search
    pub fn set_search_state_from_external(
        &mut self,
        pattern: String,
        matches: Vec<(usize, usize)>,
        dataview: &DataView,
    ) {
        info!(target: "vim_search", 
            "Setting search state from external search: pattern='{}', {} matches", 
            pattern, matches.len());

        // Convert matches to SearchMatch format
        let search_matches: Vec<SearchMatch> = matches
            .into_iter()
            .filter_map(|(row, col)| {
                if let Some(row_data) = dataview.get_row(row) {
                    if col < row_data.values.len() {
                        Some(SearchMatch {
                            row,
                            col,
                            value: row_data.values[col].to_string(),
                        })
                    } else {
                        None
                    }
                } else {
                    None
                }
            })
            .collect();

        if search_matches.is_empty() {
            warn!(target: "vim_search", "No valid matches to set in vim search state");
        } else {
            let match_count = search_matches.len();

            // Set the state to navigating
            self.state = VimSearchState::Navigating {
                pattern: pattern.clone(),
                matches: search_matches,
                current_index: 0,
            };
            self.last_search_pattern = Some(pattern);

            info!(target: "vim_search", 
                "Vim search state updated: {} matches ready for navigation", 
                match_count);
        }
    }
}