Skip to main content

sley_diff_format/
funcname.rs

1use sley_core::{GitError, Result};
2use sley_grep::{Regex, RegexMode};
3
4/// A compiled funcname spec: newline-separated POSIX regexes tried in order;
5/// a leading `!` negates (a match rejects the line). Port of
6/// `xdiff_set_find_func` + `ff_regexp` from upstream `xdiff-interface.c`.
7pub struct CompiledFuncname {
8    patterns: Vec<(bool, Regex)>,
9}
10
11/// The upstream hunk-header buffer is `char buf[80]` (`struct func_line`);
12/// headings are truncated to it before the trailing-whitespace trim.
13const FUNCNAME_BUFFER: usize = 80;
14
15impl CompiledFuncname {
16    /// Compile a funcname spec. `extended` selects ERE (`xfuncname` /
17    /// builtins) over BRE (`funcname` config). Errors mirror upstream's
18    /// `die()` calls byte-for-byte (printed to stderr, exit 128).
19    pub fn compile(spec: &[u8], extended: bool, icase: bool) -> Result<Self> {
20        let lines: Vec<&[u8]> = spec.split(|&b| b == b'\n').collect();
21        let mut patterns = Vec::new();
22        for (idx, line) in lines.iter().enumerate() {
23            let negate = line.first() == Some(&b'!');
24            if negate && idx == lines.len() - 1 {
25                // die("Last expression must not be negated: %s", value) —
26                // `value` is the remaining suffix of the spec at that point.
27                let suffix: Vec<u8> = lines[idx..].join(&b'\n');
28                eprintln!(
29                    "fatal: Last expression must not be negated: {}",
30                    String::from_utf8_lossy(&suffix)
31                );
32                return Err(GitError::Exit(128));
33            }
34            let expression = if negate { &line[1..] } else { line };
35            let mode = if extended {
36                RegexMode::Ere
37            } else {
38                RegexMode::Bre
39            };
40            let regex = Regex::compile_bytes(expression, mode, icase, false).map_err(|_| {
41                eprintln!(
42                    "fatal: Invalid regexp to look for hunk header: {}",
43                    String::from_utf8_lossy(expression)
44                );
45                GitError::Exit(128)
46            })?;
47            patterns.push((negate, regex));
48        }
49        Ok(Self { patterns })
50    }
51
52    /// Port of `ff_regexp`: match `line` (raw record bytes, trailing newline
53    /// still attached) against the pattern list; return the heading bytes, or
54    /// `None` when no pattern accepts the line.
55    pub fn match_line(&self, line: &[u8]) -> Option<Vec<u8>> {
56        // Exclude terminating newline (and cr) from matching.
57        let mut len = line.len();
58        if len > 0 && line[len - 1] == b'\n' {
59            if len > 1 && line[len - 2] == b'\r' {
60                len -= 2;
61            } else {
62                len -= 1;
63            }
64        }
65        let line = &line[..len];
66        let mut matched: Option<Vec<Option<(usize, usize)>>> = None;
67        for (negate, regex) in &self.patterns {
68            if let Some(captures) = regex.find_captures(line) {
69                if *negate {
70                    return None;
71                }
72                matched = Some(captures);
73                break;
74            }
75        }
76        let captures = matched?;
77        let (start, end) = captures
78            .get(1)
79            .copied()
80            .flatten()
81            .unwrap_or_else(|| captures[0].expect("whole-match span present"));
82        let heading = &line[start..end];
83        let mut result = heading.len().min(FUNCNAME_BUFFER);
84        while result > 0 && heading[result - 1].is_ascii_whitespace() {
85            result -= 1;
86        }
87        Some(heading[..result].to_vec())
88    }
89}
90
91/// Port of `def_ff` (the default funcname heuristic): a line whose first byte
92/// is a letter, `_`, or `$` is a section heading, truncated to the 80-byte
93/// header buffer with trailing whitespace trimmed.
94pub fn default_funcname_heading(line: &[u8]) -> Option<Vec<u8>> {
95    let first = line.first()?;
96    if !(first.is_ascii_alphabetic() || *first == b'_' || *first == b'$') {
97        return None;
98    }
99    let mut len = line.len().min(FUNCNAME_BUFFER);
100    while len > 0 && line[len - 1].is_ascii_whitespace() {
101        len -= 1;
102    }
103    Some(line[..len].to_vec())
104}