From 6b8ef632ea78c6806425089090a1671bff99b18a Mon Sep 17 00:00:00 2001 From: Luis Useche Date: Mon, 9 Sep 2024 08:15:31 -0700 Subject: [PATCH] Add per view search location and total matches to statusline. This patch changes `search_impl` to calculate the index of the current match and the total matches in the search. It also adds a new option in the status line to show this information. --- book/src/editor.md | 1 + helix-term/src/commands.rs | 107 ++++++++++++++++++++++---------- helix-term/src/ui/statusline.rs | 12 +++- helix-view/src/document.rs | 21 +++++++ helix-view/src/editor.rs | 3 + 5 files changed, 111 insertions(+), 33 deletions(-) diff --git a/book/src/editor.md b/book/src/editor.md index 82d5f8461..fbd141b42 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -103,6 +103,7 @@ The following statusline elements can be configured: | `diagnostics` | The number of warnings and/or errors | | `workspace-diagnostics` | The number of warnings and/or errors on workspace | | `selections` | The number of active selections | +| `search-position` | Current search match and total matches in the view `[/]` | | `primary-selection-length` | The number of characters currently in primary selection | | `position` | The cursor position | | `position-percentage` | The cursor position as a percentage of the total number of lines | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b1c29378d..e45d7a928 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -39,7 +39,7 @@ use helix_core::{ RopeReader, RopeSlice, Selection, SmallVec, Syntax, Tendril, Transaction, }; use helix_view::{ - document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, + document::{FormatterError, Mode, SearchMatch, SCRATCH_BUFFER_NAME}, editor::Action, info::Info, input::KeyEvent, @@ -2097,54 +2097,97 @@ fn search_impl( // it out, we need to add it back to the position of the selection. let doc = doc!(editor).text().slice(..); - // use find_at to find the next match after the cursor, loop around the end + let mut all_matches = regex.find_iter(doc.regex_input()).enumerate().peekable(); + + if all_matches.peek().is_none() { + if show_warnings { + editor.set_error("No matches"); + } + return; + } + + // We will get the number of the current match and total matches from + // `all_matches`. So, here we look in the iterator until we find exactly + // the match we were looking for (either after or behind `start`). In the + // `Backward` case, in particular, we need to look the match ahead to know + // if this is the one we need. + // Careful, `Regex` uses `bytes` as offsets, not character indices! let mut mat = match direction { - Direction::Forward => regex.find(doc.regex_input_at_bytes(start..)), - Direction::Backward => regex.find_iter(doc.regex_input_at_bytes(..start)).last(), + Direction::Forward => all_matches.by_ref().find(|&(_, m)| m.start() >= start), + Direction::Backward => { + let one_behind = std::iter::once(None).chain(all_matches.by_ref().map(Some)); + one_behind + .zip(regex.find_iter(doc.regex_input())) + .find(|&(_, m1)| m1.start() >= start) + .map(|(m0, _)| m0) + .unwrap_or(None) + } }; - if mat.is_none() { - if wrap_around { - mat = match direction { - Direction::Forward => regex.find(doc.regex_input()), - Direction::Backward => regex.find_iter(doc.regex_input_at_bytes(start..)).last(), - }; + if mat.is_none() && !wrap_around { + if show_warnings { + editor.set_error("No more matches"); } + return; + } + + // If we didn't find a match before, lets wrap the search. + if mat.is_none() { if show_warnings { - if wrap_around && mat.is_some() { - editor.set_status("Wrapped around document"); - } else { - editor.set_error("No more matches"); - } + editor.set_status("Wrapped around document"); } + + let doc = doc!(editor).text().slice(..); + all_matches = regex.find_iter(doc.regex_input()).enumerate().peekable(); + mat = match direction { + Direction::Forward => all_matches.by_ref().next(), + Direction::Backward => all_matches.by_ref().last(), + }; } + let (idx, mat) = mat.unwrap(); + let last_idx = match all_matches.last() { + None => idx, + Some((last_idx, _)) => last_idx, + }; + + // Move the cursor to the match. let (view, doc) = current!(editor); let text = doc.text().slice(..); let selection = doc.selection(view.id); - if let Some(mat) = mat { - let start = text.byte_to_char(mat.start()); - let end = text.byte_to_char(mat.end()); + let start = text.byte_to_char(mat.start()); + let end = text.byte_to_char(mat.end()); - if end == 0 { - // skip empty matches that don't make sense - return; - } - - // Determine range direction based on the primary range - let primary = selection.primary(); - let range = Range::new(start, end).with_direction(primary.direction()); + if end == 0 { + // skip empty matches that don't make sense + return; + } - let selection = match movement { - Movement::Extend => selection.clone().push(range), - Movement::Move => selection.clone().replace(selection.primary_index(), range), - }; + // Determine range direction based on the primary range + let primary = selection.primary(); + let range = Range::new(start, end).with_direction(primary.direction()); - doc.set_selection(view.id, selection); - view.ensure_cursor_in_view_center(doc, scrolloff); + let selection = match movement { + Movement::Extend => selection.clone().push(range), + Movement::Move => selection.clone().replace(selection.primary_index(), range), }; + + doc.set_selection(view.id, selection); + view.ensure_cursor_in_view_center(doc, scrolloff); + + // Set the index of this match and total number of matchs in the doc. It's + // important to set it after `set_selection` since that method resets the + // last match position. + let (view, doc) = current!(editor); + doc.set_last_search_match( + view.id, + SearchMatch { + idx: idx + 1, + count: last_idx + 1, + }, + ); } fn search_completions(cx: &mut Context, reg: Option) -> Vec { diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 7437cbd07..2766b2c22 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -2,7 +2,7 @@ use helix_core::{coords_at_pos, encoding, Position}; use helix_lsp::lsp::DiagnosticSeverity; use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::{ - document::{Mode, SCRATCH_BUFFER_NAME}, + document::{Mode, SearchMatch, SCRATCH_BUFFER_NAME}, graphics::Rect, theme::Style, Document, Editor, View, @@ -163,6 +163,7 @@ where helix_view::editor::StatusLineElement::Spacer => render_spacer, helix_view::editor::StatusLineElement::VersionControl => render_version_control, helix_view::editor::StatusLineElement::Register => render_register, + helix_view::editor::StatusLineElement::SearchPosition => render_search_position, } } @@ -531,3 +532,12 @@ where write(context, format!(" reg={} ", reg), None) } } + +fn render_search_position(context: &mut RenderContext, write: F) +where + F: Fn(&mut RenderContext, String, Option