Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmldiffer.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5// This code is adapted from QtCreator/src/libs/utils/differ.
6
7/*
8The main algorithm "diffMyers()" is based on "An O(ND) Difference Algorithm
9and Its Variations" by Eugene W. Myers: http://www.xmailserver.org/diff2.pdf
10
11Preprocessing and postprocessing functions inspired by "Diff Strategies"
12publication by Neil Fraser: http://neil.fraser.name/writing/diff/
13*/
14
15#include "qqmldiffer_p.h"
16
17#include <QStringList>
18
20
21using namespace Qt::StringLiterals;
22namespace QQmlLSUtils {
23
24static qsizetype commonPrefix(const QString &text1, const QString &text2)
25{
26 // mismatch returns first non-equal pair
27 const auto [it1, it2] = std::mismatch(text1.cbegin(), text1.cend(),
28 text2.cbegin(), text2.cend());
29
30 return qsizetype(std::distance(text1.cbegin(), it1)); // number of matching characters
31}
32
33static qsizetype commonSuffix(const QString &text1, const QString &text2)
34{
35 // mismatch returns first non-equal pair
36 const auto [it1, it2] = std::mismatch(text1.crbegin(), text1.crend(),
37 text2.crbegin(), text2.crend());
38
39 return qsizetype(std::distance(text1.crbegin(), it1)); // number of matching characters
40}
41
42static QList<Diff> decode(const QList<Diff> &diffList, const QStringList &lines)
43{
44 QList<Diff> newDiffList;
45 newDiffList.reserve(diffList.size());
46 for (const Diff &diff : diffList) {
47 QString text;
48 for (QChar c : diff.text) {
49 const qsizetype idx = static_cast<ushort>(c.unicode());
50 text += lines.value(idx);
51 }
52 newDiffList.append({diff.command, text});
53 }
54 return newDiffList;
55}
56
57static QList<Diff> squashEqualities(const QList<Diff> &diffList)
58{
59 if (diffList.size() < 3) // we need at least 3 items
60 return diffList;
61
62 QList<Diff> newDiffList;
63 Diff prevDiff = diffList.at(0);
64 Diff thisDiff = diffList.at(1);
65 Diff nextDiff = diffList.at(2);
66 qsizetype i = 2;
67 while (i < diffList.size()) {
68 if (prevDiff.command == Diff::Equal
69 && nextDiff.command == Diff::Equal) {
70 if (thisDiff.text.endsWith(prevDiff.text)) {
71 thisDiff.text = prevDiff.text
72 + thisDiff.text.left(thisDiff.text.size()
73 - prevDiff.text.size());
74 nextDiff.text = prevDiff.text + nextDiff.text;
75 } else if (thisDiff.text.startsWith(nextDiff.text)) {
76 prevDiff.text += nextDiff.text;
77 thisDiff.text = thisDiff.text.mid(nextDiff.text.size())
78 + nextDiff.text;
79 i++;
80 if (i < diffList.size())
81 nextDiff = diffList.at(i);
82 newDiffList.append(prevDiff);
83 } else {
84 newDiffList.append(prevDiff);
85 }
86 } else {
87 newDiffList.append(prevDiff);
88 }
89 prevDiff = thisDiff;
90 thisDiff = nextDiff;
91 i++;
92 if (i < diffList.size())
93 nextDiff = diffList.at(i);
94 }
95 newDiffList.append(prevDiff);
96 if (i == diffList.size())
97 newDiffList.append(thisDiff);
98 return newDiffList;
99}
100
101///////////////
102
103Diff::Diff(Command com, const QString &txt) :
104 command(com),
105 text(txt)
106{
107}
108
109bool Diff::operator==(const Diff &other) const
110{
111 return command == other.command && text == other.text;
112}
113
114bool Diff::operator!=(const Diff &other) const
115{
116 return !(operator == (other));
117}
118
119///////////////
120
122{
123}
124
125QList<Diff> Differ::diff(const QString &text1, const QString &text2)
126{
127 m_currentDiffMode = m_diffMode;
128 return merge(preprocess1AndDiff(text1, text2));
129}
130
132{
133 m_diffMode = mode;
134}
135
137{
138 return m_diffMode;
139}
140
141QList<Diff> Differ::preprocess1AndDiff(const QString &text1, const QString &text2)
142{
143 if (text1.isNull() && text2.isNull())
144 return {};
145
146 if (text1 == text2) {
147 QList<Diff> diffList;
148 if (!text1.isEmpty())
149 diffList.append(Diff(Diff::Equal, text1));
150 return diffList;
151 }
152
153 QString newText1 = text1;
154 QString newText2 = text2;
155 QString prefix;
156 QString suffix;
157 const qsizetype prefixCount = commonPrefix(text1, text2);
158 if (prefixCount) {
159 prefix = text1.left(prefixCount);
160 newText1 = text1.mid(prefixCount);
161 newText2 = text2.mid(prefixCount);
162 }
163 const qsizetype suffixCount = commonSuffix(newText1, newText2);
164 if (suffixCount) {
165 suffix = newText1.right(suffixCount);
166 newText1 = newText1.left(newText1.size() - suffixCount);
167 newText2 = newText2.left(newText2.size() - suffixCount);
168 }
169 QList<Diff> diffList = preprocess2AndDiff(newText1, newText2);
170 if (prefixCount)
171 diffList.prepend(Diff(Diff::Equal, prefix));
172 if (suffixCount)
173 diffList.append(Diff(Diff::Equal, suffix));
174 return diffList;
175}
176
177QList<Diff> Differ::preprocess2AndDiff(const QString &text1, const QString &text2)
178{
179 QList<Diff> diffList;
180
181 if (text1.isEmpty()) {
182 diffList.append(Diff(Diff::Insert, text2));
183 return diffList;
184 }
185
186 if (text2.isEmpty()) {
187 diffList.append(Diff(Diff::Delete, text1));
188 return diffList;
189 }
190
191 if (text1.size() != text2.size()) {
192 const QString longtext = text1.size() > text2.size() ? text1 : text2;
193 const QString shorttext = text1.size() > text2.size() ? text2 : text1;
194 const qsizetype i = longtext.indexOf(shorttext);
195 if (i != -1) {
196 const Diff::Command command = (text1.size() > text2.size())
198 diffList.append(Diff(command, longtext.left(i)));
199 diffList.append(Diff(Diff::Equal, shorttext));
200 diffList.append(Diff(command, longtext.mid(i + shorttext.size())));
201 return diffList;
202 }
203
204 if (shorttext.size() == 1) {
205 diffList.append(Diff(Diff::Delete, text1));
206 diffList.append(Diff(Diff::Insert, text2));
207 return diffList;
208 }
209 }
210
211 if (m_currentDiffMode != Differ::CharMode && text1.size() > 80 && text2.size() > 80)
212 return diffNonCharMode(text1, text2);
213
214 return diffMyers(text1, text2);
215}
216
217QList<Diff> Differ::diffMyers(const QString &text1, const QString &text2)
218{
219 const qsizetype n = text1.size();
220 const qsizetype m = text2.size();
221 const bool odd = (n + m) % 2;
222 const qsizetype D = odd ? (n + m) / 2 + 1 : (n + m) / 2;
223 const qsizetype delta = n - m;
224 const qsizetype vShift = D;
225 std::vector<qsizetype> forwardV(2 * D + 1);
226 std::vector<qsizetype> reverseV(2 * D + 1);
227 for (qsizetype i = 0; i <= 2 * D; i++) {
228 forwardV[i] = -1;
229 reverseV[i] = -1;
230 }
231 forwardV[vShift + 1] = 0;
232 reverseV[vShift + 1] = 0;
233 qsizetype kMinForward = -D;
234 qsizetype kMaxForward = D;
235 qsizetype kMinReverse = -D;
236 qsizetype kMaxReverse = D;
237 for (qsizetype d = 0; d <= D; d++) {
238 // going forward
239 for (qsizetype k = qMax(-d, kMinForward + qAbs(d + kMinForward) % 2);
240 k <= qMin(d, kMaxForward - qAbs(d + kMaxForward) % 2);
241 k = k + 2) {
242 qsizetype x;
243 if (k == -d || (k < d && forwardV[k + vShift - 1] < forwardV[k + vShift + 1]))
244 x = forwardV[k + vShift + 1]; // copy vertically from diagonal k + 1, y increases, y may exceed the graph
245 else
246 x = forwardV[k + vShift - 1] + 1; // copy horizontally from diagonal k - 1, x increases, x may exceed the graph
247 qsizetype y = x - k;
248
249 if (x > n) {
250 kMaxForward = k - 1; // we are beyond the graph (right border), don't check diagonals >= current k anymore
251 } else if (y > m) {
252 kMinForward = k + 1; // we are beyond the graph (bottom border), don't check diagonals <= current k anymore
253 } else {
254 // find snake
255 while (x < n && y < m) {
256 if (text1.at(x) != text2.at(y))
257 break;
258 x++;
259 y++;
260 }
261 forwardV[k + vShift] = x;
262 if (odd) { // check if overlap
263 if (k >= delta - (d - 1) && k <= delta + (d - 1)) {
264 if (n - reverseV[delta - k + vShift] <= x) {
265 return diffMyersSplit(text1, x, text2, y);
266 }
267 }
268 }
269 }
270 }
271 // in reverse direction
272 for (qsizetype k = qMax(-d, kMinReverse + qAbs(d + kMinReverse) % 2);
273 k <= qMin(d, kMaxReverse - qAbs(d + kMaxReverse) % 2);
274 k = k + 2) {
275 qsizetype x;
276 if (k == -d || (k < d && reverseV[k + vShift - 1] < reverseV[k + vShift + 1]))
277 x = reverseV[k + vShift + 1];
278 else
279 x = reverseV[k + vShift - 1] + 1;
280 qsizetype y = x - k;
281
282 if (x > n) {
283 kMaxReverse = k - 1; // we are beyond the graph (right border), don't check diagonals >= current k anymore
284 } else if (y > m) {
285 kMinReverse = k + 1; // we are beyond the graph (bottom border), don't check diagonals <= current k anymore
286 } else {
287 // find snake
288 while (x < n && y < m) {
289 if (text1.at(n - x - 1) != text2.at(m - y - 1))
290 break;
291 x++;
292 y++;
293 }
294 reverseV[k + vShift] = x;
295 if (!odd) { // check if overlap
296 if (k >= delta - d && k <= delta + d) {
297 if (n - forwardV[delta - k + vShift] <= x) {
298 return diffMyersSplit(text1, n - x, text2, m - x + k);
299 }
300 }
301 }
302 }
303 }
304 }
305 // Completely different
306 QList<Diff> diffList;
307 diffList.append(Diff(Diff::Delete, text1));
308 diffList.append(Diff(Diff::Insert, text2));
309 return diffList;
310}
311
312QList<Diff> Differ::diffMyersSplit(
313 const QString &text1, qsizetype x,
314 const QString &text2, qsizetype y)
315{
316 const QString text11 = text1.left(x);
317 const QString text12 = text1.mid(x);
318 const QString text21 = text2.left(y);
319 const QString text22 = text2.mid(y);
320
321 const QList<Diff> &diffList1 = preprocess1AndDiff(text11, text21);
322 const QList<Diff> &diffList2 = preprocess1AndDiff(text12, text22);
323 return diffList1 + diffList2;
324}
325
326QList<Diff> Differ::diffNonCharMode(const QString &text1, const QString &text2)
327{
328 QString encodedText1;
329 QString encodedText2;
330 QStringList subtexts = encode(text1, text2, &encodedText1, &encodedText2);
331
332 DiffMode diffMode = m_currentDiffMode;
333 m_currentDiffMode = CharMode;
334
335 // Each different subtext is a separate symbol
336 // process these symbols as text with bigger alphabet
337 QList<Diff> diffList = preprocess1AndDiff(encodedText1, encodedText2);
338
339 diffList = decode(diffList, subtexts);
340
341 QString lastDelete;
342 QString lastInsert;
343 QList<Diff> newDiffList;
344 for (qsizetype i = 0; i <= diffList.size(); i++) {
345 const Diff diffItem = i < diffList.size()
346 ? diffList.at(i)
347 : Diff(Diff::Equal); // dummy, ensure we process to the end
348 // even when diffList doesn't end with equality
349 if (diffItem.command == Diff::Delete) {
350 lastDelete += diffItem.text;
351 } else if (diffItem.command == Diff::Insert) {
352 lastInsert += diffItem.text;
353 } else { // Diff::Equal
354 if (!(lastDelete.isEmpty() && lastInsert.isEmpty())) {
355 // Rediff here on char basis
356 newDiffList += preprocess1AndDiff(lastDelete, lastInsert);
357
358 lastDelete.clear();
359 lastInsert.clear();
360 }
361 newDiffList.append(diffItem);
362 }
363 }
364
365 m_currentDiffMode = diffMode;
366 return newDiffList;
367}
368
369QStringList Differ::encode(const QString &text1,
370 const QString &text2,
371 QString *encodedText1,
372 QString *encodedText2)
373{
374 QStringList lines{{}}; // don't use code: 0
375 QHash<QString, qsizetype> lineToCode;
376
377 *encodedText1 = encode(text1, &lines, &lineToCode);
378 *encodedText2 = encode(text2, &lines, &lineToCode);
379
380 return lines;
381}
382
383qsizetype Differ::findSubtextEnd(const QString &text,
384 qsizetype subtextStart)
385{
386 if (m_currentDiffMode == Differ::LineMode) {
387 qsizetype subtextEnd = text.indexOf(u'\n', subtextStart);
388 if (subtextEnd == -1)
389 subtextEnd = text.size() - 1;
390 return ++subtextEnd;
391 } else if (m_currentDiffMode == Differ::WordMode) {
392 if (!text.at(subtextStart).isLetter())
393 return subtextStart + 1;
394 qsizetype i = subtextStart + 1;
395
396 const qsizetype count = text.size();
397 while (i < count && text.at(i).isLetter())
398 i++;
399 return i;
400 }
401 return subtextStart + 1; // CharMode
402}
403
404QString Differ::encode(const QString &text,
405 QStringList *lines,
406 QHash<QString, qsizetype> *lineToCode)
407{
408 qsizetype subtextStart = 0;
409 qsizetype subtextEnd = -1;
410 QString codes;
411 while (subtextEnd < text.size()) {
412 subtextEnd = findSubtextEnd(text, subtextStart);
413 const QString line = text.mid(subtextStart, subtextEnd - subtextStart);
414 subtextStart = subtextEnd;
415
416 if (lineToCode->contains(line)) {
417 codes += QChar(static_cast<ushort>(lineToCode->value(line)));
418 } else {
419 lines->append(line);
420 lineToCode->insert(line, lines->size() - 1);
421 codes += QChar(static_cast<ushort>(lines->size() - 1));
422 }
423 }
424 return codes;
425}
426
427QList<Diff> Differ::merge(const QList<Diff> &diffList)
428{
429 QString lastDelete;
430 QString lastInsert;
431 QList<Diff> newDiffList;
432 for (qsizetype i = 0; i <= diffList.size(); i++) {
433 Diff diff = i < diffList.size()
434 ? diffList.at(i)
435 : Diff(Diff::Equal); // dummy, ensure we process to the end
436 // even when diffList doesn't end with equality
437 if (diff.command == Diff::Delete) {
438 lastDelete += diff.text;
439 } else if (diff.command == Diff::Insert) {
440 lastInsert += diff.text;
441 } else { // Diff::Equal
442 if (!(lastDelete.isEmpty() && lastInsert.isEmpty())) {
443
444 // common prefix
445 const qsizetype prefixCount = commonPrefix(lastDelete, lastInsert);
446 if (prefixCount) {
447 const QString prefix = lastDelete.left(prefixCount);
448 lastDelete = lastDelete.mid(prefixCount);
449 lastInsert = lastInsert.mid(prefixCount);
450
451 if (!newDiffList.isEmpty()
452 && newDiffList.last().command == Diff::Equal) {
453 newDiffList.last().text += prefix;
454 } else {
455 newDiffList.append(Diff(Diff::Equal, prefix));
456 }
457 }
458
459 // common suffix
460 const qsizetype suffixCount = commonSuffix(lastDelete, lastInsert);
461 if (suffixCount) {
462 const QString suffix = lastDelete.right(suffixCount);
463 lastDelete = lastDelete.left(lastDelete.size() - suffixCount);
464 lastInsert = lastInsert.left(lastInsert.size() - suffixCount);
465
466 diff.text.prepend(suffix);
467 }
468
469 // append delete / insert / equal
470 if (!lastDelete.isEmpty())
471 newDiffList.append(Diff(Diff::Delete, lastDelete));
472 if (!lastInsert.isEmpty())
473 newDiffList.append(Diff(Diff::Insert, lastInsert));
474 if (!diff.text.isEmpty())
475 newDiffList.append(diff);
476 lastDelete.clear();
477 lastInsert.clear();
478 } else { // join with last equal diff
479 if (!newDiffList.isEmpty()
480 && newDiffList.last().command == Diff::Equal) {
481 newDiffList.last().text += diff.text;
482 } else {
483 if (!diff.text.isEmpty())
484 newDiffList.append(diff);
485 }
486 }
487 }
488 }
489
490 QList<Diff> squashedDiffList = squashEqualities(newDiffList);
491 if (squashedDiffList.size() != newDiffList.size())
492 return merge(squashedDiffList);
493
494 return squashedDiffList;
495}
496
497} // namespace QQmlLSUtils
498
499QT_END_NAMESPACE
bool operator==(const Diff &other) const
bool operator!=(const Diff &other) const
Diff(Command com, const QString &txt={})
QList< Diff > diff(const QString &text1, const QString &text2)
DiffMode diffMode() const
void setDiffMode(DiffMode mode)
static QList< Diff > decode(const QList< Diff > &diffList, const QStringList &lines)
static qsizetype commonSuffix(const QString &text1, const QString &text2)
static qsizetype commonPrefix(const QString &text1, const QString &text2)
static QList< Diff > squashEqualities(const QList< Diff > &diffList)
Combined button and popup list for selecting options.