diff options
author | Dan Davison <dandavison7@gmail.com> | 2020-07-18 15:34:43 -0400 |
---|---|---|
committer | Dan Davison <dandavison7@gmail.com> | 2020-07-22 17:57:57 -0400 |
commit | b2257cfae7eacc73e47299d90d9a8d479b3e362c (patch) | |
tree | 56b60ad41ce689042ce64c4793eec35100d8c322 /src/features | |
parent | 29bf022218f72157e1921412ae6ede598733b6fb (diff) |
Format files and commits as OSC 8 hyperlinks
Closes #257
Diffstat (limited to 'src/features')
-rw-r--r-- | src/features/hyperlinks.rs | 88 | ||||
-rw-r--r-- | src/features/line_numbers.rs | 40 | ||||
-rw-r--r-- | src/features/mod.rs | 5 |
3 files changed, 127 insertions, 6 deletions
diff --git a/src/features/hyperlinks.rs b/src/features/hyperlinks.rs new file mode 100644 index 00000000..8fc58e9f --- /dev/null +++ b/src/features/hyperlinks.rs @@ -0,0 +1,88 @@ +use std::borrow::Cow; + +use lazy_static::lazy_static; +use regex::{Captures, Regex}; + +use crate::config::Config; +use crate::features::OptionValueFunction; +use crate::git_config_entry::{GitConfigEntry, GitRemoteRepo}; + +pub fn make_feature() -> Vec<(String, OptionValueFunction)> { + builtin_feature!([ + ( + "hyperlinks", + bool, + None, + _opt => true + ) + ]) +} + +pub fn format_commit_line_with_osc8_commit_hyperlink<'a>( + line: &'a str, + config: &Config, +) -> Cow<'a, str> { + if let Some(GitConfigEntry::GitRemote(GitRemoteRepo::GitHubRepo(repo))) = + config.git_config_entries.get("remote.origin.url") + { + COMMIT_LINE_REGEX.replace(line, |captures: &Captures| { + format_commit_line_captures_with_osc8_commit_hyperlink(captures, repo) + }) + } else { + Cow::from(line) + } +} + +/// Create a file hyperlink to `path`, displaying `text`. +pub fn format_osc8_file_hyperlink<'a>( + relative_path: &'a str, + line_number: Option<usize>, + text: &str, + config: &Config, +) -> Cow<'a, str> { + if let Some(GitConfigEntry::Path(workdir)) = config.git_config_entries.get("delta.__workdir__") + { + let absolute_path = workdir.join(relative_path); + let mut url = config + .hyperlinks_file_link_format + .replace("{path}", &absolute_path.to_string_lossy()); + if let Some(n) = line_number { + url = url.replace("{line}", &format!("{}", n)) + } else { + url = url.replace("{line}", "") + }; + Cow::from(format!( + "{osc}8;;{url}{st}{text}{osc}8;;{st}", + url = url, + text = text, + osc = "\x1b]", + st = "\x1b\\" + )) + } else { + Cow::from(relative_path) + } +} + +lazy_static! { + static ref COMMIT_LINE_REGEX: Regex = Regex::new("(.* )([0-9a-f]{40})(.*)").unwrap(); +} + +fn format_commit_line_captures_with_osc8_commit_hyperlink<'a, 'b>( + captures: &'a Captures, + github_repo: &'b str, +) -> String { + let commit = captures.get(2).unwrap().as_str(); + format!( + "{prefix}{osc}8;;{url}{st}{commit}{osc}8;;{st}{suffix}", + url = format_github_commit_url(commit, github_repo), + commit = commit, + prefix = captures.get(1).unwrap().as_str(), + suffix = captures.get(3).unwrap().as_str(), + osc = "\x1b]", + st = "\x1b\\" + ) +} + +fn format_github_commit_url(commit: &str, github_repo: &str) -> String { + format!("https://github.com/{}/commit/{}", github_repo, commit) +} diff --git a/src/features/line_numbers.rs b/src/features/line_numbers.rs index b1d9b5b0..1543f46d 100644 --- a/src/features/line_numbers.rs +++ b/src/features/line_numbers.rs @@ -6,6 +6,7 @@ use regex::Regex; use crate::config; use crate::delta::State; +use crate::features::hyperlinks; use crate::features::side_by_side; use crate::features::OptionValueFunction; use crate::style::Style; @@ -112,6 +113,8 @@ pub fn format_and_paint_line_numbers<'a>( line_numbers_data.hunk_max_line_number_width, &minus_style, &plus_style, + &line_numbers_data.plus_file, + config, )); } @@ -124,6 +127,8 @@ pub fn format_and_paint_line_numbers<'a>( line_numbers_data.hunk_max_line_number_width, &minus_style, &plus_style, + &line_numbers_data.plus_file, + config, )); } formatted_numbers @@ -155,6 +160,7 @@ pub struct LineNumbersData<'a> { pub hunk_minus_line_number: usize, pub hunk_plus_line_number: usize, pub hunk_max_line_number_width: usize, + pub plus_file: String, } // Although it's probably unusual, a single format string can contain multiple placeholders. E.g. @@ -178,11 +184,12 @@ impl<'a> LineNumbersData<'a> { hunk_minus_line_number: 0, hunk_plus_line_number: 0, hunk_max_line_number_width: 0, + plus_file: "".to_string(), } } /// Initialize line number data for a hunk. - pub fn initialize_hunk(&mut self, line_numbers: Vec<(usize, usize)>) { + pub fn initialize_hunk(&mut self, line_numbers: Vec<(usize, usize)>, plus_file: String) { // Typically, line_numbers has length 2: an entry for the minus file, and one for the plus // file. In the case of merge commits, it may be longer. self.hunk_minus_line_number = line_numbers[0].0; @@ -190,6 +197,7 @@ impl<'a> LineNumbersData<'a> { let hunk_max_line_number = line_numbers.iter().map(|(n, d)| n + d).max().unwrap(); self.hunk_max_line_number_width = 1 + (hunk_max_line_number as f64).log10().floor() as usize; + self.plus_file = plus_file; } } @@ -233,6 +241,8 @@ fn format_and_paint_line_number_field<'a>( min_field_width: usize, minus_number_style: &Style, plus_number_style: &Style, + plus_file: &str, + config: &config::Config, ) -> Vec<ansi_term::ANSIGenericString<'a, str>> { let mut ansi_strings = Vec::new(); let mut suffix = ""; @@ -251,11 +261,15 @@ fn format_and_paint_line_number_field<'a>( minus_number, alignment_spec, width, + None, + config, ))), Some("np") => ansi_strings.push(plus_number_style.paint(format_line_number( plus_number, alignment_spec, width, + Some(plus_file), + config, ))), None => {} Some(_) => unreachable!(), @@ -267,15 +281,29 @@ fn format_and_paint_line_number_field<'a>( } /// Return line number formatted according to `alignment` and `width`. -fn format_line_number(line_number: Option<usize>, alignment: &str, width: usize) -> String { - let n = line_number - .map(|n| format!("{}", n)) - .unwrap_or_else(|| "".to_string()); - match alignment { +fn format_line_number( + line_number: Option<usize>, + alignment: &str, + width: usize, + plus_file: Option<&str>, + config: &config::Config, +) -> String { + let format_n = |n| match alignment { "<" => format!("{0:<1$}", n, width), "^" => format!("{0:^1$}", n, width), ">" => format!("{0:>1$}", n, width), _ => unreachable!(), + }; + match (line_number, config.hyperlinks, plus_file) { + (None, _, _) => format_n(""), + (Some(n), true, Some(file)) => hyperlinks::format_osc8_file_hyperlink( + file, + line_number, + &format_n(&n.to_string()), + config, + ) + .to_string(), + (Some(n), _, _) => format_n(&n.to_string()), } } diff --git a/src/features/mod.rs b/src/features/mod.rs index 58599ea6..67dbd6ba 100644 --- a/src/features/mod.rs +++ b/src/features/mod.rs @@ -39,6 +39,10 @@ pub fn make_builtin_features() -> HashMap<String, BuiltinFeature> { diff_so_fancy::make_feature().into_iter().collect(), ), ( + "hyperlinks".to_string(), + hyperlinks::make_feature().into_iter().collect(), + ), + ( "line-numbers".to_string(), line_numbers::make_feature().into_iter().collect(), ), @@ -81,6 +85,7 @@ macro_rules! builtin_feature { pub mod color_only; pub mod diff_highlight; pub mod diff_so_fancy; +pub mod hyperlinks; pub mod line_numbers; pub mod navigate; pub mod raw; |