DiffPatch.cs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. using System.Collections.Immutable;
  2. using System.Text;
  3. using static Masuit.Tools.TextDiff.DiffOperation;
  4. namespace Masuit.Tools.TextDiff;
  5. public record DiffPatch(int Start1, int Length1, int Start2, int Length2, SemanticsImmutableList<TextDiffer> Diffs)
  6. {
  7. public bool IsEmpty => Diffs.IsEmpty;
  8. public DiffPatch Bump(int length) => this with
  9. {
  10. Start1 = Start1 + length,
  11. Start2 = Start2 + length
  12. };
  13. public bool StartsWith(DiffOperation operation) => Diffs[0].Operation == operation;
  14. public bool EndsWith(DiffOperation operation) => Diffs[^1].Operation == operation;
  15. internal DiffPatch AddPadding(string padding)
  16. {
  17. var (s1, l1, s2, l2, diffs) = this;
  18. var builder = diffs.ToBuilder();
  19. (s1, l1, s2, l2) = AddPaddingBegin(builder, s1, l1, s2, l2, padding);
  20. (s1, l1, s2, l2) = AddPaddingEnd(builder, s1, l1, s2, l2, padding);
  21. return new DiffPatch(s1, l1, s2, l2, builder.ToImmutable());
  22. }
  23. internal DiffPatch AddPaddingBegin(string padding)
  24. {
  25. var (s1, l1, s2, l2, diffs) = this;
  26. var builder = diffs.ToBuilder();
  27. (s1, l1, s2, l2) = AddPaddingBegin(builder, s1, l1, s2, l2, padding);
  28. return new DiffPatch(s1, l1, s2, l2, builder.ToImmutable());
  29. }
  30. private (int s1, int l1, int s2, int l2) AddPaddingBegin(ImmutableList<TextDiffer>.Builder builder, int s1, int l1, int s2, int l2, string padding)
  31. {
  32. if (!StartsWith(Equal))
  33. {
  34. builder.Insert(0, TextDiffer.Equal(padding));
  35. return (s1 - padding.Length, l1 + padding.Length, s2 - padding.Length, l2 + padding.Length);
  36. }
  37. if (padding.Length <= Diffs[0].Text.Length)
  38. {
  39. return (s1, l1, s2, l2);
  40. }
  41. var firstDiff = Diffs[0];
  42. var extraLength = padding.Length - firstDiff.Text.Length;
  43. var text = padding[firstDiff.Text.Length..] + firstDiff.Text;
  44. builder.RemoveAt(0);
  45. builder.Insert(0, firstDiff.Replace(text));
  46. return (s1 - extraLength, l1 + extraLength, s2 - extraLength, l2 + extraLength);
  47. }
  48. internal DiffPatch AddPaddingEnd(string padding)
  49. {
  50. var (s1, l1, s2, l2, diffs) = this;
  51. var builder = diffs.ToBuilder();
  52. (s1, l1, s2, l2) = AddPaddingEnd(builder, s1, l1, s2, l2, padding);
  53. return new DiffPatch(s1, l1, s2, l2, builder.ToImmutable());
  54. }
  55. private (int s1, int l1, int s2, int l2) AddPaddingEnd(ImmutableList<TextDiffer>.Builder builder, int s1, int l1, int s2, int l2, string padding)
  56. {
  57. if (!EndsWith(Equal))
  58. {
  59. builder.Add(TextDiffer.Equal(padding));
  60. return (s1, l1 + padding.Length, s2, l2 + padding.Length);
  61. }
  62. if (padding.Length <= Diffs[^1].Text.Length)
  63. {
  64. return (s1, l1, s2, l2);
  65. }
  66. var lastDiff = Diffs[^1];
  67. var extraLength = padding.Length - lastDiff.Text.Length;
  68. var text = lastDiff.Text + padding[..extraLength];
  69. builder.RemoveAt(builder.Count - 1);
  70. builder.Add(lastDiff.Replace(text));
  71. return (s1, l1 + extraLength, s2, l2 + extraLength);
  72. }
  73. /// <summary>
  74. /// 计算一个补丁列表,将text1转换为text2。将计算一组差异
  75. /// </summary>
  76. /// <param name="text1"></param>
  77. /// <param name="text2"></param>
  78. /// <param name="diffTimeout">超时限制</param>
  79. /// <param name="diffEditCost"></param>
  80. /// <returns></returns>
  81. public static SemanticsImmutableList<DiffPatch> Compute(string text1, string text2, float diffTimeout = 0, short diffEditCost = 4)
  82. {
  83. using var cts = diffTimeout <= 0 ? new CancellationTokenSource() : new CancellationTokenSource(TimeSpan.FromSeconds(diffTimeout));
  84. return Compute(text1, DiffAlgorithm.Compute(text1, text2, true, true, cts.Token).CleanupSemantic().CleanupEfficiency(diffEditCost)).ToImmutableList().WithValueSemantics();
  85. }
  86. /// <summary>
  87. /// 计算一个patch列表,将text1转换为text2。不提供text2,Diffs是text1和text2之间的差值
  88. /// </summary>
  89. /// <param name="text1"></param>
  90. /// <param name="diffs"></param>
  91. /// <param name="patchMargin"></param>
  92. /// <returns></returns>
  93. public static IEnumerable<DiffPatch> Compute(string text1, IEnumerable<TextDiffer> diffs, short patchMargin = 4)
  94. {
  95. if (!diffs.Any())
  96. {
  97. yield break;
  98. }
  99. var charCount1 = 0;
  100. var charCount2 = 0;
  101. var prepatchText = text1;
  102. var postpatchText = text1;
  103. var newdiffs = ImmutableList.CreateBuilder<TextDiffer>();
  104. int start1 = 0, length1 = 0, start2 = 0, length2 = 0;
  105. foreach (var aDiff in diffs)
  106. {
  107. if (!newdiffs.Any() && aDiff.Operation != Equal)
  108. {
  109. start1 = charCount1;
  110. start2 = charCount2;
  111. }
  112. switch (aDiff.Operation)
  113. {
  114. case Insert:
  115. newdiffs.Add(aDiff);
  116. length2 += aDiff.Text.Length;
  117. postpatchText = postpatchText.Insert(charCount2, aDiff.Text);
  118. break;
  119. case Delete:
  120. length1 += aDiff.Text.Length;
  121. newdiffs.Add(aDiff);
  122. postpatchText = postpatchText.Remove(charCount2, aDiff.Text.Length);
  123. break;
  124. case Equal:
  125. if (aDiff.Text.Length <= 2 * patchMargin && newdiffs.Any() && aDiff != diffs.Last())
  126. {
  127. newdiffs.Add(aDiff);
  128. length1 += aDiff.Text.Length;
  129. length2 += aDiff.Text.Length;
  130. }
  131. if (aDiff.Text.Length >= 2 * patchMargin)
  132. {
  133. if (newdiffs.Any())
  134. {
  135. (start1, length1, start2, length2) = newdiffs.AddContext(prepatchText, start1, length1, start2, length2);
  136. yield return new DiffPatch(start1, length1, start2, length2, newdiffs.ToImmutable());
  137. start1 = start2 = length1 = length2 = 0;
  138. newdiffs.Clear();
  139. // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff
  140. prepatchText = postpatchText;
  141. charCount1 = charCount2;
  142. }
  143. }
  144. break;
  145. }
  146. if (aDiff.Operation != Insert)
  147. {
  148. charCount1 += aDiff.Text.Length;
  149. }
  150. if (aDiff.Operation != Delete)
  151. {
  152. charCount2 += aDiff.Text.Length;
  153. }
  154. }
  155. if (newdiffs.Any())
  156. {
  157. (start1, length1, start2, length2) = newdiffs.AddContext(prepatchText, start1, length1, start2, length2);
  158. yield return new DiffPatch(start1, length1, start2, length2, newdiffs.ToImmutable());
  159. }
  160. }
  161. /// <summary>
  162. /// 计算一个patch列表,将text1转换为text2。text1将从提供的Diff中导出。
  163. /// </summary>
  164. /// <param name="diffs"></param>
  165. /// <returns></returns>
  166. public static SemanticsImmutableList<DiffPatch> FromDiffs(IEnumerable<TextDiffer> diffs) => Compute(diffs.Text1(), diffs).ToImmutableList().WithValueSemantics();
  167. public override string ToString()
  168. {
  169. var coords1 = Length1 switch
  170. {
  171. 0 => Start1 + ",0",
  172. 1 => Convert.ToString(Start1 + 1),
  173. _ => Start1 + 1 + "," + Length1
  174. };
  175. var coords2 = Length2 switch
  176. {
  177. 0 => Start2 + ",0",
  178. 1 => Convert.ToString(Start2 + 1),
  179. _ => Start2 + 1 + "," + Length2
  180. };
  181. var text = new StringBuilder().Append("@@ -").Append(coords1).Append(" +").Append(coords2).Append(" @@\n");
  182. foreach (var aDiff in Diffs)
  183. {
  184. text.Append((char)aDiff.Operation);
  185. text.Append(aDiff.Text.UrlEncoded()).Append("\n");
  186. }
  187. return text.ToString();
  188. }
  189. }