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// A core fragment whose kind has no signature variant (chunk, section — the
8// fallbacks for flat data files, unparsed languages and parse degradation) has
9// no cheap stand-in, so when it does not fit the budget the change signal is
10// dropped instead of shrunk (#103). The excerpt is that stand-in: the changed
11// lines plus a little surrounding context, cut out of the parent's own text.
12const CONTEXT_LINES: u32 = 3;
13const MIN_PARENT_LINES: u32 = 12;
14// Above this share of the parent the excerpt saves too little to be worth
15// rendering as a separate fragment — the parent itself is the better answer.
16const MAX_SHARE_OF_PARENT: f64 = 0.7;
17
18fn has_signature_variant(kind: FragmentKind) -> bool {
19    matches!(
20        kind,
21        FragmentKind::Function
22            | FragmentKind::Class
23            | FragmentKind::Struct
24            | FragmentKind::Interface
25            | FragmentKind::Enum
26            | FragmentKind::Variable
27    )
28}
29
30fn hunk_window(parent: &Fragment, hunks: &[DiffHunk]) -> Option<(u32, u32)> {
31    let (mut lo, mut hi) = (u32::MAX, 0u32);
32    for hunk in hunks {
33        if hunk.path.as_ref() != parent.path() {
34            continue;
35        }
36        let (h_start, h_end) = hunk.core_selection_range();
37        if h_start > parent.end_line() || h_end < parent.start_line() {
38            continue;
39        }
40        lo = lo.min(h_start.max(parent.start_line()));
41        hi = hi.max(h_end.min(parent.end_line()));
42    }
43    if lo == u32::MAX { None } else { Some((lo, hi)) }
44}
45
46fn excerpt_from(parent: &Fragment, hunks: &[DiffHunk]) -> Option<Fragment> {
47    if has_signature_variant(parent.kind) || parent.kind.is_stub() {
48        return None;
49    }
50    if parent.line_count() < MIN_PARENT_LINES {
51        return None;
52    }
53    let (hunk_lo, hunk_hi) = hunk_window(parent, hunks)?;
54
55    let start = hunk_lo
56        .saturating_sub(CONTEXT_LINES)
57        .max(parent.start_line());
58    let end = (hunk_hi + CONTEXT_LINES).min(parent.end_line());
59    let span = end - start + 1;
60    if f64::from(span) > f64::from(parent.line_count()) * MAX_SHARE_OF_PARENT {
61        return None;
62    }
63
64    let offset = (start - parent.start_line()) as usize;
65    let lines: Vec<&str> = parent.content.lines().collect();
66    let take = span as usize;
67    if offset >= lines.len() {
68        return None;
69    }
70    let content: String = lines[offset..(offset + take).min(lines.len())].join("\n");
71    if content.trim().is_empty() {
72        return None;
73    }
74
75    Some(Fragment {
76        id: FragmentId::new(parent.id.path.clone(), start, end),
77        kind: FragmentKind::Excerpt,
78        identifiers: extract_identifiers(&content, 3),
79        content: Arc::from(content),
80        token_count: 0,
81        symbol_name: parent.symbol_name.clone(),
82    })
83}
84
85/// Cheap stand-ins for the core fragments that have no signature variant,
86/// keyed by the core they stand in for. Kept out of `all_fragments` on
87/// purpose: they must not become graph nodes or ordinary context candidates,
88/// only substitutes for a core that would otherwise vanish.
89pub fn generate_core_excerpts(
90    all_fragments: &[Fragment],
91    core_ids: &FxHashSet<FragmentId>,
92    hunks: &[DiffHunk],
93) -> FxHashMap<FragmentId, Fragment> {
94    let mut excerpts = FxHashMap::default();
95    for frag in all_fragments {
96        if !core_ids.contains(&frag.id) {
97            continue;
98        }
99        if let Some(excerpt) = excerpt_from(frag, hunks) {
100            excerpts.insert(frag.id.clone(), excerpt);
101        }
102    }
103    excerpts
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    fn chunk(path: &str, start: u32, lines: u32) -> Fragment {
111        let content: String = (start..start + lines)
112            .map(|n| format!("line {n}"))
113            .collect::<Vec<_>>()
114            .join("\n");
115        Fragment {
116            id: FragmentId::new(Arc::from(path), start, start + lines - 1),
117            kind: FragmentKind::Chunk,
118            content: Arc::from(content),
119            identifiers: FxHashSet::default(),
120            token_count: 0,
121            symbol_name: None,
122        }
123    }
124
125    fn hunk(path: &str, start: u32, len: u32) -> DiffHunk {
126        DiffHunk {
127            path: Arc::from(path),
128            new_start: start,
129            new_len: len,
130            old_start: start,
131            old_len: len,
132        }
133    }
134
135    #[test]
136    fn excerpt_covers_the_changed_lines_with_context() {
137        let parent = chunk("data.yml", 1, 200);
138        let excerpt = excerpt_from(&parent, &[hunk("data.yml", 100, 2)]).expect("excerpt");
139
140        assert_eq!(excerpt.kind, FragmentKind::Excerpt);
141        assert_eq!(excerpt.start_line(), 97);
142        assert_eq!(excerpt.end_line(), 104);
143        assert!(excerpt.content.contains("line 100"));
144        assert!(excerpt.content.contains("line 101"));
145        assert!(!excerpt.content.contains("line 150"));
146    }
147
148    #[test]
149    fn excerpt_is_clamped_to_the_parent_span() {
150        let parent = chunk("data.yml", 50, 100);
151        let excerpt = excerpt_from(&parent, &[hunk("data.yml", 51, 1)]).expect("excerpt");
152
153        assert_eq!(excerpt.start_line(), 50);
154        assert!(excerpt.content.starts_with("line 50"));
155    }
156
157    #[test]
158    fn no_excerpt_when_it_would_cover_most_of_the_parent() {
159        let parent = chunk("data.yml", 1, 20);
160        assert!(excerpt_from(&parent, &[hunk("data.yml", 5, 12)]).is_none());
161    }
162
163    #[test]
164    fn no_excerpt_for_kinds_that_already_have_a_signature() {
165        let mut parent = chunk("app.py", 1, 200);
166        parent.kind = FragmentKind::Function;
167        assert!(excerpt_from(&parent, &[hunk("app.py", 100, 2)]).is_none());
168    }
169
170    #[test]
171    fn no_excerpt_when_no_hunk_touches_the_fragment() {
172        let parent = chunk("data.yml", 1, 200);
173        assert!(excerpt_from(&parent, &[hunk("other.yml", 100, 2)]).is_none());
174    }
175
176    #[test]
177    fn only_core_fragments_get_excerpts() {
178        let core = chunk("data.yml", 1, 200);
179        let other = chunk("data.yml", 300, 200);
180        let core_ids: FxHashSet<FragmentId> = [core.id.clone()].into_iter().collect();
181
182        let excerpts = generate_core_excerpts(
183            &[core.clone(), other],
184            &core_ids,
185            &[hunk("data.yml", 100, 2), hunk("data.yml", 350, 2)],
186        );
187
188        assert_eq!(excerpts.len(), 1);
189        assert!(excerpts.contains_key(&core.id));
190    }
191}