kirei 25.12.29

Askama formatter
Documentation
use tree_sitter::{Node, Range};

#[derive(Debug, Clone)]
pub struct Diagnostic {
    pub level: Severity,
    pub message: String,
    pub labels: Vec<Label>,
    pub help: Option<String>,
    pub suggestion: Option<Suggestion>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
    Error,
    Warning,
}

#[derive(Debug, Clone)]
pub struct Suggestion {
    pub line_idx: usize,
    pub original_line: String,
    pub start_col: usize,
    pub end_col: usize,
    pub replacement: String,
}

#[derive(Debug, Clone)]
pub struct Label {
    pub range: Range,
    pub message: String,
    pub annotation: Annotation,
}

impl Label {
    pub fn line_idx(&self) -> usize {
        self.range.start_point.row
    }

    pub fn end_line_idx(&self) -> usize {
        self.range.end_point.row
    }

    pub fn col_display(&self) -> usize {
        self.range.start_point.column + 1
    }

    pub fn span_len(&self) -> usize {
        self.range
            .end_point
            .column
            .saturating_sub(self.range.start_point.column)
            .max(1)
    }

    pub fn underline_char(&self) -> &str {
        match self.annotation {
            Annotation::Primary => "^",
            Annotation::Secondary => "-",
        }
    }

    pub fn is_multiline(&self) -> bool {
        self.range.start_point.row != self.range.end_point.row
    }
}

#[derive(Debug, Clone)]
pub enum Annotation {
    Primary,
    Secondary,
}

impl Diagnostic {
    pub fn error(message: impl Into<String>) -> Self {
        Self {
            level: Severity::Error,
            message: message.into(),
            labels: Vec::new(),
            help: None,
            suggestion: None,
        }
    }

    pub fn warning(message: impl Into<String>) -> Self {
        Self {
            level: Severity::Warning,
            message: message.into(),
            labels: Vec::new(),
            help: None,
            suggestion: None,
        }
    }

    pub fn with_label(
        mut self,
        range: Range,
        message: impl Into<String>,
        annotation: Annotation,
    ) -> Self {
        self.labels.push(Label {
            range,
            message: message.into(),
            annotation,
        });
        self
    }

    pub fn with_help(mut self, help: impl Into<String>) -> Self {
        self.help = Some(help.into());
        self
    }

    pub fn with_suggestion(
        mut self,
        line_index: usize,
        line: impl Into<String>,
        start_col: usize,
        end_col: usize,
        replacement: impl Into<String>,
    ) -> Self {
        self.suggestion = Some(Suggestion {
            line_idx: line_index,
            original_line: line.into(),
            start_col,
            end_col,
            replacement: replacement.into(),
        });
        self
    }

    pub fn find_error_node<'a>(node: &Node<'a>) -> Option<Node<'a>> {
        if node.is_error() || node.is_missing() {
            return Some(*node);
        }
        for child in node.children(&mut node.walk()) {
            if let Some(err) = Self::find_error_node(&child) {
                return Some(err);
            }
        }
        None
    }

    pub fn refine_error_range(error_node: &Node) -> Range {
        let mut range = error_node.range();
        let children: Vec<_> = error_node.children(&mut error_node.walk()).collect();
        let non_error_children: Vec<_> = children.iter().filter(|c| !c.is_error()).collect();

        if non_error_children.is_empty() {
            if range.start_point.row != range.end_point.row {
                range.end_point = range.start_point;
                range.end_point.column = range.start_point.column + 1;
                range.end_byte = range.start_byte + 1;
            }
            return range;
        }

        let opening_pos = non_error_children
            .iter()
            .position(|c| matches!(c.kind(), "{{" | "{%" | "{#"));

        if let Some(open_idx) = opening_pos {
            let closing_kind = match non_error_children[open_idx].kind() {
                "{{" => "}}",
                "{%" => "%}",
                "{#" => "#}",
                _ => "",
            };

            if let Some(close_idx) = non_error_children[open_idx..]
                .iter()
                .position(|c| c.kind() == closing_kind)
            {
                let first = non_error_children[open_idx];
                let last = non_error_children[open_idx + close_idx];
                return Self::range_between(first, last);
            }
        }

        let first = non_error_children.first().unwrap();
        let last = non_error_children.last().unwrap();
        Self::range_between(first, last)
    }

    fn range_between(first: &Node, last: &Node) -> Range {
        Range {
            start_byte: first.range().start_byte,
            start_point: first.range().start_point,
            end_byte: last.range().end_byte,
            end_point: last.range().end_point,
        }
    }
}