tex-fmt 0.4.4

LaTeX formatter written in Rust
//! Utilities for indenting source lines

use crate::comments::*;
use crate::format::*;
use crate::ignore::*;
use crate::logging::*;
use crate::parse::*;
use crate::regexes::*;
use crate::verbatim::*;
use core::cmp::max;
use log::Level::{Trace, Warn};

/// Opening delimiters
const OPENS: [char; 3] = ['{', '(', '['];
/// Closing delimiters
const CLOSES: [char; 3] = ['}', ')', ']'];

/// Information on the indentation state of a line
#[derive(Debug, Clone)]
pub struct Indent {
    /// The indentation level of a line
    pub actual: i8,
    /// The visual indentation level of a line
    pub visual: i8,
}

impl Indent {
    /// Construct a new indentation state
    pub const fn new() -> Self {
        Self {
            actual: 0,
            visual: 0,
        }
    }
}

/// Calculate total indentation change due to the current line
fn get_diff(line: &str, contains_env_end: bool) -> i8 {
    // list environments get double indents
    let mut diff: i8 = 0;

    // other environments get single indents
    if line.contains(ENV_BEGIN) {
        // documents get no global indentation
        if line.contains(DOC_BEGIN) {
            return 0;
        };
        diff += 1;
        diff += i8::try_from(
            LISTS_BEGIN.iter().filter(|&r| line.contains(r)).count(),
        )
        .unwrap();
    } else if contains_env_end {
        // documents get no global indentation
        if line.contains(DOC_END) {
            return 0;
        };
        diff -= 1;
        diff -= i8::try_from(
            LISTS_END.iter().filter(|&r| line.contains(r)).count(),
        )
        .unwrap();
    };

    // indent for delimiters
    diff += i8::try_from(line.chars().filter(|x| OPENS.contains(x)).count())
        .unwrap();
    diff -= i8::try_from(line.chars().filter(|x| CLOSES.contains(x)).count())
        .unwrap();

    diff
}

/// Calculate dedentation for the current line
fn get_back(line: &str, contains_env_end: bool) -> i8 {
    let mut back: i8 = 0;
    let mut cumul: i8 = 0;

    // delimiters
    for c in line.chars() {
        cumul -= i8::from(OPENS.contains(&c));
        cumul += i8::from(CLOSES.contains(&c));
        back = max(cumul, back);
    }

    // other environments get single indents
    if contains_env_end {
        // documents get no global indentation
        if line.contains(DOC_END) {
            return 0;
        };
        // list environments get double indents for indenting items
        for r in LISTS_END.iter() {
            if line.contains(r) {
                return 2;
            };
        }
        back += 1;
    };

    // deindent items to make the rest of item environment appear indented
    if line.contains(ITEM) {
        back += 1;
    };

    back
}

/// Check if a line contains an environment end
fn check_contains_env_end(line: &str) -> bool {
    line.contains(ENV_END)
}

/// Calculate indentation properties of the current line
fn get_indent(line: &str, prev_indent: &Indent) -> Indent {
    let contains_env_end = check_contains_env_end(line);
    let diff = get_diff(line, contains_env_end);
    let back = get_back(line, contains_env_end);
    let actual = prev_indent.actual + diff;
    let visual = prev_indent.actual - back;
    Indent { actual, visual }
}

/// Apply the correct indentation to a line
pub fn apply_indent(
    line: &str,
    linum_old: usize,
    state: &State,
    logs: &mut Vec<Log>,
    file: &str,
    args: &Cli,
) -> (String, State) {
    let mut new_line = line.to_string();
    let mut new_state = state.clone();
    new_state.linum_old = linum_old;

    new_state.ignore = get_ignore(line, &new_state, logs, file, true);
    new_state.verbatim = get_verbatim(line, &new_state, logs, file, true);

    if !new_state.verbatim.visual && !new_state.ignore.visual {
        // calculate indent
        let comment_index = find_comment_index(line);
        let line_strip = &remove_comment(line, comment_index);
        let mut indent = get_indent(line_strip, &state.indent);
        new_state.indent = indent.clone();
        if args.trace {
            record_line_log(
                logs,
                Trace,
                file,
                state.linum_new,
                new_state.linum_old,
                line,
                &format!(
                    "Indent: actual = {}, visual = {}:",
                    indent.actual, indent.visual
                ),
            );
        }

        if (indent.visual < 0) || (indent.actual < 0) {
            record_line_log(
                logs,
                Warn,
                file,
                new_state.linum_new,
                new_state.linum_old,
                line,
                "Indent is negative.",
            );
            indent.actual = indent.actual.max(0);
            indent.visual = indent.visual.max(0);
        }

        // apply indent
        new_line = line.trim_start().to_string();
        if !new_line.is_empty() {
            let n_spaces = indent.visual * args.tab;
            for _ in 0..n_spaces {
                new_line.insert(0, ' ');
            }
        }
    }

    (new_line, new_state)
}