frigg 0.3.2

Local-first MCP server for code understanding.
Documentation
use super::*;

pub(crate) fn source_span_from_offsets(
    source: &str,
    start_byte: usize,
    end_byte: usize,
) -> SourceSpan {
    let start_byte = start_byte.min(source.len());
    let end_byte = end_byte.max(start_byte).min(source.len());
    let (start_line, start_column) = line_column_for_offset(source, start_byte);
    let (end_line, end_column) = line_column_for_offset(source, end_byte);
    SourceSpan {
        start_byte,
        end_byte,
        start_line,
        start_column,
        end_line,
        end_column,
    }
}

pub(crate) fn line_column_for_offset(source: &str, offset: usize) -> (usize, usize) {
    let clamped = offset.min(source.len());
    let bytes = source.as_bytes();
    let prefix = &bytes[..clamped];
    let line = prefix.iter().filter(|byte| **byte == b'\n').count() + 1;
    let line_start = prefix
        .iter()
        .rposition(|byte| *byte == b'\n')
        .map(|index| index + 1)
        .unwrap_or(0);
    let column = clamped.saturating_sub(line_start) + 1;
    (line, column)
}

pub(crate) fn byte_offset_for_line_column(
    source: &str,
    line: usize,
    column: usize,
) -> Option<usize> {
    if line == 0 || column == 0 {
        return None;
    }
    let bytes = source.as_bytes();
    let mut current_line = 1usize;
    let mut line_start = 0usize;
    for (index, byte) in bytes.iter().enumerate() {
        if current_line == line {
            let line_end = bytes[line_start..]
                .iter()
                .position(|candidate| *candidate == b'\n')
                .map(|offset| line_start + offset)
                .unwrap_or(bytes.len());
            let line_len = line_end.saturating_sub(line_start);
            let column_offset = column.saturating_sub(1).min(line_len);
            return Some(line_start + column_offset);
        }
        if *byte == b'\n' {
            current_line = current_line.saturating_add(1);
            line_start = index.saturating_add(1);
        }
    }
    if current_line == line {
        let line_len = bytes.len().saturating_sub(line_start);
        let column_offset = column.saturating_sub(1).min(line_len);
        return Some(line_start + column_offset);
    }
    None
}

pub(crate) fn source_span(node: Node<'_>) -> SourceSpan {
    let start = node.start_position();
    let end = node.end_position();
    SourceSpan {
        start_byte: node.start_byte(),
        end_byte: node.end_byte(),
        start_line: start.row + 1,
        start_column: start.column + 1,
        end_line: end.row + 1,
        end_column: end.column + 1,
    }
}