leaf-markdown-viewer 1.28.0

Terminal Markdown previewer with a GUI-like experience
use super::history::{
    HistoryEntry, MSG_FILE_NO_LONGER_AVAILABLE, MSG_HISTORY_DISABLED, MSG_NO_FILE_HISTORY,
};
use super::App;
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};

#[derive(Default)]
pub(crate) struct HistoryPickerState {
    pub(crate) open: bool,
    pub(crate) entries: Vec<HistoryEntry>,
    pub(crate) filenames_lower: Vec<String>,
    pub(crate) filtered: Vec<usize>,
    pub(crate) match_positions: Vec<Vec<usize>>,
    pub(crate) query: String,
    pub(crate) index: usize,
    pub(crate) error: Option<String>,
    pub(crate) activation_error: Option<String>,
}

impl App {
    pub(crate) fn queue_history_picker(&mut self) {
        self.pending_picker = super::file_picker::PendingPicker::History;
    }

    pub(crate) fn is_history_picker_open(&self) -> bool {
        self.history_picker.open
    }

    pub(crate) fn is_any_picker_active(&self) -> bool {
        self.has_pending_picker()
            || self.is_picker_loading()
            || self.is_file_picker_open()
            || self.is_history_picker_open()
    }

    pub(crate) fn close_history_picker(&mut self) {
        self.history_picker = HistoryPickerState::default();
        if self.is_history_picker_loading() {
            self.cancel_picker_loading();
        }
        self.picker_width_floor_active = false;
    }

    pub(crate) fn install_loaded_history_picker(&mut self, mut entries: Vec<HistoryEntry>) {
        let capacity = self.history_capacity();
        entries.truncate(capacity);
        self.history_picker.filenames_lower = entries
            .iter()
            .map(|e| {
                e.path
                    .file_name()
                    .map(|n| n.to_string_lossy().to_lowercase())
                    .unwrap_or_default()
            })
            .collect();
        self.history_picker.entries = entries;
        self.history_picker.query.clear();
        self.history_picker.index = 0;
        self.history_picker.error = if capacity == 0 {
            Some(MSG_HISTORY_DISABLED.to_string())
        } else if self.history_picker.entries.is_empty() {
            Some(MSG_NO_FILE_HISTORY.to_string())
        } else {
            None
        };
        self.history_picker.open = true;
        self.refresh_history_picker_matches();
    }

    pub(crate) fn history_picker_entries(&self) -> &[HistoryEntry] {
        &self.history_picker.entries
    }

    pub(crate) fn history_picker_filtered_indices(&self) -> &[usize] {
        &self.history_picker.filtered
    }

    pub(crate) fn history_picker_match_positions(&self, filtered_idx: usize) -> &[usize] {
        self.history_picker
            .match_positions
            .get(filtered_idx)
            .map(Vec::as_slice)
            .unwrap_or(&[])
    }

    pub(crate) fn history_picker_index(&self) -> usize {
        self.history_picker.index
    }

    pub(crate) fn history_picker_query(&self) -> &str {
        &self.history_picker.query
    }

    pub(crate) fn history_picker_error(&self) -> Option<&str> {
        self.history_picker.error.as_deref()
    }

    pub(crate) fn history_picker_activation_error(&self) -> Option<&str> {
        self.history_picker.activation_error.as_deref()
    }

    pub(crate) fn move_history_picker_up(&mut self) {
        self.history_picker.activation_error = None;
        let total = self.history_picker.filtered.len();
        if total == 0 {
            return;
        }
        if self.history_picker.index == 0 {
            self.history_picker.index = total - 1;
        } else {
            self.history_picker.index -= 1;
        }
    }

    pub(crate) fn move_history_picker_down(&mut self) {
        self.history_picker.activation_error = None;
        let total = self.history_picker.filtered.len();
        if total == 0 {
            return;
        }
        self.history_picker.index = (self.history_picker.index + 1) % total;
    }

    pub(crate) fn push_history_picker_query(&mut self, ch: char) {
        self.history_picker.activation_error = None;
        self.history_picker.query.push(ch);
        self.refresh_history_picker_matches();
    }

    pub(crate) fn pop_history_picker_query(&mut self) {
        self.history_picker.activation_error = None;
        self.history_picker.query.pop();
        self.refresh_history_picker_matches();
    }

    pub(crate) fn clear_history_picker_query(&mut self) {
        self.history_picker.activation_error = None;
        self.history_picker.query.clear();
        self.refresh_history_picker_matches();
    }

    pub(crate) fn refresh_history_picker_matches(&mut self) {
        let effective_total = self.history_picker.entries.len();
        let query = self.history_picker.query.trim().to_lowercase();

        if query.is_empty() {
            self.history_picker.filtered = (0..effective_total).collect();
            self.history_picker.match_positions.clear();
        } else {
            let mut scored: Vec<(usize, _, Vec<usize>)> = (0..effective_total)
                .filter_map(|idx| {
                    let filename = &self.history_picker.filenames_lower[idx];
                    super::fuzzy::fuzzy_component_match(filename, &query)
                        .map(|(score, positions)| (idx, score, positions))
                })
                .collect();
            scored.sort_by(|(l_idx, l_score, _), (r_idx, r_score, _)| {
                l_score.cmp(r_score).then_with(|| l_idx.cmp(r_idx))
            });
            self.history_picker.filtered = scored.iter().map(|(idx, _, _)| *idx).collect();
            self.history_picker.match_positions = scored.into_iter().map(|(_, _, p)| p).collect();
        }

        if self.history_picker.filtered.is_empty()
            || self.history_picker.index >= self.history_picker.filtered.len()
        {
            self.history_picker.index = 0;
        }
    }

    fn history_capacity(&self) -> usize {
        self.file_history_length()
            .filter(|n| *n > 0)
            .map(|n| n as usize)
            .unwrap_or(0)
    }

    pub(crate) fn activate_history_picker_selection(
        &mut self,
        ss: &SyntaxSet,
        themes: &ThemeSet,
    ) -> bool {
        let Some(&idx) = self.history_picker.filtered.get(self.history_picker.index) else {
            return false;
        };
        let Some(entry) = self.history_picker.entries.get(idx).cloned() else {
            return false;
        };
        if self.load_path(entry.path.clone(), ss, themes) {
            self.history_pending_removals.retain(|p| p != &entry.path);
            super::history::remove_paths(std::mem::take(&mut self.history_pending_removals));
            self.close_history_picker();
            true
        } else {
            if !self.history_pending_removals.contains(&entry.path) {
                self.history_pending_removals.push(entry.path);
            }
            self.history_picker.activation_error = Some(MSG_FILE_NO_LONGER_AVAILABLE.to_string());
            false
        }
    }
}