From bde64e7dc525ca41083a5681ef56980dc996eb6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Mon, 26 Jan 2026 01:25:40 +0100 Subject: [PATCH] Basic search mechanism in editor --- crates/ratatui/Cargo.toml | 3 +- crates/ratatui/src/editor.rs | 74 ++++++++++++++++++++++++++++++++++++ src/input.rs | 28 ++++++++++++-- src/views/render.rs | 53 +++++++++++++++++++------- 4 files changed, 141 insertions(+), 17 deletions(-) diff --git a/crates/ratatui/Cargo.toml b/crates/ratatui/Cargo.toml index 060c608..5a5c374 100644 --- a/crates/ratatui/Cargo.toml +++ b/crates/ratatui/Cargo.toml @@ -5,4 +5,5 @@ edition = "2021" [dependencies] ratatui = "0.29" -tui-textarea = "0.7" +regex = "1" +tui-textarea = { version = "0.7", features = ["search"] } diff --git a/crates/ratatui/src/editor.rs b/crates/ratatui/src/editor.rs index 2a628ca..99eaa9d 100644 --- a/crates/ratatui/src/editor.rs +++ b/crates/ratatui/src/editor.rs @@ -40,9 +40,24 @@ impl CompletionState { } } +struct SearchState { + query: String, + active: bool, +} + +impl SearchState { + fn new() -> Self { + Self { + query: String::new(), + active: false, + } + } +} + pub struct Editor { text: TextArea<'static>, completion: CompletionState, + search: SearchState, } impl Default for Editor { @@ -56,12 +71,15 @@ impl Editor { Self { text: TextArea::default(), completion: CompletionState::new(), + search: SearchState::new(), } } pub fn set_content(&mut self, lines: Vec) { self.text = TextArea::new(lines); self.completion.active = false; + self.search.query.clear(); + self.search.active = false; } pub fn set_candidates(&mut self, candidates: Vec) { @@ -99,6 +117,62 @@ impl Editor { } } + pub fn activate_search(&mut self) { + self.search.active = true; + self.completion.active = false; + } + + pub fn search_active(&self) -> bool { + self.search.active + } + + pub fn search_query(&self) -> &str { + &self.search.query + } + + pub fn search_input(&mut self, c: char) { + self.search.query.push(c); + self.apply_search_pattern(); + } + + pub fn search_backspace(&mut self) { + self.search.query.pop(); + self.apply_search_pattern(); + } + + pub fn search_confirm(&mut self) { + self.search.active = false; + } + + pub fn search_clear(&mut self) { + self.search.query.clear(); + self.search.active = false; + let _ = self.text.set_search_pattern(""); + } + + pub fn search_next(&mut self) -> bool { + if self.search.query.is_empty() { + return false; + } + self.text.search_forward(false) + } + + pub fn search_prev(&mut self) -> bool { + if self.search.query.is_empty() { + return false; + } + self.text.search_back(false) + } + + fn apply_search_pattern(&mut self) { + if self.search.query.is_empty() { + let _ = self.text.set_search_pattern(""); + } else { + let pattern = format!("(?i){}", regex::escape(&self.search.query)); + let _ = self.text.set_search_pattern(&pattern); + } + } + pub fn input(&mut self, input: impl Into) { let input: tui_textarea::Input = input.into(); let has_modifier = input.ctrl || input.alt; diff --git a/src/input.rs b/src/input.rs index 85eb868..1d2b543 100644 --- a/src/input.rs +++ b/src/input.rs @@ -385,10 +385,23 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { }, Modal::Editor => { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + let editor = &mut ctx.app.editor_ctx.editor; + + if editor.search_active() { + match key.code { + KeyCode::Esc => editor.search_clear(), + KeyCode::Enter => editor.search_confirm(), + KeyCode::Backspace => editor.search_backspace(), + KeyCode::Char(c) if !ctrl => editor.search_input(c), + _ => {} + } + return InputResult::Continue; + } + match key.code { KeyCode::Esc => { - if ctx.app.editor_ctx.editor.completion_active() { - ctx.app.editor_ctx.editor.dismiss_completion(); + if editor.completion_active() { + editor.dismiss_completion(); } else { ctx.dispatch(AppCommand::SaveEditorToStep); ctx.dispatch(AppCommand::CompileCurrentStep); @@ -399,8 +412,17 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { ctx.dispatch(AppCommand::SaveEditorToStep); ctx.dispatch(AppCommand::CompileCurrentStep); } + KeyCode::Char('f') if ctrl => { + editor.activate_search(); + } + KeyCode::Char('n') if ctrl => { + editor.search_next(); + } + KeyCode::Char('p') if ctrl => { + editor.search_prev(); + } _ => { - ctx.app.editor_ctx.editor.input(Event::Key(key)); + editor.input(Event::Key(key)); } } } diff --git a/src/views/render.rs b/src/views/render.rs index 80cf4ad..4bf5286 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -570,8 +570,30 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term highlight::highlight_line_with_runtime(line, &exec, &sel) }; - let editor_area = Rect::new(inner.x, inner.y, inner.width, inner.height.saturating_sub(1)); - let hint_area = Rect::new(inner.x, inner.y + editor_area.height, inner.width, 1); + let show_search = app.editor_ctx.editor.search_active() + || !app.editor_ctx.editor.search_query().is_empty(); + + let (search_area, editor_area, hint_area) = if show_search { + let search_area = Rect::new(inner.x, inner.y, inner.width, 1); + let editor_area = Rect::new(inner.x, inner.y + 1, inner.width, inner.height.saturating_sub(2)); + let hint_area = Rect::new(inner.x, inner.y + 1 + editor_area.height, inner.width, 1); + (Some(search_area), editor_area, hint_area) + } else { + let editor_area = Rect::new(inner.x, inner.y, inner.width, inner.height.saturating_sub(1)); + let hint_area = Rect::new(inner.x, inner.y + editor_area.height, inner.width, 1); + (None, editor_area, hint_area) + }; + + if let Some(sa) = search_area { + let style = if app.editor_ctx.editor.search_active() { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::DarkGray) + }; + let cursor = if app.editor_ctx.editor.search_active() { "_" } else { "" }; + let text = format!("/{}{}", app.editor_ctx.editor.search_query(), cursor); + frame.render_widget(Paragraph::new(Line::from(Span::styled(text, style))), sa); + } if let Some(kind) = flash_kind { let bg = match kind { @@ -586,17 +608,22 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let dim = Style::default().fg(Color::DarkGray); let key = Style::default().fg(Color::Yellow); - let hint = Line::from(vec![ - Span::styled("Esc", key), Span::styled(" save ", dim), - Span::styled("C-e", key), Span::styled(" eval ", dim), - Span::styled("C-u", key), Span::styled("/", dim), - Span::styled("C-r", key), Span::styled(" undo/redo ", dim), - Span::styled("C-j", key), Span::styled("/", dim), - Span::styled("C-k", key), Span::styled(" del-bol/eol ", dim), - Span::styled("C-x", key), Span::styled("/", dim), - Span::styled("C-c", key), Span::styled("/", dim), - Span::styled("C-y", key), Span::styled(" cut/copy/paste ", dim), - ]); + let hint = if app.editor_ctx.editor.search_active() { + Line::from(vec![ + Span::styled("Enter", key), Span::styled(" confirm ", dim), + Span::styled("Esc", key), Span::styled(" cancel", dim), + ]) + } else { + Line::from(vec![ + Span::styled("Esc", key), Span::styled(" save ", dim), + Span::styled("C-e", key), Span::styled(" eval ", dim), + Span::styled("C-f", key), Span::styled(" find ", dim), + Span::styled("C-n", key), Span::styled("/", dim), + Span::styled("C-p", key), Span::styled(" next/prev ", dim), + Span::styled("C-u", key), Span::styled("/", dim), + Span::styled("C-r", key), Span::styled(" undo/redo", dim), + ]) + }; frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area); } }