Skip to content

Commit

Permalink
Allow multiple hyperlinks per line
Browse files Browse the repository at this point in the history
Previously only the last commit was linked.

Do not link numbers (technically also commits), and stop after
finding 12 commits on a line.
  • Loading branch information
th1000s committed Nov 16, 2024
1 parent 8600dab commit cfa8d00
Showing 1 changed file with 137 additions and 35 deletions.
172 changes: 137 additions & 35 deletions src/features/hyperlinks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -31,26 +31,71 @@ 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> {
// 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..], and 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 on non-decimal:
if commit.contains(|c| matches!(c, 'a'..='f')) {
result.push_str(&self.0(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 {
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}")
})
let mut matches = COMMIT_HASH_REGEX.find_iter(line);
if let Some(first_match) = matches.next() {
let result = HyperlinkCommits(|commit| {
format_osc8_hyperlink(&commit_link_format.replace("{commit}", commit), commit)
})
.with_input(line, &first_match, &mut matches);
return Cow::from(result);
}
} 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)
let mut matches = COMMIT_HASH_REGEX.find_iter(line);
if let Some(first_match) = matches.next() {
let result = HyperlinkCommits(|commit| {
format!(
"{osc}8;;{url}{st}{commit}{osc}8;;{st}",
url = repo.format_commit_url(commit),
commit = commit,
osc = "\x1b]",
st = "\x1b\\"
)
})
.with_input(line, &first_match, &mut matches);
return Cow::from(result);
}
}
Cow::from(line)
}

/// Create a file hyperlink, displaying `text`.
Expand Down Expand Up @@ -86,38 +131,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.
Expand Down

0 comments on commit cfa8d00

Please sign in to comment.