strop-engine 0.32.4

strop editor engine: documents, grammar, services and sessions — no frontend UI dependencies
use super::super::DocumentSource;
use super::*;
use strop_workspace::Filesystem;
fn intersects(left: &ResourceLocation, right: &ResourceLocation) -> bool {
    left.filesystem == right.filesystem
        && (left.path.starts_with(&right.path) || right.path.starts_with(&left.path))
}
impl Editor {
    pub(super) fn filesystem_admission(
        &self,
        intents: &[OperationIntent],
        own_draft: Option<DocumentId>,
    ) -> Result<(), String> {
        let local_write_pending = self.local_write_pending();
        for intent in intents {
            for location in intent.source.iter().chain(intent.destination.iter()) {
                if location.filesystem == Filesystem::Local && local_write_pending {
                    return Err("local save/save-as is still in progress; wait before preparing filesystem mutations".into());
                }
                if self.filesystem.blocks(location) {
                    return Err(format!("filesystem operation pending or unconfirmed for {}; verify its receipt first", location.label()));
                }
                if matches!(location.filesystem, Filesystem::Container(_)) {
                    return Err("container filesystem operations are read-only by policy".into());
                }
                for key in self.io.open.values() {
                    if key
                        .path
                        .resource_location()
                        .is_some_and(|opened| intersects(&opened, location))
                    {
                        return Err(format!(
                            "an open/refresh is still pending for {}",
                            location.label()
                        ));
                    }
                }
            }
            if let Some(source) = intent.source.as_ref().filter(|_| {
                matches!(
                    intent.kind,
                    OperationKind::Rename
                        | OperationKind::Trash
                        | OperationKind::Remove
                        | OperationKind::Restore
                )
            }) {
                if source.filesystem == Filesystem::Local
                    && (self.cwd.starts_with(&source.path)
                        || self
                            .state_dir
                            .as_ref()
                            .is_some_and(|state| state.starts_with(&source.path)))
                {
                    return Err(format!("{} contains an active workspace/cwd/state anchor; relocation is not authorized", source.label()));
                }
            }
            for (document, entry) in self.docs.iter() {
                let Some(location) = entry
                    .file_target(&self.cwd)
                    .and_then(|target| target.resource_location())
                else {
                    continue;
                };
                let affected = intent
                    .source
                    .iter()
                    .chain(intent.destination.iter())
                    .any(|resource| intersects(resource, &location));
                if affected
                    && own_draft != Some(document)
                    && entry
                        .directory_metadata_ref()
                        .is_some_and(|source| source.draft.is_some())
                {
                    return Err(format!(
                        "{} has an active filename draft; finish or discard it first",
                        location.label()
                    ));
                }
                if affected
                    && (self.io.save_pending_for(document)
                        || matches!(self.lsp_state.after_format.as_ref(),
                            Some(super::super::lsp::state::AfterFormat::Save { document: owner, .. }) if *owner == document)
                        || self.remote_write_blocks_refresh(document))
                {
                    return Err(format!(
                        "save/edit admission/verification is pending for {}",
                        location.label()
                    ));
                }
                if intent.destination.as_ref() == Some(&location) {
                    let vacated = intents.iter().any(|other| {
                        other.source.as_ref() == Some(&location)
                            && matches!(
                                other.kind,
                                OperationKind::Rename
                                    | OperationKind::Trash
                                    | OperationKind::Remove
                                    | OperationKind::Restore
                            )
                    });
                    if !vacated {
                        return Err(format!("destination {} is already owned by an open document, including its unsaved text", location.label()));
                    }
                }
            }
        }
        Ok(())
    }

    pub(super) fn reconcile_filesystem_step(&mut self, receipt: &StepReceipt) {
        let StepOutcome::Committed {
            destination_after, ..
        } = &receipt.outcome
        else {
            return;
        };
        self.invalidate_filesystem_completions();
        let operation = &receipt.operation;
        let source = operation.source.as_ref().map(|source| &source.location);
        let destination = operation
            .destination
            .as_ref()
            .map(|destination| &destination.location);
        let moving = matches!(
            operation.intent.kind,
            OperationKind::Rename | OperationKind::Restore
        );
        let removing = matches!(
            operation.intent.kind,
            OperationKind::Trash | OperationKind::Remove
        );
        let mut affected = Vec::new();
        if moving || removing {
            if let Some(source) = source {
                for (document, entry) in self.docs.iter() {
                    let Some(location) = entry
                        .file_target(&self.cwd)
                        .and_then(|target| target.resource_location())
                    else {
                        continue;
                    };
                    let logical = operation.intent.source.as_ref().filter(|logical| {
                        logical.filesystem == location.filesystem
                            && location.path.starts_with(&logical.path)
                    });
                    let base = if location.filesystem == source.filesystem
                        && location.path.starts_with(&source.path)
                    {
                        Some(source)
                    } else {
                        logical
                    };
                    if let Some(base) = base {
                        let target = if moving {
                            destination.and_then(|target| location.relocated(base, target))
                        } else {
                            None
                        };
                        affected.push((document, location, target));
                    }
                }
            }
        }
        for (document, old, destination) in &affected {
            self.stop_remote_follow(*document);
            self.cancel_directory_filter(*document);
            self.lsp_close_document(*document);
            self.revoke_git_requests_for(*document);
            self.blame_gutters.remove(document);
            self.analysis
                .forget(super::super::analysis::AnalysisTarget::Document(*document));
            if let Some(destination) = destination {
                if let Filesystem::Remote(endpoint) = &destination.filesystem {
                    if self.doc(*document).remote_metadata().is_some() {
                        match strop_workspace::RemoteFile::from_path(
                            endpoint.clone(),
                            destination.path.clone(),
                        ) {
                            Ok(file) => self.relocate_remote_binding(*document, file),
                            Err(error) => {
                                self.message = format!("relocation binding refused: {error}");
                                continue;
                            }
                        }
                    }
                }
                let directory = self.doc(*document).directory_metadata_ref().is_some();
                if directory {
                    let header = format!("{}\n", destination.label());
                    let end = self.doc(*document).buf.line_start(1);
                    let result = self
                        .doc_mut(*document)
                        .buf
                        .system_edit()
                        .replace(strop_core::Range::charwise(0, end), &header);
                    if let Err(error) = result {
                        self.message = format!("directory header update failed: {error}");
                    }
                    if let Some(entry) = self.docs.get_mut(*document) {
                        if let Some(source) = entry.directory_metadata_mut() {
                            source.location = destination.clone();
                        }
                        let revision = entry.buf.revision();
                        if let Some(source) = entry.directory_metadata_mut() {
                            source.view_revision = revision;
                        }
                        entry.buf.name = Some(format!("directory {}", destination.label()));
                    }
                } else if destination.filesystem == Filesystem::Local {
                    let stamp = if source == Some(old) {
                        destination_after
                            .as_ref()
                            .and_then(|value| value.modified)
                            .and_then(system_time)
                    } else {
                        None
                    };
                    if let Some(entry) = self.docs.get_mut(*document) {
                        entry
                            .buf
                            .relocate_file_binding(destination.path.clone(), stamp);
                    }
                }
            } else {
                self.revoke_remote_write(*document);
                if let Some(entry) = self.docs.get_mut(*document) {
                    entry.buf.detach_file_binding();
                    entry.buf.name = Some(format!("detached {} — original removed", old.label()));
                    entry.buf.readonly = false;
                    entry.buf.dirty = true;
                    entry.syntax_hint = Some(old.path.clone());
                    entry.source = DocumentSource::Scratch;
                }
            }
        }
        self.lsp_retire_remote_servers();
        let documents: Vec<_> = affected.iter().map(|(document, _, _)| *document).collect();
        self.invalidate_filesystem_text_review(&documents);
        let collections: Vec<_> = self
            .collections
            .iter()
            .filter(|(_, collection)| {
                collection
                    .excerpts
                    .iter()
                    .any(|excerpt| documents.contains(&excerpt.source))
            })
            .map(|(id, _)| *id)
            .collect();
        for collection in collections {
            self.collection_render_view(collection);
        }
        let touched: Vec<_> = source.into_iter().chain(destination).collect();
        let previews: Vec<_> = self
            .previews
            .keys()
            .filter(|path| {
                touched.iter().any(|location| {
                    location.filesystem == path.filesystem
                        && (path.path.starts_with(&location.path)
                            || location.path.starts_with(&path.path))
                })
            })
            .cloned()
            .collect();
        for path in previews {
            self.previews.remove(&path);
            if let Some(worker::Load::Running(ticket)) = self.preview_loads.remove(&path) {
                if let Some(handle) = self.worker_handles.remove(&ticket.request) {
                    handle.cancel(worker::CancelReason::Superseded);
                }
            }
            self.analysis
                .forget(super::super::analysis::AnalysisTarget::Preview(path));
        }
        let directories: Vec<_> = self
            .docs
            .iter()
            .filter_map(|(id, entry)| entry.directory_metadata_ref().map(|_| id))
            .collect();
        for id in directories {
            let Some(entry) = self.docs.get_mut(id) else {
                continue;
            };
            if let Some(directory) = entry.directory_metadata_mut() {
                if moving || removing {
                    if let Some(source) = source {
                        directory
                            .record_relocation(source, if moving { destination } else { None });
                    }
                    if operation.intent.source.as_ref() != source {
                        if let Some(logical) = &operation.intent.source {
                            directory.record_relocation(
                                logical,
                                if moving {
                                    operation.intent.destination.as_ref()
                                } else {
                                    None
                                },
                            );
                        }
                    }
                }
                if touched.iter().any(|location| {
                    location.filesystem == directory.location.filesystem
                        && (location.path.parent() == Some(directory.location.path.as_path())
                            || directory.location.path.starts_with(&location.path))
                }) {
                    directory.stale = Some("filesystem changed; refresh required".into());
                }
            }
        }
        self.relocate_directory_history(source, if moving { destination } else { None }, removing);
        if (moving || removing) && operation.intent.source.as_ref() != source {
            self.relocate_directory_history(
                operation.intent.source.as_ref(),
                if moving {
                    operation.intent.destination.as_ref()
                } else {
                    None
                },
                removing,
            );
        }
        for glue in self
            .picker
            .iter_mut()
            .chain(self.retained_search.iter_mut())
        {
            if let Some(context) = glue.search.as_mut() {
                context.refresh_requested = true;
            }
        }
        self.filesystem_picker_changed();
        if !self.docs.is_empty() && documents.contains(&self.current()) {
            self.discover_git();
            self.lsp_maybe_attach();
        }
    }
}
fn system_time(time: strop_workspace::FileTime) -> Option<std::time::SystemTime> {
    let base = if time.seconds >= 0 {
        std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(time.seconds as u64))?
    } else {
        std::time::UNIX_EPOCH
            .checked_sub(std::time::Duration::from_secs(time.seconds.unsigned_abs()))?
    };
    base.checked_add(std::time::Duration::from_nanos(u64::from(time.nanos)))
}