Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support --color-moved #267

Merged
merged 5 commits into from
Aug 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ shell-words = "1.0.0"
structopt = "0.3.15"
unicode-segmentation = "1.6.0"
unicode-width = "0.1.8"
vte = "0.8.0"

[dependencies.git2]
version = "0.13.8"
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Code evolves, and studying diffs is a fundamental mode of work. Delta aims to ma
- Line numbering
- `diff-highlight` and `diff-so-fancy` emulation modes
- Stylable box/line decorations to draw attention to commit, file and hunk header sections.
- Support for Git's `--color-moved` feature.
- Code can be copied directly from the diff (`-/+` markers are removed by default).
- `n` and `N` keybindings to move between files in large diffs, and between diffs in `log -p` views (`--navigate`)

Expand Down Expand Up @@ -59,6 +60,7 @@ Contents
* [Side-by-side view](#side-by-side-view)
* [Custom features](#custom-features)
* [diff-highlight and diff-so-fancy emulation](#diff-highlight-and-diff-so-fancy-emulation)
* [--color-moved support](#--color-moved-support)
* [Navigation keybindings for large diffs](#navigation-keybindings-for-large-diffs)
* [24 bit color (truecolor)](#24-bit-color-truecolor)
* [Using Delta on Windows](#using-delta-on-windows)
Expand Down Expand Up @@ -356,6 +358,28 @@ You may want to know which delta configuration values the emulation mode has sel

The within-line highlighting rules employed by diff-highlight (and therefore by diff-so-fancy) are deliberately simpler than Delta's Levenshtein-type edit inference algorithm (see discussion in the [diff-highlight README](https://github.com/git/git/tree/master/contrib/diff-highlight)). diff-highlight's rules could be added to delta as an alternative highlighting algorithm, but that hasn't been done yet.

### `--color-moved` support

[_**Unreleased feature**: available now if you build Delta from source, and will be included in the next Delta release. See [#72](https://github.com/dandavison/delta/issues/2)._]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dandavison there's a typo in the URL :) it says "2" when it should say "72".

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!


Recent versions of Git are able to detect moved blocks of code and style them differently from the usual removed/added lines. If you have activated this feature in Git, then Delta will automatically detect such differently-styled lines, and display them unchanged, i.e. with the raw colors it receives from Git.

To activate the Git feature, use

```gitconfig
[diff]
colorMoved = default
```

and see the [Git documentation](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---color-movedltmodegt) for the other possible values and associated color configuration.

In order to support this feature, Delta has to look at the raw colors it receives in a line from Git, and use them to judge whether it is a typical removed/added line, or a specially-colored moved line. This should just work. However, if it causes problems, the behavior can be disabled using

```gitconfig
[delta]
inspect-raw-lines = false
```

### Navigation keybindings for large diffs

Use the `navigate` feature to activate navigation keybindings. In this mode, pressing `n` will jump forward to the next file in the diff, and `N` will jump backwards. If you are viewing multiple commits (e.g. via `git log -p`) then navigation will also visit commit boundaries.
Expand Down
3 changes: 0 additions & 3 deletions src/ansi.rs

This file was deleted.

73 changes: 73 additions & 0 deletions src/ansi/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
pub mod parse;

use std::cmp::min;

use console;
use itertools::Itertools;

pub const ANSI_CSI_CLEAR_TO_EOL: &str = "\x1b[0K";
pub const ANSI_CSI_CLEAR_TO_BOL: &str = "\x1b[1K";
pub const ANSI_SGR_RESET: &str = "\x1b[0m";

pub fn string_starts_with_ansi_escape_sequence(s: &str) -> bool {
console::AnsiCodeIterator::new(s)
.nth(0)
.map(|(_, is_ansi)| is_ansi)
.unwrap_or(false)
}

/// Return string formed from a byte slice starting at byte position `start`, where the index
/// counts bytes in non-ANSI-escape-sequence content only. All ANSI escape sequences in the
/// original string are preserved.
pub fn ansi_preserving_slice(s: &str, start: usize) -> String {
console::AnsiCodeIterator::new(s)
.scan(0, |i, (substring, is_ansi)| {
// i is the index in non-ANSI-escape-sequence content.
let substring_slice = if is_ansi || *i > start {
substring
} else {
&substring[min(substring.len(), start - *i)..]
};
if !is_ansi {
*i += substring.len();
}
Some(substring_slice)
})
.join("")
}

#[cfg(test)]
mod tests {

use crate::ansi::ansi_preserving_slice;
use crate::ansi::string_starts_with_ansi_escape_sequence;

#[test]
fn test_string_starts_with_ansi_escape_sequence() {
assert!(!string_starts_with_ansi_escape_sequence(""));
assert!(!string_starts_with_ansi_escape_sequence("-"));
assert!(string_starts_with_ansi_escape_sequence(
"\x1b[31m-XXX\x1b[m\n"
));
assert!(string_starts_with_ansi_escape_sequence("\x1b[32m+XXX"));
}

#[test]
fn test_ansi_preserving_slice() {
assert_eq!(ansi_preserving_slice("", 0), "");
assert_eq!(ansi_preserving_slice("a", 0), "a");
assert_eq!(ansi_preserving_slice("a", 1), "");
assert_eq!(
ansi_preserving_slice("\x1b[1;35m-2222.2222.2222.2222\x1b[0m", 1),
"\x1b[1;35m2222.2222.2222.2222\x1b[0m"
);
assert_eq!(
ansi_preserving_slice("\x1b[1;35m-2222.2222.2222.2222\x1b[0m", 15),
"\x1b[1;35m.2222\x1b[0m"
);
assert_eq!(
ansi_preserving_slice("\x1b[1;36m-\x1b[m\x1b[1;36m2222·2222·2222·2222\x1b[m\n", 1),
"\x1b[1;36m\x1b[m\x1b[1;36m2222·2222·2222·2222\x1b[m\n"
)
}
}
205 changes: 205 additions & 0 deletions src/ansi/parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
use ansi_term;
use vte;

pub fn parse_first_style(bytes: impl Iterator<Item = u8>) -> Option<ansi_term::Style> {
let mut machine = vte::Parser::new();
let mut performer = Performer { style: None };
for b in bytes {
if performer.style.is_some() {
return performer.style;
}
machine.advance(&mut performer, b)
}
None
}

struct Performer {
style: Option<ansi_term::Style>,
}

// Based on https://github.com/alacritty/vte/blob/0310be12d3007e32be614c5df94653d29fcc1a8b/examples/parselog.rs
impl vte::Perform for Performer {
fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) {
if ignore || intermediates.len() > 1 {
return;
}

match (c, intermediates.get(0)) {
('m', None) => {
if params.is_empty() {
// Attr::Reset;
} else {
self.style = Some(ansi_term_style_from_sgr_parameters(params))
}
}
_ => {}
}
}

fn print(&mut self, _c: char) {}

fn execute(&mut self, _byte: u8) {}

fn hook(&mut self, _params: &[i64], _intermediates: &[u8], _ignore: bool, _c: char) {}

fn put(&mut self, _byte: u8) {}

fn unhook(&mut self) {}

fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {}

fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {}
}

// Based on https://github.com/alacritty/alacritty/blob/57c4ac9145a20fb1ae9a21102503458d3da06c7b/alacritty_terminal/src/ansi.rs#L1168
fn ansi_term_style_from_sgr_parameters(parameters: &[i64]) -> ansi_term::Style {
let mut i = 0;
let mut style = ansi_term::Style::new();
loop {
if i >= parameters.len() {
break;
}

match parameters[i] {
// 0 => Some(Attr::Reset),
1 => style.is_bold = true,
2 => style.is_dimmed = true,
3 => style.is_italic = true,
4 => style.is_underline = true,
// 5 => Some(Attr::BlinkSlow),
// 6 => Some(Attr::BlinkFast),
7 => style.is_reverse = true,
8 => style.is_hidden = true,
9 => style.is_strikethrough = true,
// 21 => Some(Attr::CancelBold),
// 22 => Some(Attr::CancelBoldDim),
// 23 => Some(Attr::CancelItalic),
// 24 => Some(Attr::CancelUnderline),
// 25 => Some(Attr::CancelBlink),
// 27 => Some(Attr::CancelReverse),
// 28 => Some(Attr::CancelHidden),
// 29 => Some(Attr::CancelStrike),
30 => style.foreground = Some(ansi_term::Color::Black),
31 => style.foreground = Some(ansi_term::Color::Red),
32 => style.foreground = Some(ansi_term::Color::Green),
33 => style.foreground = Some(ansi_term::Color::Yellow),
34 => style.foreground = Some(ansi_term::Color::Blue),
35 => style.foreground = Some(ansi_term::Color::Purple),
36 => style.foreground = Some(ansi_term::Color::Cyan),
37 => style.foreground = Some(ansi_term::Color::White),
38 => {
let mut start = 0;
if let Some(color) = parse_sgr_color(&parameters[i..], &mut start) {
i += start;
style.foreground = Some(color);
}
}
// 39 => Some(Attr::Foreground(Color::Named(NamedColor::Foreground))),
40 => style.background = Some(ansi_term::Color::Black),
41 => style.background = Some(ansi_term::Color::Red),
42 => style.background = Some(ansi_term::Color::Green),
43 => style.background = Some(ansi_term::Color::Yellow),
44 => style.background = Some(ansi_term::Color::Blue),
45 => style.background = Some(ansi_term::Color::Purple),
46 => style.background = Some(ansi_term::Color::Cyan),
47 => style.background = Some(ansi_term::Color::White),
48 => {
let mut start = 0;
if let Some(color) = parse_sgr_color(&parameters[i..], &mut start) {
i += start;
style.background = Some(color);
}
}
// 49 => Some(Attr::Background(Color::Named(NamedColor::Background))),
// "bright" colors. ansi_term doesn't offer a way to emit them as, e.g., 90m; instead
// that would be 38;5;8.
90 => style.foreground = Some(ansi_term::Color::Fixed(8)),
91 => style.foreground = Some(ansi_term::Color::Fixed(9)),
92 => style.foreground = Some(ansi_term::Color::Fixed(10)),
93 => style.foreground = Some(ansi_term::Color::Fixed(11)),
94 => style.foreground = Some(ansi_term::Color::Fixed(12)),
95 => style.foreground = Some(ansi_term::Color::Fixed(13)),
96 => style.foreground = Some(ansi_term::Color::Fixed(14)),
97 => style.foreground = Some(ansi_term::Color::Fixed(15)),
100 => style.background = Some(ansi_term::Color::Fixed(8)),
101 => style.background = Some(ansi_term::Color::Fixed(9)),
102 => style.background = Some(ansi_term::Color::Fixed(10)),
103 => style.background = Some(ansi_term::Color::Fixed(11)),
104 => style.background = Some(ansi_term::Color::Fixed(12)),
105 => style.background = Some(ansi_term::Color::Fixed(13)),
106 => style.background = Some(ansi_term::Color::Fixed(14)),
107 => style.background = Some(ansi_term::Color::Fixed(15)),
_ => {}
};
i += 1;
}
style
}

// Based on https://github.com/alacritty/alacritty/blob/57c4ac9145a20fb1ae9a21102503458d3da06c7b/alacritty_terminal/src/ansi.rs#L1258
fn parse_sgr_color(attrs: &[i64], i: &mut usize) -> Option<ansi_term::Color> {
if attrs.len() < 2 {
return None;
}

match attrs[*i + 1] {
2 => {
// RGB color spec.
if attrs.len() < 5 {
// debug!("Expected RGB color spec; got {:?}", attrs);
return None;
}

let r = attrs[*i + 2];
let g = attrs[*i + 3];
let b = attrs[*i + 4];

*i += 4;

let range = 0..256;
if !range.contains(&r) || !range.contains(&g) || !range.contains(&b) {
// debug!("Invalid RGB color spec: ({}, {}, {})", r, g, b);
return None;
}

Some(ansi_term::Color::RGB(r as u8, g as u8, b as u8))
}
5 => {
if attrs.len() < 3 {
// debug!("Expected color index; got {:?}", attrs);
None
} else {
*i += 2;
let idx = attrs[*i];
match idx {
0..=255 => Some(ansi_term::Color::Fixed(idx as u8)),
_ => {
// debug!("Invalid color index: {}", idx);
None
}
}
}
}
_ => {
// debug!("Unexpected color attr: {}", attrs[*i + 1]);
None
}
}
}

#[cfg(test)]
mod tests {

use super::*;

#[test]
fn test_parse_first_style() {
let minus_line_from_unconfigured_git = "\x1b[31m-____\x1b[m\n";
let style = parse_first_style(minus_line_from_unconfigured_git.bytes());
let expected_style = ansi_term::Style {
foreground: Some(ansi_term::Color::Red),
..ansi_term::Style::default()
};
assert_eq!(Some(expected_style), style);
}
}
Loading