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:
@@ -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(
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user