alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! File-backed buffer state and persistence helpers.

mod display;
mod edit;
mod error;
mod file;
mod open;

pub use display::BufferDisplayPath;
pub use edit::{
    BufferEdit, BufferEditError, BufferEditKind, BufferEditReport, BufferEditShape,
    DEFAULT_UNDO_RECORD_LIMIT, DEFAULT_UNDO_RETAINED_BYTES, EditTransaction, EditTransactionShape,
    UndoRecord, UndoRecordShape, UndoStack,
};
pub use error::{BufferOpenError, BufferWriteError};
pub use file::{BufferFile, BufferWriteReport};
pub use open::{launch_file_argument, open_launch_buffer};

#[cfg(test)]
mod tests {
    use super::{BufferDisplayPath, BufferFile, BufferWriteReport, launch_file_argument};
    use crate::{fs_utils::FilesystemConfig, text_stream::TextByteStream};
    use std::{
        ffi::OsString,
        path::PathBuf,
        time::{SystemTime, UNIX_EPOCH},
    };

    /// Returns a unique temporary file path below a missing parent directory.
    fn missing_parent_file_path(name: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time should be after unix epoch")
            .as_nanos();
        std::env::temp_dir()
            .join(format!("alma-missing-parent-{name}-{nanos}"))
            .join("buffer.txt")
    }

    #[test]
    fn launch_argument_uses_first_user_argument() {
        assert_eq!(
            launch_file_argument([OsString::from("alma"), OsString::from("file.txt")]),
            Some(OsString::from("alma"))
        );
        assert_eq!(
            launch_file_argument([OsString::from("file.txt")]),
            Some(OsString::from("file.txt"))
        );
        assert_eq!(launch_file_argument([]), None);
    }

    #[test]
    fn buffer_tracks_saved_revision() {
        let mut stream = TextByteStream::new("hello");
        let buffer = BufferFile::scratch(stream.revision());
        assert!(!buffer.is_modified(&stream));

        stream.replace_all("goodbye");
        assert!(buffer.is_modified(&stream));
    }

    #[test]
    fn failed_write_does_not_advance_saved_revision() {
        let mut stream = TextByteStream::new("hello");
        let mut buffer = BufferFile::backed_by(missing_parent_file_path("write-error"), 0);

        stream.replace_all("dirty");
        assert!(buffer.is_modified(&stream));
        let filesystem_config =
            FilesystemConfig::discover().expect("filesystem config should be discovered");

        assert!(buffer.write(&stream, &filesystem_config).is_err());
        assert!(buffer.is_modified(&stream));
    }

    #[test]
    fn write_report_matches_vim_shape() {
        let report = BufferWriteReport::new(
            BufferDisplayPath::from_sanitized("note.txt"),
            "one\ntwo\n",
            8,
        );

        assert_eq!(report.status_text(), "\"note.txt\" 2L, 8B written");
    }

    #[test]
    fn display_path_prefers_workspace_relative_text() {
        let root = std::env::temp_dir();
        let config = FilesystemConfig::from_workspace_root(&root)
            .expect("temporary directory should be a workspace root");
        let path = config.workspace_root.join("nested/note.txt");

        let display_path = BufferDisplayPath::from_resolved_path(&path, &config);

        assert_eq!(display_path.as_str(), "nested/note.txt");
        assert!(
            !display_path
                .as_str()
                .contains(config.workspace_root.to_string_lossy().as_ref())
        );
    }

    #[test]
    fn display_path_escapes_status_control_characters() {
        let root = std::env::temp_dir();
        let config = FilesystemConfig::from_workspace_root(&root)
            .expect("temporary directory should be a workspace root");
        let path = config
            .workspace_root
            .join("bad\nname\tquote\"bidi\u{202E}.txt");

        let display_path = BufferDisplayPath::from_resolved_path(&path, &config);

        assert_eq!(
            display_path.as_str(),
            "bad\\nname\\tquote\\\"bidi\\u{202E}.txt"
        );
        assert!(!display_path.as_str().contains('\n'));
        assert!(!display_path.as_str().contains('\t'));
        assert!(!display_path.as_str().contains('\u{202E}'));
    }
}