Merge pull request 'Cleanup tmux & add pattern for quoted strings' (#3) from regex into master

Reviewed-on: https://gitea.grael.cc/grael/tmux-feat: copyrat/pulls/3
This commit is contained in:
graelo 2021-03-27 16:14:45 +00:00
commit 62a5d10746
6 changed files with 114 additions and 74 deletions

View file

@ -87,6 +87,8 @@ setup_pattern_binding "h" "--pattern-name sha"
setup_pattern_binding "d" "--pattern-name datetime" setup_pattern_binding "d" "--pattern-name datetime"
# prefix + t + e searches for email addresses (see https://www.regular-expressions.info/email.html) # prefix + t + e searches for email addresses (see https://www.regular-expressions.info/email.html)
setup_pattern_binding "e" "--pattern-name email" 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 # prefix + t + D searches for docker shas
setup_pattern_binding "D" "--pattern-name docker" setup_pattern_binding "D" "--pattern-name docker"
# prefix + t + c searches for hex colors #aa00f5 # prefix + t + c searches for hex colors #aa00f5

View file

@ -9,14 +9,14 @@ fn main() -> Result<(), error::ParseError> {
let config = ConfigExt::initialize()?; let config = ConfigExt::initialize()?;
// Identify active pane and capture its content. // Identify active pane and capture its content.
let panes: Vec<tmux::Pane> = tmux::list_panes()?; let panes: Vec<tmux::Pane> = tmux::available_panes()?;
let active_pane = panes let active_pane = panes
.into_iter() .into_iter()
.find(|p| p.is_active) .find(|p| p.is_active)
.expect("Exactly one tmux pane should be active in the current window."); .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::<Vec<_>>(); let lines = buffer.split('\n').collect::<Vec<_>>();
// We have to dance a little with Panes, because this process' i/o streams // We have to dance a little with Panes, because this process' i/o streams

View file

@ -573,13 +573,48 @@ mod tests {
assert_eq!(spans.get(0).unwrap().text, "2021-03-04T12:23:34"); 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::<Vec<_>>();
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] #[test]
fn priority_between_regexes() { 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 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::<Vec<_>>(); let lines = buffer.split('\n').collect::<Vec<_>>();
let use_all_patterns = true; let use_all_patterns = true;
let named_pat = vec![]; let named_pat = vec![];
let custom: Vec<String> = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"] let custom: Vec<String> = ["(CUSTOM-[0-9]{4,})", "(ISSUE-[0-9]{3})"]
.iter() .iter()
.map(|&s| s.to_string()) .map(|&s| s.to_string())
.collect(); .collect();

View file

@ -133,17 +133,16 @@ fn find_raw_spans<'a>(
if *pat_name != "ansi_colors" { if *pat_name != "ansi_colors" {
let text = reg_match.as_str(); let text = reg_match.as_str();
// In case the pattern has a capturing group, try obtaining // All patterns must have a capturing group: try obtaining
// that text and start offset, else use the entire match. // that text and start offset.
let (subtext, substart) = match reg let capture = reg
.captures_iter(text) .captures_iter(text)
.next() .next()
.expect("This regex is guaranteed to match.") .expect("This regex is guaranteed to match.")
.get(1) .get(1)
{ .expect("This regex should have a capture group.");
Some(capture) => (capture.as_str(), capture.start()),
None => (text, 0), let (subtext, substart) = (capture.as_str(), capture.start());
};
raw_spans.push(RawSpan { raw_spans.push(RawSpan {
x: offset + reg_match.start() as i32 + substart as i32, x: offset + reg_match.start() as i32 + substart as i32,

View file

@ -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; use crate::error;
pub(super) const EXCLUDE_PATTERNS: [(&str, &str); 1] = 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. /// Holds all the regex patterns that are currently supported.
/// ///
/// The email address was obtained at https://www.regular-expressions.info/email.html. /// The email address was obtained at https://www.regular-expressions.info/email.html.
/// Others were obtained from Ferran Basora. /// Some others were obtained from Ferran Basora, the rest is by me.
pub(super) const PATTERNS: [(&str, &str); 17] = [ pub(super) const PATTERNS: [(&str, &str); 20] = [
("markdown-url", r"\[[^]]*\]\(([^)]+)\)"), ("markdown-url", r"\[[^]]*\]\(([^)]+)\)"),
( (
"url", "url",
r"((https?://|git@|git://|ssh://|ftp://|file:///)[^ \(\)\[\]\{\}]+)", 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-a", r"--- a/([^ ]+)"),
("diff-b", r"\+\+\+ b/([^ ]+)"), ("diff-b", r"\+\+\+ b/([^ ]+)"),
("docker", r"sha256:([0-9a-f]{64})"), ("docker", r"sha256:([0-9a-f]{64})"),
("path", r"(([.\w\-@~]+)?(/[.\w\-@]+)+)"), ("path", r"(([.\w\-@~]+)?(/[.\w\-@]+)+)"),
("hexcolor", r"#[0-9a-fA-F]{6}"), ("hexcolor", r"(#[0-9a-fA-F]{6})"),
( (
"uuid", "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", "version",
r"(v?\d{1,4}\.\d{1,4}(\.\d{1,4})?(-(alpha|beta|rc)(\.\d)?)?)[^.0-9s]", 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}"), ("ipfs", r"(Qm[0-9a-zA-Z]{44})"),
("sha", r"[0-9a-f]{7,40}"), ("sha", r"([0-9a-f]{7,40})"),
("ipv4", r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"), ("ipv4", r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"),
("ipv6", r"[A-f0-9:]+:+[A-f0-9:]+[%\w\d]+"), ("ipv6", r"([A-f0-9:]+:+[A-f0-9:]+[%\w\d]+)"),
("pointer-address", r"0x[0-9a-fA-F]+"), ("pointer-address", r"(0x[0-9a-fA-F]+)"),
( (
"datetime", "datetime",
r"(\d{4}-?\d{2}-?\d{2}([ T]\d{2}:\d{2}:\d{2}(\.\d{3,9})?)?)", 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). /// Type-safe string Pattern Name (newtype).

View file

@ -53,11 +53,6 @@ impl FromStr for Pane {
// Pane id must be start with '%' followed by a `u32` // Pane id must be start with '%' followed by a `u32`
let id_str = iter.next().unwrap(); let id_str = iter.next().unwrap();
let id = PaneId::from_str(id_str)?; let id = PaneId::from_str(id_str)?;
// if !id_str.starts_with('%') {
// return Err(ParseError::ExpectedPaneIdMarker);
// }
// let id = id_str[1..].parse::<u32>()?;
// let id = format!("%{}", id);
let is_copy_mode = iter.next().unwrap().parse::<bool>()?; let is_copy_mode = iter.next().unwrap().parse::<bool>()?;
@ -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<String, ParseError> {
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(&region_str);
let args: Vec<&str> = args_str.split(' ').collect();
let output = duct::cmd("tmux", &args).read()?;
Ok(output)
}
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct PaneId(String); pub struct PaneId(String);
@ -90,12 +132,12 @@ impl FromStr for PaneId {
type Err = ParseError; type Err = ParseError;
/// Parse into PaneId. The `&str` must be start with '%' /// Parse into PaneId. The `&str` must be start with '%'
/// followed by a `u32`. /// followed by a `u16`.
fn from_str(src: &str) -> Result<Self, Self::Err> { fn from_str(src: &str) -> Result<Self, Self::Err> {
if !src.starts_with('%') { if !src.starts_with('%') {
return Err(ParseError::ExpectedPaneIdMarker); return Err(ParseError::ExpectedPaneIdMarker);
} }
let id = src[1..].parse::<u32>()?; let id = src[1..].parse::<u16>()?;
let id = format!("%{}", id); let id = format!("%{}", id);
Ok(PaneId(id)) Ok(PaneId(id))
} }
@ -114,7 +156,7 @@ impl fmt::Display for PaneId {
} }
/// Returns a list of `Pane` from the current tmux session. /// Returns a list of `Pane` from the current tmux session.
pub fn list_panes() -> Result<Vec<Pane>, ParseError> { pub fn available_panes() -> Result<Vec<Pane>, ParseError> {
let args = vec![ let args = vec![
"list-panes", "list-panes",
"-F", "-F",
@ -162,51 +204,6 @@ pub fn get_options(prefix: &str) -> Result<HashMap<String, String>, ParseError>
Ok(args) 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<String, ParseError> {
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(&region_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). /// Ask tmux to swap the current Pane with the target_pane (uses Tmux format).
pub fn swap_pane_with(target_pane: &str) -> Result<(), ParseError> { pub fn swap_pane_with(target_pane: &str) -> Result<(), ParseError> {
// -Z: keep the window zoomed if it was zoomed. // -Z: keep the window zoomed if it was zoomed.