Skip to main content

rumdl_lib/utils/
line_ending.rs

1use std::borrow::Cow;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum LineEnding {
5    Lf,
6    Crlf,
7    Mixed,
8}
9
10pub fn detect_line_ending_enum(content: &str) -> LineEnding {
11    let bytes = content.as_bytes();
12    let mut has_crlf = false;
13    let mut has_standalone_lf = false;
14    let mut i = 0;
15
16    while i < bytes.len() {
17        if bytes[i] == b'\r' && i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
18            has_crlf = true;
19            i += 2;
20        } else if bytes[i] == b'\n' {
21            has_standalone_lf = true;
22            i += 1;
23        } else {
24            i += 1;
25        }
26        // Early exit once both types are found
27        if has_crlf && has_standalone_lf {
28            return LineEnding::Mixed;
29        }
30    }
31
32    match (has_crlf, has_standalone_lf) {
33        (true, true) => LineEnding::Mixed,
34        (true, false) => LineEnding::Crlf,
35        (false, _) => LineEnding::Lf,
36    }
37}
38
39pub fn detect_line_ending(content: &str) -> &'static str {
40    // Compatibility function matching the old signature
41    let crlf_count = content.matches("\r\n").count();
42    let lf_count = content.matches('\n').count() - crlf_count;
43
44    if crlf_count > lf_count { "\r\n" } else { "\n" }
45}
46
47pub fn normalize_line_ending<'a>(content: &'a str, target: LineEnding) -> Cow<'a, str> {
48    match target {
49        LineEnding::Lf => {
50            if !content.contains('\r') {
51                Cow::Borrowed(content)
52            } else {
53                Cow::Owned(content.replace("\r\n", "\n"))
54            }
55        }
56        LineEnding::Crlf => {
57            // First normalize everything to LF, then convert to CRLF
58            let normalized = content.replace("\r\n", "\n");
59            Cow::Owned(normalized.replace('\n', "\r\n"))
60        }
61        LineEnding::Mixed => Cow::Borrowed(content),
62    }
63}
64
65pub fn ensure_consistent_line_endings(original: &str, modified: &str) -> String {
66    let original_ending = detect_line_ending_enum(original);
67
68    // For mixed line endings, normalize to the most common one (like detect_line_ending does)
69    let target_ending = if original_ending == LineEnding::Mixed {
70        // Use the same logic as detect_line_ending: prefer the more common one
71        let crlf_count = original.matches("\r\n").count();
72        let lf_count = original.matches('\n').count() - crlf_count;
73        if crlf_count > lf_count {
74            LineEnding::Crlf
75        } else {
76            LineEnding::Lf
77        }
78    } else {
79        original_ending
80    };
81
82    let modified_ending = detect_line_ending_enum(modified);
83
84    if target_ending != modified_ending {
85        normalize_line_ending(modified, target_ending).into_owned()
86    } else {
87        modified.to_string()
88    }
89}
90
91pub fn get_line_ending_str(ending: LineEnding) -> &'static str {
92    match ending {
93        LineEnding::Lf => "\n",
94        LineEnding::Crlf => "\r\n",
95        LineEnding::Mixed => "\n", // Default to LF for mixed
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_detect_line_ending_enum() {
105        assert_eq!(detect_line_ending_enum("hello\nworld"), LineEnding::Lf);
106        assert_eq!(detect_line_ending_enum("hello\r\nworld"), LineEnding::Crlf);
107        assert_eq!(detect_line_ending_enum("hello\r\nworld\nmixed"), LineEnding::Mixed);
108        assert_eq!(detect_line_ending_enum("no line endings"), LineEnding::Lf);
109    }
110
111    #[test]
112    fn test_detect_line_ending() {
113        assert_eq!(detect_line_ending("hello\nworld"), "\n");
114        assert_eq!(detect_line_ending("hello\r\nworld"), "\r\n");
115        assert_eq!(detect_line_ending("hello\r\nworld\nmixed"), "\n"); // More LF than CRLF
116        assert_eq!(detect_line_ending("no line endings"), "\n");
117    }
118
119    #[test]
120    fn test_normalize_line_ending() {
121        assert_eq!(normalize_line_ending("hello\r\nworld", LineEnding::Lf), "hello\nworld");
122        assert_eq!(
123            normalize_line_ending("hello\nworld", LineEnding::Crlf),
124            "hello\r\nworld"
125        );
126        assert_eq!(
127            normalize_line_ending("hello\r\nworld\nmixed", LineEnding::Lf),
128            "hello\nworld\nmixed"
129        );
130    }
131
132    #[test]
133    fn test_ensure_consistent_line_endings() {
134        let original = "hello\r\nworld";
135        let modified = "hello\nworld\nextra";
136        assert_eq!(
137            ensure_consistent_line_endings(original, modified),
138            "hello\r\nworld\r\nextra"
139        );
140
141        let original = "hello\nworld";
142        let modified = "hello\r\nworld\r\nextra";
143        assert_eq!(
144            ensure_consistent_line_endings(original, modified),
145            "hello\nworld\nextra"
146        );
147    }
148}