Skip to main content

_diffctx/
excerpt.rs

1use std::sync::Arc;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4
5use crate::types::{DiffHunk, Fragment, FragmentId, FragmentKind, extract_identifiers};
6
7// The changed lines plus a little surrounding context, cut out of a core
8// fragment's own text.
9//
10// Originally a budget fallback for the kinds with no signature variant (chunk,
11// section — flat data files, unparsed languages, parse degradation), where an
12// oversized core would otherwise be dropped and the change signal lost (#103).
13// It is now also the *downshift* target for a core that is mostly unchanged:
14// emitting a 264-line function for a two-line edit is the over-dump behind #105,
15// #107 and #149, and the signature variant is not a substitute there because it
16// drops the changed lines entirely.
17const CONTEXT_LINES: u32 = 3;
18const MIN_PARENT_LINES: u32 = 12;
19// A core is downshifted to its excerpt when the excerpt covers no more than
20// this share of it — i.e. when most of what the parent would contribute is
21// unchanged context. Deliberately stricter than `MAX_SHARE_OF_PARENT`: that
22// one only decides whether an excerpt is worth cutting at all, this one
23// decides whether to prefer it over the real fragment.
24const DOWNSHIFT_MAX_SHARE: f64 = 0.5;
25// Above this share of the parent the excerpt saves too little to be worth
26// rendering as a separate fragment — the parent itself is the better answer.
27const MAX_SHARE_OF_PARENT: f64 = 0.7;
28
29fn hunk_window(parent: &Fragment, hunks: &[DiffHunk]) -> Option<(u32, u32)> {
30    let (mut lo, mut hi) = (u32::MAX, 0u32);
31    for hunk in hunks {
32        if hunk.path.as_ref() != parent.path() {
33            continue;
34        }
35        let (h_start, h_end) = hunk.core_selection_range();
36        if h_start > parent.end_line() || h_end < parent.start_line() {
37            continue;
38        }
39        lo = lo.min(h_start.max(parent.start_line()));
40        hi = hi.max(h_end.min(parent.end_line()));
41    }
42    if lo == u32::MAX { None } else { Some((lo, hi)) }
43}
44
45fn excerpt_from(parent: &Fragment, hunks: &[DiffHunk]) -> Option<Fragment> {
46    if parent.kind.is_stub() {
47        return None;
48    }
49    if parent.line_count() < MIN_PARENT_LINES {
50        return None;
51    }
52    let (hunk_lo, hunk_hi) = hunk_window(parent, hunks)?;
53
54    let start = hunk_lo
55        .saturating_sub(CONTEXT_LINES)
56        .max(parent.start_line());
57    let end = (hunk_hi + CONTEXT_LINES).min(parent.end_line());
58    let span = end - start + 1;
59    if f64::from(span) > f64::from(parent.line_count()) * MAX_SHARE_OF_PARENT {
60        return None;
61    }
62
63    let offset = (start - parent.start_line()) as usize;
64    let lines: Vec<&str> = parent.content.lines().collect();
65    let take = span as usize;
66    if offset >= lines.len() {
67        return None;
68    }
69    let content: String = lines[offset..(offset + take).min(lines.len())].join("\n");
70    if content.trim().is_empty() {
71        return None;
72    }
73
74    Some(Fragment {
75        id: FragmentId::new(parent.id.path.clone(), start, end),
76        kind: FragmentKind::Excerpt,
77        identifiers: extract_identifiers(&content, 3),
78        content: Arc::from(content),
79        token_count: 0,
80        symbol_name: parent.symbol_name.clone(),
81    })
82}
83
84/// Whether a core should be rendered as its excerpt rather than in full.
85///
86/// True when the hunk window covers only a small share of the parent, which is
87/// exactly the over-dump shape: the parent's remaining lines are unchanged
88/// context that the reader did not ask for. A change spread across most of the
89/// fragment keeps the fragment.
90pub fn is_downshift_worthwhile(parent: &Fragment, excerpt: &Fragment) -> bool {
91    f64::from(excerpt.line_count()) <= f64::from(parent.line_count()) * DOWNSHIFT_MAX_SHARE
92}
93
94/// Hunk-window stand-ins for core fragments, keyed by the core each replaces.
95/// Kept out of `all_fragments` on purpose: they must not become graph nodes or
96/// ordinary context candidates, only substitutes for their own core.
97pub fn generate_core_excerpts(
98    all_fragments: &[Fragment],
99    core_ids: &FxHashSet<FragmentId>,
100    hunks: &[DiffHunk],
101) -> FxHashMap<FragmentId, Fragment> {
102    let mut excerpts = FxHashMap::default();
103    for frag in all_fragments {
104        if !core_ids.contains(&frag.id) {
105            continue;
106        }
107        if let Some(excerpt) = excerpt_from(frag, hunks) {
108            excerpts.insert(frag.id.clone(), excerpt);
109        }
110    }
111    excerpts
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn chunk(path: &str, start: u32, lines: u32) -> Fragment {
119        let content: String = (start..start + lines)
120            .map(|n| format!("line {n}"))
121            .collect::<Vec<_>>()
122            .join("\n");
123        Fragment {
124            id: FragmentId::new(Arc::from(path), start, start + lines - 1),
125            kind: FragmentKind::Chunk,
126            content: Arc::from(content),
127            identifiers: FxHashSet::default(),
128            token_count: 0,
129            symbol_name: None,
130        }
131    }
132
133    fn hunk(path: &str, start: u32, len: u32) -> DiffHunk {
134        DiffHunk {
135            path: Arc::from(path),
136            new_start: start,
137            new_len: len,
138            old_start: start,
139            old_len: len,
140        }
141    }
142
143    #[test]
144    fn excerpt_covers_the_changed_lines_with_context() {
145        let parent = chunk("data.yml", 1, 200);
146        let excerpt = excerpt_from(&parent, &[hunk("data.yml", 100, 2)]).expect("excerpt");
147
148        assert_eq!(excerpt.kind, FragmentKind::Excerpt);
149        assert_eq!(excerpt.start_line(), 97);
150        assert_eq!(excerpt.end_line(), 104);
151        assert!(excerpt.content.contains("line 100"));
152        assert!(excerpt.content.contains("line 101"));
153        assert!(!excerpt.content.contains("line 150"));
154    }
155
156    #[test]
157    fn excerpt_is_clamped_to_the_parent_span() {
158        let parent = chunk("data.yml", 50, 100);
159        let excerpt = excerpt_from(&parent, &[hunk("data.yml", 51, 1)]).expect("excerpt");
160
161        assert_eq!(excerpt.start_line(), 50);
162        assert!(excerpt.content.starts_with("line 50"));
163    }
164
165    #[test]
166    fn no_excerpt_when_it_would_cover_most_of_the_parent() {
167        let parent = chunk("data.yml", 1, 20);
168        assert!(excerpt_from(&parent, &[hunk("data.yml", 5, 12)]).is_none());
169    }
170
171    /// Kinds WITH a signature variant get an excerpt too. The signature is not
172    /// a substitute for a mostly-unchanged core: it drops the changed lines
173    /// entirely, which is the whole point of emitting the core in the first
174    /// place. Excluding them was what made a two-line edit ship a 200-line
175    /// function (#105/#107/#149).
176    #[test]
177    fn kinds_with_a_signature_variant_still_get_an_excerpt() {
178        let mut parent = chunk("app.py", 1, 200);
179        parent.kind = FragmentKind::Function;
180
181        let excerpt = excerpt_from(&parent, &[hunk("app.py", 100, 2)]).expect("excerpt");
182        assert_eq!(excerpt.kind, FragmentKind::Excerpt);
183        assert!(excerpt.content.contains("line 100"));
184        assert!(excerpt.content.contains("line 101"));
185        assert!(crate::excerpt::is_downshift_worthwhile(&parent, &excerpt));
186    }
187
188    /// A change spread across most of the fragment keeps the fragment: there is
189    /// no unchanged bulk to trim, and a window would just lose context.
190    #[test]
191    fn a_widely_spread_change_is_not_downshifted() {
192        let parent = chunk("app.py", 1, 40);
193        let excerpt = excerpt_from(&parent, &[hunk("app.py", 5, 20)]).expect("excerpt");
194        assert!(!crate::excerpt::is_downshift_worthwhile(&parent, &excerpt));
195    }
196
197    #[test]
198    fn no_excerpt_when_no_hunk_touches_the_fragment() {
199        let parent = chunk("data.yml", 1, 200);
200        assert!(excerpt_from(&parent, &[hunk("other.yml", 100, 2)]).is_none());
201    }
202
203    #[test]
204    fn only_core_fragments_get_excerpts() {
205        let core = chunk("data.yml", 1, 200);
206        let other = chunk("data.yml", 300, 200);
207        let core_ids: FxHashSet<FragmentId> = [core.id.clone()].into_iter().collect();
208
209        let excerpts = generate_core_excerpts(
210            &[core.clone(), other],
211            &core_ids,
212            &[hunk("data.yml", 100, 2), hunk("data.yml", 350, 2)],
213        );
214
215        assert_eq!(excerpts.len(), 1);
216        assert!(excerpts.contains_key(&core.id));
217    }
218}