From 5b08d16214f6290cfd7e02a3e5f21ddec716119e Mon Sep 17 00:00:00 2001 From: rhysd Date: Fri, 8 Jul 2022 10:05:53 +0900 Subject: [PATCH] implement text search in editor example --- examples/editor.rs | 218 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 178 insertions(+), 40 deletions(-) diff --git a/examples/editor.rs b/examples/editor.rs index b8cdcd8..e807018 100644 --- a/examples/editor.rs +++ b/examples/editor.rs @@ -13,7 +13,7 @@ use tui::backend::CrosstermBackend; use tui::layout::{Constraint, Direction, Layout}; use tui::style::{Color, Modifier, Style}; use tui::text::{Span, Spans}; -use tui::widgets::Paragraph; +use tui::widgets::{Block, Borders, Paragraph}; use tui::Terminal; use tui_textarea::{Input, Key, TextArea}; @@ -23,6 +23,60 @@ macro_rules! error { }}; } +struct SearchBox<'a> { + textarea: TextArea<'a>, + open: bool, +} + +impl<'a> Default for SearchBox<'a> { + fn default() -> Self { + let mut textarea = TextArea::default(); + textarea.set_block(Block::default().borders(Borders::ALL).title("Search")); + Self { + textarea, + open: false, + } + } +} + +impl<'a> SearchBox<'a> { + fn open(&mut self) { + self.open = true; + } + + fn close(&mut self) { + self.open = false; + let block = self.textarea.block().unwrap().clone(); + self.textarea = TextArea::default(); + self.textarea.set_block(block); + } + + fn height(&self) -> u16 { + if self.open { + 3 + } else { + 0 + } + } + + fn input(&mut self, input: Input) -> Option<&'_ str> { + match input { + Input { + key: Key::Enter, .. + } + | Input { + key: Key::Char('m'), + ctrl: true, + .. + } => None, + input => { + let modified = self.textarea.input(input); + modified.then(|| self.textarea.lines()[0].as_str()) + } + } + } +} + struct Buffer<'a> { textarea: TextArea<'a>, path: PathBuf, @@ -73,6 +127,7 @@ struct Editor<'a> { buffers: Vec>, term: Terminal>, message: Option>, + search: SearchBox<'a>, } impl<'a> Editor<'a> { @@ -99,28 +154,36 @@ impl<'a> Editor<'a> { buffers, term, message: None, + search: SearchBox::default(), }) } fn run(&mut self) -> io::Result<()> { - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Min(1), - Constraint::Length(1), - Constraint::Length(1), - ] - .as_ref(), - ); - loop { + let search_height = self.search.height(); + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(search_height), + Constraint::Min(1), + Constraint::Length(1), + Constraint::Length(1), + ] + .as_ref(), + ); + self.term.draw(|f| { + let chunks = layout.split(f.size()); + + if search_height > 0 { + f.render_widget(self.search.textarea.widget(), chunks[0]); + } + let buffer = &self.buffers[self.current]; let textarea = &buffer.textarea; - let chunks = layout.split(f.size()); let widget = textarea.widget(); - f.render_widget(widget, chunks[0]); + f.render_widget(widget, chunks[1]); // Render status line let modified = if buffer.modified { " [modified]" } else { "" }; @@ -138,7 +201,7 @@ impl<'a> Editor<'a> { ] .as_ref(), ) - .split(chunks[1]); + .split(chunks[2]); let status_style = Style::default().add_modifier(Modifier::REVERSED); f.render_widget(Paragraph::new(slot).style(status_style), status_chunks[0]); f.render_widget(Paragraph::new(path).style(status_style), status_chunks[1]); @@ -147,6 +210,18 @@ impl<'a> Editor<'a> { // Render message at bottom let message = if let Some(message) = self.message.take() { Spans::from(Span::raw(message)) + } else if search_height > 0 { + Spans::from(vec![ + Span::raw("Press "), + Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to jump to first match and close, "), + Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to close, "), + Span::styled("^G or ^→", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to search next, "), + Span::styled("M-G or ^←", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to search previous"), + ]) } else { Spans::from(vec![ Span::raw("Press "), @@ -154,38 +229,101 @@ impl<'a> Editor<'a> { Span::raw(" to quit, "), Span::styled("^S", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" to save, "), + Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to search, "), Span::styled("^X", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" to switch buffer"), ]) }; - f.render_widget(Paragraph::new(message), chunks[2]); + f.render_widget(Paragraph::new(message), chunks[3]); })?; - match Input::from(crossterm::event::read()?) { - Input { - key: Key::Char('q'), - ctrl: true, - .. - } => break, - Input { - key: Key::Char('x'), - ctrl: true, - .. - } => { - self.current = (self.current + 1) % self.buffers.len(); - self.message = Some(format!("Switched to buffer #{}", self.current + 1).into()); + if search_height > 0 { + let textarea = &mut self.buffers[self.current].textarea; + match Input::from(crossterm::event::read()?) { + Input { + key: Key::Char('g'), + ctrl: true, + alt: false, + } + | Input { + key: Key::Left, + ctrl: true, + .. + } => { + if !textarea.search_forward(false) { + self.message = Some("Pattern not found".into()); + } + } + Input { + key: Key::Char('g'), + ctrl: false, + alt: true, + } + | Input { + key: Key::Right, + ctrl: true, + .. + } => { + if !textarea.search_back(false) { + self.message = Some("Pattern not found".into()); + } + } + Input { + key: Key::Enter, .. + } => { + textarea.search_forward(true); + self.search.close(); + textarea.set_search_pattern("").unwrap(); + } + Input { key: Key::Esc, .. } => { + self.search.close(); + textarea.set_search_pattern("").unwrap(); + } + input => { + if let Some(query) = self.search.input(input) { + if let Err(err) = textarea.set_search_pattern(query) { + self.message = + Some(format!("Invalid search pattern: {}", err).into()); + } + } + } } - Input { - key: Key::Char('s'), - ctrl: true, - .. - } => { - self.buffers[self.current].save()?; - self.message = Some("Saved!".into()); - } - input => { - let buffer = &mut self.buffers[self.current]; - buffer.modified = buffer.textarea.input(input); + } else { + match Input::from(crossterm::event::read()?) { + Input { + key: Key::Char('q'), + ctrl: true, + .. + } => break, + Input { + key: Key::Char('x'), + ctrl: true, + .. + } => { + self.current = (self.current + 1) % self.buffers.len(); + self.message = + Some(format!("Switched to buffer #{}", self.current + 1).into()); + } + Input { + key: Key::Char('s'), + ctrl: true, + .. + } => { + self.buffers[self.current].save()?; + self.message = Some("Saved!".into()); + } + Input { + key: Key::Char('g'), + ctrl: true, + .. + } => { + self.search.open(); + } + input => { + let buffer = &mut self.buffers[self.current]; + buffer.modified = buffer.textarea.input(input); + } } } }