Skip to content

Commit

Permalink
Merge pull request #261 from dandavison/257-hyperlinks
Browse files Browse the repository at this point in the history
Add OSC hyperlinks
  • Loading branch information
dandavison authored Jul 22, 2020
2 parents 8233a75 + b2257cf commit cac42e3
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 25 deletions.
29 changes: 29 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use syntect::parsing::SyntaxSet;
use crate::bat::assets::HighlightingAssets;
use crate::bat::output::PagingMode;
use crate::git_config::GitConfig;
use crate::git_config_entry::GitConfigEntry;
use crate::options;

#[derive(StructOpt, Clone, Default)]
Expand Down Expand Up @@ -340,6 +341,31 @@ pub struct Opt {
/// (overline), or the combination 'ul ol'.
pub file_decoration_style: String,

#[structopt(long = "hyperlinks")]
/// Render commit hashes, file names, and line numbers as hyperlinks, according to the
/// hyperlink spec for terminal emulators:
/// https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda. By default, file names
/// and line numbers link to the local file using a file URL, whereas commit hashes link to the
/// commit in GitHub, if the remote repository is hosted by GitHub. See
/// --hyperlinks-file-link-format for full control over the file URLs emitted. Hyperlinks are
/// supported by several common terminal emulators. However, they are not yet supported by
/// less, so they will not work in delta unless you install a patched fork of less (see
/// https://github.com/dandavison/less). If you use tmux, then you will also need a patched
/// fork of tmux (see https://github.com/dandavison/tmux).
pub hyperlinks: bool,

/// Format string for file hyperlinks. The placeholders "{path}" and "{line}" will be replaced
/// by the absolute file path and the line number, respectively. The default value of this
/// option creates hyperlinks using standard file URLs; your operating system should open these
/// in the application registered for that file type. However, these do not make use of the
/// line number. In order for the link to open the file at the correct line number, you could
/// use a custom URL format such as "file-line://{path}:{line_number}" and register an
/// application to handle the custom "file-line" URL scheme by opening the file in your
/// editor/IDE at the indicated line number. See https://github.com/dandavison/open-in-editor
/// for an example.
#[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 Expand Up @@ -517,6 +543,9 @@ pub struct Opt {

#[structopt(skip)]
pub computed: ComputedValues,

#[structopt(skip)]
pub git_config_entries: HashMap<String, GitConfigEntry>,
}

#[derive(Default, Clone, Debug)]
Expand Down
8 changes: 8 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::process;

Expand All @@ -13,6 +14,7 @@ use crate::color;
use crate::delta::State;
use crate::env;
use crate::features::side_by_side;
use crate::git_config_entry::GitConfigEntry;
use crate::style::Style;

pub struct Config {
Expand All @@ -25,8 +27,11 @@ pub struct Config {
pub file_removed_label: String,
pub file_renamed_label: String,
pub file_style: Style,
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 @@ -134,8 +139,11 @@ impl From<cli::Opt> for Config {
file_removed_label: opt.file_removed_label,
file_renamed_label: opt.file_renamed_label,
file_style,
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
51 changes: 43 additions & 8 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 @@ -60,7 +63,7 @@ where
{
let mut painter = Painter::new(writer, config);
let mut minus_file = "".to_string();
let mut plus_file;
let mut plus_file = "".to_string();
let mut state = State::Unknown;
let mut source = Source::Unknown;

Expand Down Expand Up @@ -115,7 +118,7 @@ where
painter.set_highlighter();
if should_handle(&state, config) {
painter.emit()?;
handle_hunk_header_line(&mut painter, &line, &raw_line, config)?;
handle_hunk_header_line(&mut painter, &line, &raw_line, &plus_file, config)?;
continue;
}
} else if source == Source::DiffUnified && line.starts_with("Only in ")
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 Expand Up @@ -332,6 +354,7 @@ fn handle_hunk_header_line(
painter: &mut Painter,
line: &str,
raw_line: &str,
plus_file: &str,
config: &Config,
) -> std::io::Result<()> {
if config.hunk_header_style.is_omitted {
Expand Down Expand Up @@ -425,16 +448,28 @@ fn handle_hunk_header_line(
};
// Emit a single line number, or prepare for full line-numbering
if config.line_numbers {
painter.line_numbers_data.initialize_hunk(line_numbers);
painter
.line_numbers_data
.initialize_hunk(line_numbers, plus_file.to_string());
} else {
let plus_line_number = line_numbers[line_numbers.len() - 1].0;
let formatted_plus_line_number = if config.hyperlinks {
features::hyperlinks::format_osc8_file_hyperlink(
plus_file,
Some(plus_line_number),
&format!("{}", plus_line_number),
config,
)
} else {
Cow::from(format!("{}", plus_line_number))
};
match config.hunk_header_style.decoration_ansi_term_style() {
Some(style) => writeln!(
painter.writer,
"{}",
style.paint(format!("{}", plus_line_number))
style.paint(formatted_plus_line_number)
)?,
None => writeln!(painter.writer, "{}", plus_line_number)?,
None => writeln!(painter.writer, "{}", formatted_plus_line_number)?,
}
}
Ok(())
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}", &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)
}
Loading

0 comments on commit cac42e3

Please sign in to comment.