Allow multiple hyperlinks per line

Previously only the last commit was linked.

Do not link numbers (technically also commits), and stop after
finding 12 commits on a line.
This commit is contained in:
Thomas Otto 2024-11-15 00:07:10 +01:00
parent 4ea8f9ab60
commit 959471392d

View File

@ -2,7 +2,7 @@ use std::borrow::Cow;
use std::path::Path;
use lazy_static::lazy_static;
use regex::{Captures, Regex};
use regex::{Match, Matches, Regex};
use crate::config::Config;
use crate::features::OptionValueFunction;
@ -31,26 +31,62 @@ pub fn remote_from_config(cfg: &Option<&GitConfig>) -> Option<GitRemoteRepo> {
cfg.and_then(GitConfig::get_remote_url)
}
lazy_static! {
// note: pure numbers are filtered out later again
static ref COMMIT_HASH_REGEX: Regex = Regex::new(r"\b[0-9a-f]{8,40}\b").unwrap();
}
pub fn format_commit_line_with_osc8_commit_hyperlink<'a>(
line: &'a str,
config: &Config,
) -> Cow<'a, str> {
if let Some(commit_link_format) = &config.hyperlinks_commit_link_format {
COMMIT_LINE_REGEX.replace(line, |captures: &Captures| {
let prefix = captures.get(1).map(|m| m.as_str()).unwrap_or("");
let commit = captures.get(2).map(|m| m.as_str()).unwrap();
let suffix = captures.get(3).map(|m| m.as_str()).unwrap_or("");
let formatted_commit =
format_osc8_hyperlink(&commit_link_format.replace("{commit}", commit), commit);
format!("{prefix}{formatted_commit}{suffix}")
})
} else if let Some(repo) = remote_from_config(&config.git_config()) {
COMMIT_LINE_REGEX.replace(line, |captures: &Captures| {
format_commit_line_captures_with_osc8_commit_hyperlink(captures, &repo)
})
} else {
Cow::from(line)
// Given matches in a line, m = matches[0] and pos = 0: store line[pos..m.start()] first, then
// store the T(line[m.start()..m.end()]) match transformation, then set pos = m.end().
// Repeat for matches[1..]. Finally, store line[pos..].
struct HyperlinkCommits<T>(T)
where
T: Fn(&str) -> String;
impl<T: for<'b> Fn(&'b str) -> String> HyperlinkCommits<T> {
fn _m(&self, result: &mut String, line: &str, m: &Match, prev_pos: usize) -> usize {
result.push_str(&line[prev_pos..m.start()]);
let commit = &line[m.start()..m.end()];
// Do not link numbers, require at least one non-decimal:
if commit.contains(|c| matches!(c, 'a'..='f')) {
result.push_str(&format_osc8_hyperlink(&self.0(commit), commit));
} else {
result.push_str(commit);
}
m.end()
}
fn with_input(&self, line: &str, m0: &Match, matches123: &mut Matches) -> String {
let mut result = String::new();
let mut pos = self._m(&mut result, line, m0, 0);
// limit number of matches per line, an exhaustive `find_iter` is O(len(line) * len(regex)^2)
for m in matches123.take(12) {
pos = self._m(&mut result, line, &m, pos);
}
result.push_str(&line[pos..]);
result
}
}
if let Some(commit_link_format) = &config.hyperlinks_commit_link_format {
let mut matches = COMMIT_HASH_REGEX.find_iter(line);
if let Some(first_match) = matches.next() {
let result =
HyperlinkCommits(|commit_hash| commit_link_format.replace("{commit}", commit_hash))
.with_input(line, &first_match, &mut matches);
return Cow::from(result);
}
} else if let Some(repo) = remote_from_config(&config.git_config()) {
let mut matches = COMMIT_HASH_REGEX.find_iter(line);
if let Some(first_match) = matches.next() {
let result = HyperlinkCommits(|commit_hash| repo.format_commit_url(commit_hash))
.with_input(line, &first_match, &mut matches);
return Cow::from(result);
}
}
Cow::from(line)
}
/// Create a file hyperlink, displaying `text`.
@ -89,38 +125,95 @@ fn format_osc8_hyperlink(url: &str, text: &str) -> String {
)
}
lazy_static! {
static ref COMMIT_LINE_REGEX: Regex = Regex::new("(.* )?([0-9a-f]{8,40})(.*)").unwrap();
}
fn format_commit_line_captures_with_osc8_commit_hyperlink(
captures: &Captures,
repo: &GitRemoteRepo,
) -> String {
let commit = captures.get(2).unwrap().as_str();
format!(
"{prefix}{osc}8;;{url}{st}{commit}{osc}8;;{st}{suffix}",
url = repo.format_commit_url(commit),
commit = commit,
prefix = captures.get(1).map(|m| m.as_str()).unwrap_or(""),
suffix = captures.get(3).unwrap().as_str(),
osc = "\x1b]",
st = "\x1b\\"
)
}
#[cfg(not(target_os = "windows"))]
#[cfg(test)]
pub mod tests {
use std::iter::FromIterator;
use std::path::PathBuf;
use pretty_assertions::assert_eq;
use super::*;
use crate::{
tests::integration_test_utils::{self, DeltaTest},
tests::integration_test_utils::{self, make_config_from_args, DeltaTest},
utils,
};
#[test]
fn test_formatted_hyperlinks() {
let config = make_config_from_args(&["--hyperlinks-commit-link-format", "HERE:{commit}"]);
let line = "001234abcdf";
let result = format_commit_line_with_osc8_commit_hyperlink(line, &config);
assert_eq!(
result,
"\u{1b}]8;;HERE:001234abcdf\u{1b}\\001234abcdf\u{1b}]8;;\u{1b}\\",
);
let line = "a2272718f0b398e48652ace17fca85c1962b3fc22"; // length: 41 > 40
let result = format_commit_line_with_osc8_commit_hyperlink(line, &config);
assert_eq!(result, "a2272718f0b398e48652ace17fca85c1962b3fc22",);
let line = "a2272718f0+b398e48652ace17f,ca85c1962b3fc2";
let result = format_commit_line_with_osc8_commit_hyperlink(line, &config);
assert_eq!(result, "\u{1b}]8;;HERE:a2272718f0\u{1b}\\a2272718f0\u{1b}]8;;\u{1b}\\+\u{1b}]8;;\
HERE:b398e48652ace17f\u{1b}\\b398e48652ace17f\u{1b}]8;;\u{1b}\\,\u{1b}]8;;HERE:ca85c1962b3fc2\
\u{1b}\\ca85c1962b3fc2\u{1b}]8;;\u{1b}\\");
let line = "This 01234abcdf Hash";
let result = format_commit_line_with_osc8_commit_hyperlink(line, &config);
assert_eq!(
result,
"This \u{1b}]8;;HERE:01234abcdf\u{1b}\\01234abcdf\u{1b}]8;;\u{1b}\\ Hash",
);
let line =
"Another 01234abcdf hash but also this one: dc623b084ad2dd14fe5d90189cacad5d49bfbfd3!";
let result = format_commit_line_with_osc8_commit_hyperlink(line, &config);
assert_eq!(
result,
"Another \u{1b}]8;;HERE:01234abcdf\u{1b}\\01234abcdf\u{1b}]8;;\u{1b}\\ hash but \
also this one: \u{1b}]8;;HERE:dc623b084ad2dd14fe5d90189cacad5d49bfbfd3\u{1b}\
\\dc623b084ad2dd14fe5d90189cacad5d49bfbfd3\u{1b}]8;;\u{1b}\\!"
);
let line = "01234abcdf 03043baf30 12abcdef0 12345678";
let result = format_commit_line_with_osc8_commit_hyperlink(line, &config);
assert_eq!(
result,
"\u{1b}]8;;HERE:01234abcdf\u{1b}\\01234abcdf\u{1b}]8;;\u{1b}\\ \u{1b}]8;;\
HERE:03043baf30\u{1b}\\03043baf30\u{1b}]8;;\u{1b}\\ \u{1b}]8;;HERE:12abcdef0\u{1b}\\\
12abcdef0\u{1b}]8;;\u{1b}\\ 12345678"
);
}
#[test]
fn test_hyperlinks_to_repo() {
let mut config = make_config_from_args(&["--hyperlinks"]);
config.git_config = GitConfig::for_testing();
let line = "This a589ff9debaefdd delta commit";
let result = format_commit_line_with_osc8_commit_hyperlink(line, &config);
assert_eq!(
result,
"This \u{1b}]8;;https://github.com/dandavison/delta/commit/a589ff9debaefdd\u{1b}\
\\a589ff9debaefdd\u{1b}]8;;\u{1b}\\ delta commit",
);
let line =
"Another a589ff9debaefdd hash but also this one: c5696757c0827349a87daa95415656!";
let result = format_commit_line_with_osc8_commit_hyperlink(line, &config);
assert_eq!(
result,
"Another \u{1b}]8;;https://github.com/dandavison/delta/commit/a589ff9debaefdd\
\u{1b}\\a589ff9debaefdd\u{1b}]8;;\u{1b}\\ hash but also this one: \u{1b}]8;;\
https://github.com/dandavison/delta/commit/c5696757c0827349a87daa95415656\u{1b}\
\\c5696757c0827349a87daa95415656\u{1b}]8;;\
\u{1b}\\!"
);
}
#[test]
fn test_paths_and_hyperlinks_user_in_repo_root_dir() {
// Expectations are uninfluenced by git's --relative and delta's relative_paths options.