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.
pull/11700/head
Luis Useche 3 months ago
parent f6d39cbc1d
commit 6b8ef632ea

@ -103,6 +103,7 @@ The following statusline elements can be configured:
| `diagnostics` | The number of warnings and/or errors | | `diagnostics` | The number of warnings and/or errors |
| `workspace-diagnostics` | The number of warnings and/or errors on workspace | | `workspace-diagnostics` | The number of warnings and/or errors on workspace |
| `selections` | The number of active selections | | `selections` | The number of active selections |
| `search-position` | Current search match and total matches in the view `[<current>/<total>]` |
| `primary-selection-length` | The number of characters currently in primary selection | | `primary-selection-length` | The number of characters currently in primary selection |
| `position` | The cursor position | | `position` | The cursor position |
| `position-percentage` | The cursor position as a percentage of the total number of lines | | `position-percentage` | The cursor position as a percentage of the total number of lines |

@ -39,7 +39,7 @@ use helix_core::{
RopeReader, RopeSlice, Selection, SmallVec, Syntax, Tendril, Transaction, RopeReader, RopeSlice, Selection, SmallVec, Syntax, Tendril, Transaction,
}; };
use helix_view::{ use helix_view::{
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, document::{FormatterError, Mode, SearchMatch, SCRATCH_BUFFER_NAME},
editor::Action, editor::Action,
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
@ -2097,34 +2097,66 @@ fn search_impl(
// it out, we need to add it back to the position of the selection. // it out, we need to add it back to the position of the selection.
let doc = doc!(editor).text().slice(..); 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! // Careful, `Regex` uses `bytes` as offsets, not character indices!
let mut mat = match direction { let mut mat = match direction {
Direction::Forward => regex.find(doc.regex_input_at_bytes(start..)), Direction::Forward => all_matches.by_ref().find(|&(_, m)| m.start() >= start),
Direction::Backward => regex.find_iter(doc.regex_input_at_bytes(..start)).last(), 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 mat.is_none() && !wrap_around {
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 show_warnings { if show_warnings {
if wrap_around && mat.is_some() {
editor.set_status("Wrapped around document");
} else {
editor.set_error("No more matches"); 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 {
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 (view, doc) = current!(editor);
let text = doc.text().slice(..); let text = doc.text().slice(..);
let selection = doc.selection(view.id); let selection = doc.selection(view.id);
if let Some(mat) = mat {
let start = text.byte_to_char(mat.start()); let start = text.byte_to_char(mat.start());
let end = text.byte_to_char(mat.end()); let end = text.byte_to_char(mat.end());
@ -2144,7 +2176,18 @@ fn search_impl(
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
view.ensure_cursor_in_view_center(doc, scrolloff); 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<char>) -> Vec<String> { fn search_completions(cx: &mut Context, reg: Option<char>) -> Vec<String> {

@ -2,7 +2,7 @@ use helix_core::{coords_at_pos, encoding, Position};
use helix_lsp::lsp::DiagnosticSeverity; use helix_lsp::lsp::DiagnosticSeverity;
use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::{ use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME}, document::{Mode, SearchMatch, SCRATCH_BUFFER_NAME},
graphics::Rect, graphics::Rect,
theme::Style, theme::Style,
Document, Editor, View, Document, Editor, View,
@ -163,6 +163,7 @@ where
helix_view::editor::StatusLineElement::Spacer => render_spacer, helix_view::editor::StatusLineElement::Spacer => render_spacer,
helix_view::editor::StatusLineElement::VersionControl => render_version_control, helix_view::editor::StatusLineElement::VersionControl => render_version_control,
helix_view::editor::StatusLineElement::Register => render_register, 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) write(context, format!(" reg={} ", reg), None)
} }
} }
fn render_search_position<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
if let Some(SearchMatch { idx, count }) = context.doc.get_last_search_match(context.view.id) {
write(context, format!(" [{}/{}] ", idx, count), None);
}
}

@ -130,11 +130,21 @@ pub enum DocumentOpenError {
IoError(#[from] io::Error), IoError(#[from] io::Error),
} }
#[derive(Debug, Clone, Copy)]
pub struct SearchMatch {
/// nth match from the beginning of the document.
pub idx: usize,
/// Total number of matches in the document.
pub count: usize,
}
pub struct Document { pub struct Document {
pub(crate) id: DocumentId, pub(crate) id: DocumentId,
text: Rope, text: Rope,
selections: HashMap<ViewId, Selection>, selections: HashMap<ViewId, Selection>,
view_data: HashMap<ViewId, ViewData>, view_data: HashMap<ViewId, ViewData>,
/// Current search information.
last_search_match: HashMap<ViewId, SearchMatch>,
/// Inlay hints annotations for the document, by view. /// Inlay hints annotations for the document, by view.
/// ///
@ -661,6 +671,7 @@ impl Document {
text, text,
selections: HashMap::default(), selections: HashMap::default(),
inlay_hints: HashMap::default(), inlay_hints: HashMap::default(),
last_search_match: HashMap::default(),
inlay_hints_oudated: false, inlay_hints_oudated: false,
view_data: Default::default(), view_data: Default::default(),
indent_style: DEFAULT_INDENT, indent_style: DEFAULT_INDENT,
@ -1215,6 +1226,8 @@ impl Document {
/// Select text within the [`Document`]. /// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) { pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
self.last_search_match.remove(&view_id);
// TODO: use a transaction? // TODO: use a transaction?
self.selections self.selections
.insert(view_id, selection.ensure_invariants(self.text().slice(..))); .insert(view_id, selection.ensure_invariants(self.text().slice(..)));
@ -1224,6 +1237,14 @@ impl Document {
}) })
} }
pub fn set_last_search_match(&mut self, view_id: ViewId, search_match: SearchMatch) {
self.last_search_match.insert(view_id, search_match);
}
pub fn get_last_search_match(&self, view_id: ViewId) -> Option<SearchMatch> {
self.last_search_match.get(&view_id).copied()
}
/// Find the origin selection of the text in a document, i.e. where /// Find the origin selection of the text in a document, i.e. where
/// a single cursor would go if it were on the first grapheme. If /// a single cursor would go if it were on the first grapheme. If
/// the text is empty, returns (0, 0). /// the text is empty, returns (0, 0).

@ -580,6 +580,9 @@ pub enum StatusLineElement {
/// Indicator for selected register /// Indicator for selected register
Register, Register,
/// Search index and count
SearchPosition,
} }
// Cursor shape is read and used on every rendered frame and so needs // Cursor shape is read and used on every rendered frame and so needs

Loading…
Cancel
Save