bernardo-tui 0.2.7

A keyboard-only, distraction-free TUI widget library
Documentation
use std::collections::HashMap;
use std::fmt::Debug;
use std::ops::Range;
use std::sync::{Arc, RwLock};

use log::{debug, error, warn};
use ropey::Rope;
use streaming_iterator::StreamingIterator;
use tree_sitter::{InputEdit, Language, Parser, Point, Query, QueryCursor};

use crate::tsw::lang_id::LangId;
use crate::tsw::parsing_tuple::ParsingTuple;
use crate::tsw::rope_wrappers::RopeWrapper;
use crate::unpack_or_e;

static EMPTY_SLICE: [u8; 0] = [0; 0];

pub fn byte_offset_to_point(rope: &Rope, byte_offset: usize) -> Option<Point> {
    let char_idx = rope.try_byte_to_char(byte_offset).ok()?;
    let line_idx = rope.try_char_to_line(char_idx).ok()?;
    let line_begin_char_idx = rope.try_line_to_char(line_idx).ok()?;

    // some paranoia
    if char_idx < line_begin_char_idx {
        None
    } else {
        let column_idx = char_idx - line_begin_char_idx;
        Some(Point::new(line_idx, column_idx))
    }
}

pub fn pack_rope_with_callback<'a>(rope: &'a Rope) -> Box<dyn FnMut(usize, Point) -> &'a [u8] + 'a> {
    return Box::new(move |offset: usize, point: Point| {
        debug!("request to parse point {:?}, offset {:?}", point, offset);

        if offset >= rope.len_bytes() {
            // debug!("byte offset beyond rope length: {} >= {}", offset, rope.len_bytes());
            return &EMPTY_SLICE;
        }

        let point_from_offset = match byte_offset_to_point(rope, offset) {
            Some(point) => point,
            None => return &EMPTY_SLICE,
        };
        if point != point_from_offset {
            error!(
                "byte offset diverted from point. Point is {},{} and offset {}:{},{}",
                point.column, point.row, offset, point_from_offset.column, point_from_offset.row
            );
        }
        // end of sanity check

        //(chunk, chunk_byte_idx, chunk_char_idx, chunk_line_idx).
        let (chunk, chunk_byte_idx, _, _) = rope.chunk_at_byte(offset);
        let chunk_as_bytes = chunk.as_bytes();
        // now chunk most probably begins BEFORE our offset. We need to offset for the offset.

        debug_assert!(offset >= chunk_byte_idx); //TODO add some non-panicking failsafe.
        let cut_from_beginning = offset - chunk_byte_idx;
        let result = &chunk_as_bytes[cut_from_beginning..];

        debug!("parser reading bytes [{}-{}) |{}|", offset, offset + result.len(), result.len());

        result
    });
}

/// All data needed to parse and highlight a single language.
/// Constructed in gladius (which owns the compiled C grammars) and passed to
/// `TreeSitterWrapper::new`.
#[derive(Debug)]
pub struct TreeSitterTuple {
    pub lang_id: LangId,
    pub language: Language,
    pub highlighter_query: String,
    /// Optional indent query (currently only populated for Rust).
    pub indent_query: Option<String>,
}

#[derive(Debug)]
pub struct TreeSitterWrapper {
    languages: HashMap<LangId, TreeSitterTuple>,
}

impl TreeSitterWrapper {
    /// Build the wrapper from a pre-constructed language map.
    /// The map is built in gladius (the crate that links the C grammars).
    pub fn new(languages: HashMap<LangId, TreeSitterTuple>) -> TreeSitterWrapper {
        TreeSitterWrapper { languages }
    }

    pub fn indent_query(&self, lang_id: LangId) -> Option<&str> {
        self.languages.get(&lang_id)?.indent_query.as_deref()
    }

    // This should be called on loading a file. On update, ParserAndTree struct should be used.
    pub fn new_parse(&self, lang_id: LangId) -> Option<ParsingTuple> {
        let tuple = self.languages.get(&lang_id)?;
        let mut parser = Parser::new();
        match parser.set_language(&tuple.language) {
            Ok(_) => {}
            Err(e) => {
                error!("failed setting language: {}", e);
                return None;
            }
        };

        let highlight_query = match Query::new(&tuple.language, &tuple.highlighter_query) {
            Ok(q) => q,
            Err(e) => {
                error!("failed compiling highlight query: {}", e);
                return None;
            }
        };

        let indent_query = match self.indent_query(lang_id) {
            None => None,
            Some(query_string) => match Query::new(&tuple.language, query_string) {
                Ok(q) => Some(q),
                Err(e) => {
                    error!("failed compiling indent query: {}", e);
                    None
                }
            },
        };

        let id_to_name: Vec<Arc<String>> = highlight_query.capture_names().iter().map(|cn| Arc::new(cn.to_string())).collect();

        Some(ParsingTuple {
            tree: None,
            lang_id,
            parser: Arc::new(RwLock::new(parser)),
            language: tuple.language.clone(),
            highlight_query: Arc::new(highlight_query),
            indent_query: indent_query.map(Arc::new),
            id_to_name: Arc::new(id_to_name),
        })
    }
}

#[derive(Clone, Debug)]
pub struct HighlightItem {
    pub char_begin: usize,
    pub char_end: usize,
    pub identifier: Arc<String>,
}

impl ParsingTuple {
    // TODO I would prefer it to be an iterator, but I have no time to fix it.
    pub fn highlight_iter<'a>(&'a self, rope: &'a ropey::Rope, char_range_op: Option<Range<usize>>) -> Option<Vec<HighlightItem>> {
        if self.tree.is_none() {
            return None;
        }

        let mut cursor = QueryCursor::new();

        if let Some(char_range) = char_range_op {
            let begin_byte = rope.try_char_to_byte(char_range.start).ok()?;
            let end_byte = rope.try_char_to_byte(char_range.end).ok()?;

            cursor.set_byte_range(begin_byte..end_byte);
        };

        // let _query_captures: Vec<_> = cursor
        //     .captures(&self.highlight_query, self.tree.as_ref()?.root_node(), RopeWrapper(&rope));
        //

        let mut query_matches = cursor.matches(&self.highlight_query, self.tree.as_ref()?.root_node(), RopeWrapper(&rope));

        let mut results: Vec<HighlightItem> = vec![];

        while let Some(m) = query_matches.next() {
            if m.captures.len() != 1 {
                warn!("unexpected number of captures (expected 1, got {})", m.captures.len());
            }

            for c in m.captures {
                let begin_char = rope.try_byte_to_char(c.node.start_byte()).ok()?;
                let end_char = rope.try_byte_to_char(c.node.end_byte()).ok()?;

                let name = self.id_to_name.get(c.index as usize)?;

                results.push(HighlightItem {
                    char_begin: begin_char,
                    char_end: end_char,
                    identifier: name.clone(),
                })
            }
        }

        debug!("highlight result size = {}", results.len());

        Some(results)
    }

    pub fn try_reparse(&mut self, rope: &ropey::Rope) -> bool {
        let mut callback = pack_rope_with_callback(rope);
        let mut parser = unpack_or_e!(self.parser.try_write().ok(), false, "failed to lock parser");

        debug!("doing reparse, tree = {:?}", self.tree);
        let tree = unpack_or_e!(
            parser.parse_with_options(&mut callback, self.tree.as_ref(), None),
            false,
            "failed parse"
        );

        self.tree = Some(tree);

        // #[cfg(debug_assertions)]
        // {
        //     let mut cursor = QueryCursor::new()
        //         .matches(&self.highlight_query, self.tree.as_ref().unwrap().root_node(), RopeWrapper(&rope));
        //
        //     let mut idx: usize = 0;
        //
        //     while let Some(m) = &cursor.next() {
        //         for (_cidx, c) in m.captures.iter().enumerate() {
        //             let name = &self.id_to_name[c.index as usize];
        //             debug!(
        //                 "m[{}]c[{}] : [{}:{}) = {}",
        //                 idx,
        //                 _cidx,
        //                 c.node.start_byte(),
        //                 c.node.end_byte(),
        //                 name,
        //             );
        //         }
        //
        //         idx += 1;
        //     }
        // }

        true
    }

    pub fn update_parse_on_insert(&mut self, rope: &ropey::Rope, char_idx_begin: usize, char_idx_end: usize) -> bool {
        if rope.len_chars() < char_idx_begin {
            error!("rope.len_chars() < char_idx_begin: {} >= {}", rope.len_chars(), char_idx_begin);
            return false;
        }

        let start_byte = match rope.try_char_to_byte(char_idx_begin) {
            Ok(byte) => byte,
            _ => return false,
        };

        let new_end_byte = match rope.try_char_to_byte(char_idx_end) {
            Ok(byte) => byte,
            _ => return false,
        };

        let start_point = match byte_offset_to_point(&rope, start_byte) {
            Some(point) => point,
            None => return false,
        };

        let new_end_point = match byte_offset_to_point(&rope, new_end_byte) {
            Some(point) => point,
            None => return false,
        };

        let input_edit = InputEdit {
            start_byte,
            old_end_byte: start_byte,
            new_end_byte,
            start_position: start_point,
            old_end_position: start_point,
            new_end_position: new_end_point,
        };

        self.tree.as_mut().map(|tree| tree.edit(&input_edit));
        self.try_reparse(rope)
    }

    pub fn update_parse_on_delete(&mut self, rope: &ropey::Rope, char_idx_begin: usize, char_idx_end: usize) -> bool {
        if char_idx_begin >= char_idx_end {
            error!("char_idx_begin >= char_idx_end: {} >= {}", char_idx_begin, char_idx_end);
            return false;
        }

        if rope.len_chars() < char_idx_begin {
            error!("rope.len_chars() < char_idx_begin: {} >= {}", rope.len_chars(), char_idx_begin);
            return false;
        }

        let start_byte = match rope.try_char_to_byte(char_idx_begin) {
            Ok(byte) => byte,
            _ => return false,
        };

        let old_end_byte = match rope.try_char_to_byte(char_idx_end) {
            Ok(byte) => byte,
            _ => return false,
        };

        let start_point = match byte_offset_to_point(&rope, start_byte) {
            Some(point) => point,
            None => return false,
        };

        let old_end_point = match byte_offset_to_point(&rope, old_end_byte) {
            Some(point) => point,
            None => return false,
        };

        let input_edit = InputEdit {
            start_byte,
            old_end_byte,
            new_end_byte: start_byte,
            start_position: start_point,
            old_end_position: old_end_point,
            new_end_position: start_point,
        };

        self.tree.as_mut().map(|tree| tree.edit(&input_edit));
        self.try_reparse(rope)
    }
}