par-term 0.29.0

Cross-platform GPU-accelerated terminal emulator with inline graphics support (Sixel, iTerm2, Kitty)
//! Search highlighting functionality for terminal cells.

use crate::cell_renderer::Cell;
use crate::search::SearchMatch;

/// Parameters for [`apply_search_highlights_to_cells`].
pub(crate) struct SearchHighlightParams<'a> {
    /// Mutable cell grid for the current frame.
    pub cells: &'a mut [Cell],
    /// Number of columns per row.
    pub cols: usize,
    /// Current viewport scroll offset.
    pub scroll_offset: usize,
    /// Total lines in the scrollback buffer.
    pub scrollback_len: usize,
    /// Number of visible rows in the grid.
    pub visible_lines: usize,
    /// All current search matches.
    pub matches: &'a [SearchMatch],
    /// Index of the "current" match (rendered with a distinct color).
    pub current_match_idx: usize,
    /// Background color applied to non-current match cells.
    pub highlight_color: [u8; 4],
    /// Background color applied to the current match cells.
    pub current_highlight_color: [u8; 4],
}

/// Apply search highlights directly to a cell slice.
///
/// Called per-pane after `gather_pane_render_data` in `gpu_submit.rs`.
pub(crate) fn apply_search_highlights_to_cells(params: SearchHighlightParams<'_>) {
    let SearchHighlightParams {
        cells,
        cols,
        scroll_offset,
        scrollback_len,
        visible_lines,
        matches,
        current_match_idx,
        highlight_color,
        current_highlight_color,
    } = params;
    if matches.is_empty() {
        return;
    }

    let total_lines = scrollback_len + visible_lines;
    let visible_end = total_lines.saturating_sub(scroll_offset);
    let visible_start = visible_end.saturating_sub(visible_lines);

    for (match_idx, search_match) in matches.iter().enumerate() {
        if search_match.line < visible_start || search_match.line >= visible_end {
            continue;
        }

        let viewport_row = search_match.line - visible_start;
        let color = if match_idx == current_match_idx {
            current_highlight_color
        } else {
            highlight_color
        };

        for offset in 0..search_match.length {
            let col = search_match.column + offset;
            if col >= cols {
                break;
            }
            let cell_idx = viewport_row * cols + col;
            if cell_idx < cells.len() {
                cells[cell_idx].bg_color = color;
            }
        }
    }
}

/// Get all searchable lines (scrollback + current screen) as an iterator of (line_index, line_text).
///
/// This function ensures consistent handling of wide characters by converting all content
/// from cells rather than using pre-built scrollback strings.
///
/// # Arguments
/// * `term` - The terminal manager
/// * `visible_lines` - Number of visible terminal rows
///
/// # Returns
/// Iterator of (absolute_line_index, line_text) pairs where line 0 is the oldest scrollback line.
pub(crate) fn get_all_searchable_lines(
    term: &crate::terminal::TerminalManager,
    visible_lines: usize,
) -> impl Iterator<Item = (usize, String)> {
    let cols = term.dimensions().0;
    let scrollback_len = term.scrollback_len();

    // Get scrollback lines from their cell representation
    let scrollback_lines = term.scrollback_as_cells();
    let scrollback_iter = scrollback_lines
        .into_iter()
        .enumerate()
        .map(move |(idx, cells)| {
            let line = cells_row_to_string(&cells, cols);
            (idx, line)
        });

    // Get current screen lines
    let screen_cells = term.get_cells_with_scrollback(0, None, false, None);
    let current_lines = cells_to_lines(&screen_cells, cols, visible_lines);
    let current_iter = current_lines
        .into_iter()
        .enumerate()
        .map(move |(idx, line)| (scrollback_len + idx, line));

    scrollback_iter.chain(current_iter)
}

/// Convert a flat array of cells into lines.
fn cells_to_lines(cells: &[Cell], cols: usize, num_lines: usize) -> Vec<String> {
    let mut lines = Vec::with_capacity(num_lines);

    for row in 0..num_lines {
        let row_start = row * cols;
        let row_end = (row_start + cols).min(cells.len());

        if row_start >= cells.len() {
            lines.push(String::new());
            continue;
        }

        let line = cells_row_to_string(&cells[row_start..row_end], cols);
        lines.push(line);
    }

    lines
}

/// Convert a row of cells to a string for searching.
/// Wide character spacers are converted to spaces to maintain cell index alignment.
fn cells_row_to_string(cells: &[Cell], _cols: usize) -> String {
    let line: String = cells
        .iter()
        .map(|cell| {
            if cell.grapheme.is_empty() || cell.wide_char_spacer {
                ' '
            } else {
                cell.grapheme.chars().next().unwrap_or(' ')
            }
        })
        .collect();

    // Trim trailing whitespace but keep the line
    line.trim_end().to_string()
}