diff --git a/Cargo.lock b/Cargo.lock index 5e0ac38..c405de7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "anstream" version = "0.5.0" @@ -68,6 +74,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -144,6 +165,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossterm" version = "0.27.0" @@ -170,6 +200,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "dirs" version = "5.0.1" @@ -203,6 +239,22 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -238,6 +290,7 @@ dependencies = [ "paste", "serde", "serde_ignored", + "syntect", "test-case", "toml", "vte", @@ -256,6 +309,12 @@ dependencies = [ "url", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.0" @@ -279,6 +338,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.0.0" @@ -286,7 +355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.0", ] [[package]] @@ -298,6 +367,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + [[package]] name = "jobserver" version = "0.1.24" @@ -337,6 +412,21 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "lock_api" version = "0.4.7" @@ -374,6 +464,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.4" @@ -396,6 +495,34 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -443,6 +570,20 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "plist" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" +dependencies = [ + "base64", + "indexmap 1.9.3", + "line-wrap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -476,6 +617,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.29" @@ -505,6 +655,33 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -540,6 +717,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.3" @@ -612,6 +800,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syntect" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + [[package]] name = "test-case" version = "3.2.1" @@ -667,6 +876,34 @@ dependencies = [ "syn 2.0.23", ] +[[package]] +name = "time" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a79d09ac6b08c1ab3906a2f7cc2e81a0e27c7ae89c63812df75e52bef0751e07" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c65469ed6b3a4809d987a41eb1dc918e9bc1d92211cbad7ae82931846f7451" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -709,7 +946,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ff63e60a958cefbb518ae1fd6566af80d9d4be430a33f3723dfc47d1d411d95" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -788,6 +1025,16 @@ dependencies = [ "quote", ] +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -810,6 +1057,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -933,3 +1189,12 @@ checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index e29f84c..92aa921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ nom = "7.1.3" paste = "1.0.14" serde = { version = "1.0.168", features = [ "derive" ] } serde_ignored = "0.1.9" +syntect = "5.1.0" toml = "0.8.0" vte = "0.11.1" diff --git a/src/parse.rs b/src/parse.rs index 7c14c2a..360b127 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,12 +1,81 @@ -use std::collections::HashMap; +use std::{collections::HashMap, iter}; use anyhow::{Context, Result}; use itertools::Itertools; use nom::{bytes::complete::tag, character::complete::not_line_ending, IResult}; +use syntect::{ + easy::HighlightLines, + highlighting::{Color, FontStyle, Style, Theme, ThemeSet}, + parsing::{SyntaxReference, SyntaxSet}, + util::as_24_bit_terminal_escaped, +}; + +// Diff styles: +const MARKER_NONE: Style = Style { + foreground: Color::WHITE, + background: Color::BLACK, + font_style: FontStyle::empty(), +}; + +const MARKER_ADDED: Style = Style { + foreground: Color { + r: 0x78, + g: 0xde, + b: 0x0c, + a: 0xff, + }, + background: Color::BLACK, + font_style: FontStyle::empty(), +}; +const MARKER_REMOVED: Style = Style { + foreground: Color { + r: 0xd3, + g: 0x2e, + b: 0x09, + a: 0xff, + }, + background: Color::BLACK, + font_style: FontStyle::empty(), +}; + +#[derive(Debug)] +pub struct SyntaxHighlight { + syntax_set: SyntaxSet, + theme: Theme, +} + +impl SyntaxHighlight { + pub fn new() -> Self { + Self { + syntax_set: SyntaxSet::load_defaults_newlines(), + // TODO: theme configuration + theme: ThemeSet::load_defaults().themes["base16-eighties.dark"].clone(), + } + } + + fn get_syntax(&self, path: &str) -> &SyntaxReference { + // TODO: probably better to use std::path? + let file_ext = path.rsplit('.').next().unwrap_or(""); + self.syntax_set + .find_syntax_by_extension(file_ext) + .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()) + } +} + +// TODO: just a workaround so that Status::default() still works +impl Default for SyntaxHighlight { + fn default() -> Self { + Self::new() + } +} /// The returned hashmap associates a filename with a `Vec` of `String` where the strings contain /// the content of each hunk. -pub fn parse_diff(input: &str) -> Result>> { +pub fn parse_diff<'a>( + input: &'a str, + highlight: &SyntaxHighlight, +) -> Result>> { + // HACK: persist this somewhere else let mut diffs = HashMap::new(); for diff in input .lines() @@ -14,7 +83,45 @@ pub fn parse_diff(input: &str) -> Result>> { .split(|l| l.starts_with("diff")) .skip(1) { - diffs.insert(get_path(diff)?, get_hunks(diff)?); + let path = get_path(diff)?; + let syntax = highlight.get_syntax(path); + let mut highlighter = HighlightLines::new(syntax, &highlight.theme); + let hunks = get_hunks(diff)? + .into_iter() + .map(|hunk| { + let s: String = hunk + .lines() + .map(|line| { + let Some(diff_char) = line.chars().next() else { + return line.to_owned(); + }; + + let diff_char_len = diff_char.len_utf8(); + + // add marker and one space + let (marker, diff_line) = match diff_char { + '+' => ((MARKER_ADDED, "+ "), &line[diff_char_len..]), + '-' => ((MARKER_REMOVED, "- "), &line[diff_char_len..]), + _ => ((MARKER_NONE, " "), line), + }; + + let Ok(ranges) = + highlighter.highlight_line(diff_line, &highlight.syntax_set) + else { + // Syntax highighting failed, fallback to no highlighting + // TODO: propagate error? + return diff_line.to_owned(); + }; + + let ranges: Vec<_> = iter::once(marker).chain(ranges).collect(); + as_24_bit_terminal_escaped(&ranges, false) + }) + .join("\n"); + s + }) + .collect(); + + diffs.insert(path, hunks); } Ok(diffs) } @@ -83,6 +190,8 @@ pub fn parse_hunk_new(header: &str) -> Result<&str> { mod tests { use test_case::test_case; + use crate::parse::SyntaxHighlight; + const ISSUE_62: &str = "diff --git a/asteroid-loop/index.html b/asteroid-loop/index.html index d79df71..e2d1e9f 100644 --- a/asteroid-loop/index.html @@ -114,7 +223,8 @@ index d79df71..e2d1e9f 100644 #[test_case(ISSUE_62 ; "issue 62")] fn parse(diff: &str) { - let parsed = super::parse_diff(diff); + let highlight = SyntaxHighlight::new(); + let parsed = super::parse_diff(diff, &highlight); assert!(parsed.is_ok()); let parsed = parsed.unwrap(); assert_eq!(parsed.len(), 1); diff --git a/src/status.rs b/src/status.rs index 90a1844..d045ee2 100644 --- a/src/status.rs +++ b/src/status.rs @@ -16,7 +16,7 @@ use crate::{ config::{Config, Options, CONFIG}, git_process, minibuffer::{MessageType, MiniBuffer}, - parse::{self, parse_hunk_new, parse_hunk_old}, + parse::{self, parse_hunk_new, parse_hunk_old, SyntaxHighlight}, render::{self, Renderer, ResetAttributes, ResetColor}, }; @@ -280,6 +280,7 @@ pub struct Status { pub count_unstaged: usize, pub count_staged: usize, pub cursor: usize, + highlight: SyntaxHighlight, } impl render::Render for Status { @@ -515,13 +516,25 @@ impl Status { // Get the diff information for unstaged changes let diff = git_process(&["diff", "--no-ext-diff"])?; - Self::populate_diffs(&mut unstaged, &self.file_diffs, &diff, options) - .context("failed to populate unstaged file diffs")?; + Self::populate_diffs( + &mut unstaged, + &self.file_diffs, + &diff, + options, + &self.highlight, + ) + .context("failed to populate unstaged file diffs")?; // Get the diff information for staged changes let diff = git_process(&["diff", "--cached", "--no-ext-diff"])?; - Self::populate_diffs(&mut staged, &self.file_diffs, &diff, options) - .context("failed to populate unstaged file diffs")?; + Self::populate_diffs( + &mut staged, + &self.file_diffs, + &diff, + options, + &self.highlight, + ) + .context("failed to populate unstaged file diffs")?; self.branch = branch; self.head = std::str::from_utf8( @@ -559,9 +572,10 @@ impl Status { prev_file_diffs: &[FileDiff], diff: &Output, options: &Options, + highlight: &SyntaxHighlight, ) -> Result<()> { let diff = std::str::from_utf8(&diff.stdout).context("malformed stdout from `git diff`")?; - let hunks = parse::parse_diff(diff)?; + let hunks = parse::parse_diff(diff, highlight)?; for file in file_diffs { if let Some(hunks) = hunks.get(file.path.as_str()) { // Get all the diffs entries of this file from the previous iteration.