git-delta 0.19.2

A syntax-highlighting pager for git
// See https://github.com/BurntSushi/ripgrep
// This module implements handling of `rg --json` output. It is called by the
// handler in handlers/grep.rs. Normal rg output (i.e. without --json) is
// handled by the same code paths as `git grep` etc output, in handlers/grep.rs.
use std::borrow::Cow;

use crate::handlers::grep;
use serde::Deserialize;
use serde_json::Value;

pub fn parse_line(line: &str) -> Option<grep::GrepLine<'_>> {
    let ripgrep_line: Option<RipGrepLine> = serde_json::from_str(line).ok();
    match ripgrep_line {
        Some(ripgrep_line) => {
            // A real line of rg --json output, i.e. either of type "match" or
            // "context".
            let mut code = ripgrep_line.data.lines.text;
            // Keep newlines so the syntax highlighter handles C-style line comments
            // correctly. Also remove \r, see [EndCRLF] in src/delta.rs, but this time
            // it is syntect which adds an ANSI escape sequence in between \r\n later.
            if code.ends_with("\r\n") {
                code.truncate(code.len() - 2);
                code.push('\n');
            }
            Some(grep::GrepLine {
                grep_type: crate::config::GrepType::Ripgrep,
                line_type: ripgrep_line._type,
                line_number: ripgrep_line.data.line_number,
                path: Cow::from(ripgrep_line.data.path.text),
                code: Cow::from(code),
                submatches: Some(
                    ripgrep_line
                        .data
                        .submatches
                        .iter()
                        .map(|m| (m.start, m.end))
                        .collect(),
                ),
            })
        }
        None => {
            let value: Value = serde_json::from_str(line).ok()?;
            match &value["type"] {
                Value::String(s) if s == "begin" || s == "end" || s == "summary" => {
                    Some(grep::GrepLine {
                        // ripgrep --json also emits these metadata lines at
                        // file boundaries. We emit nothing but signal that the
                        // line has been handled.
                        grep_type: crate::config::GrepType::Ripgrep,
                        line_type: grep::LineType::Ignore,
                        line_number: None,
                        path: "".into(),
                        code: "".into(),
                        submatches: None,
                    })
                }
                _ => {
                    // Failed to interpret the line as ripgrep output; allow
                    // another delta handler to try.
                    None
                }
            }
        }
    }
}

//   {
//     "type": "match",
//     "data": {
//       "path": {
//         "text": "src/cli.rs"
//       },
//       "lines": {
//         "text": "    fn from_clap_and_git_config(\n"
//       },
//       "line_number": null,
//       "absolute_offset": 35837,
//       "submatches": [
//         {
//           "match": {
//             "text": "fn"
//           },
//           "start": 4,
//           "end": 6
//         }
//       ]
//     }
//   }

#[derive(Deserialize, PartialEq, Debug)]
struct RipGrepLine {
    #[serde(rename(deserialize = "type"))]
    _type: grep::LineType,
    data: RipGrepLineData,
}

#[derive(Deserialize, PartialEq, Debug)]
struct RipGrepLineData {
    path: RipGrepLineText,
    lines: RipGrepLineText,
    line_number: Option<usize>,
    absolute_offset: usize,
    submatches: Vec<RipGrepLineSubmatch>,
}

#[derive(Deserialize, PartialEq, Debug)]
struct RipGrepLineText {
    text: String,
}

#[derive(Deserialize, PartialEq, Debug)]
struct RipGrepLineSubmatch {
    #[serde(rename(deserialize = "match"))]
    _match: RipGrepLineText,
    start: usize,
    end: usize,
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::tests::integration_test_utils::DeltaTest;
    use insta::assert_snapshot;

    /* FILE test.c:
    // i ABC
    int f() { return 4; }
    const char* i = "ABC";
    double n = 1.23;
     */
    #[test]
    fn test_syntax_in_rg_output_with_context() {
        // `rg int  -C2 --json test.c`
        let data = r#"{"type":"begin","data":{"path":{"text":"test.c"}}}
{"type":"context","data":{"path":{"text":"test.c"},"lines":{"text":"// i ABC\n"},"line_number":1,"absolute_offset":0,"submatches":[]}}
{"type":"match","data":{"path":{"text":"test.c"},"lines":{"text":"int f() { return 4; }\n"},"line_number":2,"absolute_offset":9,"submatches":[{"match":{"text":"int"},"start":0,"end":3}]}}
{"type":"context","data":{"path":{"text":"test.c"},"lines":{"text":"const char* i = \"ABC\";\n"},"line_number":3,"absolute_offset":31,"submatches":[]}}
{"type":"context","data":{"path":{"text":"test.c"},"lines":{"text":"double n = 1.23;\n"},"line_number":4,"absolute_offset":54,"submatches":[]}}
{"type":"end","data":{"path":{"text":"test.c"},"binary_offset":null,"stats":{"elapsed":{"secs":0,"nanos":26941,"human":"0.000027s"},"searches":1,"searches_with_match":1,"bytes_searched":71,"bytes_printed":670,"matched_lines":1,"matches":1}}}
{"data":{"elapsed_total":{"human":"0.000479s","nanos":478729,"secs":0},"stats":{"bytes_printed":670,"bytes_searched":71,"elapsed":{"human":"0.000027s","nanos":26941,"secs":0},"matched_lines":1,"matches":1,"searches":1,"searches_with_match":1}},"type":"summary"}"#;
        let result = DeltaTest::with_args(&[]).explain_ansi().with_input(data);
        // eprintln!("{}", result.raw_output);
        assert_snapshot!(result.output, @r#"
        (purple)test.c(normal) 
        (green)1(normal)-(242)// i ABC(normal)
        (green)2(normal):(81 28)int(231) (149)f(231)() { (203)return(231) (141)4(231); }(normal)
        (green)3(normal)-(203)const(231) (81)char(203)*(231) i (203)=(231) (186)"ABC"(231);(normal)
        (green)4(normal)-(81)double(231) n (203)=(231) (141)1.23(231);(normal)
        "#);
    }

    #[test]
    fn test_syntax_in_rg_output_no_context() {
        // `rg i  --json test.c`
        let data = r#"{"type":"begin","data":{"path":{"text":"test.c"}}}
{"type":"match","data":{"path":{"text":"test.c"},"lines":{"text":"// i ABC\n"},"line_number":1,"absolute_offset":0,"submatches":[{"match":{"text":"i"},"start":3,"end":4}]}}
{"type":"match","data":{"path":{"text":"test.c"},"lines":{"text":"int f() { return 4; }\n"},"line_number":2,"absolute_offset":9,"submatches":[{"match":{"text":"i"},"start":0,"end":1}]}}
{"type":"match","data":{"path":{"text":"test.c"},"lines":{"text":"const char* i = \"ABC\";\n"},"line_number":3,"absolute_offset":31,"submatches":[{"match":{"text":"i"},"start":12,"end":13}]}}
{"type":"end","data":{"path":{"text":"test.c"},"binary_offset":null,"stats":{"elapsed":{"secs":0,"nanos":23885,"human":"0.000024s"},"searches":1,"searches_with_match":1,"bytes_searched":71,"bytes_printed":602,"matched_lines":3,"matches":3}}}
{"data":{"elapsed_total":{"human":"0.000433s","nanos":432974,"secs":0},"stats":{"bytes_printed":602,"bytes_searched":71,"elapsed":{"human":"0.000024s","nanos":23885,"secs":0},"matched_lines":3,"matches":3,"searches":1,"searches_with_match":1}},"type":"summary"}
"#;
        let result = DeltaTest::with_args(&[]).explain_ansi().with_input(data);
        // eprintln!("{}", result.raw_output);
        assert_snapshot!(result.output, @r#"
        (purple)test.c(normal) 
        (green)1(normal):(242)// (normal 28)i(242) ABC(normal)
        (green)2(normal):(81 28)i(81)nt(231) (149)f(231)() { (203)return(231) (141)4(231); }(normal)
        (green)3(normal):(203)const(231) (81)char(203)*(231) (normal 28)i(231) (203)=(231) (186)"ABC"(231);(normal)
        "#);
    }

    #[test]
    fn test_deserialize() {
        let line = r#"{"type":"match","data":{"path":{"text":"src/cli.rs"},"lines":{"text":"    fn from_clap_and_git_config(\n"},"line_number":null,"absolute_offset":35837,"submatches":[{"match":{"text":"fn"},"start":4,"end":6}]}}"#;
        let ripgrep_line: RipGrepLine = serde_json::from_str(line).unwrap();
        assert_eq!(
            ripgrep_line,
            RipGrepLine {
                _type: grep::LineType::Match,
                data: RipGrepLineData {
                    path: RipGrepLineText {
                        text: "src/cli.rs".into()
                    },
                    lines: RipGrepLineText {
                        text: "    fn from_clap_and_git_config(\n".into(),
                    },
                    line_number: None,
                    absolute_offset: 35837,
                    submatches: vec![RipGrepLineSubmatch {
                        _match: RipGrepLineText { text: "fn".into() },
                        start: 4,
                        end: 6
                    }]
                }
            }
        )
    }

    #[test]
    fn test_deserialize_2() {
        let line = r#"{"type":"match","data":{"path":{"text":"src/handlers/submodule.rs"},"lines":{"text":"                        .paint(minus_commit.chars().take(7).collect::<String>()),\n"},"line_number":41,"absolute_offset":1430,"submatches":[{"match":{"text":"("},"start":30,"end":31},{"match":{"text":"("},"start":49,"end":50},{"match":{"text":")"},"start":50,"end":51},{"match":{"text":"("},"start":56,"end":57},{"match":{"text":")"},"start":58,"end":59},{"match":{"text":"("},"start":77,"end":78},{"match":{"text":")"},"start":78,"end":79},{"match":{"text":")"},"start":79,"end":80}]}}"#;
        let ripgrep_line: RipGrepLine = serde_json::from_str(line).unwrap();
        assert_eq!(
            ripgrep_line,
            RipGrepLine {
                _type: grep::LineType::Match,
                data: RipGrepLineData {
                    path: RipGrepLineText {
                        text: "src/handlers/submodule.rs".into()
                    },
                    lines: RipGrepLineText {
                        text: "                        .paint(minus_commit.chars().take(7).collect::<String>()),\n".into(),
                    },
                    line_number: Some(41),
                    absolute_offset: 1430,
                    submatches: vec![
                        RipGrepLineSubmatch {
                            _match: RipGrepLineText { text: "(".into() },
                            start: 30,
                            end: 31
                        },
                        RipGrepLineSubmatch {
                            _match: RipGrepLineText { text: "(".into() },
                            start: 49,
                            end: 50
                        },
                        RipGrepLineSubmatch {
                            _match: RipGrepLineText { text: ")".into() },
                            start: 50,
                            end: 51
                        },
                        RipGrepLineSubmatch {
                            _match: RipGrepLineText { text: "(".into() },
                            start: 56,
                            end: 57
                        },
                        RipGrepLineSubmatch {
                            _match: RipGrepLineText { text: ")".into() },
                            start: 58,
                            end: 59
                        },
                        RipGrepLineSubmatch {
                            _match: RipGrepLineText { text: "(".into() },
                            start: 77,
                            end: 78
                        },
                        RipGrepLineSubmatch {
                            _match: RipGrepLineText { text: ")".into() },
                            start: 78,
                            end: 79
                        },
                        RipGrepLineSubmatch {
                            _match: RipGrepLineText { text: ")".into() },
                            start: 79,
                            end: 80
                        },
                    ]
                }
            }
        )
    }
}