Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Live search preview #51

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
search: show live preview as user types
Similar to the hlsearch feature from vim, show search results as user
types. This can be turned off via a config.
quark-zju committed Jul 21, 2021
commit 07a2d45a178c493dd87ebd7f4a47a8406c6e7d2d
72 changes: 52 additions & 20 deletions src/command.rs
Original file line number Diff line number Diff line change
@@ -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<DisplayAction, Error> {
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<DisplayAction, Error> {
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
}
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
3 changes: 2 additions & 1 deletion src/display.rs
Original file line number Diff line number Diff line change
@@ -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)
}
37 changes: 35 additions & 2 deletions src/prompt.rs
Original file line number Diff line number Diff line change
@@ -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<DisplayAction, Error>;
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<Box<PromptRunFn>>,

/// The closure to run when the user changes input.
/// Also called with empty text when the prompt is canceled.
preview: Option<Arc<Mutex<Box<PromptPreviewFn>>>>,
}

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<PromptPreviewFn>) -> Self {
self.preview = Some(Arc::new(Mutex::new(preview)));
self
}

fn state(&self) -> &PromptState {
self.history.state()
}
@@ -365,19 +378,24 @@ 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};
const CTRL: Modifiers = Modifiers::CTRL;
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
}

24 changes: 23 additions & 1 deletion src/screen.rs
Original file line number Diff line number Diff line change
@@ -258,6 +258,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
@@ -1210,18 +1220,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()),
@@ -1296,6 +1312,12 @@ impl Screen {
self.search_line_cache.clear();
}

/// Take the search. Useful for backing up the current search
/// and restore later.
pub(crate) fn take_search(&mut self) -> Option<Search> {
self.search.take()
}

/// Set the error file for this file.
pub(crate) fn set_error_file(&mut self, error_file: Option<File>) {
self.error_file = error_file;