Implement box function
This commit is contained in:
parent
8f7e5c52fe
commit
42d37a6c02
@ -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]
|
||||||
|
332
src/box_.rs
332
src/box_.rs
@ -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 {
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// 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;
|
||||||
|
Loading…
Reference in New Issue
Block a user