arity 0.6.0

An LSP, formatter, and linter for R
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FormatStyle {
    pub line_width: usize,
    pub indent_width: usize,
    pub line_ending: LineEnding,
}

impl Default for FormatStyle {
    fn default() -> Self {
        Self {
            line_width: 80,
            indent_width: 2,
            line_ending: LineEnding::default(),
        }
    }
}

/// The character sequence the formatter emits at the end of each line.
///
/// The layout engine always builds output with `\n` line breaks (the formatter
/// is the sole authority on *where* breaks go, Tenet 1); this only selects the
/// byte sequence those breaks render as in the final string.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineEnding {
    /// Detect the newline style per file from the first line ending in the
    /// source, defaulting to `\n` when the source has none. The default.
    #[default]
    Auto,
    /// Always `\n` (Unix).
    Lf,
    /// Always `\r\n` (Windows).
    Crlf,
    /// `\n` on Unix, `\r\n` on Windows.
    Native,
}

impl LineEnding {
    /// Resolve to the concrete sequence to emit. `source` is consulted only for
    /// [`LineEnding::Auto`], which mirrors the source's first line ending.
    pub fn resolve(self, source: &str) -> &'static str {
        match self {
            LineEnding::Lf => "\n",
            LineEnding::Crlf => "\r\n",
            LineEnding::Native => {
                if cfg!(windows) {
                    "\r\n"
                } else {
                    "\n"
                }
            }
            LineEnding::Auto => {
                if source_is_crlf(source) {
                    "\r\n"
                } else {
                    "\n"
                }
            }
        }
    }
}

/// Whether the source's first line ending is CRLF. A bare `\r` (old Mac) or no
/// newline at all reads as LF.
fn source_is_crlf(source: &str) -> bool {
    match source.find('\n') {
        Some(idx) => idx > 0 && source.as_bytes()[idx - 1] == b'\r',
        None => false,
    }
}

/// Re-render `formatted` (built with `\n` breaks, but possibly carrying verbatim
/// `\r\n` from multi-line string tokens copied out of the source) with a uniform
/// line ending. CRLF is first canonicalized to LF so the target is applied
/// exactly once; a lone `\r` is left untouched.
pub(crate) fn apply_line_ending(formatted: &str, eol: &str) -> String {
    let lf = if formatted.contains('\r') {
        formatted.replace("\r\n", "\n")
    } else {
        formatted.to_string()
    };
    if eol == "\n" {
        lf
    } else {
        lf.replace('\n', eol)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn auto_detects_crlf_from_first_line_ending() {
        assert_eq!(LineEnding::Auto.resolve("a\r\nb\n"), "\r\n");
        assert_eq!(LineEnding::Auto.resolve("a\nb\r\n"), "\n");
        assert_eq!(LineEnding::Auto.resolve("no newline"), "\n");
        assert_eq!(LineEnding::Auto.resolve(""), "\n");
    }

    #[test]
    fn explicit_endings_ignore_source() {
        assert_eq!(LineEnding::Lf.resolve("a\r\n"), "\n");
        assert_eq!(LineEnding::Crlf.resolve("a\n"), "\r\n");
    }

    #[test]
    fn apply_canonicalizes_then_expands() {
        // Verbatim CRLF (e.g. from a multi-line string) is normalized before the
        // target is applied, so CRLF target never doubles to `\r\r\n`.
        assert_eq!(apply_line_ending("a\nb\r\nc\n", "\r\n"), "a\r\nb\r\nc\r\n");
        assert_eq!(apply_line_ending("a\nb\r\nc\n", "\n"), "a\nb\nc\n");
    }
}