Basic search mechanism in editor

This commit is contained in:
2026-01-26 01:25:40 +01:00
parent 2453b78237
commit 2235a4b0a1
4 changed files with 141 additions and 17 deletions

View File

@@ -5,4 +5,5 @@ edition = "2021"
[dependencies]
ratatui = "0.29"
tui-textarea = "0.7"
regex = "1"
tui-textarea = { version = "0.7", features = ["search"] }

View File

@@ -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<String>) {
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<CompletionCandidate>) {
@@ -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<tui_textarea::Input>) {
let input: tui_textarea::Input = input.into();
let has_modifier = input.ctrl || input.alt;

View File

@@ -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));
}
}
}

View File

@@ -570,8 +570,30 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
highlight::highlight_line_with_runtime(line, &exec, &sel)
};
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![
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),
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),
]);
Span::styled("C-r", key), Span::styled(" undo/redo", dim),
])
};
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
}
}