зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-03 14:26:17 +03:00
implement text search in editor example
Этот коммит содержится в:
@@ -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<Buffer<'a>>,
|
||||
term: Terminal<CrosstermBackend<io::Stdout>>,
|
||||
message: Option<Cow<'static, str>>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user