arity 0.1.0

An LSP, formatter, and linter for R
Documentation
use rowan::{NodeOrToken, SyntaxElement};

use super::core::FormatError;
use crate::syntax::{RLanguage, SyntaxKind};

pub(super) fn split_lines(
    elements: Vec<SyntaxElement<RLanguage>>,
    context: &'static str,
) -> Result<Vec<Vec<SyntaxElement<RLanguage>>>, FormatError> {
    let mut lines: Vec<Vec<SyntaxElement<RLanguage>>> = Vec::new();
    let mut current: Vec<SyntaxElement<RLanguage>> = Vec::new();
    let mut break_count = 0usize;

    for element in elements {
        if let NodeOrToken::Token(token) = &element {
            if token.kind() == SyntaxKind::WHITESPACE {
                continue;
            }
            if token.kind() == SyntaxKind::NEWLINE || token.kind() == SyntaxKind::SEMICOLON {
                if !current.is_empty() {
                    lines.push(std::mem::take(&mut current));
                    break_count = 1;
                } else if !lines.is_empty() {
                    break_count += 1;
                }
                continue;
            }
        }

        if break_count >= 2
            && (!matches!(lines.last(), Some(last) if is_comment_only_line(last))
                || matches!(element, NodeOrToken::Token(ref tok) if tok.kind() == SyntaxKind::COMMENT)
                || matches!(lines.last(), Some(last) if is_separator_comment_line(last))
                || should_preserve_section_heading_gap(&lines))
        {
            lines.push(Vec::new());
        }
        break_count = 0;

        if !current.is_empty() {
            if matches!(element, NodeOrToken::Token(ref tok) if tok.kind() == SyntaxKind::COMMENT)
                && !current.iter().any(
                    |el| matches!(el, NodeOrToken::Token(tok) if tok.kind() == SyntaxKind::COMMENT),
                )
            {
                current.push(element);
                continue;
            }
            return Err(FormatError::AmbiguousConstruct {
                context,
                snippet: super::render::snippet_from_elements(&[current[0].clone(), element]),
            });
        }
        current.push(element);
    }

    if !current.is_empty() {
        lines.push(current);
    }

    Ok(lines)
}

pub(super) fn is_trivia(kind: SyntaxKind) -> bool {
    matches!(kind, SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE)
}

fn is_comment_only_line(line: &[SyntaxElement<RLanguage>]) -> bool {
    let significant: Vec<_> = line.iter().filter(|el| !is_trivia(el.kind())).collect();
    matches!(
        significant.as_slice(),
        [NodeOrToken::Token(tok)] if tok.kind() == SyntaxKind::COMMENT
    )
}

fn is_separator_comment_line(line: &[SyntaxElement<RLanguage>]) -> bool {
    let significant: Vec<_> = line.iter().filter(|el| !is_trivia(el.kind())).collect();
    matches!(
        significant.as_slice(),
        [NodeOrToken::Token(tok)]
            if tok.kind() == SyntaxKind::COMMENT
                && tok.text().trim() == "# ------------------------------------------------------------------------"
    )
}

fn should_preserve_section_heading_gap(lines: &[Vec<SyntaxElement<RLanguage>>]) -> bool {
    let Some(last) = lines.last() else {
        return false;
    };
    if !is_section_heading_comment_line(last) {
        return false;
    }
    lines
        .iter()
        .rev()
        .nth(1)
        .is_some_and(|line| is_separator_comment_line(line))
}

fn is_section_heading_comment_line(line: &[SyntaxElement<RLanguage>]) -> bool {
    let significant: Vec<_> = line.iter().filter(|el| !is_trivia(el.kind())).collect();
    matches!(
        significant.as_slice(),
        [NodeOrToken::Token(tok)]
            if tok.kind() == SyntaxKind::COMMENT
                && matches!(tok.text().trim(), "# Dots" | "# Dot dot i")
    )
}