strop-engine 0.32.0

strop editor engine: documents, grammar, services and sessions — no frontend UI dependencies
//! Source and file-card navigation.
use super::{CollectionRow, Editor};
use strop_core::id::DocumentId;

impl Editor {
    /// `g<Space>` in a collection, Enter on a header row, and
    /// `:collection source` (0049 §5): open the full source under the
    /// caret — the live document with its unsaved edits, never a disk
    /// reload. A body row maps to the exact source position; a header
    /// row opens the file at that excerpt's first line. The jump is
    /// recorded so Ctrl-O returns to the collection working context.
    pub fn collection_open_source_pub(&mut self) {
        self.collection_open_source();
    }

    /// Shared source context for navigation actions, including a collection's
    /// source-bearing file header. Presentation chrome never becomes a path.
    pub(crate) fn navigation_source(&self) -> Option<(DocumentId, usize)> {
        if let Some(source) = self.source_position(self.current(), self.head()) {
            return Some(source);
        }
        let line = self.buf().line_of(self.head());
        self.collections
            .get(&self.current())?
            .excerpts
            .iter()
            .find(|excerpt| excerpt.view_line == line)
            .map(|excerpt| (excerpt.source, excerpt.start))
    }
    pub(crate) fn collection_open_source(&mut self) {
        if !self.collections.contains_key(&self.current()) {
            return;
        }
        let target = self.navigation_source();
        let Some((document, head)) = target else {
            self.message = "not on an excerpt".into();
            return;
        };
        if self.docs.get(document).is_none() {
            self.message = "collection: that source was closed".into();
            return;
        }
        self.push_jump();
        self.jump_land(document, head);
        self.lsp_maybe_attach();
    }
}

impl Editor {
    /// `]f` / `[f` in a collection: next / previous file card (0049 §5's
    /// excerpt navigation through the command registry).
    pub fn collection_file_step_pub(&mut self, forward: bool) {
        self.collection_file_step(forward);
    }

    pub(crate) fn collection_file_step(&mut self, forward: bool) {
        let id = self.current();
        let Some(collection) = self.collections.get(&id) else {
            self.message = "file cards live in collections".into();
            return;
        };
        let caret = self.buf().line_of(self.head());
        let mut tops: Vec<usize> = Vec::new();
        for (row, kind) in collection.rows.iter().enumerate() {
            if matches!(kind, CollectionRow::CardTop(_)) {
                tops.push(row);
            }
        }
        let target = if forward {
            tops.iter().copied().find(|row| *row > caret)
        } else {
            tops.iter().copied().rev().find(|row| *row < caret)
        };
        let Some(row) = target else {
            self.message = if forward { "last card" } else { "first card" }.into();
            return;
        };
        self.push_jump();
        self.set_head(self.buf().line_start(row));
        self.clamp_cursor();
        self.scroll_to_cursor(self.view_rows());
    }
}

impl Editor {
    pub fn collection_excerpt_step(&mut self, forward: bool) {
        let Some(collection) = self.collections.get(&self.current()) else {
            self.message = "excerpt navigation belongs to a collection".into();
            return;
        };
        let after = collection
            .excerpts
            .partition_point(|excerpt| excerpt.view_start <= self.head());
        let index = if forward {
            Some(after)
        } else {
            after.checked_sub(2)
        };
        let target = index
            .and_then(|index| collection.excerpts.get(index))
            .map(|excerpt| excerpt.view_start);
        let Some(target) = target else {
            self.message = if forward {
                "last excerpt"
            } else {
                "first excerpt"
            }
            .into();
            return;
        };
        self.push_jump();
        self.set_head(target);
        self.place_jump_target();
    }
}