-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Started adding StringDiff. Added ReadOnlyCrudService. Added InfoDialog.
- Loading branch information
1 parent
d37a92e
commit c1fa0b0
Showing
28 changed files
with
1,163 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
src/DotNetElements.Core/Core/StringDiff/DiffBuilder/InlineDiffBuilder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
namespace DotNetElements.Core.StringDiff; | ||
|
||
public class InlineDiffBuilder | ||
{ | ||
private readonly bool ignoreWhiteSpace; | ||
private readonly bool ignoreCase; | ||
|
||
private readonly LineChunker chunker; | ||
|
||
/// <summary> | ||
/// Creates a new instance of a <see cref="InlineDiffBuilder"/> | ||
/// </summary> | ||
/// <param name="ignoreWhiteSpace"><see langword="true"/> if ignore the white space; otherwise, <see langword="false"/>.</param> | ||
/// <param name="ignoreCase"><see langword="true"/> if case-insensitive; otherwise, <see langword="false"/>.</param> | ||
public InlineDiffBuilder(bool ignoreWhiteSpace = true, bool ignoreCase = false) | ||
{ | ||
this.ignoreWhiteSpace = ignoreWhiteSpace; | ||
this.ignoreCase = ignoreCase; | ||
|
||
chunker = new LineChunker(); | ||
} | ||
|
||
/// <summary> | ||
/// Gets the inline textual diffs. | ||
/// </summary> | ||
/// <param name="differ">The differ instance.</param> | ||
/// <param name="oldText">The old text to diff.</param> | ||
/// <param name="newText">The new text.</param> | ||
/// <param name="ignoreWhiteSpace"><see langword="true"/> if ignore the white space; otherwise, <see langword="false"/>.</param> | ||
/// <param name="ignoreCase"><see langword="true"/> if case-insensitive; otherwise, <see langword="false"/>.</param> | ||
/// <param name="chunker">The chunker.</param> | ||
/// <returns>The diffs result.</returns> | ||
public InlineDiffModel Diff(string oldText, string newText) | ||
{ | ||
ArgumentNullException.ThrowIfNull(oldText); | ||
ArgumentNullException.ThrowIfNull(newText); | ||
|
||
InlineDiffModel model = new InlineDiffModel(); | ||
DiffResult diffResult = Differ.CreateDiffs(oldText, newText, ignoreWhiteSpace, ignoreCase, chunker); | ||
BuildDiffPieces(diffResult, model.Lines); | ||
|
||
return model; | ||
} | ||
|
||
private static void BuildDiffPieces(DiffResult diffResult, List<DiffPiece> pieces) | ||
{ | ||
int bPos = 0; | ||
|
||
foreach (var diffBlock in diffResult.DiffBlocks) | ||
{ | ||
for (; bPos < diffBlock.InsertStartB; bPos++) | ||
pieces.Add(new DiffPiece(diffResult.PiecesNew[bPos], ChangeType.Unchanged, bPos + 1)); | ||
|
||
int i = 0; | ||
for (; i < Math.Min(diffBlock.DeleteCountA, diffBlock.InsertCountB); i++) | ||
pieces.Add(new DiffPiece(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted)); | ||
|
||
for (i = 0; i < Math.Min(diffBlock.DeleteCountA, diffBlock.InsertCountB); i++) | ||
{ | ||
pieces.Add(new DiffPiece(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1)); | ||
bPos++; | ||
} | ||
|
||
if (diffBlock.DeleteCountA > diffBlock.InsertCountB) | ||
{ | ||
for (; i < diffBlock.DeleteCountA; i++) | ||
pieces.Add(new DiffPiece(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted)); | ||
} | ||
else | ||
{ | ||
for (; i < diffBlock.InsertCountB; i++) | ||
{ | ||
pieces.Add(new DiffPiece(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1)); | ||
bPos++; | ||
} | ||
} | ||
} | ||
|
||
for (; bPos < diffResult.PiecesNew.Length; bPos++) | ||
pieces.Add(new DiffPiece(diffResult.PiecesNew[bPos], ChangeType.Unchanged, bPos + 1)); | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/DiffPiece.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
namespace DotNetElements.Core.StringDiff; | ||
|
||
public enum ChangeType | ||
{ | ||
Unchanged, | ||
Deleted, | ||
Inserted, | ||
Imaginary, | ||
Modified | ||
} | ||
|
||
public class DiffPiece : IEquatable<DiffPiece> | ||
{ | ||
public ChangeType Type { get; set; } | ||
public int? Position { get; set; } | ||
public string? Text { get; set; } | ||
public List<DiffPiece> SubPieces { get; set; } = []; | ||
|
||
public DiffPiece(string? text, ChangeType type, int? position = null) | ||
{ | ||
Text = text; | ||
Position = position; | ||
Type = type; | ||
} | ||
|
||
public DiffPiece() : this(null, ChangeType.Imaginary) | ||
{ | ||
} | ||
|
||
public override bool Equals(object? obj) | ||
{ | ||
return Equals(obj as DiffPiece); | ||
} | ||
|
||
public bool Equals(DiffPiece? other) | ||
{ | ||
return other != null | ||
&& Type == other.Type | ||
&& EqualityComparer<int?>.Default.Equals(Position, other.Position) | ||
&& Text == other.Text | ||
&& SubPiecesEqual(other); | ||
} | ||
|
||
public override int GetHashCode() | ||
{ | ||
ArgumentNullException.ThrowIfNull(Position); | ||
ArgumentNullException.ThrowIfNull(Text); | ||
ArgumentNullException.ThrowIfNull(SubPieces); | ||
|
||
var hashCode = 1688038063; | ||
hashCode = hashCode * -1521134295 + Type.GetHashCode(); | ||
hashCode = hashCode * -1521134295 + EqualityComparer<int?>.Default.GetHashCode(Position); | ||
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Text); | ||
hashCode = hashCode * -1521134295 + EqualityComparer<int?>.Default.GetHashCode(SubPieces.Count); | ||
return hashCode; | ||
} | ||
|
||
private bool SubPiecesEqual(DiffPiece other) | ||
{ | ||
if (SubPieces is null) | ||
return other.SubPieces is null; | ||
else if (other.SubPieces is null) | ||
return false; | ||
|
||
if (SubPieces.Count != other.SubPieces.Count) | ||
return false; | ||
|
||
for (int i = 0; i < SubPieces.Count; i++) | ||
{ | ||
if (!Equals(SubPieces[i], other.SubPieces[i])) | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/InlineDiffModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
namespace DotNetElements.Core.StringDiff; | ||
|
||
public record class InlineDiffModel(List<DiffPiece> Lines) | ||
{ | ||
public bool HasDifferences => Lines.Any(x => x.Type != ChangeType.Unchanged); | ||
|
||
public InlineDiffModel() : this([]) | ||
{ | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
src/DotNetElements.Core/Core/StringDiff/DiffBuilder/Model/SideBySideDiffModel.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
namespace DotNetElements.Core.StringDiff; | ||
|
||
/// <summary> | ||
/// A model which represents differences between to texts to be shown side by side | ||
/// </summary> | ||
public class SideBySideDiffModel | ||
{ | ||
public InlineDiffModel OldText { get; } | ||
public InlineDiffModel NewText { get; } | ||
|
||
public SideBySideDiffModel() | ||
{ | ||
OldText = new InlineDiffModel(); | ||
NewText = new InlineDiffModel(); | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
src/DotNetElements.Core/Core/StringDiff/DiffBuilder/SideBySideDiffBuilder.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
namespace DotNetElements.Core.StringDiff; | ||
|
||
public class SideBySideDiffBuilder | ||
{ | ||
private delegate ChangeType PieceBuilder(string oldText, string newText, List<DiffPiece> oldPieces, List<DiffPiece> newPieces, bool ignoreWhitespace, bool ignoreCase); | ||
|
||
private readonly bool ignoreWhiteSpace; | ||
private readonly bool ignoreCase; | ||
|
||
private readonly LineChunker lineChunker; | ||
private readonly WordChunker wordChunker; | ||
|
||
/// <summary> | ||
/// Creates a new instance of a <see cref="SideBySideDiffBuilder"/> | ||
/// </summary> | ||
/// <param name="ignoreWhiteSpace"><see langword="true"/> if ignore the white space; otherwise, <see langword="false"/>.</param> | ||
/// <param name="ignoreCase"><see langword="true"/> if case-insensitive; otherwise, <see langword="false"/>.</param> | ||
public SideBySideDiffBuilder(bool ignoreWhiteSpace = false, bool ignoreCase = false) | ||
{ | ||
this.ignoreWhiteSpace = ignoreWhiteSpace; | ||
this.ignoreCase = ignoreCase; | ||
|
||
lineChunker = new LineChunker(); | ||
wordChunker = new WordChunker(); | ||
} | ||
|
||
/// <summary> | ||
/// Gets the side-by-side textual diffs. | ||
/// </summary> | ||
/// <param name="oldText">The old text to diff.</param> | ||
/// <param name="newText">The new text.</param> | ||
/// <returns>The diffs result.</returns> | ||
public SideBySideDiffModel Diff(string oldText, string newText) | ||
{ | ||
ArgumentNullException.ThrowIfNull(oldText); | ||
ArgumentNullException.ThrowIfNull(newText); | ||
|
||
SideBySideDiffModel model = new(); | ||
DiffResult diffResult = Differ.CreateDiffs(oldText, newText, ignoreWhiteSpace, ignoreCase, lineChunker); | ||
BuildDiffPieces(diffResult, model.OldText.Lines, model.NewText.Lines, BuildWordDiffPieces, ignoreWhiteSpace, ignoreCase); | ||
|
||
return model; | ||
} | ||
|
||
private ChangeType BuildWordDiffPieces(string oldText, string newText, List<DiffPiece> oldPieces, List<DiffPiece> newPieces, bool ignoreWhiteSpace, bool ignoreCase) | ||
{ | ||
DiffResult diffResult = Differ.CreateDiffs(oldText, newText, ignoreWhiteSpace: ignoreWhiteSpace, ignoreCase, wordChunker); | ||
|
||
return BuildDiffPieces(diffResult, oldPieces, newPieces, subPieceBuilder: null, ignoreWhiteSpace, ignoreCase); | ||
} | ||
|
||
private static ChangeType BuildDiffPieces(DiffResult diffResult, List<DiffPiece> oldPieces, List<DiffPiece> newPieces, PieceBuilder? subPieceBuilder, bool ignoreWhiteSpace, bool ignoreCase) | ||
{ | ||
int aPos = 0; | ||
int bPos = 0; | ||
|
||
ChangeType changeSummary = ChangeType.Unchanged; | ||
|
||
foreach (DiffBlock diffBlock in diffResult.DiffBlocks) | ||
{ | ||
while (bPos < diffBlock.InsertStartB && aPos < diffBlock.DeleteStartA) | ||
{ | ||
oldPieces.Add(new DiffPiece(diffResult.PiecesOld[aPos], ChangeType.Unchanged, aPos + 1)); | ||
newPieces.Add(new DiffPiece(diffResult.PiecesNew[bPos], ChangeType.Unchanged, bPos + 1)); | ||
aPos++; | ||
bPos++; | ||
} | ||
|
||
int i = 0; | ||
for (; i < Math.Min(diffBlock.DeleteCountA, diffBlock.InsertCountB); i++) | ||
{ | ||
DiffPiece oldPiece = new(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted, aPos + 1); | ||
DiffPiece newPiece = new(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1); | ||
|
||
if (subPieceBuilder is not null) | ||
{ | ||
ChangeType subChangeSummary = subPieceBuilder(diffResult.PiecesOld[aPos], diffResult.PiecesNew[bPos], oldPiece.SubPieces, newPiece.SubPieces, ignoreWhiteSpace, ignoreCase); | ||
newPiece.Type = oldPiece.Type = subChangeSummary; | ||
} | ||
|
||
oldPieces.Add(oldPiece); | ||
newPieces.Add(newPiece); | ||
aPos++; | ||
bPos++; | ||
} | ||
|
||
if (diffBlock.DeleteCountA > diffBlock.InsertCountB) | ||
{ | ||
for (; i < diffBlock.DeleteCountA; i++) | ||
{ | ||
oldPieces.Add(new DiffPiece(diffResult.PiecesOld[i + diffBlock.DeleteStartA], ChangeType.Deleted, aPos + 1)); | ||
newPieces.Add(new DiffPiece()); | ||
aPos++; | ||
} | ||
} | ||
else | ||
{ | ||
for (; i < diffBlock.InsertCountB; i++) | ||
{ | ||
newPieces.Add(new DiffPiece(diffResult.PiecesNew[i + diffBlock.InsertStartB], ChangeType.Inserted, bPos + 1)); | ||
oldPieces.Add(new DiffPiece()); | ||
bPos++; | ||
} | ||
} | ||
} | ||
|
||
while (bPos < diffResult.PiecesNew.Length && aPos < diffResult.PiecesOld.Length) | ||
{ | ||
oldPieces.Add(new DiffPiece(diffResult.PiecesOld[aPos], ChangeType.Unchanged, aPos + 1)); | ||
newPieces.Add(new DiffPiece(diffResult.PiecesNew[bPos], ChangeType.Unchanged, bPos + 1)); | ||
aPos++; | ||
bPos++; | ||
} | ||
|
||
// Consider the whole diff as "modified" if we found any change, otherwise we consider it unchanged | ||
if (oldPieces.Any(x => x.Type is ChangeType.Modified or ChangeType.Inserted or ChangeType.Deleted)) | ||
changeSummary = ChangeType.Modified; | ||
else if (newPieces.Any(x => x.Type is ChangeType.Modified or ChangeType.Inserted or ChangeType.Deleted)) | ||
changeSummary = ChangeType.Modified; | ||
|
||
return changeSummary; | ||
} | ||
} |
70 changes: 70 additions & 0 deletions
70
src/DotNetElements.Core/Core/StringDiff/Internal/Chunkers/DelimiterChunker.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
namespace DotNetElements.Core.StringDiff; | ||
|
||
internal class DelimiterChunker : IChunker | ||
{ | ||
private readonly char[] delimiters; | ||
|
||
public DelimiterChunker(char[] delimiters) | ||
{ | ||
ThrowIf.CollectionIsNullOrEmpty(delimiters); | ||
|
||
this.delimiters = delimiters; | ||
} | ||
|
||
public string[] Chunk(string str) | ||
{ | ||
List<string> list = []; | ||
int begin = 0; | ||
bool processingDelimiter = false; | ||
int delimiterBegin = 0; | ||
|
||
for (int i = 0; i < str.Length; i++) | ||
{ | ||
if (Array.IndexOf(delimiters, str[i]) != -1) | ||
{ | ||
if (i >= str.Length - 1) | ||
{ | ||
if (processingDelimiter) | ||
{ | ||
list.Add(str[delimiterBegin..(i + 1)]); | ||
} | ||
else | ||
{ | ||
list.Add(str[begin..i]); | ||
list.Add(str.Substring(i, 1)); | ||
} | ||
} | ||
else | ||
{ | ||
if (!processingDelimiter) | ||
{ | ||
// Add everything up to this delimiter as the next chunk (if there is anything) | ||
if (i - begin > 0) | ||
list.Add(str[begin..i]); | ||
|
||
processingDelimiter = true; | ||
delimiterBegin = i; | ||
} | ||
} | ||
|
||
begin = i + 1; | ||
} | ||
else | ||
{ | ||
if (processingDelimiter) | ||
{ | ||
if (i - delimiterBegin > 0) | ||
list.Add(str[delimiterBegin..i]); | ||
|
||
processingDelimiter = false; | ||
} | ||
|
||
// If we are at the end, add the remaining as the last chunk | ||
if (i >= str.Length - 1) | ||
list.Add(str[begin..(i + 1)]); | ||
} | ||
} | ||
|
||
return [.. list]; | ||
} | ||
} |
Oops, something went wrong.