alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! File-backed buffer metadata and write reports.

use super::{display::BufferDisplayPath, error::BufferWriteError};
use crate::{
    fs_utils::{self, FilesystemConfig},
    text_stream::{TextByteStream, TextRevision},
};
use bevy::prelude::Component;
use std::path::PathBuf;

/// Buffer metadata for an optional file on disk.
#[derive(Clone, Component, Debug, Default, Eq, PartialEq)]
pub struct BufferFile {
    /// Path backing the current buffer, when one was provided at launch.
    path: Option<PathBuf>,
    /// Stream revision that is known to have been written to disk.
    saved_revision: TextRevision,
}

impl BufferFile {
    /// Creates scratch buffer metadata with no backing file.
    #[must_use]
    pub fn scratch(saved_revision: impl Into<TextRevision>) -> Self {
        Self {
            path: None,
            saved_revision: saved_revision.into(),
        }
    }

    /// Creates file-backed buffer metadata.
    #[must_use]
    pub fn backed_by(path: PathBuf, saved_revision: impl Into<TextRevision>) -> Self {
        Self {
            path: Some(path),
            saved_revision: saved_revision.into(),
        }
    }

    /// Returns the backing path, if one exists.
    #[must_use]
    pub const fn path(&self) -> Option<&PathBuf> {
        self.path.as_ref()
    }

    /// Returns whether the current stream has changes not known to be saved.
    #[must_use]
    pub fn is_modified(&self, stream: &TextByteStream) -> bool {
        self.saved_revision != stream.revision()
    }

    /// Writes the stream to the backing path and updates the saved revision.
    ///
    /// # Errors
    ///
    /// Returns [`BufferWriteError::NoFileName`] when the buffer is scratch, or
    /// [`BufferWriteError::File`] when persistence fails.
    pub fn write(
        &mut self,
        stream: &TextByteStream,
        config: &FilesystemConfig,
    ) -> Result<BufferWriteReport, BufferWriteError> {
        let Some(path) = &self.path else {
            return Err(BufferWriteError::NoFileName);
        };

        let written_path = fs_utils::atomic_write(path, stream.as_bytes(), config)
            .map_err(BufferWriteError::File)?;
        self.saved_revision = stream.revision();

        Ok(BufferWriteReport::new(
            BufferDisplayPath::from_resolved_path(&written_path, config),
            stream.as_str(),
            stream.as_bytes().len(),
        ))
    }
}

/// Summary of a successful buffer write.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BufferWriteReport {
    /// User-facing path that was written.
    display_path: BufferDisplayPath,
    /// Count of logical buffer lines written.
    line_count: usize,
    /// Count of bytes written.
    byte_count: usize,
}

impl BufferWriteReport {
    /// Creates a write report from persisted text.
    pub(super) fn new(display_path: BufferDisplayPath, text: &str, byte_count: usize) -> Self {
        Self {
            display_path,
            line_count: line_count(text),
            byte_count,
        }
    }

    /// Renders a compact Vim-like write message.
    #[must_use]
    pub fn status_text(&self) -> String {
        format!(
            "\"{}\" {}L, {}B written",
            self.display_path.as_str(),
            self.line_count,
            self.byte_count
        )
    }
}

/// Counts lines the way an editor status message presents the whole buffer.
fn line_count(text: &str) -> usize {
    if text.is_empty() {
        0
    } else {
        text.lines().count()
    }
}