diff --git a/src/command.rs b/src/command.rs index c09d0c8..65733f9 100644 --- a/src/command.rs +++ b/src/command.rs @@ -7,7 +7,7 @@ use crate::error::Error; use crate::event::EventSender; use crate::file::FileInfo; use crate::prompt::Prompt; -use crate::screen::Screen; +use crate::screen::{Screen, Scroll}; use crate::search::{MatchMotion, Search, SearchKind}; /// Go to a line (Shortcut: ':') @@ -37,7 +37,7 @@ pub(crate) fn goto() -> Prompt { value_percent += 100; } let value = value_percent * (lines - 1) / 100; - screen.scroll_to(value as usize); + screen.scroll_to(Scroll::Center(value as usize)); } Err(e) => { screen.error = Some(e.to_string()); @@ -56,7 +56,7 @@ pub(crate) fn goto() -> Prompt { } else { value - 1 }; - screen.scroll_to(value as usize); + screen.scroll_to(Scroll::Center(value as usize)); } Err(e) => { screen.error = Some(e.to_string()); @@ -72,27 +72,59 @@ pub(crate) fn goto() -> Prompt { /// Search for text (Shortcuts: '/', '<', '>') /// /// Prompts the user for text to search. -pub(crate) fn search(kind: SearchKind, event_sender: EventSender) -> Prompt { - Prompt::new( - "search", - "Search:", - Box::new( - move |screen: &mut Screen, value: &str| -> Result { - screen.refresh_matched_lines(); - if value.is_empty() { - match kind { - SearchKind::First | SearchKind::FirstAfter(_) => { - screen.move_match(MatchMotion::NextLine) +pub(crate) fn search(kind: SearchKind, event_sender: EventSender, preview: bool) -> Prompt { + let mut prompt = { + let event_sender = event_sender.clone(); + Prompt::new( + "search", + "Search:", + Box::new( + move |screen: &mut Screen, value: &str| -> Result { + screen.refresh_matched_lines(); + if value.is_empty() { + match kind { + SearchKind::First | SearchKind::FirstAfter(_) => { + screen.move_match(MatchMotion::NextLine) + } + SearchKind::FirstBefore(_) => { + screen.move_match(MatchMotion::PreviousLine) + } } - SearchKind::FirstBefore(_) => screen.move_match(MatchMotion::PreviousLine), + } else { + screen.set_search( + Search::new(&screen.file, value, kind, event_sender.clone()).ok(), + ); } - } else { - screen.set_search( - Search::new(&screen.file, value, kind, event_sender.clone()).ok(), - ); + Ok(DisplayAction::Render) + }, + ), + ) + }; + if preview { + let mut orig_top = None; + let mut orig_search = None; + let preview = move |screen: &mut Screen, value: &str| -> Result<(), Error> { + if orig_top.is_none() { + // Remember the original top line position. + orig_top = Some(screen.top_line()); + orig_search = screen.take_search(); + } + screen.refresh_matched_lines(); + if value.is_empty() { + // Cancel a search. Restore to the previous state. + screen.set_search(orig_search.take()); + if let Some(top) = orig_top { + // Restore the original top line position. + screen.scroll_to(Scroll::Top(top)); } - Ok(DisplayAction::Render) - }, - ), - ) + orig_top = None; + } else { + screen + .set_search(Search::new(&screen.file, value, kind, event_sender.clone()).ok()); + } + Ok(()) + }; + prompt = prompt.with_preview(Box::new(preview)); + } + prompt } diff --git a/src/config.rs b/src/config.rs index 25da246..adcb291 100644 --- a/src/config.rs +++ b/src/config.rs @@ -175,6 +175,9 @@ pub struct Config { /// Specify the name of the default key map. pub keymap: KeymapConfig, + + /// Specify whether search is highlighted when typing. + pub highlight_search: bool, } impl Default for Config { @@ -187,6 +190,7 @@ impl Default for Config { show_ruler: true, wrapping_mode: Default::default(), keymap: Default::default(), + highlight_search: true, } } } diff --git a/src/display.rs b/src/display.rs index 750f846..e5eb255 100644 --- a/src/display.rs +++ b/src/display.rs @@ -292,11 +292,12 @@ pub(crate) fn start( } Some(Event::Input(InputEvent::Paste(ref text))) => { let width = screen.width(); + let preview = screen.config().highlight_search; screen .prompt() .get_or_insert_with(|| { // Assume the user wanted to search for what they're pasting. - command::search(SearchKind::First, event_sender.clone()) + command::search(SearchKind::First, event_sender.clone(), preview) }) .paste(text, width) } diff --git a/src/pager.rs b/src/pager.rs index 7803640..77c8eae 100644 --- a/src/pager.rs +++ b/src/pager.rs @@ -241,6 +241,11 @@ impl Pager { self.events.action_sender() } + /// Configure the pager. + pub fn configure(&mut self, mut func: impl FnMut(&mut Config)) { + func(&mut self.config) + } + /// Run Stream Pager. pub fn run(self) -> Result<()> { crate::display::start( diff --git a/src/prompt.rs b/src/prompt.rs index fb54194..398365c 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -2,6 +2,8 @@ use std::char; use std::fmt::Write; +use std::sync::Arc; +use std::sync::Mutex; use termwiz::cell::{AttributeChange, CellAttributes}; use termwiz::color::{AnsiColor, ColorAttribute}; @@ -17,6 +19,7 @@ use crate::screen::Screen; use crate::util; type PromptRunFn = dyn FnMut(&mut Screen, &str) -> Result; +type PromptPreviewFn = dyn FnMut(&mut Screen, &str) -> Result<(), Error>; /// A prompt for input from the user. pub(crate) struct Prompt { @@ -28,6 +31,10 @@ pub(crate) struct Prompt { /// The closure to run when the user presses Return. Will only be called once. run: Option>, + + /// The closure to run when the user changes input. + /// Also called with empty text when the prompt is canceled. + preview: Option>>>, } pub(crate) struct PromptState { @@ -330,9 +337,15 @@ impl Prompt { prompt: prompt.to_string(), history: PromptHistory::open(ident), run: Some(run), + preview: None, } } + pub(crate) fn with_preview(mut self, preview: Box) -> Self { + self.preview = Some(Arc::new(Mutex::new(preview))); + self + } + fn state(&self) -> &PromptState { self.history.state() } @@ -365,6 +378,11 @@ impl Prompt { self.state_mut().render(changes, offset, width); } + /// Current text input by the user. + fn value(&self) -> String { + self.state().value[..].iter().collect() + } + /// Dispatch a key press to the prompt. pub(crate) fn dispatch_key(&mut self, key: KeyEvent, width: usize) -> DisplayAction { use termwiz::input::{KeyCode::*, Modifiers}; @@ -372,12 +390,12 @@ impl Prompt { const NONE: Modifiers = Modifiers::NONE; const ALT: Modifiers = Modifiers::ALT; let value_width = width - self.prompt.width() - 4; + let value: String = self.value(); let action = match (key.modifiers, key.key) { (NONE, Enter) | (CTRL, Char('J')) | (CTRL, Char('M')) => { // Finish. let _ = self.history.save(); let mut run = self.run.take(); - let value: String = self.state().value[..].iter().collect(); return DisplayAction::Run(Box::new(move |screen: &mut Screen| { screen.clear_prompt(); if let Some(ref mut run) = run { @@ -389,7 +407,12 @@ impl Prompt { } (NONE, Escape) | (CTRL, Char('C')) => { // Cancel. - return DisplayAction::Run(Box::new(|screen: &mut Screen| { + let preview = self.preview.clone(); + return DisplayAction::Run(Box::new(move |screen: &mut Screen| { + if let Some(preview) = &preview { + let mut preview = preview.lock().unwrap(); + preview(screen, "")?; + } screen.clear_prompt(); Ok(DisplayAction::Render) })); @@ -413,6 +436,16 @@ impl Prompt { _ => return DisplayAction::None, }; self.state_mut().clamp_offset(value_width); + let new_value: String = self.value(); + if let Some(preview) = self.preview.clone() { + if value != new_value && !matches!(action, DisplayAction::Run(_)) { + return DisplayAction::Run(Box::new(move |screen: &mut Screen| { + let mut preview = preview.lock().unwrap(); + preview(screen, &new_value)?; + Ok(DisplayAction::Render) + })); + } + } action } diff --git a/src/screen.rs b/src/screen.rs index 848c578..1c283f9 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -27,6 +27,7 @@ //! ``` use std::cmp::{max, min}; +use std::ops::RangeInclusive; use std::sync::Arc; use termwiz::cell::{CellAttributes, Intensity}; @@ -193,7 +194,7 @@ pub(crate) struct Screen { following_end: bool, /// Scroll to a particular line in the file. - pending_absolute_scroll: Option, + pending_absolute_scroll: Option, /// Scroll relative number of rows. pending_relative_scroll: isize, @@ -208,6 +209,15 @@ pub(crate) struct Screen { repeat_count: Option, } +#[derive(Copy, Clone, Debug)] +pub(crate) enum Scroll { + /// Scroll to the given line and make it the center of the screen. + Center(usize), + + /// Scroll to the given line and make it the top of the screen. + Top(usize), +} + impl Screen { /// Create a screen that displays a file. pub(crate) fn new(file: File, config: Arc) -> Result { @@ -249,6 +259,16 @@ impl Screen { } } + /// Get the config. + pub(crate) fn config(&self) -> &Config { + &self.config + } + + /// Get the rendered top line position.j + pub(crate) fn top_line(&self) -> usize { + self.top_line + } + /// Get the screen width pub(crate) fn width(&self) -> usize { self.width @@ -445,13 +465,19 @@ impl Screen { } // Perform pending absolute scroll - if let Some(line) = self.pending_absolute_scroll.take() { - self.top_line = line; - self.top_line_portion = 0; - pending_refresh.add_range(0, file_view_height); - // Scroll up so that the target line is in the center of the - // file view. - self.pending_relative_scroll -= (file_view_height / 2) as isize; + if let Some(scroll) = self.pending_absolute_scroll.take() { + match scroll { + Scroll::Top(line) | Scroll::Center(line) => { + self.top_line = line; + self.top_line_portion = 0; + pending_refresh.add_range(0, file_view_height); + if matches!(scroll, Scroll::Center(_)) { + // Scroll up so that the target line is in the center of the + // file view. + self.pending_relative_scroll -= (file_view_height / 2) as isize; + } + } + } } enum Direction { @@ -1052,8 +1078,8 @@ impl Screen { } /// Scrolls to the given line number. - pub(crate) fn scroll_to(&mut self, line: usize) { - self.pending_absolute_scroll = Some(line); + pub(crate) fn scroll_to(&mut self, scroll: Scroll) { + self.pending_absolute_scroll = Some(scroll); self.pending_relative_scroll = 0; self.following_end = false; } @@ -1164,10 +1190,10 @@ impl Screen { ScrollToTop | ScrollToBottom if self.repeat_count.is_some() => { if let Some(n) = self.repeat_count { // Convert 1-based to 0-based line number. - self.scroll_to(n.max(1) - 1); + self.scroll_to(Scroll::Center(n.max(1) - 1)); } } - ScrollToTop => self.scroll_to(0), + ScrollToTop => self.scroll_to(Scroll::Top(0)), ScrollToBottom => self.following_end = true, ScrollLeftColumns(n) => { let n = self.apply_repeat_count(n); @@ -1195,18 +1221,24 @@ impl Screen { } PromptGoToLine => self.prompt = Some(command::goto()), PromptSearchFromStart => { - self.prompt = Some(command::search(SearchKind::First, event_sender.clone())) + self.prompt = Some(command::search( + SearchKind::First, + event_sender.clone(), + self.config.highlight_search, + )) } PromptSearchForwards => { self.prompt = Some(command::search( SearchKind::FirstAfter(self.rendered.top_line), event_sender.clone(), + self.config.highlight_search, )) } PromptSearchBackwards => { self.prompt = Some(command::search( SearchKind::FirstBefore(self.rendered.bottom_line), event_sender.clone(), + self.config.highlight_search, )) } PreviousMatch => self.create_or_move_match(MatchMotion::Previous, event_sender.clone()), @@ -1279,6 +1311,14 @@ impl Screen { pub(crate) fn set_search(&mut self, search: Option) { self.search = search; self.search_line_cache.clear(); + self.refresh_search_status(); + self.refresh_prompt(); + } + + /// Take the search. Useful for backing up the current search + /// and restore later. + pub(crate) fn take_search(&mut self) -> Option { + self.search.take() } /// Set the error file for this file. @@ -1345,7 +1385,10 @@ impl Screen { .as_ref() .and_then(|ref search| search.current_match()); if let Some((line_index, _match_index)) = current_match { - self.scroll_to(line_index); + let range = self.visible_line_range(); + if !range.contains(&line_index) { + self.scroll_to(Scroll::Center(line_index)); + } self.refresh_matched_lines(); self.refresh_overlay(); return DisplayAction::Render; @@ -1361,14 +1404,21 @@ impl Screen { DisplayAction::Render } + /// Range of the visible lines. + pub(crate) fn visible_line_range(&self) -> RangeInclusive { + self.rendered.top_line..=self.rendered.bottom_line + } + /// Move the currently selected match to a new match. pub(crate) fn move_match(&mut self, motion: MatchMotion) { self.refresh_matched_line(); + let scope = self.visible_line_range(); if let Some(ref mut search) = self.search { - let scope = self.rendered.top_line..=self.rendered.bottom_line; - search.move_match(motion, scope); + search.move_match(motion, scope.clone()); if let Some((line_index, _match_index)) = search.current_match() { - self.scroll_to(line_index); + if !scope.contains(&line_index) { + self.scroll_to(Scroll::Center(line_index)); + } } self.refresh_matched_line(); self.refresh_search_status();