jiq 3.22.1

Interactive JSON query tool with real-time output
Documentation
use std::sync::Arc;

pub const SCROLLOFF: u16 = 4;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SelectionMode {
    Normal,
    Visual,
}

#[derive(Debug, Clone)]
pub struct CursorState {
    cursor_line: u32,
    mode: SelectionMode,
    selection_anchor: u32,
    hovered_line: Option<u32>,
    total_lines: u32,
    line_widths: Option<Arc<Vec<u16>>>,
}

impl Default for CursorState {
    fn default() -> Self {
        Self::new()
    }
}

impl CursorState {
    pub fn new() -> Self {
        Self {
            cursor_line: 0,
            mode: SelectionMode::Normal,
            selection_anchor: 0,
            hovered_line: None,
            total_lines: 0,
            line_widths: None,
        }
    }

    pub fn cursor_line(&self) -> u32 {
        self.cursor_line
    }

    #[allow(dead_code)]
    pub fn mode(&self) -> SelectionMode {
        self.mode
    }

    pub fn is_visual_mode(&self) -> bool {
        self.mode == SelectionMode::Visual
    }

    pub fn hovered_line(&self) -> Option<u32> {
        self.hovered_line
    }

    pub fn total_lines(&self) -> u32 {
        self.total_lines
    }

    pub fn update_total_lines(&mut self, total: u32) {
        self.total_lines = total;
        if self.cursor_line >= total && total > 0 {
            self.cursor_line = total - 1;
        }
        if self.selection_anchor >= total && total > 0 {
            self.selection_anchor = total - 1;
        }
    }

    pub fn update_line_widths(&mut self, widths: Arc<Vec<u16>>) {
        self.line_widths = Some(widths);
    }

    pub fn get_cursor_line_width(&self) -> u16 {
        self.get_line_width(self.cursor_line)
    }

    pub fn get_line_width(&self, line: u32) -> u16 {
        self.line_widths
            .as_ref()
            .and_then(|widths| widths.get(line as usize).copied())
            .unwrap_or(0)
    }

    pub fn get_max_selected_line_width(&self) -> u16 {
        if self.mode != SelectionMode::Visual {
            return self.get_cursor_line_width();
        }

        let (start, end) = self.selection_range();
        let widths = match &self.line_widths {
            Some(w) => w,
            None => return 0,
        };

        (start..=end)
            .filter_map(|line| widths.get(line as usize).copied())
            .max()
            .unwrap_or(0)
    }

    pub fn selection_range(&self) -> (u32, u32) {
        if self.mode == SelectionMode::Normal {
            (self.cursor_line, self.cursor_line)
        } else {
            let start = self.cursor_line.min(self.selection_anchor);
            let end = self.cursor_line.max(self.selection_anchor);
            (start, end)
        }
    }

    #[allow(dead_code)]
    pub fn is_line_selected(&self, line: u32) -> bool {
        if self.mode == SelectionMode::Normal {
            return false;
        }
        let (start, end) = self.selection_range();
        line >= start && line <= end
    }

    #[allow(dead_code)]
    pub fn is_cursor_line(&self, line: u32) -> bool {
        self.cursor_line == line
    }

    pub fn move_up(&mut self, lines: u32) {
        self.cursor_line = self.cursor_line.saturating_sub(lines);
    }

    pub fn move_down(&mut self, lines: u32) {
        if self.total_lines == 0 {
            return;
        }
        let max_line = self.total_lines.saturating_sub(1);
        self.cursor_line = self.cursor_line.saturating_add(lines).min(max_line);
    }

    pub fn move_to_first(&mut self) {
        self.cursor_line = 0;
    }

    pub fn move_to_last(&mut self) {
        if self.total_lines == 0 {
            return;
        }
        self.cursor_line = self.total_lines - 1;
    }

    pub fn move_to_line(&mut self, line: u32) {
        if self.total_lines == 0 {
            return;
        }
        self.cursor_line = line.min(self.total_lines - 1);
    }

    pub fn enter_visual_mode(&mut self) {
        self.mode = SelectionMode::Visual;
        self.selection_anchor = self.cursor_line;
    }

    pub fn exit_visual_mode(&mut self) {
        self.mode = SelectionMode::Normal;
    }

    #[allow(dead_code)]
    pub fn toggle_visual_mode(&mut self) {
        match self.mode {
            SelectionMode::Normal => self.enter_visual_mode(),
            SelectionMode::Visual => self.exit_visual_mode(),
        }
    }

    pub fn set_hovered(&mut self, line: Option<u32>) {
        self.hovered_line = line;
    }

    pub fn clear_hover(&mut self) {
        self.hovered_line = None;
    }

    pub fn reset(&mut self) {
        self.cursor_line = 0;
        self.mode = SelectionMode::Normal;
        self.selection_anchor = 0;
        self.hovered_line = None;
        self.line_widths = None;
    }

    pub fn click_select(&mut self, line: u32) {
        self.cursor_line = line.min(self.total_lines.saturating_sub(1));
        self.mode = SelectionMode::Visual;
        self.selection_anchor = self.cursor_line;
    }

    pub fn drag_extend(&mut self, line: u32) {
        if self.mode != SelectionMode::Visual {
            return;
        }
        self.cursor_line = line.min(self.total_lines.saturating_sub(1));
    }

    #[allow(dead_code)]
    pub fn selected_line_count(&self) -> u32 {
        let (start, end) = self.selection_range();
        end - start + 1
    }

    #[allow(dead_code)]
    pub fn compute_scroll_for_cursor(
        &self,
        current_offset: u16,
        viewport_height: u16,
        max_offset: u16,
    ) -> u16 {
        if viewport_height == 0 {
            return current_offset;
        }

        let cursor_line = self.cursor_line.min(u16::MAX as u32) as u16;
        let effective_scrolloff = SCROLLOFF.min(viewport_height / 2);

        let visible_start = current_offset;
        let visible_end = current_offset.saturating_add(viewport_height);

        if cursor_line < visible_start.saturating_add(effective_scrolloff) {
            cursor_line.saturating_sub(effective_scrolloff)
        } else if cursor_line >= visible_end.saturating_sub(effective_scrolloff) {
            cursor_line
                .saturating_add(effective_scrolloff)
                .saturating_add(1)
                .saturating_sub(viewport_height)
                .min(max_offset)
        } else {
            current_offset
        }
    }
}

#[cfg(test)]
#[path = "cursor_state_tests.rs"]
mod cursor_state_tests;