Merge pull request 'Refactoring' (#2) from clean into master

Reviewed-on: https://gitea.grael.cc/grael/tmux-feat: copyrat/pulls/2
This commit is contained in:
graelo 2021-03-25 18:42:26 +00:00
commit 9a69cf5819
19 changed files with 542 additions and 484 deletions

View file

@ -134,7 +134,7 @@ set -g @thumbs-reverse
`default: disabled` `default: disabled`
Choose if you want to assign the same hint for the same matched strings. Choose if you want to assign the same hint for the same text spans.
For example: For example:
@ -146,7 +146,7 @@ set -g @thumbs-unique
`default: left` `default: left`
Choose where do you want to show the hint in the matched string. Options (left, right). Choose where do you want to show the hint in the text spans. Options (left, right).
For example: For example:
@ -193,7 +193,7 @@ set -g @thumbs-upcase-command 'echo -n {} | pbcopy'
`default: black` `default: black`
Sets the background color for matches Sets the background color for spans
For example: For example:
@ -205,7 +205,7 @@ set -g @thumbs-bg-color blue
`default: green` `default: green`
Sets the foreground color for matches Sets the foreground color for spans
For example: For example:
@ -314,7 +314,7 @@ This is the list of available alphabets:
## Extra features ## Extra features
- **Arrow navigation:** You can use the arrows to move around between all matched items. - **Arrow navigation:** You can use the arrows to move around between all spans.
- **Auto paste:** If your last typed hint character is uppercase, you are going to pick and paste the desired hint. - **Auto paste:** If your last typed hint character is uppercase, you are going to pick and paste the desired hint.
- **Multi selection:** If you run thumb with multi selection mode you will be able to choose multiple hints pressing the desired letter and `Space` to finalize the selection. - **Multi selection:** If you run thumb with multi selection mode you will be able to choose multiple hints pressing the desired letter and `Space` to finalize the selection.
@ -361,13 +361,13 @@ FLAGS:
-h, --help Prints help information -h, --help Prints help information
-m, --multi Enable multi-selection -m, --multi Enable multi-selection
-r, --reverse Reverse the order for assigned hints -r, --reverse Reverse the order for assigned hints
-u, --unique Don't show duplicated hints for the same match -u, --unique Don't show duplicated hints for the same span
-V, --version Prints version information -V, --version Prints version information
OPTIONS: OPTIONS:
-a, --alphabet <alphabet> Sets the alphabet [default: qwerty] -a, --alphabet <alphabet> Sets the alphabet [default: qwerty]
--bg-color <background_color> Sets the background color for matches [default: black] --bg-color <background_color> Sets the background color for spans [default: black]
--fg-color <foreground_color> Sets the foregroud color for matches [default: green] --fg-color <foreground_color> Sets the foregroud color for spans [default: green]
-f, --format <format> -f, --format <format>
Specifies the out format for the picked hint. (%U: Upcase, %H: Hint) [default: %H] Specifies the out format for the picked hint. (%U: Upcase, %H: Hint) [default: %H]

View file

@ -7,7 +7,7 @@
# #
# set -g @copyrat-keytable "foobar" # set -g @copyrat-keytable "foobar"
# set -g @copyrat-keyswitch "z" # set -g @copyrat-keyswitch "z"
# set -g @copyrat-match-bg "magenta" # set -g @copyrat-span-bg "magenta"
# #
# and bindings like # and bindings like
# #
@ -81,8 +81,10 @@ setup_pattern_binding "p" "--pattern-name path"
setup_pattern_binding "u" "--pattern-name url" setup_pattern_binding "u" "--pattern-name url"
# prefix + t + m searches for Markdown URLs [...](matched.url) # prefix + t + m searches for Markdown URLs [...](matched.url)
setup_pattern_binding "m" "--pattern-name markdown-url" setup_pattern_binding "m" "--pattern-name markdown-url"
# prefix + t + h searches for SHA1/2 (hashes) # prefix + t + h searches for SHA1/2 short or long hashes
setup_pattern_binding "h" "--pattern-name sha" setup_pattern_binding "h" "--pattern-name sha"
# prefix + t + d searches for dates or datetimes
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 + D searches for docker shas # prefix + t + D searches for docker shas
@ -93,10 +95,10 @@ setup_pattern_binding "c" "--pattern-name hexcolor"
setup_pattern_binding "U" "--pattern-name uuid" setup_pattern_binding "U" "--pattern-name uuid"
# prefix + t + v searches for version numbers # prefix + t + v searches for version numbers
setup_pattern_binding "v" "--pattern-name version" setup_pattern_binding "v" "--pattern-name version"
# prefix + t + d searches for any string of 4+ digits # prefix + t + G searches for any string of 4+ digits
setup_pattern_binding "d" "--pattern-name digits" setup_pattern_binding "G" "--pattern-name digits"
# prefix + t + m searches for hex numbers: 0xbedead # prefix + t + m searches for hex numbers: 0xbedead
setup_pattern_binding "m" "--pattern-name mem-address" setup_pattern_binding "P" "--pattern-name pointer-address"
# prefix + t + 4 searches for IPV4 # prefix + t + 4 searches for IPV4
setup_pattern_binding "4" "--pattern-name ipv4" setup_pattern_binding "4" "--pattern-name ipv4"
# prefix + t + 6 searches for IPV6 # prefix + t + 6 searches for IPV6

View file

@ -15,7 +15,7 @@ fn main() {
let lines = buffer.split('\n').collect::<Vec<_>>(); let lines = buffer.split('\n').collect::<Vec<_>>();
// Execute copyrat over the buffer (will take control over stdout). // Execute copyrat over the buffer (will take control over stdout).
// This returns the selected matche. // This returns the selected span of text.
let selection: Option<Selection> = run(&lines, &opt); let selection: Option<Selection> = run(&lines, &opt);
// Early exit, signaling no selections were found. // Early exit, signaling no selections were found.

View file

@ -40,6 +40,10 @@ fn main() -> Result<(), error::ParseError> {
output_destination, output_destination,
}) => { }) => {
if uppercased { if uppercased {
if active_pane.is_copy_mode {
// break out of copy mode
duct::cmd!("tmux", "copy-mode", "-t", active_pane.id.as_str(), "-q").run()?;
}
duct::cmd!("tmux", "send-keys", "-t", active_pane.id.as_str(), &text).run()?; duct::cmd!("tmux", "send-keys", "-t", active_pane.id.as_str(), &text).run()?;
} }

View file

@ -40,18 +40,18 @@ pub struct Config {
#[clap(short, long)] #[clap(short, long)]
pub reverse: bool, pub reverse: bool,
/// Keep the same hint for identical matches. /// Keep the same hint for identical spans.
#[clap(short, long)] #[clap(short, long)]
pub unique_hint: bool, pub unique_hint: bool,
/// Move focus back to first/last match. /// Move focus back to first/last span.
#[clap(short = 'w', long)] #[clap(short = 'w', long)]
pub focus_wrap_around: bool, pub focus_wrap_around: bool,
#[clap(flatten)] #[clap(flatten)]
pub colors: ui::colors::UiColors, pub colors: ui::colors::UiColors,
/// Align hint with its match. /// Align hint with its span.
#[clap(long, arg_enum, default_value = "leading")] #[clap(long, arg_enum, default_value = "leading")]
pub hint_alignment: ui::HintAlignment, pub hint_alignment: ui::HintAlignment,
@ -83,10 +83,12 @@ impl FromStr for HintStyleArg {
fn from_str(s: &str) -> Result<Self, error::ParseError> { fn from_str(s: &str) -> Result<Self, error::ParseError> {
match s { match s {
"leading" => Ok(HintStyleArg::Underline), "bold" => Ok(HintStyleArg::Bold),
"trailing" => Ok(HintStyleArg::Surround), "italic" => Ok(HintStyleArg::Italic),
"underline" => Ok(HintStyleArg::Underline),
"surrond" => Ok(HintStyleArg::Surround),
_ => Err(error::ParseError::ExpectedString(String::from( _ => Err(error::ParseError::ExpectedString(String::from(
"underline or surround", "bold, italic, underline or surround",
))), ))),
} }
} }

View file

@ -4,11 +4,7 @@ use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use super::basic; use super::basic;
use crate::{ use crate::{error, textbuf::alphabet, tmux, ui};
error,
textbuf::{alphabet, regexes},
tmux, ui,
};
/// Extended configuration for handling Tmux-specific configuration (options /// Extended configuration for handling Tmux-specific configuration (options
/// and outputs). This is only used by `tmux-copyrat` and parsed from command /// and outputs). This is only used by `tmux-copyrat` and parsed from command
@ -61,18 +57,12 @@ impl ConfigExt {
for (name, value) in &tmux_options { for (name, value) in &tmux_options {
match name.as_ref() { match name.as_ref() {
"@copyrat-capture" => { "@copyrat-capture-region" => {
config_ext.capture_region = CaptureRegion::from_str(&value)? config_ext.capture_region = CaptureRegion::from_str(&value)?
} }
"@copyrat-alphabet" => { "@copyrat-alphabet" => {
wrapped.alphabet = alphabet::parse_alphabet(value)?; wrapped.alphabet = alphabet::parse_alphabet(value)?;
} }
"@copyrat-pattern-name" => {
wrapped.named_patterns = vec![regexes::parse_pattern_name(value)?]
}
"@copyrat-custom-pattern" => {
wrapped.custom_patterns = vec![String::from(value)]
}
"@copyrat-reverse" => { "@copyrat-reverse" => {
wrapped.reverse = value.parse::<bool>()?; wrapped.reverse = value.parse::<bool>()?;
} }
@ -80,12 +70,8 @@ impl ConfigExt {
wrapped.unique_hint = value.parse::<bool>()?; wrapped.unique_hint = value.parse::<bool>()?;
} }
"@copyrat-match-fg" => { "@copyrat-span-fg" => wrapped.colors.span_fg = ui::colors::parse_color(value)?,
wrapped.colors.match_fg = ui::colors::parse_color(value)? "@copyrat-span-bg" => wrapped.colors.span_bg = ui::colors::parse_color(value)?,
}
"@copyrat-match-bg" => {
wrapped.colors.match_bg = ui::colors::parse_color(value)?
}
"@copyrat-focused-fg" => { "@copyrat-focused-fg" => {
wrapped.colors.focused_fg = ui::colors::parse_color(value)? wrapped.colors.focused_fg = ui::colors::parse_color(value)?
} }
@ -131,8 +117,8 @@ impl FromStr for CaptureRegion {
fn from_str(s: &str) -> Result<Self, error::ParseError> { fn from_str(s: &str) -> Result<Self, error::ParseError> {
match s { match s {
"leading" => Ok(CaptureRegion::EntireHistory), "entire-history" => Ok(CaptureRegion::EntireHistory),
"trailing" => Ok(CaptureRegion::VisibleArea), "visible-area" => Ok(CaptureRegion::VisibleArea),
_ => Err(error::ParseError::ExpectedString(String::from( _ => Err(error::ParseError::ExpectedString(String::from(
"entire-history or visible-area", "entire-history or visible-area",
))), ))),

View file

@ -20,7 +20,7 @@ pub fn run(lines: &[&str], opt: &config::basic::Config) -> Option<ui::Selection>
opt.unique_hint, opt.unique_hint,
); );
if model.matches.is_empty() { if model.spans.is_empty() {
return None; return None;
} }

View file

@ -139,35 +139,35 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn simple_matches() { fn simple_hints() {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let hints = alphabet.make_hints(3); let hints = alphabet.make_hints(3);
assert_eq!(hints, ["a", "b", "c"]); assert_eq!(hints, ["a", "b", "c"]);
} }
#[test] #[test]
fn composed_matches() { fn composed_hints() {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let hints = alphabet.make_hints(6); let hints = alphabet.make_hints(6);
assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]); assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]);
} }
#[test] #[test]
fn composed_matches_multiple() { fn composed_hints_multiple() {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let hints = alphabet.make_hints(8); let hints = alphabet.make_hints(8);
assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]); assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]);
} }
#[test] #[test]
fn composed_matches_max_2() { fn composed_hints_max_2() {
let alphabet = Alphabet("ab".to_string()); let alphabet = Alphabet("ab".to_string());
let hints = alphabet.make_hints(4); let hints = alphabet.make_hints(4);
assert_eq!(hints, ["aa", "ab", "ba", "bb"]); assert_eq!(hints, ["aa", "ab", "ba", "bb"]);
} }
#[test] #[test]
fn composed_matches_max_4() { fn composed_hints_max_4() {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let hints = alphabet.make_hints(13); let hints = alphabet.make_hints(13);
assert_eq!( assert_eq!(
@ -177,7 +177,7 @@ mod tests {
} }
#[test] #[test]
fn matches_with_longest_alphabet() { fn hints_with_longest_alphabet() {
let alphabet = Alphabet("ab".to_string()); let alphabet = Alphabet("ab".to_string());
let hints = alphabet.make_hints(2500); let hints = alphabet.make_hints(2500);
assert_eq!(hints.len(), 2500); assert_eq!(hints.len(), 2500);
@ -186,7 +186,7 @@ mod tests {
} }
#[test] #[test]
fn matches_exceed_longest_alphabet() { fn hints_exceed_longest_alphabet() {
let alphabet = Alphabet("ab".to_string()); let alphabet = Alphabet("ab".to_string());
let hints = alphabet.make_hints(10000); let hints = alphabet.make_hints(10000);
// 2500 unique hints are produced from the longest alphabet // 2500 unique hints are produced from the longest alphabet

View file

@ -1,10 +0,0 @@
/// Represents matched text, its location on screen, the pattern that created
/// it, and the associated hint.
#[derive(Debug)]
pub struct Match<'a> {
pub x: i32,
pub y: i32,
pub pattern: &'a str,
pub text: &'a str,
pub hint: String,
}

View file

@ -1,11 +1,11 @@
pub(crate) mod alphabet; pub(crate) mod alphabet;
mod matches;
mod model; mod model;
mod raw_match; mod raw_span;
pub(crate) mod regexes; pub(crate) mod regexes;
mod span;
pub use matches::Match;
pub use model::Model; pub use model::Model;
pub use span::Span;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -22,7 +22,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -31,11 +31,11 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 3); assert_eq!(spans.len(), 3);
assert_eq!(results.first().unwrap().hint, "a"); assert_eq!(spans.first().unwrap().hint, "a");
assert_eq!(results.last().unwrap().hint, "c"); assert_eq!(spans.last().unwrap().hint, "c");
} }
#[test] #[test]
@ -48,7 +48,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = true; let unique_hint = true;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -57,11 +57,11 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 3); assert_eq!(spans.len(), 3);
assert_eq!(results.first().unwrap().hint, "a"); assert_eq!(spans.first().unwrap().hint, "a");
assert_eq!(results.last().unwrap().hint, "a"); assert_eq!(spans.last().unwrap().hint, "a");
} }
#[test] #[test]
@ -74,7 +74,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -83,11 +83,11 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 1); assert_eq!(spans.len(), 1);
assert_eq!( assert_eq!(
results.get(0).unwrap().text, spans.get(0).unwrap().text,
"30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4" "30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4"
); );
} }
@ -103,7 +103,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = true; let reverse = true;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -112,12 +112,12 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 3); assert_eq!(spans.len(), 3);
assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); assert_eq!(spans.get(0).unwrap().text, "/var/log/nginx.log");
assert_eq!(results.get(1).unwrap().text, "test/log/nginx-2.log"); assert_eq!(spans.get(1).unwrap().text, "test/log/nginx-2.log");
assert_eq!(results.get(2).unwrap().text, "folder/.nginx@4df2.log"); assert_eq!(spans.get(2).unwrap().text, "folder/.nginx@4df2.log");
} }
#[test] #[test]
@ -131,7 +131,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -140,12 +140,12 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 3); assert_eq!(spans.len(), 3);
assert_eq!(results.get(0).unwrap().text, "/tmp/foo/bar_lol"); assert_eq!(spans.get(0).unwrap().text, "/tmp/foo/bar_lol");
assert_eq!(results.get(1).unwrap().text, "/var/log/boot-strap.log"); assert_eq!(spans.get(1).unwrap().text, "/var/log/boot-strap.log");
assert_eq!(results.get(2).unwrap().text, "../log/kern.log"); assert_eq!(spans.get(2).unwrap().text, "../log/kern.log");
} }
#[test] #[test]
@ -158,7 +158,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -167,10 +167,10 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 1); assert_eq!(spans.len(), 1);
assert_eq!(results.get(0).unwrap().text, "~/.gnu/.config.txt"); assert_eq!(spans.get(0).unwrap().text, "~/.gnu/.config.txt");
} }
#[test] #[test]
@ -183,7 +183,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -192,9 +192,9 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 1); assert_eq!(spans.len(), 1);
} }
#[test] #[test]
@ -207,7 +207,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -216,14 +216,14 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 4); assert_eq!(spans.len(), 4);
assert_eq!(results.get(0).unwrap().text, "fd70b5695"); assert_eq!(spans.get(0).unwrap().text, "fd70b5695");
assert_eq!(results.get(1).unwrap().text, "5246ddf"); assert_eq!(spans.get(1).unwrap().text, "5246ddf");
assert_eq!(results.get(2).unwrap().text, "f924213"); assert_eq!(spans.get(2).unwrap().text, "f924213");
assert_eq!( assert_eq!(
results.get(3).unwrap().text, spans.get(3).unwrap().text,
"973113963b491874ab2e372ee60d4b4cb75f717c" "973113963b491874ab2e372ee60d4b4cb75f717c"
); );
} }
@ -238,7 +238,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -247,15 +247,15 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 3); assert_eq!(spans.len(), 3);
assert_eq!(results.get(0).unwrap().pattern, "ipv4"); assert_eq!(spans.get(0).unwrap().pattern, "ipv4");
assert_eq!(results.get(0).unwrap().text, "127.0.0.1"); assert_eq!(spans.get(0).unwrap().text, "127.0.0.1");
assert_eq!(results.get(1).unwrap().pattern, "ipv4"); assert_eq!(spans.get(1).unwrap().pattern, "ipv4");
assert_eq!(results.get(1).unwrap().text, "255.255.10.255"); assert_eq!(spans.get(1).unwrap().text, "255.255.10.255");
assert_eq!(results.get(2).unwrap().pattern, "ipv4"); assert_eq!(spans.get(2).unwrap().pattern, "ipv4");
assert_eq!(results.get(2).unwrap().text, "127.0.0.1"); assert_eq!(spans.get(2).unwrap().text, "127.0.0.1");
} }
#[test] #[test]
@ -268,7 +268,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -277,16 +277,16 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 4); assert_eq!(spans.len(), 4);
assert_eq!(results.get(0).unwrap().text, "fe80::2:202:fe4"); assert_eq!(spans.get(0).unwrap().text, "fe80::2:202:fe4");
assert_eq!( assert_eq!(
results.get(1).unwrap().text, spans.get(1).unwrap().text,
"2001:67c:670:202:7ba8:5e41:1591:d723" "2001:67c:670:202:7ba8:5e41:1591:d723"
); );
assert_eq!(results.get(2).unwrap().text, "fe80::2:1"); assert_eq!(spans.get(2).unwrap().text, "fe80::2:1");
assert_eq!(results.get(3).unwrap().text, "fe80:22:312:fe::1%eth0"); assert_eq!(spans.get(3).unwrap().text, "fe80:22:312:fe::1%eth0");
} }
#[test] #[test]
@ -300,7 +300,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -309,13 +309,13 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 2); assert_eq!(spans.len(), 2);
assert_eq!(results.get(0).unwrap().pattern, "markdown-url"); assert_eq!(spans.get(0).unwrap().pattern, "markdown-url");
assert_eq!(results.get(0).unwrap().text, "https://github.io?foo=bar"); assert_eq!(spans.get(0).unwrap().text, "https://github.io?foo=bar");
assert_eq!(results.get(1).unwrap().pattern, "markdown-url"); assert_eq!(spans.get(1).unwrap().pattern, "markdown-url");
assert_eq!(results.get(1).unwrap().text, "http://cdn.com/img.jpg"); assert_eq!(spans.get(1).unwrap().text, "http://cdn.com/img.jpg");
} }
#[test] #[test]
@ -328,7 +328,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -337,20 +337,20 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 4); assert_eq!(spans.len(), 4);
assert_eq!( assert_eq!(
results.get(0).unwrap().text, spans.get(0).unwrap().text,
"https://www.rust-lang.org/tools" "https://www.rust-lang.org/tools"
); );
assert_eq!(results.get(0).unwrap().pattern, "url"); assert_eq!(spans.get(0).unwrap().pattern, "url");
assert_eq!(results.get(1).unwrap().text, "https://crates.io"); assert_eq!(spans.get(1).unwrap().text, "https://crates.io");
assert_eq!(results.get(1).unwrap().pattern, "url"); assert_eq!(spans.get(1).unwrap().pattern, "url");
assert_eq!(results.get(2).unwrap().text, "https://github.io?foo=bar"); assert_eq!(spans.get(2).unwrap().text, "https://github.io?foo=bar");
assert_eq!(results.get(2).unwrap().pattern, "url"); assert_eq!(spans.get(2).unwrap().pattern, "url");
assert_eq!(results.get(3).unwrap().text, "ssh://github.io"); assert_eq!(spans.get(3).unwrap().text, "ssh://github.io");
assert_eq!(results.get(3).unwrap().pattern, "url"); assert_eq!(spans.get(3).unwrap().pattern, "url");
} }
#[test] #[test]
@ -364,7 +364,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -373,23 +373,20 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 2); assert_eq!(spans.len(), 2);
assert_eq!(results.get(0).unwrap().pattern, "email"); assert_eq!(spans.get(0).unwrap().pattern, "email");
assert_eq!(spans.get(0).unwrap().text, "first.last+social@example.com");
assert_eq!(spans.get(1).unwrap().pattern, "email");
assert_eq!( assert_eq!(
results.get(0).unwrap().text, spans.get(1).unwrap().text,
"first.last+social@example.com"
);
assert_eq!(results.get(1).unwrap().pattern, "email");
assert_eq!(
results.get(1).unwrap().text,
"john@server.department.company.com" "john@server.department.company.com"
); );
} }
#[test] #[test]
fn match_addresses() { fn match_pointer_addresses() {
let buffer = "Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"; let buffer = "Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem";
let lines = buffer.split('\n').collect::<Vec<_>>(); let lines = buffer.split('\n').collect::<Vec<_>>();
let use_all_patterns = true; let use_all_patterns = true;
@ -398,7 +395,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -407,15 +404,15 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 3); assert_eq!(spans.len(), 3);
assert_eq!(results.get(0).unwrap().pattern, "mem-address"); assert_eq!(spans.get(0).unwrap().pattern, "pointer-address");
assert_eq!(results.get(0).unwrap().text, "0xfd70b5695"); assert_eq!(spans.get(0).unwrap().text, "0xfd70b5695");
assert_eq!(results.get(1).unwrap().pattern, "mem-address"); assert_eq!(spans.get(1).unwrap().pattern, "pointer-address");
assert_eq!(results.get(1).unwrap().text, "0x5246ddf"); assert_eq!(spans.get(1).unwrap().text, "0x5246ddf");
assert_eq!(results.get(2).unwrap().pattern, "mem-address"); assert_eq!(spans.get(2).unwrap().pattern, "pointer-address");
assert_eq!(results.get(2).unwrap().text, "0x973113"); assert_eq!(spans.get(2).unwrap().text, "0x973113");
} }
#[test] #[test]
@ -428,7 +425,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -437,13 +434,13 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 4); assert_eq!(spans.len(), 4);
assert_eq!(results.get(0).unwrap().text, "#fd7b56"); assert_eq!(spans.get(0).unwrap().text, "#fd7b56");
assert_eq!(results.get(1).unwrap().text, "#FF00FF"); assert_eq!(spans.get(1).unwrap().text, "#FF00FF");
assert_eq!(results.get(2).unwrap().text, "#00fF05"); assert_eq!(spans.get(2).unwrap().text, "#00fF05");
assert_eq!(results.get(3).unwrap().text, "#abcd00"); assert_eq!(spans.get(3).unwrap().text, "#abcd00");
} }
#[test] #[test]
@ -456,7 +453,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -465,11 +462,11 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 1); assert_eq!(spans.len(), 1);
assert_eq!( assert_eq!(
results.get(0).unwrap().text, spans.get(0).unwrap().text,
"QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ" "QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ"
); );
} }
@ -484,7 +481,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -493,9 +490,9 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 8); assert_eq!(spans.len(), 8);
} }
#[test] #[test]
@ -508,7 +505,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -517,11 +514,11 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 1); assert_eq!(spans.len(), 1);
assert_eq!(results.get(0).unwrap().pattern, "diff-a"); assert_eq!(spans.get(0).unwrap().pattern, "diff-a");
assert_eq!(results.get(0).unwrap().text, "src/main.rs"); assert_eq!(spans.get(0).unwrap().text, "src/main.rs");
} }
#[test] #[test]
@ -534,7 +531,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -543,11 +540,37 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 1); assert_eq!(spans.len(), 1);
assert_eq!(results.get(0).unwrap().pattern, "diff-b"); assert_eq!(spans.get(0).unwrap().pattern, "diff-b");
assert_eq!(results.get(0).unwrap().text, "src/main.rs"); assert_eq!(spans.get(0).unwrap().text, "src/main.rs");
}
#[test]
fn match_datetime() {
let buffer = "12 days ago = 2021-03-04T12:23:34 text";
let lines = buffer.split('\n').collect::<Vec<_>>();
let use_all_patterns = true;
let named_pat = vec![];
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(), 1);
assert_eq!(spans.get(0).unwrap().pattern, "datetime");
assert_eq!(spans.get(0).unwrap().text, "2021-03-04T12:23:34");
} }
#[test] #[test]
@ -563,7 +586,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -572,22 +595,22 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 9); assert_eq!(spans.len(), 9);
assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); assert_eq!(spans.get(0).unwrap().text, "http://foo.bar");
assert_eq!(results.get(1).unwrap().text, "CUSTOM-52463"); assert_eq!(spans.get(1).unwrap().text, "CUSTOM-52463");
assert_eq!(results.get(2).unwrap().text, "ISSUE-123"); assert_eq!(spans.get(2).unwrap().text, "ISSUE-123");
assert_eq!(results.get(3).unwrap().text, "/var/fd70b569/9999.log"); assert_eq!(spans.get(3).unwrap().text, "/var/fd70b569/9999.log");
assert_eq!(results.get(4).unwrap().text, "52463"); assert_eq!(spans.get(4).unwrap().text, "52463");
assert_eq!(results.get(5).unwrap().text, "973113"); assert_eq!(spans.get(5).unwrap().text, "973113");
assert_eq!( assert_eq!(
results.get(6).unwrap().text, spans.get(6).unwrap().text,
"123e4567-e89b-12d3-a456-426655440000" "123e4567-e89b-12d3-a456-426655440000"
); );
assert_eq!(results.get(7).unwrap().text, "8888"); assert_eq!(spans.get(7).unwrap().text, "8888");
assert_eq!( assert_eq!(
results.get(8).unwrap().text, spans.get(8).unwrap().text,
"https://crates.io/23456/fd70b569" "https://crates.io/23456/fd70b569"
); );
} }
@ -605,7 +628,7 @@ mod tests {
let alphabet = Alphabet("abcd".to_string()); let alphabet = Alphabet("abcd".to_string());
let reverse = false; let reverse = false;
let unique_hint = false; let unique_hint = false;
let results = Model::new( let spans = Model::new(
&lines, &lines,
&alphabet, &alphabet,
use_all_patterns, use_all_patterns,
@ -614,12 +637,12 @@ mod tests {
reverse, reverse,
unique_hint, unique_hint,
) )
.matches; .spans;
assert_eq!(results.len(), 2); assert_eq!(spans.len(), 2);
assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); assert_eq!(spans.get(0).unwrap().text, "http://foo.bar");
assert_eq!( assert_eq!(
results.get(1).unwrap().text, spans.get(1).unwrap().text,
"https://crates.io/23456/fd70b569" "https://crates.io/23456/fd70b569"
); );
} }

View file

@ -4,22 +4,20 @@ use regex::Regex;
use sequence_trie::SequenceTrie; use sequence_trie::SequenceTrie;
use super::alphabet::Alphabet; use super::alphabet::Alphabet;
use super::matches::Match; use super::raw_span::RawSpan;
use super::raw_match::RawMatch;
use super::regexes::{NamedPattern, EXCLUDE_PATTERNS, PATTERNS}; use super::regexes::{NamedPattern, EXCLUDE_PATTERNS, PATTERNS};
use super::span::Span;
/// Holds data for the `Ui`. /// Holds data for the `Ui`.
pub struct Model<'a> { pub struct Model<'a> {
// buffer: &'a str,
pub lines: &'a [&'a str], pub lines: &'a [&'a str],
pub reverse: bool, pub reverse: bool,
pub matches: Vec<Match<'a>>, pub spans: Vec<Span<'a>>,
pub lookup_trie: SequenceTrie<char, usize>, pub lookup_trie: SequenceTrie<char, usize>,
} }
impl<'a> Model<'a> { impl<'a> Model<'a> {
pub fn new( pub fn new(
// buffer: &'a str,
lines: &'a [&'a str], lines: &'a [&'a str],
alphabet: &'a Alphabet, alphabet: &'a Alphabet,
use_all_patterns: bool, use_all_patterns: bool,
@ -28,36 +26,34 @@ impl<'a> Model<'a> {
reverse: bool, reverse: bool,
unique_hint: bool, unique_hint: bool,
) -> Model<'a> { ) -> Model<'a> {
// let lines = buffer.split('\n').collect::<Vec<_>>(); let mut raw_spans =
find_raw_spans(&lines, named_patterns, custom_patterns, use_all_patterns);
let mut raw_matches =
raw_matches(&lines, named_patterns, custom_patterns, use_all_patterns);
if reverse { if reverse {
raw_matches.reverse(); raw_spans.reverse();
} }
let mut matches = associate_hints(&raw_matches, alphabet, unique_hint); let mut spans = associate_hints(&raw_spans, alphabet, unique_hint);
if reverse { if reverse {
matches.reverse(); spans.reverse();
} }
let lookup_trie = build_lookup_trie(&matches); let lookup_trie = build_lookup_trie(&spans);
Model { Model {
// buffer, // buffer,
lines, lines,
reverse, reverse,
matches, spans,
lookup_trie, lookup_trie,
} }
} }
} }
/// Internal function that searches the model's lines for pattern matches. /// Internal function that searches the model's lines for pattern matches.
/// Returns a vector of `RawMatch`es (text, location, pattern id) without /// Returns a vector of `RawSpan` (text, location, pattern id) without
/// an associated hint. The hint is attached to `Match`, not to `RawMatch`. /// an associated hint. The hint is attached to `Span`, not to `RawSpan`.
/// ///
/// # Notes /// # Notes
/// ///
@ -65,12 +61,12 @@ impl<'a> Model<'a> {
/// ///
/// If no named patterns were specified, it will search for all available /// If no named patterns were specified, it will search for all available
/// patterns from the `PATTERNS` catalog. /// patterns from the `PATTERNS` catalog.
fn raw_matches<'a>( fn find_raw_spans<'a>(
lines: &'a [&'a str], lines: &'a [&'a str],
named_patterns: &'a [NamedPattern], named_patterns: &'a [NamedPattern],
custom_patterns: &'a [String], custom_patterns: &'a [String],
use_all_patterns: bool, use_all_patterns: bool,
) -> Vec<RawMatch<'a>> { ) -> Vec<RawSpan<'a>> {
let exclude_regexes = EXCLUDE_PATTERNS let exclude_regexes = EXCLUDE_PATTERNS
.iter() .iter()
.map(|&(name, pattern)| (name, Regex::new(pattern).unwrap())) .map(|&(name, pattern)| (name, Regex::new(pattern).unwrap()))
@ -100,7 +96,7 @@ fn raw_matches<'a>(
let all_regexes = [exclude_regexes, custom_regexes, regexes].concat(); let all_regexes = [exclude_regexes, custom_regexes, regexes].concat();
let mut raw_matches = Vec::new(); let mut raw_spans = Vec::new();
for (index, line) in lines.iter().enumerate() { for (index, line) in lines.iter().enumerate() {
// Chunk is the remainder of the line to be searched for matches. // Chunk is the remainder of the line to be searched for matches.
@ -110,7 +106,7 @@ fn raw_matches<'a>(
// Use all avail regexes to match the chunk and select the match // Use all avail regexes to match the chunk and select the match
// occuring the earliest on the chunk. Save its matched text and // occuring the earliest on the chunk. Save its matched text and
// position in a `RawMatch` struct. // position in a `RawSpan` struct.
loop { loop {
// For each avalable regex, use the `find_iter` iterator to // For each avalable regex, use the `find_iter` iterator to
// get the first non-overlapping match in the chunk, returning // get the first non-overlapping match in the chunk, returning
@ -149,7 +145,7 @@ fn raw_matches<'a>(
None => (text, 0), None => (text, 0),
}; };
raw_matches.push(RawMatch { raw_spans.push(RawSpan {
x: offset + reg_match.start() as i32 + substart as i32, x: offset + reg_match.start() as i32 + substart as i32,
y: index as i32, y: index as i32,
pattern: pat_name, pattern: pat_name,
@ -164,54 +160,54 @@ fn raw_matches<'a>(
} }
} }
raw_matches raw_spans
} }
/// Associate a hint to each `RawMatch`, returning a vector of `Match`es. /// Associate a hint to each `RawSpan`, returning a vector of `Span`.
/// ///
/// If `unique` is `true`, all duplicate matches will have the same hint. /// If `unique` is `true`, all duplicate spans will have the same hint.
/// For copying matched text, this seems easier and more natural. /// For copying text spans, this seems easier and more natural.
/// If `unique` is `false`, duplicate matches will have their own hint. /// If `unique` is `false`, duplicate spans will have their own hint.
fn associate_hints<'a>( fn associate_hints<'a>(
raw_matches: &[RawMatch<'a>], raw_spans: &[RawSpan<'a>],
alphabet: &'a Alphabet, alphabet: &'a Alphabet,
unique: bool, unique: bool,
) -> Vec<Match<'a>> { ) -> Vec<Span<'a>> {
let hints = alphabet.make_hints(raw_matches.len()); let hints = alphabet.make_hints(raw_spans.len());
let mut hints_iter = hints.iter(); let mut hints_iter = hints.iter();
let mut result: Vec<Match<'a>> = vec![]; let mut result: Vec<Span<'a>> = vec![];
if unique { if unique {
// Map (text, hint) // Map (text, hint)
let mut known: collections::HashMap<&str, &str> = collections::HashMap::new(); let mut known: collections::HashMap<&str, &str> = collections::HashMap::new();
for raw_mat in raw_matches { for raw_span in raw_spans {
let hint: &str = known.entry(raw_mat.text).or_insert_with(|| { let hint: &str = known.entry(raw_span.text).or_insert_with(|| {
hints_iter hints_iter
.next() .next()
.expect("We should have as many hints as necessary, even invisible ones.") .expect("We should have as many hints as necessary, even invisible ones.")
}); });
result.push(Match { result.push(Span {
x: raw_mat.x, x: raw_span.x,
y: raw_mat.y, y: raw_span.y,
pattern: raw_mat.pattern, pattern: raw_span.pattern,
text: raw_mat.text, text: raw_span.text,
hint: hint.to_string(), hint: hint.to_string(),
}); });
} }
} else { } else {
for raw_mat in raw_matches { for raw_span in raw_spans {
let hint = hints_iter let hint = hints_iter
.next() .next()
.expect("We should have as many hints as necessary, even invisible ones."); .expect("We should have as many hints as necessary, even invisible ones.");
result.push(Match { result.push(Span {
x: raw_mat.x, x: raw_span.x,
y: raw_mat.y, y: raw_span.y,
pattern: raw_mat.pattern, pattern: raw_span.pattern,
text: raw_mat.text, text: raw_span.text,
hint: hint.to_string(), hint: hint.to_string(),
}); });
} }
@ -222,12 +218,12 @@ fn associate_hints<'a>(
/// Builds a `SequenceTrie` that helps determine if a sequence of keys /// Builds a `SequenceTrie` that helps determine if a sequence of keys
/// entered by the user corresponds to a match. This kind of lookup /// entered by the user corresponds to a match. This kind of lookup
/// directly returns a reference to the corresponding `Match` if any. /// directly returns a reference to the corresponding `Span` if any.
fn build_lookup_trie<'a>(matches: &'a [Match<'a>]) -> SequenceTrie<char, usize> { fn build_lookup_trie<'a>(spans: &'a [Span<'a>]) -> SequenceTrie<char, usize> {
let mut trie = SequenceTrie::new(); let mut trie = SequenceTrie::new();
for (index, mat) in matches.iter().enumerate() { for (index, span) in spans.iter().enumerate() {
let hint_chars = mat.hint.chars().collect::<Vec<char>>(); let hint_chars = span.hint.chars().collect::<Vec<char>>();
// no need to insert twice the same hint // no need to insert twice the same hint
if trie.get(&hint_chars).is_none() { if trie.get(&hint_chars).is_none() {

View file

@ -1,8 +0,0 @@
/// Internal surrogate for `Match`, before a Hint has been associated.
#[derive(Debug)]
pub(super) struct RawMatch<'a> {
pub x: i32,
pub y: i32,
pub pattern: &'a str,
pub text: &'a str,
}

8
src/textbuf/raw_span.rs Normal file
View file

@ -0,0 +1,8 @@
/// Internal surrogate for `Span`, before a Hint has been associated.
#[derive(Debug)]
pub(super) struct RawSpan<'a> {
pub x: i32,
pub y: i32,
pub pattern: &'a str,
pub text: &'a str,
}

View file

@ -7,7 +7,7 @@ pub(super) const EXCLUDE_PATTERNS: [(&str, &str); 1] =
/// ///
/// 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. /// Others were obtained from Ferran Basora.
pub(super) const PATTERNS: [(&str, &str); 16] = [ pub(super) const PATTERNS: [(&str, &str); 17] = [
("markdown-url", r"\[[^]]*\]\(([^)]+)\)"), ("markdown-url", r"\[[^]]*\]\(([^)]+)\)"),
( (
"url", "url",
@ -31,7 +31,11 @@ pub(super) const PATTERNS: [(&str, &str); 16] = [
("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]+"),
("mem-address", r"0x[0-9a-fA-F]+"), ("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,}"), ("digits", r"[0-9]{4,}"),
]; ];

10
src/textbuf/span.rs Normal file
View file

@ -0,0 +1,10 @@
/// Represents some span of text, its location on screen, the pattern that
/// created it, and the associated hint.
#[derive(Debug)]
pub struct Span<'a> {
pub x: i32,
pub y: i32,
pub pattern: &'a str,
pub text: &'a str,
pub hint: String,
}

View file

@ -15,16 +15,16 @@ use crate::error::ParseError;
pub struct Pane { pub struct Pane {
/// Pane identifier, e.g. `%37`. /// Pane identifier, e.g. `%37`.
pub id: PaneId, pub id: PaneId,
/// Describes if the pane is in some mode. /// Describes if the pane is in copy mode.
pub in_mode: bool, pub is_copy_mode: bool,
/// Number of lines in the pane. /// Number of lines in the pane.
pub height: u32, pub height: i32,
/// Optional offset from the bottom if the pane is in some mode. /// Optional offset from the bottom if the pane is in some mode.
/// ///
/// When a pane is in copy mode, scrolling up changes the /// When a pane is in copy mode, scrolling up changes the
/// `scroll_position`. If the pane is in normal mode, or unscrolled, /// `scroll_position`. If the pane is in normal mode, or unscrolled,
/// then `0` is returned. /// then `0` is returned.
pub scroll_position: u32, pub scroll_position: i32,
/// Describes if the pane is currently active (focused). /// Describes if the pane is currently active (focused).
pub is_active: bool, pub is_active: bool,
} }
@ -59,9 +59,9 @@ impl FromStr for Pane {
// let id = id_str[1..].parse::<u32>()?; // let id = id_str[1..].parse::<u32>()?;
// let id = format!("%{}", id); // let id = format!("%{}", id);
let in_mode = iter.next().unwrap().parse::<bool>()?; let is_copy_mode = iter.next().unwrap().parse::<bool>()?;
let height = iter.next().unwrap().parse::<u32>()?; let height = iter.next().unwrap().parse::<i32>()?;
let scroll_position = iter.next().unwrap(); let scroll_position = iter.next().unwrap();
let scroll_position = if scroll_position.is_empty() { let scroll_position = if scroll_position.is_empty() {
@ -69,13 +69,13 @@ impl FromStr for Pane {
} else { } else {
scroll_position scroll_position
}; };
let scroll_position = scroll_position.parse::<u32>()?; let scroll_position = scroll_position.parse::<i32>()?;
let is_active = iter.next().unwrap().parse::<bool>()?; let is_active = iter.next().unwrap().parse::<bool>()?;
Ok(Pane { Ok(Pane {
id, id,
in_mode, is_copy_mode,
height, height,
scroll_position, scroll_position,
is_active, is_active,
@ -141,10 +141,10 @@ pub fn list_panes() -> Result<Vec<Pane>, ParseError> {
/// # Example /// # Example
/// ```get_options("@copyrat-")``` /// ```get_options("@copyrat-")```
pub fn get_options(prefix: &str) -> Result<HashMap<String, String>, ParseError> { pub fn get_options(prefix: &str) -> Result<HashMap<String, String>, ParseError> {
let output = duct::cmd!("tmux", "show", "-g").read()?; let output = duct::cmd!("tmux", "show-options", "-g").read()?;
let lines: Vec<&str> = output.split('\n').collect(); let lines: Vec<&str> = output.split('\n').collect();
let pattern = format!(r#"{prefix}([\w\-0-9]+) "?(\w+)"?"#, prefix = prefix); let pattern = format!(r#"({prefix}[\w\-0-9]+) "?(\w+)"?"#, prefix = prefix);
let re = Regex::new(&pattern).unwrap(); let re = Regex::new(&pattern).unwrap();
let args: HashMap<String, String> = lines let args: HashMap<String, String> = lines
@ -167,35 +167,41 @@ pub fn get_options(prefix: &str) -> Result<HashMap<String, String>, ParseError>
/// The provided `region` specifies if the visible area is captured, or the /// The provided `region` specifies if the visible area is captured, or the
/// entire history. /// entire history.
/// ///
/// # TODO
///
/// Capture with `capture-pane -J` joins wrapped lines.
///
/// # Note /// # Note
/// ///
/// If the pane is in normal mode, capturing the visible area can be done /// In Tmux, the start line is the line at the top of the pane. The end line
/// without extra arguments (default behavior of `capture-pane`), but if the /// is the last line at the bottom of the pane.
/// pane is in copy mode, we need to take into account the current scroll ///
/// position. To support both cases, the implementation always provides those /// - In normal mode, the index of the start line is always 0. The index of
/// parameters to tmux. /// 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> { pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result<String, ParseError> {
let mut args = format!("capture-pane -t {pane_id} -J -p", pane_id = pane.id); let mut args_str = format!("capture-pane -t {pane_id} -J -p", pane_id = pane.id);
let region_str = match region { let region_str = match region {
CaptureRegion::VisibleArea => { CaptureRegion::VisibleArea => {
// Providing start/end helps support both copy and normal modes. if pane.is_copy_mode && pane.scroll_position > 0 {
format!( format!(
" -S {start} -E {end}", " -S {start} -E {end}",
start = pane.scroll_position, start = -pane.scroll_position,
end = pane.height - pane.scroll_position - 1 end = pane.height - pane.scroll_position - 1
) )
} else {
String::new()
}
} }
CaptureRegion::EntireHistory => String::from(" -S - -E -"), CaptureRegion::EntireHistory => String::from(" -S - -E -"),
}; };
args.push_str(&region_str); args_str.push_str(&region_str);
let args: Vec<&str> = args.split(' ').collect(); let args: Vec<&str> = args_str.split(' ').collect();
let output = duct::cmd("tmux", &args).read()?; let output = duct::cmd("tmux", &args).read()?;
Ok(output) Ok(output)
@ -226,7 +232,7 @@ mod tests {
let expected = vec![ let expected = vec![
Pane { Pane {
id: PaneId::from_str("%52").unwrap(), id: PaneId::from_str("%52").unwrap(),
in_mode: false, is_copy_mode: false,
height: 62, height: 62,
scroll_position: 3, scroll_position: 3,
is_active: false, is_active: false,
@ -234,7 +240,7 @@ mod tests {
Pane { Pane {
// id: PaneId::from_str("%53").unwrap(), // id: PaneId::from_str("%53").unwrap(),
id: PaneId(String::from("%53")), id: PaneId(String::from("%53")),
in_mode: false, is_copy_mode: false,
height: 23, height: 23,
scroll_position: 0, scroll_position: 0,
is_active: true, is_active: true,

View file

@ -12,15 +12,15 @@ pub fn parse_color(src: &str) -> Result<Box<dyn color::Color>, error::ParseError
"magenta" => Ok(Box::new(color::Magenta)), "magenta" => Ok(Box::new(color::Magenta)),
"cyan" => Ok(Box::new(color::Cyan)), "cyan" => Ok(Box::new(color::Cyan)),
"white" => Ok(Box::new(color::White)), "white" => Ok(Box::new(color::White)),
"bright-black" => Ok(Box::new(color::LightBlack)), "bright-black" | "brightblack" => Ok(Box::new(color::LightBlack)),
"bright-red" => Ok(Box::new(color::LightRed)), "bright-red" | "brightred" => Ok(Box::new(color::LightRed)),
"bright-green" => Ok(Box::new(color::LightGreen)), "bright-green" | "brightgreen" => Ok(Box::new(color::LightGreen)),
"bright-yellow" => Ok(Box::new(color::LightYellow)), "bright-yellow" | "brightyellow" => Ok(Box::new(color::LightYellow)),
"bright-blue" => Ok(Box::new(color::LightBlue)), "bright-blue" | "brightblue" => Ok(Box::new(color::LightBlue)),
"bright-magenta" => Ok(Box::new(color::LightMagenta)), "bright-magenta" | "brightmagenta" => Ok(Box::new(color::LightMagenta)),
"bright-cyan" => Ok(Box::new(color::LightCyan)), "bright-cyan" | "brightcyan" => Ok(Box::new(color::LightCyan)),
"bright-white" => Ok(Box::new(color::LightWhite)), "bright-white" | "brightwhite" => Ok(Box::new(color::LightWhite)),
// "default" => Ok(Box::new(color::Reset)), "none" => Ok(Box::new(color::Reset)),
_ => Err(error::ParseError::UnknownColor), _ => Err(error::ParseError::UnknownColor),
} }
} }
@ -30,7 +30,7 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn match_color() { fn span_color() {
let text1 = format!( let text1 = format!(
"{}{}", "{}{}",
color::Fg(parse_color("green").unwrap().as_ref()), color::Fg(parse_color("green").unwrap().as_ref()),
@ -42,15 +42,15 @@ mod tests {
} }
#[test] #[test]
fn no_match_color() { fn no_span_color() {
assert!(parse_color("wat").is_err(), "this color should not exist"); assert!(parse_color("wat").is_err(), "this color should not exist");
} }
} }
/// Holds color-related data. /// Holds color-related data.
/// ///
/// - `focus_*` colors are used to render the currently focused matched text. /// - `focus_*` colors are used to render the currently focused text span.
/// - `normal_*` colors are used to render other matched text. /// - `normal_*` colors are used to render other text spans.
/// - `hint_*` colors are used to render the hints. /// - `hint_*` colors are used to render the hints.
#[derive(Clap, Debug)] #[derive(Clap, Debug)]
#[clap(about)] // Needed to avoid this doc comment to be used as overall `about`. #[clap(about)] // Needed to avoid this doc comment to be used as overall `about`.
@ -60,36 +60,36 @@ pub struct UiColors {
pub text_fg: Box<dyn color::Color>, pub text_fg: Box<dyn color::Color>,
/// Background color for base text. /// Background color for base text.
#[clap(long, default_value = "bright-white", parse(try_from_str = parse_color))] #[clap(long, default_value = "none", parse(try_from_str = parse_color))]
pub text_bg: Box<dyn color::Color>, pub text_bg: Box<dyn color::Color>,
/// Foreground color for matches. /// Foreground color for spans.
#[clap(long, default_value = "yellow", #[clap(long, default_value = "blue",
parse(try_from_str = parse_color))] parse(try_from_str = parse_color))]
pub match_fg: Box<dyn color::Color>, pub span_fg: Box<dyn color::Color>,
/// Background color for matches. /// Background color for spans.
#[clap(long, default_value = "bright-white", #[clap(long, default_value = "none",
parse(try_from_str = parse_color))] parse(try_from_str = parse_color))]
pub match_bg: Box<dyn color::Color>, pub span_bg: Box<dyn color::Color>,
/// Foreground color for the focused match. /// Foreground color for the focused span.
#[clap(long, default_value = "magenta", #[clap(long, default_value = "magenta",
parse(try_from_str = parse_color))] parse(try_from_str = parse_color))]
pub focused_fg: Box<dyn color::Color>, pub focused_fg: Box<dyn color::Color>,
/// Background color for the focused match. /// Background color for the focused span.
#[clap(long, default_value = "bright-white", #[clap(long, default_value = "none",
parse(try_from_str = parse_color))] parse(try_from_str = parse_color))]
pub focused_bg: Box<dyn color::Color>, pub focused_bg: Box<dyn color::Color>,
/// Foreground color for hints. /// Foreground color for hints.
#[clap(long, default_value = "white", #[clap(long, default_value = "yellow",
parse(try_from_str = parse_color))] parse(try_from_str = parse_color))]
pub hint_fg: Box<dyn color::Color>, pub hint_fg: Box<dyn color::Color>,
/// Background color for hints. /// Background color for hints.
#[clap(long, default_value = "magenta", #[clap(long, default_value = "none",
parse(try_from_str = parse_color))] parse(try_from_str = parse_color))]
pub hint_bg: Box<dyn color::Color>, pub hint_bg: Box<dyn color::Color>,
} }

View file

@ -3,7 +3,7 @@
//! //!
//! In particular, the `Ui` struct //! In particular, the `Ui` struct
//! //!
//! - renders text, matched text and hints from the structured buffer content //! - renders text, spans and hints from the structured buffer content
//! to the screen, //! to the screen,
//! - listens for keypress events, //! - listens for keypress events,
//! - and returns the user selection in the form of a `Selection` struct. //! - and returns the user selection in the form of a `Selection` struct.
@ -12,8 +12,8 @@
//! //!
//! - navigate the buffer (in case it is larger than the number of lines in //! - navigate the buffer (in case it is larger than the number of lines in
//! the terminal) //! the terminal)
//! - move the focus from one match to another //! - move the focus from one span to another
//! - select one of the matches //! - select one of the available spans
//! - toggle the output destination (tmux buffer or clipboard) //! - toggle the output destination (tmux buffer or clipboard)
//! //!

View file

@ -9,10 +9,34 @@ use super::Selection;
use super::{HintAlignment, HintStyle}; use super::{HintAlignment, HintStyle};
use crate::{config::extended::OutputDestination, textbuf}; use crate::{config::extended::OutputDestination, textbuf};
/// Describes where a line from the buffer is displayed on the screen and how
/// much vertical lines it takes.
///
/// The `pos_y` field is the actual vertical position due to wrapped lines
/// before this line. The `size` field is the number of screen lines occupied
/// by this line.
///
/// For example, given a buffer in which
///
/// - the first line is smaller than the screen width,
/// - the second line is slightly larger,
/// - and the third line is smaller than the screen width,
///
/// The corresponding `WrappedLine`s are
///
/// - the first `WrappedLine` has `pos_y: 0` and `size: 1`
/// - the second `WrappedLine` has `pos_y: 1` and `size: 2` (larger than screen
/// width)
/// - the third `WrappedLine` has `pos_y: 3` and `size: 1`
///
struct WrappedLine {
pos_y: usize,
}
pub struct ViewController<'a> { pub struct ViewController<'a> {
model: &'a textbuf::Model<'a>, model: &'a textbuf::Model<'a>,
term_width: u16, term_width: u16,
line_offsets: Vec<usize>, wrapped_lines: Vec<WrappedLine>,
focus_index: usize, focus_index: usize,
focus_wrap_around: bool, focus_wrap_around: bool,
default_output_destination: OutputDestination, default_output_destination: OutputDestination,
@ -33,18 +57,18 @@ impl<'a> ViewController<'a> {
hint_style: Option<HintStyle>, hint_style: Option<HintStyle>,
) -> ViewController<'a> { ) -> ViewController<'a> {
let focus_index = if model.reverse { let focus_index = if model.reverse {
model.matches.len() - 1 model.spans.len() - 1
} else { } else {
0 0
}; };
let (term_width, _) = termion::terminal_size().unwrap_or((80u16, 30u16)); // .expect("Cannot read the terminal size."); let (term_width, _) = termion::terminal_size().unwrap_or((80u16, 30u16)); // .expect("Cannot read the terminal size.");
let line_offsets = get_line_offsets(&model.lines, term_width); let wrapped_lines = compute_wrapped_lines(&model.lines, term_width);
ViewController { ViewController {
model, model,
term_width, term_width,
line_offsets, wrapped_lines,
focus_index, focus_index,
focus_wrap_around, focus_wrap_around,
default_output_destination, default_output_destination,
@ -57,53 +81,56 @@ impl<'a> ViewController<'a> {
// }}} // }}}
// Coordinates {{{1 // Coordinates {{{1
/// Convert the `Match` text into the coordinates of the wrapped lines. /// Returns the adjusted position of a given `Span` within the buffer
/// line.
/// ///
/// Compute the new x offset of the text as the remainder of the line width /// This adjustment is necessary if multibyte characters occur before the
/// (e.g. the match could start at offset 120 in a 80-width terminal, the new /// span (in the "prefix"). If this is the case then their compouding
/// offset being 40). /// takes less space on screen when printed: for instance ´ + e = é.
/// Consequently the span position has to be adjusted to the left.
/// ///
/// Compute the new y offset of the text as the initial y offset plus any /// This computation must happen before mapping the span position to the
/// additional offset due to previous split lines. This is obtained thanks to /// wrapped screen space.
/// the `offset_per_line` member. fn adjusted_span_position(&self, span: &textbuf::Span<'a>) -> (usize, usize) {
fn map_coords_to_wrapped_space(&self, offset_x: usize, offset_y: usize) -> (usize, usize) { let pos_x = {
let line_width = self.term_width as usize; let line = &self.model.lines[span.y as usize];
let prefix = &line[0..span.x as usize];
let adjust = prefix.len() - prefix.chars().count();
(span.x as usize) - adjust
};
let pos_y = span.y as usize;
let new_offset_x = offset_x % line_width; (pos_x, pos_y)
let new_offset_y =
self.line_offsets.get(offset_y as usize).unwrap() + offset_x / line_width;
(new_offset_x, new_offset_y)
} }
/// Returns screen offset of a given `Match`. /// Convert the `Span` text into the coordinates of the wrapped lines.
/// ///
/// If multibyte characters occur before the hint (in the "prefix"), then /// Compute the new x position of the text as the remainder of the line width
/// their compouding takes less space on screen when printed: for /// (e.g. the Span could start at position 120 in a 80-width terminal, the new
/// instance ´ + e = é. Consequently the hint offset has to be adjusted /// position being 40).
/// to the left. ///
fn match_offsets(&self, mat: &textbuf::Match<'a>) -> (usize, usize) { /// Compute the new y position of the text as the initial y position plus any
let offset_x = { /// additional offset due to previous split lines. This is obtained thanks to
let line = &self.model.lines[mat.y as usize]; /// the `wrapped_lines` field.
let prefix = &line[0..mat.x as usize]; fn map_coords_to_wrapped_space(&self, pos_x: usize, pos_y: usize) -> (usize, usize) {
let adjust = prefix.len() - prefix.chars().count(); let line_width = self.term_width as usize;
(mat.x as usize) - (adjust)
};
let offset_y = mat.y as usize;
(offset_x, offset_y) let new_pos_x = pos_x % line_width;
let new_pos_y = self.wrapped_lines[pos_y as usize].pos_y + pos_x / line_width;
(new_pos_x, new_pos_y)
} }
// }}} // }}}
// Focus management {{{1 // Focus management {{{1
/// Move focus onto the previous hint, returning both the index of the /// Move focus onto the previous hint, returning both the index of the
/// previously focused match, and the index of the newly focused one. /// previously focused Span, and the index of the newly focused one.
fn prev_focus_index(&mut self) -> (usize, usize) { fn prev_focus_index(&mut self) -> (usize, usize) {
let old_index = self.focus_index; let old_index = self.focus_index;
if self.focus_wrap_around { if self.focus_wrap_around {
if self.focus_index == 0 { if self.focus_index == 0 {
self.focus_index = self.model.matches.len() - 1; self.focus_index = self.model.spans.len() - 1;
} else { } else {
self.focus_index -= 1; self.focus_index -= 1;
} }
@ -115,16 +142,16 @@ impl<'a> ViewController<'a> {
} }
/// Move focus onto the next hint, returning both the index of the /// Move focus onto the next hint, returning both the index of the
/// previously focused match, and the index of the newly focused one. /// previously focused Span, and the index of the newly focused one.
fn next_focus_index(&mut self) -> (usize, usize) { fn next_focus_index(&mut self) -> (usize, usize) {
let old_index = self.focus_index; let old_index = self.focus_index;
if self.focus_wrap_around { if self.focus_wrap_around {
if self.focus_index == self.model.matches.len() - 1 { if self.focus_index == self.model.spans.len() - 1 {
self.focus_index = 0; self.focus_index = 0;
} else { } else {
self.focus_index += 1; self.focus_index += 1;
} }
} else if self.focus_index < self.model.matches.len() - 1 { } else if self.focus_index < self.model.spans.len() - 1 {
self.focus_index += 1; self.focus_index += 1;
} }
let new_index = self.focus_index; let new_index = self.focus_index;
@ -136,7 +163,7 @@ impl<'a> ViewController<'a> {
/// Render entire model lines on provided writer. /// Render entire model lines on provided writer.
/// ///
/// This renders the basic content on which matches and hints can be rendered. /// This renders the basic content on which spans and hints can be rendered.
/// ///
/// # Notes /// # Notes
/// - All trailing whitespaces are trimmed, empty lines are skipped. /// - All trailing whitespaces are trimmed, empty lines are skipped.
@ -144,7 +171,7 @@ impl<'a> ViewController<'a> {
fn render_base_text( fn render_base_text(
stdout: &mut dyn io::Write, stdout: &mut dyn io::Write,
lines: &[&str], lines: &[&str],
line_offsets: &[usize], wrapped_lines: &[WrappedLine],
colors: &UiColors, colors: &UiColors,
) { ) {
write!( write!(
@ -159,13 +186,12 @@ impl<'a> ViewController<'a> {
let trimmed_line = line.trim_end(); let trimmed_line = line.trim_end();
if !trimmed_line.is_empty() { if !trimmed_line.is_empty() {
let offset_y: usize = let pos_y: usize = wrapped_lines[line_index].pos_y;
*(line_offsets.get(line_index)).expect("Cannot get offset_per_line.");
write!( write!(
stdout, stdout,
"{goto}{text}", "{goto}{text}",
goto = cursor::Goto(1, offset_y as u16 + 1), goto = cursor::Goto(1, pos_y as u16 + 1),
text = &trimmed_line, text = &trimmed_line,
) )
.unwrap(); .unwrap();
@ -181,32 +207,32 @@ impl<'a> ViewController<'a> {
.unwrap(); .unwrap();
} }
/// Render the Match's `text` field on provided writer using the `match_*g` color. /// Render the Span's `text` field on provided writer using the `span_*g` color.
/// ///
/// If a Mach is "focused", it is then rendered with the `focused_*g` colors. /// If a Mach is "focused", it is then rendered with the `focused_*g` colors.
/// ///
/// # Note /// # Note
/// ///
/// This writes directly on the writer, avoiding extra allocation. /// This writes directly on the writer, avoiding extra allocation.
fn render_matched_text( fn render_span_text(
stdout: &mut dyn io::Write, stdout: &mut dyn io::Write,
text: &str, text: &str,
focused: bool, focused: bool,
offset: (usize, usize), pos: (usize, usize),
colors: &UiColors, colors: &UiColors,
) { ) {
// To help identify it, the match thas has focus is rendered with a dedicated color. // To help identify it, the span thas has focus is rendered with a dedicated color.
let (fg_color, bg_color) = if focused { let (fg_color, bg_color) = if focused {
(&colors.focused_fg, &colors.focused_bg) (&colors.focused_fg, &colors.focused_bg)
} else { } else {
(&colors.match_fg, &colors.match_bg) (&colors.span_fg, &colors.span_bg)
}; };
// Render just the Match's text on top of existing content. // Render just the Span's text on top of existing content.
write!( write!(
stdout, stdout,
"{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}", "{goto}{bg_color}{fg_color}{text}{fg_reset}{bg_reset}",
goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1), goto = cursor::Goto(pos.0 as u16 + 1, pos.1 as u16 + 1),
fg_color = color::Fg(fg_color.as_ref()), fg_color = color::Fg(fg_color.as_ref()),
bg_color = color::Bg(bg_color.as_ref()), bg_color = color::Bg(bg_color.as_ref()),
fg_reset = color::Fg(color::Reset), fg_reset = color::Fg(color::Reset),
@ -216,20 +242,21 @@ impl<'a> ViewController<'a> {
.unwrap(); .unwrap();
} }
/// Render a Match's `hint` field on the provided writer. /// Render a Span's `hint` field on the provided writer.
/// ///
/// This renders the hint according to some provided style: /// This renders the hint according to some provided style:
/// - just colors /// - just colors
/// - underlined with colors /// - styled (bold, italic, underlined) with colors
/// - surrounding the hint's text with some delimiters, see /// - surrounding the hint's text with some delimiters, see
/// `HintStyle::Delimited`. /// `HintStyle::Delimited`.
/// ///
/// # Note /// # Note
///
/// This writes directly on the writer, avoiding extra allocation. /// This writes directly on the writer, avoiding extra allocation.
fn render_matched_hint( fn render_span_hint(
stdout: &mut dyn io::Write, stdout: &mut dyn io::Write,
hint_text: &str, hint_text: &str,
offset: (usize, usize), pos: (usize, usize),
colors: &UiColors, colors: &UiColors,
hint_style: &Option<HintStyle>, hint_style: &Option<HintStyle>,
) { ) {
@ -237,7 +264,7 @@ impl<'a> ViewController<'a> {
let bg_color = color::Bg(colors.hint_bg.as_ref()); let bg_color = color::Bg(colors.hint_bg.as_ref());
let fg_reset = color::Fg(color::Reset); let fg_reset = color::Fg(color::Reset);
let bg_reset = color::Bg(color::Reset); let bg_reset = color::Bg(color::Reset);
let goto = cursor::Goto(offset.0 as u16 + 1, offset.1 as u16 + 1); let goto = cursor::Goto(pos.0 as u16 + 1, pos.1 as u16 + 1);
match hint_style { match hint_style {
None => { None => {
@ -264,7 +291,7 @@ impl<'a> ViewController<'a> {
fg_reset = fg_reset, fg_reset = fg_reset,
bg_reset = bg_reset, bg_reset = bg_reset,
sty = style::Bold, sty = style::Bold,
sty_reset = style::NoBold, sty_reset = style::Reset, // NoBold is not sufficient
hint = hint_text, hint = hint_text,
) )
.unwrap(); .unwrap();
@ -318,35 +345,35 @@ impl<'a> ViewController<'a> {
} }
} }
/// Convenience function that renders both the matched text and its hint, /// Convenience function that renders both the text span and its hint,
/// if focused. /// if focused.
fn render_match(&self, stdout: &mut dyn io::Write, mat: &textbuf::Match<'a>, focused: bool) { fn render_span(&self, stdout: &mut dyn io::Write, span: &textbuf::Span<'a>, focused: bool) {
let text = mat.text; let text = span.text;
let (offset_x, offset_y) = self.match_offsets(mat); let (pos_x, pos_y) = self.adjusted_span_position(span);
let (offset_x, offset_y) = self.map_coords_to_wrapped_space(offset_x, offset_y); let (pos_x, pos_y) = self.map_coords_to_wrapped_space(pos_x, pos_y);
ViewController::render_matched_text( ViewController::render_span_text(
stdout, stdout,
text, text,
focused, focused,
(offset_x, offset_y), (pos_x, pos_y),
&self.rendering_colors, &self.rendering_colors,
); );
if !focused { if !focused {
// If not focused, render the hint (e.g. "eo") as an overlay on // If not focused, render the hint (e.g. "eo") as an overlay on
// top of the rendered matched text, aligned at its leading or the // top of the rendered text span, aligned at its leading or the
// trailing edge. // trailing edge.
let extra_offset = match self.hint_alignment { let offset = match self.hint_alignment {
HintAlignment::Leading => 0, HintAlignment::Leading => 0,
HintAlignment::Trailing => text.len() - mat.hint.len(), HintAlignment::Trailing => text.len() - span.hint.len(),
}; };
ViewController::render_matched_hint( ViewController::render_span_hint(
stdout, stdout,
&mat.hint, &span.hint,
(offset_x + extra_offset, offset_y), (pos_x + offset, pos_y),
&self.rendering_colors, &self.rendering_colors,
&self.hint_style, &self.hint_style,
); );
@ -357,50 +384,51 @@ impl<'a> ViewController<'a> {
/// ///
/// This renders in 3 phases: /// This renders in 3 phases:
/// - all lines are rendered verbatim /// - all lines are rendered verbatim
/// - each Match's `text` is rendered as an overlay on top of it /// - each Span's `text` is rendered as an overlay on top of it
/// - each Match's `hint` text is rendered as a final overlay /// - each Span's `hint` text is rendered as a final overlay
/// ///
/// Depending on the value of `self.hint_alignment`, the hint can be rendered /// Depending on the value of `self.hint_alignment`, the hint can be
/// on the leading edge of the underlying Match's `text`, /// rendered on the leading edge of the underlying Span's `text`, or on
/// or on the trailing edge. /// the trailing edge.
/// ///
/// # Note /// # Note
/// Multibyte characters are taken into account, so that the Match's `text` ///
/// Multibyte characters are taken into account, so that the Span's `text`
/// and `hint` are rendered in their proper position. /// and `hint` are rendered in their proper position.
fn full_render(&self, stdout: &mut dyn io::Write) { fn full_render(&self, stdout: &mut dyn io::Write) {
// 1. Trim all lines and render non-empty ones. // 1. Trim all lines and render non-empty ones.
ViewController::render_base_text( ViewController::render_base_text(
stdout, stdout,
&self.model.lines, &self.model.lines,
&self.line_offsets, &self.wrapped_lines,
&self.rendering_colors, &self.rendering_colors,
); );
for (index, mat) in self.model.matches.iter().enumerate() { for (index, span) in self.model.spans.iter().enumerate() {
let focused = index == self.focus_index; let focused = index == self.focus_index;
self.render_match(stdout, mat, focused); self.render_span(stdout, span, focused);
} }
stdout.flush().unwrap(); stdout.flush().unwrap();
} }
/// Render the previous match with its hint, and render the newly focused /// Render the previous span with its hint, and render the newly focused
/// match without its hint. This is more efficient than a full render. /// span without its hint. This is more efficient than a full render.
fn diff_render( fn diff_render(
&self, &self,
stdout: &mut dyn io::Write, stdout: &mut dyn io::Write,
old_focus_index: usize, old_focus_index: usize,
new_focus_index: usize, new_focus_index: usize,
) { ) {
// Render the previously focused match as non-focused // Render the previously focused span as non-focused
let mat = self.model.matches.get(old_focus_index).unwrap(); let span = self.model.spans.get(old_focus_index).unwrap();
let focused = false; let focused = false;
self.render_match(stdout, mat, focused); self.render_span(stdout, span, focused);
// Render the previously focused match as non-focused // Render the previously focused span as non-focused
let mat = self.model.matches.get(new_focus_index).unwrap(); let span = self.model.spans.get(new_focus_index).unwrap();
let focused = true; let focused = true;
self.render_match(stdout, mat, focused); self.render_span(stdout, span, focused);
stdout.flush().unwrap(); stdout.flush().unwrap();
} }
@ -409,7 +437,7 @@ impl<'a> ViewController<'a> {
// Listening {{{1 // Listening {{{1
/// Listen to keys entered on stdin, moving focus accordingly, or /// Listen to keys entered on stdin, moving focus accordingly, or
/// selecting one match. /// selecting one span.
/// ///
/// # Panics /// # Panics
/// ///
@ -417,7 +445,7 @@ impl<'a> ViewController<'a> {
fn listen(&mut self, reader: &mut dyn io::Read, writer: &mut dyn io::Write) -> Event { fn listen(&mut self, reader: &mut dyn io::Read, writer: &mut dyn io::Write) -> Event {
use termion::input::TermRead; // Trait for `reader.keys().next()`. use termion::input::TermRead; // Trait for `reader.keys().next()`.
if self.model.matches.is_empty() { if self.model.spans.is_empty() {
return Event::Exit; return Event::Exit;
} }
@ -448,7 +476,7 @@ impl<'a> ViewController<'a> {
break; break;
} }
// Move focus to next/prev match. // Move focus to next/prev span.
event::Key::Up => { event::Key::Up => {
let (old_index, focused_index) = self.prev_focus_index(); let (old_index, focused_index) = self.prev_focus_index();
self.diff_render(writer, old_index, focused_index); self.diff_render(writer, old_index, focused_index);
@ -484,16 +512,16 @@ impl<'a> ViewController<'a> {
// Yank/copy // Yank/copy
event::Key::Char(_ch @ 'y') | event::Key::Char(_ch @ '\n') => { event::Key::Char(_ch @ 'y') | event::Key::Char(_ch @ '\n') => {
let text = self.model.matches.get(self.focus_index).unwrap().text; let text = self.model.spans.get(self.focus_index).unwrap().text;
return Event::Match(Selection { return Event::Select(Selection {
text: text.to_string(), text: text.to_string(),
uppercased: false, uppercased: false,
output_destination, output_destination,
}); });
} }
event::Key::Char(_ch @ 'Y') => { event::Key::Char(_ch @ 'Y') => {
let text = self.model.matches.get(self.focus_index).unwrap().text; let text = self.model.spans.get(self.focus_index).unwrap().text;
return Event::Match(Selection { return Event::Select(Selection {
text: text.to_string(), text: text.to_string(),
uppercased: true, uppercased: true,
output_destination, output_destination,
@ -511,7 +539,7 @@ impl<'a> ViewController<'a> {
// Use a Trie or another data structure to determine // Use a Trie or another data structure to determine
// if the entered key belongs to a longer hint. // if the entered key belongs to a longer hint.
// Attempts at finding a match with a corresponding hint. // Attempts at finding a span with a corresponding hint.
// //
// If any of the typed character is caps, the typed hint is // If any of the typed character is caps, the typed hint is
// deemed as uppercased. // deemed as uppercased.
@ -535,12 +563,12 @@ impl<'a> ViewController<'a> {
let node = node.unwrap(); let node = node.unwrap();
if node.is_leaf() { if node.is_leaf() {
// The last key of a hint was entered. // The last key of a hint was entered.
let match_index = node.value().expect( let span_index = node.value().expect(
"By construction, the Lookup Trie should have a value for each leaf.", "By construction, the Lookup Trie should have a value for each leaf.",
); );
let mat = self.model.matches.get(*match_index).expect("By construction, the value in a leaf should correspond to an existing hint."); let span = self.model.spans.get(*span_index).expect("By construction, the value in a leaf should correspond to an existing hint.");
let text = mat.text.to_string(); let text = span.text.to_string();
return Event::Match(Selection { return Event::Select(Selection {
text, text,
uppercased, uppercased,
output_destination, output_destination,
@ -585,7 +613,7 @@ impl<'a> ViewController<'a> {
let selection = match self.listen(&mut stdin, &mut stdout) { let selection = match self.listen(&mut stdin, &mut stdout) {
Event::Exit => None, Event::Exit => None,
Event::Match(selection) => Some(selection), Event::Select(selection) => Some(selection),
}; };
write!(stdout, "{}", cursor::Show).unwrap(); write!(stdout, "{}", cursor::Show).unwrap();
@ -596,37 +624,40 @@ impl<'a> ViewController<'a> {
// }}} // }}}
} }
/// Compute each line's actual y offset if displayed in a terminal of width /// Compute each line's actual y position and size if displayed in a terminal of width
/// `term_width`. /// `term_width`.
fn get_line_offsets(lines: &[&str], term_width: u16) -> Vec<usize> { fn compute_wrapped_lines(lines: &[&str], term_width: u16) -> Vec<WrappedLine> {
lines lines
.iter() .iter()
.scan(0, |offset, &line| { .scan(0, |position, &line| {
// Save the value to return (yield is in unstable). // Save the value to return (yield is in unstable).
let value = *offset; let value = *position;
let line_width = line.trim_end().chars().count() as isize; let line_width = line.trim_end().chars().count() as isize;
// Amount of extra y space taken by this line. // Amount of extra y space taken by this line.
// If the line has n chars, on a term of width n, this does not // If the line has n chars, on a term of width n, this does not
// produce an extra line; it needs to exceed the width by 1 char. // produce an extra line; it needs to exceed the width by 1 char.
// In case the width is 0, we need to clamp line_width - 1 first. // In case the width is 0, we need to first clamp line_width - 1.
let extra = cmp::max(0, line_width - 1) as usize / term_width as usize; let extra = cmp::max(0, line_width - 1) as usize / term_width as usize;
// Update the offset of the next line. // Update the position of the next line.
*offset = *offset + 1 + extra; *position += 1 + extra;
Some(value) Some(WrappedLine {
pos_y: value,
// size: 1 + extra,
})
}) })
.collect() .collect()
} }
/// Returned value after the `Ui` has finished listening to events. /// Returned value after the `Ui` has finished listening to events.
enum Event { enum Event {
/// Exit with no selected matches, /// Exit with no selected spans,
Exit, Exit,
/// A vector of matched text and whether it was selected with uppercase. /// The selected span of text and whether it was selected with uppercase.
Match(Selection), Select(Selection),
} }
#[cfg(test)] #[cfg(test)]
@ -643,21 +674,28 @@ path: /usr/local/bin/git
path: /usr/local/bin/cargo"; path: /usr/local/bin/cargo";
let lines: Vec<&str> = content.split('\n').collect(); let lines: Vec<&str> = content.split('\n').collect();
let line_offsets: Vec<usize> = (0..lines.len()).collect(); let wrapped_lines: Vec<WrappedLine> = vec![
WrappedLine { pos_y: 0 },
WrappedLine { pos_y: 1 },
WrappedLine { pos_y: 2 },
WrappedLine { pos_y: 3 },
WrappedLine { pos_y: 4 },
WrappedLine { pos_y: 5 },
];
let colors = UiColors { let colors = UiColors {
text_fg: Box::new(color::Black), text_fg: Box::new(color::Black),
text_bg: Box::new(color::White), text_bg: Box::new(color::White),
focused_fg: Box::new(color::Red), focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue), focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green), span_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta), span_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow), hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan), hint_bg: Box::new(color::Cyan),
}; };
let mut writer = vec![]; let mut writer = vec![];
ViewController::render_base_text(&mut writer, &lines, &line_offsets, &colors); ViewController::render_base_text(&mut writer, &lines, &wrapped_lines, &colors);
let goto1 = cursor::Goto(1, 1); let goto1 = cursor::Goto(1, 1);
let goto2 = cursor::Goto(1, 2); let goto2 = cursor::Goto(1, 2);
@ -678,23 +716,23 @@ path: /usr/local/bin/cargo";
} }
#[test] #[test]
fn test_render_focused_matched_text() { fn test_render_focused_span_text() {
let mut writer = vec![]; let mut writer = vec![];
let text = "https://en.wikipedia.org/wiki/Barcelona"; let text = "https://en.wikipedia.org/wiki/Barcelona";
let focused = true; let focused = true;
let offset: (usize, usize) = (3, 1); let position: (usize, usize) = (3, 1);
let colors = UiColors { let colors = UiColors {
text_fg: Box::new(color::Black), text_fg: Box::new(color::Black),
text_bg: Box::new(color::White), text_bg: Box::new(color::White),
focused_fg: Box::new(color::Red), focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue), focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green), span_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta), span_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow), hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan), hint_bg: Box::new(color::Cyan),
}; };
ViewController::render_matched_text(&mut writer, text, focused, offset, &colors); ViewController::render_span_text(&mut writer, text, focused, position, &colors);
assert_eq!( assert_eq!(
writer, writer,
@ -712,31 +750,31 @@ path: /usr/local/bin/cargo";
} }
#[test] #[test]
fn test_render_matched_text() { fn test_render_span_text() {
let mut writer = vec![]; let mut writer = vec![];
let text = "https://en.wikipedia.org/wiki/Barcelona"; let text = "https://en.wikipedia.org/wiki/Barcelona";
let focused = false; let focused = false;
let offset: (usize, usize) = (3, 1); let position: (usize, usize) = (3, 1);
let colors = UiColors { let colors = UiColors {
text_fg: Box::new(color::Black), text_fg: Box::new(color::Black),
text_bg: Box::new(color::White), text_bg: Box::new(color::White),
focused_fg: Box::new(color::Red), focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue), focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green), span_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta), span_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow), hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan), hint_bg: Box::new(color::Cyan),
}; };
ViewController::render_matched_text(&mut writer, text, focused, offset, &colors); ViewController::render_span_text(&mut writer, text, focused, position, &colors);
assert_eq!( assert_eq!(
writer, writer,
format!( format!(
"{goto}{bg}{fg}{text}{fg_reset}{bg_reset}", "{goto}{bg}{fg}{text}{fg_reset}{bg_reset}",
goto = cursor::Goto(4, 2), goto = cursor::Goto(4, 2),
fg = color::Fg(colors.match_fg.as_ref()), fg = color::Fg(colors.span_fg.as_ref()),
bg = color::Bg(colors.match_bg.as_ref()), bg = color::Bg(colors.span_bg.as_ref()),
fg_reset = color::Fg(color::Reset), fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset), bg_reset = color::Bg(color::Reset),
text = &text, text = &text,
@ -746,28 +784,28 @@ path: /usr/local/bin/cargo";
} }
#[test] #[test]
fn test_render_unstyled_matched_hint() { fn test_render_unstyled_span_hint() {
let mut writer = vec![]; let mut writer = vec![];
let hint_text = "eo"; let hint_text = "eo";
let offset: (usize, usize) = (3, 1); let position: (usize, usize) = (3, 1);
let colors = UiColors { let colors = UiColors {
text_fg: Box::new(color::Black), text_fg: Box::new(color::Black),
text_bg: Box::new(color::White), text_bg: Box::new(color::White),
focused_fg: Box::new(color::Red), focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue), focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green), span_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta), span_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow), hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan), hint_bg: Box::new(color::Cyan),
}; };
let extra_offset = 0; let offset = 0;
let hint_style = None; let hint_style = None;
ViewController::render_matched_hint( ViewController::render_span_hint(
&mut writer, &mut writer,
hint_text, hint_text,
(offset.0 + extra_offset, offset.1), (position.0 + offset, position.1),
&colors, &colors,
&hint_style, &hint_style,
); );
@ -788,28 +826,28 @@ path: /usr/local/bin/cargo";
} }
#[test] #[test]
fn test_render_underlined_matched_hint() { fn test_render_underlined_span_hint() {
let mut writer = vec![]; let mut writer = vec![];
let hint_text = "eo"; let hint_text = "eo";
let offset: (usize, usize) = (3, 1); let position: (usize, usize) = (3, 1);
let colors = UiColors { let colors = UiColors {
text_fg: Box::new(color::Black), text_fg: Box::new(color::Black),
text_bg: Box::new(color::White), text_bg: Box::new(color::White),
focused_fg: Box::new(color::Red), focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue), focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green), span_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta), span_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow), hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan), hint_bg: Box::new(color::Cyan),
}; };
let extra_offset = 0; let offset = 0;
let hint_style = Some(HintStyle::Underline); let hint_style = Some(HintStyle::Underline);
ViewController::render_matched_hint( ViewController::render_span_hint(
&mut writer, &mut writer,
hint_text, hint_text,
(offset.0 + extra_offset, offset.1), (position.0 + offset, position.1),
&colors, &colors,
&hint_style, &hint_style,
); );
@ -832,28 +870,28 @@ path: /usr/local/bin/cargo";
} }
#[test] #[test]
fn test_render_bracketed_matched_hint() { fn test_render_bracketed_span_hint() {
let mut writer = vec![]; let mut writer = vec![];
let hint_text = "eo"; let hint_text = "eo";
let offset: (usize, usize) = (3, 1); let position: (usize, usize) = (3, 1);
let colors = UiColors { let colors = UiColors {
text_fg: Box::new(color::Black), text_fg: Box::new(color::Black),
text_bg: Box::new(color::White), text_bg: Box::new(color::White),
focused_fg: Box::new(color::Red), focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue), focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green), span_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta), span_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow), hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan), hint_bg: Box::new(color::Cyan),
}; };
let extra_offset = 0; let offset = 0;
let hint_style = Some(HintStyle::Surround('{', '}')); let hint_style = Some(HintStyle::Surround('{', '}'));
ViewController::render_matched_hint( ViewController::render_span_hint(
&mut writer, &mut writer,
hint_text, hint_text,
(offset.0 + extra_offset, offset.1), (position.0 + offset, position.1),
&colors, &colors,
&hint_style, &hint_style,
); );
@ -876,8 +914,8 @@ path: /usr/local/bin/cargo";
} }
#[test] #[test]
/// Simulates rendering without any match. /// Simulates rendering without any span.
fn test_render_full_without_matches() { fn test_render_full_without_available_spans() {
let buffer = "lorem 127.0.0.1 lorem let buffer = "lorem 127.0.0.1 lorem
Barcelona https://en.wikipedia.org/wiki/Barcelona - "; Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
@ -899,24 +937,24 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
unique_hint, unique_hint,
); );
let term_width: u16 = 80; let term_width: u16 = 80;
let line_offsets = get_line_offsets(&model.lines, term_width); let wrapped_lines = compute_wrapped_lines(&model.lines, term_width);
let rendering_colors = UiColors { let rendering_colors = UiColors {
text_fg: Box::new(color::Black), text_fg: Box::new(color::Black),
text_bg: Box::new(color::White), text_bg: Box::new(color::White),
focused_fg: Box::new(color::Red), focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue), focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green), span_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta), span_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow), hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan), hint_bg: Box::new(color::Cyan),
}; };
let hint_alignment = HintAlignment::Leading; let hint_alignment = HintAlignment::Leading;
// create a Ui without any match // create a Ui without any span
let ui = ViewController { let ui = ViewController {
model: &mut model, model: &mut model,
term_width, term_width,
line_offsets, wrapped_lines,
focus_index: 0, focus_index: 0,
focus_wrap_around: false, focus_wrap_around: false,
default_output_destination: OutputDestination::Tmux, default_output_destination: OutputDestination::Tmux,
@ -945,15 +983,12 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
// println!("{:?}", writer); // println!("{:?}", writer);
// println!("{:?}", expected.as_bytes()); // println!("{:?}", expected.as_bytes());
// println!("matches: {}", ui.matches.len());
// println!("lines: {}", lines.len());
assert_eq!(writer, expected.as_bytes()); assert_eq!(writer, expected.as_bytes());
} }
#[test] #[test]
/// Simulates rendering with matches. /// Simulates rendering with available spans.
fn test_render_full_with_matches() { fn test_render_full_with_spans() {
let buffer = "lorem 127.0.0.1 lorem let buffer = "lorem 127.0.0.1 lorem
Barcelona https://en.wikipedia.org/wiki/Barcelona - "; Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
@ -982,8 +1017,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
text_bg: Box::new(color::White), text_bg: Box::new(color::White),
focused_fg: Box::new(color::Red), focused_fg: Box::new(color::Red),
focused_bg: Box::new(color::Blue), focused_bg: Box::new(color::Blue),
match_fg: Box::new(color::Green), span_fg: Box::new(color::Green),
match_bg: Box::new(color::Magenta), span_bg: Box::new(color::Magenta),
hint_fg: Box::new(color::Yellow), hint_fg: Box::new(color::Yellow),
hint_bg: Box::new(color::Cyan), hint_bg: Box::new(color::Cyan),
}; };
@ -1018,19 +1053,19 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
) )
}; };
let expected_match1_text = { let expected_span1_text = {
let goto7_1 = cursor::Goto(7, 1); let goto7_1 = cursor::Goto(7, 1);
format!( format!(
"{goto7_1}{match_bg}{match_fg}127.0.0.1{fg_reset}{bg_reset}", "{goto7_1}{span_bg}{span_fg}127.0.0.1{fg_reset}{bg_reset}",
goto7_1 = goto7_1, goto7_1 = goto7_1,
match_fg = color::Fg(rendering_colors.match_fg.as_ref()), span_fg = color::Fg(rendering_colors.span_fg.as_ref()),
match_bg = color::Bg(rendering_colors.match_bg.as_ref()), span_bg = color::Bg(rendering_colors.span_bg.as_ref()),
fg_reset = color::Fg(color::Reset), fg_reset = color::Fg(color::Reset),
bg_reset = color::Bg(color::Reset) bg_reset = color::Bg(color::Reset)
) )
}; };
let expected_match1_hint = { let expected_span1_hint = {
let goto7_1 = cursor::Goto(7, 1); let goto7_1 = cursor::Goto(7, 1);
format!( format!(
@ -1043,7 +1078,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
) )
}; };
let expected_match2_text = { let expected_span2_text = {
let goto11_3 = cursor::Goto(11, 3); let goto11_3 = cursor::Goto(11, 3);
format!( format!(
"{goto11_3}{focus_bg}{focus_fg}https://en.wikipedia.org/wiki/Barcelona{fg_reset}{bg_reset}", "{goto11_3}{focus_bg}{focus_fg}https://en.wikipedia.org/wiki/Barcelona{fg_reset}{bg_reset}",
@ -1055,10 +1090,10 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
) )
}; };
// Because reverse is true, this second match is focused, // Because reverse is true, this second span is focused,
// then the hint should not be rendered. // then the hint should not be rendered.
// let expected_match2_hint = { // let expected_span2_hint = {
// let goto11_3 = cursor::Goto(11, 3); // let goto11_3 = cursor::Goto(11, 3);
// format!( // format!(
@ -1073,10 +1108,10 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
let expected = [ let expected = [
expected_content, expected_content,
expected_match1_text, expected_span1_text,
expected_match1_hint, expected_span1_hint,
expected_match2_text, expected_span2_text,
// expected_match2_hint, // expected_span2_hint,
] ]
.concat(); .concat();
@ -1090,7 +1125,7 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - ";
// .find(|(_idx, (&l, &r))| l != r); // .find(|(_idx, (&l, &r))| l != r);
// println!("{:?}", diff_point); // println!("{:?}", diff_point);
assert_eq!(2, ui.model.matches.len()); assert_eq!(2, ui.model.spans.len());
assert_eq!(writer, expected.as_bytes()); assert_eq!(writer, expected.as_bytes());
} }