DiffPatch.cs 7.1 KB

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