1use crate::app::types::{BufferGroup, BufferGroupId, GroupLayoutNode};
8use crate::model::event::{BufferId, LeafId, SplitDirection};
9use crate::view::split::SplitViewState;
10use fresh_core::api::BufferGroupResult;
11use std::collections::HashMap;
12
13#[derive(Debug, serde::Deserialize)]
15#[serde(tag = "type")]
16enum LayoutDesc {
17 #[serde(rename = "scrollable")]
18 Scrollable {
19 id: String,
20 scrollable: Option<bool>,
23 },
24 #[serde(rename = "fixed")]
25 Fixed {
26 id: String,
27 height: u16,
28 scrollable: Option<bool>,
33 },
34 #[serde(rename = "split")]
35 Split {
36 direction: String, ratio: f32,
38 first: Box<LayoutDesc>,
39 second: Box<LayoutDesc>,
40 },
41}
42
43impl super::Editor {
44 pub(super) fn create_buffer_group(
52 &mut self,
53 name: String,
54 mode: String,
55 layout_json: String,
56 ) -> Result<BufferGroupResult, String> {
57 use crate::view::split::{SplitNode, TabTarget};
58
59 let desc: LayoutDesc =
61 serde_json::from_str(&layout_json).map_err(|e| format!("Invalid layout: {}", e))?;
62
63 let group_id = BufferGroupId(self.active_window_mut().next_buffer_group_id);
65 self.active_window_mut().next_buffer_group_id += 1;
66
67 let mut panel_buffers: HashMap<String, BufferId> = HashMap::new();
69 let mut panel_splits: HashMap<String, LeafId> = HashMap::new();
70 let layout = self.build_group_layout(&desc, &mode, &mut panel_buffers)?;
71
72 let inner_tree = self.build_split_tree(&layout, &mut panel_splits)?;
74
75 let active_inner_leaf = find_first_scrollable_leaf(&layout, &panel_splits)
77 .or_else(|| panel_splits.values().next().copied())
78 .ok_or("No panels in layout")?;
79
80 let group_leaf_id = LeafId(
83 self.windows
84 .get_mut(&self.active_window)
85 .and_then(|w| w.split_manager_mut())
86 .expect("active window must have a populated split layout")
87 .allocate_split_id(),
88 );
89
90 let grouped_node = SplitNode::Grouped {
92 split_id: group_leaf_id,
93 name: name.clone(),
94 layout: Box::new(inner_tree),
95 active_inner_leaf,
96 };
97 self.active_window_mut()
98 .grouped_subtrees
99 .insert(group_leaf_id, grouped_node);
100
101 let (tw, th) = (self.terminal_width, self.terminal_height);
103 for (panel_name, leaf_id) in &panel_splits {
104 let buffer_id = *panel_buffers
105 .get(panel_name)
106 .ok_or(format!("Panel '{}' has no buffer", panel_name))?;
107 let mut vs = SplitViewState::with_buffer(tw, th, buffer_id);
108 vs.suppress_chrome = true;
111 vs.hide_tilde = true;
112 if let Some(bs) = vs.keyed_states.get_mut(&buffer_id) {
113 bs.show_line_numbers = false;
114 bs.highlight_current_line = false;
115 }
116 self.windows
117 .get_mut(&self.active_window)
118 .and_then(|w| w.split_view_states_mut())
119 .expect("active window must have a populated split layout")
120 .insert(*leaf_id, vs);
121 }
122
123 for buffer_id in panel_buffers.values() {
126 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(buffer_id) {
127 meta.hidden_from_tabs = true;
128 }
129 }
130
131 let hidden_panel_ids: Vec<BufferId> = panel_buffers.values().copied().collect();
138 let panel_leaf_ids: std::collections::HashSet<LeafId> =
139 panel_splits.values().copied().collect();
140 for (leaf_id, vs) in self
141 .windows
142 .get_mut(&self.active_window)
143 .and_then(|w| w.split_view_states_mut())
144 .expect("active window must have a populated split layout")
145 .iter_mut()
146 {
147 if panel_leaf_ids.contains(leaf_id) {
148 continue;
150 }
151 vs.open_buffers.retain(|t| match t {
152 TabTarget::Buffer(b) => !hidden_panel_ids.contains(b),
153 TabTarget::Group(_) => true,
154 });
155 vs.keyed_states
156 .retain(|bid, _| !hidden_panel_ids.contains(bid));
157 }
158
159 let current_split_id = self
163 .windows
164 .get(&self.active_window)
165 .and_then(|w| w.buffers.splits())
166 .map(|(mgr, _)| mgr)
167 .expect("active window must have a populated split layout")
168 .active_split();
169 if let Some(current_vs) = self
170 .windows
171 .get_mut(&self.active_window)
172 .and_then(|w| w.split_view_states_mut())
173 .expect("active window must have a populated split layout")
174 .get_mut(¤t_split_id)
175 {
176 current_vs.add_group(group_leaf_id);
177 current_vs.set_active_group_tab(group_leaf_id);
178 current_vs.focused_group_leaf = Some(active_inner_leaf);
179 }
180
181 let group = BufferGroup {
183 id: group_id,
184 name: name.clone(),
185 mode,
186 layout,
187 panel_buffers: panel_buffers.clone(),
188 panel_splits,
189 representative_split: Some(group_leaf_id),
190 };
191
192 for buffer_id in panel_buffers.values() {
194 self.active_window_mut()
195 .buffer_to_group
196 .insert(*buffer_id, group_id);
197 }
198
199 self.active_window_mut()
200 .buffer_groups
201 .insert(group_id, group);
202
203 let panels: HashMap<String, u64> = panel_buffers
205 .iter()
206 .map(|(name, bid)| (name.clone(), bid.0 as u64))
207 .collect();
208
209 Ok(BufferGroupResult {
210 group_id: group_id.0 as u64,
211 panels,
212 })
213 }
214
215 fn build_split_tree(
218 &mut self,
219 node: &GroupLayoutNode,
220 panel_splits: &mut HashMap<String, crate::model::event::LeafId>,
221 ) -> Result<crate::view::split::SplitNode, String> {
222 use crate::model::event::LeafId;
223 use crate::view::split::SplitNode;
224
225 match node {
226 GroupLayoutNode::Scrollable {
227 id,
228 buffer_id: Some(bid),
229 ..
230 }
231 | GroupLayoutNode::Fixed {
232 id,
233 buffer_id: Some(bid),
234 ..
235 } => {
236 let split_id = self
237 .windows
238 .get_mut(&self.active_window)
239 .and_then(|w| w.split_manager_mut())
240 .expect("active window must have a populated split layout")
241 .allocate_split_id();
242 panel_splits.insert(id.clone(), LeafId(split_id));
243 Ok(SplitNode::leaf(*bid, split_id))
244 }
245 GroupLayoutNode::Scrollable {
246 buffer_id: None, ..
247 }
248 | GroupLayoutNode::Fixed {
249 buffer_id: None, ..
250 } => Err("Layout leaf has no buffer_id".to_string()),
251 GroupLayoutNode::Split {
252 direction,
253 ratio,
254 first,
255 second,
256 } => {
257 let first_node = self.build_split_tree(first, panel_splits)?;
258 let second_node = self.build_split_tree(second, panel_splits)?;
259 let split_id = self
260 .windows
261 .get_mut(&self.active_window)
262 .and_then(|w| w.split_manager_mut())
263 .expect("active window must have a populated split layout")
264 .allocate_split_id();
265 let mut split =
266 SplitNode::split(*direction, first_node, second_node, *ratio, split_id);
267 let fixed_first_size = fixed_height_of(first);
269 let fixed_second_size = fixed_height_of(second);
270 if let SplitNode::Split {
271 fixed_first,
272 fixed_second,
273 ..
274 } = &mut split
275 {
276 *fixed_first = fixed_first_size;
277 *fixed_second = fixed_second_size;
278 }
279 Ok(split)
280 }
281 }
282 }
283
284 fn build_group_layout(
286 &mut self,
287 desc: &LayoutDesc,
288 mode: &str,
289 panel_buffers: &mut HashMap<String, BufferId>,
290 ) -> Result<GroupLayoutNode, String> {
291 match desc {
292 LayoutDesc::Scrollable { id, scrollable } => {
293 let scrollable = scrollable.unwrap_or(true);
294 let buffer_id = self.active_window_mut().create_virtual_buffer(
295 format!("*{}*", id),
296 mode.to_string(),
297 true,
298 );
299 if let Some(state) = self
300 .windows
301 .get_mut(&self.active_window)
302 .map(|w| &mut w.buffers)
303 .expect("active window present")
304 .get_mut(&buffer_id)
305 {
306 state.show_cursors = false;
307 state.editing_disabled = true;
308 state.scrollable = scrollable;
309 state.margins.configure_for_line_numbers(false);
310 }
311 panel_buffers.insert(id.clone(), buffer_id);
312 Ok(GroupLayoutNode::Scrollable {
313 id: id.clone(),
314 buffer_id: Some(buffer_id),
315 split_id: None,
316 })
317 }
318 LayoutDesc::Fixed {
319 id,
320 height,
321 scrollable,
322 } => {
323 let scrollable = scrollable.unwrap_or(false);
324 let buffer_id = self.active_window_mut().create_virtual_buffer(
325 format!("*{}*", id),
326 mode.to_string(),
327 true,
328 );
329 if let Some(state) = self
330 .windows
331 .get_mut(&self.active_window)
332 .map(|w| &mut w.buffers)
333 .expect("active window present")
334 .get_mut(&buffer_id)
335 {
336 state.show_cursors = false;
337 state.editing_disabled = true;
338 state.scrollable = scrollable;
339 state.margins.configure_for_line_numbers(false);
340 }
341 panel_buffers.insert(id.clone(), buffer_id);
342 Ok(GroupLayoutNode::Fixed {
343 id: id.clone(),
344 height: *height,
345 buffer_id: Some(buffer_id),
346 split_id: None,
347 })
348 }
349 LayoutDesc::Split {
350 direction,
351 ratio,
352 first,
353 second,
354 } => {
355 let dir = if direction == "h" {
356 SplitDirection::Vertical } else {
358 SplitDirection::Horizontal
359 };
360 let first_node = self.build_group_layout(first, mode, panel_buffers)?;
361 let second_node = self.build_group_layout(second, mode, panel_buffers)?;
362 Ok(GroupLayoutNode::Split {
363 direction: dir,
364 ratio: *ratio,
365 first: Box::new(first_node),
366 second: Box::new(second_node),
367 })
368 }
369 }
370 }
371
372 pub(super) fn set_panel_content(
374 &mut self,
375 group_id: usize,
376 panel_name: String,
377 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
378 ) {
379 let bg_id = BufferGroupId(group_id);
380 let buffer_id = self
381 .active_window_mut()
382 .buffer_groups
383 .get(&bg_id)
384 .and_then(|g| g.panel_buffers.get(&panel_name).copied());
385
386 if let Some(buffer_id) = buffer_id {
387 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
388 tracing::error!("Failed to set panel '{}' content: {}", panel_name, e);
389 }
390 } else {
391 tracing::warn!("Panel '{}' not found in group {}", panel_name, group_id);
392 }
393 }
394
395 pub(super) fn close_buffer_group(&mut self, group_id: usize) {
398 use crate::view::split::TabTarget;
399 let bg_id = BufferGroupId(group_id);
400 if let Some(group) = self.active_window_mut().buffer_groups.remove(&bg_id) {
401 for buffer_id in group.panel_buffers.values() {
403 self.active_window_mut().buffer_to_group.remove(buffer_id);
404 }
405
406 if let Some(group_leaf_id) = group.representative_split {
408 self.active_window_mut()
410 .grouped_subtrees
411 .remove(&group_leaf_id);
412 for vs in self
415 .windows
416 .get_mut(&self.active_window)
417 .and_then(|w| w.split_view_states_mut())
418 .expect("active window must have a populated split layout")
419 .values_mut()
420 {
421 vs.open_buffers
422 .retain(|t| *t != TabTarget::Group(group_leaf_id));
423 vs.remove_group_from_history(group_leaf_id);
424 if vs.active_group_tab == Some(group_leaf_id) {
425 vs.active_group_tab = None;
426 }
427 if let Some(focused) = vs.focused_group_leaf {
428 if group.panel_splits.values().any(|&l| l == focused) {
429 vs.focused_group_leaf = None;
430 }
431 }
432 }
433 }
434
435 for split_id in group.panel_splits.values() {
437 self.windows
438 .get_mut(&self.active_window)
439 .and_then(|w| w.split_view_states_mut())
440 .expect("active window must have a populated split layout")
441 .remove(split_id);
442 }
443
444 for buffer_id in group.panel_buffers.values() {
446 if let Err(e) = self.close_buffer(*buffer_id) {
447 tracing::warn!("Failed to close panel buffer {:?}: {}", buffer_id, e);
448 }
449 }
450
451 let active_split = self
454 .windows
455 .get(&self.active_window)
456 .and_then(|w| w.buffers.splits())
457 .map(|(mgr, _)| mgr)
458 .expect("active window must have a populated split layout")
459 .active_split();
460 if let Some(vs) = self
461 .windows
462 .get(&self.active_window)
463 .and_then(|w| w.buffers.splits())
464 .map(|(_, vs)| vs)
465 .expect("active window must have a populated split layout")
466 .get(&active_split)
467 {
468 if let Some(first_buf) = vs.buffer_tab_ids().next() {
469 let _ = first_buf; }
471 }
472 }
473 }
474
475 pub(super) fn focus_panel(&mut self, group_id: usize, panel_name: String) {
481 let bg_id = BufferGroupId(group_id);
482 let (group_leaf_id, inner_leaf) = match self.active_window_mut().buffer_groups.get(&bg_id) {
483 Some(group) => {
484 let Some(&inner) = group.panel_splits.get(&panel_name) else {
485 return;
486 };
487 let Some(leaf) = group.representative_split else {
488 return;
489 };
490 (leaf, inner)
491 }
492 None => return,
493 };
494
495 let host_split = self
497 .windows
498 .get(&self.active_window)
499 .and_then(|w| w.buffers.splits())
500 .map(|(_, vs)| vs)
501 .expect("active window must have a populated split layout")
502 .iter()
503 .find(|(_, vs)| vs.has_group(group_leaf_id))
504 .map(|(sid, _)| *sid);
505
506 if let Some(host_split) = host_split {
507 self.windows
509 .get_mut(&self.active_window)
510 .and_then(|w| w.split_manager_mut())
511 .expect("active window must have a populated split layout")
512 .set_active_split(host_split);
513 if let Some(vs) = self
514 .windows
515 .get_mut(&self.active_window)
516 .and_then(|w| w.split_view_states_mut())
517 .expect("active window must have a populated split layout")
518 .get_mut(&host_split)
519 {
520 vs.active_group_tab = Some(group_leaf_id);
521 vs.focused_group_leaf = Some(inner_leaf);
522 }
523 if let Some(crate::view::split::SplitNode::Grouped {
527 active_inner_leaf, ..
528 }) = self
529 .active_window_mut()
530 .grouped_subtrees
531 .get_mut(&group_leaf_id)
532 {
533 *active_inner_leaf = inner_leaf;
534 }
535 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
538 }
539 }
540
541 pub(super) fn set_buffer_group_panel_buffer(
556 &mut self,
557 group_id: usize,
558 panel_name: String,
559 new_buffer_id: BufferId,
560 ) -> bool {
561 let bg_id = BufferGroupId(group_id);
562
563 let buffer_exists = self
565 .windows
566 .get(&self.active_window)
567 .map(|w| &w.buffers)
568 .map(|b| b.get(&new_buffer_id).is_some())
569 .unwrap_or(false);
570 if !buffer_exists {
571 tracing::warn!(
572 "setBufferGroupPanelBuffer: buffer {:?} not found",
573 new_buffer_id
574 );
575 return false;
576 }
577
578 let (panel_leaf, prior_buffer_id) =
581 match self.active_window_mut().buffer_groups.get_mut(&bg_id) {
582 Some(group) => {
583 let Some(&leaf) = group.panel_splits.get(&panel_name) else {
584 tracing::warn!(
585 "setBufferGroupPanelBuffer: panel '{}' missing in group {}",
586 panel_name,
587 group_id
588 );
589 return false;
590 };
591 let prior = group
592 .panel_buffers
593 .insert(panel_name.clone(), new_buffer_id);
594 (leaf, prior)
595 }
596 None => {
597 tracing::warn!("setBufferGroupPanelBuffer: group {} not found", group_id);
598 return false;
599 }
600 };
601
602 if let Some(prior) = prior_buffer_id {
609 let still_panel = self
610 .active_window()
611 .buffer_groups
612 .get(&bg_id)
613 .map(|g| g.panel_buffers.values().any(|b| *b == prior))
614 .unwrap_or(false);
615 if !still_panel {
616 self.active_window_mut().buffer_to_group.remove(&prior);
617 }
618 }
619 self.active_window_mut()
620 .buffer_to_group
621 .insert(new_buffer_id, bg_id);
622
623 if let Some(state) = self
630 .windows
631 .get_mut(&self.active_window)
632 .map(|w| &mut w.buffers)
633 .expect("active window present")
634 .get_mut(&new_buffer_id)
635 {
636 state.scrollable = true;
637 state.editing_disabled = true;
638 state.margins.configure_for_line_numbers(false);
639 }
640
641 for node in self.active_window_mut().grouped_subtrees.values_mut() {
648 if let Some(found) = node.find_mut(panel_leaf.into()) {
649 if let crate::view::split::SplitNode::Leaf { buffer_id, .. } = found {
650 *buffer_id = new_buffer_id;
651 break;
652 }
653 }
654 }
655
656 let line_wrap = self
661 .active_window()
662 .resolve_line_wrap_for_buffer(new_buffer_id);
663 let wrap_column = self
664 .active_window()
665 .resolve_wrap_column_for_buffer(new_buffer_id);
666 let cfg = self.config.editor.clone();
667 if let Some(vs) = self
668 .windows
669 .get_mut(&self.active_window)
670 .and_then(|w| w.split_view_states_mut())
671 .expect("active window must have a populated split layout")
672 .get_mut(&panel_leaf)
673 {
674 {
679 let buf_state = vs.ensure_buffer_state(new_buffer_id);
680 buf_state.apply_config_defaults(
681 cfg.line_numbers,
682 cfg.highlight_current_line,
683 line_wrap,
684 cfg.wrap_indent,
685 wrap_column,
686 cfg.rulers,
687 );
688 buf_state.show_line_numbers = false;
692 buf_state.highlight_current_line = false;
693 }
694 vs.active_buffer = new_buffer_id;
696 vs.layout_dirty = true;
697 }
698
699 if let Some(meta) = self
702 .active_window_mut()
703 .buffer_metadata
704 .get_mut(&new_buffer_id)
705 {
706 meta.hidden_from_tabs = true;
707 }
708
709 tracing::info!(
710 "setBufferGroupPanelBuffer: group {} panel '{}' {:?} -> {:?}",
711 group_id,
712 panel_name,
713 prior_buffer_id,
714 new_buffer_id
715 );
716 true
717 }
718
719 pub(crate) fn activate_group_tab(&mut self, split_id: LeafId, group_leaf: LeafId) {
727 let Some(crate::view::split::SplitNode::Grouped {
729 active_inner_leaf, ..
730 }) = self.active_window().grouped_subtrees.get(&group_leaf)
731 else {
732 return;
733 };
734 let inner_leaf = *active_inner_leaf;
735
736 if self
741 .windows
742 .get(&self.active_window)
743 .and_then(|w| w.buffers.splits())
744 .map(|(mgr, _)| mgr)
745 .expect("active window must have a populated split layout")
746 .active_split()
747 != split_id
748 {
749 self.active_window_mut()
750 .promote_preview_if_not_in_split(split_id);
751 if self.active_window_mut().key_context
752 == crate::input::keybindings::KeyContext::FileExplorer
753 {
754 self.active_window_mut().key_context =
755 crate::input::keybindings::KeyContext::Normal;
756 }
757 self.windows
758 .get_mut(&self.active_window)
759 .and_then(|w| w.split_manager_mut())
760 .expect("active window must have a populated split layout")
761 .set_active_split(split_id);
762 }
763
764 if let Some(vs) = self
769 .windows
770 .get_mut(&self.active_window)
771 .and_then(|w| w.split_view_states_mut())
772 .expect("active window must have a populated split layout")
773 .get_mut(&split_id)
774 {
775 vs.active_group_tab = Some(group_leaf);
776 vs.focused_group_leaf = Some(inner_leaf);
777 }
778 }
779
780 pub(crate) fn grouped_split_ratio(
784 &self,
785 container: crate::model::event::ContainerId,
786 ) -> Option<f32> {
787 self.active_window().grouped_split_ratio(container)
788 }
789
790 pub(crate) fn set_grouped_split_ratio(
794 &mut self,
795 container: crate::model::event::ContainerId,
796 new_ratio: f32,
797 ) -> bool {
798 self.active_window_mut()
799 .set_grouped_split_ratio(container, new_ratio)
800 }
801
802 pub(crate) fn close_buffer_group_by_leaf(&mut self, group_leaf: LeafId) {
804 let bg_id_opt = self
807 .active_window_mut()
808 .buffer_groups
809 .iter()
810 .find(|(_, g)| g.representative_split == Some(group_leaf))
811 .map(|(id, _)| id.0);
812
813 if let Some(bg_id) = bg_id_opt {
814 self.close_buffer_group(bg_id);
815 }
816 }
817}
818
819impl crate::app::window::Window {
820 pub fn grouped_split_ratio(&self, container: crate::model::event::ContainerId) -> Option<f32> {
824 use crate::view::split::SplitNode;
825 for node in self.grouped_subtrees.values() {
826 if let Some(SplitNode::Split { ratio, .. }) = node.find(container.into()) {
827 return Some(*ratio);
828 }
829 }
830 None
831 }
832
833 pub fn set_grouped_split_ratio(
837 &mut self,
838 container: crate::model::event::ContainerId,
839 new_ratio: f32,
840 ) -> bool {
841 use crate::view::split::SplitNode;
842 for node in self.grouped_subtrees.values_mut() {
843 if let Some(SplitNode::Split { ratio, .. }) = node.find_mut(container.into()) {
844 *ratio = new_ratio.clamp(0.1, 0.9);
845 return true;
846 }
847 }
848 false
849 }
850
851 pub fn is_non_scrollable_buffer(&self, buffer_id: BufferId) -> bool {
855 self.buffers.get(&buffer_id).is_some_and(|s| !s.scrollable)
856 }
857}
858
859fn fixed_height_of(node: &GroupLayoutNode) -> Option<u16> {
861 match node {
862 GroupLayoutNode::Fixed { height, .. } => Some(*height),
863 _ => None,
864 }
865}
866
867fn find_first_scrollable_name(node: &GroupLayoutNode) -> Option<String> {
872 match node {
873 GroupLayoutNode::Scrollable { id, .. } => Some(id.clone()),
874 GroupLayoutNode::Fixed { .. } => None,
875 GroupLayoutNode::Split { first, second, .. } => {
876 find_first_scrollable_name(first).or_else(|| find_first_scrollable_name(second))
877 }
878 }
879}
880
881fn find_first_scrollable_leaf(
883 node: &GroupLayoutNode,
884 panel_splits: &HashMap<String, LeafId>,
885) -> Option<LeafId> {
886 find_first_scrollable_name(node).and_then(|name| panel_splits.get(&name).copied())
887}