Skip to content

Commit

Permalink
Format files and commits as OSC 8 hyperlinks
Browse files Browse the repository at this point in the history
Closes #257
  • Loading branch information
dandavison committed Jul 22, 2020
1 parent 0145e5d commit cca9738
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 12 deletions.
19 changes: 19 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,25 @@ pub struct Opt {
/// (overline), or the combination 'ul ol'.
pub file_decoration_style: String,

#[structopt(long = "hyperlinks")]
/// Format commit hash and file names as hyperlinks according to the hyperlink spec for
/// terminal emulators: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda.
/// Note that these hyperlinks are not yet supported by less, so they will not work in delta
/// unless you disable paging or install a fork of less. They are supported by several but not
/// all terminal emulators, but they are not supported yet by tmux. Versions of less and tmux
/// that support hyperlinks are at https://github.com/dandavison/less and
/// https://github.com/dandavison/tmux.
pub hyperlinks: bool,

/// Format string for file hyperlinks. The default value will format file hyperlinks as a
/// standard file URL; your operating system should open this in the application registered for
/// that file type. However, these cannot link to a line number. In order for the link to open
/// the file at the correct line number, one possibility would be to use a format string like
/// "http://localhost:PORT/open{path}", and arrange for a web server to respond to that HTTP
/// request by opening the file in the desired application.
#[structopt(long = "hyperlinks-file-link-format", default_value = "file://{path}")]
pub hyperlinks_file_link_format: String,

#[structopt(long = "hunk-header-style", default_value = "syntax")]
/// Style (foreground, background, attributes) for the hunk-header. See STYLES section. The
/// style 'omit' can be used to remove the hunk header section from the output.
Expand Down
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub struct Config {
pub git_config_entries: HashMap<String, GitConfigEntry>,
pub keep_plus_minus_markers: bool,
pub hunk_header_style: Style,
pub hyperlinks: bool,
pub hyperlinks_file_link_format: String,
pub max_buffered_lines: usize,
pub max_line_distance: f64,
pub max_line_distance_for_naively_paired_lines: f64,
Expand Down Expand Up @@ -140,6 +142,8 @@ impl From<cli::Opt> for Config {
git_config_entries: opt.git_config_entries,
keep_plus_minus_markers: opt.keep_plus_minus_markers,
hunk_header_style,
hyperlinks: opt.hyperlinks,
hyperlinks_file_link_format: opt.hyperlinks_file_link_format,
max_buffered_lines: 32,
max_line_distance: opt.max_line_distance,
max_line_distance_for_naively_paired_lines,
Expand Down
28 changes: 25 additions & 3 deletions src/delta.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::io::BufRead;
use std::io::Write;

Expand All @@ -7,6 +8,8 @@ use unicode_segmentation::UnicodeSegmentation;

use crate::config::Config;
use crate::draw;
use crate::features;
use crate::format;
use crate::paint::Painter;
use crate::parse;
use crate::style::DecorationStyle;
Expand Down Expand Up @@ -155,7 +158,11 @@ where
continue;
} else {
painter.emit()?;
writeln!(painter.writer, "{}", raw_line)?;
writeln!(
painter.writer,
"{}",
format::format_raw_line(&raw_line, config)
)?;
}
}

Expand Down Expand Up @@ -240,10 +247,25 @@ fn handle_commit_meta_header_line(
draw::write_no_decoration
}
};
let (formatted_line, formatted_raw_line) = if config.hyperlinks {
(
Cow::from(
features::hyperlinks::format_commit_line_with_osc8_commit_hyperlink(line, config),
),
Cow::from(
features::hyperlinks::format_commit_line_with_osc8_commit_hyperlink(
raw_line, config,
),
),
)
} else {
(Cow::from(line), Cow::from(raw_line))
};

draw_fn(
painter.writer,
&format!("{}{}", line, if pad { " " } else { "" }),
&format!("{}{}", raw_line, if pad { " " } else { "" }),
&format!("{}{}", formatted_line, if pad { " " } else { "" }),
&format!("{}{}", formatted_raw_line, if pad { " " } else { "" }),
&config.decorations_width,
config.commit_style,
decoration_ansi_term_style,
Expand Down
88 changes: 88 additions & 0 deletions src/features/hyperlinks.rs
Original file line number Diff line number Diff line change
@@ -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_number}", &format!("{}", n))
} else {
url = url.replace("{line_number}", "")
};
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)
}
5 changes: 5 additions & 0 deletions src/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ pub fn make_builtin_features() -> HashMap<String, BuiltinFeature> {
"diff-so-fancy".to_string(),
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(),
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions src/format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use std::borrow::Cow;

use atty;

use crate::config::Config;
use crate::features;

/// If output is going to a tty, emit hyperlinks if requested.
// Although raw output should basically be emitted unaltered, we do this.
pub fn format_raw_line<'a>(line: &'a str, config: &Config) -> Cow<'a, str> {
if config.hyperlinks && atty::is(atty::Stream::Stdout) {
features::hyperlinks::format_commit_line_with_osc8_commit_hyperlink(line, config)
} else {
Cow::from(line)
}
}
40 changes: 40 additions & 0 deletions src/git_config_entry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use std::path::PathBuf;
use std::result::Result;
use std::str::FromStr;

use lazy_static::lazy_static;
use regex::Regex;

use crate::errors::*;
use crate::style::Style;

#[derive(Clone, Debug)]
pub enum GitConfigEntry {
Style(Style),
GitRemote(GitRemoteRepo),
Path(PathBuf),
}

#[derive(Clone, Debug)]
pub enum GitRemoteRepo {
GitHubRepo(String),
}

lazy_static! {
static ref GITHUB_REMOTE_URL: Regex = Regex::new(r"github\.com[:/]([^/]+)/(.+)\.git").unwrap();
}

impl FromStr for GitRemoteRepo {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(caps) = GITHUB_REMOTE_URL.captures(s) {
Ok(Self::GitHubRepo(format!(
"{user}/{repo}",
user = caps.get(1).unwrap().as_str(),
repo = caps.get(2).unwrap().as_str()
)))
} else {
Err("Not a GitHub repo.".into())
}
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod draw;
mod edits;
mod env;
mod features;
mod format;
mod git_config;
mod git_config_entry;
mod options;
Expand Down
14 changes: 14 additions & 0 deletions src/options/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ pub fn set_options(
file_style,
hunk_header_decoration_style,
hunk_header_style,
hyperlinks,
hyperlinks_file_link_format,
keep_plus_minus_markers,
max_line_distance,
// Hack: minus-style must come before minus-*emph-style because the latter default
Expand Down Expand Up @@ -296,6 +298,9 @@ fn gather_features<'a>(
if opt.diff_so_fancy {
gather_builtin_features_recursively("diff-so-fancy", &mut features, &builtin_features, opt);
}
if opt.hyperlinks {
gather_builtin_features_recursively("hyperlinks", &mut features, &builtin_features, opt);
}
if opt.line_numbers {
gather_builtin_features_recursively("line-numbers", &mut features, &builtin_features, opt);
}
Expand Down Expand Up @@ -523,6 +528,15 @@ fn set_git_config_entries(opt: &mut cli::Opt, git_config: &mut git_config::GitCo
}
}
}

if let Some(repo) = &git_config.repo {
if let Some(workdir) = repo.workdir() {
opt.git_config_entries.insert(
"delta.__workdir__".to_string(),
GitConfigEntry::Path(workdir.to_path_buf()),
);
}
}
}

#[cfg(test)]
Expand Down
31 changes: 22 additions & 9 deletions src/parse.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use lazy_static::lazy_static;
use regex::Regex;
use std::borrow::Cow;
use std::path::Path;

use crate::config::Config;
use crate::features;

// https://git-scm.com/docs/git-config#Documentation/git-config.txt-diffmnemonicPrefix
const DIFF_PREFIXES: [&str; 6] = ["a/", "b/", "c/", "i/", "o/", "w/"];
Expand Down Expand Up @@ -67,23 +69,34 @@ pub fn get_file_change_description_from_file_paths(
"".to_string()
}
};
let format_file = |file| {
if config.hyperlinks {
features::hyperlinks::format_osc8_file_hyperlink(file, None, file, config)
} else {
Cow::from(file)
}
};
match (minus_file, plus_file) {
(minus_file, plus_file) if minus_file == plus_file => format!(
"{}{}",
format_label(&config.file_modified_label),
minus_file
format_file(minus_file)
),
(minus_file, "/dev/null") => format!(
"{}{}",
format_label(&config.file_removed_label),
format_file(minus_file)
),
("/dev/null", plus_file) => format!(
"{}{}",
format_label(&config.file_added_label),
format_file(plus_file)
),
(minus_file, "/dev/null") => {
format!("{}{}", format_label(&config.file_removed_label), minus_file)
}
("/dev/null", plus_file) => {
format!("{}{}", format_label(&config.file_added_label), plus_file)
}
(minus_file, plus_file) => format!(
"{}{} ⟶ {}",
format_label(&config.file_renamed_label),
minus_file,
plus_file
format_file(minus_file),
format_file(plus_file)
),
}
}
Expand Down

0 comments on commit cca9738

Please sign in to comment.