1use crate::app::types::BufferGroupId;
8#[cfg(feature = "plugins")]
9use crate::app::types::{BufferGroup, GroupLayoutNode};
10#[cfg(feature = "plugins")]
11use crate::model::event::SplitDirection;
12use crate::model::event::{BufferId, LeafId};
13#[cfg(feature = "plugins")]
14use crate::view::split::SplitViewState;
15#[cfg(feature = "plugins")]
16use fresh_core::api::BufferGroupResult;
17#[cfg(feature = "plugins")]
18use std::collections::HashMap;
19
20#[cfg(feature = "plugins")]
22#[derive(Debug, serde::Deserialize)]
23#[serde(tag = "type")]
24enum LayoutDesc {
25 #[serde(rename = "scrollable")]
26 Scrollable {
27 id: String,
28 scrollable: Option<bool>,
31 },
32 #[serde(rename = "fixed")]
33 Fixed {
34 id: String,
35 height: u16,
36 scrollable: Option<bool>,
41 },
42 #[serde(rename = "split")]
43 Split {
44 direction: String, ratio: f32,
46 first: Box<LayoutDesc>,
47 second: Box<LayoutDesc>,
48 },
49}
50
51impl super::Editor {
52 #[cfg(feature = "plugins")]
60 pub(super) fn create_buffer_group(
61 &mut self,
62 name: String,
63 mode: String,
64 layout_json: String,
65 ) -> Result<BufferGroupResult, String> {
66 use crate::view::split::{SplitNode, TabTarget};
67
68 let desc: LayoutDesc =
70 serde_json::from_str(&layout_json).map_err(|e| format!("Invalid layout: {}", e))?;
71
72 let group_id = BufferGroupId(self.active_window_mut().next_buffer_group_id);
74 self.active_window_mut().next_buffer_group_id += 1;
75
76 let mut panel_buffers: HashMap<String, BufferId> = HashMap::new();
78 let mut panel_splits: HashMap<String, LeafId> = HashMap::new();
79 let layout = self.build_group_layout(&desc, &mode, &mut panel_buffers)?;
80
81 let inner_tree = self.build_split_tree(&layout, &mut panel_splits)?;
83
84 let active_inner_leaf = find_first_scrollable_leaf(&layout, &panel_splits)
86 .or_else(|| panel_splits.values().next().copied())
87 .ok_or("No panels in layout")?;
88
89 let group_leaf_id = LeafId(
92 self.windows
93 .get_mut(&self.active_window)
94 .and_then(|w| w.split_manager_mut())
95 .expect("active window must have a populated split layout")
96 .allocate_split_id(),
97 );
98
99 let grouped_node = SplitNode::Grouped {
101 split_id: group_leaf_id,
102 name: name.clone(),
103 layout: Box::new(inner_tree),
104 active_inner_leaf,
105 };
106 self.active_window_mut()
107 .grouped_subtrees
108 .insert(group_leaf_id, grouped_node);
109
110 let (tw, th) = (self.terminal_width, self.terminal_height);
112 for (panel_name, leaf_id) in &panel_splits {
113 let buffer_id = *panel_buffers
114 .get(panel_name)
115 .ok_or(format!("Panel '{}' has no buffer", panel_name))?;
116 let mut vs = SplitViewState::with_buffer(tw, th, buffer_id);
117 vs.suppress_chrome = true;
120 vs.hide_tilde = true;
121 if let Some(bs) = vs.keyed_states.get_mut(&buffer_id) {
122 bs.show_line_numbers = false;
123 bs.highlight_current_line = false;
124 }
125 self.windows
126 .get_mut(&self.active_window)
127 .and_then(|w| w.split_view_states_mut())
128 .expect("active window must have a populated split layout")
129 .insert(*leaf_id, vs);
130 }
131
132 for buffer_id in panel_buffers.values() {
135 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(buffer_id) {
136 meta.hidden_from_tabs = true;
137 }
138 }
139
140 let hidden_panel_ids: Vec<BufferId> = panel_buffers.values().copied().collect();
147 let panel_leaf_ids: std::collections::HashSet<LeafId> =
148 panel_splits.values().copied().collect();
149 for (leaf_id, vs) in self
150 .windows
151 .get_mut(&self.active_window)
152 .and_then(|w| w.split_view_states_mut())
153 .expect("active window must have a populated split layout")
154 .iter_mut()
155 {
156 if panel_leaf_ids.contains(leaf_id) {
157 continue;
159 }
160 vs.open_buffers.retain(|t| match t {
161 TabTarget::Buffer(b) => !hidden_panel_ids.contains(b),
162 TabTarget::Group(_) => true,
163 });
164 vs.keyed_states
165 .retain(|bid, _| !hidden_panel_ids.contains(bid));
166 }
167
168 let current_split_id = self
172 .windows
173 .get(&self.active_window)
174 .and_then(|w| w.buffers.splits())
175 .map(|(mgr, _)| mgr)
176 .expect("active window must have a populated split layout")
177 .active_split();
178 if let Some(current_vs) = self
179 .windows
180 .get_mut(&self.active_window)
181 .and_then(|w| w.split_view_states_mut())
182 .expect("active window must have a populated split layout")
183 .get_mut(¤t_split_id)
184 {
185 current_vs.add_group(group_leaf_id);
186 current_vs.set_active_group_tab(group_leaf_id);
187 current_vs.focused_group_leaf = Some(active_inner_leaf);
188 }
189
190 let group = BufferGroup {
192 id: group_id,
193 name: name.clone(),
194 mode,
195 layout,
196 panel_buffers: panel_buffers.clone(),
197 panel_splits,
198 representative_split: Some(group_leaf_id),
199 };
200
201 for buffer_id in panel_buffers.values() {
203 self.active_window_mut()
204 .buffer_to_group
205 .insert(*buffer_id, group_id);
206 }
207
208 self.active_window_mut()
209 .buffer_groups
210 .insert(group_id, group);
211
212 let panels: HashMap<String, u64> = panel_buffers
214 .iter()
215 .map(|(name, bid)| (name.clone(), bid.0 as u64))
216 .collect();
217
218 Ok(BufferGroupResult {
219 group_id: group_id.0 as u64,
220 panels,
221 })
222 }
223
224 #[cfg(feature = "plugins")]
227 fn build_split_tree(
228 &mut self,
229 node: &GroupLayoutNode,
230 panel_splits: &mut HashMap<String, crate::model::event::LeafId>,
231 ) -> Result<crate::view::split::SplitNode, String> {
232 use crate::model::event::LeafId;
233 use crate::view::split::SplitNode;
234
235 match node {
236 GroupLayoutNode::Scrollable {
237 id,
238 buffer_id: Some(bid),
239 ..
240 }
241 | GroupLayoutNode::Fixed {
242 id,
243 buffer_id: Some(bid),
244 ..
245 } => {
246 let split_id = self
247 .windows
248 .get_mut(&self.active_window)
249 .and_then(|w| w.split_manager_mut())
250 .expect("active window must have a populated split layout")
251 .allocate_split_id();
252 panel_splits.insert(id.clone(), LeafId(split_id));
253 Ok(SplitNode::leaf(*bid, split_id))
254 }
255 GroupLayoutNode::Scrollable {
256 buffer_id: None, ..
257 }
258 | GroupLayoutNode::Fixed {
259 buffer_id: None, ..
260 } => Err("Layout leaf has no buffer_id".to_string()),
261 GroupLayoutNode::Split {
262 direction,
263 ratio,
264 first,
265 second,
266 } => {
267 let first_node = self.build_split_tree(first, panel_splits)?;
268 let second_node = self.build_split_tree(second, panel_splits)?;
269 let split_id = self
270 .windows
271 .get_mut(&self.active_window)
272 .and_then(|w| w.split_manager_mut())
273 .expect("active window must have a populated split layout")
274 .allocate_split_id();
275 let mut split =
276 SplitNode::split(*direction, first_node, second_node, *ratio, split_id);
277 let fixed_first_size = fixed_height_of(first);
279 let fixed_second_size = fixed_height_of(second);
280 if let SplitNode::Split {
281 fixed_first,
282 fixed_second,
283 ..
284 } = &mut split
285 {
286 *fixed_first = fixed_first_size;
287 *fixed_second = fixed_second_size;
288 }
289 Ok(split)
290 }
291 }
292 }
293
294 #[cfg(feature = "plugins")]
296 fn build_group_layout(
297 &mut self,
298 desc: &LayoutDesc,
299 mode: &str,
300 panel_buffers: &mut HashMap<String, BufferId>,
301 ) -> Result<GroupLayoutNode, String> {
302 match desc {
303 LayoutDesc::Scrollable { id, scrollable } => {
304 let scrollable = scrollable.unwrap_or(true);
305 let buffer_id = self.active_window_mut().create_virtual_buffer(
306 format!("*{}*", id),
307 mode.to_string(),
308 true,
309 );
310 if let Some(state) = self
311 .windows
312 .get_mut(&self.active_window)
313 .map(|w| &mut w.buffers)
314 .expect("active window present")
315 .get_mut(&buffer_id)
316 {
317 state.show_cursors = false;
318 state.editing_disabled = true;
319 state.scrollable = scrollable;
320 state.margins.configure_for_line_numbers(false);
321 }
322 panel_buffers.insert(id.clone(), buffer_id);
323 Ok(GroupLayoutNode::Scrollable {
324 id: id.clone(),
325 buffer_id: Some(buffer_id),
326 split_id: None,
327 })
328 }
329 LayoutDesc::Fixed {
330 id,
331 height,
332 scrollable,
333 } => {
334 let scrollable = scrollable.unwrap_or(false);
335 let buffer_id = self.active_window_mut().create_virtual_buffer(
336 format!("*{}*", id),
337 mode.to_string(),
338 true,
339 );
340 if let Some(state) = self
341 .windows
342 .get_mut(&self.active_window)
343 .map(|w| &mut w.buffers)
344 .expect("active window present")
345 .get_mut(&buffer_id)
346 {
347 state.show_cursors = false;
348 state.editing_disabled = true;
349 state.scrollable = scrollable;
350 state.margins.configure_for_line_numbers(false);
351 }
352 panel_buffers.insert(id.clone(), buffer_id);
353 Ok(GroupLayoutNode::Fixed {
354 id: id.clone(),
355 height: *height,
356 buffer_id: Some(buffer_id),
357 split_id: None,
358 })
359 }
360 LayoutDesc::Split {
361 direction,
362 ratio,
363 first,
364 second,
365 } => {
366 let dir = if direction == "h" {
367 SplitDirection::Vertical } else {
369 SplitDirection::Horizontal
370 };
371 let first_node = self.build_group_layout(first, mode, panel_buffers)?;
372 let second_node = self.build_group_layout(second, mode, panel_buffers)?;
373 Ok(GroupLayoutNode::Split {
374 direction: dir,
375 ratio: *ratio,
376 first: Box::new(first_node),
377 second: Box::new(second_node),
378 })
379 }
380 }
381 }
382
383 #[cfg(feature = "plugins")]
385 pub(super) fn set_panel_content(
386 &mut self,
387 group_id: usize,
388 panel_name: String,
389 entries: Vec<fresh_core::text_property::TextPropertyEntry>,
390 ) {
391 let bg_id = BufferGroupId(group_id);
392 let buffer_id = self
393 .active_window_mut()
394 .buffer_groups
395 .get(&bg_id)
396 .and_then(|g| g.panel_buffers.get(&panel_name).copied());
397
398 if let Some(buffer_id) = buffer_id {
399 if let Err(e) = self.set_virtual_buffer_content(buffer_id, entries) {
400 tracing::error!("Failed to set panel '{}' content: {}", panel_name, e);
401 }
402 } else {
403 tracing::warn!("Panel '{}' not found in group {}", panel_name, group_id);
404 }
405 }
406
407 pub(super) fn close_buffer_group(&mut self, group_id: usize) {
410 use crate::view::split::TabTarget;
411 let bg_id = BufferGroupId(group_id);
412 if let Some(group) = self.active_window_mut().buffer_groups.remove(&bg_id) {
413 for buffer_id in group.panel_buffers.values() {
415 self.active_window_mut().buffer_to_group.remove(buffer_id);
416 }
417
418 if let Some(group_leaf_id) = group.representative_split {
420 self.active_window_mut()
422 .grouped_subtrees
423 .remove(&group_leaf_id);
424 for vs in self
427 .windows
428 .get_mut(&self.active_window)
429 .and_then(|w| w.split_view_states_mut())
430 .expect("active window must have a populated split layout")
431 .values_mut()
432 {
433 vs.open_buffers
434 .retain(|t| *t != TabTarget::Group(group_leaf_id));
435 vs.remove_group_from_history(group_leaf_id);
436 if vs.active_group_tab == Some(group_leaf_id) {
437 vs.active_group_tab = None;
438 }
439 if let Some(focused) = vs.focused_group_leaf {
440 if group.panel_splits.values().any(|&l| l == focused) {
441 vs.focused_group_leaf = None;
442 }
443 }
444 }
445 }
446
447 for split_id in group.panel_splits.values() {
449 self.windows
450 .get_mut(&self.active_window)
451 .and_then(|w| w.split_view_states_mut())
452 .expect("active window must have a populated split layout")
453 .remove(split_id);
454 }
455
456 for buffer_id in group.panel_buffers.values() {
458 if let Err(e) = self.close_buffer(*buffer_id) {
459 tracing::warn!("Failed to close panel buffer {:?}: {}", buffer_id, e);
460 }
461 }
462
463 let active_split = self
466 .windows
467 .get(&self.active_window)
468 .and_then(|w| w.buffers.splits())
469 .map(|(mgr, _)| mgr)
470 .expect("active window must have a populated split layout")
471 .active_split();
472 if let Some(vs) = self
473 .windows
474 .get(&self.active_window)
475 .and_then(|w| w.buffers.splits())
476 .map(|(_, vs)| vs)
477 .expect("active window must have a populated split layout")
478 .get(&active_split)
479 {
480 if let Some(first_buf) = vs.buffer_tab_ids().next() {
481 let _ = first_buf; }
483 }
484 }
485 }
486
487 #[cfg(feature = "plugins")]
493 pub(super) fn focus_panel(&mut self, group_id: usize, panel_name: String) {
494 let bg_id = BufferGroupId(group_id);
495 let (group_leaf_id, inner_leaf) = match self.active_window_mut().buffer_groups.get(&bg_id) {
496 Some(group) => {
497 let Some(&inner) = group.panel_splits.get(&panel_name) else {
498 return;
499 };
500 let Some(leaf) = group.representative_split else {
501 return;
502 };
503 (leaf, inner)
504 }
505 None => return,
506 };
507
508 let host_split = self
510 .windows
511 .get(&self.active_window)
512 .and_then(|w| w.buffers.splits())
513 .map(|(_, vs)| vs)
514 .expect("active window must have a populated split layout")
515 .iter()
516 .find(|(_, vs)| vs.has_group(group_leaf_id))
517 .map(|(sid, _)| *sid);
518
519 if let Some(host_split) = host_split {
520 self.windows
522 .get_mut(&self.active_window)
523 .and_then(|w| w.split_manager_mut())
524 .expect("active window must have a populated split layout")
525 .set_active_split(host_split);
526 if let Some(vs) = self
527 .windows
528 .get_mut(&self.active_window)
529 .and_then(|w| w.split_view_states_mut())
530 .expect("active window must have a populated split layout")
531 .get_mut(&host_split)
532 {
533 vs.active_group_tab = Some(group_leaf_id);
534 vs.focused_group_leaf = Some(inner_leaf);
535 }
536 if let Some(crate::view::split::SplitNode::Grouped {
540 active_inner_leaf, ..
541 }) = self
542 .active_window_mut()
543 .grouped_subtrees
544 .get_mut(&group_leaf_id)
545 {
546 *active_inner_leaf = inner_leaf;
547 }
548 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
551 }
552 }
553
554 #[cfg(feature = "plugins")]
569 pub(super) fn set_buffer_group_panel_buffer(
570 &mut self,
571 group_id: usize,
572 panel_name: String,
573 new_buffer_id: BufferId,
574 ) -> bool {
575 let bg_id = BufferGroupId(group_id);
576
577 let buffer_exists = self
579 .windows
580 .get(&self.active_window)
581 .map(|w| &w.buffers)
582 .map(|b| b.get(&new_buffer_id).is_some())
583 .unwrap_or(false);
584 if !buffer_exists {
585 tracing::warn!(
586 "setBufferGroupPanelBuffer: buffer {:?} not found",
587 new_buffer_id
588 );
589 return false;
590 }
591
592 let (panel_leaf, prior_buffer_id) =
595 match self.active_window_mut().buffer_groups.get_mut(&bg_id) {
596 Some(group) => {
597 let Some(&leaf) = group.panel_splits.get(&panel_name) else {
598 tracing::warn!(
599 "setBufferGroupPanelBuffer: panel '{}' missing in group {}",
600 panel_name,
601 group_id
602 );
603 return false;
604 };
605 let prior = group
606 .panel_buffers
607 .insert(panel_name.clone(), new_buffer_id);
608 (leaf, prior)
609 }
610 None => {
611 tracing::warn!("setBufferGroupPanelBuffer: group {} not found", group_id);
612 return false;
613 }
614 };
615
616 if let Some(prior) = prior_buffer_id {
623 let still_panel = self
624 .active_window()
625 .buffer_groups
626 .get(&bg_id)
627 .map(|g| g.panel_buffers.values().any(|b| *b == prior))
628 .unwrap_or(false);
629 if !still_panel {
630 self.active_window_mut().buffer_to_group.remove(&prior);
631 }
632 }
633 self.active_window_mut()
634 .buffer_to_group
635 .insert(new_buffer_id, bg_id);
636
637 if let Some(state) = self
644 .windows
645 .get_mut(&self.active_window)
646 .map(|w| &mut w.buffers)
647 .expect("active window present")
648 .get_mut(&new_buffer_id)
649 {
650 state.scrollable = true;
651 state.editing_disabled = true;
652 state.margins.configure_for_line_numbers(false);
653 }
654
655 for node in self.active_window_mut().grouped_subtrees.values_mut() {
662 if let Some(found) = node.find_mut(panel_leaf.into()) {
663 if let crate::view::split::SplitNode::Leaf { buffer_id, .. } = found {
664 *buffer_id = new_buffer_id;
665 break;
666 }
667 }
668 }
669
670 let line_wrap = self
675 .active_window()
676 .resolve_line_wrap_for_buffer(new_buffer_id);
677 let wrap_column = self
678 .active_window()
679 .resolve_wrap_column_for_buffer(new_buffer_id);
680 let cfg = self.config.editor.clone();
681 if let Some(vs) = self
682 .windows
683 .get_mut(&self.active_window)
684 .and_then(|w| w.split_view_states_mut())
685 .expect("active window must have a populated split layout")
686 .get_mut(&panel_leaf)
687 {
688 {
693 let buf_state = vs.ensure_buffer_state(new_buffer_id);
694 buf_state.apply_config_defaults(
695 cfg.line_numbers,
696 cfg.highlight_current_line,
697 line_wrap,
698 cfg.wrap_indent,
699 wrap_column,
700 cfg.rulers,
701 cfg.scroll_offset,
702 );
703 buf_state.show_line_numbers = false;
707 buf_state.highlight_current_line = false;
708 }
709 vs.active_buffer = new_buffer_id;
711 vs.layout_dirty = true;
712 }
713
714 if let Some(meta) = self
717 .active_window_mut()
718 .buffer_metadata
719 .get_mut(&new_buffer_id)
720 {
721 meta.hidden_from_tabs = true;
722 }
723
724 tracing::info!(
725 "setBufferGroupPanelBuffer: group {} panel '{}' {:?} -> {:?}",
726 group_id,
727 panel_name,
728 prior_buffer_id,
729 new_buffer_id
730 );
731 true
732 }
733
734 pub(crate) fn activate_group_tab(&mut self, split_id: LeafId, group_leaf: LeafId) {
742 let Some(crate::view::split::SplitNode::Grouped {
744 active_inner_leaf, ..
745 }) = self.active_window().grouped_subtrees.get(&group_leaf)
746 else {
747 return;
748 };
749 let inner_leaf = *active_inner_leaf;
750
751 if self
756 .windows
757 .get(&self.active_window)
758 .and_then(|w| w.buffers.splits())
759 .map(|(mgr, _)| mgr)
760 .expect("active window must have a populated split layout")
761 .active_split()
762 != split_id
763 {
764 self.active_window_mut()
765 .promote_preview_if_not_in_split(split_id);
766 if self.active_window_mut().key_context
767 == crate::input::keybindings::KeyContext::FileExplorer
768 {
769 self.active_window_mut().key_context =
770 crate::input::keybindings::KeyContext::Normal;
771 }
772 self.windows
773 .get_mut(&self.active_window)
774 .and_then(|w| w.split_manager_mut())
775 .expect("active window must have a populated split layout")
776 .set_active_split(split_id);
777 }
778
779 if let Some(vs) = self
784 .windows
785 .get_mut(&self.active_window)
786 .and_then(|w| w.split_view_states_mut())
787 .expect("active window must have a populated split layout")
788 .get_mut(&split_id)
789 {
790 vs.active_group_tab = Some(group_leaf);
791 vs.focused_group_leaf = Some(inner_leaf);
792 }
793 }
794
795 pub(crate) fn grouped_split_ratio(
799 &self,
800 container: crate::model::event::ContainerId,
801 ) -> Option<f32> {
802 self.active_window().grouped_split_ratio(container)
803 }
804
805 pub(crate) fn set_grouped_split_ratio(
809 &mut self,
810 container: crate::model::event::ContainerId,
811 new_ratio: f32,
812 ) -> bool {
813 self.active_window_mut()
814 .set_grouped_split_ratio(container, new_ratio)
815 }
816
817 pub(crate) fn close_buffer_group_by_leaf(&mut self, group_leaf: LeafId) {
819 let bg_id_opt = self
822 .active_window_mut()
823 .buffer_groups
824 .iter()
825 .find(|(_, g)| g.representative_split == Some(group_leaf))
826 .map(|(id, _)| id.0);
827
828 if let Some(bg_id) = bg_id_opt {
829 self.close_buffer_group(bg_id);
830 }
831 }
832}
833
834impl crate::app::window::Window {
835 pub fn grouped_split_ratio(&self, container: crate::model::event::ContainerId) -> Option<f32> {
839 use crate::view::split::SplitNode;
840 for node in self.grouped_subtrees.values() {
841 if let Some(SplitNode::Split { ratio, .. }) = node.find(container.into()) {
842 return Some(*ratio);
843 }
844 }
845 None
846 }
847
848 pub fn set_grouped_split_ratio(
852 &mut self,
853 container: crate::model::event::ContainerId,
854 new_ratio: f32,
855 ) -> bool {
856 use crate::view::split::SplitNode;
857 for node in self.grouped_subtrees.values_mut() {
858 if let Some(SplitNode::Split { ratio, .. }) = node.find_mut(container.into()) {
859 *ratio = new_ratio.clamp(0.1, 0.9);
860 return true;
861 }
862 }
863 false
864 }
865
866 pub fn is_non_scrollable_buffer(&self, buffer_id: BufferId) -> bool {
870 self.buffers.get(&buffer_id).is_some_and(|s| !s.scrollable)
871 }
872}
873
874#[cfg(feature = "plugins")]
876fn fixed_height_of(node: &GroupLayoutNode) -> Option<u16> {
877 match node {
878 GroupLayoutNode::Fixed { height, .. } => Some(*height),
879 _ => None,
880 }
881}
882
883#[cfg(feature = "plugins")]
888fn find_first_scrollable_name(node: &GroupLayoutNode) -> Option<String> {
889 match node {
890 GroupLayoutNode::Scrollable { id, .. } => Some(id.clone()),
891 GroupLayoutNode::Fixed { .. } => None,
892 GroupLayoutNode::Split { first, second, .. } => {
893 find_first_scrollable_name(first).or_else(|| find_first_scrollable_name(second))
894 }
895 }
896}
897
898#[cfg(feature = "plugins")]
900fn find_first_scrollable_leaf(
901 node: &GroupLayoutNode,
902 panel_splits: &HashMap<String, LeafId>,
903) -> Option<LeafId> {
904 find_first_scrollable_name(node).and_then(|name| panel_splits.get(&name).copied())
905}