Skip to content

Commit

Permalink
Started adding StringDiff. Added ReadOnlyCrudService. Added InfoDialog.
Browse files Browse the repository at this point in the history
  • Loading branch information
Felix-CodingClimber committed Mar 27, 2024
1 parent d37a92e commit c1fa0b0
Show file tree
Hide file tree
Showing 28 changed files with 1,163 additions and 191 deletions.
2 changes: 1 addition & 1 deletion src/DotNetElements.Core/Core/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public virtual async Task<CrudResult<TSelf>> CreateOrUpdateAsync<TSelf>(TKey id,
public virtual async Task<CrudResult<TEntity>> UpdateAsync<TFrom>(TKey id, TFrom from)
where TFrom : notnull
{
ThrowHelper.ThrowIfDefault(id);
ThrowIf.Default(id);

IQueryable<TEntity> query = LoadRelatedEntitiesOnUpdate(Entities);

Expand Down
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));
}
}
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;
}
}
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([])
{
}
}
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();
}
}
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;
}
}
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];
}
}
Loading

0 comments on commit c1fa0b0

Please sign in to comment.