diff --git a/melib/src/text_processing/line_break.rs b/melib/src/text_processing/line_break.rs index c3209f6f..97864bd2 100644 --- a/melib/src/text_processing/line_break.rs +++ b/melib/src/text_processing/line_break.rs @@ -1127,57 +1127,8 @@ easy to take MORE than nothing.'"#; println!("{}", l); } println!(""); - let text = r#"CHAPTER I. Down the Rabbit-Hole - -Alice was beginning to get very tired of sitting by her sister on the -bank, and of having nothing to do: once or twice she had peeped into the -book her sister was reading, but it had no pictures or conversations in -it, ‘and what is the use of a book,’ thought Alice ‘without pictures or -conversations?’ - -So she was considering in her own mind (as well as she could, for the -hot day made her feel very sleepy and stupid), whether the pleasure -of making a daisy-chain would be worth the trouble of getting up and -picking the daisies, when suddenly a White Rabbit with pink eyes ran -close by her. - ->>There was nothing so VERY remarkable in that; nor did Alice think it so ->>VERY much out of the way to hear the Rabbit say to itself, ‘Oh dear! ->> Oh dear! I shall be late!’ (when she thought it over afterwards, it ->>occurred to her that she ought to have wondered at this, but at the time ->>it all seemed quite natural); but when the Rabbit actually TOOK A WATCH -OUT OF ITS WAISTCOAT-POCKET, and looked at it, and then hurried on, ->>Alice started to her feet, for it flashed across her mind that she had ->>never before seen a rabbit with either a waistcoat-pocket, or a watch ->>to take out of it, and burning with curiosity, she ran across the field -after it, and fortunately was just in time to see it pop down a large -rabbit-hole under the hedge. - -In another moment down went Alice after it, never once considering how -in the world she was to get out again. - -The rabbit-hole went straight on like a tunnel for some way, and then -dipped suddenly down, so suddenly that Alice had not a moment to think -about stopping herself before she found herself falling down a very deep -well. - -Either the well was very deep, or she fell very slowly, for she had -plenty of time as she went down to look about her and to wonder what was -going to happen next. First, she tried to look down and make out what -she was coming to, but it was too dark to see anything; then she -looked at the sides of the well, and noticed that they were filled with -cupboards and book-shelves; here and there she saw maps and pictures -hung upon pegs. She took down a jar from one of the shelves as -she passed; it was labelled ‘ORANGE MARMALADE’, but to her great -disappointment it was empty: she did not like to drop the jar for fear -of killing somebody, so managed to put it into one of the cupboards as -she fell past it. - -‘Well!’ thought Alice to herself, ‘after such a fall as this, I shall -think nothing of tumbling down stairs! How brave they’ll all think me at -home! Why, I wouldn’t say anything about it, even if I fell off the top -of the house!’ (Which was very likely true.)"#; - for l in split_lines_reflow(text, Reflow::FormatFlowed, Some(72)) { + use super::_ALICE_CHAPTER_1; + for l in split_lines_reflow(_ALICE_CHAPTER_1, Reflow::FormatFlowed, Some(72)) { println!("{}", l); } } diff --git a/melib/src/text_processing/mod.rs b/melib/src/text_processing/mod.rs index 77efca68..228316f8 100644 --- a/melib/src/text_processing/mod.rs +++ b/melib/src/text_processing/mod.rs @@ -21,6 +21,7 @@ pub mod grapheme_clusters; pub mod line_break; +pub mod search; mod tables; mod types; pub use types::Reflow; @@ -137,3 +138,54 @@ fn test_globmatch() { assert!(!"INBOX/Lists/".matches_glob("INBOX/Lists/*")); } + +const _ALICE_CHAPTER_1: &'static str = r#"CHAPTER I. Down the Rabbit-Hole + +Alice was beginning to get very tired of sitting by her sister on the +bank, and of having nothing to do: once or twice she had peeped into the +book her sister was reading, but it had no pictures or conversations in +it, ‘and what is the use of a book,’ thought Alice ‘without pictures or +conversations?’ + +So she was considering in her own mind (as well as she could, for the +hot day made her feel very sleepy and stupid), whether the pleasure +of making a daisy-chain would be worth the trouble of getting up and +picking the daisies, when suddenly a White Rabbit with pink eyes ran +close by her. + +>>There was nothing so VERY remarkable in that; nor did Alice think it so +>>VERY much out of the way to hear the Rabbit say to itself, ‘Oh dear! +>> Oh dear! I shall be late!’ (when she thought it over afterwards, it +>>occurred to her that she ought to have wondered at this, but at the time +>>it all seemed quite natural); but when the Rabbit actually TOOK A WATCH +OUT OF ITS WAISTCOAT-POCKET, and looked at it, and then hurried on, +>>Alice started to her feet, for it flashed across her mind that she had +>>never before seen a rabbit with either a waistcoat-pocket, or a watch +>>to take out of it, and burning with curiosity, she ran across the field +after it, and fortunately was just in time to see it pop down a large +rabbit-hole under the hedge. + +In another moment down went Alice after it, never once considering how +in the world she was to get out again. + +The rabbit-hole went straight on like a tunnel for some way, and then +dipped suddenly down, so suddenly that Alice had not a moment to think +about stopping herself before she found herself falling down a very deep +well. + +Either the well was very deep, or she fell very slowly, for she had +plenty of time as she went down to look about her and to wonder what was +going to happen next. First, she tried to look down and make out what +she was coming to, but it was too dark to see anything; then she +looked at the sides of the well, and noticed that they were filled with +cupboards and book-shelves; here and there she saw maps and pictures +hung upon pegs. She took down a jar from one of the shelves as +she passed; it was labelled ‘ORANGE MARMALADE’, but to her great +disappointment it was empty: she did not like to drop the jar for fear +of killing somebody, so managed to put it into one of the cupboards as +she fell past it. + +‘Well!’ thought Alice to herself, ‘after such a fall as this, I shall +think nothing of tumbling down stairs! How brave they’ll all think me at +home! Why, I wouldn’t say anything about it, even if I fell off the top +of the house!’ (Which was very likely true.)"#; diff --git a/melib/src/text_processing/search.rs b/melib/src/text_processing/search.rs new file mode 100644 index 00000000..5220ce81 --- /dev/null +++ b/melib/src/text_processing/search.rs @@ -0,0 +1,95 @@ +/* + * meli - text_processing mod. + * + * Copyright 2020 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use super::TextProcessing; + +use smallvec::SmallVec; + +pub trait KMP: TextProcessing { + fn kmp_search(&self, pattern: &str) -> SmallVec<[usize; 256]> { + let w = pattern.split_graphemes(); + let t = kmp_table(&w); + let mut j = 0; // (the position of the current character in text) + let mut k = 0; // (the position of the current character in pattern) + let mut ret = SmallVec::new(); + + let s = self.graphemes_indices(); + while j < s.len() && k < w.len() as i32 { + if w[k as usize] == s[j].1 { + j += 1; + k += 1; + if k as usize == w.len() { + ret.push(s[j - (k as usize)].0); + k = t[k as usize]; + } + } else { + k = t[k as usize]; + if k < 0 { + j += 1; + k += 1; + } + } + } + ret + } +} + +impl KMP for str {} + +fn kmp_table(graphemes: &[&str]) -> SmallVec<[i32; 256]> { + let mut ret: SmallVec<_> = SmallVec::with_capacity(graphemes.len() + 1); + if graphemes.is_empty() { + return ret; + } + ret.push(-1); + for _ in 0..graphemes.len() { + ret.push(0); + } + let mut pos: usize = 1; + let mut cnd: i32 = 0; + while pos < graphemes.len() { + if graphemes[pos] == graphemes[cnd as usize] { + ret[pos] = ret[cnd as usize]; + } else { + ret[pos] = cnd; + cnd = ret[cnd as usize]; + while cnd >= 0 && graphemes[pos] != graphemes[cnd as usize] { + cnd = ret[cnd as usize]; + } + } + pos += 1; + cnd += 1; + } + ret[pos] = cnd; + ret +} + +#[test] +fn test_search() { + use super::_ALICE_CHAPTER_1; + for ind in _ALICE_CHAPTER_1.kmp_search("Alice") { + println!( + "{:#?}", + &_ALICE_CHAPTER_1 + [ind.saturating_sub(0)..std::cmp::min(_ALICE_CHAPTER_1.len(), ind + 25)] + ); + } +} diff --git a/src/components/mail/view.rs b/src/components/mail/view.rs index 6c699835..d25655d7 100644 --- a/src/components/mail/view.rs +++ b/src/components/mail/view.rs @@ -90,6 +90,7 @@ pub struct MailView { pager: Pager, subview: Option>, dirty: bool, + initialised: bool, mode: ViewMode, expand_headers: bool, headers_no: usize, @@ -133,6 +134,7 @@ impl MailView { pager: pager.unwrap_or_default(), subview, dirty: true, + initialised: false, mode: ViewMode::Normal, expand_headers: false, @@ -343,6 +345,7 @@ impl MailView { pub fn update(&mut self, new_coordinates: (usize, FolderHash, EnvelopeHash)) { self.coordinates = new_coordinates; self.mode = ViewMode::Normal; + self.initialised = false; self.set_dirty(true); } } @@ -587,7 +590,8 @@ impl Component for MailView { } }; - if self.dirty { + if !self.initialised { + self.initialised = true; let body = { let account = &mut context.accounts[self.coordinates.0]; let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2); @@ -595,7 +599,6 @@ impl Component for MailView { match envelope.body(op) { Ok(body) => body, Err(e) => { - self.dirty = false; clear_area( grid, (set_y(upper_left, y), bottom_right), @@ -626,10 +629,12 @@ impl Component for MailView { let attachment = &body.attachments()[aidx]; self.subview = Some(Box::new(HtmlView::new(&attachment, context))); self.mode = ViewMode::Subview; + self.initialised = false; } ViewMode::Normal if body.is_html() => { self.subview = Some(Box::new(HtmlView::new(&body, context))); self.mode = ViewMode::Subview; + self.initialised = false; } ViewMode::Normal if context @@ -659,6 +664,7 @@ impl Component for MailView { context, ))); self.mode = ViewMode::Subview; + self.initialised = false; } ViewMode::Subview | ViewMode::ContactSelector(_) => {} ViewMode::Source(source) => { @@ -787,6 +793,7 @@ impl Component for MailView { } } self.mode = ViewMode::Normal; + self.initialised = false; return true; } (ViewMode::ContactSelector(ref mut s), _) => { @@ -879,6 +886,7 @@ impl Component for MailView { context, )); self.dirty = true; + self.initialised = false; return true; } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) @@ -886,6 +894,7 @@ impl Component for MailView { { self.mode = ViewMode::Normal; self.set_dirty(true); + self.initialised = false; return true; } UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) if !self.cmd_buf.is_empty() => { @@ -916,6 +925,7 @@ impl Component for MailView { _ => ViewMode::Source(Source::Decoded), }; self.set_dirty(true); + self.initialised = false; return true; } UIEvent::Input(ref key) @@ -931,6 +941,7 @@ impl Component for MailView { { self.mode = ViewMode::Normal; self.set_dirty(true); + self.initialised = false; return true; } UIEvent::Input(ref key) @@ -1052,6 +1063,7 @@ impl Component for MailView { ContentType::Text { .. } | ContentType::PGPSignature => { self.mode = ViewMode::Attachment(lidx); + self.initialised = false; self.dirty = true; } ContentType::Multipart { .. } => { @@ -1091,6 +1103,7 @@ impl Component for MailView { { let raw_buf = RawBuffer::new(buf, name_opt); self.mode = ViewMode::Ansi(raw_buf); + self.initialised = false; self.dirty = true; return true; } @@ -1227,6 +1240,7 @@ impl Component for MailView { ViewMode::Url => self.mode = ViewMode::Normal, _ => {} } + self.initialised = false; self.dirty = true; return true; } diff --git a/src/components/utilities.rs b/src/components/utilities.rs index 59745e63..2b266c4f 100644 --- a/src/components/utilities.rs +++ b/src/components/utilities.rs @@ -276,6 +276,14 @@ pub enum PageMovement { End, } +#[derive(Default, Debug, Clone)] +pub struct SearchPattern { + pattern: String, + positions: Vec<(usize, usize)>, + cursor: usize, + movement: Option, +} + /// A pager for text. /// `Pager` holds its own content in its own `CellBuffer` and when `draw` is called, it draws the /// current view of the text. It is responsible for scrolling etc. @@ -287,6 +295,7 @@ pub struct Pager { height: usize, width: usize, minimum_width: usize, + search: Option, dirty: bool, colors: ThemeAttribute, @@ -530,7 +539,40 @@ impl Component for Pager { empty_cell.set_bg(self.colors.bg); let mut content = CellBuffer::new(width, height, empty_cell); content.set_ascii_drawing(self.content.ascii_drawing); + if let Some(ref mut search) = self.search { + use melib::text_processing::search::KMP; + search.positions.clear(); + for (y, l) in lines.iter().enumerate() { + search.positions.extend( + l.kmp_search(&search.pattern) + .into_iter() + .map(|offset| (y, offset)), + ); + } + } Pager::print_string(&mut content, lines, self.colors); + if let Some(ref mut search) = self.search { + let results_attr = crate::conf::value(context, "pager.highlight_search"); + let results_current_attr = + crate::conf::value(context, "pager.highlight_search_current"); + search.cursor = + std::cmp::min(search.positions.len().saturating_sub(1), search.cursor); + for (i, (y, x)) in search.positions.iter().enumerate() { + for c in content.row_iter(*x..*x + search.pattern.grapheme_len(), *y) { + if i == search.cursor { + content[c] + .set_fg(results_current_attr.fg) + .set_bg(results_current_attr.bg) + .set_attrs(results_current_attr.attrs); + } else { + content[c] + .set_fg(results_attr.fg) + .set_bg(results_attr.bg) + .set_attrs(results_attr.attrs); + } + } + } + } self.content = content; self.height = height; self.width = width; @@ -580,6 +622,25 @@ impl Component for Pager { if self.height == 0 || self.width == 0 { return; } + if let Some(ref mut search) = self.search { + if !search.positions.is_empty() { + if let Some(mvm) = search.movement.take() { + match mvm { + PageMovement::Up(_) => { + if self.cursor.1 > search.positions[search.cursor].0 { + self.cursor.1 = search.positions[search.cursor].0; + } + } + PageMovement::Down(_) => { + if self.cursor.1 + height < search.positions[search.cursor].0 { + self.cursor.1 = search.positions[search.cursor].0; + } + } + _ => {} + } + } + } + } clear_area(grid, area, crate::conf::value(context, "theme_default")); let (width, height) = self.content.size(); @@ -694,6 +755,49 @@ impl Component for Pager { )))); return true; } + UIEvent::Action(Action::Listing(ListingAction::Filter(pattern))) => { + self.search = Some(SearchPattern { + pattern: pattern.to_string(), + positions: vec![], + cursor: 0, + movement: None, + }); + self.initialised = false; + self.dirty = true; + return true; + } + UIEvent::Input(Key::Char('n')) if self.search.is_some() => { + if let Some(ref mut search) = self.search { + search.movement = Some(PageMovement::Down(1)); + search.cursor += 1; + } else { + unsafe { + std::hint::unreachable_unchecked(); + } + } + self.initialised = false; + self.dirty = true; + return true; + } + UIEvent::Input(Key::Char('N')) if self.search.is_some() => { + if let Some(ref mut search) = self.search { + search.movement = Some(PageMovement::Up(1)); + search.cursor = search.cursor.saturating_sub(1); + } else { + unsafe { + std::hint::unreachable_unchecked(); + } + } + self.initialised = false; + self.dirty = true; + return true; + } + UIEvent::Input(Key::Esc) if self.search.is_some() => { + self.search = None; + self.initialised = false; + self.dirty = true; + return true; + } UIEvent::Resize => { self.initialised = false; self.dirty = true; diff --git a/src/conf/themes.rs b/src/conf/themes.rs index 5f993957..ab5532db 100644 --- a/src/conf/themes.rs +++ b/src/conf/themes.rs @@ -195,6 +195,8 @@ const DEFAULT_KEYS: &'static [&'static str] = &[ "mail.view.body", "mail.listing.attachment_flag", "mail.listing.thread_snooze_flag", + "pager.highlight_search", + "pager.highlight_search_current", ]; /// `ThemeAttributeInner` but with the links resolved. @@ -717,6 +719,8 @@ impl Default for Theme { } ); + add!("pager.highlight_search", light = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::Bold }, dark = { fg: Color::White, bg: Color::Byte(6) /* Teal */, attrs: Attr::Bold }); + add!("pager.highlight_search_current", light = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::Bold }, dark = { fg: Color::White, bg: Color::Byte(17) /* NavyBlue */, attrs: Attr::Bold }); Theme { light, dark,