From 42d37a6c02a3d77dde6f3631c3a477bae3a838c9 Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Wed, 10 Aug 2022 20:07:26 -0400 Subject: [PATCH] Implement box function --- kakplugin/src/types.rs | 26 +++- src/box_.rs | 332 ++++++++++++++++++++++++++++++----------- src/main.rs | 1 + 3 files changed, 266 insertions(+), 93 deletions(-) diff --git a/kakplugin/src/types.rs b/kakplugin/src/types.rs index 75a379c..fd58ad4 100644 --- a/kakplugin/src/types.rs +++ b/kakplugin/src/types.rs @@ -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 { + pub fn intersect(&self, other: SD) -> Option + where + SD: AsRef, + { // 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] diff --git a/src/box_.rs b/src/box_.rs index 7ae9815..ded4893 100644 --- a/src/box_.rs +++ b/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 { - // 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, KakError> { - let selection_descs: Vec = 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 = get_selections_desc(Some("%"))?; - - // Now, split on newline - // TODO: Should I use ? - // kakplugin::cmd(&format!("exec 'S\\n'"))?; - kakplugin::cmd(&format!("exec ''"))?; - // TODO: Here is where I might want selections to check if they end in newline - - let mut ret_selection_descs: Vec = vec![]; - - let split_selection_descs: Vec = 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(selections_desc: SDI) -> Option +where + // SD: AsRef, + SDI: IntoIterator, +{ + 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, 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(""))?; + 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::>, KakError>>()? + .into_iter() + .flatten() + .collect::>()) +} + +/// 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 (``). +/// 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 (``) +fn to_boxed_selections( + selection_desc: SD1, + selections_desc_rows: &[SD2], +) -> Vec +where + SD1: AsRef, + SD2: AsRef, +{ + 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 .,. + // 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) + ); + } } diff --git a/src/main.rs b/src/main.rs index 3dd7416..733f1eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ // TODO: Remove #![allow(dead_code, unused_imports)] #![feature(slice_group_by)] +#![feature(slice_take)] mod box_; mod errors;