Skip to main content

hjkl_buffer/
search.rs

1//! Shared search-match scanning.
2//!
3//! One canonical "pattern → match ranges within a line" implementation for
4//! every consumer that needs to know WHERE a search pattern matches:
5//!
6//! - `hjkl-engine`'s `SearchState` per-row match cache (`n`/`N` navigation),
7//! - `hjkl-buffer-tui`'s hlsearch painting (`BufferView::row_search_ranges`),
8//! - the app's quickfix-dock match overlay (highlighting the `:grep` pattern
9//!   inside each entry's message text).
10//!
11//! All three previously (or would otherwise) hand-roll the same
12//! `find_iter → (start, end)` expression; keeping it here — the lowest crate
13//! they all already depend on — guarantees they can never disagree about
14//! what counts as a match.
15
16/// Byte ranges (`(start, end)`, half-open) of every non-overlapping match of
17/// `re` in `line`, in order. Empty when nothing matches.
18///
19/// BYTE offsets, matching `regex::Match` — callers that paint per-cell
20/// (charwise columns) convert at their own boundary (see
21/// `BufferView::row_search_ranges` in `hjkl-buffer-tui`); callers that feed
22/// byte-ranged spans (`hjkl_buffer::Span`, engine syntax spans) use them
23/// directly.
24pub fn search_match_ranges(re: &regex::Regex, line: &str) -> Vec<(usize, usize)> {
25    re.find_iter(line).map(|m| (m.start(), m.end())).collect()
26}
27
28#[cfg(test)]
29mod tests {
30    use super::search_match_ranges;
31
32    #[test]
33    fn ranges_are_byte_offsets_in_order() {
34        let re = regex::Regex::new("ab").unwrap();
35        assert_eq!(search_match_ranges(&re, "ab cd ab"), vec![(0, 2), (6, 8)]);
36    }
37
38    #[test]
39    fn no_match_is_empty() {
40        let re = regex::Regex::new("zzz").unwrap();
41        assert!(search_match_ranges(&re, "ab cd").is_empty());
42    }
43
44    #[test]
45    fn multibyte_prefix_yields_byte_not_char_offsets() {
46        // "é" is 2 bytes — a match after it must report BYTE offsets.
47        let re = regex::Regex::new("x").unwrap();
48        assert_eq!(search_match_ranges(&re, "éx"), vec![(2, 3)]);
49    }
50}