1use std::sync::Arc;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4
5use crate::types::{DiffHunk, Fragment, FragmentId, FragmentKind, extract_identifiers};
6
7const CONTEXT_LINES: u32 = 3;
18const MIN_PARENT_LINES: u32 = 12;
19const DOWNSHIFT_MAX_SHARE: f64 = 0.5;
25const 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
84pub 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
94pub 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 #[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 #[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}