2 ==============================================================================
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
7 JUCE is an open source library subject to commercial or open-source
10 The code included in this file is provided under the terms of the ISC license
11 http://www.isc.org/downloads/software-support-policy/isc-license. Permission
12 To use, copy, modify, and/or distribute this software for any purpose with or
13 without fee is hereby granted provided that the above copyright notice and
14 this permission notice appear in all copies.
16 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
17 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
20 ==============================================================================
26 struct TextDiffHelpers
28 enum { minLengthToMatch
= 3,
29 maxComplexity
= 16 * 1024 * 1024 };
33 StringRegion (const String
& s
) noexcept
34 : text (s
.getCharPointer()), start (0), length (s
.length()) {}
36 StringRegion (String::CharPointerType t
, int s
, int len
) noexcept
37 : text (t
), start (s
), length (len
) {}
39 void incrementStart() noexcept
{ ++text
; ++start
; --length
; }
41 String::CharPointerType text
;
45 static void addInsertion (TextDiff
& td
, String::CharPointerType text
, int index
, int length
)
48 c
.insertedText
= String (text
, (size_t) length
);
54 static void addDeletion (TextDiff
& td
, int index
, int length
)
62 static void diffSkippingCommonStart (TextDiff
& td
, StringRegion a
, StringRegion b
)
69 if (ca
!= cb
|| ca
== 0)
76 diffRecursively (td
, a
, b
);
79 static void diffRecursively (TextDiff
& td
, StringRegion a
, StringRegion b
)
81 int indexA
= 0, indexB
= 0;
82 auto len
= findLongestCommonSubstring (a
.text
, a
.length
, indexA
,
83 b
.text
, b
.length
, indexB
);
85 if (len
>= minLengthToMatch
)
87 if (indexA
> 0 && indexB
> 0)
88 diffSkippingCommonStart (td
, StringRegion (a
.text
, a
.start
, indexA
),
89 StringRegion (b
.text
, b
.start
, indexB
));
91 addDeletion (td
, b
.start
, indexA
);
93 addInsertion (td
, b
.text
, b
.start
, indexB
);
95 diffRecursively (td
, StringRegion (a
.text
+ (indexA
+ len
), a
.start
+ indexA
+ len
, a
.length
- indexA
- len
),
96 StringRegion (b
.text
+ (indexB
+ len
), b
.start
+ indexB
+ len
, b
.length
- indexB
- len
));
100 if (a
.length
> 0) addDeletion (td
, b
.start
, a
.length
);
101 if (b
.length
> 0) addInsertion (td
, b
.text
, b
.start
, b
.length
);
105 static int findLongestCommonSubstring (String::CharPointerType a
, const int lenA
, int& indexInA
,
106 String::CharPointerType b
, const int lenB
, int& indexInB
) noexcept
108 if (lenA
== 0 || lenB
== 0)
111 if (lenA
* lenB
> maxComplexity
)
112 return findCommonSuffix (a
, lenA
, indexInA
,
115 auto scratchSpace
= sizeof (int) * (2 + 2 * (size_t) lenB
);
117 if (scratchSpace
< 4096)
119 JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6255)
120 auto* scratch
= (int*) alloca (scratchSpace
);
121 JUCE_END_IGNORE_WARNINGS_MSVC
123 return findLongestCommonSubstring (a
, lenA
, indexInA
, b
, lenB
, indexInB
, scratchSpace
, scratch
);
126 HeapBlock
<int> scratch (scratchSpace
);
127 return findLongestCommonSubstring (a
, lenA
, indexInA
, b
, lenB
, indexInB
, scratchSpace
, scratch
);
130 static int findLongestCommonSubstring (String::CharPointerType a
, const int lenA
, int& indexInA
,
131 String::CharPointerType b
, const int lenB
, int& indexInB
,
132 const size_t scratchSpace
, int* const lines
) noexcept
134 zeromem (lines
, scratchSpace
);
137 auto* l1
= l0
+ lenB
+ 1;
139 int loopsWithoutImprovement
= 0;
142 for (int i
= 0; i
< lenA
; ++i
)
144 auto ca
= a
.getAndAdvance();
147 for (int j
= 0; j
< lenB
; ++j
)
149 if (ca
!= b2
.getAndAdvance())
155 auto len
= l0
[j
] + 1;
158 if (len
> bestLength
)
160 loopsWithoutImprovement
= 0;
168 if (++loopsWithoutImprovement
> 100)
174 indexInA
-= bestLength
- 1;
175 indexInB
-= bestLength
- 1;
179 static int findCommonSuffix (String::CharPointerType a
, int lenA
, int& indexInA
,
180 String::CharPointerType b
, int lenB
, int& indexInB
) noexcept
186 while (length
< lenA
&& length
< lenB
&& *a
== *b
)
193 indexInA
= lenA
- length
;
194 indexInB
= lenB
- length
;
199 TextDiff::TextDiff (const String
& original
, const String
& target
)
201 TextDiffHelpers::diffSkippingCommonStart (*this, original
, target
);
204 String
TextDiff::appliedTo (String text
) const
206 for (auto& c
: changes
)
207 text
= c
.appliedTo (text
);
212 bool TextDiff::Change::isDeletion() const noexcept
214 return insertedText
.isEmpty();
217 String
TextDiff::Change::appliedTo (const String
& text
) const noexcept
219 return text
.replaceSection (start
, length
, insertedText
);
223 //==============================================================================
224 //==============================================================================
227 class DiffTests
: public UnitTest
231 : UnitTest ("TextDiff class", UnitTestCategories::text
)
234 static String
createString (Random
& r
)
236 juce_wchar buffer
[500] = { 0 };
238 for (int i
= r
.nextInt (numElementsInArray (buffer
) - 1); --i
>= 0;)
240 if (r
.nextInt (10) == 0)
244 buffer
[i
] = (juce_wchar
) (1 + r
.nextInt (0x10ffff - 1));
246 while (! CharPointer_UTF16::canRepresent (buffer
[i
]));
249 buffer
[i
] = (juce_wchar
) ('a' + r
.nextInt (3));
252 return CharPointer_UTF32 (buffer
);
255 void testDiff (const String
& a
, const String
& b
)
257 TextDiff
diff (a
, b
);
258 auto result
= diff
.appliedTo (a
);
259 expectEquals (result
, b
);
262 void runTest() override
264 beginTest ("TextDiff");
266 auto r
= getRandom();
268 testDiff (String(), String());
269 testDiff ("x", String());
270 testDiff (String(), "x");
273 testDiff ("xxx", "x");
274 testDiff ("x", "xxx");
276 for (int i
= 1000; --i
>= 0;)
278 auto s
= createString (r
);
279 testDiff (s
, createString (r
));
280 testDiff (s
+ createString (r
), s
+ createString (r
));
285 static DiffTests diffTests
;