From ab2798834ca43cb65e040fce9b48b092bd7d6075 Mon Sep 17 00:00:00 2001 From: vasilito Date: Tue, 7 Jul 2026 15:30:18 +0300 Subject: [PATCH] W65: Search whole-words option (MC search dialog parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../tui/tlc/source/src/viewer/menubar.rs | 4 +++ .../recipes/tui/tlc/source/src/viewer/mod.rs | 34 ++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/local/recipes/tui/tlc/source/src/viewer/menubar.rs b/local/recipes/tui/tlc/source/src/viewer/menubar.rs index d626feb4e7..be682f27b3 100644 --- a/local/recipes/tui/tlc/source/src/viewer/menubar.rs +++ b/local/recipes/tui/tlc/source/src/viewer/menubar.rs @@ -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( diff --git a/local/recipes/tui/tlc/source/src/viewer/mod.rs b/local/recipes/tui/tlc/source/src/viewer/mod.rs index eaf476bc7e..aaf8b45cd0 100644 --- a/local/recipes/tui/tlc/source/src/viewer/mod.rs +++ b/local/recipes/tui/tlc/source/src/viewer/mod.rs @@ -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 { + pub fn search(&mut self, raw_pattern: &str, case_insensitive: bool) -> Result { + 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, 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() } }