1use crate::ast::outline::OutlineEntry;
2use crate::error::{ChronicleError, Result};
3use crate::git::GitOps;
4use crate::read::{self, MatchedRegion, ReadQuery};
5use crate::schema::annotation::{ContextLevel, Provenance, RegionAnnotation};
6
7#[derive(Debug, Clone)]
9pub struct RegionRef {
10 pub region: RegionAnnotation,
11 pub commit: String,
12 pub timestamp: String,
13 pub summary: String,
14 pub context_level: ContextLevel,
15 pub provenance: Provenance,
16}
17
18#[derive(Debug)]
20pub struct LineAnnotationMap {
21 coverage: Vec<Vec<usize>>,
23}
24
25impl LineAnnotationMap {
26 pub fn build_from_regions(regions: &[RegionRef], total_lines: usize) -> Self {
28 Self::build(regions, total_lines)
29 }
30
31 fn build(regions: &[RegionRef], total_lines: usize) -> Self {
32 let mut coverage = vec![Vec::new(); total_lines];
33 for (idx, r) in regions.iter().enumerate() {
34 let start = r.region.lines.start.saturating_sub(1) as usize;
35 let end = (r.region.lines.end as usize).min(total_lines);
36 for slot in &mut coverage[start..end] {
37 slot.push(idx);
38 }
39 }
40 Self { coverage }
41 }
42
43 pub fn regions_at_line(&self, line: u32) -> &[usize] {
45 let idx = line.saturating_sub(1) as usize;
46 self.coverage.get(idx).map(|v| v.as_slice()).unwrap_or(&[])
47 }
48
49 pub fn next_annotated_line(&self, from: u32) -> Option<u32> {
52 let start = from.saturating_sub(1) as usize;
53 for (i, regions) in self.coverage[start..].iter().enumerate() {
54 if !regions.is_empty() {
55 return Some((start + i) as u32 + 1);
56 }
57 }
58 None
59 }
60
61 pub fn prev_annotated_line(&self, from: u32) -> Option<u32> {
63 let end = (from as usize).min(self.coverage.len());
64 for i in (0..end).rev() {
65 if !self.coverage[i].is_empty() {
66 return Some(i as u32 + 1);
67 }
68 }
69 None
70 }
71}
72
73#[derive(Debug)]
75pub struct ShowData {
76 pub file_path: String,
77 pub commit: String,
78 pub source_lines: Vec<String>,
79 pub outline: Vec<OutlineEntry>,
80 pub regions: Vec<RegionRef>,
81 pub annotation_map: LineAnnotationMap,
82}
83
84pub fn build_show_data(
86 git_ops: &dyn GitOps,
87 file_path: &str,
88 commit: &str,
89 anchor: Option<&str>,
90) -> Result<ShowData> {
91 let source = git_ops
93 .file_at_commit(std::path::Path::new(file_path), commit)
94 .map_err(|e| ChronicleError::Git {
95 source: e,
96 location: snafu::Location::default(),
97 })?;
98
99 let source_lines: Vec<String> = source.lines().map(String::from).collect();
100 let total_lines = source_lines.len();
101
102 let lang = crate::ast::Language::from_path(file_path);
104 let outline = crate::ast::extract_outline(&source, lang).unwrap_or_default();
105
106 let query = ReadQuery {
108 file: file_path.to_string(),
109 anchor: anchor.map(String::from),
110 lines: None,
111 };
112 let read_result = read::execute(git_ops, &query)?;
113
114 let regions = dedup_regions(read_result.regions);
117
118 let annotation_map = LineAnnotationMap::build(®ions, total_lines);
119
120 Ok(ShowData {
121 file_path: file_path.to_string(),
122 commit: commit.to_string(),
123 source_lines,
124 outline,
125 regions,
126 annotation_map,
127 })
128}
129
130fn dedup_regions(matched: Vec<MatchedRegion>) -> Vec<RegionRef> {
132 use std::collections::HashMap;
133
134 let mut best: HashMap<String, RegionRef> = HashMap::new();
135
136 for m in matched {
137 let key = format!("{}:{}", m.region.file, m.region.ast_anchor.name);
138 let region_ref = RegionRef {
139 region: m.region,
140 commit: m.commit,
141 timestamp: m.timestamp,
142 summary: m.summary,
143 context_level: ContextLevel::Inferred, provenance: Provenance {
145 operation: crate::schema::annotation::ProvenanceOperation::Initial,
146 derived_from: vec![],
147 original_annotations_preserved: false,
148 synthesis_notes: None,
149 },
150 };
151 let existing = best.get(&key);
152 if existing.is_none() || region_ref.timestamp > existing.unwrap().timestamp {
153 best.insert(key, region_ref);
154 }
155 }
156
157 let mut regions: Vec<RegionRef> = best.into_values().collect();
158 regions.sort_by_key(|r| r.region.lines.start);
159 regions
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_line_annotation_map_empty() {
168 let map = LineAnnotationMap::build(&[], 10);
169 assert!(map.regions_at_line(1).is_empty());
170 assert!(map.regions_at_line(5).is_empty());
171 assert!(map.next_annotated_line(1).is_none());
172 assert!(map.prev_annotated_line(10).is_none());
173 }
174
175 #[test]
176 fn test_line_annotation_map_coverage() {
177 use crate::schema::annotation::*;
178
179 let regions = vec![RegionRef {
180 region: RegionAnnotation {
181 file: "test.rs".to_string(),
182 ast_anchor: AstAnchor {
183 unit_type: "function".to_string(),
184 name: "foo".to_string(),
185 signature: None,
186 },
187 lines: LineRange { start: 3, end: 5 },
188 intent: "test".to_string(),
189 reasoning: None,
190 constraints: vec![],
191 semantic_dependencies: vec![],
192 related_annotations: vec![],
193 tags: vec![],
194 risk_notes: None,
195 corrections: vec![],
196 },
197 commit: "abc".to_string(),
198 timestamp: "2025-01-01T00:00:00Z".to_string(),
199 summary: "test".to_string(),
200 context_level: ContextLevel::Inferred,
201 provenance: Provenance {
202 operation: ProvenanceOperation::Initial,
203 derived_from: vec![],
204 original_annotations_preserved: false,
205 synthesis_notes: None,
206 },
207 }];
208
209 let map = LineAnnotationMap::build(®ions, 10);
210 assert!(map.regions_at_line(1).is_empty());
211 assert!(map.regions_at_line(2).is_empty());
212 assert_eq!(map.regions_at_line(3), &[0]);
213 assert_eq!(map.regions_at_line(4), &[0]);
214 assert_eq!(map.regions_at_line(5), &[0]);
215 assert!(map.regions_at_line(6).is_empty());
216 }
217
218 #[test]
219 fn test_next_prev_annotated_line() {
220 use crate::schema::annotation::*;
221
222 let regions = vec![RegionRef {
223 region: RegionAnnotation {
224 file: "test.rs".to_string(),
225 ast_anchor: AstAnchor {
226 unit_type: "function".to_string(),
227 name: "foo".to_string(),
228 signature: None,
229 },
230 lines: LineRange { start: 5, end: 8 },
231 intent: "test".to_string(),
232 reasoning: None,
233 constraints: vec![],
234 semantic_dependencies: vec![],
235 related_annotations: vec![],
236 tags: vec![],
237 risk_notes: None,
238 corrections: vec![],
239 },
240 commit: "abc".to_string(),
241 timestamp: "2025-01-01T00:00:00Z".to_string(),
242 summary: "test".to_string(),
243 context_level: ContextLevel::Inferred,
244 provenance: Provenance {
245 operation: ProvenanceOperation::Initial,
246 derived_from: vec![],
247 original_annotations_preserved: false,
248 synthesis_notes: None,
249 },
250 }];
251
252 let map = LineAnnotationMap::build(®ions, 15);
253 assert_eq!(map.next_annotated_line(1), Some(5));
254 assert_eq!(map.next_annotated_line(5), Some(5));
255 assert_eq!(map.next_annotated_line(9), None);
256 assert_eq!(map.prev_annotated_line(10), Some(8));
257 assert_eq!(map.prev_annotated_line(4), None);
258 }
259}