241 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			241 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use itertools::Itertools;
 | |
| use std::{collections::BTreeMap, rc::Rc};
 | |
| 
 | |
| use crate::FrameId;
 | |
| 
 | |
| #[derive(PartialEq, Eq, Debug, Clone)]
 | |
| pub enum TermSplit {
 | |
|     Term {
 | |
|         frame: FrameId,
 | |
|     },
 | |
|     Horizontal {
 | |
|         left: Rc<TermSplit>,
 | |
|         right: Rc<TermSplit>,
 | |
|     },
 | |
|     Vertical {
 | |
|         top: Rc<TermSplit>,
 | |
|         bottom: Rc<TermSplit>,
 | |
|     },
 | |
| }
 | |
| 
 | |
| impl TermSplit {
 | |
|     fn collect_term_frames(&self, into: &mut BTreeMap<FrameId, usize>) {
 | |
|         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);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn validate(&self) -> Result<(), String> {
 | |
|         let mut frame_count: BTreeMap<FrameId, usize> = 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<F>(&self, pathstr: &str, mod_with: F) -> Result<Self, String>
 | |
|     where
 | |
|         F: FnOnce(&TermSplit) -> Result<Self, String>,
 | |
|     {
 | |
|         self.modify_at_pathstr_vec(&pathstr.chars().collect::<Vec<char>>(), mod_with)
 | |
|     }
 | |
| 
 | |
|     fn modify_at_pathstr_vec<F>(&self, pathstr: &[char], mod_with: F) -> Result<Self, String>
 | |
|     where
 | |
|         F: FnOnce(&TermSplit) -> Result<Self, String>,
 | |
|     {
 | |
|         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::<String>()))
 | |
|             },
 | |
|             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::<String>()))
 | |
|             },
 | |
|             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::<String>()))
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub fn hsplit(&self, pathstr: &str, new_frame: FrameId) -> Result<TermSplit, String> {
 | |
|         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<TermSplit, String> {
 | |
|         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<TermSplit, String> {
 | |
|         self.modify_at_pathstr(pathstr, move |n| match n {
 | |
|             TermSplit::Term { .. } => {
 | |
|                 Err("Can only join vertical or horizontal splits, not a terminal".to_owned())
 | |
|             }
 | |
|             TermSplit::Horizontal { left, .. } => Ok((**left).clone()),
 | |
|             TermSplit::Vertical { top, .. } => Ok((**top).clone()),
 | |
|         })
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[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(),
 | |
|             })
 | |
|         );
 | |
|     }
 | |
| }
 |