oyo_core/
change.rs

1//! Change representation for diff operations
2
3use serde::{Deserialize, Serialize};
4
5/// The kind of change in a diff
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum ChangeKind {
8    /// Content was added
9    Insert,
10    /// Content was removed
11    Delete,
12    /// Content was modified (for word-level changes within a line)
13    Replace,
14    /// Content is unchanged (context)
15    Equal,
16}
17
18/// A span of text that represents a change
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct ChangeSpan {
21    /// The kind of change
22    pub kind: ChangeKind,
23    /// The text content (old content for Delete/Replace, new for Insert)
24    pub text: String,
25    /// For Replace: the new text that replaces the old
26    pub new_text: Option<String>,
27    /// Line number in the old file (if applicable)
28    pub old_line: Option<usize>,
29    /// Line number in the new file (if applicable)
30    pub new_line: Option<usize>,
31}
32
33impl ChangeSpan {
34    pub fn new(kind: ChangeKind, text: impl Into<String>) -> Self {
35        Self {
36            kind,
37            text: text.into(),
38            new_text: None,
39            old_line: None,
40            new_line: None,
41        }
42    }
43
44    pub fn insert(text: impl Into<String>) -> Self {
45        Self::new(ChangeKind::Insert, text)
46    }
47
48    pub fn delete(text: impl Into<String>) -> Self {
49        Self::new(ChangeKind::Delete, text)
50    }
51
52    pub fn equal(text: impl Into<String>) -> Self {
53        Self::new(ChangeKind::Equal, text)
54    }
55
56    pub fn replace(old: impl Into<String>, new: impl Into<String>) -> Self {
57        Self {
58            kind: ChangeKind::Replace,
59            text: old.into(),
60            new_text: Some(new.into()),
61            old_line: None,
62            new_line: None,
63        }
64    }
65
66    pub fn with_lines(mut self, old_line: Option<usize>, new_line: Option<usize>) -> Self {
67        self.old_line = old_line;
68        self.new_line = new_line;
69        self
70    }
71
72    /// Check if this is an actual change (not just context)
73    pub fn is_change(&self) -> bool {
74        self.kind != ChangeKind::Equal
75    }
76}
77
78/// A complete change unit (may contain multiple spans for word-level diffs)
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct Change {
81    /// Unique ID for this change
82    pub id: usize,
83    /// The spans that make up this change
84    pub spans: Vec<ChangeSpan>,
85    /// Description of the change (e.g., "modified function call")
86    pub description: Option<String>,
87}
88
89impl Change {
90    pub fn new(id: usize, spans: Vec<ChangeSpan>) -> Self {
91        Self {
92            id,
93            spans,
94            description: None,
95        }
96    }
97
98    pub fn single(id: usize, span: ChangeSpan) -> Self {
99        Self::new(id, vec![span])
100    }
101
102    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
103        self.description = Some(desc.into());
104        self
105    }
106
107    /// Get all the changes (non-equal spans)
108    pub fn changes(&self) -> impl Iterator<Item = &ChangeSpan> {
109        self.spans.iter().filter(|s| s.is_change())
110    }
111
112    /// Check if this change contains any actual modifications
113    pub fn has_changes(&self) -> bool {
114        self.spans.iter().any(|s| s.is_change())
115    }
116}