diff --git a/Cargo.lock b/Cargo.lock index d7f9edd..11e7f6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,8 @@ dependencies = [ "clap", "evalexpr", "kakplugin", + "linked-hash-map", + "linked_hash_set", "rand", "regex", "shellwords", @@ -156,6 +158,21 @@ version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "linked_hash_set" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47186c6da4d81ca383c7c47c1bfc80f4b95f4720514d860a5407aaf4233f9588" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "memchr" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index 82897e9..06529ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,8 @@ shellwords = "1" rand = "0.8" evalexpr = "7" kakplugin = {path = "./kakplugin/"} +linked-hash-map = "0.5.4" +linked_hash_set = "0.1.4" [profile.release] lto = true diff --git a/kakplugin/src/lib.rs b/kakplugin/src/lib.rs index ee34610..0b06343 100644 --- a/kakplugin/src/lib.rs +++ b/kakplugin/src/lib.rs @@ -1,12 +1,13 @@ mod errors; -mod types; +pub mod types; pub use errors::KakError; use std::{ - env, fmt, + env, fs::{self, File, OpenOptions}, io::{BufWriter, Write}, str::FromStr, }; +use types::Register; pub use types::{ AnchorPosition, Selection, SelectionDesc, SelectionWithDesc, SelectionWithSubselections, }; @@ -95,14 +96,14 @@ pub fn get_selections_with_desc() -> Result, KakError> { pub fn set_selections<'a, I, S: 'a + ?Sized>(selections: I) -> Result<(), KakError> where I: IntoIterator, - S: AsRef + fmt::Display, + S: AsRef, { let mut f = open_command_fifo()?; - write!(f, "reg '\"'")?; + write!(f, "set-register '\"'")?; for i in selections { - write!(f, " '{}'", i.as_ref().replace('\'', "''"))?; + write!(f, " '{}'", escape(i))?; } - write!(f, "; exec R;")?; + write!(f, "; execute-keys R;")?; f.flush()?; Ok(()) } @@ -131,7 +132,7 @@ pub fn display_message>( message: S, debug_message: Option, ) -> Result<(), KakError> { - let msg_str = message.as_ref().replace('\'', "''"); + let msg_str = escape(message); { let mut f = open_command_fifo()?; @@ -139,32 +140,36 @@ pub fn display_message>( write!(f, "echo -debug '{}';", msg_str)?; if let Some(debug_msg_str) = &debug_message.as_ref() { - write!( - f, - "echo -debug '{}';", - debug_msg_str.as_ref().replace('\'', "''") - )?; + write!(f, "echo -debug '{}';", escape(debug_msg_str))?; } f.flush()?; } Ok(()) } +pub fn escape>(s: S) -> String { + s.as_ref().replace('\'', "''") +} + /// # Errors /// /// Will return `Err` if command fifo could not be opened or written to -pub fn exec(cmd: &str) -> Result<(), KakError> { +pub fn cmd(cmd: &str) -> Result<(), KakError> { let mut f = open_command_fifo()?; - write!(f, "{}", cmd)?; + write!(f, "{};", cmd)?; f.flush().map_err(Into::into) } +pub fn restore_register(r: &Register) -> Result<(), KakError> { + cmd(&format!("execute-keys '\"{}z'", r.kak_escaped())) +} + /// # Errors /// /// Will return `Err` if command fifo could not be opened or written to pub fn response(msg: &str) -> Result, KakError> { - exec(&format!( + cmd(&format!( "echo -quoting shell -to-file {} -- {msg}", get_var("kak_response_fifo")? ))?; diff --git a/kakplugin/src/types.rs b/kakplugin/src/types.rs index b69555b..a8a2ad5 100644 --- a/kakplugin/src/types.rs +++ b/kakplugin/src/types.rs @@ -93,6 +93,352 @@ impl FromStr for AnchorPosition { } } +#[derive(Debug, PartialEq, Eq)] +pub enum Register { + Numeric0, + Numeric1, + Numeric2, + Numeric3, + Numeric4, + Numeric5, + Numeric6, + Numeric7, + Numeric8, + Numeric9, + + UppercaseA, + UppercaseB, + UppercaseC, + UppercaseD, + UppercaseE, + UppercaseF, + UppercaseG, + UppercaseH, + UppercaseI, + UppercaseJ, + UppercaseK, + UppercaseL, + UppercaseM, + UppercaseN, + UppercaseO, + UppercaseP, + UppercaseQ, + UppercaseR, + UppercaseS, + UppercaseT, + UppercaseU, + UppercaseV, + UppercaseW, + UppercaseX, + UppercaseY, + UppercaseZ, + + LowercaseA, + LowercaseB, + LowercaseC, + LowercaseD, + LowercaseE, + LowercaseF, + LowercaseG, + LowercaseH, + LowercaseI, + LowercaseJ, + LowercaseK, + LowercaseL, + LowercaseM, + LowercaseN, + LowercaseO, + LowercaseP, + LowercaseQ, + LowercaseR, + LowercaseS, + LowercaseT, + LowercaseU, + LowercaseV, + LowercaseW, + LowercaseX, + LowercaseY, + LowercaseZ, + + Dquote, + Slash, + Arobase, + Caret, + Pipe, + Percent, + Dot, + Hash, + Underscore, + Colon, +} + +impl Register { + pub fn kak_expanded(&self) -> &'static str { + match self { + Self::Dquote => "dquote", + Self::Slash => "slash", + Self::Arobase => "arobase", + Self::Caret => "caret", + Self::Pipe => "pipe", + Self::Percent => "percent", + Self::Dot => "dot", + Self::Hash => "hash", + Self::Underscore => "underscore", + Self::Colon => "colon", + + // Everything else is the same + _ => self.kak_escaped(), + } + } + + pub fn to_char(&self) -> char { + match self { + Self::Numeric0 => '0', + Self::Numeric1 => '1', + Self::Numeric2 => '2', + Self::Numeric3 => '3', + Self::Numeric4 => '4', + Self::Numeric5 => '5', + Self::Numeric6 => '6', + Self::Numeric7 => '7', + Self::Numeric8 => '8', + Self::Numeric9 => '9', + + Self::UppercaseA => 'A', + Self::UppercaseB => 'B', + Self::UppercaseC => 'C', + Self::UppercaseD => 'D', + Self::UppercaseE => 'E', + Self::UppercaseF => 'F', + Self::UppercaseG => 'G', + Self::UppercaseH => 'H', + Self::UppercaseI => 'I', + Self::UppercaseJ => 'J', + Self::UppercaseK => 'K', + Self::UppercaseL => 'L', + Self::UppercaseM => 'M', + Self::UppercaseN => 'N', + Self::UppercaseO => 'O', + Self::UppercaseP => 'P', + Self::UppercaseQ => 'Q', + Self::UppercaseR => 'R', + Self::UppercaseS => 'S', + Self::UppercaseT => 'T', + Self::UppercaseU => 'U', + Self::UppercaseV => 'V', + Self::UppercaseW => 'W', + Self::UppercaseX => 'X', + Self::UppercaseY => 'Y', + Self::UppercaseZ => 'Z', + + Self::LowercaseA => 'a', + Self::LowercaseB => 'b', + Self::LowercaseC => 'c', + Self::LowercaseD => 'd', + Self::LowercaseE => 'e', + Self::LowercaseF => 'f', + Self::LowercaseG => 'g', + Self::LowercaseH => 'h', + Self::LowercaseI => 'i', + Self::LowercaseJ => 'j', + Self::LowercaseK => 'k', + Self::LowercaseL => 'l', + Self::LowercaseM => 'm', + Self::LowercaseN => 'n', + Self::LowercaseO => 'o', + Self::LowercaseP => 'p', + Self::LowercaseQ => 'q', + Self::LowercaseR => 'r', + Self::LowercaseS => 's', + Self::LowercaseT => 't', + Self::LowercaseU => 'u', + Self::LowercaseV => 'v', + Self::LowercaseW => 'w', + Self::LowercaseX => 'x', + Self::LowercaseY => 'y', + Self::LowercaseZ => 'z', + + Self::Dquote => '"', + Self::Slash => '/', + Self::Arobase => '@', + Self::Caret => '^', + Self::Pipe => '|', + Self::Percent => '%', + Self::Dot => '.', + Self::Hash => '#', + Self::Underscore => '_', + Self::Colon => ':', + } + } + + pub fn kak_escaped(&self) -> &'static str { + match self { + Self::Numeric0 => "0", + Self::Numeric1 => "1", + Self::Numeric2 => "2", + Self::Numeric3 => "3", + Self::Numeric4 => "4", + Self::Numeric5 => "5", + Self::Numeric6 => "6", + Self::Numeric7 => "7", + Self::Numeric8 => "8", + Self::Numeric9 => "9", + + Self::UppercaseA => "A", + Self::UppercaseB => "B", + Self::UppercaseC => "C", + Self::UppercaseD => "D", + Self::UppercaseE => "E", + Self::UppercaseF => "F", + Self::UppercaseG => "G", + Self::UppercaseH => "H", + Self::UppercaseI => "I", + Self::UppercaseJ => "J", + Self::UppercaseK => "K", + Self::UppercaseL => "L", + Self::UppercaseM => "M", + Self::UppercaseN => "N", + Self::UppercaseO => "O", + Self::UppercaseP => "P", + Self::UppercaseQ => "Q", + Self::UppercaseR => "R", + Self::UppercaseS => "S", + Self::UppercaseT => "T", + Self::UppercaseU => "U", + Self::UppercaseV => "V", + Self::UppercaseW => "W", + Self::UppercaseX => "X", + Self::UppercaseY => "Y", + Self::UppercaseZ => "Z", + + Self::LowercaseA => "a", + Self::LowercaseB => "b", + Self::LowercaseC => "c", + Self::LowercaseD => "d", + Self::LowercaseE => "e", + Self::LowercaseF => "f", + Self::LowercaseG => "g", + Self::LowercaseH => "h", + Self::LowercaseI => "i", + Self::LowercaseJ => "j", + Self::LowercaseK => "k", + Self::LowercaseL => "l", + Self::LowercaseM => "m", + Self::LowercaseN => "n", + Self::LowercaseO => "o", + Self::LowercaseP => "p", + Self::LowercaseQ => "q", + Self::LowercaseR => "r", + Self::LowercaseS => "s", + Self::LowercaseT => "t", + Self::LowercaseU => "u", + Self::LowercaseV => "v", + Self::LowercaseW => "w", + Self::LowercaseX => "x", + Self::LowercaseY => "y", + Self::LowercaseZ => "z", + + Self::Dquote => "\\\"", + Self::Slash => "/", + Self::Arobase => "@", + Self::Caret => "^", + Self::Pipe => "|", + Self::Percent => "%", + Self::Dot => ".", + Self::Hash => "#", + Self::Underscore => "_", + Self::Colon => ":", + } + } +} + +impl FromStr for Register { + type Err = KakError; + fn from_str(s: &str) -> Result { + match s { + "0" => Ok(Self::Numeric0), + "1" => Ok(Self::Numeric1), + "2" => Ok(Self::Numeric2), + "3" => Ok(Self::Numeric3), + "4" => Ok(Self::Numeric4), + "5" => Ok(Self::Numeric5), + "6" => Ok(Self::Numeric6), + "7" => Ok(Self::Numeric7), + "8" => Ok(Self::Numeric8), + "9" => Ok(Self::Numeric9), + + "A" => Ok(Self::UppercaseA), + "B" => Ok(Self::UppercaseB), + "C" => Ok(Self::UppercaseC), + "D" => Ok(Self::UppercaseD), + "E" => Ok(Self::UppercaseE), + "F" => Ok(Self::UppercaseF), + "G" => Ok(Self::UppercaseG), + "H" => Ok(Self::UppercaseH), + "I" => Ok(Self::UppercaseI), + "J" => Ok(Self::UppercaseJ), + "K" => Ok(Self::UppercaseK), + "L" => Ok(Self::UppercaseL), + "M" => Ok(Self::UppercaseM), + "N" => Ok(Self::UppercaseN), + "O" => Ok(Self::UppercaseO), + "P" => Ok(Self::UppercaseP), + "Q" => Ok(Self::UppercaseQ), + "R" => Ok(Self::UppercaseR), + "S" => Ok(Self::UppercaseS), + "T" => Ok(Self::UppercaseT), + "U" => Ok(Self::UppercaseU), + "V" => Ok(Self::UppercaseV), + "W" => Ok(Self::UppercaseW), + "X" => Ok(Self::UppercaseX), + "Y" => Ok(Self::UppercaseY), + "Z" => Ok(Self::UppercaseZ), + + "a" => Ok(Self::LowercaseA), + "b" => Ok(Self::LowercaseB), + "c" => Ok(Self::LowercaseC), + "d" => Ok(Self::LowercaseD), + "e" => Ok(Self::LowercaseE), + "f" => Ok(Self::LowercaseF), + "g" => Ok(Self::LowercaseG), + "h" => Ok(Self::LowercaseH), + "i" => Ok(Self::LowercaseI), + "j" => Ok(Self::LowercaseJ), + "k" => Ok(Self::LowercaseK), + "l" => Ok(Self::LowercaseL), + "m" => Ok(Self::LowercaseM), + "n" => Ok(Self::LowercaseN), + "o" => Ok(Self::LowercaseO), + "p" => Ok(Self::LowercaseP), + "q" => Ok(Self::LowercaseQ), + "r" => Ok(Self::LowercaseR), + "s" => Ok(Self::LowercaseS), + "t" => Ok(Self::LowercaseT), + "u" => Ok(Self::LowercaseU), + "v" => Ok(Self::LowercaseV), + "w" => Ok(Self::LowercaseW), + "x" => Ok(Self::LowercaseX), + "y" => Ok(Self::LowercaseY), + "z" => Ok(Self::LowercaseZ), + + "\"" | "dquote" => Ok(Self::Dquote), + "/" | "slash" => Ok(Self::Slash), + "@" | "arobase" => Ok(Self::Arobase), + "^" | "caret" => Ok(Self::Caret), + "|" | "pipe" => Ok(Self::Pipe), + "%" | "percent" => Ok(Self::Percent), + "." | "dot" => Ok(Self::Dot), + "#" | "hash" => Ok(Self::Hash), + "_" | "underscore" => Ok(Self::Underscore), + ":" | "colon" => Ok(Self::Colon), + + _ => Err(KakError::Parse(format!( + "Register '{s}' could not be parsed" + ))), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/main.rs b/src/main.rs index 610bcb2..a75eab6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,15 +5,19 @@ // Cannot be fixed #![allow(clippy::multiple_crate_versions)] #![allow(clippy::struct_excessive_bools)] +// TODO: Remove +#![allow(dead_code, unused_imports)] mod errors; mod math_eval; +mod set; mod shuf; mod sort; mod stdin; mod trim; mod uniq; -mod xargs; +mod utils; +// mod xargs; use clap::{Parser, Subcommand}; use kakplugin::{display_message, get_var, KakError}; @@ -31,13 +35,21 @@ struct Cli { #[derive(Subcommand, Debug)] enum Commands { + #[clap(about = "Sorts selections based on content or content regex match")] Sort(sort::Options), + #[clap(about = "Shuffle selections")] Shuf(shuf::Options), + #[clap(about = "Find unique selections based on optional regex match")] Uniq(uniq::Options), - #[clap(visible_aliases = &["bc", "eval"])] + #[clap(about = "Evaluate selections as a math expression", visible_aliases = &["bc", "eval"])] MathEval(math_eval::Options), + #[clap(about = "Trim every selection")] Trim(trim::Options), - Xargs(xargs::Options), + #[clap(about = "Perform set operations on selections")] + Set(set::Options), + // #[clap(about = "")] + // Xargs(xargs::Options), + #[clap(about = "Pass each selection null terminated to a command")] Stdin(stdin::Options), } @@ -63,8 +75,7 @@ fn main() { } fn run() -> Result { - let options = - Cli::try_parse().map_err(|e| KakError::Parse(format!("Argument parse error: {e}")))?; + let options = Cli::try_parse().map_err(|e| KakError::Custom(format!("{e}")))?; match &options.command { Commands::Sort(o) => sort::sort(o), @@ -72,7 +83,8 @@ fn run() -> Result { Commands::Uniq(o) => uniq::uniq(o), Commands::MathEval(o) => math_eval::math_eval(o), Commands::Trim(o) => trim::trim(o), - Commands::Xargs(o) => xargs::xargs(o), + Commands::Set(o) => set::set(o), + // Commands::Xargs(o) => xargs::xargs(o), Commands::Stdin(o) => stdin::stdin(o), } } diff --git a/src/set.rs b/src/set.rs new file mode 100644 index 0000000..5e2b04b --- /dev/null +++ b/src/set.rs @@ -0,0 +1,314 @@ +// use crate::utils; +use clap::ArgEnum; +use kakplugin::{ + get_selections, get_selections_with_desc, set_selections, set_selections_desc, types::Register, + KakError, Selection, SelectionWithDesc, +}; +use linked_hash_map::LinkedHashMap; +use linked_hash_set::LinkedHashSet; +use regex::Regex; +use std::{collections::HashSet, io::Write, str::FromStr}; + +#[derive(clap::StructOpt, Debug)] +pub struct Options { + #[clap(min_values = 1, max_values = 3)] + args: Vec, + + #[clap(short = 'T')] + no_trim: bool, + // #[clap(short, long)] + // regex: Option, + // #[clap(short, long)] + // ignore_case: bool, + // #[clap(short = 'S', long)] + // no_skip_whitespace: bool, +} + +#[derive(Clone, Debug)] +enum Operation { + Intersect, + Subtract, + Union, + Compare, +} + +impl Operation { + pub fn to_char(&self) -> char { + match self { + Self::Intersect => '&', + Self::Subtract => '-', + Self::Union => '+', + Self::Compare => '?', + } + } +} + +impl FromStr for Operation { + type Err = KakError; + + fn from_str(s: &str) -> Result { + match s { + "intersect" | "and" | "&" => Ok(Self::Intersect), + "subtract" | "not" | "minus" | "-" | "\\" => Ok(Self::Subtract), + "union" | "or" | "plus" | "+" => Ok(Self::Union), + "compare" | "cmp" | "?" | "=" => Ok(Self::Compare), + _ => Err(KakError::Parse(format!( + "Set operation '{s}' could not be parsed" + ))), + } + } +} + +pub fn set(options: &Options) -> Result { + let (left_register, operation, right_register) = parse_arguments(&options.args[..])?; + + // Underscore is a special case. We will treat it as the current selection + let (left_selections, right_selections) = match (&left_register, &right_register) { + (Register::Underscore, r) => { + let l_selections = get_selections()?; + kakplugin::restore_register(r)?; + let r_selections = get_selections()?; + + (l_selections, r_selections) + } + (l, Register::Underscore) => { + let r_selections = get_selections()?; + kakplugin::restore_register(l)?; + let l_selections = get_selections()?; + + (l_selections, r_selections) + } + (l, r) => { + kakplugin::restore_register(l)?; + let l_selections = get_selections()?; + + kakplugin::restore_register(r)?; + let r_selections = get_selections()?; + (l_selections, r_selections) + } + }; + + let (left_ordered_counts, right_ordered_counts) = ( + to_ordered_counts(options, left_selections), + to_ordered_counts(options, right_selections), + ); + let (left_keys, right_keys) = ( + left_ordered_counts + .keys() + .collect::>(), + right_ordered_counts + .keys() + .collect::>(), + ); + + let result = key_set_operation(&operation, &left_keys, &right_keys); + + match &operation { + Operation::Compare => compare( + &left_register, + &right_register, + &result, + &left_ordered_counts, + &right_ordered_counts, + )?, + Operation::Intersect | Operation::Subtract | Operation::Union => print_result(&result)?, + } + + Ok(match &operation { + Operation::Compare => format!("Compared {} selections", result.len()), + op => format!( + "{}{}{} returned {} selections", + left_register.to_char(), + op.to_char(), + right_register.to_char(), + result.len() + ), + }) +} + +fn print_result(key_set_operation_result: &LinkedHashSet<&Selection>) -> Result<(), KakError> { + // Manually set selections so we don't have to allocate a string + let mut f = kakplugin::open_command_fifo()?; + + // Send all of this into an evaluate-commands block + // -save-regs '"' + write!( + f, + r#"evaluate-commands %{{ + set-register '"'"# + )?; + + for k in key_set_operation_result { + write!(f, " '{}\n'", kakplugin::escape(k))?; + } + + write!( + f, + r#"; + edit -scratch '*kakplugin-set*'; + execute-keys '%_'; + }}"# + )?; + + f.flush()?; + + Ok(()) +} + +fn compare( + left_register: &Register, + right_register: &Register, + key_set_operation_result: &LinkedHashSet<&Selection>, + left_ordered_counts: &LinkedHashMap, + right_ordered_counts: &LinkedHashMap, +) -> Result<(), KakError> { + // Manually set selections so we don't have to allocate a string + let mut f = kakplugin::open_command_fifo()?; + + // Send all of this into an evaluate-commands block + write!( + f, + // -save-regs '"' + r#"evaluate-commands -save-regs '"' %{{ + set-register '"'"# + )?; + + write!( + f, + " '?\t{}\t{}\tselection\n'", + left_register.to_char(), + right_register.to_char() + )?; + + for k in key_set_operation_result { + let left_count = left_ordered_counts.get(&k as &str).unwrap_or(&0); + let right_count = right_ordered_counts.get(&k as &str).unwrap_or(&0); + + write!( + f, + " '{}\t{}\t{}\t{}\n'", + match (*left_count == 0, *right_count == 0) { + (true, true) => "?", + (true, false) => ">", + (false, true) => "<", + (false, false) => "=", + }, + left_count, + right_count, + // TODO: Do we want to escape the \n to \\n? + // kakplugin::escape(k.replace('\n', "\\n")), + kakplugin::escape(k), + )?; + } + + write!( + f, + r#"; + edit -scratch '*kakplugin-set*'; + execute-keys '%3L)_vb'; + }}"# + )?; + + f.flush()?; + + Ok(()) +} + +fn to_ordered_counts(options: &Options, sels: Vec) -> LinkedHashMap { + let mut ret = LinkedHashMap::new(); + + for i in sels.into_iter() { + let key = if options.no_trim { + i + } else { + i.trim().to_string() + }; + + if key.is_empty() { + // We don't want to even pretend to look at empty keys + continue; + } + + let entry: &mut usize = ret.entry(key).or_insert(0); + *entry = entry.saturating_add(1); + } + ret +} + +fn key_set_operation<'a>( + operation: &Operation, + left_keys: &LinkedHashSet<&'a Selection>, + right_keys: &LinkedHashSet<&'a Selection>, +) -> LinkedHashSet<&'a Selection> { + match operation { + Operation::Intersect => left_keys + .intersection(&right_keys) + // .into_iter() + // TODO: Remove this + .cloned() + .collect(), + Operation::Subtract => left_keys + .difference(&right_keys) + .into_iter() + // TODO: Remove this + .cloned() + .collect(), + Operation::Compare | Operation::Union => left_keys + .union(&right_keys) + .into_iter() + // TODO: Remove this + .cloned() + .collect(), + // TODO: Symmetric difference? + } +} + +fn parse_arguments(args: &[String]) -> Result<(Register, Operation, Register), KakError> { + let args = if args.len() == 1 { + // They gave us something like "a-b" or "c?d" + args.iter() + .flat_map(|s: &String| s.trim().chars()) + .map(|c| String::from(c)) + .collect::>() + } else { + // They gave us something like "a - b" or "c compare d" + args.iter().cloned().collect() + }; + let (left_register, middle, right_register) = match &args[..] { + [l, r] => { + // They only gave us two arguments like "- a" or "b -" + match (Operation::from_str(l), Operation::from_str(r)) { + // If the operation is on the left, then the _ register is the leftmost one + (Ok(o), Err(_)) => Ok((Register::Underscore, o, Register::from_str(r)?)), + // If the operation is on the right, then the _ register is the rightmost one + (Err(_), Ok(o)) => Ok((Register::from_str(l)?, o, Register::Underscore)), + (Ok(_), Ok(_)) => Err(KakError::Custom(format!( + "Arguments '{l}' and '{r}' cannot both be operations" + ))), + (Err(_), Err(_)) => Err(KakError::Custom(format!( + "One argument must be an operation" + ))), + } + } + [l, middle, r] => { + // They gave us three arguments like "a - b" or "_ + a" + Ok(( + Register::from_str(l)?, + Operation::from_str(middle)?, + Register::from_str(r)?, + )) + } + _ => Err(KakError::Custom(format!( + "Invalid arguments to set command" + ))), + }?; + + if left_register == right_register { + return Err(KakError::Custom(format!( + "Registers passed are the same: '{}'", + left_register.to_char() + ))); + } + + Ok((left_register, middle, right_register)) +} diff --git a/src/sort.rs b/src/sort.rs index bcf0498..3274716 100644 --- a/src/sort.rs +++ b/src/sort.rs @@ -125,7 +125,7 @@ pub fn sort(options: &Options) -> Result { .subselections_register .map::, _>(|c| { let subselections = get_selections_with_desc()?; - kakplugin::exec(&format!("exec {}", c))?; + kakplugin::cmd(&format!("exec {}", c))?; Ok(subselections) }) .transpose()?; diff --git a/src/stdin.rs b/src/stdin.rs index 61868a4..23ec5de 100644 --- a/src/stdin.rs +++ b/src/stdin.rs @@ -34,7 +34,6 @@ pub fn stdin(options: &Options) -> Result { )?; // Wait for the background process to exit - // TODO: Do not use a string handle .join() .map_err(|_e| KakError::Custom("Could not join background process".to_string()))??; diff --git a/src/uniq.rs b/src/uniq.rs index 62e0397..6e391ec 100644 --- a/src/uniq.rs +++ b/src/uniq.rs @@ -1,12 +1,11 @@ +use crate::utils; use kakplugin::{ get_selections_desc, get_selections_with_desc, set_selections, set_selections_desc, KakError, SelectionWithDesc, }; use regex::Regex; -use std::{ - collections::{hash_map::DefaultHasher, BTreeSet}, - hash::{Hash, Hasher}, -}; +use std::collections::BTreeSet; + #[derive(clap::StructOpt, Debug)] pub struct Options { #[clap(index = 1)] @@ -26,40 +25,18 @@ pub fn uniq(options: &Options) -> Result { let new_selections: Vec> = selections .into_iter() // Create a BTreeSet of hashes of selections. This way, string content is not stored, but uniqueness can be determined - .scan(BTreeSet::new(), |state, s| { - // Strip whitespace if requested - let mut key = if options.no_skip_whitespace { - s.content.as_str() - } else { - s.content.trim() - }; - - // If they requested a regex match, set the key to the string slice of that match - if let Some(regex_match) = (|| { - let captures = options.regex.as_ref()?.captures(key)?; - captures - .get(1) - .or_else(|| captures.get(0)) - .map(|m| m.as_str()) - })() { - key = regex_match; - } - - // Ignore case if requested - let key = if options.ignore_case { - key.to_lowercase() - } else { - // TODO: Do I really need to clone this? - key.to_string() - }; - - let mut hasher = DefaultHasher::new(); - key.hash(&mut hasher); + .scan(BTreeSet::new(), |state, sd| { + let hash = utils::get_hash( + &sd.content, + !options.no_skip_whitespace, + options.regex.as_ref(), + options.ignore_case, + ); // Try inserting to the hash - if state.insert(hasher.finish()) { + if state.insert(hash) { // True if this is a string we haven't seen before - Some(Some(s)) + Some(Some(sd)) } else { // Nothing was inserted because we already saw this string // Return Some(None) so the iterator can continue diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..a0aa8e2 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,54 @@ +use kakplugin::Selection; +use regex::Regex; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +pub(crate) fn get_key( + selection: &Selection, + skip_whitespace: bool, + regex: Option<&Regex>, + ignore_case: bool, +) -> String { + // Strip whitespace if requested + let mut key = if skip_whitespace { + selection.as_str() + } else { + selection.trim() + }; + + // If they requested a regex match, set the key to the string slice of that match + if let Some(regex_match) = (|| { + let captures = regex.as_ref()?.captures(key)?; + captures + .get(1) + .or_else(|| captures.get(0)) + .map(|m| m.as_str()) + })() { + key = regex_match; + } + + // Ignore case if requested + // Lowercase at the end to not mangle regex + if ignore_case { + key.to_lowercase() + } else { + // TODO: Do not perform an allocation here + key.to_string() + } +} + +/// Get a key out of a selection based on options +pub(crate) fn get_hash( + selection: &Selection, + skip_whitespace: bool, + regex: Option<&Regex>, + ignore_case: bool, +) -> u64 { + let mut hasher = DefaultHasher::new(); + + get_key(selection, skip_whitespace, regex, ignore_case).hash(&mut hasher); + + hasher.finish() +}