Implement search for CellBuffer

async
Manos Pitsidianakis 2020-02-26 12:25:57 +02:00
parent 4ac52d9d5b
commit ac71d627f1
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
5 changed files with 225 additions and 45 deletions

View File

@ -139,7 +139,7 @@ fn test_globmatch() {
assert!(!"INBOX/Lists/".matches_glob("INBOX/Lists/*")); assert!(!"INBOX/Lists/".matches_glob("INBOX/Lists/*"));
} }
const _ALICE_CHAPTER_1: &'static str = r#"CHAPTER I. Down the Rabbit-Hole pub 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 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 bank, and of having nothing to do: once or twice she had peeped into the

View File

@ -23,10 +23,41 @@ use super::TextProcessing;
use smallvec::SmallVec; use smallvec::SmallVec;
pub trait KMP: TextProcessing { pub trait KMP {
fn kmp_search(&self, pattern: &str) -> SmallVec<[usize; 256]>;
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
}
}
impl KMP for str {
fn kmp_search(&self, pattern: &str) -> SmallVec<[usize; 256]> { fn kmp_search(&self, pattern: &str) -> SmallVec<[usize; 256]> {
let w = pattern.split_graphemes(); let w = pattern.split_graphemes();
let t = kmp_table(&w); let t = Self::kmp_table(&w);
let mut j = 0; // (the position of the current character in text) 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 k = 0; // (the position of the current character in pattern)
let mut ret = SmallVec::new(); let mut ret = SmallVec::new();
@ -52,36 +83,6 @@ pub trait KMP: TextProcessing {
} }
} }
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] #[test]
fn test_search() { fn test_search() {
use super::_ALICE_CHAPTER_1; use super::_ALICE_CHAPTER_1;

View File

@ -760,7 +760,7 @@ impl Component for Pager {
pattern: pattern.to_string(), pattern: pattern.to_string(),
positions: vec![], positions: vec![],
cursor: 0, cursor: 0,
movement: None, movement: Some(PageMovement::Home),
}); });
self.initialised = false; self.initialised = false;
self.dirty = true; self.dirty = true;
@ -1442,6 +1442,7 @@ pub struct Tabbed {
help_screen_cursor: (usize, usize), help_screen_cursor: (usize, usize),
help_content: CellBuffer, help_content: CellBuffer,
help_curr_views: ShortcutMaps, help_curr_views: ShortcutMaps,
help_search: Option<SearchPattern>,
dirty: bool, dirty: bool,
id: ComponentId, id: ComponentId,
@ -1458,6 +1459,7 @@ impl Tabbed {
help_content: CellBuffer::default(), help_content: CellBuffer::default(),
help_screen_cursor: (0, 0), help_screen_cursor: (0, 0),
help_curr_views: ShortcutMaps::default(), help_curr_views: ShortcutMaps::default(),
help_search: None,
dirty: true, dirty: true,
id: ComponentId::new_v4(), id: ComponentId::new_v4(),
} }
@ -1737,6 +1739,67 @@ impl Component for Tabbed {
idx += 1; idx += 1;
} }
self.help_curr_views = children_maps; self.help_curr_views = children_maps;
if let Some(ref mut search) = self.help_search {
use crate::melib::text_processing::search::KMP;
search.positions = self
.help_content
.kmp_search(&search.pattern)
.into_iter()
.map(|offset| (offset / width, offset % width))
.collect::<Vec<(usize, usize)>>();
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 self
.help_content
.row_iter(*x..*x + search.pattern.grapheme_len(), *y)
{
if i == search.cursor {
self.help_content[c]
.set_fg(results_current_attr.fg)
.set_bg(results_current_attr.bg)
.set_attrs(results_current_attr.attrs);
} else {
self.help_content[c]
.set_fg(results_attr.fg)
.set_bg(results_attr.bg)
.set_attrs(results_attr.attrs);
}
}
}
if !search.positions.is_empty() {
if let Some(mvm) = search.movement.take() {
match mvm {
PageMovement::Home => {
if self.help_screen_cursor.1 > search.positions[search.cursor].0 {
self.help_screen_cursor.1 = search.positions[search.cursor].0;
}
if self.help_screen_cursor.1 + rows
< search.positions[search.cursor].0
{
self.help_screen_cursor.1 = search.positions[search.cursor].0;
}
}
PageMovement::Up(_) => {
if self.help_screen_cursor.1 > search.positions[search.cursor].0 {
self.help_screen_cursor.1 = search.positions[search.cursor].0;
}
}
PageMovement::Down(_) => {
if self.help_screen_cursor.1 + rows
< search.positions[search.cursor].0
{
self.help_screen_cursor.1 = search.positions[search.cursor].0;
}
}
_ => {}
}
}
}
}
copy_area( copy_area(
grid, grid,
&self.help_content, &self.help_content,
@ -1755,11 +1818,11 @@ impl Component for Tabbed {
} }
self.dirty = false; self.dirty = false;
} }
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context); let shortcuts = self.get_shortcuts(context);
match *event { match &mut event {
UIEvent::Input(Key::Alt(no)) if no >= '1' && no <= '9' => { UIEvent::Input(Key::Alt(no)) if *no >= '1' && *no <= '9' => {
let no = no as usize - '1' as usize; let no = *no as usize - '1' as usize;
if no < self.children.len() { if no < self.children.len() {
self.cursor_pos = no % self.children.len(); self.cursor_pos = no % self.children.len();
context context
@ -1791,7 +1854,7 @@ impl Component for Tabbed {
return true; return true;
} }
UIEvent::Action(Tab(NewDraft(account_idx, ref draft))) => { UIEvent::Action(Tab(NewDraft(account_idx, ref draft))) => {
let mut composer = Composer::new(account_idx, context); let mut composer = Composer::new(*account_idx, context);
if let Some(draft) = draft { if let Some(draft) = draft {
composer.set_draft(draft.clone()); composer.set_draft(draft.clone());
} }
@ -1801,13 +1864,17 @@ impl Component for Tabbed {
return true; return true;
} }
UIEvent::Action(Tab(Reply(coordinates, msg))) => { UIEvent::Action(Tab(Reply(coordinates, msg))) => {
self.add_component(Box::new(Composer::with_context(coordinates, msg, context))); self.add_component(Box::new(Composer::with_context(
*coordinates,
*msg,
context,
)));
self.cursor_pos = self.children.len() - 1; self.cursor_pos = self.children.len() - 1;
self.children[self.cursor_pos].set_dirty(true); self.children[self.cursor_pos].set_dirty(true);
return true; return true;
} }
UIEvent::Action(Tab(Edit(account_pos, msg))) => { UIEvent::Action(Tab(Edit(account_pos, msg))) => {
let composer = match Composer::edit(account_pos, msg, context) { let composer = match Composer::edit(*account_pos, *msg, context) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
context.replies.push_back(UIEvent::Notification( context.replies.push_back(UIEvent::Notification(
@ -1818,9 +1885,9 @@ impl Component for Tabbed {
log( log(
format!( format!(
"Failed to open envelope {}: {}", "Failed to open envelope {}: {}",
context.accounts[account_pos] context.accounts[*account_pos]
.collection .collection
.get_env(msg) .get_env(*msg)
.message_id_display(), .message_id_display(),
e.to_string() e.to_string()
), ),
@ -1853,7 +1920,7 @@ impl Component for Tabbed {
if self.pinned > self.cursor_pos { if self.pinned > self.cursor_pos {
return true; return true;
} }
if let Some(c_idx) = self.children.iter().position(|x| x.id() == id) { if let Some(c_idx) = self.children.iter().position(|x| x.id() == *id) {
self.children.remove(c_idx); self.children.remove(c_idx);
self.cursor_pos = 0; self.cursor_pos = 0;
self.set_dirty(true); self.set_dirty(true);
@ -1865,6 +1932,47 @@ impl Component for Tabbed {
); );
} }
} }
UIEvent::Action(Action::Listing(ListingAction::Filter(pattern)))
if self.show_shortcuts =>
{
self.help_search = Some(SearchPattern {
pattern: pattern.to_string(),
positions: vec![],
cursor: 0,
movement: Some(PageMovement::Home),
});
self.dirty = true;
return true;
}
UIEvent::Input(Key::Char('n')) if self.show_shortcuts && self.help_search.is_some() => {
if let Some(ref mut search) = self.help_search {
search.movement = Some(PageMovement::Down(1));
search.cursor += 1;
} else {
unsafe {
std::hint::unreachable_unchecked();
}
}
self.dirty = true;
return true;
}
UIEvent::Input(Key::Char('N')) if self.show_shortcuts && self.help_search.is_some() => {
if let Some(ref mut search) = self.help_search {
search.movement = Some(PageMovement::Up(1));
search.cursor = search.cursor.saturating_sub(1);
} else {
unsafe {
std::hint::unreachable_unchecked();
}
}
self.dirty = true;
return true;
}
UIEvent::Input(Key::Esc) if self.show_shortcuts && self.help_search.is_some() => {
self.help_search = None;
self.dirty = true;
return true;
}
UIEvent::Resize => { UIEvent::Resize => {
self.dirty = true; self.dirty = true;
} }

View File

@ -2425,3 +2425,74 @@ pub mod boundaries {
) )
} }
} }
use melib::text_processing::search::KMP;
impl KMP for CellBuffer {
fn kmp_search(&self, pattern: &str) -> smallvec::SmallVec<[usize; 256]> {
let (mut w, prev_ind) =
pattern
.char_indices()
.skip(1)
.fold((vec![], 0), |(mut acc, prev_ind), (i, _)| {
acc.push(&pattern[prev_ind..i]);
(acc, i)
});
w.push(&pattern[prev_ind..]);
let t = Self::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::SmallVec::new();
while j < self.buf.len() && k < w.len() as i32 {
if self.buf[j].ch() == '\n' {
j += 1;
continue;
}
if w[k as usize] == self.buf[j].ch().encode_utf8(&mut [0; 4]) {
j += 1;
k += 1;
if k as usize == w.len() {
ret.push(j - (k as usize));
k = t[k as usize];
}
} else {
k = t[k as usize];
if k < 0 {
j += 1;
k += 1;
}
}
}
ret
}
}
#[test]
fn test_cellbuffer_search() {
use melib::text_processing::{Reflow, TextProcessing, _ALICE_CHAPTER_1};
let lines: Vec<String> = _ALICE_CHAPTER_1.split_lines_reflow(Reflow::All, Some(78));
let mut buf = CellBuffer::new(
lines.iter().map(String::len).max().unwrap(),
lines.len(),
Cell::with_char(' '),
);
let width = buf.size().0;
for (i, l) in lines.iter().enumerate() {
write_string_to_grid(
l,
&mut buf,
Color::Default,
Color::Default,
Attr::Default,
((0, i), (width.saturating_sub(1), i)),
None,
);
}
for ind in buf.kmp_search("Alice") {
for c in &buf.cellvec()[ind..std::cmp::min(buf.cellvec().len(), ind + 25)] {
print!("{}", c.ch());
}
println!("");
}
}

View File

@ -252,7 +252,7 @@ pub mod segment_tree {
#[test] #[test]
fn test_segment_tree() { fn test_segment_tree() {
let array: SmallVec<[u8; 1024]> = [9, 1, 17, 2, 3, 23, 4, 5, 6, 37] let array: SmallVec<[u8; 1024]> = [9, 1, 17, 2, 3, 23, 4, 5, 6, 37]
.into_iter() .iter()
.cloned() .cloned()
.collect::<SmallVec<[u8; 1024]>>(); .collect::<SmallVec<[u8; 1024]>>();
let segment_tree = SegmentTree::from(array.clone()); let segment_tree = SegmentTree::from(array.clone());