summaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2020-07-18 15:34:43 -0400
committerDan Davison <dandavison7@gmail.com>2020-07-22 17:57:57 -0400
commitb2257cfae7eacc73e47299d90d9a8d479b3e362c (patch)
tree56b60ad41ce689042ce64c4793eec35100d8c322 /src/features
parent29bf022218f72157e1921412ae6ede598733b6fb (diff)
Format files and commits as OSC 8 hyperlinks
Closes #257
Diffstat (limited to 'src/features')
-rw-r--r--src/features/hyperlinks.rs88
-rw-r--r--src/features/line_numbers.rs40
-rw-r--r--src/features/mod.rs5
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;