use egui::{Align2, Context, Key, Modifiers, TextEdit as EguiTextEdit, Window, vec2};
pub(super) fn find_matches(text: &str, term: &str) -> Vec<usize> {
if term.is_empty() {
return Vec::new();
}
let chars: Vec<char> = text.chars().collect();
let term_chars: Vec<char> = term.chars().collect();
if term_chars.len() > chars.len() {
return Vec::new();
}
(0..=chars.len() - term_chars.len())
.filter(|&index| chars[index..index + term_chars.len()] == term_chars[..])
.collect()
}
#[derive(Default)]
pub(super) struct Search {
pub(super) open: bool,
pub(super) term: String,
pub(super) last_term: Option<String>,
}
impl Search {
pub(super) fn open(&mut self, initial: String) {
self.term = initial;
self.open = true;
}
pub(super) fn jump_to_match(
&self,
text: &str,
cursor: usize,
anchor: usize,
forward: bool,
) -> Option<(usize, usize)> {
let term = self.last_term.as_ref()?;
let matches = find_matches(text, term);
if matches.is_empty() {
return None;
}
let length = term.chars().count();
let next = if forward {
matches
.iter()
.find(|&&position| position > cursor)
.or_else(|| matches.first())
} else {
let start = anchor.min(cursor);
matches
.iter()
.rev()
.find(|&&position| position < start)
.or_else(|| matches.last())
}?;
Some((next + length, *next))
}
#[expect(clippy::useless_let_if_seq)]
pub(super) fn draw(
&mut self,
ctx: &Context,
text: &str,
cursor: usize,
anchor: usize,
id_source: impl std::hash::Hash + std::fmt::Debug,
) -> Option<(usize, usize)> {
if !self.open {
return None;
}
if ctx.input_mut(|input| input.consume_key(Modifiers::NONE, Key::Escape)) {
self.open = false;
return None;
}
let forward = ctx.input_mut(|input| input.consume_key(Modifiers::NONE, Key::Tab));
let backward = ctx.input_mut(|input| input.consume_key(Modifiers::SHIFT, Key::Tab));
let confirm = ctx.input_mut(|input| input.consume_key(Modifiers::NONE, Key::Enter));
Window::new("Find")
.id(egui::Id::new("idet-core-search").with(id_source))
.collapsible(false)
.resizable(false)
.anchor(Align2::CENTER_TOP, vec2(0.0, 40.0))
.show(ctx, |ui| {
let response =
ui.add(EguiTextEdit::singleline(&mut self.term).desired_width(240.0));
if response.changed() {
self.last_term = Some(self.term.clone());
}
if !response.has_focus() && !response.lost_focus() {
response.request_focus();
}
let matches = find_matches(text, &self.term);
if !self.term.is_empty() {
ui.label(format!("{} match(es)", matches.len()));
}
});
let mut result = None;
if forward || backward {
self.last_term = Some(self.term.clone());
result = self.jump_to_match(text, cursor, anchor, forward);
}
if confirm {
self.last_term = Some(self.term.clone());
result = self.jump_to_match(text, cursor, anchor, true);
self.open = false;
}
result
}
}