1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "diffeditorplugin.h"
27 #include "diffeditor.h"
28 #include "diffeditorconstants.h"
29 #include "diffeditorcontroller.h"
30 #include "diffeditordocument.h"
31 #include "diffeditorfactory.h"
32 
33 #include <QAction>
34 #include <QFileDialog>
35 #include <QFutureWatcher>
36 #include <QMenu>
37 #include <QTextCodec>
38 
39 #include <coreplugin/actionmanager/actioncontainer.h>
40 #include <coreplugin/actionmanager/actionmanager.h>
41 #include <coreplugin/coreconstants.h>
42 #include <coreplugin/editormanager/documentmodel.h>
43 #include <coreplugin/editormanager/editormanager.h>
44 #include <coreplugin/icore.h>
45 #include <coreplugin/progressmanager/progressmanager.h>
46 
47 #include <texteditor/textdocument.h>
48 #include <texteditor/texteditor.h>
49 #include "texteditor/texteditoractionhandler.h"
50 
51 #include <utils/algorithm.h>
52 #include <utils/differ.h>
53 #include <utils/mapreduce.h>
54 #include <utils/qtcassert.h>
55 
56 using namespace Core;
57 using namespace TextEditor;
58 using namespace Utils;
59 
60 namespace DiffEditor {
61 namespace Internal {
62 
63 class ReloadInput {
64 public:
65     QString leftText;
66     QString rightText;
67     DiffFileInfo leftFileInfo;
68     DiffFileInfo rightFileInfo;
69     FileData::FileOperation fileOperation = FileData::ChangeFile;
70     bool binaryFiles = false;
71 };
72 
73 class DiffFile
74 {
75 public:
DiffFile(bool ignoreWhitespace,int contextLineCount)76     DiffFile(bool ignoreWhitespace, int contextLineCount)
77         : m_contextLineCount(contextLineCount),
78           m_ignoreWhitespace(ignoreWhitespace)
79     {}
80 
operator ()(QFutureInterface<FileData> & futureInterface,const ReloadInput & reloadInfo) const81     void operator()(QFutureInterface<FileData> &futureInterface,
82                     const ReloadInput &reloadInfo) const
83     {
84         if (reloadInfo.leftText == reloadInfo.rightText)
85             return; // We show "No difference" in this case, regardless if it's binary or not
86 
87         Differ differ(&futureInterface);
88 
89         FileData fileData;
90         if (!reloadInfo.binaryFiles) {
91             const QList<Diff> diffList = Differ::cleanupSemantics(
92                         differ.diff(reloadInfo.leftText, reloadInfo.rightText));
93 
94             QList<Diff> leftDiffList;
95             QList<Diff> rightDiffList;
96             Differ::splitDiffList(diffList, &leftDiffList, &rightDiffList);
97             QList<Diff> outputLeftDiffList;
98             QList<Diff> outputRightDiffList;
99 
100             if (m_ignoreWhitespace) {
101                 const QList<Diff> leftIntermediate
102                         = Differ::moveWhitespaceIntoEqualities(leftDiffList);
103                 const QList<Diff> rightIntermediate
104                         = Differ::moveWhitespaceIntoEqualities(rightDiffList);
105                 Differ::ignoreWhitespaceBetweenEqualities(leftIntermediate, rightIntermediate,
106                                                           &outputLeftDiffList, &outputRightDiffList);
107             } else {
108                 outputLeftDiffList = leftDiffList;
109                 outputRightDiffList = rightDiffList;
110             }
111 
112             const ChunkData chunkData = DiffUtils::calculateOriginalData(
113                         outputLeftDiffList, outputRightDiffList);
114             fileData = DiffUtils::calculateContextData(chunkData, m_contextLineCount, 0);
115         }
116         fileData.leftFileInfo = reloadInfo.leftFileInfo;
117         fileData.rightFileInfo = reloadInfo.rightFileInfo;
118         fileData.fileOperation = reloadInfo.fileOperation;
119         fileData.binaryFiles = reloadInfo.binaryFiles;
120         futureInterface.reportResult(fileData);
121     }
122 
123 private:
124     const int m_contextLineCount;
125     const bool m_ignoreWhitespace;
126 };
127 
128 class DiffFilesController : public DiffEditorController
129 {
130     Q_OBJECT
131 public:
132     DiffFilesController(IDocument *document);
~DiffFilesController()133     ~DiffFilesController() override { cancelReload(); }
134 
135 protected:
136     virtual QList<ReloadInput> reloadInputList() const = 0;
137 
138 private:
139     void reloaded();
140     void cancelReload();
141 
142     QFutureWatcher<FileData> m_futureWatcher;
143 };
144 
DiffFilesController(IDocument * document)145 DiffFilesController::DiffFilesController(IDocument *document)
146     : DiffEditorController(document)
147 {
148     connect(&m_futureWatcher, &QFutureWatcher<FileData>::finished,
149             this, &DiffFilesController::reloaded);
150 
151     setReloader([this] {
152         cancelReload();
153         m_futureWatcher.setFuture(Utils::map(reloadInputList(),
154                                              DiffFile(ignoreWhitespace(),
155                                                       contextLineCount())));
156 
157         Core::ProgressManager::addTask(m_futureWatcher.future(),
158                                        tr("Calculating diff"), "DiffEditor");
159     });
160 }
161 
reloaded()162 void DiffFilesController::reloaded()
163 {
164     const bool success = !m_futureWatcher.future().isCanceled();
165     const QList<FileData> fileDataList = success
166             ? m_futureWatcher.future().results() : QList<FileData>();
167 
168     setDiffFiles(fileDataList);
169     reloadFinished(success);
170 }
171 
cancelReload()172 void DiffFilesController::cancelReload()
173 {
174     if (m_futureWatcher.future().isRunning()) {
175         m_futureWatcher.future().cancel();
176         m_futureWatcher.setFuture(QFuture<FileData>());
177     }
178 }
179 
180 class DiffCurrentFileController : public DiffFilesController
181 {
182     Q_OBJECT
183 public:
184     DiffCurrentFileController(IDocument *document, const QString &fileName);
185 
186 protected:
187     QList<ReloadInput> reloadInputList() const final;
188 
189 private:
190     const QString m_fileName;
191 };
192 
DiffCurrentFileController(IDocument * document,const QString & fileName)193 DiffCurrentFileController::DiffCurrentFileController(IDocument *document, const QString &fileName) :
194     DiffFilesController(document), m_fileName(fileName)
195 { }
196 
reloadInputList() const197 QList<ReloadInput> DiffCurrentFileController::reloadInputList() const
198 {
199     QList<ReloadInput> result;
200 
201     auto textDocument = qobject_cast<TextEditor::TextDocument *>(
202         DocumentModel::documentForFilePath(FilePath::fromString(m_fileName)));
203 
204     if (textDocument && textDocument->isModified()) {
205         QString errorString;
206         Utils::TextFileFormat format = textDocument->format();
207 
208         QString leftText;
209         const Utils::TextFileFormat::ReadResult leftResult
210             = Utils::TextFileFormat::readFile(FilePath::fromString(m_fileName), format.codec,
211                                         &leftText, &format, &errorString);
212 
213         const QString rightText = textDocument->plainText();
214 
215         ReloadInput reloadInput;
216         reloadInput.leftText = leftText;
217         reloadInput.rightText = rightText;
218         reloadInput.leftFileInfo.fileName = m_fileName;
219         reloadInput.rightFileInfo.fileName = m_fileName;
220         reloadInput.leftFileInfo.typeInfo = tr("Saved");
221         reloadInput.rightFileInfo.typeInfo = tr("Modified");
222         reloadInput.rightFileInfo.patchBehaviour = DiffFileInfo::PatchEditor;
223         reloadInput.binaryFiles = (leftResult == Utils::TextFileFormat::ReadEncodingError);
224 
225         if (leftResult == Utils::TextFileFormat::ReadIOError)
226             reloadInput.fileOperation = FileData::NewFile;
227 
228         result << reloadInput;
229     }
230 
231     return result;
232 }
233 
234 /////////////////
235 
236 class DiffOpenFilesController : public DiffFilesController
237 {
238     Q_OBJECT
239 public:
240     DiffOpenFilesController(IDocument *document);
241 
242 protected:
243     QList<ReloadInput> reloadInputList() const final;
244 };
245 
DiffOpenFilesController(IDocument * document)246 DiffOpenFilesController::DiffOpenFilesController(IDocument *document) :
247     DiffFilesController(document)
248 { }
249 
reloadInputList() const250 QList<ReloadInput> DiffOpenFilesController::reloadInputList() const
251 {
252     QList<ReloadInput> result;
253 
254     const QList<IDocument *> openedDocuments = DocumentModel::openedDocuments();
255 
256     for (IDocument *doc : openedDocuments) {
257         auto textDocument = qobject_cast<TextEditor::TextDocument *>(doc);
258 
259         if (textDocument && textDocument->isModified()) {
260             QString errorString;
261             Utils::TextFileFormat format = textDocument->format();
262 
263             QString leftText;
264             const QString fileName = textDocument->filePath().toString();
265             const Utils::TextFileFormat::ReadResult leftResult = Utils::TextFileFormat::readFile(
266                 FilePath::fromString(fileName), format.codec, &leftText, &format, &errorString);
267 
268             const QString rightText = textDocument->plainText();
269 
270             ReloadInput reloadInput;
271             reloadInput.leftText = leftText;
272             reloadInput.rightText = rightText;
273             reloadInput.leftFileInfo.fileName = fileName;
274             reloadInput.rightFileInfo.fileName = fileName;
275             reloadInput.leftFileInfo.typeInfo = tr("Saved");
276             reloadInput.rightFileInfo.typeInfo = tr("Modified");
277             reloadInput.rightFileInfo.patchBehaviour = DiffFileInfo::PatchEditor;
278             reloadInput.binaryFiles = (leftResult == Utils::TextFileFormat::ReadEncodingError);
279 
280             if (leftResult == Utils::TextFileFormat::ReadIOError)
281                 reloadInput.fileOperation = FileData::NewFile;
282 
283             result << reloadInput;
284         }
285     }
286 
287     return result;
288 }
289 
290 /////////////////
291 
292 class DiffModifiedFilesController : public DiffFilesController
293 {
294     Q_OBJECT
295 public:
296     DiffModifiedFilesController(IDocument *document, const QStringList &fileNames);
297 
298 protected:
299     QList<ReloadInput> reloadInputList() const final;
300 
301 private:
302     const QStringList m_fileNames;
303 };
304 
DiffModifiedFilesController(IDocument * document,const QStringList & fileNames)305 DiffModifiedFilesController::DiffModifiedFilesController(IDocument *document, const QStringList &fileNames) :
306     DiffFilesController(document), m_fileNames(fileNames)
307 { }
308 
reloadInputList() const309 QList<ReloadInput> DiffModifiedFilesController::reloadInputList() const
310 {
311     QList<ReloadInput> result;
312 
313     for (const QString &fileName : m_fileNames) {
314         auto textDocument = qobject_cast<TextEditor::TextDocument *>(
315             DocumentModel::documentForFilePath(Utils::FilePath::fromString(fileName)));
316 
317         if (textDocument && textDocument->isModified()) {
318             QString errorString;
319             Utils::TextFileFormat format = textDocument->format();
320 
321             QString leftText;
322             const QString fileName = textDocument->filePath().toString();
323             const Utils::TextFileFormat::ReadResult leftResult = Utils::TextFileFormat::readFile(
324                 FilePath::fromString(fileName), format.codec, &leftText, &format, &errorString);
325 
326             const QString rightText = textDocument->plainText();
327 
328             ReloadInput reloadInput;
329             reloadInput.leftText = leftText;
330             reloadInput.rightText = rightText;
331             reloadInput.leftFileInfo.fileName = fileName;
332             reloadInput.rightFileInfo.fileName = fileName;
333             reloadInput.leftFileInfo.typeInfo = tr("Saved");
334             reloadInput.rightFileInfo.typeInfo = tr("Modified");
335             reloadInput.rightFileInfo.patchBehaviour = DiffFileInfo::PatchEditor;
336             reloadInput.binaryFiles = (leftResult == Utils::TextFileFormat::ReadEncodingError);
337 
338             if (leftResult == Utils::TextFileFormat::ReadIOError)
339                 reloadInput.fileOperation = FileData::NewFile;
340 
341             result << reloadInput;
342         }
343     }
344 
345     return result;
346 }
347 
348 /////////////////
349 
350 class DiffExternalFilesController : public DiffFilesController
351 {
352     Q_OBJECT
353 public:
354     DiffExternalFilesController(IDocument *document, const QString &leftFileName,
355                        const QString &rightFileName);
356 
357 protected:
358     QList<ReloadInput> reloadInputList() const final;
359 
360 private:
361     const QString m_leftFileName;
362     const QString m_rightFileName;
363 };
364 
DiffExternalFilesController(IDocument * document,const QString & leftFileName,const QString & rightFileName)365 DiffExternalFilesController::DiffExternalFilesController(IDocument *document, const QString &leftFileName,
366                                        const QString &rightFileName) :
367     DiffFilesController(document), m_leftFileName(leftFileName), m_rightFileName(rightFileName)
368 { }
369 
reloadInputList() const370 QList<ReloadInput> DiffExternalFilesController::reloadInputList() const
371 {
372     QString errorString;
373     Utils::TextFileFormat format;
374     format.codec = EditorManager::defaultTextCodec();
375 
376     QString leftText;
377     QString rightText;
378 
379     const Utils::TextFileFormat::ReadResult leftResult = Utils::TextFileFormat::readFile(
380         FilePath::fromString(m_leftFileName), format.codec, &leftText, &format, &errorString);
381     const Utils::TextFileFormat::ReadResult rightResult = Utils::TextFileFormat::readFile(
382         FilePath::fromString(m_rightFileName), format.codec, &rightText, &format, &errorString);
383 
384     ReloadInput reloadInput;
385     reloadInput.leftText = leftText;
386     reloadInput.rightText = rightText;
387     reloadInput.leftFileInfo.fileName = m_leftFileName;
388     reloadInput.rightFileInfo.fileName = m_rightFileName;
389     reloadInput.binaryFiles = (leftResult == Utils::TextFileFormat::ReadEncodingError
390             || rightResult == Utils::TextFileFormat::ReadEncodingError);
391 
392     const bool leftFileExists = (leftResult != Utils::TextFileFormat::ReadIOError);
393     const bool rightFileExists = (rightResult != Utils::TextFileFormat::ReadIOError);
394     if (!leftFileExists && rightFileExists)
395         reloadInput.fileOperation = FileData::NewFile;
396     else if (leftFileExists && !rightFileExists)
397         reloadInput.fileOperation = FileData::DeleteFile;
398 
399     QList<ReloadInput> result;
400     if (leftFileExists || rightFileExists)
401         result << reloadInput;
402 
403     return result;
404 }
405 
406 /////////////////
407 
408 
currentTextDocument()409 static TextEditor::TextDocument *currentTextDocument()
410 {
411     return qobject_cast<TextEditor::TextDocument *>(
412                 EditorManager::currentDocument());
413 }
414 
415 DiffEditorServiceImpl::DiffEditorServiceImpl() = default;
416 
diffFiles(const QString & leftFileName,const QString & rightFileName)417 void DiffEditorServiceImpl::diffFiles(const QString &leftFileName, const QString &rightFileName)
418 {
419     const QString documentId = Constants::DIFF_EDITOR_PLUGIN
420             + QLatin1String(".DiffFiles.") + leftFileName + QLatin1Char('.') + rightFileName;
421     const QString title = tr("Diff Files");
422     auto const document = qobject_cast<DiffEditorDocument *>(
423                 DiffEditorController::findOrCreateDocument(documentId, title));
424     if (!document)
425         return;
426 
427     if (!DiffEditorController::controller(document))
428         new DiffExternalFilesController(document, leftFileName, rightFileName);
429     EditorManager::activateEditorForDocument(document);
430     document->reload();
431 }
432 
diffModifiedFiles(const QStringList & fileNames)433 void DiffEditorServiceImpl::diffModifiedFiles(const QStringList &fileNames)
434 {
435     const QString documentId = Constants::DIFF_EDITOR_PLUGIN
436             + QLatin1String(".DiffModifiedFiles");
437     const QString title = tr("Diff Modified Files");
438     auto const document = qobject_cast<DiffEditorDocument *>(
439                 DiffEditorController::findOrCreateDocument(documentId, title));
440     if (!document)
441         return;
442 
443     if (!DiffEditorController::controller(document))
444         new DiffModifiedFilesController(document, fileNames);
445     EditorManager::activateEditorForDocument(document);
446     document->reload();
447 }
448 
449 class DiffEditorPluginPrivate : public QObject
450 {
451     Q_DECLARE_TR_FUNCTIONS(DiffEditor::Internal::DiffEditorPlugin)
452 
453 public:
454     DiffEditorPluginPrivate();
455 
456     void updateDiffCurrentFileAction();
457     void updateDiffOpenFilesAction();
458     void diffCurrentFile();
459     void diffOpenFiles();
460     void diffExternalFiles();
461 
462     QAction *m_diffCurrentFileAction = nullptr;
463     QAction *m_diffOpenFilesAction = nullptr;
464 
465     DiffEditorFactory editorFactory;
466     DiffEditorServiceImpl service;
467 };
468 
DiffEditorPluginPrivate()469 DiffEditorPluginPrivate::DiffEditorPluginPrivate()
470 {
471     //register actions
472     ActionContainer *toolsContainer
473             = ActionManager::actionContainer(Core::Constants::M_TOOLS);
474     toolsContainer->insertGroup(Core::Constants::G_TOOLS_OPTIONS, Constants::G_TOOLS_DIFF);
475     ActionContainer *diffContainer = ActionManager::createMenu("Diff");
476     diffContainer->menu()->setTitle(tr("&Diff"));
477     toolsContainer->addMenu(diffContainer, Constants::G_TOOLS_DIFF);
478 
479     m_diffCurrentFileAction = new QAction(tr("Diff Current File"), this);
480     Command *diffCurrentFileCommand = ActionManager::registerAction(m_diffCurrentFileAction, "DiffEditor.DiffCurrentFile");
481     diffCurrentFileCommand->setDefaultKeySequence(QKeySequence(useMacShortcuts ? tr("Meta+H") : tr("Ctrl+H")));
482     connect(m_diffCurrentFileAction, &QAction::triggered, this, &DiffEditorPluginPrivate::diffCurrentFile);
483     diffContainer->addAction(diffCurrentFileCommand);
484 
485     m_diffOpenFilesAction = new QAction(tr("Diff Open Files"), this);
486     Command *diffOpenFilesCommand = ActionManager::registerAction(m_diffOpenFilesAction, "DiffEditor.DiffOpenFiles");
487     diffOpenFilesCommand->setDefaultKeySequence(QKeySequence(useMacShortcuts ? tr("Meta+Shift+H") : tr("Ctrl+Shift+H")));
488     connect(m_diffOpenFilesAction, &QAction::triggered, this, &DiffEditorPluginPrivate::diffOpenFiles);
489     diffContainer->addAction(diffOpenFilesCommand);
490 
491     QAction *diffExternalFilesAction = new QAction(tr("Diff External Files..."), this);
492     Command *diffExternalFilesCommand = ActionManager::registerAction(diffExternalFilesAction, "DiffEditor.DiffExternalFiles");
493     connect(diffExternalFilesAction, &QAction::triggered, this, &DiffEditorPluginPrivate::diffExternalFiles);
494     diffContainer->addAction(diffExternalFilesCommand);
495 
496     connect(EditorManager::instance(), &EditorManager::currentEditorChanged,
497             this, &DiffEditorPluginPrivate::updateDiffCurrentFileAction);
498     connect(EditorManager::instance(), &EditorManager::currentDocumentStateChanged,
499         this, &DiffEditorPluginPrivate::updateDiffCurrentFileAction);
500     connect(EditorManager::instance(), &EditorManager::editorOpened,
501         this, &DiffEditorPluginPrivate::updateDiffOpenFilesAction);
502     connect(EditorManager::instance(), &EditorManager::editorsClosed,
503         this, &DiffEditorPluginPrivate::updateDiffOpenFilesAction);
504     connect(EditorManager::instance(), &EditorManager::documentStateChanged,
505         this, &DiffEditorPluginPrivate::updateDiffOpenFilesAction);
506 
507     updateDiffCurrentFileAction();
508     updateDiffOpenFilesAction();
509 }
510 
updateDiffCurrentFileAction()511 void DiffEditorPluginPrivate::updateDiffCurrentFileAction()
512 {
513     auto textDocument = currentTextDocument();
514     const bool enabled = textDocument && textDocument->isModified();
515     m_diffCurrentFileAction->setEnabled(enabled);
516 }
517 
updateDiffOpenFilesAction()518 void DiffEditorPluginPrivate::updateDiffOpenFilesAction()
519 {
520     const bool enabled = Utils::anyOf(DocumentModel::openedDocuments(), [](IDocument *doc) {
521             return doc->isModified() && qobject_cast<TextEditor::TextDocument *>(doc);
522         });
523     m_diffOpenFilesAction->setEnabled(enabled);
524 }
525 
diffCurrentFile()526 void DiffEditorPluginPrivate::diffCurrentFile()
527 {
528     auto textDocument = currentTextDocument();
529     if (!textDocument)
530         return;
531 
532     const QString fileName = textDocument->filePath().toString();
533 
534     if (fileName.isEmpty())
535         return;
536 
537     const QString documentId = Constants::DIFF_EDITOR_PLUGIN
538             + QLatin1String(".Diff.") + fileName;
539     const QString title = tr("Diff \"%1\"").arg(fileName);
540     auto const document = qobject_cast<DiffEditorDocument *>(
541                 DiffEditorController::findOrCreateDocument(documentId, title));
542     if (!document)
543         return;
544 
545     if (!DiffEditorController::controller(document))
546         new DiffCurrentFileController(document, fileName);
547     EditorManager::activateEditorForDocument(document);
548     document->reload();
549 }
550 
diffOpenFiles()551 void DiffEditorPluginPrivate::diffOpenFiles()
552 {
553     const QString documentId = Constants::DIFF_EDITOR_PLUGIN
554             + QLatin1String(".DiffOpenFiles");
555     const QString title = tr("Diff Open Files");
556     auto const document = qobject_cast<DiffEditorDocument *>(
557                 DiffEditorController::findOrCreateDocument(documentId, title));
558     if (!document)
559         return;
560 
561     if (!DiffEditorController::controller(document))
562         new DiffOpenFilesController(document);
563     EditorManager::activateEditorForDocument(document);
564     document->reload();
565 }
566 
diffExternalFiles()567 void DiffEditorPluginPrivate::diffExternalFiles()
568 {
569     const QString fileName1 = QFileDialog::getOpenFileName(ICore::dialogParent(),
570                                                      tr("Select First File for Diff"),
571                                                      QString());
572     if (fileName1.isNull())
573         return;
574     if (EditorManager::skipOpeningBigTextFile(FilePath::fromString(fileName1)))
575         return;
576 
577     const QString fileName2 = QFileDialog::getOpenFileName(ICore::dialogParent(),
578                                                      tr("Select Second File for Diff"),
579                                                      QString());
580     if (fileName2.isNull())
581         return;
582     if (EditorManager::skipOpeningBigTextFile(FilePath::fromString(fileName2)))
583         return;
584 
585     const QString documentId = Constants::DIFF_EDITOR_PLUGIN
586             + QLatin1String(".DiffExternalFiles.") + fileName1 + QLatin1Char('.') + fileName2;
587     const QString title = tr("Diff \"%1\", \"%2\"").arg(fileName1, fileName2);
588     auto const document = qobject_cast<DiffEditorDocument *>(
589                 DiffEditorController::findOrCreateDocument(documentId, title));
590     if (!document)
591         return;
592 
593     if (!DiffEditorController::controller(document))
594         new DiffExternalFilesController(document, fileName1, fileName2);
595     EditorManager::activateEditorForDocument(document);
596     document->reload();
597 }
598 
~DiffEditorPlugin()599 DiffEditorPlugin::~DiffEditorPlugin()
600 {
601     delete d;
602 }
603 
initialize(const QStringList & arguments,QString * errorMessage)604 bool DiffEditorPlugin::initialize(const QStringList &arguments, QString *errorMessage)
605 {
606     d = new DiffEditorPluginPrivate;
607 
608     Q_UNUSED(arguments)
609     Q_UNUSED(errorMessage)
610 
611     return true;
612 }
613 
614 } // namespace Internal
615 } // namespace DiffEditor
616 
617 #ifdef WITH_TESTS
618 
619 #include <QTest>
620 
621 #include "diffutils.h"
622 
623 Q_DECLARE_METATYPE(DiffEditor::ChunkData)
Q_DECLARE_METATYPE(DiffEditor::FileData)624 Q_DECLARE_METATYPE(DiffEditor::FileData)
625 Q_DECLARE_METATYPE(DiffEditor::ChunkSelection)
626 
627 static inline QString _(const char *string) { return QString::fromLatin1(string); }
628 
testMakePatch_data()629 void DiffEditor::Internal::DiffEditorPlugin::testMakePatch_data()
630 {
631     QTest::addColumn<ChunkData>("sourceChunk");
632     QTest::addColumn<bool>("lastChunk");
633     QTest::addColumn<QString>("patchText");
634 
635     const QString fileName = "a.txt";
636     const QString header = "--- " + fileName + "\n+++ " + fileName + '\n';
637 
638     QList<RowData> rows;
639     rows << RowData(_("ABCD"), TextLineData::Separator);
640     rows << RowData(_("EFGH"));
641     ChunkData chunk;
642     chunk.rows = rows;
643     QString patchText = header + "@@ -1,2 +1,1 @@\n"
644                                  "-ABCD\n"
645                                  " EFGH\n";
646     QTest::newRow("Simple not a last chunk") << chunk
647                             << false
648                             << patchText;
649 
650     ///////////
651 
652     // chunk the same here
653     patchText = header + "@@ -1,2 +1,1 @@\n"
654                          "-ABCD\n"
655                          " EFGH\n"
656                          "\\ No newline at end of file\n";
657 
658     QTest::newRow("Simple last chunk") << chunk
659                             << true
660                             << patchText;
661 
662     ///////////
663 
664     rows.clear();
665     rows << RowData(_("ABCD"));
666     rows << RowData(_(""), TextLineData::Separator);
667     chunk.rows = rows;
668     patchText = header + "@@ -1,1 +1,1 @@\n"
669                          "-ABCD\n"
670                          "+ABCD\n"
671                          "\\ No newline at end of file\n";
672 
673     QTest::newRow("EOL in last line removed") << chunk
674                             << true
675                             << patchText;
676 
677     ///////////
678 
679     // chunk the same here
680     patchText = header + "@@ -1,2 +1,1 @@\n"
681                          " ABCD\n"
682                          "-\n";
683 
684     QTest::newRow("Last empty line removed") << chunk
685                             << false
686                             << patchText;
687 
688     ///////////
689 
690     rows.clear();
691     rows << RowData(_("ABCD"));
692     rows << RowData(_(""), TextLineData::Separator);
693     rows << RowData(_(""), TextLineData::Separator);
694     chunk.rows = rows;
695     patchText = header + "@@ -1,2 +1,1 @@\n"
696                          "-ABCD\n"
697                          "-\n"
698                          "+ABCD\n"
699                          "\\ No newline at end of file\n";
700 
701     QTest::newRow("Two last EOLs removed") << chunk
702                             << true
703                             << patchText;
704 
705     ///////////
706 
707     rows.clear();
708     rows << RowData(_("ABCD"));
709     rows << RowData(TextLineData::Separator, _(""));
710     chunk.rows = rows;
711     patchText = header + "@@ -1,1 +1,1 @@\n"
712                          "-ABCD\n"
713                          "\\ No newline at end of file\n"
714                          "+ABCD\n";
715 
716     QTest::newRow("EOL to last line added") << chunk
717                             << true
718                             << patchText;
719 
720     ///////////
721 
722     // chunk the same here
723     patchText = header + "@@ -1,1 +1,2 @@\n"
724                          " ABCD\n"
725                          "+\n";
726 
727     QTest::newRow("Last empty line added") << chunk
728                             << false
729                             << patchText;
730 
731     ///////////
732 
733     rows.clear();
734     rows << RowData(_("ABCD"), _("EFGH"));
735     chunk.rows = rows;
736     patchText = header + "@@ -1,1 +1,1 @@\n"
737                          "-ABCD\n"
738                          "+EFGH\n";
739 
740     QTest::newRow("Last line with a newline modified") << chunk
741                             << false
742                             << patchText;
743 
744     ///////////
745 
746     rows.clear();
747     rows << RowData(_("ABCD"), _("EFGH"));
748     rows << RowData(_(""));
749     chunk.rows = rows;
750     patchText = header + "@@ -1,2 +1,2 @@\n"
751                          "-ABCD\n"
752                          "+EFGH\n"
753                          " \n";
754     QTest::newRow("Not a last line with a newline modified") << chunk
755                             << false
756                             << patchText;
757 
758     ///////////
759 
760     rows.clear();
761     rows << RowData(_("ABCD"), _("EFGH"));
762     chunk.rows = rows;
763     patchText = header + "@@ -1,1 +1,1 @@\n"
764                          "-ABCD\n"
765                          "\\ No newline at end of file\n"
766                          "+EFGH\n"
767                          "\\ No newline at end of file\n";
768 
769     QTest::newRow("Last line without a newline modified") << chunk
770                             << true
771                             << patchText;
772 
773     ///////////
774 
775     // chunk the same here
776     patchText = header + "@@ -1,1 +1,1 @@\n"
777                          "-ABCD\n"
778                          "+EFGH\n";
779     QTest::newRow("Not a last line without a newline modified") << chunk
780                             << false
781                             << patchText;
782 
783     ///////////
784 
785     rows.clear();
786     rows << RowData(_("ABCD"), _("EFGH"));
787     rows << RowData(_("IJKL"));
788     chunk.rows = rows;
789     patchText = header + "@@ -1,2 +1,2 @@\n"
790                          "-ABCD\n"
791                          "+EFGH\n"
792                          " IJKL\n"
793                          "\\ No newline at end of file\n";
794 
795     QTest::newRow("Last but one line modified, last line without a newline")
796             << chunk
797             << true
798             << patchText;
799 
800     ///////////
801 
802     // chunk the same here
803     patchText = header + "@@ -1,2 +1,2 @@\n"
804                          "-ABCD\n"
805                          "+EFGH\n"
806                          " IJKL\n";
807 
808     QTest::newRow("Last but one line modified, last line with a newline")
809             << chunk
810             << false
811             << patchText;
812 
813     ///////////
814 
815     rows.clear();
816     rows << RowData(_("ABCD"));
817     rows << RowData(TextLineData::Separator, _(""));
818     rows << RowData(_(""), _("EFGH"));
819     chunk.rows = rows;
820     patchText = header + "@@ -1,1 +1,3 @@\n"
821                          " ABCD\n"
822                          "+\n"
823                          "+EFGH\n"
824                          "\\ No newline at end of file\n";
825 
826     QTest::newRow("Blank line followed by No newline")
827             << chunk
828             << true
829             << patchText;
830 }
831 
testMakePatch()832 void DiffEditor::Internal::DiffEditorPlugin::testMakePatch()
833 {
834     QFETCH(ChunkData, sourceChunk);
835     QFETCH(bool, lastChunk);
836     QFETCH(QString, patchText);
837 
838     const QString fileName = "a.txt";
839     const QString result = DiffUtils::makePatch(sourceChunk, fileName, fileName, lastChunk);
840 
841     QCOMPARE(result, patchText);
842 
843     bool ok;
844     QList<FileData> resultList = DiffUtils::readPatch(result, &ok);
845 
846     QVERIFY(ok);
847     QCOMPARE(resultList.count(), 1);
848     for (int i = 0; i < resultList.count(); i++) {
849         const FileData &resultFileData = resultList.at(i);
850         QCOMPARE(resultFileData.leftFileInfo.fileName, fileName);
851         QCOMPARE(resultFileData.rightFileInfo.fileName, fileName);
852         QCOMPARE(resultFileData.chunks.count(), 1);
853         for (int j = 0; j < resultFileData.chunks.count(); j++) {
854             const ChunkData &resultChunkData = resultFileData.chunks.at(j);
855             QCOMPARE(resultChunkData.leftStartingLineNumber, sourceChunk.leftStartingLineNumber);
856             QCOMPARE(resultChunkData.rightStartingLineNumber, sourceChunk.rightStartingLineNumber);
857             QCOMPARE(resultChunkData.contextChunk, sourceChunk.contextChunk);
858             QCOMPARE(resultChunkData.rows.count(), sourceChunk.rows.count());
859             for (int k = 0; k < sourceChunk.rows.count(); k++) {
860                 const RowData &sourceRowData = sourceChunk.rows.at(k);
861                 const RowData &resultRowData = resultChunkData.rows.at(k);
862                 QCOMPARE(resultRowData.equal, sourceRowData.equal);
863                 QCOMPARE(resultRowData.leftLine.text, sourceRowData.leftLine.text);
864                 QCOMPARE(resultRowData.leftLine.textLineType, sourceRowData.leftLine.textLineType);
865                 QCOMPARE(resultRowData.rightLine.text, sourceRowData.rightLine.text);
866                 QCOMPARE(resultRowData.rightLine.textLineType, sourceRowData.rightLine.textLineType);
867             }
868         }
869     }
870 }
871 
testReadPatch_data()872 void DiffEditor::Internal::DiffEditorPlugin::testReadPatch_data()
873 {
874     QTest::addColumn<QString>("sourcePatch");
875     QTest::addColumn<QList<FileData> >("fileDataList");
876 
877     QString patch = "diff --git a/src/plugins/diffeditor/diffeditor.cpp b/src/plugins/diffeditor/diffeditor.cpp\n"
878                     "index eab9e9b..082c135 100644\n"
879                     "--- a/src/plugins/diffeditor/diffeditor.cpp\n"
880                     "+++ b/src/plugins/diffeditor/diffeditor.cpp\n"
881                     "@@ -187,9 +187,6 @@ void DiffEditor::ctor()\n"
882                     "     m_controller = m_document->controller();\n"
883                     "     m_guiController = new DiffEditorGuiController(m_controller, this);\n"
884                     " \n"
885                     "-//    m_sideBySideEditor->setDiffEditorGuiController(m_guiController);\n"
886                     "-//    m_unifiedEditor->setDiffEditorGuiController(m_guiController);\n"
887                     "-\n"
888                     "     connect(m_controller, SIGNAL(cleared(QString)),\n"
889                     "             this, SLOT(slotCleared(QString)));\n"
890                     "     connect(m_controller, SIGNAL(diffContentsChanged(QList<DiffEditorController::DiffFilesContents>,QString)),\n"
891                     "diff --git a/src/plugins/diffeditor/diffutils.cpp b/src/plugins/diffeditor/diffutils.cpp\n"
892                     "index 2f641c9..f8ff795 100644\n"
893                     "--- a/src/plugins/diffeditor/diffutils.cpp\n"
894                     "+++ b/src/plugins/diffeditor/diffutils.cpp\n"
895                     "@@ -464,5 +464,12 @@ QString DiffUtils::makePatch(const ChunkData &chunkData,\n"
896                     "     return diffText;\n"
897                     " }\n"
898                     " \n"
899                     "+FileData DiffUtils::makeFileData(const QString &patch)\n"
900                     "+{\n"
901                     "+    FileData fileData;\n"
902                     "+\n"
903                     "+    return fileData;\n"
904                     "+}\n"
905                     "+\n"
906                     " } // namespace Internal\n"
907                     " } // namespace DiffEditor\n"
908                     "diff --git a/new b/new\n"
909                     "new file mode 100644\n"
910                     "index 0000000..257cc56\n"
911                     "--- /dev/null\n"
912                     "+++ b/new\n"
913                     "@@ -0,0 +1 @@\n"
914                     "+foo\n"
915                     "diff --git a/deleted b/deleted\n"
916                     "deleted file mode 100644\n"
917                     "index 257cc56..0000000\n"
918                     "--- a/deleted\n"
919                     "+++ /dev/null\n"
920                     "@@ -1 +0,0 @@\n"
921                     "-foo\n"
922                     "diff --git a/empty b/empty\n"
923                     "new file mode 100644\n"
924                     "index 0000000..e69de29\n"
925                     "diff --git a/empty b/empty\n"
926                     "deleted file mode 100644\n"
927                     "index e69de29..0000000\n"
928                     "diff --git a/file a.txt b/file b.txt\n"
929                     "similarity index 99%\n"
930                     "copy from file a.txt\n"
931                     "copy to file b.txt\n"
932                     "index 1234567..9876543\n"
933                     "--- a/file a.txt\n"
934                     "+++ b/file b.txt\n"
935                     "@@ -20,3 +20,3 @@\n"
936                     " A\n"
937                     "-B\n"
938                     "+C\n"
939                     " D\n"
940                     "diff --git a/file a.txt b/file b.txt\n"
941                     "similarity index 99%\n"
942                     "rename from file a.txt\n"
943                     "rename to file b.txt\n"
944                     "diff --git a/file.txt b/file.txt\n"
945                     "old mode 100644\n"
946                     "new mode 100755\n"
947                     "index 1234567..9876543\n"
948                     "--- a/file.txt\n"
949                     "+++ b/file.txt\n"
950                     "@@ -20,3 +20,3 @@\n"
951                     " A\n"
952                     "-B\n"
953                     "+C\n"
954                     " D\n"
955                     ;
956 
957     FileData fileData1;
958     fileData1.leftFileInfo = DiffFileInfo("src/plugins/diffeditor/diffeditor.cpp", "eab9e9b");
959     fileData1.rightFileInfo = DiffFileInfo("src/plugins/diffeditor/diffeditor.cpp", "082c135");
960     ChunkData chunkData1;
961     chunkData1.leftStartingLineNumber = 186;
962     chunkData1.rightStartingLineNumber = 186;
963     QList<RowData> rows1;
964     rows1 << RowData(_("    m_controller = m_document->controller();"));
965     rows1 << RowData(_("    m_guiController = new DiffEditorGuiController(m_controller, this);"));
966     rows1 << RowData(_(""));
967     rows1 << RowData(_("//    m_sideBySideEditor->setDiffEditorGuiController(m_guiController);"), TextLineData::Separator);
968     rows1 << RowData(_("//    m_unifiedEditor->setDiffEditorGuiController(m_guiController);"), TextLineData::Separator);
969     rows1 << RowData(_(""), TextLineData::Separator);
970     rows1 << RowData(_("    connect(m_controller, SIGNAL(cleared(QString)),"));
971     rows1 << RowData(_("            this, SLOT(slotCleared(QString)));"));
972     rows1 << RowData(_("    connect(m_controller, SIGNAL(diffContentsChanged(QList<DiffEditorController::DiffFilesContents>,QString)),"));
973     chunkData1.rows = rows1;
974     fileData1.chunks << chunkData1;
975 
976     FileData fileData2;
977     fileData2.leftFileInfo = DiffFileInfo(_("src/plugins/diffeditor/diffutils.cpp"), _("2f641c9"));
978     fileData2.rightFileInfo = DiffFileInfo(_("src/plugins/diffeditor/diffutils.cpp"), _("f8ff795"));
979     ChunkData chunkData2;
980     chunkData2.leftStartingLineNumber = 463;
981     chunkData2.rightStartingLineNumber = 463;
982     QList<RowData> rows2;
983     rows2 << RowData(_("    return diffText;"));
984     rows2 << RowData(_("}"));
985     rows2 << RowData(_(""));
986     rows2 << RowData(TextLineData::Separator, _("FileData DiffUtils::makeFileData(const QString &patch)"));
987     rows2 << RowData(TextLineData::Separator, _("{"));
988     rows2 << RowData(TextLineData::Separator, _("    FileData fileData;"));
989     rows2 << RowData(TextLineData::Separator, _(""));
990     rows2 << RowData(TextLineData::Separator, _("    return fileData;"));
991     rows2 << RowData(TextLineData::Separator, _("}"));
992     rows2 << RowData(TextLineData::Separator, _(""));
993     rows2 << RowData(_("} // namespace Internal"));
994     rows2 << RowData(_("} // namespace DiffEditor"));
995     chunkData2.rows = rows2;
996     fileData2.chunks << chunkData2;
997 
998     FileData fileData3;
999     fileData3.leftFileInfo = DiffFileInfo("new", "0000000");
1000     fileData3.rightFileInfo = DiffFileInfo("new", "257cc56");
1001     fileData3.fileOperation = FileData::NewFile;
1002     ChunkData chunkData3;
1003     chunkData3.leftStartingLineNumber = -1;
1004     chunkData3.rightStartingLineNumber = 0;
1005     QList<RowData> rows3;
1006     rows3 << RowData(TextLineData::Separator, _("foo"));
1007     TextLineData textLineData3(TextLineData::TextLine);
1008     rows3 << RowData(TextLineData::Separator, textLineData3);
1009     chunkData3.rows = rows3;
1010     fileData3.chunks << chunkData3;
1011 
1012     FileData fileData4;
1013     fileData4.leftFileInfo = DiffFileInfo("deleted", "257cc56");
1014     fileData4.rightFileInfo = DiffFileInfo("deleted", "0000000");
1015     fileData4.fileOperation = FileData::DeleteFile;
1016     ChunkData chunkData4;
1017     chunkData4.leftStartingLineNumber = 0;
1018     chunkData4.rightStartingLineNumber = -1;
1019     QList<RowData> rows4;
1020     rows4 << RowData(_("foo"), TextLineData::Separator);
1021     TextLineData textLineData4(TextLineData::TextLine);
1022     rows4 << RowData(textLineData4, TextLineData::Separator);
1023     chunkData4.rows = rows4;
1024     fileData4.chunks << chunkData4;
1025 
1026     FileData fileData5;
1027     fileData5.leftFileInfo = DiffFileInfo("empty", "0000000");
1028     fileData5.rightFileInfo = DiffFileInfo("empty", "e69de29");
1029     fileData5.fileOperation = FileData::NewFile;
1030 
1031     FileData fileData6;
1032     fileData6.leftFileInfo = DiffFileInfo("empty", "e69de29");
1033     fileData6.rightFileInfo = DiffFileInfo("empty", "0000000");
1034     fileData6.fileOperation = FileData::DeleteFile;
1035 
1036     FileData fileData7;
1037     fileData7.leftFileInfo = DiffFileInfo("file a.txt", "1234567");
1038     fileData7.rightFileInfo = DiffFileInfo("file b.txt", "9876543");
1039     fileData7.fileOperation = FileData::CopyFile;
1040     ChunkData chunkData7;
1041     chunkData7.leftStartingLineNumber = 19;
1042     chunkData7.rightStartingLineNumber = 19;
1043     QList<RowData> rows7;
1044     rows7 << RowData(_("A"));
1045     rows7 << RowData(_("B"), _("C"));
1046     rows7 << RowData(_("D"));
1047     chunkData7.rows = rows7;
1048     fileData7.chunks << chunkData7;
1049 
1050     FileData fileData8;
1051     fileData8.leftFileInfo = DiffFileInfo("file a.txt");
1052     fileData8.rightFileInfo = DiffFileInfo("file b.txt");
1053     fileData8.fileOperation = FileData::RenameFile;
1054 
1055     FileData fileData9;
1056     fileData9.leftFileInfo = DiffFileInfo("file.txt", "1234567");
1057     fileData9.rightFileInfo = DiffFileInfo("file.txt", "9876543");
1058     fileData9.chunks << chunkData7;
1059     QList<FileData> fileDataList1;
1060     fileDataList1 << fileData1 << fileData2 << fileData3 << fileData4 << fileData5
1061                   << fileData6 << fileData7 << fileData8 << fileData9;
1062 
1063     QTest::newRow("Git patch") << patch
1064                                << fileDataList1;
1065 
1066     //////////////
1067 
1068     patch = "diff --git a/file foo.txt b/file foo.txt\n"
1069             "index 1234567..9876543 100644\n"
1070             "--- a/file foo.txt\n"
1071             "+++ b/file foo.txt\n"
1072             "@@ -50,4 +50,5 @@ void DiffEditor::ctor()\n"
1073             " A\n"
1074             " B\n"
1075             " C\n"
1076             "+\n";
1077 
1078     fileData1.leftFileInfo = DiffFileInfo("file foo.txt", "1234567");
1079     fileData1.rightFileInfo = DiffFileInfo("file foo.txt", "9876543");
1080     fileData1.fileOperation = FileData::ChangeFile;
1081     chunkData1.leftStartingLineNumber = 49;
1082     chunkData1.rightStartingLineNumber = 49;
1083     rows1.clear();
1084     rows1 << RowData(_("A"));
1085     rows1 << RowData(_("B"));
1086     rows1 << RowData(_("C"));
1087     rows1 << RowData(TextLineData::Separator, _(""));
1088     chunkData1.rows = rows1;
1089     fileData1.chunks.clear();
1090     fileData1.chunks << chunkData1;
1091 
1092     QList<FileData> fileDataList2;
1093     fileDataList2 << fileData1;
1094 
1095     QTest::newRow("Added line") << patch
1096                                 << fileDataList2;
1097 
1098     //////////////
1099 
1100     patch = "diff --git a/file foo.txt b/file foo.txt\n"
1101             "index 1234567..9876543 100644\n"
1102             "--- a/file foo.txt\n"
1103             "+++ b/file foo.txt\n"
1104             "@@ -1,1 +1,1 @@\n"
1105             "-ABCD\n"
1106             "\\ No newline at end of file\n"
1107             "+ABCD\n";
1108 
1109     fileData1.leftFileInfo = DiffFileInfo("file foo.txt", "1234567");
1110     fileData1.rightFileInfo = DiffFileInfo("file foo.txt", "9876543");
1111     fileData1.fileOperation = FileData::ChangeFile;
1112     chunkData1.leftStartingLineNumber = 0;
1113     chunkData1.rightStartingLineNumber = 0;
1114     rows1.clear();
1115     rows1 << RowData(_("ABCD"));
1116     rows1 << RowData(TextLineData::Separator, _(""));
1117     chunkData1.rows = rows1;
1118     fileData1.chunks.clear();
1119     fileData1.chunks << chunkData1;
1120 
1121     QList<FileData> fileDataList3;
1122     fileDataList3 << fileData1;
1123 
1124     QTest::newRow("Last newline added to a line without newline") << patch
1125                                 << fileDataList3;
1126 
1127     patch = "diff --git a/difftest.txt b/difftest.txt\n"
1128             "index 1234567..9876543 100644\n"
1129             "--- a/difftest.txt\n"
1130             "+++ b/difftest.txt\n"
1131             "@@ -2,5 +2,5 @@ void func()\n"
1132             " A\n"
1133             " B\n"
1134             "-C\n"
1135             "+Z\n"
1136             " D\n"
1137             " \n"
1138             "@@ -9,2 +9,4 @@ void OtherFunc()\n"
1139             " \n"
1140             " D\n"
1141             "+E\n"
1142             "+F\n"
1143             ;
1144 
1145     fileData1.leftFileInfo = DiffFileInfo("difftest.txt", "1234567");
1146     fileData1.rightFileInfo = DiffFileInfo("difftest.txt", "9876543");
1147     fileData1.fileOperation = FileData::ChangeFile;
1148     chunkData1.leftStartingLineNumber = 1;
1149     chunkData1.rightStartingLineNumber = 1;
1150     rows1.clear();
1151     rows1 << RowData(_("A"));
1152     rows1 << RowData(_("B"));
1153     rows1 << RowData(_("C"), _("Z"));
1154     rows1 << RowData(_("D"));
1155     rows1 << RowData(_(""));
1156     chunkData1.rows = rows1;
1157 
1158     chunkData2.leftStartingLineNumber = 8;
1159     chunkData2.rightStartingLineNumber = 8;
1160     rows2.clear();
1161     rows2 << RowData(_(""));
1162     rows2 << RowData(_("D"));
1163     rows2 << RowData(TextLineData::Separator, _("E"));
1164     rows2 << RowData(TextLineData::Separator, _("F"));
1165     chunkData2.rows = rows2;
1166     fileData1.chunks.clear();
1167     fileData1.chunks << chunkData1;
1168     fileData1.chunks << chunkData2;
1169 
1170     QList<FileData> fileDataList4;
1171     fileDataList4 << fileData1;
1172 
1173     QTest::newRow("2 chunks - first ends with blank line") << patch
1174                                 << fileDataList4;
1175 
1176     //////////////
1177 
1178     patch = "diff --git a/file foo.txt b/file foo.txt\n"
1179             "index 1234567..9876543 100644\n"
1180             "--- a/file foo.txt\n"
1181             "+++ b/file foo.txt\n"
1182             "@@ -1,1 +1,3 @@ void DiffEditor::ctor()\n"
1183             " ABCD\n"
1184             "+\n"
1185             "+EFGH\n"
1186             "\\ No newline at end of file\n";
1187 
1188     fileData1.leftFileInfo = DiffFileInfo("file foo.txt", "1234567");
1189     fileData1.rightFileInfo = DiffFileInfo("file foo.txt", "9876543");
1190     fileData1.fileOperation = FileData::ChangeFile;
1191     chunkData1.leftStartingLineNumber = 0;
1192     chunkData1.rightStartingLineNumber = 0;
1193     rows1.clear();
1194     rows1 << RowData(_("ABCD"));
1195     rows1 << RowData(TextLineData::Separator, _(""));
1196     rows1 << RowData(_(""), _("EFGH"));
1197     chunkData1.rows = rows1;
1198     fileData1.chunks.clear();
1199     fileData1.chunks << chunkData1;
1200 
1201     QList<FileData> fileDataList5;
1202     fileDataList5 << fileData1;
1203 
1204     QTest::newRow("Blank line followed by No newline") << patch
1205                                 << fileDataList5;
1206 
1207     //////////////
1208 
1209     // Based on 953cdb97
1210     patch = "diff --git a/src/plugins/texteditor/basetextdocument.h b/src/plugins/texteditor/textdocument.h\n"
1211             "similarity index 100%\n"
1212             "rename from src/plugins/texteditor/basetextdocument.h\n"
1213             "rename to src/plugins/texteditor/textdocument.h\n"
1214             "diff --git a/src/plugins/texteditor/basetextdocumentlayout.cpp b/src/plugins/texteditor/textdocumentlayout.cpp\n"
1215             "similarity index 79%\n"
1216             "rename from src/plugins/texteditor/basetextdocumentlayout.cpp\n"
1217             "rename to src/plugins/texteditor/textdocumentlayout.cpp\n"
1218             "index 0121933..01cc3a0 100644\n"
1219             "--- a/src/plugins/texteditor/basetextdocumentlayout.cpp\n"
1220             "+++ b/src/plugins/texteditor/textdocumentlayout.cpp\n"
1221             "@@ -2,5 +2,5 @@ void func()\n"
1222             " A\n"
1223             " B\n"
1224             "-C\n"
1225             "+Z\n"
1226             " D\n"
1227             " \n"
1228             ;
1229 
1230     fileData1 = FileData();
1231     fileData1.leftFileInfo = DiffFileInfo("src/plugins/texteditor/basetextdocument.h");
1232     fileData1.rightFileInfo = DiffFileInfo("src/plugins/texteditor/textdocument.h");
1233     fileData1.fileOperation = FileData::RenameFile;
1234     fileData2 = FileData();
1235     fileData2.leftFileInfo = DiffFileInfo("src/plugins/texteditor/basetextdocumentlayout.cpp", "0121933");
1236     fileData2.rightFileInfo = DiffFileInfo("src/plugins/texteditor/textdocumentlayout.cpp", "01cc3a0");
1237     fileData2.fileOperation = FileData::RenameFile;
1238     chunkData2.leftStartingLineNumber = 1;
1239     chunkData2.rightStartingLineNumber = 1;
1240     rows2.clear();
1241     rows2 << RowData(_("A"));
1242     rows2 << RowData(_("B"));
1243     rows2 << RowData(_("C"), _("Z"));
1244     rows2 << RowData(_("D"));
1245     rows2 << RowData(_(""));
1246     chunkData2.rows = rows2;
1247     fileData2.chunks.clear();
1248     fileData2.chunks << chunkData2;
1249 
1250     QList<FileData> fileDataList6;
1251     fileDataList6 << fileData1 << fileData2;
1252 
1253     QTest::newRow("Multiple renames") << patch
1254                                       << fileDataList6;
1255 
1256     //////////////
1257 
1258     // Dirty submodule
1259     patch = "diff --git a/src/shared/qbs b/src/shared/qbs\n"
1260             "--- a/src/shared/qbs\n"
1261             "+++ b/src/shared/qbs\n"
1262             "@@ -1 +1 @@\n"
1263             "-Subproject commit eda76354077a427d692fee05479910de31040d3f\n"
1264             "+Subproject commit eda76354077a427d692fee05479910de31040d3f-dirty\n"
1265             ;
1266     fileData1 = FileData();
1267     fileData1.leftFileInfo = DiffFileInfo("src/shared/qbs");
1268     fileData1.rightFileInfo = DiffFileInfo("src/shared/qbs");
1269     chunkData1.leftStartingLineNumber = 0;
1270     chunkData1.rightStartingLineNumber = 0;
1271     rows1.clear();
1272     rows1 << RowData(_("Subproject commit eda76354077a427d692fee05479910de31040d3f"),
1273                      _("Subproject commit eda76354077a427d692fee05479910de31040d3f-dirty"));
1274     chunkData1.rows = rows1;
1275     fileData1.chunks.clear();
1276     fileData1.chunks <<  chunkData1;
1277 
1278     QList<FileData> fileDataList7;
1279     fileDataList7 << fileData1;
1280     QTest::newRow("Dirty submodule") << patch
1281                                      << fileDataList7;
1282 
1283     //////////////
1284     patch = "diff --git a/demos/arthurplugin/arthurplugin.pro b/demos/arthurplugin/arthurplugin.pro\n"
1285             "new file mode 100644\n"
1286             "index 0000000..c5132b4\n"
1287             "--- /dev/null\n"
1288             "+++ b/demos/arthurplugin/arthurplugin.pro\n"
1289             "@@ -0,0 +1 @@\n"
1290             "+XXX\n"
1291             "diff --git a/demos/arthurplugin/bg1.jpg b/demos/arthurplugin/bg1.jpg\n"
1292             "new file mode 100644\n"
1293             "index 0000000..dfc7cee\n"
1294             "Binary files /dev/null and b/demos/arthurplugin/bg1.jpg differ\n"
1295             "diff --git a/demos/arthurplugin/flower.jpg b/demos/arthurplugin/flower.jpg\n"
1296             "new file mode 100644\n"
1297             "index 0000000..f8e022c\n"
1298             "Binary files /dev/null and b/demos/arthurplugin/flower.jpg differ\n"
1299             ;
1300 
1301     fileData1 = FileData();
1302     fileData1.leftFileInfo = DiffFileInfo("demos/arthurplugin/arthurplugin.pro", "0000000");
1303     fileData1.rightFileInfo = DiffFileInfo("demos/arthurplugin/arthurplugin.pro", "c5132b4");
1304     fileData1.fileOperation = FileData::NewFile;
1305     chunkData1 = ChunkData();
1306     chunkData1.leftStartingLineNumber = -1;
1307     chunkData1.rightStartingLineNumber = 0;
1308     rows1.clear();
1309     rows1 << RowData(TextLineData::Separator, _("XXX"));
1310     rows1 << RowData(TextLineData::Separator, TextLineData(TextLineData::TextLine));
1311     chunkData1.rows = rows1;
1312     fileData1.chunks << chunkData1;
1313     fileData2 = FileData();
1314     fileData2.leftFileInfo = DiffFileInfo("demos/arthurplugin/bg1.jpg", "0000000");
1315     fileData2.rightFileInfo = DiffFileInfo("demos/arthurplugin/bg1.jpg", "dfc7cee");
1316     fileData2.fileOperation = FileData::NewFile;
1317     fileData2.binaryFiles = true;
1318     fileData3 = FileData();
1319     fileData3.leftFileInfo = DiffFileInfo("demos/arthurplugin/flower.jpg", "0000000");
1320     fileData3.rightFileInfo = DiffFileInfo("demos/arthurplugin/flower.jpg", "f8e022c");
1321     fileData3.fileOperation = FileData::NewFile;
1322     fileData3.binaryFiles = true;
1323 
1324     QList<FileData> fileDataList8;
1325     fileDataList8 << fileData1 << fileData2 << fileData3;
1326 
1327     QTest::newRow("Binary files") << patch
1328                                   << fileDataList8;
1329 
1330     //////////////
1331     patch = "diff --git a/script.sh b/script.sh\n"
1332             "old mode 100644\n"
1333             "new mode 100755\n"
1334             ;
1335 
1336     fileData1 = FileData();
1337     fileData1.leftFileInfo = DiffFileInfo("script.sh");
1338     fileData1.rightFileInfo = DiffFileInfo("script.sh");
1339     fileData1.fileOperation = FileData::ChangeMode;
1340 
1341     QList<FileData> fileDataList9;
1342     fileDataList9 << fileData1;
1343 
1344     QTest::newRow("Mode change") << patch << fileDataList9;
1345 
1346     //////////////
1347 
1348     // Subversion New
1349     patch = "Index: src/plugins/subversion/subversioneditor.cpp\n"
1350             "===================================================================\n"
1351             "--- src/plugins/subversion/subversioneditor.cpp\t(revision 0)\n"
1352             "+++ src/plugins/subversion/subversioneditor.cpp\t(revision 0)\n"
1353             "@@ -0,0 +125 @@\n\n";
1354     fileData1 = FileData();
1355     fileData1.leftFileInfo = DiffFileInfo("src/plugins/subversion/subversioneditor.cpp");
1356     fileData1.rightFileInfo = DiffFileInfo("src/plugins/subversion/subversioneditor.cpp");
1357     chunkData1 = ChunkData();
1358     chunkData1.leftStartingLineNumber = -1;
1359     chunkData1.rightStartingLineNumber = 124;
1360     fileData1.chunks << chunkData1;
1361     QList<FileData> fileDataList21;
1362     fileDataList21 << fileData1;
1363     QTest::newRow("Subversion New") << patch
1364                                     << fileDataList21;
1365 
1366     //////////////
1367 
1368     // Subversion Deleted
1369     patch = "Index: src/plugins/subversion/subversioneditor.cpp\n"
1370             "===================================================================\n"
1371             "--- src/plugins/subversion/subversioneditor.cpp\t(revision 42)\n"
1372             "+++ src/plugins/subversion/subversioneditor.cpp\t(working copy)\n"
1373             "@@ -1,125 +0,0 @@\n\n";
1374     fileData1 = FileData();
1375     fileData1.leftFileInfo = DiffFileInfo("src/plugins/subversion/subversioneditor.cpp");
1376     fileData1.rightFileInfo = DiffFileInfo("src/plugins/subversion/subversioneditor.cpp");
1377     chunkData1 = ChunkData();
1378     chunkData1.leftStartingLineNumber = 0;
1379     chunkData1.rightStartingLineNumber = -1;
1380     fileData1.chunks << chunkData1;
1381     QList<FileData> fileDataList22;
1382     fileDataList22 << fileData1;
1383     QTest::newRow("Subversion Deleted") << patch
1384                                         << fileDataList22;
1385 
1386     //////////////
1387 
1388     // Subversion Normal
1389     patch = "Index: src/plugins/subversion/subversioneditor.cpp\n"
1390             "===================================================================\n"
1391             "--- src/plugins/subversion/subversioneditor.cpp\t(revision 42)\n"
1392             "+++ src/plugins/subversion/subversioneditor.cpp\t(working copy)\n"
1393             "@@ -120,7 +120,7 @@\n\n";
1394     fileData1 = FileData();
1395     fileData1.leftFileInfo = DiffFileInfo("src/plugins/subversion/subversioneditor.cpp");
1396     fileData1.rightFileInfo = DiffFileInfo("src/plugins/subversion/subversioneditor.cpp");
1397     chunkData1 = ChunkData();
1398     chunkData1.leftStartingLineNumber = 119;
1399     chunkData1.rightStartingLineNumber = 119;
1400     fileData1.chunks << chunkData1;
1401     QList<FileData> fileDataList23;
1402     fileDataList23 << fileData1;
1403     QTest::newRow("Subversion Normal") << patch
1404                                        << fileDataList23;
1405 }
1406 
testReadPatch()1407 void DiffEditor::Internal::DiffEditorPlugin::testReadPatch()
1408 {
1409     QFETCH(QString, sourcePatch);
1410     QFETCH(QList<FileData>, fileDataList);
1411 
1412     bool ok;
1413     const QList<FileData> &result = DiffUtils::readPatch(sourcePatch, &ok);
1414 
1415     QVERIFY(ok);
1416     QCOMPARE(result.count(), fileDataList.count());
1417     for (int i = 0; i < fileDataList.count(); i++) {
1418         const FileData &origFileData = fileDataList.at(i);
1419         const FileData &resultFileData = result.at(i);
1420         QCOMPARE(resultFileData.leftFileInfo.fileName, origFileData.leftFileInfo.fileName);
1421         QCOMPARE(resultFileData.leftFileInfo.typeInfo, origFileData.leftFileInfo.typeInfo);
1422         QCOMPARE(resultFileData.rightFileInfo.fileName, origFileData.rightFileInfo.fileName);
1423         QCOMPARE(resultFileData.rightFileInfo.typeInfo, origFileData.rightFileInfo.typeInfo);
1424         QCOMPARE(resultFileData.chunks.count(), origFileData.chunks.count());
1425         QCOMPARE(resultFileData.fileOperation, origFileData.fileOperation);
1426         for (int j = 0; j < origFileData.chunks.count(); j++) {
1427             const ChunkData &origChunkData = origFileData.chunks.at(j);
1428             const ChunkData &resultChunkData = resultFileData.chunks.at(j);
1429             QCOMPARE(resultChunkData.leftStartingLineNumber, origChunkData.leftStartingLineNumber);
1430             QCOMPARE(resultChunkData.rightStartingLineNumber, origChunkData.rightStartingLineNumber);
1431             QCOMPARE(resultChunkData.contextChunk, origChunkData.contextChunk);
1432             QCOMPARE(resultChunkData.rows.count(), origChunkData.rows.count());
1433             for (int k = 0; k < origChunkData.rows.count(); k++) {
1434                 const RowData &origRowData = origChunkData.rows.at(k);
1435                 const RowData &resultRowData = resultChunkData.rows.at(k);
1436                 QCOMPARE(resultRowData.equal, origRowData.equal);
1437                 QCOMPARE(resultRowData.leftLine.text, origRowData.leftLine.text);
1438                 QCOMPARE(resultRowData.leftLine.textLineType, origRowData.leftLine.textLineType);
1439                 QCOMPARE(resultRowData.rightLine.text, origRowData.rightLine.text);
1440                 QCOMPARE(resultRowData.rightLine.textLineType, origRowData.rightLine.textLineType);
1441             }
1442         }
1443     }
1444 }
1445 
1446 using ListOfStringPairs = QList<QPair<QString, QString>>;
1447 
testFilterPatch_data()1448 void DiffEditor::Internal::DiffEditorPlugin::testFilterPatch_data()
1449 {
1450     QTest::addColumn<ChunkData>("chunk");
1451     QTest::addColumn<ListOfStringPairs>("rows");
1452     QTest::addColumn<ChunkSelection>("selection");
1453     QTest::addColumn<bool>("revert");
1454 
1455     auto createChunk = []() {
1456         ChunkData chunk;
1457         chunk.contextInfo = "void DiffEditor::ctor()";
1458         chunk.contextChunk = false;
1459         chunk.leftStartingLineNumber = 49;
1460         chunk.rightStartingLineNumber = 49;
1461         return chunk;
1462     };
1463     auto appendRow = [](ChunkData *chunk, const QString &left, const QString &right) {
1464         RowData row;
1465         row.equal = (left == right);
1466         row.leftLine.text = left;
1467         row.leftLine.textLineType = left.isEmpty() ? TextLineData::Separator : TextLineData::TextLine;
1468         row.rightLine.text = right;
1469         row.rightLine.textLineType = right.isEmpty() ? TextLineData::Separator : TextLineData::TextLine;
1470         chunk->rows.append(row);
1471     };
1472     ChunkData chunk;
1473     ListOfStringPairs rows;
1474 
1475     chunk = createChunk();
1476     appendRow(&chunk, "A", "A"); // 50
1477     appendRow(&chunk, "",  "B"); // 51 +
1478     appendRow(&chunk, "C", "C"); // 52
1479     rows = ListOfStringPairs {
1480         {"A", "A"},
1481         {"", "B"},
1482         {"C", "C"}
1483     };
1484     QTest::newRow("one added") << chunk << rows << ChunkSelection() << false;
1485 
1486     chunk = createChunk();
1487     appendRow(&chunk, "A", "A"); // 50
1488     appendRow(&chunk, "B", "");  // 51 -
1489     appendRow(&chunk, "C", "C"); // 52
1490     rows = ListOfStringPairs {
1491         {"A", "A"},
1492         {"B", ""},
1493         {"C", "C"}
1494     };
1495     QTest::newRow("one removed") << chunk << rows << ChunkSelection() << false;
1496 
1497     chunk = createChunk();
1498     appendRow(&chunk, "A", "A"); // 50
1499     appendRow(&chunk, "",  "B"); // 51
1500     appendRow(&chunk, "",  "C"); // 52 +
1501     appendRow(&chunk, "",  "D"); // 53 +
1502     appendRow(&chunk, "",  "E"); // 54
1503     appendRow(&chunk, "F", "F"); // 55
1504     rows = ListOfStringPairs {
1505         {"A", "A"},
1506         {"", "C"},
1507         {"", "D"},
1508         {"F", "F"}
1509     };
1510     QTest::newRow("stage selected added") << chunk << rows << ChunkSelection({2, 3}, {2, 3}) << false;
1511 
1512     chunk = createChunk();
1513     appendRow(&chunk, "A", "A"); // 50
1514     appendRow(&chunk, "",  "B"); // 51 +
1515     appendRow(&chunk, "C", "D"); // 52
1516     appendRow(&chunk, "E", "E"); // 53
1517     rows = ListOfStringPairs {
1518         {"A", "A"},
1519         {"", "B"},
1520         {"C", "C"},
1521         {"E", "E"}
1522     };
1523     QTest::newRow("stage selected added keep changed") << chunk << rows << ChunkSelection({1}, {1}) << false;
1524 
1525     chunk = createChunk();
1526     appendRow(&chunk, "A", "A"); // 50
1527     appendRow(&chunk, "B", "");  // 51
1528     appendRow(&chunk, "C", "");  // 52 -
1529     appendRow(&chunk, "D", "");  // 53 -
1530     appendRow(&chunk, "E", "");  // 54
1531     appendRow(&chunk, "F", "F"); // 55
1532     rows = ListOfStringPairs {
1533         {"A", "A"},
1534         {"B", "B"},
1535         {"C", ""},
1536         {"D", ""},
1537         {"E", "E"},
1538         {"F", "F"}
1539     };
1540     QTest::newRow("stage selected removed") << chunk << rows << ChunkSelection({2, 3}, {2, 3}) << false;
1541 
1542     chunk = createChunk();
1543     appendRow(&chunk, "A", "A"); // 50
1544     appendRow(&chunk, "B", "");  // 51
1545     appendRow(&chunk, "C", "");  // 52 -
1546     appendRow(&chunk, "",  "D"); // 53 +
1547     appendRow(&chunk, "",  "E"); // 54
1548     appendRow(&chunk, "F", "F"); // 55
1549     rows = ListOfStringPairs {
1550         {"A", "A"},
1551         {"B", "B"},
1552         {"C", ""},
1553         {"", "D"},
1554         {"F", "F"}
1555     };
1556     QTest::newRow("stage selected added/removed") << chunk << rows << ChunkSelection({2, 3}, {2, 3}) << false;
1557 
1558     chunk = createChunk();
1559     appendRow(&chunk, "A", "A"); // 50
1560     appendRow(&chunk, "B", "C"); // 51 -/+
1561     appendRow(&chunk, "D", "D"); // 52
1562     rows = ListOfStringPairs {
1563         {"A", "A"},
1564         {"B", "C"},
1565         {"D", "D"}
1566     };
1567     QTest::newRow("stage modified row") << chunk << rows << ChunkSelection({1}, {1}) << false;
1568 
1569     chunk = createChunk();
1570     appendRow(&chunk, "A", "A"); // 50
1571     appendRow(&chunk, "B", "C"); // 51 -/+
1572     appendRow(&chunk, "D", "D"); // 52
1573     rows = ListOfStringPairs {
1574         {"A", "A"},
1575         {"B", "C"},
1576         {"D", "D"}
1577     };
1578     QTest::newRow("stage modified and unmodified rows") << chunk << rows << ChunkSelection({0, 1, 2}, {0, 1, 2}) << false;
1579 
1580     chunk = createChunk();
1581     appendRow(&chunk, "A", "A"); // 50
1582     appendRow(&chunk, "B", "C"); // 51 -/+
1583     appendRow(&chunk, "D", "D"); // 52
1584     rows = ListOfStringPairs {
1585         {"A", "A"},
1586         {"B", "C"},
1587         {"D", "D"}
1588     };
1589     QTest::newRow("stage unmodified left rows") << chunk << rows << ChunkSelection({0, 1, 2}, {1}) << false;
1590 
1591     chunk = createChunk();
1592     appendRow(&chunk, "A", "A"); // 50
1593     appendRow(&chunk, "B", "C"); // 51 -/+
1594     appendRow(&chunk, "D", "D"); // 52
1595     rows = ListOfStringPairs {
1596         {"A", "A"},
1597         {"B", "C"},
1598         {"D", "D"}
1599     };
1600     QTest::newRow("stage unmodified right rows") << chunk << rows << ChunkSelection({1}, {0, 1, 2}) << false;
1601 
1602     chunk = createChunk();
1603     appendRow(&chunk, "A", "A"); // 50
1604     appendRow(&chunk, "B", "C"); // 51 -/+
1605     appendRow(&chunk, "D", "D"); // 52
1606     rows = ListOfStringPairs {
1607         {"A", "A"},
1608         {"B", ""},
1609         {"D", "D"}
1610     };
1611     QTest::newRow("stage left only") << chunk << rows << ChunkSelection({1}, {}) << false;
1612 
1613     chunk = createChunk();
1614     appendRow(&chunk, "A", "A"); // 50
1615     appendRow(&chunk, "B", "C"); // 51 -/+
1616     appendRow(&chunk, "D", "D"); // 52
1617     rows = ListOfStringPairs {
1618         {"A", "A"},
1619         {"B", "B"},
1620         {"", "C"},
1621         {"D", "D"}
1622     };
1623     QTest::newRow("stage right only") << chunk << rows << ChunkSelection({}, {1}) << false;
1624 
1625     chunk = createChunk();
1626     appendRow(&chunk, "A", "A"); // 50
1627     appendRow(&chunk, "B", "C"); // 51 -/+
1628     appendRow(&chunk, "D", "D"); // 52
1629     rows = ListOfStringPairs {
1630         {"A", "A"},
1631         {"B", "C"},
1632         {"D", "D"}
1633     };
1634     QTest::newRow("stage modified row and revert") << chunk << rows << ChunkSelection({1}, {1}) << true;
1635 
1636     chunk = createChunk();
1637     appendRow(&chunk, "A", "A"); // 50
1638     appendRow(&chunk, "B", "C"); // 51 -/+
1639     appendRow(&chunk, "D", "D"); // 52
1640     rows = ListOfStringPairs {
1641         {"A", "A"},
1642         {"B", ""},
1643         {"C", "C"},
1644         {"D", "D"}
1645     };
1646     // symmetric to: "stage right only"
1647     QTest::newRow("stage left only and revert") << chunk << rows << ChunkSelection({1}, {}) << true;
1648 
1649     chunk = createChunk();
1650     appendRow(&chunk, "A", "A"); // 50
1651     appendRow(&chunk, "B", "C"); // 51 -/+
1652     appendRow(&chunk, "D", "D"); // 52
1653     rows = ListOfStringPairs {
1654         {"A", "A"},
1655         {"", "C"},
1656         {"D", "D"}
1657     };
1658     // symmetric to: "stage left only"
1659     QTest::newRow("stage right only and revert") << chunk << rows << ChunkSelection({}, {1}) << true;
1660 
1661 }
1662 
testFilterPatch()1663 void DiffEditor::Internal::DiffEditorPlugin::testFilterPatch()
1664 {
1665     QFETCH(ChunkData, chunk);
1666     QFETCH(ListOfStringPairs, rows);
1667     QFETCH(ChunkSelection, selection);
1668     QFETCH(bool, revert);
1669 
1670     const ChunkData result = DiffEditorDocument::filterChunk(chunk, selection, revert);
1671     QCOMPARE(result.rows.size(), rows.size());
1672     for (int i = 0; i < rows.size(); ++i) {
1673         QCOMPARE(result.rows.at(i).leftLine.text, rows.at(i).first);
1674         QCOMPARE(result.rows.at(i).rightLine.text, rows.at(i).second);
1675     }
1676 }
1677 
1678 #endif // WITH_TESTS
1679 
1680 #include "diffeditorplugin.moc"
1681