1use rustc_hash::{FxHashMap, FxHashSet};
2
3use crate::types::{DiffHunk, Fragment, FragmentId, FragmentKind};
4
5fn kind_priority(kind: FragmentKind) -> u8 {
6 if kind.is_semantic() { 0 } else { 1 }
7}
8
9fn find_core_for_hunk(frags: &[&Fragment], h_start: u32, h_end: u32) -> FxHashSet<FragmentId> {
10 let mut core = FxHashSet::default();
11
12 let covering: Vec<&Fragment> = frags
13 .iter()
14 .copied()
15 .filter(|f| f.start_line() <= h_start && h_end <= f.end_line())
16 .collect();
17 if !covering.is_empty() {
23 let best = covering
24 .iter()
25 .min_by(|a, b| {
26 let ka = kind_priority(a.kind);
27 let kb = kind_priority(b.kind);
28 ka.cmp(&kb)
29 .then(a.line_count().cmp(&b.line_count()))
30 .then_with(|| a.id.cmp(&b.id))
31 })
32 .unwrap();
33 core.insert(best.id.clone());
34 return core;
35 }
36
37 let overlapping: Vec<&Fragment> = frags
38 .iter()
39 .copied()
40 .filter(|f| f.start_line() <= h_end && f.end_line() >= h_start)
41 .collect();
42 if !overlapping.is_empty() {
43 for f in &overlapping {
44 core.insert(f.id.clone());
45 }
46 return core;
47 }
48
49 let before: Vec<&Fragment> = frags
50 .iter()
51 .copied()
52 .filter(|f| f.end_line() < h_start)
53 .collect();
54 let after: Vec<&Fragment> = frags
55 .iter()
56 .copied()
57 .filter(|f| f.start_line() > h_end)
58 .collect();
59 if let Some(nearest_before) = before.iter().max_by(|a, b| {
60 a.end_line()
61 .cmp(&b.end_line())
62 .then_with(|| a.id.cmp(&b.id))
63 }) {
64 core.insert(nearest_before.id.clone());
65 }
66 if let Some(nearest_after) = after.iter().min_by(|a, b| {
67 a.start_line()
68 .cmp(&b.start_line())
69 .then_with(|| a.id.cmp(&b.id))
70 }) {
71 core.insert(nearest_after.id.clone());
72 }
73
74 core
75}
76
77fn add_container_headers(
78 core_ids: &mut FxHashSet<FragmentId>,
79 frags_by_path: &FxHashMap<&str, Vec<&Fragment>>,
80) {
81 let core_paths: FxHashSet<&str> = core_ids.iter().map(|fid| fid.path.as_ref()).collect();
82 let mut headers_to_add = Vec::new();
83
84 for &path in &core_paths {
85 if let Some(frags) = frags_by_path.get(path) {
86 for frag in frags {
87 if !frag.kind.is_container() || core_ids.contains(&frag.id) {
88 continue;
89 }
90 let contains_core = core_ids.iter().any(|core_id| {
91 core_id.path.as_ref() == path
92 && frag.start_line() <= core_id.start_line
93 && core_id.end_line <= frag.end_line()
94 });
95 if contains_core {
96 headers_to_add.push(frag.id.clone());
97 }
98 }
99 }
100 }
101
102 for h in headers_to_add {
103 core_ids.insert(h);
104 }
105}
106
107pub fn identify_core_fragments(
108 hunks: &[DiffHunk],
109 all_fragments: &[Fragment],
110) -> FxHashSet<FragmentId> {
111 let mut frags_by_path: FxHashMap<&str, Vec<&Fragment>> = FxHashMap::default();
112 for frag in all_fragments {
113 frags_by_path.entry(frag.path()).or_default().push(frag);
114 }
115
116 let mut core_ids = FxHashSet::default();
117 for h in hunks {
118 if let Some(frags) = frags_by_path.get(h.path.as_ref()) {
119 let (h_start, h_end) = h.core_selection_range();
120 core_ids.extend(find_core_for_hunk(frags, h_start, h_end));
121 }
122 }
123
124 add_container_headers(&mut core_ids, &frags_by_path);
125 core_ids
126}
127
128fn map_hunks_to_fragments(
129 hunks: &[DiffHunk],
130 core_ids: &FxHashSet<FragmentId>,
131 all_fragments: &[Fragment],
132) -> FxHashMap<FragmentId, f64> {
133 let mut result: FxHashMap<FragmentId, f64> = FxHashMap::default();
134 for h in hunks {
135 let (h_start, h_end) = h.core_selection_range();
136 let hunk_size = (h_end as i64 - h_start as i64 + 1).max(1) as f64;
137 for frag in all_fragments {
138 if !core_ids.contains(&frag.id) || frag.path() != h.path.as_ref() {
139 continue;
140 }
141 if frag.start_line() <= h_end && frag.end_line() >= h_start {
142 *result.entry(frag.id.clone()).or_insert(0.0) += hunk_size;
143 }
144 }
145 }
146 result
147}
148
149fn add_container_weights(
150 frag_hunk_lines: &mut FxHashMap<FragmentId, f64>,
151 core_ids: &FxHashSet<FragmentId>,
152 all_fragments: &[Fragment],
153) {
154 let mut to_add = Vec::new();
155 for frag in all_fragments {
156 if !core_ids.contains(&frag.id) || frag_hunk_lines.contains_key(&frag.id) {
157 continue;
158 }
159 if !frag.kind.is_container() {
160 continue;
161 }
162 let contained_weight: f64 = frag_hunk_lines
163 .iter()
164 .filter(|(fid, _)| {
165 fid.path.as_ref() == frag.path()
166 && frag.start_line() <= fid.start_line
167 && fid.end_line <= frag.end_line()
168 })
169 .map(|(_, w)| *w)
170 .sum();
171 if contained_weight > 0.0 {
172 to_add.push((frag.id.clone(), contained_weight));
173 }
174 }
175 for (id, w) in to_add {
176 frag_hunk_lines.insert(id, w);
177 }
178}
179
180fn best_hunk_size_for_path(hunks: &[DiffHunk], path: &str) -> u32 {
181 let mut best = 0u32;
182 for h in hunks {
183 if h.path.as_ref() == path {
184 let (h_start, h_end) = h.core_selection_range();
185 let size = h_end.saturating_sub(h_start) + 1;
186 best = best.max(size);
187 }
188 }
189 best
190}
191
192fn fill_missing_core_weights(
193 frag_hunk_lines: &mut FxHashMap<FragmentId, f64>,
194 core_ids: &FxHashSet<FragmentId>,
195 hunks: &[DiffHunk],
196) {
197 let missing: Vec<FragmentId> = core_ids
198 .iter()
199 .filter(|fid| !frag_hunk_lines.contains_key(*fid))
200 .cloned()
201 .collect();
202 for fid in missing {
203 let best = best_hunk_size_for_path(hunks, fid.path.as_ref());
204 if best > 0 {
205 frag_hunk_lines.insert(fid, best as f64);
206 }
207 }
208}
209
210pub fn compute_seed_weights(
211 hunks: &[DiffHunk],
212 core_ids: &FxHashSet<FragmentId>,
213 all_fragments: &[Fragment],
214) -> FxHashMap<FragmentId, f64> {
215 let mut frag_hunk_lines = map_hunks_to_fragments(hunks, core_ids, all_fragments);
216 if frag_hunk_lines.is_empty() {
217 return FxHashMap::default();
218 }
219
220 add_container_weights(&mut frag_hunk_lines, core_ids, all_fragments);
221 fill_missing_core_weights(&mut frag_hunk_lines, core_ids, hunks);
222
223 frag_hunk_lines
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use std::sync::Arc;
230
231 fn frag(path: &str, start: u32, end: u32, kind: FragmentKind) -> Fragment {
232 Fragment {
233 id: FragmentId::new(Arc::from(path), start, end),
234 kind,
235 content: Arc::from(""),
236 identifiers: FxHashSet::default(),
237 token_count: 10,
238 symbol_name: None,
239 }
240 }
241
242 fn hunk(path: &str, start: u32, len: u32) -> DiffHunk {
243 DiffHunk {
244 path: Arc::from(path),
245 new_start: start,
246 new_len: len,
247 old_start: start,
248 old_len: len,
249 }
250 }
251
252 fn sorted_ids(core: &FxHashSet<FragmentId>) -> Vec<FragmentId> {
253 let mut v: Vec<FragmentId> = core.iter().cloned().collect();
254 v.sort();
255 v
256 }
257
258 #[test]
265 fn core_identification_is_invariant_under_fragment_order() {
266 let fragments = vec![
269 frag("a.rs", 10, 20, FragmentKind::Function),
270 frag("a.rs", 15, 25, FragmentKind::Function),
271 frag("a.rs", 60, 70, FragmentKind::Function),
272 frag("a.rs", 90, 100, FragmentKind::Function),
273 frag("b.rs", 1, 11, FragmentKind::Function),
274 frag("b.rs", 30, 40, FragmentKind::Function),
275 ];
276 let hunks = vec![
277 hunk("a.rs", 16, 3),
278 hunk("a.rs", 80, 1),
279 hunk("b.rs", 20, 1),
280 ];
281
282 let baseline = sorted_ids(&identify_core_fragments(&hunks, &fragments));
283 assert!(!baseline.is_empty(), "no core identified at all");
284
285 for permuted in [
286 {
287 let mut v = fragments.clone();
288 v.reverse();
289 v
290 },
291 {
292 let mut v = fragments.clone();
293 v.rotate_left(3);
294 v
295 },
296 {
297 let mut v = fragments.clone();
298 v.sort_by_key(|f| std::cmp::Reverse(f.start_line()));
299 v
300 },
301 ] {
302 assert_eq!(
303 sorted_ids(&identify_core_fragments(&hunks, &permuted)),
304 baseline,
305 "core set changed with fragment order"
306 );
307 }
308 }
309}