neovide 0.16.1

Neovide: No Nonsense Neovim Gui
use std::collections::HashMap;

use crate::bridge::GridLineCell;

use super::{DrawCommandBatcher, window::Window};

const INTRO_HEADER_PREFIX: &str = "NVIM ";
const INTRO_FINAL_LINE: &str = "type  :help Kuwasha<Enter>    for information";
const SPONSOR_MESSAGE_LINES: &[&str] =
    &["", "Sponsor Neovide: https://github.com/sponsors/neovide"];

#[derive(Default)]
pub(crate) struct IntroMessageExtender {
    per_grid: HashMap<u64, IntroState>,
    sponsor_allowed: bool,
}

#[derive(Default)]
struct IntroState {
    saw_intro_header: bool,
    banner_start_row: Option<u64>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum IntroProcessing {
    Skip,
    Process,
    ClearBanner,
}

#[derive(Clone, Copy)]
enum IntroLineKind {
    Blank,
    Filler,
    HeaderCandidate,
    Other,
}

impl IntroState {
    fn mark_intro_possible(&mut self) {
        self.saw_intro_header = true;
    }

    fn remember_injection(&mut self, row: u64) {
        self.saw_intro_header = false;
        self.banner_start_row = Some(row);
    }

    fn has_visible_banner(&self) -> bool {
        self.banner_start_row.is_some()
    }

    fn take_banner_start_row(&mut self) -> Option<u64> {
        self.banner_start_row.take()
    }
}

impl IntroMessageExtender {
    pub(crate) fn new() -> Self {
        Self::default()
    }

    pub(crate) fn reset(&mut self, grid: u64) {
        self.per_grid.remove(&grid);
    }

    pub(crate) fn preprocess_line(&mut self, grid: u64, cells: &[GridLineCell]) -> IntroProcessing {
        let state = self.per_grid.entry(grid).or_default();

        if state.saw_intro_header {
            return IntroProcessing::Process;
        }

        if state.has_visible_banner() {
            return IntroProcessing::ClearBanner;
        }

        match classify_intro_line_start(cells) {
            IntroLineKind::Blank | IntroLineKind::Filler => IntroProcessing::Skip,
            IntroLineKind::HeaderCandidate => IntroProcessing::Process,
            IntroLineKind::Other => IntroProcessing::Skip,
        }
    }

    pub(crate) fn banner_injection_row(
        &mut self,
        grid: u64,
        row: u64,
        line_text: &str,
    ) -> Option<u64> {
        if !self.sponsor_allowed {
            return None;
        }

        let state = self.per_grid.entry(grid).or_default();

        if !state.saw_intro_header && !is_intro_header(line_text) {
            return None;
        } else if !state.saw_intro_header {
            state.mark_intro_possible();
        }

        if !is_neovim_intro_final_line_text(line_text) {
            return None;
        }

        if state.has_visible_banner() {
            return None;
        }

        let sponsor_start_row = row + 1;
        Some(sponsor_start_row)
    }

    pub(crate) fn inject_banner(
        &mut self,
        grid: u64,
        window: &mut Window,
        sponsor_start_row: u64,
        batcher: &mut DrawCommandBatcher,
    ) {
        for (offset, line) in SPONSOR_MESSAGE_LINES.iter().enumerate() {
            let target_row = sponsor_start_row + offset as u64;
            if target_row >= window.get_height() {
                break;
            }

            window.draw_centered_text_line(batcher, target_row as usize, line);
        }

        if let Some(state) = self.per_grid.get_mut(&grid) {
            state.remember_injection(sponsor_start_row);
        }
    }

    pub(crate) fn maybe_hide_banner(
        &mut self,
        grid: u64,
        windows: &mut HashMap<u64, Window>,
        batcher: &mut DrawCommandBatcher,
    ) {
        if let Some(state) = self.per_grid.get_mut(&grid) {
            if let Some(start_row) = state.take_banner_start_row() {
                if let Some(window) = windows.get_mut(&grid) {
                    clear_banner_rows(window, start_row, batcher);
                }
            }
        }
    }

    pub(crate) fn set_sponsor_allowed(
        &mut self,
        allowed: bool,
        windows: &mut HashMap<u64, Window>,
        batcher: &mut DrawCommandBatcher,
    ) {
        if self.sponsor_allowed == allowed {
            return;
        }

        self.sponsor_allowed = allowed;
        if !allowed {
            let grids: Vec<u64> = self.per_grid.keys().copied().collect();
            for grid in grids {
                self.maybe_hide_banner(grid, windows, batcher);
            }
        }
    }

    pub(crate) fn sponsor_allowed(&self) -> bool {
        self.sponsor_allowed
    }
}

fn is_intro_header(text: &str) -> bool {
    text.trim_start().starts_with(INTRO_HEADER_PREFIX)
}

fn is_neovim_intro_final_line_text(text: &str) -> bool {
    text.split_whitespace().eq(INTRO_FINAL_LINE.split_whitespace())
}

fn classify_intro_line_start(cells: &[GridLineCell]) -> IntroLineKind {
    match first_non_whitespace_char(cells) {
        None => IntroLineKind::Blank,
        Some('~') => IntroLineKind::Filler,
        Some('N') => IntroLineKind::HeaderCandidate,
        _ => IntroLineKind::Other,
    }
}

fn first_non_whitespace_char(cells: &[GridLineCell]) -> Option<char> {
    for cell in cells {
        let repeat = cell.repeat.unwrap_or(1);
        for _ in 0..repeat {
            for ch in cell.text.chars() {
                if ch.is_whitespace() {
                    continue;
                }
                return Some(ch);
            }
        }
    }
    None
}

fn clear_banner_rows(window: &mut Window, start_row: u64, batcher: &mut DrawCommandBatcher) {
    for offset in 0..SPONSOR_MESSAGE_LINES.len() {
        let row = start_row + offset as u64;
        if row >= window.get_height() {
            break;
        }
        window.draw_centered_text_line(batcher, row as usize, "");
    }
}