Implement box function

This commit is contained in:
Austen Adler 2022-08-10 20:07:26 -04:00
parent 8f7e5c52fe
commit 42d37a6c02
3 changed files with 266 additions and 93 deletions

View File

@ -78,6 +78,14 @@ pub struct SelectionDesc {
} }
impl SelectionDesc { impl SelectionDesc {
/// Gets the number of rows this selection spans
///
/// The newline at the end of a line does not count as an extra row
pub fn row_span(&self) -> usize {
let s = self.sort();
s.right.row - s.left.row + 1
}
#[must_use] #[must_use]
pub fn sort(&self) -> Self { pub fn sort(&self) -> Self {
if self.left < self.right { if self.left < self.right {
@ -107,9 +115,15 @@ impl SelectionDesc {
} }
#[must_use] #[must_use]
pub fn intersect(&self, other: &Self) -> Option<Self> { pub fn intersect<SD>(&self, other: SD) -> Option<Self>
where
SD: AsRef<Self>,
{
// Set a and b to the leftmost and rightmost selection // Set a and b to the leftmost and rightmost selection
let (a, b) = (min(self, other).sort(), max(self, other).sort()); let (a, b) = (
min(self, other.as_ref()).sort(),
max(self, other.as_ref()).sort(),
);
match (b.contains(&a.left), b.contains(&a.right), a.contains(&b)) { match (b.contains(&a.left), b.contains(&a.right), a.contains(&b)) {
(false, false, false) => { (false, false, false) => {
@ -153,7 +167,8 @@ impl SelectionDesc {
// None // None
if a.right.row == b.left.row && a.right.col == b.left.col.saturating_sub(1) { if a.right.row == b.left.row && a.right.col == b.left.col.saturating_sub(1) {
Some(Self { Some(Self {
left: a.left,right: b.right left: a.left,
right: b.right,
}) })
} else { } else {
None None
@ -731,6 +746,11 @@ mod test {
assert_eq!(SD.sort(), SD.sort().sort()); assert_eq!(SD.sort(), SD.sort().sort());
assert_eq!(sd!(10, 1, 18, 9).sort(), sd!(10, 1, 18, 9)); assert_eq!(sd!(10, 1, 18, 9).sort(), sd!(10, 1, 18, 9));
assert_eq!(sdr!(10, 1, 18, 9).sort(), sd!(10, 1, 18, 9)); assert_eq!(sdr!(10, 1, 18, 9).sort(), sd!(10, 1, 18, 9));
assert!(sd!(10, 1, 18, 9).sort().left < sd!(10, 1, 18, 9).sort().right);
assert!(sdr!(10, 1, 18, 9).sort().left < sdr!(10, 1, 18, 9).sort().right);
assert!(sd!(0, 1).sort().left < sd!(0, 1).sort().right);
assert!(sdr!(0, 1).sort().left < sdr!(0, 1).sort().right);
} }
#[test] #[test]

View File

@ -5,100 +5,252 @@ use std::cmp::{max, min};
#[derive(clap::StructOpt, Debug)] #[derive(clap::StructOpt, Debug)]
pub struct Options { pub struct Options {
// /// Bounding box mode, which selects the largest box to contain everything // /// Bounding box mode, which selects the largest box to contain everything
// #[clap(short, long, help = "Select the bonding box of all selections")] #[clap(short, long, help = "Select the bonding box of all selections")]
// bounding_box: bool, bounding_box: bool,
// /// Allow selecting trailing newlines
// #[clap(short, long, help = "Allow selecting trailing newlines")]
// preserve_newlines: bool,
} }
pub fn box_(options: &Options) -> Result<String, KakError> { pub fn box_(options: &Options) -> Result<String, KakError> {
// TODO: Research if having multiple bounding boxes makes sense if options.bounding_box {
// let ret_selection_descs = if options.bounding_box { // The user requested only the bounding box, so select it first
// // Get the bounding box and select it set_selections_desc(vec![get_bounding_box(get_selections_desc(None)?)
// bounding_box(options)? .ok_or_else(|| KakError::Custom(String::from("Selection is empty")))?])?;
// } else {
// // Get a box per selection
// todo!("Implement per_selection(options: &Options);");
// };
let ret_selection_descs = bounding_box(options)?;
set_selections_desc(ret_selection_descs.iter())?;
Ok(format!("Box {} selections", ret_selection_descs.len()))
}
fn bounding_box(_options: &Options) -> Result<Vec<SelectionDesc>, KakError> {
let selection_descs: Vec<SelectionDesc> = get_selections_desc(None)?
.iter()
// TODO: Do they need to be sorted?
.map(|sd| sd.sort())
.collect();
let (leftmost_col, rightmost_col) = selection_descs
.iter()
// Extract the columns so they can be reduced
// Make the left one be the smaller one in case the first one is max or min (reduce function would not be called)
.map(|sd| {
(
min(sd.left.col, sd.right.col),
max(sd.left.col, sd.right.col),
)
})
// Get the smallest column or row
.reduce(|(leftmost_col, rightmost_col), (left, right)| {
(
min(leftmost_col, min(left, right)),
max(rightmost_col, min(left, right)),
)
})
.ok_or_else(|| KakError::Custom(String::from("Selection is empty")))?;
// let (leftmost_row, rightmost_row) = selection_descs
// .first()
// .map(|sd| sd.left.row)
// .zip(selection_descs.last().map(|sd| sd.right.row))
// .ok_or_else(|| KakError::Custom(String::from("Selection is empty")))?;
// Get every line in the document
// let document_selections_desc: Vec<SelectionDesc> = get_selections_desc(Some("%<a-s>"))?;
// Now, split on newline
// TODO: Should I use <a-s>?
// kakplugin::cmd(&format!("exec 'S\\n<ret>'"))?;
kakplugin::cmd(&format!("exec '<a-s>'"))?;
// TODO: Here is where I might want selections to check if they end in newline
let mut ret_selection_descs: Vec<SelectionDesc> = vec![];
let split_selection_descs: Vec<SelectionDesc> = get_selections_desc(None)?
.iter()
.map(|sd| sd.sort())
.collect();
for sd in &split_selection_descs {
if sd.left.col > rightmost_col || sd.right.col < leftmost_col {
// If this selection is out of bounds, exclude this line
continue;
} }
ret_selection_descs.push(SelectionDesc { let ret_selections_desc = boxed_selections(options)?;
set_selections_desc(ret_selections_desc.iter())?;
Ok(format!("Boxed {} selection(s)", ret_selections_desc.len()))
}
/// Get the bounding box of some iterator of selections
fn get_bounding_box<SDI>(selections_desc: SDI) -> Option<SelectionDesc>
where
// SD: AsRef<SelectionDesc>,
SDI: IntoIterator<Item = SelectionDesc>,
{
selections_desc
.into_iter()
.map(|sd| sd.as_ref().sort())
.reduce(|acc, sd| SelectionDesc {
left: AnchorPosition { left: AnchorPosition {
row: sd.left.row, row: min(
min(acc.left.row, acc.right.row),
min(sd.left.row, sd.right.row),
),
col: min(
min(acc.left.col, acc.right.col),
min(sd.left.col, sd.right.col),
),
},
right: AnchorPosition {
row: max(
max(acc.right.row, acc.left.row),
max(sd.right.row, sd.left.row),
),
col: max(
max(acc.right.col, acc.left.col),
max(sd.right.col, sd.left.col),
),
},
})
}
/// Implementation that converts each selection to a box with the top left corner at min(anchor.col, cursor.col) and bottom right at max(anchor.col, cursor.col)
///
/// Do this by getting each selection, then getting each whole-row (col 0 to col max) and passing the range of whole-rows into helper `to_boxed_selections`
fn boxed_selections(_options: &Options) -> Result<Vec<SelectionDesc>, KakError> {
// The selections we want to box, one per box
let selections_desc = {
let mut ret = get_selections_desc(None)?;
ret.sort();
ret
};
// Whole-row selections split on newline
let selections_desc_rows = {
let mut ret = get_selections_desc(Some("<a-x><a-s>"))?;
ret.sort();
ret
};
// let mut ret = vec![];
// for sd in selections_desc {
// // The index in the array that contains the first row in the split lines
// let first_row_idx = selections_desc_rows
// .binary_search_by(|sd_search| sd_search.left.row.cmp(&sd.left.row))
// .map_err(|_| {
// KakError::Custom(format!(
// "Selection row {} not found in split rows",
// sd.left.row
// ))
// })?;
// // The slice of full row selections
// let sd_rows = selections_desc_rows
// .as_slice()
// // Start at the first (should be only) position in the list with this row
// .take(first_row_idx..)
// .ok_or_else(|| {
// KakError::Custom(format!(
// "Rows selections_desc (len={}) has no idx={}",
// selections_desc_rows.len(),
// first_row_idx
// ))
// })?
// // Take row_span rows. For an 8 row selection, get 8 rows, including the one taken before
// .take(..(sd.row_span()))
// .ok_or_else(|| {
// eprintln!(
// "rows: {}, row_span: {}, remaining: selections_desc_rows: {}",
// selections_desc_rows.len(),
// sd.row_span(),
// selections_desc_rows.len()
// );
// KakError::Custom(String::from(
// "Selections split on line count mismatch (too few rows)",
// ))
// })?;
// ret.extend_from_slice(&to_boxed_selections(sd, sd_rows)[..]);
// }
// Ok(ret)
Ok(selections_desc
.iter()
.map(|sd| {
// The index in the array that contains the first row in the split lines
let first_row_idx = selections_desc_rows
.binary_search_by(|sd_search| sd_search.left.row.cmp(&sd.left.row))
.map_err(|_| {
KakError::Custom(format!(
"Selection row {} not found in split rows",
sd.left.row
))
})?;
// The slice of full row selections
let sd_rows = selections_desc_rows
.as_slice()
// Start at the first (should be only) position in the list with this row
.take(first_row_idx..)
.ok_or_else(|| {
KakError::Custom(format!(
"Rows selections_desc (len={}) has no idx={}",
selections_desc_rows.len(),
first_row_idx
))
})?
// Take row_span rows. For an 8 row selection, get 8 rows, including the one taken before
.take(..(sd.row_span()))
.ok_or_else(|| {
eprintln!(
"rows: {}, row_span: {}, remaining: selections_desc_rows: {}",
selections_desc_rows.len(),
sd.row_span(),
selections_desc_rows.len()
);
KakError::Custom(String::from(
"Selections split on line count mismatch (too few rows)",
))
})?;
Ok(to_boxed_selections(sd, sd_rows))
})
.collect::<Result<Vec<Vec<SelectionDesc>>, KakError>>()?
.into_iter()
.flatten()
.collect::<Vec<SelectionDesc>>())
}
/// Returns a vec of selections_desc of the intersection of the bounding box and the component rows
///
/// This function takes a selection desc, and its whole-row split selections (`<a-x><a-s>`).
/// For each whole-row (col 1 to max col) selection, it finds the intersection between the min col and max col in `selection_desc`
///
/// * `selection_desc` - The base (possibly multiline) selection_desc
/// * `selections_desc_rows` - Vec of above `selection_desc` split by line (`<a-x><a-s>`)
fn to_boxed_selections<SD1, SD2>(
selection_desc: SD1,
selections_desc_rows: &[SD2],
) -> Vec<SelectionDesc>
where
SD1: AsRef<SelectionDesc>,
SD2: AsRef<SelectionDesc>,
{
let (leftmost_col, rightmost_col) = (
min(
selection_desc.as_ref().left.col,
selection_desc.as_ref().right.col,
),
max(
selection_desc.as_ref().left.col,
selection_desc.as_ref().right.col,
),
);
selections_desc_rows
.iter()
.map(|split_sd| {
// Find the intersection of <row>.<min_col>,<row>.<max_col>
// If empty, return none. Flatten will not add it to the resulting vec
split_sd.as_ref().intersect(SelectionDesc {
left: AnchorPosition {
row: split_sd.as_ref().left.row,
col: leftmost_col, col: leftmost_col,
}, },
right: AnchorPosition { right: AnchorPosition {
row: sd.right.row, row: split_sd.as_ref().right.row,
col: rightmost_col, col: rightmost_col,
}, },
}); })
// The left- and right-most col })
.flatten()
.collect()
}
// let subselection_descs #[cfg(test)]
mod tests {
use super::*;
// Selection desc creator
macro_rules! sd {
($b:expr, $d:expr) => {{
sd!(1, $b, 1, $d)
}};
($a:expr, $b:expr,$c:expr,$d:expr) => {{
SelectionDesc {
left: AnchorPosition { row: $a, col: $b },
right: AnchorPosition { row: $c, col: $d },
}
}};
} }
set_selections_desc(&ret_selection_descs[..])?; // Reversed
macro_rules! sdr {
($b:expr, $d:expr) => {{
sd!(1, $d, 1, $b)
}};
($a:expr, $b:expr,$c:expr,$d:expr) => {{
SelectionDesc {
left: AnchorPosition { row: $c, col: $d },
right: AnchorPosition { row: $a, col: $b },
}
}};
}
Ok(ret_selection_descs) #[test]
fn test_get_bounding_box() {
assert!(get_bounding_box(Vec::new()).is_none());
assert_eq!(get_bounding_box(vec![sd!(0, 1)]).unwrap(), sd!(0, 1));
assert_eq!(
get_bounding_box(vec![sd!(0, 0, 8, 2), sd!(1, 15, 9, 3)]).unwrap(),
sd!(0, 0, 9, 15)
);
assert_eq!(get_bounding_box(vec![sdr!(0, 1)]).unwrap(), sd!(0, 1));
assert_eq!(
get_bounding_box(vec![sdr!(0, 0, 8, 2), sdr!(1, 15, 9, 3)]).unwrap(),
sd!(0, 0, 9, 15)
);
}
} }

View File

@ -8,6 +8,7 @@
// TODO: Remove // TODO: Remove
#![allow(dead_code, unused_imports)] #![allow(dead_code, unused_imports)]
#![feature(slice_group_by)] #![feature(slice_group_by)]
#![feature(slice_take)]
mod box_; mod box_;
mod errors; mod errors;