chiark / gitweb /
Implement searching within files.
authorSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 13:55:28 +0000 (13:55 +0000)
committerSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 13:56:22 +0000 (13:56 +0000)
I think that's now all the functionality from the Python prototype,
and we're ready to switch over!

src/activity_stack.rs
src/editor.rs
src/file.rs
src/tui.rs

index c5de9290aff4841388b81fc28f5531c25aed4788..ef670c0d5580ed2d38ca083f7fc9b08063902069 100644 (file)
@@ -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)]
index 75c63fba3dfd1d5a5d157be6ec2640ecc553d2f5..26554a6fc7ebd85625d659e7b269fba28a84e3aa 100644 (file)
@@ -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<dyn ActivityState> {
     ))
 }
 
+pub fn get_search_expression(dir: SearchDirection) -> Box<dyn ActivityState> {
+    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,
index b3518ae645b6c634a4c6b49b4242550c45688810..48f151494bc4f9fb99922d52435c737f51f096c4 100644 (file)
@@ -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<Type: FileType, Source: FileDataSource> {
     contents: FileContents<Type, Source>,
     rendered: HashMap<isize, Vec<ColouredString>>,
@@ -286,6 +290,8 @@ struct File<Type: FileType, Source: FileDataSource> {
     selection: Option<(isize, usize)>,
     select_aux: Option<bool>, // distinguishes fave from unfave, etc
     file_desc: Type,
+    search_direction: Option<SearchDirection>,
+    last_search: Option<Regex>,
 }
 
 impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
@@ -327,6 +333,8 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
             selection: None,
             select_aux: None,
             file_desc,
+            search_direction: None,
+            last_search: None,
         };
         Ok(ff)
     }
@@ -753,6 +761,44 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
 
         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<Type: FileType, Source: FileDataSource>
@@ -1050,6 +1096,21 @@ impl<Type: FileType, Source: FileDataSource>
                     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<Type: FileType, Source: FileDataSource>
     {
         self.file_desc.save_file_position(self.pos, file_positions);
     }
+
+    fn got_search_expression(&mut self, dir: SearchDirection, regex: String)
+                             -> LogicalAction
+    {
+        match Regex::new(&regex) {
+            Ok(re) => {
+                self.search_direction = Some(dir);
+                self.last_search = Some(re);
+                self.search()
+            }
+            Err(..) => LogicalAction::Beep,
+        }
+    }
 }
 
 pub fn home_timeline(file_positions: &HashMap<FeedId, FilePosition>,
index af8d2382e663dc7b12c041f813bc5da067a3fec5..ef3db9423418a5e477eeb601129ac1b9de81c642 100644 (file)
@@ -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<FeedId, FilePosition>) {}
+    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)) =>