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"
# 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

View file

@ -9,14 +9,14 @@ fn main() -> Result<(), error::ParseError> {
let config = ConfigExt::initialize()?;
// 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
.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::<Vec<_>>();
// 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");
}
#[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]
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::<Vec<_>>();
let use_all_patterns = true;
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()
.map(|&s| s.to_string())
.collect();

View file

@ -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,

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;
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).

View file

@ -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::<u32>()?;
// let id = format!("%{}", id);
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)]
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<Self, Self::Err> {
if !src.starts_with('%') {
return Err(ParseError::ExpectedPaneIdMarker);
}
let id = src[1..].parse::<u32>()?;
let id = src[1..].parse::<u16>()?;
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<Vec<Pane>, ParseError> {
pub fn available_panes() -> Result<Vec<Pane>, ParseError> {
let args = vec![
"list-panes",
"-F",
@ -162,51 +204,6 @@ pub fn get_options(prefix: &str) -> Result<HashMap<String, String>, 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<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).
pub fn swap_pane_with(target_pane: &str) -> Result<(), ParseError> {
// -Z: keep the window zoomed if it was zoomed.