oli_server/tools/fs/diff.rs
1use anyhow::Result;
2use std::fmt::Write;
3
4/// Represents a line in a diff
5#[derive(Debug)]
6pub enum DiffLine {
7 Added(String),
8 Removed(String),
9 Context(String),
10}
11
12/// Utilities for generating and formatting diffs between text
13pub struct DiffTools;
14
15impl DiffTools {
16 /// Generate a diff between two strings
17 pub fn generate_diff(old_text: &str, new_text: &str) -> Vec<DiffLine> {
18 let old_lines: Vec<&str> = old_text.lines().collect();
19 let new_lines: Vec<&str> = new_text.lines().collect();
20
21 // Simple line-by-line diff implementation
22 let mut diff = Vec::new();
23 let mut i = 0;
24 let mut j = 0;
25
26 while i < old_lines.len() || j < new_lines.len() {
27 if i < old_lines.len() && j < new_lines.len() && old_lines[i] == new_lines[j] {
28 // Line is the same
29 diff.push(DiffLine::Context(old_lines[i].to_string()));
30 i += 1;
31 j += 1;
32 } else {
33 // Find next matching line
34 let mut found_match = false;
35
36 // Look ahead in new_lines
37 for look_ahead in 0..3 {
38 if i < old_lines.len()
39 && j + look_ahead < new_lines.len()
40 && old_lines[i] == new_lines[j + look_ahead]
41 {
42 // Found a matching line in new_lines, add the added lines before it
43 for k in 0..look_ahead {
44 diff.push(DiffLine::Added(new_lines[j + k].to_string()));
45 }
46 j += look_ahead;
47 found_match = true;
48 break;
49 }
50 }
51
52 // Look ahead in old_lines if no match found
53 if !found_match {
54 for look_ahead in 0..3 {
55 if i + look_ahead < old_lines.len()
56 && j < new_lines.len()
57 && old_lines[i + look_ahead] == new_lines[j]
58 {
59 // Found a matching line in old_lines, add the removed lines before it
60 for k in 0..look_ahead {
61 diff.push(DiffLine::Removed(old_lines[i + k].to_string()));
62 }
63 i += look_ahead;
64 found_match = true;
65 break;
66 }
67 }
68 }
69
70 // If no match found, add one line as difference
71 if !found_match {
72 if i < old_lines.len() {
73 diff.push(DiffLine::Removed(old_lines[i].to_string()));
74 i += 1;
75 }
76 if j < new_lines.len() {
77 diff.push(DiffLine::Added(new_lines[j].to_string()));
78 j += 1;
79 }
80 }
81 }
82 }
83
84 diff
85 }
86
87 /// Format diff as a string with line numbers and colors
88 pub fn format_diff(diff: &[DiffLine], file_path: &str) -> Result<String> {
89 let mut output = String::new();
90 let mut line_number = 0;
91 let mut adds = 0;
92 let mut removes = 0;
93
94 // Count additions and removals first
95 for line in diff {
96 match line {
97 DiffLine::Added(_) => adds += 1,
98 DiffLine::Removed(_) => removes += 1,
99 _ => {}
100 }
101 }
102
103 // Add header
104 writeln!(
105 output,
106 " ⎿ Updated {} with {} addition{} and {} removal{}",
107 file_path,
108 adds,
109 if adds == 1 { "" } else { "s" },
110 removes,
111 if removes == 1 { "" } else { "s" }
112 )?;
113
114 // Only show the diff if there are changes
115 if adds > 0 || removes > 0 {
116 // Add the diff content with line numbers and colored indicators
117 for line in diff {
118 match line {
119 DiffLine::Context(text) => {
120 line_number += 1;
121 writeln!(output, " {:3} {}", line_number, text)?;
122 }
123 DiffLine::Added(text) => {
124 line_number += 1;
125 // Use ANSI colors to show additions in light green
126 writeln!(output, " \x1b[92m{:3}+ {}\x1b[0m", line_number, text)?;
127 }
128 DiffLine::Removed(text) => {
129 // For removed lines, use a darker red color
130 // Don't increment line number for removed lines
131 writeln!(output, " \x1b[91m{:3}- {}\x1b[0m", line_number, text)?;
132 }
133 }
134 }
135 }
136
137 Ok(output)
138 }
139}