Skip to content

Commit

Permalink
Add per view search location and total matches to statusline.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
useche committed Sep 14, 2024
1 parent 237cbe4 commit 2af66e5
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 33 deletions.
1 change: 1 addition & 0 deletions book/src/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `[<current>/<total>]` |
| `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 |
Expand Down
107 changes: 75 additions & 32 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(|m| Some(m)));
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
// match.
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> {
Expand Down
12 changes: 11 additions & 1 deletion helix-term/src/ui/statusline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -531,3 +532,12 @@ where
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);
}
}
21 changes: 21 additions & 0 deletions helix-view/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,21 @@ pub enum DocumentOpenError {
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(crate) id: DocumentId,
text: Rope,
selections: HashMap<ViewId, Selection>,
view_data: HashMap<ViewId, ViewData>,
/// Current search information.
last_search_match: HashMap<ViewId, SearchMatch>,

/// Inlay hints annotations for the document, by view.
///
Expand Down Expand Up @@ -661,6 +671,7 @@ impl Document {
text,
selections: HashMap::default(),
inlay_hints: HashMap::default(),
last_search_match: HashMap::default(),
inlay_hints_oudated: false,
view_data: Default::default(),
indent_style: DEFAULT_INDENT,
Expand Down Expand Up @@ -1215,6 +1226,8 @@ impl Document {

/// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
self.last_search_match.remove(&view_id);

// TODO: use a transaction?
self.selections
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
Expand All @@ -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
/// a single cursor would go if it were on the first grapheme. If
/// the text is empty, returns (0, 0).
Expand Down
3 changes: 3 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,9 @@ pub enum StatusLineElement {

/// Indicator for selected register
Register,

/// Search index and count
SearchPosition,
}

// Cursor shape is read and used on every rendered frame and so needs
Expand Down

0 comments on commit 2af66e5

Please sign in to comment.