idet-core 0.4.0

Editing logic for text editors, without a frontend
Documentation
use egui::{Area, Context, Frame, Id, Label, Order, RichText, TextWrapMode};

use crate::completion;

#[derive(Default)]
pub(super) struct Suggest {
    pub(super) open: bool,
    pub(super) matches: Vec<String>,
    pub(super) selected: usize,
    pub(super) suppressed: bool,
}

impl Suggest {
    pub(super) fn accept(&mut self, text: &mut String, cursor: usize) -> Option<(usize, usize)> {
        let (start, end) = completion::current_word(text, cursor);
        let matched = self.matches.get(self.selected)?.clone();
        let start_byte = completion::char_to_byte(text, start);
        let end_byte = completion::char_to_byte(text, end);
        text.replace_range(start_byte..end_byte, &matched);
        self.open = false;
        let caret = start + matched.chars().count();
        Some((caret, caret))
    }

    pub(super) fn refresh(&mut self, text: &str, cursor: usize, words: &[String]) {
        if self.suppressed {
            self.open = false;
            return;
        }
        let (start, end) = completion::current_word(text, cursor);
        if start == end {
            self.open = false;
            return;
        }
        let word: String = text.chars().skip(start).take(end - start).collect();
        let found: Vec<String> = words
            .iter()
            .filter(|candidate| candidate.starts_with(&word))
            .cloned()
            .collect();
        if found.iter().all(|candidate| *candidate == word) {
            self.open = false;
            return;
        }
        if !self.open {
            self.selected = 0;
        }
        self.selected = self.selected.min(found.len() - 1);
        self.matches = found;
        self.open = true;
    }

    pub(super) const fn visible(&self) -> bool {
        self.open && !self.matches.is_empty()
    }

    pub(super) fn draw(
        &self,
        ctx: &Context,
        galley: &egui::Galley,
        galley_pos: egui::Pos2,
        cursor: usize,
        id_source: impl std::hash::Hash + std::fmt::Debug,
    ) {
        let cursor_rect = galley.pos_from_cursor(egui::text::CCursor::new(cursor));
        let popup_pos = galley_pos + cursor_rect.left_bottom().to_vec2();
        Area::new(Id::new("idet-core-suggest").with(id_source))
            .order(Order::Foreground)
            .fixed_pos(popup_pos)
            .show(ctx, |ui| {
                Frame::popup(ui.style()).show(ui, |ui| {
                    for (index, candidate) in self.matches.iter().enumerate() {
                        let mut label = RichText::new(candidate.as_str()).monospace();
                        if index == self.selected {
                            label = label
                                .strong()
                                .background_color(ui.visuals().selection.bg_fill);
                        }
                        ui.add(Label::new(label).wrap_mode(TextWrapMode::Extend));
                    }
                });
            });
    }
}