372 lines
11 KiB
Rust
372 lines
11 KiB
Rust
// 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,
|
|
help = "Register operation and operand. Empty register is current selection. Example: 'a-b' or '+b'"
|
|
)]
|
|
args: Vec<String>,
|
|
|
|
#[clap(
|
|
short = 'T',
|
|
help = "Do not trim the selections before doing set operations"
|
|
)]
|
|
no_trim: bool,
|
|
// #[clap(short, long)]
|
|
// regex: Option<Regex>,
|
|
// #[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 const 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<Self, Self::Err> {
|
|
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<String, KakError> {
|
|
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::<LinkedHashSet<&Selection>>(),
|
|
right_ordered_counts
|
|
.keys()
|
|
.collect::<LinkedHashSet<&Selection>>(),
|
|
);
|
|
|
|
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::Union => print_result(&result)?,
|
|
Operation::Intersect | Operation::Subtract => {
|
|
if left_register == Register::Underscore {
|
|
// If the user asked for an intersection or subtraction from the current selection, we can update selection_descs only
|
|
reduce_selections(&options, &result)?
|
|
} else {
|
|
// The user asked for registers that *aren't* the current selection
|
|
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()
|
|
),
|
|
})
|
|
}
|
|
|
|
/// Reduces selections to those that are in the key_set_operation_result
|
|
fn reduce_selections(
|
|
options: &Options,
|
|
key_set_operation_result: &LinkedHashSet<&Selection>,
|
|
) -> Result<(), KakError> {
|
|
kakplugin::restore_register(&Register::Caret)?;
|
|
let selections_with_desc = {
|
|
let mut r = get_selections_with_desc()?;
|
|
r.sort_by_key(|s| s.desc.sort());
|
|
r
|
|
};
|
|
|
|
eprintln!("Key set operation result: {:?}", key_set_operation_result);
|
|
|
|
set_selections_desc(selections_with_desc.into_iter().filter_map(|swd| {
|
|
// Does not matter if the operation was - or &
|
|
// Since key_set_operation_result contains elements that should be in the set,
|
|
// we can just use contains here
|
|
let key = into_key(options, swd.content)?;
|
|
if key_set_operation_result.contains(&key) {
|
|
Some(swd.desc)
|
|
} else {
|
|
eprintln!("Key {key} not found");
|
|
None
|
|
}
|
|
}))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
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 '%<a-R>_';
|
|
}}"#
|
|
)?;
|
|
|
|
f.flush()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn compare(
|
|
left_register: &Register,
|
|
right_register: &Register,
|
|
key_set_operation_result: &LinkedHashSet<&Selection>,
|
|
left_ordered_counts: &LinkedHashMap<Selection, usize>,
|
|
right_ordered_counts: &LinkedHashMap<Selection, usize>,
|
|
) -> 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 '%<a-R><a-;>3<a-W>L)<a-space>_vb';
|
|
}}"#
|
|
)?;
|
|
|
|
f.flush()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn to_ordered_counts(options: &Options, sels: Vec<Selection>) -> LinkedHashMap<Selection, usize> {
|
|
let mut ret = LinkedHashMap::new();
|
|
|
|
for i in sels {
|
|
match into_key(options, i) {
|
|
Some(key) => {
|
|
let entry: &mut usize = ret.entry(key).or_insert(0);
|
|
*entry = entry.saturating_add(1);
|
|
}
|
|
None => {
|
|
// We don't want to even pretend to look at empty keys
|
|
}
|
|
}
|
|
}
|
|
ret
|
|
}
|
|
|
|
fn into_key(options: &Options, sel: Selection) -> Option<Selection> {
|
|
let key = if options.no_trim {
|
|
sel
|
|
} else {
|
|
sel.trim().to_string()
|
|
};
|
|
|
|
if key.is_empty() {
|
|
// Never treat an empty string as a key
|
|
None
|
|
} else {
|
|
Some(key)
|
|
}
|
|
}
|
|
|
|
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
|
|
.copied()
|
|
.collect(),
|
|
Operation::Subtract => left_keys
|
|
.difference(right_keys)
|
|
.into_iter()
|
|
// TODO: Remove this
|
|
.copied()
|
|
.collect(),
|
|
Operation::Compare | Operation::Union => left_keys
|
|
.union(right_keys)
|
|
.into_iter()
|
|
// TODO: Remove this
|
|
.copied()
|
|
.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(String::from)
|
|
.collect::<Vec<String>>()
|
|
} else {
|
|
// They gave us something like "a - b" or "c compare d"
|
|
args.to_vec()
|
|
};
|
|
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(
|
|
"One argument must be an operation".to_string(),
|
|
)),
|
|
}
|
|
}
|
|
[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(
|
|
"Invalid arguments to set command".to_string(),
|
|
)),
|
|
}?;
|
|
|
|
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))
|
|
}
|