strop_engine/editor/
panes.rs1use strop_core::id::DisplayColumn;
8use strop_core::selection::SelectionSet;
9
10use super::Editor;
11
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14pub struct Pane {
15 pub doc: strop_core::id::DocumentId,
16 pub sels: SelectionSet,
17 pub view_top: usize,
18 pub hscroll: DisplayColumn,
21 pub desired_column: Option<DisplayColumn>,
23}
24
25impl Pane {
26 pub fn reveal_column(&mut self, column: DisplayColumn, width: usize) {
30 if width == 0 {
31 return;
32 }
33 if column < self.hscroll {
34 self.hscroll = column;
35 } else if column.get() - self.hscroll.get() >= width {
36 self.hscroll = DisplayColumn::new(column.get() - (width - 1));
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
44pub enum LayoutDir {
45 Row,
46 Column,
47}
48
49impl Editor {
50 #[inline]
52 pub fn view(&self) -> &Pane {
53 &self.panes[self.active_pane]
54 }
55
56 #[inline]
57 pub fn view_mut(&mut self) -> &mut Pane {
58 &mut self.panes[self.active_pane]
59 }
60
61 pub(crate) fn split(&mut self, vertical: bool, path: Option<&str>) {
64 if let Some(path) = path {
65 self.request_user_open(path, super::io::OpenIntent::Split { vertical });
66 } else {
67 self.split_document(vertical, self.current());
68 }
69 }
70 pub(crate) fn split_document(&mut self, vertical: bool, doc: strop_core::id::DocumentId) {
71 self.cancel_pending();
74 self.cancel_open(strop_core::worker::CancelReason::Superseded);
75 let view = self.view().clone();
76 self.panes.push(if doc == view.doc {
79 view
80 } else {
81 Pane {
82 doc,
83 sels: SelectionSet::default(),
84 view_top: 0,
85 hscroll: DisplayColumn::new(0),
86 desired_column: None,
87 }
88 });
89 self.layout = if vertical {
90 LayoutDir::Row
91 } else {
92 LayoutDir::Column
93 };
94 self.active_pane = self.panes.len() - 1;
95 self.focus_epoch += 1;
96 self.discover_git();
97 self.lsp_maybe_attach();
98 }
99
100 pub(crate) fn close_pane_or_buffer(&mut self, force: bool) {
102 if self.panes.len() > 1 {
103 self.cancel_pending();
104 self.panes.remove(self.active_pane);
105 self.active_pane = self.active_pane.min(self.panes.len() - 1);
106 self.focus_epoch += 1;
107 self.cancel_open(strop_core::worker::CancelReason::OwnerClosed);
108 self.discover_git();
111 } else {
112 self.close_buffer(force);
113 }
114 }
115
116 pub(crate) fn quit_all(&mut self, force: bool) {
120 if !force {
121 let dirty = self.docs.iter().filter(|(_, d)| d.buf.dirty).count();
122 if dirty > 0 {
123 self.message = format!("{dirty} unsaved buffer(s) — :qa! to discard");
124 return;
125 }
126 }
127 while !self.docs.is_empty() {
128 self.close_buffer(force);
129 }
130 }
131
132 pub(crate) fn pane_move(&mut self, key: char) {
134 let n = self.panes.len();
135 if n < 2 {
136 self.message = "no other pane".into();
137 return;
138 }
139 let next = match (self.layout, key) {
140 (LayoutDir::Row, 'h') => self.active_pane.checked_sub(1).unwrap_or(n - 1),
141 (LayoutDir::Row, 'l') => (self.active_pane + 1) % n,
142 (LayoutDir::Column, 'k') => self.active_pane.checked_sub(1).unwrap_or(n - 1),
143 (LayoutDir::Column, 'j') => (self.active_pane + 1) % n,
144 (_, 'w') => (self.active_pane + 1) % n,
145 _ => return,
146 };
147 if next != self.active_pane {
148 self.cancel_pending();
149 }
150 self.active_pane = next; self.focus_epoch += 1;
152 self.cancel_open(strop_core::worker::CancelReason::Superseded);
153 self.discover_git();
154 self.clamp_cursor();
155 }
156}
157
158impl Editor {
159 pub(crate) fn pane_move_pub(&mut self, key: char) {
161 self.pane_move(key);
162 }
163 pub(crate) fn split_pub(&mut self, key: char) {
164 self.split(key == 'v', None);
165 }
166 pub(crate) fn pane_close_pub(&mut self) {
167 self.close_pane_or_buffer(false);
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use strop_core::Buffer;
175
176 #[test]
177 fn vsplit_shares_buffer_and_navigates() {
178 let dir = tempfile::tempdir().unwrap();
180 let a = dir.path().join("vsplit-a.rs");
181 std::fs::write(&a, "fn a() {}\nfn b() {}\n").unwrap();
182 let mut e = Editor::new(Buffer::open(a.to_str().unwrap()).unwrap());
183 e.feed_text("j"); e.feed_text(":vs<cr>");
185 assert_eq!(e.panes.len(), 2);
186 assert_eq!(e.active_pane, 1);
187 e.feed_text("gg");
189 e.feed(crate::editor::Key::CtrlW);
191 e.feed(crate::editor::Key::Char('h'));
192 assert_eq!(e.active_pane, 0);
193 assert_eq!(e.buf().line_of(e.head()), 1, "pane 1 kept its own cursor");
194 e.feed_text(":q<cr>");
196 assert_eq!(e.panes.len(), 1);
197 assert_eq!(e.docs.len(), 1);
198 }
199
200 #[test]
201 fn split_with_path_opens_other_file() {
202 let dir = tempfile::tempdir().unwrap();
203 let a = dir.path().join("split-a.rs");
204 let b = dir.path().join("split-b.rs");
205 std::fs::write(&a, "fn a() {}\n").unwrap();
206 std::fs::write(&b, "fn b() {}\n").unwrap();
207 let mut e = Editor::new(Buffer::open(a.to_str().unwrap()).unwrap());
208 e.feed_text(&format!(":vs {}<cr>", b.display()));
209 e.wait_io().unwrap();
210 assert_eq!(e.panes.len(), 2);
211 assert_eq!(e.buf().path.as_deref(), Some(b.as_path()));
212 e.feed(crate::editor::Key::CtrlW);
213 e.feed(crate::editor::Key::Char('h'));
214 assert_eq!(e.buf().path.as_deref(), Some(a.as_path()));
215 }
216}