Skip to content

Commit

Permalink
Highlight all search matches in document
Browse files Browse the repository at this point in the history
References:
#4798
  • Loading branch information
wes-adams committed Mar 3, 2023
1 parent 0a7c697 commit a3a435d
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 4 deletions.
11 changes: 11 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1719,6 +1719,11 @@ fn search_impl(
let text = doc.text().slice(..);
let selection = doc.selection(view.id);

// Update search_matches, but only if search criteria has changed since
// last call to search_impl. If 'n' is being used to traverse matches,
// update will be skipped.
view.update_search_matches(contents, regex);

// Get the right side of the primary block cursor for forward search, or the
// grapheme before the start of the selection for reverse search.
let start = match direction {
Expand Down Expand Up @@ -2324,6 +2329,12 @@ fn collapse_selection(cx: &mut Context) {
Range::new(pos, pos)
});
doc.set_selection(view.id, selection);

// clear highlights on search matches
// but set search_matches.old to "" so that if 'n' is pressed
// to continue to traverse matches, highlights resume
view.search_matches.clear();
view.search_matches.old = Regex::new("").unwrap();
}

fn flip_selections(cx: &mut Context) {
Expand Down
18 changes: 18 additions & 0 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ impl EditorView {
highlights = Box::new(syntax::merge(highlights, overlay_highlights));
}

if !view.search_matches.matches.is_empty() {
highlights = match view.get_search_matches(theme) {
Some(match_highlights) => Box::new(syntax::merge(highlights, match_highlights)),
None => highlights,
};
}

for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
// Most of the `diagnostic` Vecs are empty most of the time. Skipping
// a merge for any empty Vec saves a significant amount of work.
Expand Down Expand Up @@ -1207,6 +1214,9 @@ impl Component for EditorView {
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff);

// document has been alterred, clear search_matches
view.search_matches.clear();

// Store a history state if not in insert mode. Otherwise wait till we exit insert
// to include any edits to the paste in the history state.
if mode != Mode::Insert {
Expand Down Expand Up @@ -1356,6 +1366,14 @@ impl Component for EditorView {
Self::render_bufferline(cx.editor, area.with_height(1), surface);
}

{
let (view, doc) = current!(cx.editor);
if !doc.changes().is_empty() || doc.has_changed() {
// document has been alterred, clear search_matches
view.search_matches.clear();
}
}

for (view, is_focused) in cx.editor.tree.views() {
let doc = cx.editor.document(view.doc).unwrap();
self.render_view(cx.editor, doc, view, area, surface, is_focused);
Expand Down
12 changes: 12 additions & 0 deletions helix-view/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -979,12 +979,24 @@ impl Document {
self.earlier_later_impl(view, uk, false)
}

pub fn has_changed(&self) -> bool {
if self.changes.is_empty() {
return false;
}

let new_changeset = ChangeSet::new(self.text());
self.changes != new_changeset
}

/// Commit pending changes to history
pub fn append_changes_to_history(&mut self, view: &mut View) {
if self.changes.is_empty() {
return;
}

// doc has changed, so clear search_matches highlights
view.search_matches.clear();

let new_changeset = ChangeSet::new(self.text());
let changes = std::mem::replace(&mut self.changes, new_changeset);
// Instead of doing this messy merge we could always commit, and based on transaction
Expand Down
76 changes: 72 additions & 4 deletions helix-view/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ use crate::{
graphics::Rect,
Align, Document, DocumentId, Theme, ViewId,
};

use helix_core::{
char_idx_at_visual_offset, doc_formatter::TextFormat, text_annotations::TextAnnotations,
visual_offset_from_anchor, visual_offset_from_block, Position, RopeSlice, Selection,
Transaction,
char_idx_at_visual_offset, doc_formatter::TextFormat, regex::Regex,
text_annotations::TextAnnotations, visual_offset_from_anchor, visual_offset_from_block,
Position, RopeSlice, Selection, Transaction,
};

use std::{
Expand Down Expand Up @@ -102,6 +101,28 @@ pub struct ViewPosition {
pub vertical_offset: usize,
}

#[derive(Clone)]
pub struct SearchMatches {
pub matches: Vec<(usize, usize)>,
pub new: Regex,
pub old: Regex,
}

impl SearchMatches {
pub fn clear(&mut self) {
self.matches.clear();
}

pub fn has_changed(&self, regex: &Regex) -> bool {
regex.as_str() != self.old.as_str()
}

pub fn update(&mut self, regex: &Regex) {
self.old = self.new.clone();
self.new = regex.clone();
}
}

#[derive(Clone)]
pub struct View {
pub id: ViewId,
Expand All @@ -125,6 +146,7 @@ pub struct View {
/// mapping keeps track of the last applied history revision so that only new changes
/// are applied.
doc_revisions: HashMap<DocumentId, usize>,
pub search_matches: SearchMatches,
}

impl fmt::Debug for View {
Expand Down Expand Up @@ -154,6 +176,11 @@ impl View {
object_selections: Vec::new(),
gutters,
doc_revisions: HashMap::new(),
search_matches: SearchMatches {
matches: Vec::new(),
new: Regex::new("").unwrap(),
old: Regex::new("").unwrap(),
},
}
}

Expand Down Expand Up @@ -576,6 +603,47 @@ impl View {
self.apply(&transaction, doc);
}
}

pub fn get_backup_theme(&self, theme: &Theme) -> usize {
theme
.find_scope_index("ui.selection")
.expect("could not find `ui.selection` scope in the theme!")
}

pub fn get_search_matches(
&self,
theme: &Theme,
) -> Option<Vec<(usize, std::ops::Range<usize>)>> {
// let selection_scope = match theme.find_scope_index("ui.selection.primary") {
let selection_scope = match theme.find_scope_index("ui.cursor.match") {
Some(theme) => theme,
None => self.get_backup_theme(theme),
};

let mut ret = Vec::new();
for (start, end) in &self.search_matches.matches {
ret.push((selection_scope, *start..*end));
}

if ret.is_empty() {
None
} else {
Some(ret)
}
}

pub fn update_search_matches(&mut self, contents: &str, regex: &Regex) {
// this is expensive. only update search matches if search criteria has changed
if self.search_matches.has_changed(regex) {
self.search_matches.update(regex);
self.search_matches.clear();
for one_match in self.search_matches.new.find_iter(contents) {
self.search_matches
.matches
.push((one_match.start(), one_match.end()));
}
}
}
}

#[cfg(test)]
Expand Down

0 comments on commit a3a435d

Please sign in to comment.