diff --git a/projects/GKCore/GKCore.csproj b/projects/GKCore/GKCore.csproj
index aad5d8f3f..034fa9d5c 100644
--- a/projects/GKCore/GKCore.csproj
+++ b/projects/GKCore/GKCore.csproj
@@ -376,6 +376,7 @@
+
diff --git a/projects/GKCore/GKCore/NetDiff/DiffUtil.cs b/projects/GKCore/GKCore/NetDiff/DiffUtil.cs
new file mode 100644
index 000000000..804678743
--- /dev/null
+++ b/projects/GKCore/GKCore/NetDiff/DiffUtil.cs
@@ -0,0 +1,474 @@
+/*
+ * NetDiff (Diff4Net), v1.2.0.0
+ * This is the C# implementation of the Diff algorithm.
+ * Copyright © 2017 by skanmera
+ * https://github.com/skanmera/NetDiff
+ * License: MIT License (http://opensource.org/licenses/MIT)
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace GKCore.NetDiff
+{
+ public enum DiffStatus
+ {
+ Equal,
+ Deleted,
+ Inserted,
+ Modified,
+ }
+
+
+ public class DiffResult
+ {
+ public T Obj1 { get; private set; }
+ public T Obj2 { get; private set; }
+ public DiffStatus Status { get; private set; }
+
+ public DiffResult(T obj1, T obj2, DiffStatus status)
+ {
+ Obj1 = obj1;
+ Obj2 = obj2;
+ Status = status;
+ }
+
+ public override string ToString()
+ {
+ var obj = Status != DiffStatus.Inserted ? Obj1 : Obj2;
+ return string.Format("{0} {1}", DiffUtil.GetStatusChar(Status), obj);
+ }
+ }
+
+
+ public class DiffOption
+ {
+ ///
+ /// Specify IEqualityComparer to be used for comparing equality.
+ ///
+ public IEqualityComparer EqualityComparer { get; set; }
+
+ ///
+ /// Specify the maximum number of nodes that can exist at once at the edit graph.
+ /// The lower the number, the better the performance, but the redundant differences increase.
+ /// The default is 1000.
+ ///
+ public int Limit { get; set; }
+ }
+
+
+ public class DiffUtil
+ {
+ public static IEnumerable> Diff(IEnumerable seq1, IEnumerable seq2)
+ {
+ return Diff(seq1, seq2, new DiffOption());
+ }
+
+ public static IEnumerable> Diff(IEnumerable seq1, IEnumerable seq2, DiffOption option)
+ {
+ if (seq1 == null || seq2 == null || (!seq1.Any() && !seq2.Any()))
+ return Enumerable.Empty>();
+
+ var editGrap = new EditGraph(seq1, seq2);
+ var waypoints = editGrap.CalculatePath(option);
+
+ return MakeResults(waypoints, seq1, seq2);
+ }
+
+ public static IEnumerable CreateSrc(IEnumerable> diffResults)
+ {
+ return diffResults.Where(r => r.Status != DiffStatus.Inserted).Select(r => r.Obj1);
+ }
+
+ public static IEnumerable CreateDst(IEnumerable> diffResults)
+ {
+ return diffResults.Where(r => r.Status != DiffStatus.Deleted).Select(r => r.Obj2);
+ }
+
+ public static IEnumerable> OptimizeCaseDeletedFirst(IEnumerable> diffResults)
+ {
+ return Optimize(diffResults, true);
+ }
+
+ public static IEnumerable> OptimizeCaseInsertedFirst(IEnumerable> diffResults)
+ {
+ return Optimize(diffResults, false);
+ }
+
+ private static IEnumerable> Optimize(IEnumerable> diffResults, bool deleteFirst = true)
+ {
+ var currentStatus = deleteFirst ? DiffStatus.Deleted : DiffStatus.Inserted;
+ var nextStatus = deleteFirst ? DiffStatus.Inserted : DiffStatus.Deleted;
+
+ var queue = new Queue>(diffResults);
+ while (queue.Any())
+ {
+ var result = queue.Dequeue();
+ if (result.Status == currentStatus)
+ {
+ if (queue.Any() && queue.Peek().Status == nextStatus)
+ {
+ var obj1 = deleteFirst ? result.Obj1 : queue.Dequeue().Obj1;
+ var obj2 = deleteFirst ? queue.Dequeue().Obj2 : result.Obj2;
+ yield return new DiffResult(obj1, obj2, DiffStatus.Modified);
+ }
+ else
+ yield return result;
+
+ continue;
+ }
+
+ yield return result;
+ }
+ }
+
+ private static IEnumerable> MakeResults(IEnumerable waypoints, IEnumerable seq1, IEnumerable seq2)
+ {
+ var array1 = seq1.ToArray();
+ var array2 = seq2.ToArray();
+
+ foreach (var pair in MakePairsWithNext(waypoints))
+ {
+ var status = GetStatus(pair.Item1, pair.Item2);
+ T obj1 = default(T);
+ T obj2 = default(T);
+ switch (status)
+ {
+ case DiffStatus.Equal:
+ obj1 = array1[pair.Item2.X - 1];
+ obj2 = array2[pair.Item2.Y - 1];
+ break;
+ case DiffStatus.Inserted:
+ obj2 = array2[pair.Item2.Y - 1];
+ break;
+ case DiffStatus.Deleted:
+ obj1 = array1[pair.Item2.X - 1];
+ break;
+ }
+
+ yield return new DiffResult(obj1, obj2, status);
+ }
+ }
+
+ private static IEnumerable> MakePairsWithNext(IEnumerable source)
+ {
+ using (var enumerator = source.GetEnumerator())
+ {
+ if (enumerator.MoveNext())
+ {
+ var previous = enumerator.Current;
+ while (enumerator.MoveNext())
+ {
+ var current = enumerator.Current;
+
+ yield return new Tuple(previous, current);
+
+ previous = current;
+ }
+ }
+ }
+ }
+
+ private static DiffStatus GetStatus(Point current, Point prev)
+ {
+ if (current.X != prev.X && current.Y != prev.Y)
+ return DiffStatus.Equal;
+ else if (current.X != prev.X)
+ return DiffStatus.Deleted;
+ else if (current.Y != prev.Y)
+ return DiffStatus.Inserted;
+ else
+ throw new Exception();
+ }
+
+ internal static char GetStatusChar(DiffStatus status)
+ {
+ switch (status)
+ {
+ case DiffStatus.Equal: return '=';
+ case DiffStatus.Deleted: return '-';
+ case DiffStatus.Inserted: return '+';
+ case DiffStatus.Modified: return 'M';
+ }
+
+ throw new System.Exception();
+ }
+
+ #region EditGraph
+
+ internal enum Direction
+ {
+ Right,
+ Bottom,
+ Diagonal,
+ }
+
+ internal struct Point : IEquatable
+ {
+ public int X { get; private set; }
+ public int Y { get; private set; }
+
+ public Point(int x, int y)
+ {
+ X = x;
+ Y = y;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (!(obj is Point))
+ return false;
+
+ return Equals((Point)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ var hash = 17;
+ hash = hash * 23 + X.GetHashCode();
+ hash = hash * 23 + Y.GetHashCode();
+
+ return hash;
+ }
+
+ public bool Equals(Point other)
+ {
+ return X == other.X && Y == other.Y;
+ }
+
+ public override string ToString()
+ {
+ return string.Format("X:{0} Y:{1}", X, Y);
+ }
+ }
+
+ internal class Node
+ {
+ public Point Point { get; set; }
+ public Node Parent { get; set; }
+
+ public Node(Point point)
+ {
+ Point = point;
+ }
+
+ public override string ToString()
+ {
+ return string.Format("X:{0} Y:{1}", Point.X, Point.Y);
+ }
+ }
+
+ internal class EditGraph
+ {
+ private T[] seq1;
+ private T[] seq2;
+ private DiffOption option;
+ private List heads;
+ private Point endpoint;
+ private int[] farthestPoints;
+ private int offset;
+ private bool isEnd;
+
+ public EditGraph(
+ IEnumerable seq1, IEnumerable seq2)
+ {
+ this.seq1 = seq1.ToArray();
+ this.seq2 = seq2.ToArray();
+ endpoint = new Point(this.seq1.Length, this.seq2.Length);
+ offset = this.seq2.Length;
+ }
+
+ public List CalculatePath(DiffOption option)
+ {
+ if (!seq1.Any())
+ return Enumerable.Range(0, seq2.Length + 1).Select(i => new Point(0, i)).ToList();
+
+ if (!seq2.Any())
+ return Enumerable.Range(0, seq1.Length + 1).Select(i => new Point(i, 0)).ToList();
+
+ this.option = option;
+
+ BeginCalculatePath();
+
+ while (Next()) { }
+
+ return EndCalculatePath();
+ }
+
+ private void Initialize()
+ {
+ farthestPoints = new int[seq1.Length + seq2.Length + 1];
+ heads = new List();
+ }
+
+ private void BeginCalculatePath()
+ {
+ Initialize();
+
+ heads.Add(new Node(new Point(0, 0)));
+
+ Snake();
+ }
+
+ private List EndCalculatePath()
+ {
+ var wayponit = new List();
+
+ var current = heads.Where(h => h.Point.Equals(endpoint)).FirstOrDefault();
+ while (current != null)
+ {
+ wayponit.Add(current.Point);
+
+ current = current.Parent;
+ }
+
+ wayponit.Reverse();
+
+ return wayponit;
+ }
+
+ private bool Next()
+ {
+ if (isEnd)
+ return false;
+
+ UpdateHeads();
+
+ return true;
+ }
+
+ private void UpdateHeads()
+ {
+ if (option.Limit > 0 && heads.Count > option.Limit)
+ {
+ var tmp = heads.First();
+ heads.Clear();
+
+ heads.Add(tmp);
+ }
+
+ var updated = new List();
+
+ foreach (var head in heads)
+ {
+ Node rightHead;
+ if (TryCreateHead(head, Direction.Right, out rightHead))
+ {
+ updated.Add(rightHead);
+ }
+
+ Node bottomHead;
+ if (TryCreateHead(head, Direction.Bottom, out bottomHead))
+ {
+ updated.Add(bottomHead);
+ }
+ }
+
+ heads = updated;
+
+ Snake();
+ }
+
+ private void Snake()
+ {
+ var tmp = new List();
+ foreach (var h in heads)
+ {
+ var newHead = Snake(h);
+
+ if (newHead != null)
+ tmp.Add(newHead);
+ else
+ tmp.Add(h);
+ }
+
+ heads = tmp;
+ }
+
+ private Node Snake(Node head)
+ {
+ Node newHead = null;
+ while (true)
+ {
+ Node tmp;
+ if (TryCreateHead(newHead ?? head, Direction.Diagonal, out tmp))
+ newHead = tmp;
+ else
+ break;
+ }
+
+ return newHead;
+ }
+
+ private bool TryCreateHead(Node head, Direction direction, out Node newHead)
+ {
+ newHead = null;
+ var newPoint = GetPoint(head.Point, direction);
+
+ if (!CanCreateHead(head.Point, direction, newPoint))
+ return false;
+
+ newHead = new Node(newPoint);
+ newHead.Parent = head;
+
+ isEnd |= newHead.Point.Equals(endpoint);
+
+ return true;
+ }
+
+ private bool CanCreateHead(Point currentPoint, Direction direction, Point nextPoint)
+ {
+ if (!InRange(nextPoint))
+ return false;
+
+ if (direction == Direction.Diagonal)
+ {
+ var equal = option.EqualityComparer != null
+ ? option.EqualityComparer.Equals(seq1[nextPoint.X - 1], (seq2[nextPoint.Y - 1]))
+ : seq1[nextPoint.X - 1].Equals(seq2[nextPoint.Y - 1]);
+
+ if (!equal)
+ return false;
+ }
+
+ return UpdateFarthestPoint(nextPoint);
+ }
+
+ private Point GetPoint(Point currentPoint, Direction direction)
+ {
+ switch (direction)
+ {
+ case Direction.Right:
+ return new Point(currentPoint.X + 1, currentPoint.Y);
+ case Direction.Bottom:
+ return new Point(currentPoint.X, currentPoint.Y + 1);
+ case Direction.Diagonal:
+ return new Point(currentPoint.X + 1, currentPoint.Y + 1);
+ }
+
+ throw new ArgumentException();
+ }
+
+ private bool InRange(Point point)
+ {
+ return point.X >= 0 && point.Y >= 0 && point.X <= endpoint.X && point.Y <= endpoint.Y;
+ }
+
+ private bool UpdateFarthestPoint(Point point)
+ {
+ var k = point.X - point.Y;
+ var y = farthestPoints[k + offset];
+
+ if (point.Y <= y)
+ return false;
+
+ farthestPoints[k + offset] = point.Y;
+
+ return true;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/projects/GKTests/GKCore/NetDiffTests.cs b/projects/GKTests/GKCore/NetDiffTests.cs
new file mode 100644
index 000000000..ac05af536
--- /dev/null
+++ b/projects/GKTests/GKCore/NetDiffTests.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+
+namespace GKCore.NetDiff
+{
+ [TestFixture]
+ public class NetDiffTests
+ {
+ [Test]
+ public void StdCase_General()
+ {
+ var str1 = "string";
+ var str2 = "strength";
+
+ var results = DiffUtil.Diff(str1, str2).ToList();
+
+ Assert.AreEqual(9, results.Count());
+
+ Assert.AreEqual("= s", results[0].ToString());
+ Assert.AreEqual("= t", results[1].ToString());
+ Assert.AreEqual("= r", results[2].ToString());
+ Assert.AreEqual("- i", results[3].ToString());
+ Assert.AreEqual("+ e", results[4].ToString());
+ Assert.AreEqual("= n", results[5].ToString());
+ Assert.AreEqual("= g", results[6].ToString());
+ Assert.AreEqual("+ t", results[7].ToString());
+ Assert.AreEqual("+ h", results[8].ToString());
+ }
+
+ [Test]
+ public void StdCase_Equal()
+ {
+ var str1 = "abcde";
+ var str2 = "abcde";
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.AreEqual(str1.Count(), results.Count());
+ Assert.IsTrue(results.All(r => r.Status == DiffStatus.Equal));
+ }
+
+ /*
+ a b a b a b
+ - + - + - +
+ */
+ [Test]
+ public void StdCase_DifferentAll()
+ {
+ var str1 = "aaa";
+ var str2 = "bbb";
+
+ var results = DiffUtil.Diff(str1, str2).ToList();
+
+ Assert.AreEqual("+ b", results.ElementAt(0).ToString());
+ Assert.AreEqual("- a", results.ElementAt(1).ToString());
+ Assert.AreEqual("- a", results.ElementAt(2).ToString());
+ Assert.AreEqual("- a", results.ElementAt(3).ToString());
+ Assert.AreEqual("+ b", results.ElementAt(4).ToString());
+ Assert.AreEqual("+ b", results.ElementAt(5).ToString());
+ }
+
+ /*
+ a b c d
+ = = = +
+ */
+ [Test]
+ public void StdCase_Appended()
+ {
+ var str1 = "abc";
+ var str2 = "abcd";
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(0).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(1).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(3).Status);
+ }
+
+ /*
+ a b c d
+ + = = =
+ */
+ [Test]
+ public void StdCase_Prepended()
+ {
+ var str1 = "bcd";
+ var str2 = "abcd";
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(0).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(1).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(3).Status);
+ }
+
+ [Test]
+ public void StdCase_CaseMultiSameScore()
+ {
+ var str1 = "cdhijkz";
+ var str2 = "ldxhnokz";
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(0).Status);
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(1).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(3).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(4).Status);
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(5).Status);
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(6).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(7).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(8).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(9).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(10).Status);
+ }
+
+ [Test]
+ public void StdCase_CaseRepeat()
+ {
+ string str1 = "abbbc";
+ string str2 = "adbbc";
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(0).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(1).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(3).Status);
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(4).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(5).Status);
+ }
+
+
+ [Test]
+ public void StdCase_SpecifiedComparer()
+ {
+ var str1 = "abc";
+ var str2 = "dBf";
+
+ var option = new DiffOption();
+ option.EqualityComparer = new CaseInsensitiveComparer();
+
+ var results = DiffUtil.Diff(str1, str2, option).ToList();
+
+ Assert.AreEqual("+ d", results[0].ToString());
+ Assert.AreEqual("- a", results[1].ToString());
+ Assert.AreEqual("= b", results[2].ToString());
+ Assert.AreEqual("- c", results[3].ToString());
+ Assert.AreEqual("+ f", results[4].ToString());
+
+ Assert.AreEqual("abc", new String(DiffUtil.CreateSrc(results).ToArray()));
+ Assert.AreEqual("dBf", new String(DiffUtil.CreateDst(results).ToArray()));
+ }
+
+ [Test]
+ public void StdCase_CaseReplace()
+ {
+ string str1 = "abbbc";
+ string str2 = "adbbc";
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(0).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(1).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(3).Status);
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(4).Status);
+ Assert.AreEqual(DiffStatus.Equal, results.ElementAt(5).Status);
+ }
+
+ [Test]
+ public void BadCase_Seq1Empty()
+ {
+ string str1 = "";
+ string str2 = "abcde";
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(0).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(1).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(2).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(3).Status);
+ Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(4).Status);
+ }
+
+ [Test]
+ public void BadCase_Seq2Empty()
+ {
+ string str1 = "abced";
+ string str2 = "";
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(0).Status);
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(1).Status);
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(2).Status);
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(3).Status);
+ Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(4).Status);
+ }
+
+ [Test]
+ public void BadCase_Empty()
+ {
+ var str1 = string.Empty;
+ var str2 = string.Empty;
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.IsTrue(!results.Any());
+ }
+
+ [Test]
+ public void BadCase_Null()
+ {
+ string str1 = null;
+ var str2 = string.Empty;
+
+ var results = DiffUtil.Diff(str1, str2);
+
+ Assert.IsTrue(!results.Any());
+ }
+
+ internal class CaseInsensitiveComparer : IEqualityComparer
+ {
+ public bool Equals(char x, char y)
+ {
+ return x.ToString().ToLower().Equals(y.ToString().ToLower());
+ }
+
+ public int GetHashCode(char obj)
+ {
+ return obj.ToString().ToLower().GetHashCode();
+ }
+ }
+ }
+}
diff --git a/projects/GKTests/GKTests.csproj b/projects/GKTests/GKTests.csproj
index d28bc6717..a8a35ce9d 100644
--- a/projects/GKTests/GKTests.csproj
+++ b/projects/GKTests/GKTests.csproj
@@ -112,6 +112,7 @@
+