diff --git a/DiffPlex/DiffBuilder/InlineDiffBuilder.cs b/DiffPlex/DiffBuilder/InlineDiffBuilder.cs index 485c515f..6d0114bc 100644 --- a/DiffPlex/DiffBuilder/InlineDiffBuilder.cs +++ b/DiffPlex/DiffBuilder/InlineDiffBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using DiffPlex.Chunkers; using DiffPlex.DiffBuilder.Model; using DiffPlex.Model; @@ -10,6 +11,8 @@ public class InlineDiffBuilder : IInlineDiffBuilder { private readonly IDiffer differ; + private delegate ChangeType PieceBuilder(string oldText, string newText, List pieces, bool ignoreWhitespace, bool ignoreCase); + /// /// Gets the default singleton instance of the inline diff builder. /// @@ -77,6 +80,130 @@ public static DiffPaneModel Diff(IDiffer differ, string oldText, string newText, return model; } + public static DiffPaneModel Diff( + IDiffer differ, + string oldText, string newText, + List detailsPack, + bool ignoreWhiteSpace = true, bool ignoreCase = false) + { + if (oldText == null) throw new ArgumentNullException(nameof(oldText)); + if (newText == null) throw new ArgumentNullException(nameof(newText)); + + if (differ == null) return Diff(oldText, newText, ignoreWhiteSpace, ignoreCase); + + LinkedList chunkers; + if (detailsPack == null || !detailsPack.Any()) + { + chunkers = new LinkedList(); + chunkers.AddLast(DiffPlex.Chunkers.LineChunker.Instance); + chunkers.AddLast(DiffPlex.Chunkers.WordChunker.Instance); + chunkers.AddLast(DiffPlex.Chunkers.CharacterChunker.Instance); + } + else + { + chunkers = new LinkedList(detailsPack); + } + + var model = new DiffPaneModel(); + var cnode = chunkers.First; + var diffResult = differ.CreateDiffs(oldText, newText, ignoreWhiteSpace, ignoreCase, cnode.Value); + BuildDiffPieces(diffResult, model.Lines, NextPieceBuilderInternal(differ, cnode.Next), ignoreWhiteSpace, ignoreCase); + + return model; + } + + + private static PieceBuilder NextPieceBuilderInternal( + IDiffer differ, + LinkedListNode chunkerNode) + { + if (chunkerNode == null) + { + return null; + } + else + { + return (ot, nt, p, iw, ic) => + { + var r = differ.CreateDiffs(ot, nt, iw, ic, chunkerNode.Value); + return BuildDiffPieces(r, p, NextPieceBuilderInternal(differ, chunkerNode.Next), iw, ic); + }; + } + } + + private static ChangeType BuildDiffPieces( + DiffResult diffResult, + List pieces, PieceBuilder subPieceBuilder, + bool ignoreWhiteSpace, bool ignoreCase) + { + int aPos = 0; + int bPos = 0; + + ChangeType changeSummary = ChangeType.Unchanged; + + foreach (var diffBlock in diffResult.DiffBlocks) + { + while (bPos < diffBlock.InsertStartB && aPos < diffBlock.DeleteStartA) + { + pieces.Add(new DiffPiece(diffResult.PiecesOld[aPos], ChangeType.Unchanged, aPos + 1)); + aPos++; + bPos++; + } + + int i = 0; + for (; i < Math.Min(diffBlock.DeleteCountA, diffBlock.InsertCountB); i++) + { + var piece = new DiffPiece(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted, aPos + 1); + //var newPiece = new DiffPiece(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1); + + if (subPieceBuilder != null) + { + var subChangeSummary = subPieceBuilder(diffResult.PiecesOld[aPos], diffResult.PiecesNew[bPos], piece.SubPieces, ignoreWhiteSpace, ignoreCase); + piece.Type = subChangeSummary; + } + + pieces.Add(piece); + aPos++; + bPos++; + } + + if (diffBlock.DeleteCountA > diffBlock.InsertCountB) + { + for (; i < diffBlock.DeleteCountA; i++) + { + pieces.Add(new DiffPiece(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted, aPos + 1)); + + aPos++; + } + } + else + { + for (; i < diffBlock.InsertCountB; i++) + { + pieces.Add(new DiffPiece(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1)); + + bPos++; + } + } + } + + while (bPos < diffResult.PiecesNew.Length && aPos < diffResult.PiecesOld.Length) + { + pieces.Add(new DiffPiece(diffResult.PiecesOld[aPos], ChangeType.Unchanged, aPos + 1)); + aPos++; + bPos++; + } + + // Consider the whole diff as "modified" if we found any change, otherwise we consider it unchanged + if (pieces.Any(x => x.Type == ChangeType.Modified || x.Type == ChangeType.Inserted || x.Type == ChangeType.Deleted)) + { + changeSummary = ChangeType.Modified; + } + + return changeSummary; + } + + private static void BuildDiffPieces(DiffResult diffResult, List pieces) { int bPos = 0; diff --git a/DiffPlex/DiffBuilder/Model/DiffPaneModel.cs b/DiffPlex/DiffBuilder/Model/DiffPaneModel.cs index f7a19831..fb165737 100644 --- a/DiffPlex/DiffBuilder/Model/DiffPaneModel.cs +++ b/DiffPlex/DiffBuilder/Model/DiffPaneModel.cs @@ -1,10 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace DiffPlex.DiffBuilder.Model { public class DiffPaneModel { + public List Chunks => Lines; + public List Lines { get; } public bool HasDifferences diff --git a/DiffPlex/DiffBuilder/SideBySideDiffBuilder.cs b/DiffPlex/DiffBuilder/SideBySideDiffBuilder.cs index c961596e..a6d50de2 100644 --- a/DiffPlex/DiffBuilder/SideBySideDiffBuilder.cs +++ b/DiffPlex/DiffBuilder/SideBySideDiffBuilder.cs @@ -104,6 +104,59 @@ public static SideBySideDiffModel Diff(IDiffer differ, string oldText, string ne return model; } + public static SideBySideDiffModel Diff( + IDiffer differ, + string oldText, string newText, + List detailsPack, + bool ignoreWhiteSpace = true, bool ignoreCase = false) + { + if (oldText == null) throw new ArgumentNullException(nameof(oldText)); + if (newText == null) throw new ArgumentNullException(nameof(newText)); + + if (differ == null) return Diff(oldText, newText, ignoreWhiteSpace, ignoreCase); + + LinkedList chunkers; + if (detailsPack == null || !detailsPack.Any()) + { + chunkers = new LinkedList(); + chunkers.AddLast(DiffPlex.Chunkers.LineChunker.Instance); + chunkers.AddLast(DiffPlex.Chunkers.WordChunker.Instance); + chunkers.AddLast(DiffPlex.Chunkers.CharacterChunker.Instance); + } + else + { + chunkers = new LinkedList(detailsPack); + } + + var model = new SideBySideDiffModel(); + var cnode = chunkers.First; + + var diffResult = differ.CreateDiffs(oldText, newText, ignoreWhiteSpace, ignoreCase, cnode.Value); + BuildDiffPieces(diffResult, model.OldText.Lines, model.NewText.Lines, NextPieceBuilderInternal(differ, cnode.Next), ignoreWhiteSpace, ignoreCase); + + return model; + } + + + private static PieceBuilder NextPieceBuilderInternal( + IDiffer differ, + LinkedListNode chunkerNode) + { + if (chunkerNode == null) + { + return null; + } + else + { + return (ot, nt, op, np, iw, ic) => + { + var r = differ.CreateDiffs(ot, nt, iw, ic, chunkerNode.Value); + return BuildDiffPieces(r, op, np, NextPieceBuilderInternal(differ, chunkerNode.Next), iw, ic); + }; + } + } + + private static ChangeType BuildWordDiffPiecesInternal(string oldText, string newText, List oldPieces, List newPieces, bool ignoreWhiteSpace, bool ignoreCase) { var diffResult = Differ.Instance.CreateDiffs(oldText, newText, ignoreWhiteSpace, ignoreCase, WordChunker.Instance); diff --git a/Facts.DiffPlex/InlineDiffBuilderFacts.cs b/Facts.DiffPlex/InlineDiffBuilderFacts.cs index d382d75f..eb2b0610 100644 --- a/Facts.DiffPlex/InlineDiffBuilderFacts.cs +++ b/Facts.DiffPlex/InlineDiffBuilderFacts.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using DiffPlex; using DiffPlex.DiffBuilder; using DiffPlex.DiffBuilder.Model; @@ -344,6 +345,68 @@ public void Can_compare_whitespace() new DiffPiece("5", ChangeType.Unchanged, 5), }); } + + + [Fact] + public void Will_build_hierarchial_diffModel_lines_words_chars() + { + string textOld = + @"What is Lorem Ipsum? +Lorem Ipsum is simply dummy text of the printing and typesetting industry. +Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, +when an unknown printer took a galley of type and scrambled it to make a type +specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, +remaining essentially unchanged. +It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, +and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; + + string textNew = + @"What the heck is Lorem Ipsum? +Lorem Ipsum is simply dummy text of the printing and typesetting industry. +when an unknown printer took a galley of type and scrambled it to make a type +specimen book. It hos survived not only five centuries, but also the leap into electronic typesatting, +remaining essentially unchanged. +It was popularised in the 1961s with the release of Letraset sheets containing Lorem Ipsum passages, +and more recently with desktop publishing software like Aldus PagesMaker including versions of Lorem Ipsum."; + + + var diff = InlineDiffBuilder.Diff( + new Differ(), + textOld, textNew, + detailsPack: null, + ignoreWhiteSpace: false, + ignoreCase: false + ); + + Assert.NotNull(diff); + Assert.True(diff.Lines.Count == 8); + + Assert.True(diff.HasDifferences); + + // Check on Line level + var changedLines = diff.Lines.Where(x => x.Type != ChangeType.Unchanged).ToList(); + Assert.Equal(5, changedLines.Count); + + // Check on Word level + var changedWords = changedLines[0].SubPieces.Where(x => x.Type != ChangeType.Unchanged).ToList(); + Assert.NotNull(changedWords); + Assert.True(changedWords.Count == 4); + Assert.Equal(ChangeType.Inserted, changedWords[0].Type); + Assert.Equal("the", changedWords[0].Text); + Assert.Equal(ChangeType.Inserted, changedWords[1].Type); + Assert.Equal(" ", changedWords[1].Text); + Assert.Equal(ChangeType.Inserted, changedWords[2].Type); + Assert.Equal("heck", changedWords[2].Text); + Assert.Equal(ChangeType.Inserted, changedWords[3].Type); + Assert.Equal(" ", changedWords[3].Text); + + // Check on Character level + var changedChars = changedLines[2].SubPieces[30].SubPieces.Where(x => x.Type != ChangeType.Unchanged).ToList(); + Assert.NotNull(changedChars); + Assert.Single(changedChars); + Assert.Equal(ChangeType.Deleted, changedChars[0].Type); + Assert.Equal("e", changedChars[0].Text); + } } } } diff --git a/Facts.DiffPlex/SideBySideDiffBuilderFacts.cs b/Facts.DiffPlex/SideBySideDiffBuilderFacts.cs index 66401e5d..18e6bf8c 100644 --- a/Facts.DiffPlex/SideBySideDiffBuilderFacts.cs +++ b/Facts.DiffPlex/SideBySideDiffBuilderFacts.cs @@ -6,6 +6,7 @@ using DiffPlex.DiffBuilder.Model; using Moq; using Xunit; +using System.Linq; namespace Facts.DiffPlex { @@ -333,6 +334,90 @@ public void Will_build_diffModel_for_partially_different_lines() Assert.True(bidiff.OldText.HasDifferences && bidiff.NewText.HasDifferences); } + [Fact] + public void Will_build_hierarchial_diffModel_lines_words_chars() + { + string textOld = + @"What is Lorem Ipsum? +Lorem Ipsum is simply dummy text of the printing and typesetting industry. +Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, +when an unknown printer took a galley of type and scrambled it to make a type +specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, +remaining essentially unchanged. +It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, +and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; + + string textNew = + @"What the heck is Lorem Ipsum? +Lorem Ipsum is simply dummy text of the printing and typesetting industry. +when an unknown printer took a galley of type and scrambled it to make a type +specimen book. It hos survived not only five centuries, but also the leap into electronic typesatting, +remaining essentially unchanged. +It was popularised in the 1961s with the release of Letraset sheets containing Lorem Ipsum passages, +and more recently with desktop publishing software like Aldus PagesMaker including versions of Lorem Ipsum."; + + + var bidiff = SideBySideDiffBuilder.Diff( + new Differ(), + textOld, textNew, + detailsPack: null, + ignoreWhiteSpace: false, + ignoreCase: false + ); + + Assert.NotNull(bidiff); + Assert.True(bidiff.OldText.Lines.Count == 8); + Assert.True(bidiff.NewText.Lines.Count == 8); + + Assert.True(bidiff.OldText.HasDifferences && bidiff.NewText.HasDifferences); + + // Check on Line level + var changedOldLines = bidiff.OldText.Lines.Where(x => x.Type != ChangeType.Unchanged).ToList(); + Assert.Equal(5, changedOldLines.Count); + + var changedNewLines = bidiff.NewText.Lines.Where(x => x.Type != ChangeType.Unchanged).ToList(); + Assert.Equal(5, changedNewLines.Count); + + // Check on Word level + var changedOldWords = changedOldLines[0].SubPieces.Where(x => x.Type != ChangeType.Unchanged).ToList(); + Assert.NotNull(changedOldWords); + Assert.True(changedOldWords.Count == 4); + Assert.Equal(ChangeType.Imaginary, changedOldWords[0].Type); + Assert.Null(changedOldWords[0].Text); + Assert.Equal(ChangeType.Imaginary, changedOldWords[1].Type); + Assert.Null(changedOldWords[1].Text); + Assert.Equal(ChangeType.Imaginary, changedOldWords[2].Type); + Assert.Null(changedOldWords[2].Text); + Assert.Equal(ChangeType.Imaginary, changedOldWords[3].Type); + Assert.Null(changedOldWords[3].Text); + + var changedNewWords = changedNewLines[0].SubPieces.Where(x => x.Type != ChangeType.Unchanged).ToList(); + Assert.NotNull(changedNewWords); + Assert.True(changedNewWords.Count == 4); + Assert.Equal(ChangeType.Inserted, changedNewWords[0].Type); + Assert.Equal("the", changedNewWords[0].Text); + Assert.Equal(ChangeType.Inserted, changedNewWords[1].Type); + Assert.Equal(" ", changedNewWords[1].Text); + Assert.Equal(ChangeType.Inserted, changedNewWords[2].Type); + Assert.Equal("heck", changedNewWords[2].Text); + Assert.Equal(ChangeType.Inserted, changedNewWords[3].Type); + Assert.Equal(" ", changedNewWords[3].Text); + + + // Check on Character level + var changedOldChars = changedOldLines[2].SubPieces[30].SubPieces.Where(x => x.Type != ChangeType.Unchanged).ToList(); + Assert.NotNull(changedOldChars); + Assert.Single(changedOldChars); + Assert.Equal(ChangeType.Deleted, changedOldChars[0].Type); + Assert.Equal("e", changedOldChars[0].Text); + + var changedNewChars = changedNewLines[2].SubPieces[30].SubPieces.Where(x => x.Type != ChangeType.Unchanged).ToList(); + Assert.NotNull(changedNewChars); + Assert.Single(changedNewChars); + Assert.Equal(ChangeType.Inserted, changedNewChars[0].Type); + Assert.Equal("a", changedNewChars[0].Text); + } + [Fact] public void Will_ignore_whitespace_by_default_1() {