alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
//! Active host-import session state.

use super::super::{
    PendingPluginWorkspaceIoBatch, PluginBufferHandle, PluginHandleStore, PluginHostContext,
    PluginViewHandle, PluginWorkspaceIoBudget, PluginWorkspaceObserveTaskHandle,
    PluginWorkspaceObserveTaskQueue,
    effect::{PendingPluginUpdateBatch, PluginBufferEditProposal, PluginEffectBatchLimit},
    workspace_io::PendingPluginWorkspaceObserveTaskBatch,
};
#[cfg(any(test, feature = "plugin-runtime"))]
use super::PluginBufferObservationError;
use super::snapshot::PluginHostImportSnapshot;
use super::{
    PluginHostImportBatches, PluginHostImportDiscardReport, PluginHostImportError,
    PluginWorkspaceObserveTaskResourceAuthority,
};
use crate::{
    buffer::BufferEdit,
    fs_utils::FilesystemConfig,
    plugin::{
        PluginCapabilityRef, PluginIdentity, PluginWorkspaceObserveOutcome, WorkspacePath,
        WorkspacePathRef,
    },
};
use std::fmt::{Debug, Formatter};
#[cfg(any(test, feature = "plugin-runtime"))]
use std::num::NonZeroU64;

/// Host-import state for one guest update.
pub struct PluginHostImportSession {
    /// Captured authority for this update.
    snapshot: PluginHostImportSnapshot,
    /// Discardable editor effects.
    effects: PendingPluginUpdateBatch,
    /// Discardable workspace I/O.
    workspace_io: PendingPluginWorkspaceIoBatch,
    /// Discardable workspace observation tasks.
    workspace_observe_tasks: PendingPluginWorkspaceObserveTaskBatch,
}

impl PluginHostImportSession {
    /// Starts a guest update import session.
    ///
    /// # Errors
    ///
    /// Returns [`PluginHostImportError`] when captured host state is identity-mismatched.
    pub fn new(
        host: &PluginHostContext,
        handles: &PluginHandleStore,
        effect_limit: PluginEffectBatchLimit,
        workspace_budget: PluginWorkspaceIoBudget,
    ) -> Result<Self, PluginHostImportError> {
        Ok(Self::from_snapshot(
            PluginHostImportSnapshot::new(host, handles)?,
            effect_limit,
            workspace_budget,
        ))
    }

    /// Starts a guest update import session from an active-instance snapshot.
    pub(in crate::plugin) fn from_snapshot(
        snapshot: PluginHostImportSnapshot,
        effect_limit: PluginEffectBatchLimit,
        workspace_budget: PluginWorkspaceIoBudget,
    ) -> Self {
        Self {
            effects: PendingPluginUpdateBatch::from_validated_limit(
                snapshot.identity_proof().clone(),
                effect_limit,
            ),
            workspace_io: PendingPluginWorkspaceIoBatch::from_validated_budget(
                snapshot.identity_proof().clone(),
                workspace_budget,
            ),
            workspace_observe_tasks: PendingPluginWorkspaceObserveTaskBatch::new(
                snapshot.identity_proof().clone(),
            ),
            snapshot,
        }
    }

    /// Returns the validated identity captured for this update.
    #[must_use]
    pub(in crate::plugin) const fn identity_proof(&self) -> &PluginIdentity {
        self.snapshot.identity_proof()
    }

    /// Queues a status message for a resolved view.
    ///
    /// # Errors
    ///
    /// Returns [`PluginHostImportError`] when authorization, handle resolution, or queuing fails.
    pub fn push_status_text(
        &mut self,
        view: PluginViewHandle,
        text: impl AsRef<str>,
    ) -> Result<(), PluginHostImportError> {
        self.snapshot
            .authorize(PluginCapabilityRef::StatusPublish)?;
        let view = self.snapshot.handles().resolve_view(view)?;
        self.effects.push_status_text(view.target(), text)?;
        Ok(())
    }

    /// Returns a bounded buffer snapshot for an authorized buffer handle.
    ///
    /// # Errors
    ///
    /// Returns [`PluginHostImportError`] when authorization, handle resolution, snapshot
    /// provenance, or return-size bounds reject the request.
    #[cfg(any(test, feature = "plugin-runtime"))]
    pub(in crate::plugin) fn observe_buffer(
        &self,
        buffer: PluginBufferHandle,
        max_text_bytes: NonZeroU64,
    ) -> Result<super::super::handles::PluginBufferSnapshot, PluginHostImportError> {
        self.snapshot
            .authorize(PluginCapabilityRef::BufferObserve)?;
        let resolved = self.snapshot.handles().resolve_buffer(buffer)?;
        let snapshot = resolved.observed_snapshot().ok_or_else(|| {
            PluginBufferObservationError::MissingSnapshot {
                handle: buffer.shape(),
            }
        })?;
        let byte_len = u64::try_from(snapshot.byte_len()).map_err(|_source| {
            PluginBufferObservationError::TextTooLarge {
                handle: buffer.shape(),
                byte_len: u64::MAX,
                max: max_text_bytes.get(),
            }
        })?;
        if byte_len > max_text_bytes.get() {
            return Err(PluginBufferObservationError::TextTooLarge {
                handle: buffer.shape(),
                byte_len,
                max: max_text_bytes.get(),
            }
            .into());
        }
        Ok(snapshot.clone())
    }

    /// Proposes a revision-guarded buffer edit.
    ///
    /// # Errors
    ///
    /// Returns [`PluginHostImportError`] when authorization, handle resolution, provenance, or
    /// queuing fails.
    pub fn propose_buffer_edit(
        &mut self,
        buffer: PluginBufferHandle,
        edit: BufferEdit,
    ) -> Result<(), PluginHostImportError> {
        self.snapshot
            .authorize(PluginCapabilityRef::BufferProposeEdit)?;
        let resolved = self.snapshot.handles().resolve_buffer(buffer)?;
        let base_revision = resolved.observed_revision().ok_or_else(|| {
            PluginHostImportError::MissingObservedRevision {
                handle: buffer.shape(),
            }
        })?;
        self.effects
            .propose_buffer_edit(PluginBufferEditProposal::from_observed_revision(
                resolved.target(),
                edit,
                base_revision,
            ))?;
        Ok(())
    }

    /// Queues a workspace observation for later filesystem-policy execution.
    ///
    /// # Errors
    ///
    /// Returns [`PluginHostImportError`] when the workspace handle, grant, or queue rejects it.
    pub fn queue_workspace_observe(
        &mut self,
        path: WorkspacePathRef<'_>,
    ) -> Result<(), PluginHostImportError> {
        let access = self.snapshot.authorize_workspace_read(path)?;
        self.workspace_io.push_read(access)?;
        Ok(())
    }

    /// Reads a workspace path through filesystem policy for direct guest delivery.
    ///
    /// This does not enqueue deferred workspace I/O. Authorization and malformed paths are import
    /// failures; filesystem-policy read failures are closed guest-domain rejections.
    ///
    /// # Errors
    ///
    /// Returns [`PluginHostImportError`] when the captured grants or per-update request budget
    /// reject the path.
    #[allow(dead_code)]
    pub(in crate::plugin) fn observe_workspace(
        &mut self,
        path: WorkspacePathRef<'_>,
        filesystem: &FilesystemConfig,
    ) -> Result<PluginWorkspaceObserveOutcome, PluginHostImportError> {
        let access = self.snapshot.authorize_workspace_read(path)?;
        Ok(self.workspace_io.observe_existing(access, filesystem)?)
    }

    /// Reserves a workspace observation task for later host-owned filesystem-policy execution.
    ///
    /// The returned proof carries the stable task handle and the path that was authorized for the
    /// resource. Filesystem work is discarded if the guest update fails before returning
    /// successfully.
    ///
    /// # Errors
    ///
    /// Returns [`PluginHostImportError`] when captured grants, the per-update request budget, or
    /// the host task queue reject the path.
    #[allow(dead_code)]
    pub(in crate::plugin) fn queue_workspace_observe_task(
        &mut self,
        path: WorkspacePathRef<'_>,
        queue: &mut PluginWorkspaceObserveTaskQueue,
    ) -> Result<PluginWorkspaceObserveTaskResourceAuthority, PluginHostImportError> {
        let access = self.snapshot.authorize_workspace_read(path)?;
        let handle = self
            .workspace_observe_tasks
            .reserve_charged(access, &mut self.workspace_io, queue)
            .map_err(PluginHostImportError::from)?;
        Ok(PluginWorkspaceObserveTaskResourceAuthority::new(
            handle,
            WorkspacePath::from_ref(path),
        ))
    }

    /// Returns whether the active update has started but not yet sealed this task.
    #[must_use]
    pub(in crate::plugin) fn has_pending_workspace_observe_task(
        &self,
        handle: &PluginWorkspaceObserveTaskHandle,
    ) -> bool {
        self.workspace_observe_tasks.contains_handle(handle)
    }

    /// Removes an uncommitted task request after the guest drops its resource.
    pub(in crate::plugin) fn remove_pending_workspace_observe_task(
        &mut self,
        handle: &PluginWorkspaceObserveTaskHandle,
    ) -> bool {
        self.workspace_observe_tasks.remove_handle(handle)
    }

    /// Removes uncommitted task requests during runtime cancellation.
    pub(in crate::plugin) fn cancel_pending_workspace_observe_tasks(
        &mut self,
        identity: &PluginIdentity,
    ) -> usize {
        self.workspace_observe_tasks.cancel_identity(identity)
    }

    /// Revalidates the capability that keeps an existing workspace-observe task resource live.
    ///
    /// This does not spend request budget. The task resource is an inert handle; the current
    /// update snapshot must still authorize observing the original path before poll or take can
    /// inspect queue state.
    pub(in crate::plugin) fn authorize_workspace_observe_task_resource(
        &self,
        path: WorkspacePathRef<'_>,
    ) -> Result<(), PluginHostImportError> {
        self.snapshot
            .authorize(PluginCapabilityRef::WorkspaceObserve(path))?;
        Ok(())
    }

    /// Queues a workspace artifact write for later filesystem-policy execution.
    ///
    /// # Errors
    ///
    /// Returns [`PluginHostImportError`] when the workspace handle, grant, payload, or queue
    /// rejects it.
    pub fn queue_workspace_artifact_write(
        &mut self,
        path: WorkspacePathRef<'_>,
        bytes: impl Into<Vec<u8>>,
    ) -> Result<(), PluginHostImportError> {
        let access = self.snapshot.authorize_workspace_write(path)?;
        self.workspace_io.push_write(access, bytes)?;
        Ok(())
    }

    /// Seals effects after a successful guest return.
    #[must_use]
    pub fn seal(self) -> PluginHostImportBatches {
        PluginHostImportBatches::new(
            self.effects.seal(),
            self.workspace_io.seal(),
            self.workspace_observe_tasks.seal(),
        )
    }

    /// Discards effects after a failed guest update.
    #[must_use]
    pub fn discard(self) -> PluginHostImportDiscardReport {
        PluginHostImportDiscardReport::new(
            self.effects.discard(),
            self.workspace_io.discard(),
            self.workspace_observe_tasks.discard(),
        )
    }
}

impl Debug for PluginHostImportSession {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("PluginHostImportSession")
            .field("snapshot", &self.snapshot)
            .field("effects", &self.effects)
            .field("workspace_io", &self.workspace_io)
            .field("workspace_observe_tasks", &self.workspace_observe_tasks)
            .finish()
    }
}