W65: Search whole-words option (MC search dialog parity)

Add search_whole_words toggle to the viewer. When enabled, the
search pattern is wrapped in \b boundaries to match only whole
words. Accessible via Search F9 menu → Whole words.

- Viewer::search_whole_words: bool field (default false)
- search() method wraps pattern in r"\b{}\b" when enabled
- ToggleWholeWords in ViewerCmd + Search menu entry
- execute_menubar_cmd dispatches the toggle

Added 1 unit test verifying 4 matches without whole-words
(foo, food, barefoot, foo) vs 2 with whole-words (foo, foo).

Tests: 1424 pass (was 1423), zero warnings.
This commit is contained in:
2026-07-07 15:30:18 +03:00
parent da8fe50c4a
commit ab2798834c
2 changed files with 34 additions and 4 deletions
@@ -38,6 +38,8 @@ pub enum ViewerCmd {
ToggleRuler,
/// Toggle hex column navigation (hex↔ASCII text column).
ToggleHexNavigation,
/// Toggle whole-word search.
ToggleWholeWords,
/// Search forward.
Search,
/// Search next.
@@ -166,6 +168,8 @@ impl ViewerMenuBar {
MenuItem::item("Find...", 'f', ViewerCmd::Search),
MenuItem::item("Find next", 'n', ViewerCmd::SearchNext),
MenuItem::item("Find prev", 'p', ViewerCmd::SearchPrev),
MenuItem::separator(),
MenuItem::item("Whole words", 'w', ViewerCmd::ToggleWholeWords),
],
),
Menu::new(
+30 -4
View File
@@ -104,6 +104,9 @@ pub struct Viewer {
marker: usize,
/// Last search direction (true = forward / 'n', false = backward / 'N').
last_search_forward: bool,
/// If true, wrap the search pattern in \b boundaries to match
/// whole words only. Toggled via the search options.
pub search_whole_words: bool,
/// Whether to show the column ruler (MC CK_Ruler / Alt-R).
pub ruler: bool,
/// True when the cursor navigates the ASCII text column in
@@ -190,6 +193,7 @@ impl Viewer {
marks: [None; 10],
marker: 0,
last_search_forward: true,
search_whole_words: false,
ruler: false,
hexview_in_text: false,
nroff_enabled: false,
@@ -237,6 +241,7 @@ impl Viewer {
marks: [None; 10],
marker: 0,
last_search_forward: true,
search_whole_words: false,
ruler: false,
hexview_in_text: false,
nroff_enabled: false,
@@ -372,22 +377,27 @@ impl Viewer {
}
/// Run a search and update the engine. Returns the number of hits.
pub fn search(&mut self, pattern: &str, case_insensitive: bool) -> Result<usize> {
pub fn search(&mut self, raw_pattern: &str, case_insensitive: bool) -> Result<usize> {
let pattern = if self.search_whole_words {
format!(r"\b{}\b", raw_pattern)
} else {
raw_pattern.to_string()
};
match &self.source {
source::FileSource::Inline { bytes } => {
let text = String::from_utf8_lossy(bytes);
self.search.find_all(&text, pattern, case_insensitive)?;
self.search.find_all(&text, &pattern, case_insensitive)?;
}
source::FileSource::Compressed { bytes, .. } => {
let text = String::from_utf8_lossy(bytes);
self.search.find_all(&text, pattern, case_insensitive)?;
self.search.find_all(&text, &pattern, case_insensitive)?;
}
source::FileSource::Chunked { .. } => {
let size = self.source.size();
let mut offset = 0u64;
let chunk_size = source::CHUNK_SIZE as u64;
self.search.find_all_streaming(
pattern,
&pattern,
case_insensitive,
|| -> Result<Option<(Vec<u8>, bool)>, anyhow::Error> {
if offset >= size {
@@ -1223,6 +1233,9 @@ impl Viewer {
ViewerCmd::ToggleHexNavigation => {
self.hexview_in_text = !self.hexview_in_text;
}
ViewerCmd::ToggleWholeWords => {
self.search_whole_words = !self.search_whole_words;
}
ViewerCmd::Search => {
self.prompt = Some(ViewerPrompt::Search);
self.prompt_input.clear();
@@ -1999,6 +2012,19 @@ mod tests {
assert!(!v.hexview_in_text);
}
#[test]
fn whole_words_search_matches_boundaries() {
let p = make_text("wsearch.txt", b"foo bar food barefoot foo");
let mut v = Viewer::open(&p).unwrap();
// Without whole-words: "foo" should match in "food" and "barefoot"
let n = v.search("foo", false).unwrap();
assert_eq!(n, 4); // foo, food, barefoot, foo
// With whole-words: "foo" should only match standalone "foo"
v.search_whole_words = true;
let n = v.search("foo", false).unwrap();
assert_eq!(n, 2); // only standalone "foo" entries
}
fn k(c: char) -> Key {
Key { code: c as u32, mods: crate::key::Modifiers::empty() }
}