ratatui_toolkit/widgets/code_diff/widget/constructors/
from_unified_diff.rs

1use std::collections::HashMap;
2
3use crate::primitives::resizable_split::ResizableSplit;
4use crate::services::theme::AppTheme;
5use crate::widgets::code_diff::code_diff::CodeDiff;
6use crate::widgets::code_diff::diff_config::DiffConfig;
7use crate::widgets::code_diff::diff_file_tree::DiffFileTree;
8use crate::widgets::code_diff::diff_hunk::DiffHunk;
9use crate::widgets::code_diff::diff_line::DiffLine;
10
11impl CodeDiff {
12    /// Creates a diff widget by parsing unified diff format text.
13    ///
14    /// Parses the standard unified diff format used by `git diff`, `diff -u`, etc.
15    ///
16    /// # Arguments
17    ///
18    /// * `diff_text` - The unified diff text to parse
19    ///
20    /// # Returns
21    ///
22    /// A new `CodeDiff` instance with parsed hunks
23    ///
24    /// # Example
25    ///
26    /// ```rust
27    /// use ratatui_toolkit::code_diff::CodeDiff;
28    ///
29    /// let diff_text = r#"
30    /// --- a/file.txt
31    /// +++ b/file.txt
32    /// @@ -1,4 +1,5 @@
33    ///  context line
34    /// -removed line
35    /// +added line
36    ///  more context
37    /// "#;
38    ///
39    /// let diff = CodeDiff::from_unified_diff(diff_text);
40    /// assert!(!diff.hunks.is_empty());
41    /// ```
42    pub fn from_unified_diff(diff_text: &str) -> Self {
43        let (file_path, hunks) = parse_unified_diff(diff_text);
44        let config = DiffConfig::new();
45
46        // Create ResizableSplit with config values
47        let mut sidebar_split = ResizableSplit::new(config.sidebar_default_width);
48        sidebar_split.min_percent = config.sidebar_min_width;
49        sidebar_split.max_percent = config.sidebar_max_width;
50
51        Self {
52            file_path,
53            hunks,
54            scroll_offset: 0,
55            file_tree: DiffFileTree::new(),
56            file_diffs: HashMap::new(),
57            show_sidebar: config.sidebar_enabled,
58            sidebar_split,
59            sidebar_focused: true,
60            config,
61            theme: AppTheme::default(),
62        }
63    }
64}
65
66/// Parses unified diff format text into hunks.
67///
68/// # Arguments
69///
70/// * `diff_text` - The unified diff text to parse
71///
72/// # Returns
73///
74/// A tuple of (optional file path, vector of hunks)
75fn parse_unified_diff(diff_text: &str) -> (Option<String>, Vec<DiffHunk>) {
76    let mut file_path: Option<String> = None;
77    let mut hunks: Vec<DiffHunk> = Vec::new();
78    let mut current_hunk: Option<DiffHunk> = None;
79    let mut old_line_num: usize = 0;
80    let mut new_line_num: usize = 0;
81
82    for line in diff_text.lines() {
83        // Parse file headers
84        if line.starts_with("--- ") {
85            // Old file header - extract path
86            let path = line
87                .strip_prefix("--- ")
88                .and_then(|p| p.strip_prefix("a/"))
89                .unwrap_or_else(|| line.strip_prefix("--- ").unwrap_or(""));
90            if file_path.is_none() && !path.is_empty() {
91                file_path = Some(path.to_string());
92            }
93            continue;
94        }
95
96        if line.starts_with("+++ ") {
97            // New file header - skip
98            continue;
99        }
100
101        // Parse hunk header
102        if line.starts_with("@@") {
103            // Save previous hunk if exists
104            if let Some(hunk) = current_hunk.take() {
105                hunks.push(hunk);
106            }
107
108            // Parse new hunk header
109            if let Some(hunk) = DiffHunk::from_header(line) {
110                old_line_num = hunk.old_start;
111                new_line_num = hunk.new_start;
112                current_hunk = Some(hunk);
113            }
114            continue;
115        }
116
117        // Parse diff lines within a hunk
118        if let Some(ref mut hunk) = current_hunk {
119            let diff_line = if let Some(content) = line.strip_prefix('+') {
120                let line = DiffLine::added(content, new_line_num);
121                new_line_num += 1;
122                Some(line)
123            } else if let Some(content) = line.strip_prefix('-') {
124                let line = DiffLine::removed(content, old_line_num);
125                old_line_num += 1;
126                Some(line)
127            } else if let Some(content) = line.strip_prefix(' ') {
128                let line = DiffLine::context(content, old_line_num, new_line_num);
129                old_line_num += 1;
130                new_line_num += 1;
131                Some(line)
132            } else if line.is_empty() {
133                // Empty line in diff (context line with no content)
134                let line = DiffLine::context("", old_line_num, new_line_num);
135                old_line_num += 1;
136                new_line_num += 1;
137                Some(line)
138            } else {
139                None
140            };
141
142            if let Some(diff_line) = diff_line {
143                hunk.add_line(diff_line);
144            }
145        }
146    }
147
148    // Don't forget the last hunk
149    if let Some(hunk) = current_hunk {
150        hunks.push(hunk);
151    }
152
153    (file_path, hunks)
154}