1use crate::error::Result;
2use crate::models::Change;
3use similar::{ChangeTag, TextDiff};
4
5#[derive(Debug, Clone)]
6pub struct FileDiff {
7 pub path: String,
8 pub old_content: Option<String>,
9 pub new_content: Option<String>,
10 pub diff_lines: Vec<DiffLine>,
11}
12
13#[derive(Debug, Clone)]
14pub struct DiffLine {
15 pub line_type: DiffLineType,
16 pub content: String,
17 pub old_line_number: Option<usize>,
18 pub new_line_number: Option<usize>,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum DiffLineType {
23 Context,
24 Addition,
25 Deletion,
26}
27
28impl FileDiff {
29 pub fn from_change(change: &Change) -> Result<Self> {
30 let old_content = change
31 .content_before
32 .as_ref()
33 .and_then(|bytes| String::from_utf8(bytes.clone()).ok());
34
35 let new_content = change
36 .content_after
37 .as_ref()
38 .and_then(|bytes| String::from_utf8(bytes.clone()).ok());
39
40 let diff_lines = if let (Some(old), Some(new)) = (&old_content, &new_content) {
41 Self::compute_diff(old, new)
42 } else {
43 Vec::new()
44 };
45
46 Ok(FileDiff {
47 path: change.path.to_string_lossy().to_string(),
48 old_content,
49 new_content,
50 diff_lines,
51 })
52 }
53
54 fn compute_diff(old_text: &str, new_text: &str) -> Vec<DiffLine> {
55 let diff = TextDiff::from_lines(old_text, new_text);
56 let mut lines = Vec::new();
57 let mut old_line_num = 1;
58 let mut new_line_num = 1;
59
60 for change in diff.iter_all_changes() {
61 let (line_type, old_num, new_num) = match change.tag() {
62 ChangeTag::Delete => {
63 let num = old_line_num;
64 old_line_num += 1;
65 (DiffLineType::Deletion, Some(num), None)
66 }
67 ChangeTag::Insert => {
68 let num = new_line_num;
69 new_line_num += 1;
70 (DiffLineType::Addition, None, Some(num))
71 }
72 ChangeTag::Equal => {
73 let old_num = old_line_num;
74 let new_num = new_line_num;
75 old_line_num += 1;
76 new_line_num += 1;
77 (DiffLineType::Context, Some(old_num), Some(new_num))
78 }
79 };
80
81 lines.push(DiffLine {
82 line_type,
83 content: change.to_string(),
84 old_line_number: old_num,
85 new_line_number: new_num,
86 });
87 }
88
89 lines
90 }
91
92 pub fn format_unified(&self, context_lines: usize) -> String {
93 let mut output = String::new();
94
95 output.push_str(&format!("--- {}\n", self.path));
96 output.push_str(&format!("+++ {}\n", self.path));
97
98 let mut in_hunk = false;
99 let mut hunk_start = 0;
100 let mut hunk_lines = Vec::new();
101
102 for (i, line) in self.diff_lines.iter().enumerate() {
103 if line.line_type != DiffLineType::Context || in_hunk {
104 if !in_hunk {
105 in_hunk = true;
106 hunk_start = i.saturating_sub(context_lines);
107 }
108
109 let prefix = match line.line_type {
110 DiffLineType::Addition => "+",
111 DiffLineType::Deletion => "-",
112 DiffLineType::Context => " ",
113 };
114
115 hunk_lines.push(format!("{}{}", prefix, line.content));
116
117 if i + context_lines >= self.diff_lines.len() - 1 {
119 if !hunk_lines.is_empty() {
120 output.push_str(&format!(
121 "@@ -{},{} +{},{} @@\n",
122 self.diff_lines[hunk_start].old_line_number.unwrap_or(0),
123 hunk_lines.len(),
124 self.diff_lines[hunk_start].new_line_number.unwrap_or(0),
125 hunk_lines.len()
126 ));
127 output.push_str(&hunk_lines.join(""));
128 hunk_lines.clear();
129 }
130 in_hunk = false;
131 }
132 }
133 }
134
135 output
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::models::ChangeType;
143 use std::path::PathBuf;
144 use uuid::Uuid;
145
146 #[test]
147 fn test_diff_computation() {
148 let old_text = "line 1\nline 2\nline 3\n";
149 let new_text = "line 1\nline 2 modified\nline 3\nline 4\n";
150
151 let diff_lines = FileDiff::compute_diff(old_text, new_text);
152
153 assert!(!diff_lines.is_empty());
154 assert!(diff_lines
155 .iter()
156 .any(|l| l.line_type == DiffLineType::Addition));
157 assert!(diff_lines
158 .iter()
159 .any(|l| l.line_type == DiffLineType::Deletion));
160 }
161
162 #[test]
163 fn test_file_diff_from_change() {
164 let session_id = Uuid::new_v4();
165 let change = Change::new(ChangeType::Modify, PathBuf::from("test.txt"), session_id)
166 .with_content_before(b"Hello\nWorld\n".to_vec())
167 .with_content_after(b"Hello\nRust\nWorld\n".to_vec());
168
169 let file_diff = FileDiff::from_change(&change).unwrap();
170
171 assert_eq!(file_diff.path, "test.txt");
172 assert!(file_diff.old_content.is_some());
173 assert!(file_diff.new_content.is_some());
174 assert!(!file_diff.diff_lines.is_empty());
175 }
176}