1+ using System ;
2+ using System . Collections . Generic ;
3+ using System . Linq ;
4+ using System . Text ;
5+ using DiffPlex ;
6+ using DiffPlex . Chunkers ;
7+ using DiffPlex . Model ;
8+
9+ namespace DiffPlex . Renderer
10+ {
11+ /// <summary>
12+ /// Renderer for generating unified diff (unidiff) format output from diff results
13+ /// </summary>
14+ public class UnidiffRenderer
15+ {
16+ private readonly IDiffer differ ;
17+ private readonly int contextLines ;
18+
19+ /// <summary>
20+ /// Gets the default singleton instance of the unidiff renderer.
21+ /// </summary>
22+ public static UnidiffRenderer Instance { get ; } = new UnidiffRenderer ( ) ;
23+
24+ /// <summary>
25+ /// Initializes a new instance of the <see cref="UnidiffRenderer"/> class.
26+ /// </summary>
27+ /// <param name="differ">The differ to use. If null, uses the default Differ.</param>
28+ /// <param name="contextLines">Number of unchanged context lines to include around changes.</param>
29+ public UnidiffRenderer ( IDiffer differ = null , int contextLines = 3 )
30+ {
31+ this . differ = differ ?? Differ . Instance ;
32+ this . contextLines = contextLines ;
33+ }
34+
35+ /// <summary>
36+ /// Generates a unified diff format output from two texts.
37+ /// </summary>
38+ /// <param name="oldText">The old text to diff.</param>
39+ /// <param name="newText">The new text.</param>
40+ /// <param name="oldFileName">The old file name to show in the headers.</param>
41+ /// <param name="newFileName">The new file name to show in the headers.</param>
42+ /// <param name="ignoreWhitespace">Whether to ignore whitespace differences.</param>
43+ /// <param name="ignoreCase">Whether to ignore case differences.</param>
44+ /// <returns>A string containing the unified diff output.</returns>
45+ public string Generate ( string oldText , string newText , string oldFileName = "a" , string newFileName = "b" , bool ignoreWhitespace = true , bool ignoreCase = false )
46+ {
47+ if ( oldText == null ) throw new ArgumentNullException ( nameof ( oldText ) ) ;
48+ if ( newText == null ) throw new ArgumentNullException ( nameof ( newText ) ) ;
49+ if ( oldFileName == null ) throw new ArgumentNullException ( nameof ( oldFileName ) ) ;
50+ if ( newFileName == null ) throw new ArgumentNullException ( nameof ( newFileName ) ) ;
51+
52+ var diffResult = differ . CreateDiffs ( oldText , newText , ignoreWhitespace , ignoreCase , new LineChunker ( ) ) ;
53+ return Generate ( diffResult , oldFileName , newFileName ) ;
54+ }
55+
56+ /// <summary>
57+ /// Generates a unified diff format output from a diff result.
58+ /// </summary>
59+ /// <param name="diffResult">The diff result to render.</param>
60+ /// <param name="oldFileName">The old file name to show in the headers.</param>
61+ /// <param name="newFileName">The new file name to show in the headers.</param>
62+ /// <returns>A string containing the unified diff output.</returns>
63+ public string Generate ( DiffResult diffResult , string oldFileName = "a" , string newFileName = "b" )
64+ {
65+ if ( diffResult == null ) throw new ArgumentNullException ( nameof ( diffResult ) ) ;
66+ if ( oldFileName == null ) throw new ArgumentNullException ( nameof ( oldFileName ) ) ;
67+ if ( newFileName == null ) throw new ArgumentNullException ( nameof ( newFileName ) ) ;
68+
69+ if ( diffResult . DiffBlocks . Count == 0 )
70+ {
71+ return string . Empty ;
72+ }
73+
74+ var sb = new StringBuilder ( ) ;
75+
76+ // Generate the unified diff header
77+ sb . AppendLine ( $ "--- { oldFileName } ") ;
78+ sb . AppendLine ( $ "+++ { newFileName } ") ;
79+
80+ // Group changes into hunks with context
81+ var hunks = CreateHunks ( diffResult ) ;
82+
83+ foreach ( var hunk in hunks )
84+ {
85+ // Calculate line numbers for the hunk header
86+ int oldStart = hunk . OldStartLine ;
87+ int oldCount = hunk . OldLength ;
88+ int newStart = hunk . NewStartLine ;
89+ int newCount = hunk . NewLength ;
90+
91+ // Generate the hunk header
92+ sb . AppendLine ( $ "@@ -{ oldStart } ,{ oldCount } +{ newStart } ,{ newCount } @@") ;
93+
94+ // Generate the hunk content
95+ foreach ( var line in hunk . Lines )
96+ {
97+ switch ( line . Type )
98+ {
99+ case LineType . Unchanged :
100+ sb . AppendLine ( $ " { line . Text } ") ;
101+ break ;
102+ case LineType . Deleted :
103+ sb . AppendLine ( $ "-{ line . Text } ") ;
104+ break ;
105+ case LineType . Inserted :
106+ sb . AppendLine ( $ "+{ line . Text } ") ;
107+ break ;
108+ }
109+ }
110+ }
111+
112+ return sb . ToString ( ) ;
113+ }
114+
115+ private List < DiffHunk > CreateHunks ( DiffResult diffResult )
116+ {
117+ var hunks = new List < DiffHunk > ( ) ;
118+
119+ if ( diffResult . DiffBlocks . Count == 0 ) return hunks ;
120+
121+ var oldPieces = diffResult . PiecesOld ;
122+ var newPieces = diffResult . PiecesNew ;
123+
124+ // First, organize the diff blocks into potential hunks separated by contextLines boundary
125+ List < List < DiffBlock > > hunkGroups = new List < List < DiffBlock > > ( ) ;
126+ List < DiffBlock > currentGroup = new List < DiffBlock > ( ) ;
127+ hunkGroups . Add ( currentGroup ) ;
128+
129+ DiffBlock previousBlock = null ;
130+ foreach ( var block in diffResult . DiffBlocks )
131+ {
132+ if ( previousBlock != null )
133+ {
134+ // If the blocks are too far apart, start a new group
135+ // We want to create a new hunk if the distance between blocks is more than 2*contextLines
136+ if ( block . DeleteStartA > ( previousBlock . DeleteStartA + previousBlock . DeleteCountA + 2 * contextLines ) )
137+ {
138+ currentGroup = new List < DiffBlock > ( ) ;
139+ hunkGroups . Add ( currentGroup ) ;
140+ }
141+ }
142+
143+ currentGroup . Add ( block ) ;
144+ previousBlock = block ;
145+ }
146+
147+ // Now convert each group to a hunk
148+ foreach ( var group in hunkGroups )
149+ {
150+ if ( group . Count == 0 ) continue ;
151+
152+ // Find the range of the entire group with context
153+ int firstBlockStartA = group [ 0 ] . DeleteStartA ;
154+ int lastBlockEndA = group [ group . Count - 1 ] . DeleteStartA + group [ group . Count - 1 ] . DeleteCountA ;
155+
156+ int contextStartA = Math . Max ( 0 , firstBlockStartA - contextLines ) ;
157+ int contextEndA = Math . Min ( oldPieces . Count , lastBlockEndA + contextLines ) ;
158+
159+ // Calculate B file positions
160+ int firstBlockStartB = group [ 0 ] . InsertStartB ;
161+ int contextStartB = Math . Max ( 0 , firstBlockStartB - contextLines ) ;
162+
163+ // Create a new hunk
164+ DiffHunk hunk = new DiffHunk
165+ {
166+ OldStartLine = contextStartA + 1 , // 1-based indexing
167+ NewStartLine = contextStartB + 1 // 1-based indexing
168+ } ;
169+
170+ // Add context lines before first change
171+ for ( int i = contextStartA ; i < firstBlockStartA ; i ++ )
172+ {
173+ hunk . Lines . Add ( new DiffLine
174+ {
175+ Type = LineType . Unchanged ,
176+ Text = oldPieces [ i ] ,
177+ OldIndex = i ,
178+ NewIndex = contextStartB + ( i - contextStartA )
179+ } ) ;
180+ }
181+
182+ // Add all blocks and intermediate context
183+ int currentPosA = firstBlockStartA ;
184+ int currentPosB = firstBlockStartB ;
185+
186+ for ( int blockIndex = 0 ; blockIndex < group . Count ; blockIndex ++ )
187+ {
188+ var block = group [ blockIndex ] ;
189+
190+ // Add context between blocks if needed
191+ for ( int i = currentPosA ; i < block . DeleteStartA ; i ++ )
192+ {
193+ int newIndex = currentPosB + ( i - currentPosA ) ;
194+ hunk . Lines . Add ( new DiffLine
195+ {
196+ Type = LineType . Unchanged ,
197+ Text = oldPieces [ i ] ,
198+ OldIndex = i ,
199+ NewIndex = newIndex
200+ } ) ;
201+ }
202+
203+ // Update the current position in B
204+ if ( currentPosA < block . DeleteStartA )
205+ {
206+ currentPosB += ( block . DeleteStartA - currentPosA ) ;
207+ }
208+
209+ // Add deleted lines
210+ for ( int i = 0 ; i < block . DeleteCountA ; i ++ )
211+ {
212+ hunk . Lines . Add ( new DiffLine
213+ {
214+ Type = LineType . Deleted ,
215+ Text = oldPieces [ block . DeleteStartA + i ] ,
216+ OldIndex = block . DeleteStartA + i ,
217+ NewIndex = - 1
218+ } ) ;
219+ }
220+
221+ // Add inserted lines
222+ for ( int i = 0 ; i < block . InsertCountB ; i ++ )
223+ {
224+ hunk . Lines . Add ( new DiffLine
225+ {
226+ Type = LineType . Inserted ,
227+ Text = newPieces [ block . InsertStartB + i ] ,
228+ OldIndex = - 1 ,
229+ NewIndex = block . InsertStartB + i
230+ } ) ;
231+ }
232+
233+ currentPosA = block . DeleteStartA + block . DeleteCountA ;
234+ currentPosB = block . InsertStartB + block . InsertCountB ;
235+ }
236+
237+ // Add context after last block
238+ for ( int i = currentPosA ; i < contextEndA ; i ++ )
239+ {
240+ int newIndex = currentPosB + ( i - currentPosA ) ;
241+ if ( newIndex < newPieces . Count ) // Ensure we don't go out of bounds
242+ {
243+ hunk . Lines . Add ( new DiffLine
244+ {
245+ Type = LineType . Unchanged ,
246+ Text = oldPieces [ i ] ,
247+ OldIndex = i ,
248+ NewIndex = newIndex
249+ } ) ;
250+ }
251+ }
252+
253+ // Calculate final hunk lengths
254+ hunk . OldLength = hunk . Lines . Count ( l => l . Type != LineType . Inserted ) ;
255+ hunk . NewLength = hunk . Lines . Count ( l => l . Type != LineType . Deleted ) ;
256+
257+ hunks . Add ( hunk ) ;
258+ }
259+
260+ return hunks ;
261+ }
262+
263+ /// <summary>
264+ /// Generate a unified diff format output directly from two texts.
265+ /// </summary>
266+ /// <param name="oldText">The old text to diff.</param>
267+ /// <param name="newText">The new text.</param>
268+ /// <param name="oldFileName">The old file name to show in the headers.</param>
269+ /// <param name="newFileName">The new file name to show in the headers.</param>
270+ /// <param name="ignoreWhitespace">Whether to ignore whitespace differences.</param>
271+ /// <param name="ignoreCase">Whether to ignore case differences.</param>
272+ /// <param name="contextLines">Number of unchanged context lines to include around changes.</param>
273+ /// <returns>A string containing the unified diff output.</returns>
274+ public static string GenerateUnidiff (
275+ string oldText ,
276+ string newText ,
277+ string oldFileName = "a" ,
278+ string newFileName = "b" ,
279+ bool ignoreWhitespace = true ,
280+ bool ignoreCase = false ,
281+ int contextLines = 3 )
282+ {
283+ var renderer = new UnidiffRenderer ( contextLines : contextLines ) ;
284+ return renderer . Generate ( oldText , newText , oldFileName , newFileName , ignoreWhitespace , ignoreCase ) ;
285+ }
286+
287+ #region Helper Classes
288+ private enum LineType
289+ {
290+ Unchanged ,
291+ Deleted ,
292+ Inserted
293+ }
294+
295+ private class DiffLine
296+ {
297+ public LineType Type { get ; set ; }
298+ public string Text { get ; set ; }
299+ public int OldIndex { get ; set ; }
300+ public int NewIndex { get ; set ; }
301+ }
302+
303+ private class DiffHunk
304+ {
305+ public int OldStartLine { get ; set ; }
306+ public int OldLength { get ; set ; }
307+ public int NewStartLine { get ; set ; }
308+ public int NewLength { get ; set ; }
309+ public List < DiffLine > Lines { get ; } = new List < DiffLine > ( ) ;
310+ }
311+ #endregion
312+ }
313+ }
0 commit comments