From 3d867c7f0d503f65c685d9f9b2ea91668b8063ac Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Sat, 4 Jun 2022 23:45:05 -0400 Subject: [PATCH] Start splitting code into module --- Cargo.lock | 8 ++ Cargo.toml | 22 +++++ kakplugin/Cargo.toml | 9 ++ kakplugin/src/errors.rs | 32 +++++++ kakplugin/src/lib.rs | 198 ++++++++++++++++++++++++++++++++++++++++ kakplugin/src/types.rs | 141 ++++++++++++++++++++++++++++ src/main.rs | 7 ++ src/uniq.rs | 15 +-- 8 files changed, 425 insertions(+), 7 deletions(-) create mode 100644 kakplugin/Cargo.toml create mode 100644 kakplugin/src/errors.rs create mode 100644 kakplugin/src/lib.rs create mode 100644 kakplugin/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 507279d..d7f9edd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,13 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "kakplugin" +version = "0.1.0" +dependencies = [ + "shellwords", +] + [[package]] name = "kakutils-rs" version = "0.1.0" @@ -131,6 +138,7 @@ dependencies = [ "alphanumeric-sort", "clap", "evalexpr", + "kakplugin", "rand", "regex", "shellwords", diff --git a/Cargo.toml b/Cargo.toml index 048ec85..82897e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,5 @@ +# syntax = denzp/cargo-wharf-frontend + # cargo-features = ["strip"] [package] name = "kakutils-rs" @@ -7,6 +9,25 @@ license = "MIT" readme = "README.adoc" keywords = ["cli", "kakoune"] +[workspace] +members = [ + ".", + "./kakplugin/", +] + +[[package.metadata.wharf.binary]] +name = "kakutils-rs" +destination = "/bin/kakutils-rs" + +[package.metadata.wharf.builder] +# image = "rust" +# image = "ekidd/rust-musl-builder" +image = "clux/muslrust:nightly-2021-03-02" +target = "x86_64-unknown-linux-musl" + +[package.metadata.wharf.output] +image = "scratch" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -17,6 +38,7 @@ alphanumeric-sort = "1" shellwords = "1" rand = "0.8" evalexpr = "7" +kakplugin = {path = "./kakplugin/"} [profile.release] lto = true diff --git a/kakplugin/Cargo.toml b/kakplugin/Cargo.toml new file mode 100644 index 0000000..b9269e0 --- /dev/null +++ b/kakplugin/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "kakplugin" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +shellwords = "1" diff --git a/kakplugin/src/errors.rs b/kakplugin/src/errors.rs new file mode 100644 index 0000000..d00d0b7 --- /dev/null +++ b/kakplugin/src/errors.rs @@ -0,0 +1,32 @@ +use std::num::ParseIntError; + +#[derive(Debug)] +pub enum KakError { + /// A required environment variable was not set + EnvVarNotSet(String), + /// An environment variable was not parsable in unicode + EnvVarUnicode(String), + /// There was an error parsing a response from kak + Parse(String), + /// There was an error with a response kak gave + KakResponse(String), + /// IO Error + Io(std::io::Error), +} +impl From for KakError { + fn from(e: std::io::Error) -> Self { + Self::Io(e) + } +} + +impl From for KakError { + fn from(e: shellwords::MismatchedQuotes) -> Self { + Self::Parse(format!("Shell could not be parsed: {e:?}")) + } +} + +impl From for KakError { + fn from(e: ParseIntError) -> Self { + Self::Parse(format!("Could not parse as integer: {e:?}")) + } +} diff --git a/kakplugin/src/lib.rs b/kakplugin/src/lib.rs new file mode 100644 index 0000000..ebb9867 --- /dev/null +++ b/kakplugin/src/lib.rs @@ -0,0 +1,198 @@ +mod errors; +mod types; +pub use errors::KakError; +use std::{ + env, fmt, + fs::{self, File, OpenOptions}, + io::{BufWriter, Write}, + str::FromStr, +}; +pub use types::{ + AnchorPosition, Selection, SelectionDesc, SelectionWithDesc, SelectionWithSubselections, +}; + +/// # Errors +/// +/// Will return `Err` if command fifo could not be opened, read from, or written to +pub fn get_selections() -> Result, KakError> { + response("%val{selections}") +} + +/// # Errors +/// +/// Will return `Err` if command fifo could not be opened, read from, or written to +pub fn get_selections_desc() -> Result, KakError> { + response("%val{selections_desc}")? + .iter() + .map(|sd| SelectionDesc::from_str(sd)) + .collect::, KakError>>() +} + +// pub fn get_selections_with_subselections( +// register: &str, +// ) -> Result, KakError> { +// // TODO: Escape register +// let subselections = get_selections_with_desc()?; +// exec(format!("\"{}z", register.replace('\'', "''")))?; +// let selections = get_selections_with_desc()?; + +// for sel in selections { +// for i in subselections {} +// } +// } + +/// # Errors +/// +/// Will return `Err` if command fifo could not be opened, read from, or written to, +/// or if `selections.len() != selections_desc.len` +pub fn get_selections_with_desc() -> Result, KakError> { + let mut selections = get_selections()?; + let selections_desc = get_selections_desc()?; + + if selections.len() != selections_desc.len() { + return Err(KakError::KakResponse(format!( + "When requesting selections (={}) and selections_desc (={}), their count did not match", + selections.len(), + selections_desc.len() + ))); + } + + let min_selection = selections_desc.iter().min().ok_or_else(|| { + KakError::KakResponse(format!( + "Selections are empty, which should not be possible" + )) + })?; + + // Kakoune prints selections in file order, but prints selections_desc rotated based on current selection + // Ex: + // [a] [b] (c) [d] where () is primary selection + // selections: a b c d + // selections_desc: c d a b + + // Need to rotate selections by primary selection's position in the list + match selections_desc.iter().position(|p| p == min_selection) { + Some(i) => { + selections.rotate_right(i); + } + None => { + return Err(KakError::KakResponse(format!( + "Primary selection {} not found in selection_desc list ({:#?})", + min_selection, selections_desc + ))); + } + } + + selections + .into_iter() + .zip(selections_desc.into_iter()) + .map(|(content, desc)| Ok(SelectionWithDesc { content, desc })) + .collect::, _>>() +} + +/// # Errors +/// +/// Will return `Err` if command fifo could not be opened, read from, or written to +pub fn set_selections<'a, I, S: 'a + ?Sized>(selections: I) -> Result<(), KakError> +where + I: IntoIterator, + S: AsRef + fmt::Display, +{ + let mut f = open_command_fifo()?; + write!(f, "reg '\"'")?; + for i in selections { + write!(f, " '{}'", i.as_ref().replace('\'', "''"))?; + } + write!(f, "; exec R;")?; + f.flush()?; + Ok(()) +} + +/// # Errors +/// +/// Will return `Err` if command fifo could not be opened, read from, or written to +pub fn set_selections_desc<'a, I>(selections: I) -> Result<(), KakError> +where + I: IntoIterator, +{ + let mut f = open_command_fifo()?; + write!(f, "select")?; + for i in selections { + write!(f, " {}", i)?; + } + write!(f, ";")?; + f.flush()?; + Ok(()) +} + +/// # Errors +/// +/// Will return `Err` if command fifo could not be opened, read from, or written to +pub fn send_message>(message: S, debug_message: Option) -> Result<(), KakError> { + let msg_str = message.as_ref().replace('\'', "''"); + { + let mut f = open_command_fifo()?; + + write!(f, "echo '{}';", msg_str)?; + 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('\'', "''") + )?; + } + f.flush()?; + } + Ok(()) +} + +/// # Errors +/// +/// Will return `Err` if command fifo could not be opened or written to +pub fn exec(cmd: &str) -> Result<(), KakError> { + let mut f = open_command_fifo()?; + + write!(f, "{}", cmd)?; + f.flush().map_err(Into::into) +} + +/// # Errors +/// +/// Will return `Err` if command fifo could not be opened or written to +pub fn response(msg: &str) -> Result, KakError> { + exec(&format!( + "echo -quoting shell -to-file {} -- {msg}", + get_var("kak_response_fifo")? + ))?; + + let selections = shellwords::split(&fs::read_to_string(&get_var("kak_response_fifo")?)?)?; + + Ok(selections) +} + +/// # Errors +/// +/// Will return `Err` if command fifo could not be opened +pub fn open_command_fifo() -> Result, KakError> { + OpenOptions::new() + .write(true) + .append(true) + .open(&get_var("kak_command_fifo")?) + .map(BufWriter::new) + .map_err(Into::into) +} + +/// # Errors +/// +/// Will return `Err` if requested environment variable is not unicode or not present +fn get_var(var_name: &str) -> Result { + env::var(var_name).map_err(|e| match e { + env::VarError::NotPresent => { + KakError::EnvVarNotSet(format!("Env var {} is not defined", var_name)) + } + env::VarError::NotUnicode(_) => { + KakError::EnvVarUnicode(format!("Env var {} is not unicode", var_name)) + } + }) +} diff --git a/kakplugin/src/types.rs b/kakplugin/src/types.rs new file mode 100644 index 0000000..b69555b --- /dev/null +++ b/kakplugin/src/types.rs @@ -0,0 +1,141 @@ +use crate::KakError; +use std::{fmt, str::FromStr}; + +pub type Selection = String; + +#[derive(PartialEq, Eq, Debug)] +pub struct SelectionWithDesc { + pub content: Selection, + pub desc: SelectionDesc, +} + +#[derive(PartialEq, Eq, Debug)] +pub struct SelectionWithSubselections { + pub selection: SelectionWithDesc, + pub subselections: Vec, +} + +#[derive(PartialEq, PartialOrd, Ord, Eq, Debug)] +pub struct SelectionDesc { + pub left: AnchorPosition, + pub right: AnchorPosition, +} + +impl SelectionDesc { + #[must_use] + pub 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(), + } + } + } + + #[must_use] + pub 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_a.right + } +} + +impl fmt::Display for SelectionDesc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{},{}", self.left, self.right) + } +} + +impl FromStr for SelectionDesc { + type Err = KakError; + fn from_str(s: &str) -> Result { + let (left, right) = s + .split_once(',') + .ok_or_else(|| KakError::Parse(format!("Could not parse as position: {}", s)))?; + + Ok(Self { + left: AnchorPosition::from_str(left)?, + right: AnchorPosition::from_str(right)?, + }) + } +} + +#[derive(PartialOrd, PartialEq, Clone, Eq, Ord, Debug)] +pub struct AnchorPosition { + pub row: usize, + pub col: usize, +} +impl fmt::Display for AnchorPosition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}", self.row, self.col) + } +} + +impl FromStr for AnchorPosition { + type Err = KakError; + fn from_str(s: &str) -> Result { + let (left, right) = s + .split_once('.') + .ok_or_else(|| KakError::Parse(format!("Could not parse as position: {}", s)))?; + Ok(Self { + row: usize::from_str(left)?, + col: usize::from_str(right)?, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + const SD: SelectionDesc = SelectionDesc { + left: AnchorPosition { row: 18, col: 9 }, + right: AnchorPosition { row: 10, col: 1 }, + }; + #[test] + fn test_anchor_position() { + // Check parsing + assert_eq!(SelectionDesc::from_str("18.9,10.1").unwrap(), SD); + // Check if multiple parsed ones match + assert_eq!( + SelectionDesc::from_str("18.9,10.1").unwrap(), + SelectionDesc::from_str("18.9,10.1").unwrap() + ); + } + + #[test] + fn test_sort() { + // Check if sorting works + assert_eq!(SD.sort(), SelectionDesc::from_str("10.1,18.9").unwrap()); + assert_eq!(SD.sort(), SD.sort().sort()); + } + + #[test] + fn test_contains() { + assert!(SD.contains(&SD)); + assert!(SD.contains(&SelectionDesc::from_str("17.9,10.1").unwrap())); + assert!(SD.contains(&SelectionDesc::from_str("18.8,10.1").unwrap())); + assert!(SD.contains(&SelectionDesc::from_str("18.9,11.1").unwrap())); + assert!(SD.contains(&SelectionDesc::from_str("18.9,10.2").unwrap())); + assert!(SD.contains(&SelectionDesc::from_str("10.1,17.9").unwrap())); + assert!(SD.contains(&SelectionDesc::from_str("10.1,18.8").unwrap())); + assert!(SD.contains(&SelectionDesc::from_str("11.1,18.9").unwrap())); + assert!(SD.contains(&SelectionDesc::from_str("10.2,18.9").unwrap())); + assert!(!SD.contains(&SelectionDesc::from_str("19.9,10.1").unwrap())); + assert!(!SD.contains(&SelectionDesc::from_str("18.10,10.1").unwrap())); + assert!(!SD.contains(&SelectionDesc::from_str("18.9,9.1").unwrap())); + assert!(!SD.contains(&SelectionDesc::from_str("18.9,10.0").unwrap())); + assert!(!SD.contains(&SelectionDesc::from_str("10.1,19.9").unwrap())); + assert!(!SD.contains(&SelectionDesc::from_str("10.1,18.10").unwrap())); + assert!(!SD.contains(&SelectionDesc::from_str("9.1,18.9").unwrap())); + assert!(!SD.contains(&SelectionDesc::from_str("10.0,18.9").unwrap())); + } +} diff --git a/src/main.rs b/src/main.rs index baea35b..71ea843 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,13 @@ enum Commands { } fn main() { + if get_var("kak_command_fifo") + .and(get_var("kak_response_fifo")) + .is_err() + { + panic!("Environment variable kak_command_fifo and kak_response_fifo must be set"); + } + let msg = match run() { Ok(msg) => msg, Err(msg) => { diff --git a/src/uniq.rs b/src/uniq.rs index e27fbc7..02a281a 100644 --- a/src/uniq.rs +++ b/src/uniq.rs @@ -1,5 +1,6 @@ -use crate::{ - get_selections_desc, set_selections, set_selections_desc, KakMessage, SelectionWithDesc, +use kakplugin::{ + get_selections_desc, get_selections_with_desc, set_selections, set_selections_desc, KakError, + SelectionWithDesc, }; use regex::Regex; use std::{ @@ -16,8 +17,8 @@ pub struct Options { #[clap(short = 'S', long)] no_skip_whitespace: bool, } -pub fn uniq(options: &Options) -> Result { - let mut selections = crate::get_selections_with_desc()?; +pub fn uniq(options: &Options) -> Result { + let mut selections = get_selections_with_desc()?; // Sort selections so the first element is the unique one, not an arbitrary one based on primary selection selections.sort_by_key(|s| s.desc.sort()); @@ -89,8 +90,8 @@ pub fn uniq(options: &Options) -> Result { let old_count = new_selections.len(); let new_count = new_selections.iter().flatten().count(); - Ok(KakMessage( - format!("{} unique selections out of {}", new_count, old_count), - None, + Ok(format!( + "{} unique selections out of {}", + new_count, old_count )) }