strop-engine 0.32.0

strop editor engine: documents, grammar, services and sessions — no frontend UI dependencies
use super::*;
use crate::editor::directory::DirectoryTask;
impl Editor {
    pub fn filename_draft(&self, document: DocumentId) -> Option<&Draft> {
        self.docs
            .get(document)?
            .directory_metadata_ref()?
            .draft
            .as_ref()
    }
    pub(crate) fn sync_filename_draft(&mut self, document: DocumentId) {
        let Some(doc) = self.docs.get_mut(document) else {
            return;
        };
        let super::super::super::DocumentSource::Directory(source) = &mut doc.source else {
            return;
        };
        let Some(draft) = source.draft.as_mut() else {
            return;
        };
        draft.sync(&doc.buf);
    }
    pub(crate) fn begin_filename_draft(&mut self) -> Result<(), String> {
        let source = self
            .directory()
            .ok_or("Edit names requires a Directory; use Space e or :browse first")?;
        if matches!(
            source.location.filesystem,
            strop_workspace::Filesystem::Container(_)
        ) {
            return Err("container filename drafts are unavailable: container mutations are read-only by policy".into());
        }
        if source.stale.is_some() {
            return Err("refresh the stale Directory before editing names".into());
        }
        if self.filesystem.blocks(&source.location) {
            return Err("filesystem operations must settle or verify before editing names".into());
        }
        self.start_directory_task(self.current(), DirectoryTask::EditNames, None)
    }
    pub(crate) fn discard_filename_draft(&mut self) -> Result<(), String> {
        let document = self.current();
        let phase = self
            .filename_draft(document)
            .ok_or("no filename draft is active")?
            .phase;
        if matches!(phase, Phase::Applying(_) | Phase::Unconfirmed(_)) {
            return Err(
                "admitted filesystem outcomes must settle or verify before discarding the draft"
                    .into(),
            );
        }
        if self.filesystem.pending.as_ref().is_some_and(|proposal| {
            proposal.ticket.key.origin == document && proposal.ticket.key.draft.is_some()
        }) {
            self.retire_filesystem_review("filename draft discarded");
        }
        if self
            .filesystem
            .preparing
            .as_ref()
            .is_some_and(|ticket| ticket.key.origin == document && ticket.key.draft.is_some())
        {
            if let Some(ticket) = self.filesystem.preparing.take() {
                if let Some(handle) = self.worker_handles.remove(&ticket.request) {
                    handle.cancel(worker::CancelReason::Dismissed);
                }
            }
            self.filesystem.preparing_copies.clear();
        }
        if let Some(doc) = self.docs.get_mut(document) {
            if let Some(draft) = doc
                .directory_metadata_mut()
                .and_then(|source| source.draft.as_mut())
            {
                draft.phase = Phase::Reloading;
            }
            doc.buf.readonly = true;
            doc.buf.dirty = false;
        }
        self.start_directory_task(document, DirectoryTask::Reload, None)
    }
    pub(crate) fn filename_copy_policy(&mut self, argument: &str) -> Result<(), String> {
        let policy = match argument {
            "stored" => CopyVersion::Stored,
            "buffer" => CopyVersion::Buffer,
            _ => return Err("use :fs copies stored or :fs copies buffer".into()),
        };
        let document = self.current();
        let draft = self
            .docs
            .get_mut(document)
            .and_then(|doc| doc.directory_metadata_mut())
            .and_then(|source| source.draft.as_mut())
            .ok_or("copy policy requires a filename draft")?;
        if !draft.editable() {
            return Err("filename draft is waiting for an admitted operation".into());
        }
        draft.intent_epoch = draft
            .intent_epoch
            .checked_add(1)
            .ok_or("filename intent identity exhausted")?;
        draft.copy_version = Some(policy);
        self.message = format!("filename draft copies use {policy:?} contents");
        Ok(())
    }
    pub(crate) fn filename_removal_policy(&mut self, argument: &str) -> Result<(), String> {
        let policy = match argument {
            "trash" => Removal::Trash,
            "permanent" => Removal::Permanent,
            _ => return Err("use :fs deletes trash or :fs deletes permanent".into()),
        };
        let document = self.current();
        let draft = self
            .docs
            .get_mut(document)
            .and_then(|doc| doc.directory_metadata_mut())
            .and_then(|source| source.draft.as_mut())
            .ok_or("deletion policy requires a filename draft")?;
        if !draft.editable() {
            return Err("filename draft is waiting for an admitted operation".into());
        }
        if policy == Removal::Trash && draft.root.filesystem != strop_workspace::Filesystem::Local {
            return Err(
                "remote Trash is unavailable; permanent removal must be chosen explicitly".into(),
            );
        }
        draft.intent_epoch = draft
            .intent_epoch
            .checked_add(1)
            .ok_or("filename intent identity exhausted")?;
        draft.removal = policy;
        self.message = format!("filename draft deletions use {policy:?}");
        Ok(())
    }
    pub(crate) fn prepare_filename_draft(&mut self, document: DocumentId) -> Result<(), String> {
        let draft = self
            .filename_draft(document)
            .cloned()
            .ok_or("no filename draft is active")?;
        if !draft.editable() {
            return Err("filename draft is waiting for its admitted operation".into());
        }
        let mut copies = HashMap::new();
        let mut seen = std::collections::HashSet::new();
        for row in &draft.geometry.rows {
            let Some(Origin::Copy(source)) = &row.origin else {
                continue;
            };
            if !seen.insert(source.location.clone()) {
                continue;
            }
            let target = crate::files::FileTarget::from_location(&source.location)
                .map_err(|error| error.to_string())?;
            let open = self
                .docs
                .iter()
                .find_map(|(_, doc)| doc.matches_target(&target).then_some(doc));
            if draft.copy_version.is_none() && open.is_some_and(|doc| doc.buf.dirty) {
                return Err("draft copies include unsaved sources; choose :fs copies stored or :fs copies buffer before :w".into());
            }
            if draft.copy_version == Some(CopyVersion::Buffer) {
                let open = open.ok_or(
                    "copy current buffer requires that source to be open; choose stored otherwise",
                )?;
                copies.insert(source.location.clone(), open.buf.snapshot());
            }
        }
        let key = FsKey {
            origin: document,
            revision: self.doc(document).buf.revision(),
            focus: self.focus_epoch,
            open_created: false,
            intents: Arc::new(Vec::new()),
            recovery: None,
            draft: Some(Stamp {
                id: draft.id,
                intent: draft.intent_epoch,
                root: draft.root.clone(),
            }),
        };
        let text = self.doc(document).buf.snapshot();
        let environment = self.filesystem.environment.clone();
        self.start_filesystem_preparation(key, copies, move |token| {
            let work = || -> Result<PreparedFilesystem, String> {
                let compiled = draft.compile(&text, &token)?;
                let batch = strop_fs::batch::prepare(&compiled.intents, &environment, &token).map_err(|error| error.to_string())?;
                for step in &batch.steps {
                    let Some(source) = step.source.as_ref() else { continue };
                    if let Some(expected) = compiled.sources.iter().find(|expected| step.intent.source.as_ref() == Some(&expected.location)) {
                        if !expected.value.as_ref().zip(source.value.as_ref()).is_some_and(|(before, now)| compile::matches_observed(before, now)) {
                            return Err(format!("filename source changed since the captured listing: {}; draft retained", expected.location.label()));
                        }
                    }
                }
                let draft_targets = compiled.targets.into_iter().map(|(row, target)| {
                    let resolved = batch.steps.iter().find(|step| step.intent.destination.as_ref() == Some(&target))
                        .and_then(|step| step.destination.as_ref()).map(|destination| destination.location.clone()).unwrap_or(target);
                    (row, resolved)
                }).collect();
                Ok(PreparedFilesystem { batch, draft_targets })
            };
            match work() {
                Ok(batch) => Outcome::Success(batch),
                Err(_) if token.is_cancelled() => Outcome::Cancelled(worker::CancelReason::Superseded),
                Err(error) => Outcome::failed(FailureKind::InvalidInput, error),
            }
        })
    }
    pub(crate) fn filename_draft_fresh(
        &self,
        document: DocumentId,
        revision: BufferRevision,
        stamp: &Stamp,
    ) -> bool {
        self.docs.get(document).is_some_and(|doc| {
            doc.buf.revision() == revision
                && doc.directory_metadata_ref().is_some_and(|source| {
                    source.location == stamp.root
                        && source.draft.as_ref().is_some_and(|draft| {
                            draft.id == stamp.id
                                && draft.intent_epoch == stamp.intent
                                && draft.editable()
                        })
                })
        })
    }
    pub(crate) fn freeze_filename_draft(
        &mut self,
        document: DocumentId,
        stamp: &Stamp,
        operation: WorkerId,
    ) {
        if let Some(doc) = self.docs.get_mut(document) {
            if let Some(draft) = doc
                .directory_metadata_mut()
                .and_then(|source| source.draft.as_mut())
                .filter(|draft| draft.id == stamp.id)
            {
                draft.phase = Phase::Applying(operation);
                doc.buf.readonly = true;
            }
        }
    }
    pub(crate) fn finish_filename_draft(
        &mut self,
        document: DocumentId,
        stamp: &Stamp,
        operation: WorkerId,
        committed: bool,
        unconfirmed: bool,
    ) {
        let published: std::collections::HashSet<_> = self
            .filesystem
            .history
            .iter()
            .find(|attempt| attempt.ticket.request == operation)
            .into_iter()
            .flat_map(|attempt| &attempt.receipts)
            .filter(|receipt| receipt.outcome.is_committed())
            .filter_map(|receipt| {
                receipt
                    .operation
                    .destination
                    .as_ref()
                    .map(|destination| destination.location.clone())
            })
            .collect();
        let Some(doc) = self.docs.get_mut(document) else {
            return;
        };
        let Some(draft) = doc
            .directory_metadata_mut()
            .and_then(|source| source.draft.as_mut())
            .filter(|draft| draft.id == stamp.id)
        else {
            return;
        };
        draft.phase = if unconfirmed {
            Phase::Unconfirmed(operation)
        } else if committed {
            Phase::Reloading
        } else {
            Phase::Editing
        };
        if committed && !unconfirmed {
            let originals: HashMap<_, _> = draft
                .geometry
                .rows
                .iter()
                .filter_map(|row| {
                    if let Some(Origin::Original(index)) = &row.origin {
                        draft
                            .base
                            .get(*index)
                            .map(|source| (row.id, source.location.clone()))
                    } else {
                        None
                    }
                })
                .collect();
            draft.targets.retain(|row, location| {
                published.contains(location) || originals.get(row) == Some(location)
            });
        }
        doc.buf.readonly = unconfirmed || committed;
        if committed && !unconfirmed {
            doc.buf.dirty = false;
            if let Err(error) = self.start_directory_task(document, DirectoryTask::Reload, None) {
                self.message = error;
            }
        }
    }
}