idet-core 0.2.0

Shared text-editing library for a Micro-like terminal editor and a gedit-like egui GUI
Documentation
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
    }
}