diff --git a/src/plugins/diffeditor/differ.cpp b/src/plugins/diffeditor/differ.cpp index 90204ac4e92..f89c7fae474 100644 --- a/src/plugins/diffeditor/differ.cpp +++ b/src/plugins/diffeditor/differ.cpp @@ -32,6 +32,7 @@ #include #include #include +#include namespace DiffEditor { @@ -46,6 +47,34 @@ Diff::Diff(Command com, const QString &txt) : { } +bool Diff::operator==(const Diff &other) const +{ + return command == other.command && text == other.text; +} + +bool Diff::operator!=(const Diff &other) const +{ + return !(operator == (other)); +} + +QString Diff::commandString(Command com) +{ + if (com == Delete) + return QCoreApplication::translate("Diff", "Delete"); + else if (com == Insert) + return QCoreApplication::translate("Diff", "Insert"); + return QCoreApplication::translate("Diff", "Equal"); +} + +QString Diff::toString() const +{ + QString prettyText = text; + // Replace linebreaks with pretty char + prettyText.replace(QLatin1Char('\n'), QLatin1Char(L'\u00b6')); + return commandString(command) + QLatin1String(" \"") + + prettyText + QLatin1String("\""); +} + Differ::Differ() : m_diffMode(Differ::LineMode), m_currentDiffMode(Differ::LineMode) @@ -389,7 +418,7 @@ QList Differ::merge(const QList &diffList) QString lastInsert; QList newDiffList; for (int i = 0; i <= diffList.count(); i++) { - const Diff diff = i < diffList.count() + Diff diff = i < diffList.count() ? diffList.at(i) : Diff(Diff::Equal, QString()); // dummy, ensure we process to the end even when diffList doesn't end with equality if (diff.command == Diff::Delete) { @@ -398,8 +427,33 @@ QList Differ::merge(const QList &diffList) lastInsert += diff.text; } else { // Diff::Equal if (lastDelete.count() || lastInsert.count()) { - // common prefix and suffix? + // common prefix + const int prefixCount = commonPrefix(lastDelete, lastInsert); + if (prefixCount) { + const QString prefix = lastDelete.left(prefixCount); + lastDelete = lastDelete.mid(prefixCount); + lastInsert = lastInsert.mid(prefixCount); + + if (newDiffList.count() + && newDiffList.last().command == Diff::Equal) { + newDiffList.last().text += prefix; + } else { + newDiffList.append(Diff(Diff::Equal, prefix)); + } + } + + // common suffix + const int suffixCount = commonSuffix(lastDelete, lastInsert); + if (suffixCount) { + const QString suffix = lastDelete.right(suffixCount); + lastDelete = lastDelete.left(lastDelete.count() - suffixCount); + lastInsert = lastInsert.left(lastInsert.count() - suffixCount); + + diff.text.prepend(suffix); + } + + // append delete / insert / equal if (lastDelete.count()) newDiffList.append(Diff(Diff::Delete, lastDelete)); if (lastInsert.count()) @@ -429,7 +483,7 @@ QList Differ::merge(const QList &diffList) QList Differ::squashEqualities(const QList &diffList) { - if (diffList.count() <= 3) // we need at least 3 items + if (diffList.count() < 3) // we need at least 3 items return diffList; QList squashedDiffList; Diff prevDiff = diffList.at(0); diff --git a/src/plugins/diffeditor/differ.h b/src/plugins/diffeditor/differ.h index 8c334cb0ed3..5741c5aac78 100644 --- a/src/plugins/diffeditor/differ.h +++ b/src/plugins/diffeditor/differ.h @@ -52,6 +52,10 @@ public: QString text; Diff(Command com, const QString &txt); Diff(); + bool operator==(const Diff &other) const; + bool operator!=(const Diff &other) const; + QString toString() const; + static QString commandString(Command com); }; class DIFFEDITOR_EXPORT Differ @@ -67,13 +71,13 @@ public: QList diff(const QString &text1, const QString &text2); void setDiffMode(DiffMode mode); bool diffMode() const; + QList merge(const QList &diffList); private: QList preprocess1AndDiff(const QString &text1, const QString &text2); QList preprocess2AndDiff(const QString &text1, const QString &text2); QList diffMyers(const QString &text1, const QString &text2); QList diffMyersSplit(const QString &text1, int x, const QString &text2, int y); - QList merge(const QList &diffList); QList squashEqualities(const QList &diffList); QList diffNonCharMode(const QString text1, const QString text2); QStringList encode(const QString &text1, diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index 520d3962132..d9653e88a15 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -5,6 +5,7 @@ SUBDIRS += \ changeset \ cplusplus \ debugger \ + diff \ extensionsystem \ environment \ generichighlighter \ diff --git a/tests/auto/diff/diff.pro b/tests/auto/diff/diff.pro new file mode 100644 index 00000000000..6c75f1e84d0 --- /dev/null +++ b/tests/auto/diff/diff.pro @@ -0,0 +1,5 @@ +TEMPLATE = subdirs +CONFIG += ordered + +SUBDIRS = \ + differ diff --git a/tests/auto/diff/differ/differ.pro b/tests/auto/diff/differ/differ.pro new file mode 100644 index 00000000000..bf1c5c8cf14 --- /dev/null +++ b/tests/auto/diff/differ/differ.pro @@ -0,0 +1,10 @@ +include(../../qttest.pri) + +include($$IDE_SOURCE_TREE/src/plugins/diffeditor/diffeditor.pri) + +LIBS += -L$$IDE_PLUGIN_PATH/QtProject + +SOURCES += tst_differ.cpp + +INCLUDEPATH += $$IDE_SOURCE_TREE/src/plugins $$IDE_SOURCE_TREE/src/libs + diff --git a/tests/auto/diff/differ/tst_differ.cpp b/tests/auto/diff/differ/tst_differ.cpp new file mode 100644 index 00000000000..2b680033465 --- /dev/null +++ b/tests/auto/diff/differ/tst_differ.cpp @@ -0,0 +1,428 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(DiffEditor::Diff); +Q_DECLARE_METATYPE(QList); + +using namespace DiffEditor; + +namespace QTest { + template<> + char *toString(const Diff &diff) + { + QByteArray ba = diff.toString().toUtf8(); + return qstrdup(ba.data()); + } +} + +namespace QTest { + template<> + char *toString(const QList &diffList) + { + QByteArray ba = "QList(" + QByteArray::number(diffList.count()) + " diffs:"; + for (int i = 0; i < diffList.count(); i++) { + if (i > 0) + ba += ","; + ba += " " + QByteArray::number(i + 1) + ". " + diffList.at(i).toString().toUtf8(); + } + ba += ")"; + return qstrdup(ba.data()); + } +} + + +class tst_Differ: public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void preprocess_data(); + void preprocess(); + void myers_data(); + void myers(); + void merge_data(); + void merge(); +}; + + +void tst_Differ::preprocess_data() +{ + QTest::addColumn("text1"); + QTest::addColumn("text2"); + QTest::addColumn >("expected"); + + QTest::newRow("Null texts") << QString() << QString() << QList(); + QTest::newRow("Empty texts") << QString("") << QString("") << QList(); + QTest::newRow("Null and empty text") << QString() << QString("") << QList(); + QTest::newRow("Empty and null text") << QString("") << QString() << QList(); + QTest::newRow("Simple delete") << QString("A") << QString() << (QList() << Diff(Diff::Delete, QString("A"))); + QTest::newRow("Simple insert") << QString() << QString("A") << (QList() << Diff(Diff::Insert, QString("A"))); + QTest::newRow("Simple equal") << QString("A") << QString("A") << (QList() + << Diff(Diff::Equal, QString("A"))); + QTest::newRow("Common prefix 1") << QString("ABCD") << QString("AB") << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Delete, QString("CD"))); + QTest::newRow("Common prefix 2") << QString("AB") << QString("ABCD") << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Insert, QString("CD"))); + QTest::newRow("Common suffix 1") << QString("ABCD") << QString("CD") << (QList() + << Diff(Diff::Delete, QString("AB")) + << Diff(Diff::Equal, QString("CD"))); + QTest::newRow("Common suffix 2") << QString("CD") << QString("ABCD") << (QList() + << Diff(Diff::Insert, QString("AB")) + << Diff(Diff::Equal, QString("CD"))); + QTest::newRow("Common prefix and suffix 1") << QString("ABCDEF") << QString("ABEF") << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Delete, QString("CD")) + << Diff(Diff::Equal, QString("EF"))); + QTest::newRow("Common prefix and suffix 2") << QString("ABEF") << QString("ABCDEF") << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Insert, QString("CD")) + << Diff(Diff::Equal, QString("EF"))); + QTest::newRow("Two edits 1") << QString("ABCDEF") << QString("ACDF") << (QList() + << Diff(Diff::Equal, QString("A")) // prefix + << Diff(Diff::Delete, QString("B")) + << Diff(Diff::Equal, QString("CD")) // CD inside BCDE + << Diff(Diff::Delete, QString("E")) + << Diff(Diff::Equal, QString("F"))); // suffix + QTest::newRow("Two edits 2") << QString("ACDF") << QString("ABCDEF") << (QList() + << Diff(Diff::Equal, QString("A")) // prefix + << Diff(Diff::Insert, QString("B")) + << Diff(Diff::Equal, QString("CD")) // CD inside BCDE + << Diff(Diff::Insert, QString("E")) + << Diff(Diff::Equal, QString("F"))); // suffix + QTest::newRow("Single char not in the other text 1") << QString("ABCDEF") << QString("AXF") << (QList() + << Diff(Diff::Equal, QString("A")) // prefix + << Diff(Diff::Delete, QString("BCDE")) + << Diff(Diff::Insert, QString("X")) // single char not in the other text + << Diff(Diff::Equal, QString("F"))); // suffix + QTest::newRow("Single char not in the other text 2") << QString("AXF") << QString("ABCDEF") << (QList() + << Diff(Diff::Equal, QString("A")) // prefix + << Diff(Diff::Delete, QString("X")) // single char not in the other text + << Diff(Diff::Insert, QString("BCDE")) + << Diff(Diff::Equal, QString("F"))); // suffix +} + +void tst_Differ::preprocess() +{ + QFETCH(QString, text1); + QFETCH(QString, text2); + QFETCH(QList, expected); + + Differ differ; + QList result = differ.diff(text1, text2); + QCOMPARE(result, expected); +} + +void tst_Differ::myers_data() +{ + QTest::addColumn("text1"); + QTest::addColumn("text2"); + QTest::addColumn >("expected"); + + QTest::newRow("Myers 1") << QString("XAXCXABC") << QString("ABCY") << (QList() + << Diff(Diff::Delete, QString("XAXCX")) + << Diff(Diff::Equal, QString("ABC")) + << Diff(Diff::Insert, QString("Y"))); +} + +void tst_Differ::myers() +{ + QFETCH(QString, text1); + QFETCH(QString, text2); + QFETCH(QList, expected); + + Differ differ; + QList result = differ.diff(text1, text2); + QCOMPARE(result, expected); +} + +void tst_Differ::merge_data() +{ + QTest::addColumn >("input"); + QTest::addColumn >("expected"); + + QTest::newRow("Remove null insert") + << (QList() + << Diff(Diff::Insert, QString())) + << QList(); + QTest::newRow("Remove null delete") + << (QList() + << Diff(Diff::Delete, QString())) + << QList(); + QTest::newRow("Remove null equal") + << (QList() + << Diff(Diff::Equal, QString())) + << QList(); + QTest::newRow("Remove empty insert") + << (QList() + << Diff(Diff::Insert, QString(""))) + << QList(); + QTest::newRow("Remove empty delete") + << (QList() + << Diff(Diff::Delete, QString(""))) + << QList(); + QTest::newRow("Remove empty equal") + << (QList() + << Diff(Diff::Equal, QString(""))) + << QList(); + QTest::newRow("Remove empty inserts") + << (QList() + << Diff(Diff::Insert, QString("")) + << Diff(Diff::Insert, QString())) + << QList(); + QTest::newRow("Remove empty deletes") + << (QList() + << Diff(Diff::Delete, QString("")) + << Diff(Diff::Delete, QString())) + << QList(); + QTest::newRow("Remove empty equals") + << (QList() + << Diff(Diff::Equal, QString("")) + << Diff(Diff::Equal, QString())) + << QList(); + QTest::newRow("Remove empty inserts / deletes / equals") + << (QList() + << Diff(Diff::Insert, QString("")) + << Diff(Diff::Delete, QString("")) + << Diff(Diff::Equal, QString("")) + << Diff(Diff::Insert, QString()) + << Diff(Diff::Delete, QString()) + << Diff(Diff::Equal, QString())) + << QList(); + QTest::newRow("Two equals") + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Equal, QString("CD"))) + << (QList() + << Diff(Diff::Equal, QString("ABCD"))); + QTest::newRow("Two deletes") + << (QList() + << Diff(Diff::Delete, QString("AB")) + << Diff(Diff::Delete, QString("CD"))) + << (QList() + << Diff(Diff::Delete, QString("ABCD"))); + QTest::newRow("Two inserts") + << (QList() + << Diff(Diff::Insert, QString("AB")) + << Diff(Diff::Insert, QString("CD"))) + << (QList() + << Diff(Diff::Insert, QString("ABCD"))); + QTest::newRow("Change order of insert / delete") + << (QList() + << Diff(Diff::Insert, QString("AB")) + << Diff(Diff::Delete, QString("CD"))) + << (QList() + << Diff(Diff::Delete, QString("CD")) + << Diff(Diff::Insert, QString("AB"))); + QTest::newRow("Squash into equal 1") + << (QList() + << Diff(Diff::Insert, QString("AB")) + << Diff(Diff::Delete, QString("AB"))) + << (QList() + << Diff(Diff::Equal, QString("AB"))); + QTest::newRow("Squash into equal 2") + << (QList() + << Diff(Diff::Insert, QString("A")) + << Diff(Diff::Delete, QString("ABC")) + << Diff(Diff::Insert, QString("B")) + << Diff(Diff::Delete, QString("D")) + << Diff(Diff::Insert, QString("CD"))) + << (QList() + << Diff(Diff::Equal, QString("ABCD"))); + QTest::newRow("Prefix and suffix detection 1") + << (QList() + << Diff(Diff::Delete, QString("A")) + << Diff(Diff::Insert, QString("ABC")) + << Diff(Diff::Delete, QString("DC"))) + << (QList() + << Diff(Diff::Equal, QString("A")) + << Diff(Diff::Delete, QString("D")) + << Diff(Diff::Insert, QString("B")) + << Diff(Diff::Equal, QString("C"))); + QTest::newRow("Prefix and suffix detection 2") + << (QList() + << Diff(Diff::Equal, QString("X")) + << Diff(Diff::Delete, QString("A")) + << Diff(Diff::Insert, QString("ABC")) + << Diff(Diff::Delete, QString("DC")) + << Diff(Diff::Equal, QString("Y"))) + << (QList() + << Diff(Diff::Equal, QString("XA")) + << Diff(Diff::Delete, QString("D")) + << Diff(Diff::Insert, QString("B")) + << Diff(Diff::Equal, QString("CY"))); + QTest::newRow("Merge inserts") + << (QList() + << Diff(Diff::Insert, QString("AB")) + << Diff(Diff::Delete, QString("CD")) + << Diff(Diff::Insert, QString("EF"))) + << (QList() + << Diff(Diff::Delete, QString("CD")) + << Diff(Diff::Insert, QString("ABEF"))); + QTest::newRow("Merge deletes") + << (QList() + << Diff(Diff::Delete, QString("AB")) + << Diff(Diff::Insert, QString("CD")) + << Diff(Diff::Delete, QString("EF"))) + << (QList() + << Diff(Diff::Delete, QString("ABEF")) + << Diff(Diff::Insert, QString("CD"))); + QTest::newRow("Merge many") + << (QList() + << Diff(Diff::Equal, QString("A")) + << Diff(Diff::Equal, QString("B")) + << Diff(Diff::Insert, QString("CD")) + << Diff(Diff::Delete, QString("EF")) + << Diff(Diff::Insert, QString("GH")) + << Diff(Diff::Delete, QString("IJ")) + << Diff(Diff::Equal, QString("K")) + << Diff(Diff::Equal, QString("L"))) + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Delete, QString("EFIJ")) + << Diff(Diff::Insert, QString("CDGH")) + << Diff(Diff::Equal, QString("KL"))); + QTest::newRow("Don't merge") + << (QList() + << Diff(Diff::Delete, QString("AB")) + << Diff(Diff::Insert, QString("CD")) + << Diff(Diff::Equal, QString("EF")) + << Diff(Diff::Delete, QString("GH")) + << Diff(Diff::Insert, QString("IJ"))) + << (QList() + << Diff(Diff::Delete, QString("AB")) + << Diff(Diff::Insert, QString("CD")) + << Diff(Diff::Equal, QString("EF")) + << Diff(Diff::Delete, QString("GH")) + << Diff(Diff::Insert, QString("IJ"))); + QTest::newRow("Squash equalities surrounding insert sliding edit left") + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Insert, QString("CDAB")) + << Diff(Diff::Equal, QString("EF"))) + << (QList() + << Diff(Diff::Insert, QString("ABCD")) + << Diff(Diff::Equal, QString("ABEF"))); + QTest::newRow("Squash equalities surrounding insert sliding edit right") + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Insert, QString("CDEF")) + << Diff(Diff::Equal, QString("CD"))) + << (QList() + << Diff(Diff::Equal, QString("ABCD")) + << Diff(Diff::Insert, QString("EFCD"))); + QTest::newRow("Squash equalities surrounding delete sliding edit left") + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Delete, QString("CDAB")) + << Diff(Diff::Equal, QString("EF"))) + << (QList() + << Diff(Diff::Delete, QString("ABCD")) + << Diff(Diff::Equal, QString("ABEF"))); + QTest::newRow("Squash equalities surrounding delete sliding edit right") + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Delete, QString("CDEF")) + << Diff(Diff::Equal, QString("CD"))) + << (QList() + << Diff(Diff::Equal, QString("ABCD")) + << Diff(Diff::Delete, QString("EFCD"))); + QTest::newRow("Squash equalities complex 1") + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Insert, QString("CDGH")) + << Diff(Diff::Equal, QString("CD")) + << Diff(Diff::Insert, QString("EFIJ")) + << Diff(Diff::Equal, QString("EF"))) + << (QList() + << Diff(Diff::Equal, QString("ABCD")) + << Diff(Diff::Insert, QString("GHCDEFIJ")) + << Diff(Diff::Equal, QString("EF"))); + QTest::newRow("Squash equalities complex 2") + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Insert, QString("CDGH")) + << Diff(Diff::Equal, QString("CD")) + << Diff(Diff::Delete, QString("EFIJ")) + << Diff(Diff::Equal, QString("EF"))) + << (QList() + << Diff(Diff::Equal, QString("ABCD")) + << Diff(Diff::Delete, QString("EFIJ")) + << Diff(Diff::Insert, QString("GHCD")) + << Diff(Diff::Equal, QString("EF"))); + QTest::newRow("Squash equalities complex 3") + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Insert, QString("CDEF")) + << Diff(Diff::Equal, QString("CD")) + << Diff(Diff::Insert, QString("GH"))) + << (QList() + << Diff(Diff::Equal, QString("ABCD")) + << Diff(Diff::Insert, QString("EFCDGH"))); + QTest::newRow("Squash equalities complex 4") + << (QList() + << Diff(Diff::Equal, QString("AB")) + << Diff(Diff::Insert, QString("CDEF")) + << Diff(Diff::Equal, QString("CD")) + << Diff(Diff::Insert, QString("GH")) + << Diff(Diff::Equal, QString("EF")) + << Diff(Diff::Insert, QString("IJ")) + << Diff(Diff::Equal, QString("CD"))) + << (QList() + << Diff(Diff::Equal, QString("ABCDEFCD")) + << Diff(Diff::Insert, QString("GHEFIJCD"))); +} + +void tst_Differ::merge() +{ + QFETCH(QList, input); + QFETCH(QList, expected); + + Differ differ; + QList result = differ.merge(input); + QCOMPARE(result, expected); +} + + + +QTEST_MAIN(tst_Differ) + +#include "tst_differ.moc" + +