diff --git a/Cargo.lock b/Cargo.lock index 2105c9b..c9f28d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "3.0.10" @@ -70,6 +76,17 @@ dependencies = [ "syn", ] +[[package]] +name = "getrandom" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -107,6 +124,7 @@ version = "0.1.0" dependencies = [ "alphanumeric-sort", "clap", + "rand", "regex", "shellwords", ] @@ -138,6 +156,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -180,6 +204,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.5.4" @@ -251,6 +305,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index ae02e87..3477dde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ regex = "1" clap = {version = "3", features = ["derive", "env"]} alphanumeric-sort = "1" shellwords = "1" +rand = "0.8" [profile.release] lto = true diff --git a/src/errors.rs b/src/errors.rs index 8380318..1cc8c07 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,5 @@ +use std::num::ParseIntError; + #[derive(Debug)] pub struct KakMessage(pub String, pub Option); @@ -21,3 +23,9 @@ impl From for KakMessage { Self("Corrupt kak response".to_string(), Some(err.to_string())) } } + +impl From for KakMessage { + fn from(err: ParseIntError) -> Self { + Self(format!("Could not parse int: {}", err), None) + } +} diff --git a/src/main.rs b/src/main.rs index 1712086..763ae4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,20 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +#![allow(dead_code, unused_imports)] mod errors; -use alphanumeric_sort::compare_str; +mod shuf; +mod sort; use clap::Parser; use clap::Subcommand; use errors::KakMessage; -use regex::Regex; +use shuf::ShufOptions; +use sort::SortOptions; use std::env; use std::fs; use std::fs::File; use std::fs::OpenOptions; use std::io::Write; +use std::str::FromStr; #[derive(Parser, Debug)] #[clap(about, version, author)] @@ -26,32 +30,84 @@ struct Cli { #[derive(Subcommand, Debug)] enum Commands { - // #[clap(flatten)] Sort(SortOptions), + Shuf(ShufOptions), } -#[derive(clap::StructOpt, Debug)] -struct SortOptions { - #[clap(index = 1)] - regex: Option, - #[clap(short = 's', long)] - subselections_register: Option, - // TODO: Can we invert a boolean? This name is terrible - #[clap(short = 'S', long)] - no_skip_whitespace: bool, - #[clap(short, long)] - lexicographic_sort: bool, - #[clap(short, long)] - reverse: bool, - #[clap(short, long)] - ignore_case: bool, +#[derive(PartialEq, PartialOrd, Debug)] +pub struct SelectionDesc { + left: AnchorPosition, + right: AnchorPosition, } -struct SortableSelection { - content: &str, - subselections: Vec<&str>, +impl FromStr for SelectionDesc { + type Err = KakMessage; + fn from_str(s: &str) -> Result { + let (left, right) = s.split_once(',').ok_or_else(|| { + KakMessage( + "Could not parse position".to_string(), + Some(format!("Could not parse as position: {}", s)), + ) + })?; + + Ok(Self { + left: AnchorPosition::from_str(left)?, + right: AnchorPosition::from_str(right)?, + }) + } } +#[derive(PartialOrd, PartialEq, Clone, Debug)] +pub struct AnchorPosition { + row: usize, + col: usize, +} +impl FromStr for AnchorPosition { + type Err = KakMessage; + fn from_str(s: &str) -> Result { + let (left, right) = s.split_once('.').ok_or_else(|| { + KakMessage( + "Could not parse position".to_string(), + Some(format!("Could not parse as position: {}", s)), + ) + })?; + Ok(Self { + row: usize::from_str(left)?, + col: usize::from_str(right)?, + }) + } +} + +impl SelectionDesc { + fn sort(&self) -> Self { + if self.left < self.right { + // left anchor is first + Self { + left: self.left.clone(), + right: self.right.clone(), + } + } else { + // right anchor is first + Self { + left: self.right.clone(), + right: self.left.clone(), + } + } + } + + fn contains(&self, b: &Self) -> bool { + // Cursor and anchor can be flipped. Set a.0 to be leftmost cursor + let sorted_a = self.sort(); + let sorted_b = b.sort(); + + sorted_b.left >= sorted_a.left && sorted_b.right <= sorted_b.right + } +} + +// impl PartialOrd for SelectionDesc { +// fn cmp() {} +// } + fn main() { let msg = match run() { Ok(msg) => msg, @@ -91,128 +147,18 @@ fn run() -> Result { })?; match &options.command { - Commands::Sort(sort_options) => sort(sort_options), + Commands::Sort(sort_options) => sort::sort(sort_options), + Commands::Shuf(shuf_options) => shuf::shuf(shuf_options), } } -fn sort_keys_simple<'a>( - sort_options: &SortOptions, - selections: &[&str], -) -> Result, KakMessage> { - let re = sort_options - .regex - .as_ref() - .map(|r| Regex::new(r)) - .transpose() - .map_err(|_| { - format!( - "Invalid regular expression: {}", - sort_options.regex.as_ref().unwrap_or(&"".to_string()) - ) - })?; - - Ok(selections - .iter() - .map(|s| { - if sort_options.no_skip_whitespace { - SortableSelection{ - content: s, - subselections: vec![s], - } - } else { - SortableSelection{ - content: s, - subselections: vec![s.trim()], - } - } - }) - .map(|(s, k)| { - let captures = re.as_ref()?.captures(k)?; - captures - .get(1) - .or_else(|| captures.get(0)) - .map(|m| m.as_str()) - }) - .collect::>>()) -} - -// fn sort_keys_subselection(sort_options: &SortOptions) -> Result)>, KakMessage> { -// let sort_selections = kak_response("%val{selections}")?.iter_mut().map(|a| { -// if sort_options.no_skip_whitespace { -// a -// } else { -// a.trim() -// } -// }); -// let sort_selections_desc = kak_response("%val{selections_desc}")?; -// kak_exec("z")?; -// let selections = kak_response("%val{selections}")?; -// let selections_desc = kak_response("%val{selections_desc}")?; -// } - -fn sort(sort_options: &SortOptions) -> Result { - // let selections = kak_response("%val{selections}")?; - - // let sort_keys = if let Some(r) = sort_options.subselections_register { - // let selections_desc = kak_response("%val{selections_desc}")?; - // } else { - // }; - - let mut zipped = match sort_options.subselections_register { - Some(c) => { - let selections = kak_response("%val{selections}")?; - selections - .into_iter() - .zip(sort_keys_simple(sort_options, &selections)) - } - None => { - let selections = kak_response("%val{selections}")?; - selections.iter().zip(selections.iter().map(|s| s.as_str())) - } - }; - - // let mut zipped = sort_keys_simple(sort_options)?; - - zipped.sort_by(|(a, a_key), (b, b_key)| { - let a = a_key.unwrap_or(a); - let b = b_key.unwrap_or(b); - - if sort_options.lexicographic_sort { - a.cmp(b) - } else { - compare_str(a, b) - } - }); - - let mut f = open_command_fifo()?; - - write!(f, "reg '\"'")?; - - let iter: Box> = if sort_options.reverse { - Box::new(zipped.iter().rev()) - } else { - Box::new(zipped.iter()) - }; - - for i in iter { - let new_selection = i.0.replace('\'', "''"); - write!(f, " '{}'", new_selection)?; - } - write!(f, " ; exec R;")?; - - Ok(KakMessage( - format!("Sorted {} selections", zipped.len()), - None, - )) -} - -fn kak_exec(cmd: &str) -> Result<(), KakMessage> { +pub fn kak_exec(cmd: &str) -> Result<(), KakMessage> { let mut f = open_command_fifo()?; write!(f, "{}", cmd).map_err(|e| e.into()) } -fn kak_response(msg: &str) -> Result, KakMessage> { +pub fn kak_response(msg: &str) -> Result, KakMessage> { kak_exec(&format!( "echo -quoting shell -to-file {} -- {msg}", get_var("kak_response_fifo")? @@ -223,7 +169,7 @@ fn kak_response(msg: &str) -> Result, KakMessage> { Ok(selections) } -fn open_command_fifo() -> Result { +pub fn open_command_fifo() -> Result { OpenOptions::new() .write(true) .append(true) @@ -231,7 +177,7 @@ fn open_command_fifo() -> Result { .map_err(|e| e.into()) } -fn get_var(var_name: &str) -> Result { +pub fn get_var(var_name: &str) -> Result { env::var(var_name).map_err(|e| match e { env::VarError::NotPresent => { KakMessage(format!("Env var {} is not defined", var_name), None) @@ -241,3 +187,25 @@ fn get_var(var_name: &str) -> Result { } }) } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_anchor_position() { + let sd = SelectionDesc { + left: AnchorPosition { row: 18, col: 9 }, + right: AnchorPosition { row: 10, col: 0 }, + }; + // Check parsing + assert_eq!(SelectionDesc::from_str("18.9,10.0").unwrap(), sd); + // Check if multiple parsed ones match + assert_eq!( + SelectionDesc::from_str("18.9,10.0").unwrap(), + SelectionDesc::from_str("18.9,10.0").unwrap() + ); + // Check if sorting works + assert_eq!(sd.sort(), SelectionDesc::from_str("10.0,18.9").unwrap()); + assert_eq!(sd.sort(), sd.sort().sort()); + } +} diff --git a/src/shuf.rs b/src/shuf.rs new file mode 100644 index 0000000..483ee1c --- /dev/null +++ b/src/shuf.rs @@ -0,0 +1,27 @@ +use crate::KakMessage; +use crate::{kak_response, open_command_fifo}; +use rand::seq::SliceRandom; +use rand::thread_rng; +use std::io::Write; +#[derive(clap::StructOpt, Debug)] +pub struct ShufOptions; +pub fn shuf(_shuf_options: &ShufOptions) -> Result { + let mut selections = kak_response("%val{selections}")?; + let mut rng = thread_rng(); + + selections.shuffle(&mut rng); + + let mut f = open_command_fifo()?; + write!(f, "reg '\"'")?; + + for i in selections.iter() { + let new_selection = i.replace('\'', "''"); + write!(f, " '{}'", new_selection)?; + } + write!(f, " ; exec R;")?; + + Ok(KakMessage( + format!("Shuf {} selections", selections.len()), + None, + )) +} diff --git a/src/sort.rs b/src/sort.rs new file mode 100644 index 0000000..bd17273 --- /dev/null +++ b/src/sort.rs @@ -0,0 +1,147 @@ +use crate::KakMessage; +use crate::{kak_response, open_command_fifo}; +use alphanumeric_sort::compare_str; +use rand::seq::SliceRandom; +use rand::thread_rng; +use regex::Regex; +use std::cmp::Ordering; +use std::io::Write; + +#[derive(clap::StructOpt, Debug)] +pub struct SortOptions { + #[clap(index = 1)] + regex: Option, + #[clap(short = 's', long)] + subselections_register: Option, + // TODO: Can we invert a boolean? This name is terrible + #[clap(short = 'S', long)] + no_skip_whitespace: bool, + #[clap(short, long)] + lexicographic_sort: bool, + #[clap(short, long)] + reverse: bool, + #[clap(short, long)] + ignore_case: bool, +} + +struct SortableSelection<'a> { + /// The content of the selection + content: &'a str, + /// The string used to compare the content + content_comparison: &'a str, + /// Any subselections + subselections: Vec<&'a str>, +} + +fn get_sortable_selections_subselections<'a, 'b, 'tmp, S: AsRef + std::fmt::Debug + 'a>( + sort_options: &'b SortOptions, + selections: &'a [S], + sort_selections_desc: &'tmp [S], + subselections: &'a [S], + subselections_desc: &'tmp [S], +) -> Result>, KakMessage> { + eprintln!( + "All units: {:?}\n{:?}\n{:?}\n{:?}", + selections, sort_selections_desc, subselections, subselections_desc, + ); + let mut sortable_selections = selections + .iter() + .map(|s| to_sortable_selection(s.as_ref(), sort_options)) + .collect::>(); + + // sortable_selections.into_iter().map + + // Ok(vec![]) + todo!(); +} + +fn to_sortable_selection<'a, 'b>( + selection: &'a str, + sort_options: &'b SortOptions, +) -> SortableSelection<'a> { + if sort_options.no_skip_whitespace { + SortableSelection { + content: selection, + content_comparison: selection, + subselections: vec![], + } + } else { + SortableSelection { + content: selection, + content_comparison: selection.trim(), + subselections: vec![], + } + } +} + +pub fn sort(sort_options: &SortOptions) -> Result { + let subselections: Option<(Vec, Vec)> = sort_options + .subselections_register + .map::, Vec), KakMessage>, _>(|_c| { + let subselections = kak_response("%val{selections}")?; + let subselections_desc = kak_response("%val{selections_desc}")?; + // kak_exec(&format!("exec z",))?; + Ok((subselections, subselections_desc)) + }) + .transpose()?; + let selections = kak_response("%val{selections}")?; + + let mut zipped: Vec> = match subselections { + Some((ref subselections, ref subselections_desc)) => { + let selections_desc = kak_response("%val{selections_desc}")?; + + get_sortable_selections_subselections( + sort_options, + &selections, + &selections_desc, + subselections, + subselections_desc, + )? + } + None => selections + .iter() + .map(|s| to_sortable_selection(s, sort_options)) + .collect(), + }; + + zipped.sort_by(|a, b| { + for (a_subselection, b_subselection) in a.subselections.iter().zip(b.subselections.iter()) { + let comparison = if sort_options.lexicographic_sort { + a_subselection.cmp(b_subselection) + } else { + compare_str(a_subselection, b_subselection) + }; + match comparison { + Ordering::Less | Ordering::Greater => return comparison, + Ordering::Equal => {} + } + } + + if sort_options.lexicographic_sort { + a.content_comparison.cmp(b.content_comparison) + } else { + compare_str(a.content_comparison, b.content_comparison) + } + }); + + let mut f = open_command_fifo()?; + + write!(f, "reg '\"'")?; + + let iter: Box> = if sort_options.reverse { + Box::new(zipped.iter().rev()) + } else { + Box::new(zipped.iter()) + }; + + for i in iter { + let new_selection = i.content.replace('\'', "''"); + write!(f, " '{}'", new_selection)?; + } + write!(f, " ; exec R;")?; + + Ok(KakMessage( + format!("Sorted {} selections", zipped.len()), + None, + )) +}