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;
13const MIN_PARENT_LINES: u32 = 12;
14const 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
85pub 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}