544 lines
19 KiB
Rust
544 lines
19 KiB
Rust
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<TermSplit>,
|
|
right: Rc<TermSplit>,
|
|
},
|
|
Vertical {
|
|
top: Rc<TermSplit>,
|
|
bottom: Rc<TermSplit>,
|
|
},
|
|
Tabs {
|
|
tabs: OrdMap<char, Rc<TermSplit>>,
|
|
},
|
|
}
|
|
|
|
impl TermSplit {
|
|
pub fn iter<'t>(&'t self) -> AccessibleSplitIter<'t> {
|
|
AccessibleSplitIter {
|
|
queue: vec![self].into(),
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
TermSplit::Tabs { tabs } => {
|
|
for tab in tabs.values() {
|
|
tab.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::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::<String>())),
|
|
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::<String>()))
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn get_at_pathstr(&self, pathstr: &str) -> Result<&Self, String> {
|
|
self.get_at_pathstr_vec(&pathstr.chars().collect::<Vec<char>>())
|
|
}
|
|
|
|
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::<String>()))
|
|
},
|
|
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::<String>()))
|
|
},
|
|
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::<String>())),
|
|
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::<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 (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<TermSplit, String> {
|
|
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<TermSplit, String> {
|
|
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<TermSplit, String> {
|
|
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<Self::Item> {
|
|
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(),
|
|
}
|
|
);
|
|
}
|
|
}
|