xi_core_lib/
line_ending.rs

1// Copyright 2018 The xi-editor Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Utilities for detecting and working with line endings
16
17extern crate xi_rope;
18
19use memchr::memchr2;
20use xi_rope::Rope;
21
22/// An enumeration of valid line endings
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum LineEnding {
25    CrLf, // DOS style, \r\n
26    Lf,   // *nix style, \n
27}
28
29/// A struct representing a mixed line ending error.
30#[derive(Debug)]
31pub struct MixedLineEndingError;
32
33impl LineEnding {
34    /// Breaks a rope down into chunks, and checks each chunk for line endings
35    pub fn parse(rope: &Rope) -> Result<Option<Self>, MixedLineEndingError> {
36        let mut crlf = false;
37        let mut lf = false;
38
39        for chunk in rope.iter_chunks(..) {
40            match LineEnding::parse_chunk(&chunk) {
41                Ok(Some(LineEnding::CrLf)) => crlf = true,
42                Ok(Some(LineEnding::Lf)) => lf = true,
43                Ok(None) => (),
44                Err(e) => return Err(e),
45            }
46        }
47
48        match (crlf, lf) {
49            (true, false) => Ok(Some(LineEnding::CrLf)),
50            (false, true) => Ok(Some(LineEnding::Lf)),
51            (false, false) => Ok(None),
52            _ => Err(MixedLineEndingError),
53        }
54    }
55
56    /// Checks a chunk for line endings, assuming \n or \r\n
57    pub fn parse_chunk(chunk: &str) -> Result<Option<Self>, MixedLineEndingError> {
58        let bytes = chunk.as_bytes();
59        let newline = memchr2(b'\n', b'\r', bytes);
60        match newline {
61            Some(x) if bytes[x] == b'\r' && bytes.len() > x + 1 && bytes[x + 1] == b'\n' => {
62                Ok(Some(LineEnding::CrLf))
63            }
64            Some(x) if bytes[x] == b'\n' => Ok(Some(LineEnding::Lf)),
65            Some(_) => Err(MixedLineEndingError),
66            _ => Ok(None),
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn crlf() {
77        let result = LineEnding::parse_chunk("\r\n");
78        assert!(result.is_ok());
79        assert_eq!(result.unwrap(), Some(LineEnding::CrLf));
80    }
81
82    #[test]
83    fn lf() {
84        let result = LineEnding::parse_chunk("\n");
85        assert!(result.is_ok());
86        assert_eq!(result.unwrap(), Some(LineEnding::Lf));
87    }
88
89    #[test]
90    fn legacy_mac_errors() {
91        assert!(LineEnding::parse_chunk("\r").is_err());
92    }
93
94    #[test]
95    fn bad_space() {
96        assert!(LineEnding::parse_chunk("\r \n").is_err());
97    }
98}