@@ -11,9 +11,10 @@ namespace RoslynDiff.TestUtilities.Parsers;
1111public class TextOutputParser : ILineNumberParser
1212{
1313 // Regex patterns for parsing text output
14+ // Change markers: + (added), - (removed), ~ (modified), > (moved), @ (renamed), = (unchanged), ? (unknown)
1415 private static readonly Regex LineReferencePattern = new ( @"\(line (\d+)(?:-(\d+))?\)" , RegexOptions . Compiled ) ;
1516 private static readonly Regex LineReferencePattern2 = new ( @"line (\d+)(?:-(\d+))?" , RegexOptions . Compiled ) ;
16- private static readonly Regex ChangeLinePattern = new ( @"^\s*(\[[\+\-M ]\])\s+(\w+):\s+(.+?)(?:\s+\(line (\d+)(?:-(\d+))?\))?$" , RegexOptions . Compiled | RegexOptions . Multiline ) ;
17+ private static readonly Regex ChangeLinePattern = new ( @"^\s*(\[[\+\-~>=@\? ]\])\s+(\w+):\s+(.+?)(?:\s+\(line (\d+)(?:-(\d+))?\))?\s* $" , RegexOptions . Compiled | RegexOptions . Multiline ) ;
1718
1819 /// <inheritdoc/>
1920 public string FormatName => "Text" ;
@@ -75,12 +76,15 @@ public static ParsedDiffResult Parse(string textContent)
7576 } ;
7677 }
7778
79+ // Normalize line endings for cross-platform compatibility
80+ var normalizedContent = textContent . Replace ( "\r \n " , "\n " ) . Replace ( "\r " , "\n " ) ;
81+
7882 var changes = new List < ParsedChange > ( ) ;
7983 var errors = new List < string > ( ) ;
8084 var metadata = new Dictionary < string , string > ( ) ;
8185
8286 // Parse header lines for metadata
83- var lines = textContent . Split ( '\n ' ) ;
87+ var lines = normalizedContent . Split ( '\n ' ) ;
8488 string ? oldPath = null ;
8589 string ? newPath = null ;
8690 string ? mode = null ;
@@ -103,7 +107,7 @@ public static ParsedDiffResult Parse(string textContent)
103107 }
104108
105109 // Parse change lines (e.g., "[+] Method: Multiply (line 5-8)")
106- var matches = ChangeLinePattern . Matches ( textContent ) ;
110+ var matches = ChangeLinePattern . Matches ( normalizedContent ) ;
107111 foreach ( Match match in matches )
108112 {
109113 var changeIndicator = match . Groups [ 1 ] . Value ;
@@ -114,7 +118,10 @@ public static ParsedDiffResult Parse(string textContent)
114118 {
115119 "[+]" => "added" ,
116120 "[-]" => "removed" ,
117- "[M]" => "modified" ,
121+ "[~]" => "modified" ,
122+ "[>]" => "moved" ,
123+ "[@]" => "renamed" ,
124+ "[=]" => "unchanged" ,
118125 _ => "unknown"
119126 } ;
120127
@@ -258,8 +265,18 @@ public List<string> ExtractLineReferences(string content)
258265 /// <summary>
259266 /// Recursively adds line numbers from a change and its children.
260267 /// </summary>
268+ /// <remarks>
269+ /// Skips unchanged items (context lines) to only include actual changes.
270+ /// </remarks>
261271 private static void AddLineNumbersFromChange ( ParsedChange change , HashSet < int > lineNumbers )
262272 {
273+ // Skip unchanged items - these are context lines, not actual changes
274+ var isUnchanged = change . ChangeType . Equals ( "unchanged" , StringComparison . OrdinalIgnoreCase ) ;
275+ if ( isUnchanged )
276+ {
277+ return ;
278+ }
279+
263280 if ( change . LineRange != null )
264281 {
265282 for ( int i = change . LineRange . Start ; i <= change . LineRange . End ; i ++ )
@@ -286,17 +303,21 @@ private static void AddLineNumbersFromChange(ParsedChange change, HashSet<int> l
286303 /// Recursively collects line ranges from a change and its children.
287304 /// </summary>
288305 /// <remarks>
289- /// Only collects line ranges from non-removed, non-container items. Removed items have
290- /// line numbers from the OLD file, not the NEW file. Container items (Namespace, Class,
291- /// Interface, etc.) have line ranges that span their children, which would cause
292- /// false overlap detection.
306+ /// Only collects line ranges from non-removed, non-unchanged, non-container items.
307+ /// Removed items have line numbers from the OLD file, not the NEW file.
308+ /// Unchanged items are context lines, not actual changes.
309+ /// Container items (Namespace, Class, Interface, etc.) have line ranges that span
310+ /// their children, which would cause false overlap detection.
293311 /// Also skips parent nodes with children to avoid hierarchical parent-child overlaps.
294312 /// </remarks>
295313 private static void CollectLineRangesFromChange ( ParsedChange change , List < LineRange > ranges )
296314 {
297315 // Skip removed items - their line numbers are from the old file context
298316 var isRemoved = change . ChangeType . Equals ( "removed" , StringComparison . OrdinalIgnoreCase ) ;
299317
318+ // Skip unchanged items - these are context lines, not actual changes
319+ var isUnchanged = change . ChangeType . Equals ( "unchanged" , StringComparison . OrdinalIgnoreCase ) ;
320+
300321 // Skip container types whose line ranges span their children
301322 var isContainer = change . Kind != null &&
302323 ( change . Kind . Equals ( "namespace" , StringComparison . OrdinalIgnoreCase ) ||
@@ -311,8 +332,8 @@ private static void CollectLineRangesFromChange(ParsedChange change, List<LineRa
311332 // Parent nodes naturally encompass their children's line ranges.
312333 if ( change . Children . Count == 0 )
313334 {
314- // Only collect from non-removed, non-container items (new file context, actual changes)
315- if ( change . LineRange != null && ! isRemoved && ! isContainer )
335+ // Only collect from non-removed, non-unchanged, non- container items (new file context, actual changes)
336+ if ( change . LineRange != null && ! isRemoved && ! isUnchanged && ! isContainer )
316337 {
317338 ranges . Add ( change . LineRange ) ;
318339 }
0 commit comments