summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorThomas Otto <th1000s@posteo.net>2024-11-15 00:07:10 +0100
committerThomas Otto <th1000s@posteo.net>2024-11-20 23:45:55 +0100
commit959471392d5aa0289f979c8898260e0f133d9ae7 (patch)
treec26156b1ee39fc74df554c8a6d1efa5947522ea1 /src
parent4ea8f9ab6038b934a03c6370e5c3e45da0466fe5 (diff)
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.
Diffstat (limited to 'src')
-rw-r--r--src/features/hyperlinks.rs163
1 files changed, 128 insertions, 35 deletions
diff --git a/src/features/hyperlinks.rs b/src/features/hyperlinks.rs
index ca21eb3..22ed273 100644
--- a/src/features/hyperlinks.rs
+++ b/src/features/hyperlinks.rs
@@ -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> {
+ // 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 {
- 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_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()) {
- 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_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,39 +125,96 @@ 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.
let input_type = InputType::GitDiff;