From 670bacddd65ffd9552e1a77981d2631a44088135 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 4 Jan 2024 13:55:28 +0000 Subject: [PATCH] Implement searching within files. I think that's now all the functionality from the Python prototype, and we're ready to switch over! --- src/activity_stack.rs | 3 ++ src/editor.rs | 14 ++++++ src/file.rs | 76 +++++++++++++++++++++++++++- src/tui.rs | 112 ++++++++++++++++++++++++------------------ 4 files changed, 156 insertions(+), 49 deletions(-) diff --git a/src/activity_stack.rs b/src/activity_stack.rs index c5de929..ef670c0 100644 --- a/src/activity_stack.rs +++ b/src/activity_stack.rs @@ -1,3 +1,5 @@ +use super::file::SearchDirection; + #[derive(PartialEq, Eq, Debug, Clone)] pub enum NonUtilityActivity { MainMenu, @@ -33,6 +35,7 @@ pub enum OverlayActivity { GetUserToExamine, GetPostIdToRead, GetHashtagToRead, + GetSearchExpression(SearchDirection), } #[derive(PartialEq, Eq, Debug, Clone)] diff --git a/src/editor.rs b/src/editor.rs index 75c63fb..26554a6 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -4,6 +4,7 @@ use unicode_width::UnicodeWidthChar; use super::activity_stack::{NonUtilityActivity, UtilityActivity}; use super::client::{Client, ClientError}; use super::coloured_string::ColouredString; +use super::file::SearchDirection; use super::text::*; use super::tui::{ ActivityState, CursorPosition, LogicalAction, @@ -721,6 +722,19 @@ pub fn get_hashtag_to_read() -> Box { )) } +pub fn get_search_expression(dir: SearchDirection) -> Box { + let title = match dir { + SearchDirection::Up => "Search back (blank = last): ", + SearchDirection::Down => "Search (blank = last): ", + }; + Box::new(BottomLineEditorOverlay::new( + ColouredString::plain(title), + Box::new(move |s, _client| { + LogicalAction::GotSearchExpression(dir, s.to_owned()) + }) + )) +} + #[derive(Debug, PartialEq, Eq, Clone)] struct ComposeBufferRegion { start: usize, diff --git a/src/file.rs b/src/file.rs index b3518ae..48f1514 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,8 +1,9 @@ use itertools::Itertools; +use regex::Regex; use std::cmp::{min, max}; use std::collections::{HashMap, HashSet}; -use super::activity_stack::UtilityActivity; +use super::activity_stack::{UtilityActivity, OverlayActivity}; use super::client::{Client, ClientError, FeedId, FeedExtend}; use super::coloured_string::ColouredString; use super::text::*; @@ -277,6 +278,9 @@ enum UIMode { Select(HighlightType, SelectionPurpose), } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum SearchDirection { Up, Down } + struct File { contents: FileContents, rendered: HashMap>, @@ -286,6 +290,8 @@ struct File { selection: Option<(isize, usize)>, select_aux: Option, // distinguishes fave from unfave, etc file_desc: Type, + search_direction: Option, + last_search: Option, } impl File { @@ -327,6 +333,8 @@ impl File { selection: None, select_aux: None, file_desc, + search_direction: None, + last_search: None, }; Ok(ff) } @@ -753,6 +761,44 @@ impl File { result } + + fn search(&mut self) -> LogicalAction { + if let Some(dir) = self.search_direction { + if !self.last_search.is_some() { + return LogicalAction::Beep; + } + + let next_line = match dir { + SearchDirection::Up => |s: &mut Self| s.move_up(1), + SearchDirection::Down => |s: &mut Self| s.move_down(1), + }; + loop { + let old_pos = self.pos; + next_line(self); + if self.pos == old_pos { + break LogicalAction::Beep; + } + + let rendered = self.rendered.get(&self.pos.item) + .expect("we should have just rendered it"); + // self.pos.line indicates the line number just off + // the bottom of the screen, so it's never 0 unless + // we're at the very top of the file + if let Some(lineno) = self.pos.line.checked_sub(1) { + if let Some(line) = rendered.get(lineno) { + if self.last_search.as_ref() + .expect("we just checked it above") + .find(line.text()).is_some() + { + break LogicalAction::Nothing; + } + } + } + } + } else { + LogicalAction::Beep + } + } } impl @@ -1050,6 +1096,21 @@ impl LogicalAction::Nothing } + Pr('/') | Pr('\\') => { + let search_direction = match key { + Pr('/') => SearchDirection::Down, + Pr('\\') => SearchDirection::Up, + _ => panic!("how are we in this arm anyway?") + }; + self.search_direction = Some(search_direction); + LogicalAction::Goto(OverlayActivity::GetSearchExpression( + search_direction).into()) + } + + Pr('n') | Pr('N') => { + self.search() + } + _ => LogicalAction::Nothing, } UIMode::ListSubmenu => match key { @@ -1126,6 +1187,19 @@ impl { self.file_desc.save_file_position(self.pos, file_positions); } + + fn got_search_expression(&mut self, dir: SearchDirection, regex: String) + -> LogicalAction + { + match Regex::new(®ex) { + Ok(re) => { + self.search_direction = Some(dir); + self.last_search = Some(re); + self.search() + } + Err(..) => LogicalAction::Beep, + } + } } pub fn home_timeline(file_positions: &HashMap, diff --git a/src/tui.rs b/src/tui.rs index af8d238..ef3db94 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -399,6 +399,7 @@ pub enum LogicalAction { Pop, PopOverlaySilent, PopOverlayBeep, + GotSearchExpression(SearchDirection, String), Goto(Activity), Exit, Nothing, @@ -417,6 +418,11 @@ pub trait ActivityState { _client: &mut Client) {} fn save_file_position( &self, _file_positions: &mut HashMap) {} + fn got_search_expression(&mut self, _dir: SearchDirection, _regex: String) + -> LogicalAction + { + panic!("a trait returning GetSearchExpression should fill this in"); + } } struct TuiLogicalState { @@ -509,7 +515,7 @@ impl TuiLogicalState { fn handle_keypress(&mut self, key: OurKey, client: &mut Client) -> PhysicalAction { - let logact = match key { + let mut logact = match key { // Central handling of [ESC]: it _always_ goes to the // utilities menu, from any UI context at all. OurKey::Escape => LogicalAction::Goto( @@ -522,53 +528,61 @@ impl TuiLogicalState { } }; - match logact { - LogicalAction::Beep => PhysicalAction::Beep, - LogicalAction::Exit => PhysicalAction::Exit, - LogicalAction::Nothing => PhysicalAction::Nothing, - LogicalAction::Goto(activity) => { - self.activity_stack.goto(activity); - self.changed_activity(client, None); - PhysicalAction::Nothing - } - LogicalAction::Pop => { - self.activity_stack.pop(); - self.changed_activity(client, None); - PhysicalAction::Nothing - } - LogicalAction::PopOverlaySilent => { - self.pop_overlay_activity(); - PhysicalAction::Nothing - } - LogicalAction::PopOverlayBeep => { - self.pop_overlay_activity(); - PhysicalAction::Beep - } - LogicalAction::Error(_) => PhysicalAction::Beep, // FIXME: Error Log - LogicalAction::PostComposed(post) => { - let newact = match self.activity_stack.top() { - Activity::NonUtil(NonUtilityActivity::ComposeToplevel) => - NonUtilityActivity::PostComposeMenu.into(), - Activity::Util(UtilityActivity::ComposeReply(id)) => - UtilityActivity::PostReplyMenu(id.clone()).into(), - act => panic!("can't postcompose {act:?}"), - }; - self.activity_stack.chain_to(newact); - self.changed_activity(client, Some(post)); - PhysicalAction::Nothing - } - LogicalAction::PostReEdit(post) => { - let newact = match self.activity_stack.top() { - Activity::NonUtil(NonUtilityActivity::PostComposeMenu) => - NonUtilityActivity::ComposeToplevel.into(), - Activity::Util(UtilityActivity::PostReplyMenu(id)) => - UtilityActivity::ComposeReply(id.clone()).into(), - act => panic!("can't reedit {act:?}"), - }; - self.activity_stack.chain_to(newact); - self.changed_activity(client, Some(post)); - PhysicalAction::Nothing - } + loop { + logact = match logact { + LogicalAction::Beep => break PhysicalAction::Beep, + LogicalAction::Exit => break PhysicalAction::Exit, + LogicalAction::Nothing => break PhysicalAction::Nothing, + LogicalAction::Goto(activity) => { + self.activity_stack.goto(activity); + self.changed_activity(client, None); + break PhysicalAction::Nothing + } + LogicalAction::Pop => { + self.activity_stack.pop(); + self.changed_activity(client, None); + break PhysicalAction::Nothing + } + LogicalAction::PopOverlaySilent => { + self.pop_overlay_activity(); + break PhysicalAction::Nothing + } + LogicalAction::PopOverlayBeep => { + self.pop_overlay_activity(); + break PhysicalAction::Beep + } + LogicalAction::GotSearchExpression(dir, regex) => { + self.pop_overlay_activity(); + self.activity_state.got_search_expression(dir, regex) + } + LogicalAction::Error(_) => + break PhysicalAction::Beep, // FIXME: Error Log + LogicalAction::PostComposed(post) => { + let newact = match self.activity_stack.top() { + Activity::NonUtil( + NonUtilityActivity::ComposeToplevel) => + NonUtilityActivity::PostComposeMenu.into(), + Activity::Util(UtilityActivity::ComposeReply(id)) => + UtilityActivity::PostReplyMenu(id.clone()).into(), + act => panic!("can't postcompose {act:?}"), + }; + self.activity_stack.chain_to(newact); + self.changed_activity(client, Some(post)); + break PhysicalAction::Nothing + } + LogicalAction::PostReEdit(post) => { + let newact = match self.activity_stack.top() { + Activity::NonUtil(NonUtilityActivity::PostComposeMenu) => + NonUtilityActivity::ComposeToplevel.into(), + Activity::Util(UtilityActivity::PostReplyMenu(id)) => + UtilityActivity::ComposeReply(id.clone()).into(), + act => panic!("can't reedit {act:?}"), + }; + self.activity_stack.chain_to(newact); + self.changed_activity(client, Some(post)); + break PhysicalAction::Nothing + } + }; } } @@ -644,6 +658,8 @@ fn new_activity_state(&self, activity: Activity, client: &mut Client, Ok(get_post_id_to_read()), Activity::Overlay(OverlayActivity::GetHashtagToRead) => Ok(get_hashtag_to_read()), + Activity::Overlay(OverlayActivity::GetSearchExpression(dir)) => + Ok(get_search_expression(dir)), Activity::Util(UtilityActivity::ExamineUser(ref name)) => examine_user(client, name), Activity::Util(UtilityActivity::InfoStatus(ref id)) => -- 2.30.2