use im::OrdMap; use itertools::Itertools; use std::{ collections::{BTreeMap, VecDeque}, ops::Deref, rc::Rc, }; use crate::FrameId; #[derive(PartialEq, Eq, Debug, Clone)] pub enum TermSplit { Term { frame: FrameId, }, Horizontal { left: Rc, right: Rc, }, Vertical { top: Rc, bottom: Rc, }, Tabs { tabs: OrdMap>, }, } impl TermSplit { pub fn iter<'t>(&'t self) -> AccessibleSplitIter<'t> { AccessibleSplitIter { queue: vec![self].into(), } } fn collect_term_frames(&self, into: &mut BTreeMap) { match self { TermSplit::Term { frame } => { into.entry(frame.clone()) .and_modify(|v| *v += 1) .or_insert(1); } TermSplit::Horizontal { left, right } => { left.collect_term_frames(into); right.collect_term_frames(into); } TermSplit::Vertical { top, bottom } => { top.collect_term_frames(into); bottom.collect_term_frames(into); } TermSplit::Tabs { tabs } => { for tab in tabs.values() { tab.collect_term_frames(into); } } } } pub fn validate(&self) -> Result<(), String> { let mut frame_count: BTreeMap = BTreeMap::new(); self.collect_term_frames(&mut frame_count); let duplicate_terminal = frame_count .iter() .filter_map(|(k, v)| { if *v > 1 { Some(format!("Terminal {}", k.0)) } else { None } }) .join(", "); if !duplicate_terminal.is_empty() { Err(format!( "Attempt to create layout that duplicates reference to: {}", duplicate_terminal ))?; } Ok(()) } pub fn modify_at_pathstr(&self, pathstr: &str, mod_with: F) -> Result where F: FnOnce(&TermSplit) -> Result, { self.modify_at_pathstr_vec(&pathstr.chars().collect::>(), mod_with) } fn modify_at_pathstr_vec(&self, pathstr: &[char], mod_with: F) -> Result where F: FnOnce(&TermSplit) -> Result, { match self { TermSplit::Horizontal { left, right } => match pathstr.split_first() { None => mod_with(self), Some(('l', path_rest)) => Ok(TermSplit::Horizontal { left: left.modify_at_pathstr_vec(path_rest, mod_with)?.into(), right: right.clone() }), Some(('r', path_rest)) => Ok(TermSplit::Horizontal { left: left.clone(), right: right.modify_at_pathstr_vec(path_rest, mod_with)?.into() }), Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a horizontal split", c, path_rest.iter().collect::())) }, TermSplit::Vertical { top, bottom } => match pathstr.split_first() { None => mod_with(self), Some(('t', path_rest)) => Ok(TermSplit::Vertical { top: top.modify_at_pathstr_vec(path_rest, mod_with)?.into(), bottom: bottom.clone() }), Some(('b', path_rest)) => Ok(TermSplit::Vertical { top: top.clone(), bottom: bottom.modify_at_pathstr_vec(path_rest, mod_with)?.into() }), Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a vertical split", c, path_rest.iter().collect::())) }, TermSplit::Tabs { tabs } => { match pathstr.split_first() { None => mod_with(self), Some((c, path_rest)) => { match tabs.get(c) { None => Err(format!("Reference to tab '{}', which doesn't exist, before {} in split path", c, path_rest.iter().collect::())), Some(t) => Ok(TermSplit::Tabs { tabs: tabs.update(*c, t.modify_at_pathstr_vec(path_rest, mod_with)?.into())}), } } } }, TermSplit::Term { .. } => match pathstr.split_first() { None => mod_with(self), Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::())) }, } } pub fn get_at_pathstr(&self, pathstr: &str) -> Result<&Self, String> { self.get_at_pathstr_vec(&pathstr.chars().collect::>()) } fn get_at_pathstr_vec(&self, pathstr: &[char]) -> Result<&Self, String> { match self { TermSplit::Horizontal { left, right } => match pathstr.split_first() { None => Ok(self), Some(('l', path_rest)) => left.get_at_pathstr_vec(path_rest), Some(('r', path_rest)) => right.get_at_pathstr_vec(path_rest), Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a horizontal split", c, path_rest.iter().collect::())) }, TermSplit::Vertical { top, bottom } => match pathstr.split_first() { None => Ok(self), Some(('t', path_rest)) => top.get_at_pathstr_vec(path_rest), Some(('b', path_rest)) => bottom.get_at_pathstr_vec(path_rest), Some((c, path_rest)) => Err(format!("In split path, found {} before {}, which was unexpected for a vertical split", c, path_rest.iter().collect::())) }, TermSplit::Tabs { tabs } => match pathstr.split_first() { None => Ok(self), Some((c, path_rest)) => match tabs.get(c) { None => Err(format!("Reference to tab '{}', which doesn't exist, before {} in split path", c, path_rest.iter().collect::())), Some(t) => t.get_at_pathstr_vec(path_rest), } }, TermSplit::Term { .. } => match pathstr.split_first() { None => Ok(self), Some(_) => Err(format!("In split path, found trailing junk {} after addressing terminal", pathstr.iter().collect::())) } } } pub fn hsplit(&self, pathstr: &str, new_frame: FrameId) -> Result { let new = self.modify_at_pathstr(pathstr, move |n| { Ok(TermSplit::Horizontal { left: n.clone().into(), right: TermSplit::Term { frame: new_frame }.into(), }) })?; new.validate()?; Ok(new) } pub fn vsplit(&self, pathstr: &str, new_frame: FrameId) -> Result { let new = self.modify_at_pathstr(pathstr, move |n| { Ok(TermSplit::Vertical { top: n.clone().into(), bottom: TermSplit::Term { frame: new_frame }.into(), }) })?; new.validate()?; Ok(new) } pub fn join(&self, pathstr: &str) -> Result { self.modify_at_pathstr(pathstr, move |n| match n { TermSplit::Term { .. } => { Err("Can only join vertical or horizontal splits (or tab with one entry), not a terminal".to_owned()) } TermSplit::Horizontal { left, .. } => Ok((**left).clone()), TermSplit::Vertical { top, .. } => Ok((**top).clone()), TermSplit::Tabs { tabs } if tabs.len() != 1 => Err("Can only join tabs when only one tab left".to_owned()), TermSplit::Tabs { tabs } => Ok((**tabs.iter().next().unwrap().1).clone()), }) } pub fn swap(&self, pathstr1: &str, pathstr2: &str) -> Result { let split1 = self.get_at_pathstr(pathstr1)?; let split2 = self.get_at_pathstr(pathstr2)?; let new = self .modify_at_pathstr(pathstr1, |_t| Ok(split2.clone()))? .modify_at_pathstr(pathstr2, |_t| Ok(split1.clone()))?; // It can fail if one path nests the other. new.validate()?; Ok(new) } pub fn tabbed(&self, first_tab: char, pathstr: &str) -> Result { self.modify_at_pathstr(pathstr, |t| { Ok(TermSplit::Tabs { tabs: OrdMap::unit(first_tab, t.clone().into()), }) }) } pub fn add_tab( &self, pathstr: &str, new_tab: char, new_frame: FrameId, ) -> Result { let new = self.modify_at_pathstr(pathstr, |t| match t { TermSplit::Tabs { tabs } => Ok(TermSplit::Tabs { tabs: tabs.update(new_tab, TermSplit::Term { frame: new_frame }.into()), }), _ => Err(format!( "Tried to add tab at path {}, which isn't a tab set.", pathstr )), })?; new.validate()?; Ok(new) } } pub struct AccessibleSplitIter<'t> { queue: VecDeque<&'t TermSplit>, } impl<'t> Iterator for AccessibleSplitIter<'t> { type Item = &'t FrameId; fn next(&mut self) -> Option { loop { match self.queue.pop_back() { Some(TermSplit::Horizontal { left, right }) => { self.queue.push_back(right); self.queue.push_back(left); } Some(TermSplit::Vertical { top, bottom }) => { self.queue.push_back(bottom); self.queue.push_back(top); } Some(TermSplit::Tabs { tabs }) => { self.queue.extend(tabs.values().rev().map(|v| v.deref())); } Some(TermSplit::Term { frame }) => break Some(frame), None => break None, } } } } #[cfg(test)] mod tests { use super::*; #[test] fn validate_accepts_valid() { use TermSplit::*; assert_eq!( (Vertical { top: Term { frame: FrameId(1) }.into(), bottom: Horizontal { left: Term { frame: FrameId(2) }.into(), right: Term { frame: FrameId(3) }.into(), } .into() }) .validate(), Ok(()) ); } #[test] fn validate_rejects_duplicate() { use TermSplit::*; assert_eq!( (Vertical { top: Term { frame: FrameId(1) }.into(), bottom: Horizontal { left: Term { frame: FrameId(1) }.into(), right: Term { frame: FrameId(3) }.into(), } .into() }) .validate(), Err("Attempt to create layout that duplicates reference to: Terminal 1".to_owned()) ); assert_eq!( (Vertical { top: Term { frame: FrameId(42) }.into(), bottom: Horizontal { left: Term { frame: FrameId(1) }.into(), right: Term { frame: FrameId(42) }.into(), } .into() }) .validate(), Err("Attempt to create layout that duplicates reference to: Terminal 42".to_owned()) ); } #[test] fn modify_at_pathstr_works() { use TermSplit::*; let t = Term { frame: FrameId(1) }; assert_eq!( t.modify_at_pathstr("", |_v| { Ok(Term { frame: FrameId(2) }) }), Ok(Term { frame: FrameId(2) }) ); assert_eq!( t.modify_at_pathstr("tlr", |_v| { Ok(Term { frame: FrameId(2) }) }), Err("In split path, found trailing junk tlr after addressing terminal".to_owned()) ); let t = Vertical { top: Horizontal { left: Horizontal { left: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(64) }.into(), } .into(), right: Term { frame: FrameId(42) }.into(), } .into(), bottom: Vertical { top: Term { frame: FrameId(43) }.into(), bottom: Term { frame: FrameId(44) }.into(), } .into(), }; assert_eq!( t.modify_at_pathstr("tlr", |_v| { Ok(Term { frame: FrameId(2) }) }) .and_then(|t| t.modify_at_pathstr("bb", |_v| { Ok(Term { frame: FrameId(3) }) })), Ok(Vertical { top: Horizontal { left: Horizontal { left: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(2) }.into(), } .into(), right: Term { frame: FrameId(42) }.into(), } .into(), bottom: Vertical { top: Term { frame: FrameId(43) }.into(), bottom: Term { frame: FrameId(3) }.into(), } .into(), }) ); } #[test] fn iterates_termframes() { use TermSplit::*; let t = Vertical { top: Horizontal { left: Horizontal { left: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(43) }.into(), } .into(), right: Term { frame: FrameId(44) }.into(), } .into(), bottom: Vertical { top: Term { frame: FrameId(45) }.into(), bottom: Tabs { tabs: vec![ ('0', Term { frame: FrameId(46) }), ('1', Term { frame: FrameId(47) }), ] .into(), } .into(), } .into(), }; let frames: Vec<_> = t.iter().collect(); assert_eq!( frames, vec![ &FrameId(42), &FrameId(43), &FrameId(44), &FrameId(45), &FrameId(46), &FrameId(47), ] ); } #[test] fn swapping_works() { use TermSplit::*; let t = Vertical { top: Horizontal { left: Horizontal { left: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(64) }.into(), } .into(), right: Term { frame: FrameId(46) }.into(), } .into(), bottom: Vertical { top: Term { frame: FrameId(43) }.into(), bottom: Term { frame: FrameId(44) }.into(), } .into(), }; assert_eq!( t.swap("tl", "b"), Ok(Vertical { top: Horizontal { left: Vertical { top: Term { frame: FrameId(43) }.into(), bottom: Term { frame: FrameId(44) }.into(), } .into(), right: Term { frame: FrameId(46) }.into(), } .into(), bottom: Horizontal { left: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(64) }.into(), } .into(), }) ); } #[test] fn framing_works() { use TermSplit::*; let t = Vertical { top: Horizontal { left: Horizontal { left: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(43) }.into(), } .into(), right: Term { frame: FrameId(44) }.into(), } .into(), bottom: Vertical { top: Term { frame: FrameId(45) }.into(), bottom: Term { frame: FrameId(46) }.into(), } .into(), }; assert_eq!( t.tabbed('0', "bb").unwrap(), Vertical { top: Horizontal { left: Horizontal { left: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(43) }.into(), } .into(), right: Term { frame: FrameId(44) }.into(), } .into(), bottom: Vertical { top: Term { frame: FrameId(45) }.into(), bottom: Tabs { tabs: vec![('0', Term { frame: FrameId(46) })].into() } .into(), } .into(), } ); } #[test] fn can_add_tab() { use TermSplit::*; let t = Vertical { top: Horizontal { left: Horizontal { left: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(43) }.into(), } .into(), right: Term { frame: FrameId(44) }.into(), } .into(), bottom: Vertical { top: Term { frame: FrameId(45) }.into(), bottom: Tabs { tabs: vec![('0', Term { frame: FrameId(46) })].into(), } .into(), } .into(), }; assert_eq!( t.add_tab("bb", '1', FrameId(47)).unwrap(), Vertical { top: Horizontal { left: Horizontal { left: Term { frame: FrameId(42) }.into(), right: Term { frame: FrameId(43) }.into(), } .into(), right: Term { frame: FrameId(44) }.into(), } .into(), bottom: Vertical { top: Term { frame: FrameId(45) }.into(), bottom: Tabs { tabs: vec![ ('0', Term { frame: FrameId(46) }), ('1', Term { frame: FrameId(47) }), ] .into() } .into() } .into(), } ); } }