Skip to main content

strop_engine/editor/collections/
navigation.rs

1//! Source and file-card navigation.
2use super::{CollectionRow, Editor};
3use strop_core::id::DocumentId;
4
5impl Editor {
6    /// `g<Space>` in a collection, Enter on a header row, and
7    /// `:collection source` (0049 §5): open the full source under the
8    /// caret — the live document with its unsaved edits, never a disk
9    /// reload. A body row maps to the exact source position; a header
10    /// row opens the file at that excerpt's first line. The jump is
11    /// recorded so Ctrl-O returns to the collection working context.
12    pub fn collection_open_source_pub(&mut self) {
13        self.collection_open_source();
14    }
15
16    /// Shared source context for navigation actions, including a collection's
17    /// source-bearing file header. Presentation chrome never becomes a path.
18    pub(crate) fn navigation_source(&self) -> Option<(DocumentId, usize)> {
19        if let Some(source) = self.source_position(self.current(), self.head()) {
20            return Some(source);
21        }
22        let line = self.buf().line_of(self.head());
23        self.collections
24            .get(&self.current())?
25            .excerpts
26            .iter()
27            .find(|excerpt| excerpt.view_line == line)
28            .map(|excerpt| (excerpt.source, excerpt.start))
29    }
30    pub(crate) fn collection_open_source(&mut self) {
31        if !self.collections.contains_key(&self.current()) {
32            return;
33        }
34        let target = self.navigation_source();
35        let Some((document, head)) = target else {
36            self.message = "not on an excerpt".into();
37            return;
38        };
39        if self.docs.get(document).is_none() {
40            self.message = "collection: that source was closed".into();
41            return;
42        }
43        self.push_jump();
44        self.jump_land(document, head);
45        self.lsp_maybe_attach();
46    }
47}
48
49impl Editor {
50    /// `]f` / `[f` in a collection: next / previous file card (0049 §5's
51    /// excerpt navigation through the command registry).
52    pub fn collection_file_step_pub(&mut self, forward: bool) {
53        self.collection_file_step(forward);
54    }
55
56    pub(crate) fn collection_file_step(&mut self, forward: bool) {
57        let id = self.current();
58        let Some(collection) = self.collections.get(&id) else {
59            self.message = "file cards live in collections".into();
60            return;
61        };
62        let caret = self.buf().line_of(self.head());
63        let mut tops: Vec<usize> = Vec::new();
64        for (row, kind) in collection.rows.iter().enumerate() {
65            if matches!(kind, CollectionRow::CardTop(_)) {
66                tops.push(row);
67            }
68        }
69        let target = if forward {
70            tops.iter().copied().find(|row| *row > caret)
71        } else {
72            tops.iter().copied().rev().find(|row| *row < caret)
73        };
74        let Some(row) = target else {
75            self.message = if forward { "last card" } else { "first card" }.into();
76            return;
77        };
78        self.push_jump();
79        self.set_head(self.buf().line_start(row));
80        self.clamp_cursor();
81        self.scroll_to_cursor(self.view_rows());
82    }
83}
84
85impl Editor {
86    pub fn collection_excerpt_step(&mut self, forward: bool) {
87        let Some(collection) = self.collections.get(&self.current()) else {
88            self.message = "excerpt navigation belongs to a collection".into();
89            return;
90        };
91        let after = collection
92            .excerpts
93            .partition_point(|excerpt| excerpt.view_start <= self.head());
94        let index = if forward {
95            Some(after)
96        } else {
97            after.checked_sub(2)
98        };
99        let target = index
100            .and_then(|index| collection.excerpts.get(index))
101            .map(|excerpt| excerpt.view_start);
102        let Some(target) = target else {
103            self.message = if forward {
104                "last excerpt"
105            } else {
106                "first excerpt"
107            }
108            .into();
109            return;
110        };
111        self.push_jump();
112        self.set_head(target);
113        self.place_jump_target();
114    }
115}