Implement box function
This commit is contained in:
parent
8f7e5c52fe
commit
42d37a6c02
@ -78,6 +78,14 @@ pub struct 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]
|
||||
pub fn sort(&self) -> Self {
|
||||
if self.left < self.right {
|
||||
@ -107,9 +115,15 @@ impl SelectionDesc {
|
||||
}
|
||||
|
||||
#[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
|
||||
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)) {
|
||||
(false, false, false) => {
|
||||
@ -153,7 +167,8 @@ impl SelectionDesc {
|
||||
// None
|
||||
if a.right.row == b.left.row && a.right.col == b.left.col.saturating_sub(1) {
|
||||
Some(Self {
|
||||
left: a.left,right: b.right
|
||||
left: a.left,
|
||||
right: b.right,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@ -731,6 +746,11 @@ mod test {
|
||||
assert_eq!(SD.sort(), SD.sort().sort());
|
||||
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!(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]
|
||||
|
332
src/box_.rs
332
src/box_.rs
@ -5,100 +5,252 @@ use std::cmp::{max, min};
|
||||
#[derive(clap::StructOpt, Debug)]
|
||||
pub struct Options {
|
||||
// /// Bounding box mode, which selects the largest box to contain everything
|
||||
// #[clap(short, long, help = "Select the bonding box of all selections")]
|
||||
// bounding_box: bool,
|
||||
// /// Allow selecting trailing newlines
|
||||
// #[clap(short, long, help = "Allow selecting trailing newlines")]
|
||||
// preserve_newlines: bool,
|
||||
#[clap(short, long, help = "Select the bonding box of all selections")]
|
||||
bounding_box: bool,
|
||||
}
|
||||
|
||||
pub fn box_(options: &Options) -> Result<String, KakError> {
|
||||
// TODO: Research if having multiple bounding boxes makes sense
|
||||
// let ret_selection_descs = if options.bounding_box {
|
||||
// // Get the bounding box and select it
|
||||
// bounding_box(options)?
|
||||
// } 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 {
|
||||
left: AnchorPosition {
|
||||
row: sd.left.row,
|
||||
col: leftmost_col,
|
||||
},
|
||||
right: AnchorPosition {
|
||||
row: sd.right.row,
|
||||
col: rightmost_col,
|
||||
},
|
||||
});
|
||||
// The left- and right-most col
|
||||
|
||||
// let subselection_descs
|
||||
if options.bounding_box {
|
||||
// The user requested only the bounding box, so select it first
|
||||
set_selections_desc(vec![get_bounding_box(get_selections_desc(None)?)
|
||||
.ok_or_else(|| KakError::Custom(String::from("Selection is empty")))?])?;
|
||||
}
|
||||
|
||||
set_selections_desc(&ret_selection_descs[..])?;
|
||||
let ret_selections_desc = boxed_selections(options)?;
|
||||
|
||||
Ok(ret_selection_descs)
|
||||
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 {
|
||||
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,
|
||||
},
|
||||
right: AnchorPosition {
|
||||
row: split_sd.as_ref().right.row,
|
||||
col: rightmost_col,
|
||||
},
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[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 },
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
// 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 },
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
// TODO: Remove
|
||||
#![allow(dead_code, unused_imports)]
|
||||
#![feature(slice_group_by)]
|
||||
#![feature(slice_take)]
|
||||
|
||||
mod box_;
|
||||
mod errors;
|
||||
|
Loading…
Reference in New Issue
Block a user