accepted 0.3.2

A text editor to be ACCEPTED.
Documentation
use super::Mode;
use super::Transition;
use crate::buffer::Buffer;
use crate::core::CoreBuffer;
use crate::draw;
use fuzzy_matcher::clangd::fuzzy_indices;
use rayon::prelude::*;
use std::cmp::Ordering;
use std::cmp::Reverse;
use std::collections::{BTreeSet, HashSet};
use std::io::{BufRead, BufReader};
use std::path;
use std::process;
use termion::event::{Event, Key};

use async_trait::async_trait;

#[derive(Eq)]
struct MatchedItem {
    score: i64,
    index: usize,
    line: String,
    match_indices: HashSet<usize>,
}

impl MatchedItem {
    fn cmp_key(&self) -> (Reverse<i64>, usize) {
        (Reverse(self.score), self.index)
    }
}

impl PartialEq for MatchedItem {
    fn eq(&self, other: &Self) -> bool {
        self.cmp_key().eq(&other.cmp_key())
    }
}

impl PartialOrd for MatchedItem {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.cmp_key().partial_cmp(&other.cmp_key())
    }
}

impl Ord for MatchedItem {
    fn cmp(&self, other: &Self) -> Ordering {
        self.cmp_key().cmp(&other.cmp_key())
    }
}

pub struct FuzzyOpen {
    receiver: tokio::sync::mpsc::UnboundedReceiver<String>,
    finds: Vec<String>,
    line_buf: Vec<char>,

    index: usize,
    result: BTreeSet<MatchedItem>,
}

fn fuzzy_match(line: &str, query: &str) -> Option<(i64, HashSet<usize>)> {
    let mut maxi = std::i64::MIN;
    let mut set = HashSet::new();

    for q in query.split_whitespace() {
        if let Some((score, idxs)) = fuzzy_indices(line, q) {
            maxi = std::cmp::max(maxi, score);
            set.extend(idxs.into_iter());
        } else {
            return None;
        }
    }

    Some((maxi, set))
}

impl Default for FuzzyOpen {
    fn default() -> Self {
        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();

        tokio::spawn(async move {
            let mut child = process::Command::new("find")
                .arg(".")
                .stdout(process::Stdio::piped())
                .stderr(process::Stdio::piped())
                .spawn()?;
            if let Some(stdout) = child.stdout.take() {
                let reader = BufReader::new(stdout);
                for line in reader.lines() {
                    if let Ok(line) = line {
                        if tx.send(line.trim_end().to_string()).is_err() {
                            child.kill()?;
                            break;
                        }
                    }
                }
            }
            Ok::<(), anyhow::Error>(())
        });

        Self {
            receiver: rx,
            finds: Vec::new(),
            line_buf: Vec::new(),

            index: 0,
            result: Default::default(),
        }
    }
}

impl FuzzyOpen {
    fn update(&mut self) {
        if self.line_buf.is_empty() {
            self.result = self
                .finds
                .iter()
                .enumerate()
                .map(|(i, s)| MatchedItem {
                    score: 0,
                    index: i,
                    line: s.clone(),
                    match_indices: Default::default(),
                })
                .collect();
        } else {
            let query: String = self.line_buf.iter().collect();

            self.result = self
                .finds
                .par_iter()
                .enumerate()
                .filter_map(|(i, s)| {
                    fuzzy_match(s.as_str(), query.as_str()).map(|(score, indices)| MatchedItem {
                        score,
                        index: i,
                        line: s.clone(),
                        match_indices: indices,
                    })
                })
                .collect();
        }
    }

    fn push_line(&mut self, line: String) {
        let query: String = self.line_buf.iter().collect();
        let index = self.finds.len();
        if let Some((score, matches)) = fuzzy_match(&line, query.as_str()) {
            self.result.insert(MatchedItem {
                score,
                index,
                match_indices: matches,
                line: line.clone(),
            });
        }
        self.finds.push(line);
    }
}

#[async_trait(?Send)]
impl<B: CoreBuffer + 'static> Mode<B> for FuzzyOpen {
    async fn event(
        &mut self,
        buf: &mut Buffer<'_, B>,
        event: termion::event::Event,
    ) -> Transition<B> {
        match event {
            Event::Key(Key::Char('\n')) => {
                if let Some(item) = self.result.iter().nth(self.index) {
                    buf.open(path::PathBuf::from(&item.line));
                }
                return super::Normal::default().into_transition();
            }
            Event::Key(Key::Char(c)) if !c.is_control() => {
                self.line_buf.push(c);
                self.update();
            }
            Event::Key(Key::Backspace) => {
                if self.line_buf.pop().is_some() {
                    self.update();
                }
            }
            Event::Key(Key::Esc) => {
                return super::Normal::default().into_transition();
            }
            Event::Key(Key::Up) => {
                if !self.result.is_empty() {
                    self.index = std::cmp::min(self.index + 1, self.result.len() - 1);
                }
            }
            Event::Key(Key::Down) => {
                if self.index > 0 {
                    self.index -= 1;
                }
            }
            _ => {}
        }
        Transition::Nothing
    }
    fn draw(&mut self, buf: &mut Buffer<B>, mut view: draw::TermView) -> draw::CursorState {
        while let Ok(line) = self.receiver.try_recv() {
            self.push_line(line);
        }

        let height = view.height();
        {
            let mut sub = view.view((0, 0), height - 1, view.width());
            let buf_view_len = if sub.height() > self.result.len() {
                sub.height() - self.result.len()
            } else {
                0
            };

            if buf_view_len > 0 {
                let view_buf = sub.view((0, 0), buf_view_len, sub.width());
                buf.draw(view_buf);
            }

            let mut result_view =
                sub.view((buf_view_len, 0), sub.height() - buf_view_len, sub.width());
            for (i, item) in self
                .result
                .iter()
                .take(result_view.height())
                .enumerate()
                .collect::<Vec<_>>()
                .into_iter()
                .rev()
            {
                for (j, c) in item.line.chars().enumerate() {
                    let mut style = if item.match_indices.contains(&j) {
                        draw::styles::HIGHLIGHT
                    } else {
                        draw::styles::DEFAULT
                    };

                    if i == self.index {
                        style.bg = draw::Color::Rgb {
                            r: 0x44,
                            g: 0x44,
                            b: 0x44,
                        };
                    }

                    result_view.put_inline(c, style, None);
                }
                result_view.newline();
            }
        }
        let mut query_view = view.view((view.height() - 1, 0), 1, view.width());
        query_view.puts(
            &format!("Fuzzy> {}", self.line_buf.iter().collect::<String>()),
            draw::styles::DEFAULT,
        );

        if query_view.is_out() {
            draw::CursorState::Hide
        } else {
            draw::CursorState::Show(query_view.cursor, draw::CursorShape::Bar)
        }
    }
}