nanorust 0.1.0

A terminal-based text editor written in Rust, inspired by GNU nano
Documentation
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;

use crate::error::{EditorError, Result};

/// File handler for reading and writing files
pub struct FileHandler;

impl FileHandler {
    /// Read a file and return its contents as a vector of lines
    /// 
    /// Handles UTF-8 encoding and different line endings (LF, CRLF)
    pub fn read_file(path: &Path) -> Result<Vec<String>> {
        // Check if file exists
        if !path.exists() {
            return Err(EditorError::FileNotFound(path.to_path_buf()));
        }

        // Check if we have read permissions
        let metadata = fs::metadata(path)?;
        if metadata.permissions().readonly() && !metadata.is_file() {
            return Err(EditorError::PermissionDenied(path.to_path_buf()));
        }

        // Open the file
        let file = File::open(path).map_err(|e| {
            if e.kind() == std::io::ErrorKind::PermissionDenied {
                EditorError::PermissionDenied(path.to_path_buf())
            } else {
                EditorError::IoError(e)
            }
        })?;

        let reader = BufReader::new(file);
        let mut lines = Vec::new();

        // Read lines and handle different line endings
        for line_result in reader.lines() {
            let line = line_result.map_err(|e| {
                if e.kind() == std::io::ErrorKind::InvalidData {
                    EditorError::InvalidUtf8
                } else {
                    EditorError::IoError(e)
                }
            })?;
            lines.push(line);
        }

        // Ensure at least one empty line if file is empty
        if lines.is_empty() {
            lines.push(String::new());
        }

        Ok(lines)
    }

    /// Write lines to a file
    /// 
    /// Handles file permissions and I/O errors
    pub fn write_file(path: &Path, lines: &[String]) -> Result<()> {
        // Check if parent directory exists
        if let Some(parent) = path.parent() {
            if !parent.exists() {
                fs::create_dir_all(parent)?;
            }
        }

        // Create or truncate the file
        let mut file = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(path)
            .map_err(|e| {
                if e.kind() == std::io::ErrorKind::PermissionDenied {
                    EditorError::PermissionDenied(path.to_path_buf())
                } else {
                    EditorError::IoError(e)
                }
            })?;

        // Write each line with LF line ending
        for (i, line) in lines.iter().enumerate() {
            file.write_all(line.as_bytes())?;
            // Add newline after each line except potentially the last one
            // (nano behavior: add newline after every line)
            if i < lines.len() - 1 || !line.is_empty() {
                file.write_all(b"\n")?;
            }
        }

        file.flush()?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_read_file_success() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test.txt");
        fs::write(&file_path, "line1\nline2\nline3").unwrap();

        let lines = FileHandler::read_file(&file_path).unwrap();
        assert_eq!(lines, vec!["line1", "line2", "line3"]);
    }

    #[test]
    fn test_read_file_with_crlf() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("test_crlf.txt");
        fs::write(&file_path, "line1\r\nline2\r\nline3").unwrap();

        let lines = FileHandler::read_file(&file_path).unwrap();
        assert_eq!(lines, vec!["line1", "line2", "line3"]);
    }

    #[test]
    fn test_read_empty_file() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("empty.txt");
        fs::write(&file_path, "").unwrap();

        let lines = FileHandler::read_file(&file_path).unwrap();
        assert_eq!(lines, vec![""]);
    }

    #[test]
    fn test_read_file_not_found() {
        let result = FileHandler::read_file(Path::new("/nonexistent/file.txt"));
        assert!(matches!(result, Err(EditorError::FileNotFound(_))));
    }

    #[test]
    fn test_write_file_success() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("output.txt");
        let lines = vec!["line1".to_string(), "line2".to_string(), "line3".to_string()];

        FileHandler::write_file(&file_path, &lines).unwrap();

        let content = fs::read_to_string(&file_path).unwrap();
        assert_eq!(content, "line1\nline2\nline3\n");
    }

    #[test]
    fn test_write_empty_file() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("empty_output.txt");
        let lines = vec!["".to_string()];

        FileHandler::write_file(&file_path, &lines).unwrap();

        let content = fs::read_to_string(&file_path).unwrap();
        assert_eq!(content, "");
    }

    #[test]
    fn test_write_file_creates_directory() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("subdir").join("output.txt");
        let lines = vec!["test".to_string()];

        FileHandler::write_file(&file_path, &lines).unwrap();

        assert!(file_path.exists());
        let content = fs::read_to_string(&file_path).unwrap();
        assert_eq!(content, "test\n");
    }

    #[test]
    fn test_read_utf8_file() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("utf8.txt");
        fs::write(&file_path, "สวัสดี\n世界\n🦀").unwrap();

        let lines = FileHandler::read_file(&file_path).unwrap();
        assert_eq!(lines, vec!["สวัสดี", "世界", "🦀"]);
    }

    #[test]
    fn test_write_utf8_file() {
        let dir = tempdir().unwrap();
        let file_path = dir.path().join("utf8_output.txt");
        let lines = vec!["สวัสดี".to_string(), "世界".to_string(), "🦀".to_string()];

        FileHandler::write_file(&file_path, &lines).unwrap();

        let content = fs::read_to_string(&file_path).unwrap();
        assert_eq!(content, "สวัสดี\n世界\n🦀\n");
    }
}