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