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    pub(crate) fn collection_open_source(&mut self) {
17        let id = self.current();
18        let cursor_line = self.buf().line_of(self.head());
19        let cursor_col = self.buf().col_of(self.head());
20        let Some(collection) = self.collections.get(&id) else {
21            return;
22        };
23        let mut target: Option<(DocumentId, usize)> = None;
24        for excerpt in &collection.excerpts {
25            if cursor_line == excerpt.view_line {
26                // header row: the file, at this excerpt's first line
27                target = Some((excerpt.source, excerpt.start));
28                break;
29            }
30            if cursor_line > excerpt.view_line
31                && cursor_line <= excerpt.view_line + excerpt.view_lines
32            {
33                // body row: same line-in-excerpt, same column
34                let Some(source) = self.docs.get(excerpt.source) else {
35                    break;
36                };
37                let source_line =
38                    source.buf.line_of(excerpt.start) + (cursor_line - excerpt.view_line - 1);
39                let line = source_line.min(source.buf.len_lines().saturating_sub(1));
40                target = Some((
41                    excerpt.source,
42                    source
43                        .buf
44                        .clamp_boundary(source.buf.line_start(line).saturating_add(cursor_col)),
45                ));
46                break;
47            }
48        }
49        let Some((document, head)) = target else {
50            self.message = "not on an excerpt".into();
51            return;
52        };
53        if self.docs.get(document).is_none() {
54            self.message = "collection: that source was closed".into();
55            return;
56        }
57        self.push_jump();
58        self.jump_land(document, head);
59        self.lsp_maybe_attach();
60    }
61}
62
63impl Editor {
64    /// `]f` / `[f` in a collection: next / previous file card (0049 §5's
65    /// excerpt navigation through the command registry).
66    pub fn collection_file_step_pub(&mut self, forward: bool) {
67        self.collection_file_step(forward);
68    }
69
70    pub(crate) fn collection_file_step(&mut self, forward: bool) {
71        let id = self.current();
72        let Some(collection) = self.collections.get(&id) else {
73            self.message = "file cards live in collections".into();
74            return;
75        };
76        let caret = self.buf().line_of(self.head());
77        let mut tops: Vec<usize> = Vec::new();
78        for (row, kind) in collection.rows.iter().enumerate() {
79            if matches!(kind, CollectionRow::CardTop(_)) {
80                tops.push(row);
81            }
82        }
83        let target = if forward {
84            tops.iter().copied().find(|row| *row > caret)
85        } else {
86            tops.iter().copied().rev().find(|row| *row < caret)
87        };
88        let Some(row) = target else {
89            self.message = if forward { "last card" } else { "first card" }.into();
90            return;
91        };
92        self.push_jump();
93        self.set_head(self.buf().line_start(row));
94        self.clamp_cursor();
95        self.scroll_to_cursor(self.view_rows());
96    }
97}
98
99impl Editor {
100    pub fn collection_excerpt_step(&mut self, forward: bool) {
101        let Some(collection) = self.collections.get(&self.current()) else {
102            self.message = "excerpt navigation belongs to a collection".into();
103            return;
104        };
105        let after = collection
106            .excerpts
107            .partition_point(|excerpt| excerpt.view_start <= self.head());
108        let index = if forward {
109            Some(after)
110        } else {
111            after.checked_sub(2)
112        };
113        let target = index
114            .and_then(|index| collection.excerpts.get(index))
115            .map(|excerpt| excerpt.view_start);
116        let Some(target) = target else {
117            self.message = if forward {
118                "last excerpt"
119            } else {
120                "first excerpt"
121            }
122            .into();
123            return;
124        };
125        self.push_jump();
126        self.set_head(target);
127        self.place_jump_target();
128    }
129}