strop-engine 0.32.0

strop editor engine: documents, grammar, services and sessions — no frontend UI dependencies
use super::*;
use strop_core::id::LineIndex;
use strop_workspace::Filesystem;
impl Editor {
    pub(crate) fn filesystem_completion_context(
        &self,
        command: &str,
        argument: &str,
    ) -> Result<ResourceLocation, String> {
        if command == "rename" && !argument.contains('/') {
            if let Some(directory) = self.directory() {
                return Ok(directory.location.clone());
            }
            let source = self
                .navigation_source()
                .map_or(self.current(), |(source, _)| source);
            return self
                .doc(source)
                .file_target(&self.cwd)
                .and_then(|target| target.resource_location())
                .and_then(|location| strop_fs::parent(&location))
                .ok_or_else(|| "rename requires a filesystem source".into());
        }
        Ok(self.directory_context())
    }
    pub(crate) fn run_filesystem_ex(&mut self, argument: &str, range: Option<(usize, usize)>) {
        if let Err(error) = self.filesystem_command(argument, range) {
            self.message = error;
        }
    }
    fn filesystem_sources(
        &self,
        range: Option<(usize, usize)>,
    ) -> Result<Vec<ResourceLocation>, String> {
        if let Some(directory) = self.directory() {
            if let Some(draft) = directory.draft.as_ref() {
                let line = self.buf().line_of(self.head());
                let (first, last) = range.unwrap_or((line, line));
                if last.saturating_sub(first) >= strop_fs::batch::STEP_LIMIT {
                    return Err("selection exceeds the 512-row bound".into());
                }
                return (first..=last)
                    .map(|line| {
                        draft
                            .source(line)
                            .map(|source| source.location.clone())
                            .ok_or_else(|| "this filename row has no stored source identity".into())
                    })
                    .collect();
            }
            if directory.stale.is_some() || directory.view_revision != self.buf().revision() {
                return Err(
                    "directory is stale or edited; refresh before preparing operations".into(),
                );
            }
            let mut sources = Vec::new();
            if !directory.marked.is_empty() {
                for (name, before) in &directory.marked {
                    let location = ResourceLocation {
                        filesystem: directory.location.filesystem.clone(),
                        path: directory.location.path.join(name.as_path()),
                    };
                    let entry = directory
                        .entry_index(&location)
                        .map(|index| &directory.entries[index])
                        .ok_or("a marked entry no longer exists; refresh marks")?;
                    if &entry.observation != before {
                        return Err("a marked entry changed; refresh marks before operating".into());
                    }
                    sources.push(location);
                    if sources.len() > strop_fs::batch::STEP_LIMIT {
                        return Err("marked operation exceeds the 512-step bound".into());
                    }
                }
            } else {
                let line = self.buf().line_of(self.head());
                let (first, last) = range.unwrap_or((line, line));
                if last.saturating_sub(first) >= strop_fs::batch::STEP_LIMIT {
                    return Err("selection exceeds the 512-step bound".into());
                }
                for line in first..=last {
                    let entry = directory.entry(LineIndex::new(line)).ok_or(
                        "selection includes a directory heading or parent control, not an entry",
                    )?;
                    sources.push(directory.location_of(entry));
                }
            }
            return Ok(sources);
        }
        let source = self
            .navigation_source()
            .map_or(self.current(), |(source, _)| source);
        self.doc(source)
            .file_target(&self.cwd)
            .and_then(|target| target.resource_location())
            .map(|source| vec![source])
            .ok_or_else(|| "current view has no filesystem source".into())
    }
    fn filesystem_command(
        &mut self,
        argument: &str,
        range: Option<(usize, usize)>,
    ) -> Result<(), String> {
        if argument.is_empty() {
            self.open_filesystem_actions();
            return Ok(());
        }
        let (command, argument) = argument.split_once(' ').unwrap_or((argument, ""));
        match command {
            "terminal" => {
                if !argument.is_empty() || range.is_some() {
                    return Err(
                        "fs terminal uses the selected directory, not an operand or range".into(),
                    );
                }
                let directory = self
                    .directory()
                    .ok_or("fs terminal requires a Directory buffer")?;
                if directory.draft.is_some() {
                    return Err("finish the filename draft before opening a terminal here".into());
                }
                let line = LineIndex::new(self.buf().line_of(self.head()));
                let location = directory
                    .entry(line)
                    .filter(|entry| entry.observation.kind == strop_workspace::EntryKind::Directory)
                    .map_or_else(
                        || directory.location.clone(),
                        |entry| directory.location_of(entry),
                    );
                self.launch_terminal_at(location, "");
                return Ok(());
            }
            "search" => {
                if !argument.is_empty() || range.is_some() {
                    return Err(
                        "fs search takes the current Directory scope, not an operand or range"
                            .into(),
                    );
                }
                let root = self
                    .directory()
                    .ok_or("fs search requires a Directory buffer")?
                    .location
                    .clone();
                self.open_search_in(super::super::picker::SearchScope { root }, false);
                return Ok(());
            }
            "edit" => return self.begin_filename_draft(),
            "discard" => return self.discard_filename_draft(),
            "copies" => return self.filename_copy_policy(argument),
            "deletes" => return self.filename_removal_policy(argument),
            "refresh" => {
                if !self.refresh_directory() {
                    return Err("filesystem refresh requires a Directory buffer".into());
                }
                return Ok(());
            }
            "mark" | "unmark" => {
                if self.filename_draft(self.current()).is_some() {
                    return Err("filename drafts review all changed rows; use listing marks outside the draft".into());
                }
                let line = LineIndex::new(self.buf().line_of(self.head()));
                let current = self.current();
                let directory = self
                    .docs
                    .get_mut(current)
                    .and_then(|document| document.directory_metadata_mut())
                    .ok_or("mark requires a Directory buffer")?;
                if command == "unmark" {
                    directory.marked.clear();
                } else if !directory.toggle_mark(line) {
                    return Err("no directory entry at this position".into());
                }
                self.message = format!("{} filesystem entries marked", directory.marked.len());
                return Ok(());
            }
            "path" => {
                let sources = self.filesystem_sources(range)?;
                let mut text = String::new();
                for source in sources {
                    if !text.is_empty() {
                        text.push('\n');
                    }
                    text.push_str(&source.uri().map_err(|error| error.to_string())?);
                }
                self.set_register(Some('+'), super::super::Register::characterwise(text));
                self.message = "resource URI copied to clipboard register".into();
                return Ok(());
            }
            "operations" => {
                if self.filesystem.history.is_empty() {
                    return Err("no admitted filesystem operations in this session".into());
                }
                let mut text = String::new();
                let mut rows = Vec::new();
                for attempt in &self.filesystem.history {
                    let (body, kinds) = render::receipt(
                        attempt.ticket.request,
                        &attempt.batch,
                        &attempt.receipts,
                        attempt.warning.as_deref(),
                    );
                    text.push_str(&body);
                    rows.extend(kinds);
                }
                let mut buffer = strop_core::Buffer::from_text(&text);
                buffer.name = Some("filesystem operations".into());
                let document = self.open_temporary_output(buffer);
                self.set_filesystem_review_rows(document, rows);
                return Ok(());
            }
            "verify" | "undo" => {
                let (operation, step) = self.filesystem_receipt_key(argument)?;
                return if command == "verify" {
                    self.verify_filesystem_step(operation, step)
                } else {
                    self.recover_filesystem_step(operation, step)
                };
            }
            _ => {}
        }
        if self.filename_draft(self.current()).is_some() {
            return Err("edit filename fields and use :w for their review, or :fs discard before explicit operations".into());
        }
        if matches!(command, "create" | "mkdir") {
            if argument.is_empty() {
                return Err("creation requires an explicit destination".into());
            }
            let destination = self
                .directory_operand(argument)?
                .resource_location()
                .ok_or("creation requires a filesystem destination")?;
            return self.prepare_filesystem(
                vec![OperationIntent {
                    kind: if command == "mkdir" {
                        OperationKind::CreateDirectory
                    } else {
                        OperationKind::CreateFile
                    },
                    source: None,
                    destination: Some(destination),
                    copy_version: CopyVersion::Stored,
                    expected_content: None,
                }],
                None,
            );
        }
        let kind = match command {
            "rename" | "move" => OperationKind::Rename,
            "copy" => OperationKind::Copy,
            "trash" => OperationKind::Trash,
            "remove" => OperationKind::Remove,
            _ => return Err("filesystem actions: create, mkdir, rename, move, copy [stored|buffer], trash, remove, edit, discard, copies, deletes, mark, unmark, refresh, search, path, operations, verify, undo".into()),
        };
        let sources = self.filesystem_sources(range)?;
        let (version, explicit, argument) = if command == "copy" {
            if let Some(destination) = argument.strip_prefix("stored ") {
                (CopyVersion::Stored, true, destination)
            } else if let Some(destination) = argument.strip_prefix("buffer ") {
                (CopyVersion::Buffer, true, destination)
            } else {
                (CopyVersion::Stored, false, argument)
            }
        } else {
            (CopyVersion::Stored, true, argument)
        };
        if kind == OperationKind::Copy && !explicit {
            for source in &sources {
                let target = crate::files::FileTarget::from_location(source)
                    .map_err(|error| error.to_string())?;
                if self
                    .docs
                    .iter()
                    .any(|(_, document)| document.matches_target(&target) && document.buf.dirty)
                {
                    return Err("source has unsaved text; choose :fs copy stored DEST or :fs copy buffer DEST".into());
                }
            }
        }
        let needs_destination = matches!(kind, OperationKind::Rename | OperationKind::Copy);
        if needs_destination && argument.is_empty() {
            return Err("operation requires an explicit destination".into());
        }
        if !needs_destination && !argument.is_empty() {
            return Err("removal operates only on the explicitly selected entries".into());
        }
        let mut intents = Vec::with_capacity(sources.len());
        for source in &sources {
            if matches!(source.filesystem, Filesystem::Container(_)) {
                return Err("container filesystem operations are read-only by policy".into());
            }
            let destination = if needs_destination {
                let mut target = if command == "rename"
                    && !argument.contains('/')
                    && !argument.starts_with("file:")
                    && !argument.starts_with("container:")
                {
                    let parent = strop_fs::parent(source).ok_or("source has no parent")?;
                    ResourceLocation {
                        filesystem: parent.filesystem,
                        path: parent.path.join(argument),
                    }
                } else {
                    self.directory_operand(argument)?
                        .resource_location()
                        .ok_or("destination requires a filesystem namespace")?
                };
                if command == "move"
                    || (command == "copy" && (sources.len() > 1 || argument.ends_with('/')))
                {
                    target
                        .path
                        .push(source.path.file_name().ok_or("source has no basename")?);
                }
                Some(target)
            } else {
                None
            };
            intents.push(OperationIntent {
                kind,
                source: Some(source.clone()),
                destination,
                copy_version: version,
                expected_content: None,
            });
        }
        self.prepare_filesystem(intents, None)
    }
}