diff --git a/copyrat.tmux b/copyrat.tmux index b07d1be..4fff745 100755 --- a/copyrat.tmux +++ b/copyrat.tmux @@ -87,6 +87,8 @@ setup_pattern_binding "h" "--pattern-name sha" setup_pattern_binding "d" "--pattern-name datetime" # prefix + t + e searches for email addresses (see https://www.regular-expressions.info/email.html) setup_pattern_binding "e" "--pattern-name email" +# prefix + t + q searches for strings inside single|double|backticks +setup_pattern_binding "q" "-x quoted-single -x quoted-double -x quoted-tick" # prefix + t + D searches for docker shas setup_pattern_binding "D" "--pattern-name docker" # prefix + t + c searches for hex colors #aa00f5 diff --git a/src/bin/tmux_copyrat.rs b/src/bin/tmux_copyrat.rs index df8b672..49e5d66 100644 --- a/src/bin/tmux_copyrat.rs +++ b/src/bin/tmux_copyrat.rs @@ -9,14 +9,14 @@ fn main() -> Result<(), error::ParseError> { let config = ConfigExt::initialize()?; // Identify active pane and capture its content. - let panes: Vec = tmux::list_panes()?; + let panes: Vec = tmux::available_panes()?; let active_pane = panes .into_iter() .find(|p| p.is_active) .expect("Exactly one tmux pane should be active in the current window."); - let buffer = tmux::capture_pane(&active_pane, &config.capture_region)?; + let buffer = active_pane.capture(&config.capture_region)?; let lines = buffer.split('\n').collect::>(); // We have to dance a little with Panes, because this process' i/o streams diff --git a/src/textbuf/mod.rs b/src/textbuf/mod.rs index 25a0295..86553c4 100644 --- a/src/textbuf/mod.rs +++ b/src/textbuf/mod.rs @@ -573,13 +573,48 @@ mod tests { assert_eq!(spans.get(0).unwrap().text, "2021-03-04T12:23:34"); } + #[test] + fn match_quoted_string() { + let buffer = + r#"Lorem 'first string' and "second string" and `rustc --explain E0223` ipsum."#; + let lines = buffer.split('\n').collect::>(); + + let use_all_patterns = false; + use crate::textbuf::regexes::parse_pattern_name; + let named_pat = vec![ + parse_pattern_name("quoted-single").unwrap(), + parse_pattern_name("quoted-double").unwrap(), + parse_pattern_name("quoted-tick").unwrap(), + ]; + + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let spans = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .spans; + + assert_eq!(spans.len(), 3); + assert_eq!(spans.get(0).unwrap().text, "first string"); + assert_eq!(spans.get(1).unwrap().text, "second string"); + assert_eq!(spans.get(2).unwrap().text, "rustc --explain E0223"); + } + #[test] fn priority_between_regexes() { let buffer = "Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"; let lines = buffer.split('\n').collect::>(); let use_all_patterns = true; let named_pat = vec![]; - let custom: Vec = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"] + let custom: Vec = ["(CUSTOM-[0-9]{4,})", "(ISSUE-[0-9]{3})"] .iter() .map(|&s| s.to_string()) .collect(); diff --git a/src/textbuf/model.rs b/src/textbuf/model.rs index 742b42d..9cf1c51 100644 --- a/src/textbuf/model.rs +++ b/src/textbuf/model.rs @@ -133,17 +133,16 @@ fn find_raw_spans<'a>( if *pat_name != "ansi_colors" { let text = reg_match.as_str(); - // In case the pattern has a capturing group, try obtaining - // that text and start offset, else use the entire match. - let (subtext, substart) = match reg + // All patterns must have a capturing group: try obtaining + // that text and start offset. + let capture = reg .captures_iter(text) .next() .expect("This regex is guaranteed to match.") .get(1) - { - Some(capture) => (capture.as_str(), capture.start()), - None => (text, 0), - }; + .expect("This regex should have a capture group."); + + let (subtext, substart) = (capture.as_str(), capture.start()); raw_spans.push(RawSpan { x: offset + reg_match.start() as i32 + substart as i32, diff --git a/src/textbuf/regexes.rs b/src/textbuf/regexes.rs index 31603ea..50fc9a2 100644 --- a/src/textbuf/regexes.rs +++ b/src/textbuf/regexes.rs @@ -1,3 +1,7 @@ +//! This module defines the regex patterns. +//! +//! All patterns must have one capture group. The first group is used. + use crate::error; pub(super) const EXCLUDE_PATTERNS: [(&str, &str); 1] = @@ -6,37 +10,40 @@ pub(super) const EXCLUDE_PATTERNS: [(&str, &str); 1] = /// Holds all the regex patterns that are currently supported. /// /// The email address was obtained at https://www.regular-expressions.info/email.html. -/// Others were obtained from Ferran Basora. -pub(super) const PATTERNS: [(&str, &str); 17] = [ +/// Some others were obtained from Ferran Basora, the rest is by me. +pub(super) const PATTERNS: [(&str, &str); 20] = [ ("markdown-url", r"\[[^]]*\]\(([^)]+)\)"), ( "url", r"((https?://|git@|git://|ssh://|ftp://|file:///)[^ \(\)\[\]\{\}]+)", ), - ("email", r"\b[A-z0-9._%+-]+@[A-z0-9.-]+\.[A-z]{2,}\b"), + ("email", r"\b([A-z0-9._%+-]+@[A-z0-9.-]+\.[A-z]{2,})\b"), ("diff-a", r"--- a/([^ ]+)"), ("diff-b", r"\+\+\+ b/([^ ]+)"), ("docker", r"sha256:([0-9a-f]{64})"), ("path", r"(([.\w\-@~]+)?(/[.\w\-@]+)+)"), - ("hexcolor", r"#[0-9a-fA-F]{6}"), + ("hexcolor", r"(#[0-9a-fA-F]{6})"), ( "uuid", - r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})", ), ( "version", r"(v?\d{1,4}\.\d{1,4}(\.\d{1,4})?(-(alpha|beta|rc)(\.\d)?)?)[^.0-9s]", ), - ("ipfs", r"Qm[0-9a-zA-Z]{44}"), - ("sha", r"[0-9a-f]{7,40}"), - ("ipv4", r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"), - ("ipv6", r"[A-f0-9:]+:+[A-f0-9:]+[%\w\d]+"), - ("pointer-address", r"0x[0-9a-fA-F]+"), + ("ipfs", r"(Qm[0-9a-zA-Z]{44})"), + ("sha", r"([0-9a-f]{7,40})"), + ("ipv4", r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"), + ("ipv6", r"([A-f0-9:]+:+[A-f0-9:]+[%\w\d]+)"), + ("pointer-address", r"(0x[0-9a-fA-F]+)"), ( "datetime", r"(\d{4}-?\d{2}-?\d{2}([ T]\d{2}:\d{2}:\d{2}(\.\d{3,9})?)?)", ), - ("digits", r"[0-9]{4,}"), + ("quoted-single", r#"'([^']+)'"#), + ("quoted-double", r#""([^"]+)""#), + ("quoted-tick", r#"`([^`]+)`"#), + ("digits", r"([0-9]{4,})"), ]; /// Type-safe string Pattern Name (newtype). diff --git a/src/tmux.rs b/src/tmux.rs index 4e5c459..e019526 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -53,11 +53,6 @@ impl FromStr for Pane { // Pane id must be start with '%' followed by a `u32` let id_str = iter.next().unwrap(); let id = PaneId::from_str(id_str)?; - // if !id_str.starts_with('%') { - // return Err(ParseError::ExpectedPaneIdMarker); - // } - // let id = id_str[1..].parse::()?; - // let id = format!("%{}", id); let is_copy_mode = iter.next().unwrap().parse::()?; @@ -83,6 +78,53 @@ impl FromStr for Pane { } } +impl Pane { + /// Returns the entire Pane content as a `String`. + /// + /// The provided `region` specifies if the visible area is captured, or the + /// entire history. + /// + /// # Note + /// + /// In Tmux, the start line is the line at the top of the pane. The end line + /// is the last line at the bottom of the pane. + /// + /// - In normal mode, the index of the start line is always 0. The index of + /// the end line is always the pane's height minus one. These do not need to + /// be specified when capturing the pane's content. + /// + /// - If navigating history in copy mode, the index of the start line is the + /// opposite of the pane's scroll position. For instance a pane of 40 lines, + /// scrolled up by 3 lines. It is necessarily in copy mode. Its start line + /// index is `-3`. The index of the last line is `(40-1) - 3 = 36`. + /// + pub fn capture(&self, region: &CaptureRegion) -> Result { + let mut args_str = format!("capture-pane -t {pane_id} -J -p", pane_id = self.id); + + let region_str = match region { + CaptureRegion::VisibleArea => { + if self.is_copy_mode && self.scroll_position > 0 { + format!( + " -S {start} -E {end}", + start = -self.scroll_position, + end = self.height - self.scroll_position - 1 + ) + } else { + String::new() + } + } + CaptureRegion::EntireHistory => String::from(" -S - -E -"), + }; + + args_str.push_str(®ion_str); + + let args: Vec<&str> = args_str.split(' ').collect(); + + let output = duct::cmd("tmux", &args).read()?; + Ok(output) + } +} + #[derive(Debug, PartialEq)] pub struct PaneId(String); @@ -90,12 +132,12 @@ impl FromStr for PaneId { type Err = ParseError; /// Parse into PaneId. The `&str` must be start with '%' - /// followed by a `u32`. + /// followed by a `u16`. fn from_str(src: &str) -> Result { if !src.starts_with('%') { return Err(ParseError::ExpectedPaneIdMarker); } - let id = src[1..].parse::()?; + let id = src[1..].parse::()?; let id = format!("%{}", id); Ok(PaneId(id)) } @@ -114,7 +156,7 @@ impl fmt::Display for PaneId { } /// Returns a list of `Pane` from the current tmux session. -pub fn list_panes() -> Result, ParseError> { +pub fn available_panes() -> Result, ParseError> { let args = vec![ "list-panes", "-F", @@ -162,51 +204,6 @@ pub fn get_options(prefix: &str) -> Result, ParseError> Ok(args) } -/// Returns the entire Pane content as a `String`. -/// -/// The provided `region` specifies if the visible area is captured, or the -/// entire history. -/// -/// # Note -/// -/// In Tmux, the start line is the line at the top of the pane. The end line -/// is the last line at the bottom of the pane. -/// -/// - In normal mode, the index of the start line is always 0. The index of -/// the end line is always the pane's height minus one. These do not need to -/// be specified when capturing the pane's content. -/// -/// - If navigating history in copy mode, the index of the start line is the -/// opposite of the pane's scroll position. For instance a pane of 40 lines, -/// scrolled up by 3 lines. It is necessarily in copy mode. Its start line -/// index is `-3`. The index of the last line is `(40-1) - 3 = 36`. -/// -pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result { - let mut args_str = format!("capture-pane -t {pane_id} -J -p", pane_id = pane.id); - - let region_str = match region { - CaptureRegion::VisibleArea => { - if pane.is_copy_mode && pane.scroll_position > 0 { - format!( - " -S {start} -E {end}", - start = -pane.scroll_position, - end = pane.height - pane.scroll_position - 1 - ) - } else { - String::new() - } - } - CaptureRegion::EntireHistory => String::from(" -S - -E -"), - }; - - args_str.push_str(®ion_str); - - let args: Vec<&str> = args_str.split(' ').collect(); - - let output = duct::cmd("tmux", &args).read()?; - Ok(output) -} - /// Ask tmux to swap the current Pane with the target_pane (uses Tmux format). pub fn swap_pane_with(target_pane: &str) -> Result<(), ParseError> { // -Z: keep the window zoomed if it was zoomed.