File indexing completed on 2024-05-12 05:52:06

0001 /*
0002     SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 #include "diffwidget.h"
0006 #include "gitdiff.h"
0007 #include "gitprocess.h"
0008 #include "hostprocess.h"
0009 #include "ktexteditor_utils.h"
0010 
0011 #include <QApplication>
0012 #include <QFileInfo>
0013 #include <QHBoxLayout>
0014 #include <QPainter>
0015 #include <QPainterPath>
0016 #include <QPointer>
0017 #include <QRegularExpression>
0018 #include <QScopedValueRollback>
0019 #include <QScrollBar>
0020 #include <QSyntaxHighlighter>
0021 #include <QTemporaryFile>
0022 #include <QTimer>
0023 #include <QToolBar>
0024 #include <QToolButton>
0025 
0026 #include <KConfigGroup>
0027 #include <KLocalizedString>
0028 #include <KSharedConfig>
0029 #include <KSyntaxHighlighting/Definition>
0030 #include <KSyntaxHighlighting/Format>
0031 #include <KSyntaxHighlighting/Repository>
0032 #include <KTextEditor/Editor>
0033 
0034 DiffWidget *DiffWidgetManager::existingDiffWidgetForParams(KTextEditor::MainWindow *mw, const DiffParams &p)
0035 {
0036     const auto widgets = Utils::widgets(mw);
0037     for (auto widget : widgets) {
0038         auto diffWidget = qobject_cast<DiffWidget *>(widget);
0039         if (!diffWidget) {
0040             continue;
0041         }
0042 
0043         if (diffWidget->m_params.arguments == p.arguments) {
0044             return diffWidget;
0045             break;
0046         }
0047     }
0048     return nullptr;
0049 }
0050 
0051 void DiffWidgetManager::openDiff(const QByteArray &diff, DiffParams p, class KTextEditor::MainWindow *mw)
0052 {
0053     DiffWidget *existing = existingDiffWidgetForParams(mw, p);
0054     if (!existing) {
0055         existing = new DiffWidget(p);
0056         if (!p.tabTitle.isEmpty()) {
0057             existing->setWindowTitle(p.tabTitle);
0058         } else {
0059             if (p.destFile.isEmpty())
0060                 existing->setWindowTitle(i18n("Diff %1", Utils::fileNameFromPath(p.srcFile)));
0061             else
0062                 existing->setWindowTitle(i18n("Diff %1..%2", Utils::fileNameFromPath(p.srcFile), Utils::fileNameFromPath(p.destFile)));
0063         }
0064         existing->setWindowIcon(QIcon::fromTheme(QStringLiteral("text-x-patch")));
0065         existing->openDiff(diff);
0066         Utils::addWidget(existing, mw);
0067     } else {
0068         existing->clearData();
0069         existing->m_params = p;
0070         existing->openDiff(diff);
0071         Utils::activateWidget(existing, mw);
0072     }
0073 }
0074 
0075 void DiffWidgetManager::diffDocs(KTextEditor::Document *l, KTextEditor::Document *r, class KTextEditor::MainWindow *mw)
0076 {
0077     DiffParams p;
0078     p.arguments = DiffWidget::diffDocsGitArgs(l, r);
0079     DiffWidget *existing = existingDiffWidgetForParams(mw, p);
0080     if (!existing) {
0081         existing = new DiffWidget(p);
0082         existing->diffDocs(l, r);
0083         existing->setWindowTitle(i18n("Diff %1 .. %2", l->documentName(), r->documentName()));
0084         existing->setWindowIcon(QIcon::fromTheme(QStringLiteral("text-x-patch")));
0085         Utils::addWidget(existing, mw);
0086     } else {
0087         existing->clearData();
0088         existing->m_params = p;
0089         existing->diffDocs(l, r);
0090         Utils::activateWidget(existing, mw);
0091     }
0092 }
0093 
0094 class Toolbar : public QToolBar
0095 {
0096     Q_OBJECT
0097 public:
0098     Toolbar(QWidget *parent)
0099         : QToolBar(parent)
0100     {
0101         setContentsMargins({});
0102         if (layout()) {
0103             layout()->setContentsMargins({});
0104         }
0105 
0106         setToolButtonStyle(Qt::ToolButtonIconOnly);
0107 
0108         KConfigGroup cgGeneral = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
0109         bool show = cgGeneral.readEntry("DiffWidget Show Commit Info", true);
0110 
0111         m_showCommitInfoAction = addAction(QIcon::fromTheme(QStringLiteral("view-visible")), QString());
0112         m_showCommitInfoAction->setCheckable(true);
0113         m_showCommitInfoAction->setToolTip(i18n("Show/Hide Commit Info"));
0114         m_showCommitInfoAction->setChecked(show);
0115         connect(m_showCommitInfoAction, &QAction::toggled, this, &Toolbar::showCommitInfoChanged);
0116 
0117         m_showNextFile = addAction(QIcon::fromTheme(QStringLiteral("arrow-down-double")), QString());
0118         m_showNextFile->setToolTip(i18n("Jump to Next File"));
0119         connect(m_showNextFile, &QAction::triggered, this, &Toolbar::jumpToNextFile);
0120 
0121         m_showPrevFile = addAction(QIcon::fromTheme(QStringLiteral("arrow-up-double")), QString());
0122         m_showPrevFile->setToolTip(i18n("Jump to Previous File"));
0123         connect(m_showPrevFile, &QAction::triggered, this, &Toolbar::jumpToPrevFile);
0124 
0125         m_showNextHunk = addAction(QIcon::fromTheme(QStringLiteral("arrow-down")), QString());
0126         m_showNextHunk->setToolTip(i18n("Jump to Next Hunk"));
0127         connect(m_showNextHunk, &QAction::triggered, this, &Toolbar::jumpToNextHunk);
0128 
0129         m_showPrevHunk = addAction(QIcon::fromTheme(QStringLiteral("arrow-up")), QString());
0130         m_showPrevHunk->setToolTip(i18n("Jump to Previous Hunk"));
0131         connect(m_showPrevHunk, &QAction::triggered, this, &Toolbar::jumpToPrevHunk);
0132 
0133         m_reload = addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), QString());
0134         m_reload->setToolTip(i18nc("Tooltip for a button, clicking the button reloads the diff", "Reload Diff"));
0135         connect(m_reload, &QAction::triggered, this, &Toolbar::reload);
0136 
0137         m_fullContext = addAction(QIcon::fromTheme(QStringLiteral("view-fullscreen")), QString());
0138         m_fullContext->setToolTip(i18nc("Tooltip for a button", "Show diff with full context, not just the changed lines"));
0139         connect(m_fullContext, &QAction::triggered, this, &Toolbar::showWithFullContext);
0140     }
0141 
0142     void setShowCommitActionVisible(bool vis)
0143     {
0144         if (m_showCommitInfoAction->isVisible() != vis) {
0145             m_showCommitInfoAction->setVisible(vis);
0146         }
0147     }
0148 
0149     bool showCommitInfo()
0150     {
0151         return m_showCommitInfoAction->isChecked();
0152     }
0153 
0154     void hideShowFullContext()
0155     {
0156         m_fullContext->setVisible(false);
0157     }
0158 
0159 private:
0160     QAction *m_showCommitInfoAction;
0161     QAction *m_showNextFile;
0162     QAction *m_showPrevFile;
0163     QAction *m_showNextHunk;
0164     QAction *m_showPrevHunk;
0165     QAction *m_reload;
0166     QAction *m_fullContext;
0167 
0168 Q_SIGNALS:
0169     void showCommitInfoChanged(bool);
0170     void jumpToNextFile();
0171     void jumpToPrevFile();
0172     void jumpToNextHunk();
0173     void jumpToPrevHunk();
0174     void reload();
0175     void showWithFullContext();
0176 };
0177 
0178 static void syncScroll(QPlainTextEdit *src, QPlainTextEdit *tgt)
0179 {
0180     int srcValue = src->verticalScrollBar()->value();
0181     const auto srcBlock = src->document()->findBlockByLineNumber(srcValue);
0182     auto tgtBlock = tgt->document()->findBlockByLineNumber(srcValue);
0183     if (srcBlock.blockNumber() == tgtBlock.blockNumber()) {
0184         tgt->verticalScrollBar()->setValue(srcValue);
0185     } else {
0186         tgtBlock = tgt->document()->findBlockByNumber(srcBlock.blockNumber());
0187         tgt->verticalScrollBar()->setValue(tgtBlock.firstLineNumber());
0188     }
0189 }
0190 
0191 DiffWidget::DiffWidget(DiffParams p, QWidget *parent)
0192     : QWidget(parent)
0193     , m_left(new DiffEditor(p.flags, this))
0194     , m_right(new DiffEditor(p.flags, this))
0195     , m_commitInfo(new QPlainTextEdit(this))
0196     , m_toolbar(new Toolbar(this))
0197     , m_params(p)
0198 {
0199     auto layout = new QVBoxLayout(this);
0200     layout->setSpacing(2);
0201     layout->setContentsMargins({});
0202     layout->addWidget(m_commitInfo);
0203     layout->addWidget(m_toolbar);
0204     auto diffLayout = new QHBoxLayout;
0205     diffLayout->setContentsMargins({});
0206     diffLayout->addWidget(m_left);
0207     diffLayout->addWidget(m_right);
0208     layout->addLayout(diffLayout);
0209 
0210     leftHl = new DiffSyntaxHighlighter(m_left->document(), this);
0211     rightHl = new DiffSyntaxHighlighter(m_right->document(), this);
0212     leftHl->setTheme(KTextEditor::Editor::instance()->theme());
0213     rightHl->setTheme(KTextEditor::Editor::instance()->theme());
0214 
0215     connect(m_left->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int) {
0216         if (m_stopScrollSync) {
0217             return;
0218         }
0219         m_stopScrollSync = true;
0220         syncScroll(m_left, m_right);
0221         m_stopScrollSync = false;
0222     });
0223     connect(m_right->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int) {
0224         if (m_stopScrollSync) {
0225             return;
0226         }
0227         m_stopScrollSync = true;
0228         syncScroll(m_right, m_left);
0229         m_stopScrollSync = false;
0230     });
0231 
0232     for (auto *e : {m_left, m_right}) {
0233         connect(e, &DiffEditor::switchStyle, this, &DiffWidget::handleStyleChange);
0234         connect(e, &DiffEditor::actionTriggered, this, &DiffWidget::handleStageUnstage);
0235     }
0236 
0237     m_commitInfo->hide();
0238     m_commitInfo->setWordWrapMode(QTextOption::WordWrap);
0239     m_commitInfo->setFont(Utils::editorFont());
0240     m_commitInfo->setReadOnly(true);
0241     m_commitInfo->setTextInteractionFlags(Qt::TextSelectableByMouse);
0242     m_commitInfo->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
0243     m_commitInfo->setMaximumHeight(250);
0244 
0245     connect(m_toolbar, &Toolbar::showCommitInfoChanged, this, [this](bool v) {
0246         m_commitInfo->setVisible(v);
0247         KConfigGroup cgGeneral = KConfigGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
0248         cgGeneral.writeEntry("DiffWidget Show Commit Info", v);
0249     });
0250     connect(m_toolbar, &Toolbar::jumpToNextFile, this, &DiffWidget::jumpToNextFile);
0251     connect(m_toolbar, &Toolbar::jumpToPrevFile, this, &DiffWidget::jumpToPrevFile);
0252     connect(m_toolbar, &Toolbar::jumpToNextHunk, this, &DiffWidget::jumpToNextHunk);
0253     connect(m_toolbar, &Toolbar::jumpToPrevHunk, this, &DiffWidget::jumpToPrevHunk);
0254     connect(m_toolbar, &Toolbar::reload, this, &DiffWidget::runGitDiff);
0255     connect(m_toolbar, &Toolbar::showWithFullContext, this, &DiffWidget::showWithFullContext);
0256 
0257     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0258     KConfigGroup cgGeneral = KConfigGroup(config, QStringLiteral("General"));
0259     handleStyleChange(cgGeneral.readEntry("Diff Show Style", (int)SideBySide));
0260     // clear, after handleStyleChange there might be "no differences found" text
0261     m_left->clear();
0262     m_right->clear();
0263 }
0264 
0265 DiffWidget::~DiffWidget()
0266 {
0267     // if there are any living processes, disconnect them now before we get destroyed
0268     for (QObject *child : children()) {
0269         QProcess *p = qobject_cast<QProcess *>(child);
0270         if (p) {
0271             disconnect(p, nullptr, nullptr, nullptr);
0272         }
0273     }
0274 }
0275 
0276 void DiffWidget::showEvent(QShowEvent *e)
0277 {
0278     if (!m_blockShowEvent && m_params.flags & DiffParams::ReloadOnShow) {
0279         runGitDiff();
0280     }
0281 
0282     QWidget::showEvent(e);
0283 }
0284 
0285 void DiffWidget::handleStyleChange(int newStyle)
0286 {
0287     if (newStyle == m_style) {
0288         return;
0289     }
0290     m_style = (DiffStyle)newStyle;
0291     const auto diff = m_rawDiff;
0292     const auto params = m_params;
0293     clearData();
0294     m_params = params;
0295 
0296     if (m_style == SideBySide) {
0297         m_left->setVisible(true);
0298         m_right->setVisible(true);
0299         openDiff(diff);
0300     } else if (m_style == Unified) {
0301         m_left->setVisible(true);
0302         m_right->setVisible(false);
0303         openDiff(diff);
0304     } else if (m_style == Raw) {
0305         m_left->setVisible(true);
0306         m_right->setVisible(false);
0307         openDiff(diff);
0308     } else {
0309         qWarning() << "Unexpected diff style value: " << newStyle;
0310         Q_UNREACHABLE();
0311     }
0312 
0313     if (sender() && (sender() == m_left || sender() == m_right)) {
0314         KSharedConfig::Ptr config = KSharedConfig::openConfig();
0315         KConfigGroup cgGeneral = KConfigGroup(config, QStringLiteral("General"));
0316         cgGeneral.writeEntry("Diff Show Style", (int)m_style);
0317     }
0318 }
0319 
0320 void DiffWidget::handleStageUnstage(DiffEditor *e, int startLine, int endLine, int actionType, DiffParams::Flag f)
0321 {
0322     if (m_style == SideBySide) {
0323         handleStageUnstage_sideBySide(e, startLine, endLine, actionType, f);
0324     } else if (m_style == Unified) {
0325         handleStageUnstage_unified(startLine, endLine, actionType, f);
0326     } else if (m_style == Raw) {
0327         handleStageUnstage_raw(startLine, endLine, actionType, f);
0328     }
0329 }
0330 
0331 void DiffWidget::handleStageUnstage_unified(int startLine, int endLine, int actionType, DiffParams::Flag f)
0332 {
0333     handleStageUnstage_sideBySide(m_left, startLine, endLine, actionType, f);
0334 }
0335 
0336 void DiffWidget::handleStageUnstage_sideBySide(DiffEditor *e, int startLine, int endLine, int actionType, DiffParams::Flag flags)
0337 {
0338     bool added = e == m_right;
0339     int diffLine = -1;
0340     if (actionType == DiffEditor::Line) {
0341         for (auto vToD : std::as_const(m_lineToRawDiffLine)) {
0342             if (vToD.line == startLine && vToD.added == added) {
0343                 diffLine = vToD.diffLine;
0344                 break;
0345             }
0346         }
0347 
0348         // If the selection by user isn't exact i.e.,
0349         // the start line isn't a changed line but just a context line
0350         if (diffLine == -1) {
0351             for (auto vToD : std::as_const(m_lineToRawDiffLine)) {
0352                 // find the closes line to start line
0353                 if (vToD.line > startLine && vToD.added == added) {
0354                     // check if the found line is in selection range
0355                     if (vToD.line <= endLine) {
0356                         // adjust startline
0357                         startLine = vToD.line;
0358                         diffLine = vToD.diffLine;
0359                     }
0360                     break;
0361                 }
0362             }
0363         }
0364 
0365     } else if (actionType == DiffEditor::Hunk) {
0366         // find the first hunk smaller than/equal this line
0367         for (auto it = m_lineToDiffHunkLine.crbegin(); it != m_lineToDiffHunkLine.crend(); ++it) {
0368             if (it->line <= startLine) {
0369                 diffLine = it->diffLine;
0370                 break;
0371             }
0372         }
0373     }
0374 
0375     if (diffLine == -1) {
0376         // Nothing found somehow
0377         return;
0378     }
0379 
0380     doStageUnStage(diffLine, diffLine + (endLine - startLine), actionType, flags);
0381 }
0382 
0383 void DiffWidget::handleStageUnstage_raw(int startLine, int endLine, int actionType, DiffParams::Flag flags)
0384 {
0385     doStageUnStage(startLine, endLine + (endLine - startLine), actionType, flags);
0386 }
0387 
0388 void DiffWidget::doStageUnStage(int startLine, int endLine, int actionType, DiffParams::Flag flags)
0389 {
0390     VcsDiff d;
0391     const auto stageOrDiscard = flags == DiffParams::Flag::ShowDiscard || flags == DiffParams::Flag::ShowUnstage;
0392     const auto dir = stageOrDiscard ? VcsDiff::Reverse : VcsDiff::Forward;
0393     d.setDiff(QString::fromUtf8(m_rawDiff));
0394     const auto x = actionType == DiffEditor::Line ? d.subDiff(startLine, startLine + (endLine - startLine), dir) : d.subDiffHunk(startLine, dir);
0395 
0396     if (flags & DiffParams::Flag::ShowStage) {
0397         applyDiff(x.diff(), ApplyFlags::None);
0398     } else if (flags & DiffParams::ShowUnstage) {
0399         applyDiff(x.diff(), ApplyFlags::Staged);
0400     } else if (flags & DiffParams::ShowDiscard) {
0401         applyDiff(x.diff(), ApplyFlags::Discard);
0402     }
0403 }
0404 
0405 void DiffWidget::applyDiff(const QString &diff, ApplyFlags flags)
0406 {
0407     //     const QString diff = getDiff(v, flags & Hunk, flags & (Staged | Discard));
0408     if (diff.isEmpty()) {
0409         return;
0410     }
0411 
0412     QTemporaryFile *file = new QTemporaryFile(this);
0413     if (!file->open()) {
0414         //         sendMessage(i18n("Failed to stage selection"), true);
0415         return;
0416     }
0417     file->write(diff.toUtf8());
0418     file->close();
0419 
0420     Q_ASSERT(!m_params.workingDir.isEmpty());
0421     QProcess *git = new QProcess(this);
0422     QStringList args;
0423     if (flags & Discard) {
0424         args = QStringList{QStringLiteral("apply"), file->fileName()};
0425     } else {
0426         args = QStringList{QStringLiteral("apply"), QStringLiteral("--index"), QStringLiteral("--cached"), file->fileName()};
0427     }
0428     setupGitProcess(*git, m_params.workingDir, args);
0429 
0430     connect(git, &QProcess::finished, this, [=](int exitCode, QProcess::ExitStatus es) {
0431         if (es != QProcess::NormalExit || exitCode != 0) {
0432             onError(git->readAllStandardError(), git->exitCode());
0433         } else {
0434             runGitDiff();
0435             if (m_params.updateStatusCallback) {
0436                 m_params.updateStatusCallback();
0437             }
0438         }
0439         delete file;
0440         git->deleteLater();
0441     });
0442     startHostProcess(*git, QProcess::ReadOnly);
0443 }
0444 
0445 void DiffWidget::runGitDiff()
0446 {
0447     const QStringList arguments = m_params.arguments;
0448     const QString workingDir = m_params.workingDir;
0449     if (workingDir.isEmpty() || arguments.isEmpty()) {
0450         return;
0451     }
0452     m_blockShowEvent = true;
0453 
0454     const int lf = m_left->firstVisibleBlockNumber();
0455     const int rf = m_right->firstVisibleBlockNumber();
0456 
0457     QProcess *git = new QProcess(this);
0458     setupGitProcess(*git, workingDir, arguments);
0459     connect(git, &QProcess::finished, this, [=](int, QProcess::ExitStatus) {
0460         const auto params = m_params;
0461         clearData();
0462         m_params = params;
0463         const auto out = git->readAllStandardOutput();
0464         const auto err = git->readAllStandardError();
0465         if (!err.isEmpty()) {
0466             onError(git->readAllStandardError(), git->exitCode());
0467         } else {
0468             openDiff(out);
0469             QMetaObject::invokeMethod(
0470                 this,
0471                 [this, lf, rf] {
0472                     m_left->scrollToBlock(lf);
0473                     m_right->scrollToBlock(rf);
0474                 },
0475                 Qt::QueuedConnection);
0476         }
0477         m_blockShowEvent = false;
0478         git->deleteLater();
0479     });
0480     startHostProcess(*git, QProcess::ReadOnly);
0481 }
0482 
0483 void DiffWidget::clearData()
0484 {
0485     m_left->clearData();
0486     m_right->clearData();
0487     m_rawDiff.clear();
0488     m_lineToRawDiffLine.clear();
0489     m_lineToDiffHunkLine.clear();
0490     m_params.clear();
0491     m_linesWithFileName.clear();
0492     m_commitInfo->clear();
0493     m_commitInfo->hide();
0494 }
0495 
0496 QStringList DiffWidget::diffDocsGitArgs(KTextEditor::Document *l, KTextEditor::Document *r)
0497 {
0498     const QString left = l->url().toLocalFile();
0499     const QString right = r->url().toLocalFile();
0500     return {QStringLiteral("diff"), QStringLiteral("--no-color"), QStringLiteral("--no-index"), left, right};
0501 }
0502 
0503 void DiffWidget::showWithFullContext()
0504 {
0505     if (m_params.arguments.size() < 1) {
0506         return;
0507     }
0508 
0509     const int lineNo = m_left->firstVisibleLineNumber();
0510 
0511     int idx = m_params.arguments.indexOf(QLatin1String("--"));
0512     if (idx != -1) {
0513         m_params.arguments.insert(idx, QLatin1String("-U5000"));
0514     } else if (m_params.arguments.size() == 1) {
0515         m_params.arguments << QLatin1String("-U5000");
0516     } else {
0517         m_params.arguments.insert(2, QLatin1String("-U5000"));
0518     }
0519     runGitDiff();
0520 
0521     // After the diff runs and we show the result, try to take the user back to where he was
0522     QPointer<QTimer> delayedSlotTrigger = new QTimer(this);
0523     delayedSlotTrigger->setSingleShot(true);
0524     delayedSlotTrigger->setInterval(10);
0525     delayedSlotTrigger->callOnTimeout(this, [this, lineNo, delayedSlotTrigger] {
0526         if (delayedSlotTrigger) {
0527             m_left->scrollToLineNumber(lineNo);
0528             m_toolbar->hideShowFullContext();
0529             delete delayedSlotTrigger;
0530         }
0531     });
0532     connect(m_left, &QPlainTextEdit::textChanged, delayedSlotTrigger, qOverload<>(&QTimer::start));
0533 }
0534 
0535 void DiffWidget::diffDocs(KTextEditor::Document *l, KTextEditor::Document *r)
0536 {
0537     clearData();
0538     const auto &repo = KTextEditor::Editor::instance()->repository();
0539     const auto def = repo.definitionForMimeType(l->mimeType());
0540     if (l->mimeType() == r->mimeType()) {
0541         leftHl->setDefinition(def);
0542         rightHl->setDefinition(def);
0543     } else {
0544         leftHl->setDefinition(def);
0545         rightHl->setDefinition(repo.definitionForMimeType(r->mimeType()));
0546     }
0547 
0548     QProcess git;
0549     if (!setupGitProcess(git, qApp->applicationDirPath(), diffDocsGitArgs(l, r))) {
0550         Utils::showMessage(
0551             i18n("<b>git</b> not found. Git is needed to diff the documents. If git is already installed, make sure it is your PATH variable. See "
0552                  "https://git-scm.com/downloads"),
0553             gitIcon(),
0554             i18n("Diff"),
0555             MessageType::Error);
0556         return;
0557     }
0558 
0559     m_params.arguments = git.arguments();
0560     m_params.flags.setFlag(DiffParams::ReloadOnShow);
0561     m_params.workingDir = git.workingDirectory();
0562     runGitDiff();
0563 }
0564 
0565 static void balanceHunkLines(QStringList &left, QStringList &right, int &lineA, int &lineB, std::vector<int> &lineNosA, std::vector<int> &lineNosB)
0566 {
0567     while (left.size() < right.size()) {
0568         lineA++;
0569         left.push_back({});
0570         lineNosA.push_back(-1);
0571     }
0572     while (right.size() < left.size()) {
0573         lineB++;
0574         right.push_back({});
0575         lineNosB.push_back(-1);
0576     }
0577 }
0578 
0579 static std::pair<Change, Change> inlineDiff(QStringView l, QStringView r)
0580 {
0581     auto sitl = l.begin();
0582     auto sitr = r.begin();
0583     while (sitl != l.end() || sitr != r.end()) {
0584         if (*sitl != *sitr) {
0585             break;
0586         }
0587         ++sitl;
0588         ++sitr;
0589     }
0590 
0591     int lStart = std::distance(l.begin(), sitl);
0592     int rStart = std::distance(r.begin(), sitr);
0593 
0594     auto eitl = l.rbegin();
0595     auto eitr = r.rbegin();
0596     int lcount = l.size();
0597     int rcount = r.size();
0598     while (eitl != l.rend() || eitr != r.rend()) {
0599         if (*eitl != *eitr) {
0600             break;
0601         }
0602         // do not allow overlap
0603         if (lcount == lStart || rcount == rStart) {
0604             break;
0605         }
0606         --lcount;
0607         --rcount;
0608         ++eitl;
0609         ++eitr;
0610     }
0611 
0612     int lEnd = l.size() - std::distance(l.rbegin(), eitl);
0613     int rEnd = r.size() - std::distance(r.rbegin(), eitr);
0614 
0615     Change cl;
0616     cl.pos = lStart;
0617     cl.len = lEnd - lStart;
0618     Change cr;
0619     cr.pos = rStart;
0620     cr.len = rEnd - rStart;
0621     return {cl, cr};
0622 }
0623 
0624 // Struct representing a changed line in hunk
0625 struct HunkChangedLine {
0626     HunkChangedLine(const QString &ln, int lineNum, bool isAdd)
0627         : line(ln)
0628         , lineNo(lineNum)
0629         , added(isAdd)
0630     {
0631     }
0632     // Line Text
0633     const QString line;
0634     // Line Number in text editor (not hunk)
0635     int lineNo;
0636     // Which portion of the line changed (len, pos)
0637     bool added;
0638     Change c = {-1, -1};
0639 };
0640 using HunkChangedLines = std::vector<HunkChangedLine>;
0641 
0642 static void markInlineDiffs(HunkChangedLines &hunkChangedLinesA,
0643                             HunkChangedLines &hunkChangedLinesB,
0644                             std::vector<LineHighlight> &leftHlts,
0645                             std::vector<LineHighlight> &rightHlts)
0646 {
0647     if (hunkChangedLinesA.size() != hunkChangedLinesB.size()) {
0648         hunkChangedLinesA.clear();
0649         hunkChangedLinesB.clear();
0650         return;
0651     }
0652     const int size = (int)hunkChangedLinesA.size();
0653     for (int i = 0; i < size; ++i) {
0654         const auto [leftChange, rightChange] = inlineDiff(hunkChangedLinesA.at(i).line, hunkChangedLinesB.at(i).line);
0655         hunkChangedLinesA[i].c = leftChange;
0656         hunkChangedLinesB[i].c = rightChange;
0657     }
0658 
0659     auto addHighlights = [](HunkChangedLines &hunkChangedLines, std::vector<LineHighlight> &hlts) {
0660         for (int i = 0; i < (int)hunkChangedLines.size(); ++i) {
0661             auto &change = hunkChangedLines[i];
0662             for (int j = hlts.size() - 1; j >= 0; --j) {
0663                 if (hlts.at(j).line == change.lineNo && hlts.at(j).added == change.added) {
0664                     hlts[j].changes.push_back(change.c);
0665                     break;
0666                 } else if (hlts.at(j).line < change.lineNo) {
0667                     break;
0668                 }
0669             }
0670         }
0671     };
0672 
0673     addHighlights(hunkChangedLinesA, leftHlts);
0674     addHighlights(hunkChangedLinesB, rightHlts);
0675     hunkChangedLinesA.clear();
0676     hunkChangedLinesB.clear();
0677 }
0678 
0679 static std::vector<KSyntaxHighlighting::Definition> defsForFileExtensions(const QSet<QString> &fileExtensions)
0680 {
0681     const auto &repo = KTextEditor::Editor::instance()->repository();
0682     if (fileExtensions.size() == 1) {
0683         const QString name = QStringLiteral("a.") + *fileExtensions.begin();
0684         return {repo.definitionForFileName(name)};
0685     }
0686 
0687     std::vector<KSyntaxHighlighting::Definition> defs;
0688     for (const auto &ext : fileExtensions) {
0689         const QString name = QStringLiteral("a.") + ext;
0690         defs.push_back(repo.definitionForFileName(name));
0691     }
0692     QSet<QString> seenDefs;
0693     std::vector<KSyntaxHighlighting::Definition> uniqueDefs;
0694     for (const auto &def : defs) {
0695         if (!seenDefs.contains(def.name())) {
0696             uniqueDefs.push_back(def);
0697             seenDefs.insert(def.name());
0698         }
0699     }
0700     if (uniqueDefs.size() == 1) {
0701         return uniqueDefs;
0702     } else {
0703         return {repo.definitionForName(QStringLiteral("None"))};
0704     }
0705 }
0706 
0707 void DiffWidget::parseAndShowDiff(const QByteArray &raw)
0708 {
0709     //     printf("show diff:\n%s\n================================", raw.constData());
0710     const QStringList text = QString::fromUtf8(raw).replace(QStringLiteral("\r\n"), QStringLiteral("\n")).split(QLatin1Char('\n'), Qt::SkipEmptyParts);
0711 
0712     static const QRegularExpression HUNK_HEADER_RE(QStringLiteral("^@@ -([0-9,]+) \\+([0-9,]+) @@(.*)"));
0713     static const QRegularExpression DIFF_FILENAME_RE(QStringLiteral("^[-+]{3} [ab]/(.*)"));
0714 
0715     // Actual lines that will get added to the text editor
0716     QStringList left;
0717     QStringList right;
0718 
0719     // Highlighting data for modified lines
0720     std::vector<LineHighlight> leftHlts;
0721     std::vector<LineHighlight> rightHlts;
0722     // QList<QPair<int, HunkData>> hunkDatas; // lineNo => HunkData
0723 
0724     // Line numbers that will be shown in the editor
0725     std::vector<int> lineNumsA;
0726     std::vector<int> lineNumsB;
0727 
0728     // Changed lines of hunk, used to determine differences between two lines
0729     HunkChangedLines hunkChangedLinesA;
0730     HunkChangedLines hunkChangedLinesB;
0731 
0732     QSet<QString> fileExtensions;
0733     QString srcFile;
0734     QString tgtFile;
0735 
0736     // viewLine => rawDiffLine
0737     std::vector<ViewLineToDiffLine> lineToRawDiffLine;
0738     // for Folding/stage/unstage hunk
0739     std::vector<ViewLineToDiffLine> linesWithHunkHeading;
0740     std::vector<int> linesWithFileName;
0741 
0742     int maxLineNoFound = 0;
0743     int lineA = 0;
0744     int lineB = 0;
0745     for (int i = 0; i < text.size(); ++i) {
0746         const QString &line = text.at(i);
0747         auto match = DIFF_FILENAME_RE.match(line);
0748         if ((match.hasMatch() || line == QStringLiteral("--- /dev/null")) && i + 1 < text.size()) {
0749             srcFile = match.hasMatch() ? match.captured(1) : QString();
0750             if (!srcFile.isEmpty()) {
0751                 fileExtensions.insert(QFileInfo(srcFile).suffix());
0752             }
0753             auto match = DIFF_FILENAME_RE.match(text.at(i + 1));
0754 
0755             if (match.hasMatch() || text.at(i + 1) == QStringLiteral("--- /dev/null")) {
0756                 tgtFile = match.hasMatch() ? match.captured(1) : QString();
0757                 if (!tgtFile.isEmpty()) {
0758                     fileExtensions.insert(QFileInfo(tgtFile).suffix());
0759                 }
0760             }
0761             i++;
0762 
0763             if (m_params.flags.testFlag(DiffParams::ShowFileName)) {
0764                 if (srcFile.isEmpty() && !tgtFile.isEmpty()) {
0765                     left.append(QLatin1String("---"));
0766                     right.append(i18n("New file %1", Utils::fileNameFromPath(tgtFile)));
0767                 } else if (!srcFile.isEmpty() && tgtFile.isEmpty()) {
0768                     left.append(i18n("Deleted file %1", Utils::fileNameFromPath(srcFile)));
0769                     right.append(QLatin1String("+++"));
0770                 } else {
0771                     left.append(Utils::fileNameFromPath(srcFile));
0772                     right.append(Utils::fileNameFromPath(tgtFile));
0773                 }
0774                 Q_ASSERT(left.size() == right.size() && lineA == lineB);
0775                 linesWithFileName.push_back(lineA);
0776                 lineNumsA.push_back(-1);
0777                 lineNumsB.push_back(-1);
0778                 lineA++;
0779                 lineB++;
0780             }
0781             continue;
0782         }
0783 
0784         match = HUNK_HEADER_RE.match(line);
0785         if (!match.hasMatch())
0786             continue;
0787 
0788         const auto oldRange = parseRange(match.captured(1));
0789         const auto newRange = parseRange(match.captured(2));
0790         const QString headingLeft = QStringLiteral("@@ ") + match.captured(1) + match.captured(3) /* + QStringLiteral(" ") + srcFile*/;
0791         const QString headingRight = QStringLiteral("@@ ") + match.captured(2) + match.captured(3) /* + QStringLiteral(" ") + tgtFile*/;
0792 
0793         lineNumsA.push_back(-1);
0794         lineNumsB.push_back(-1);
0795         left.push_back(headingLeft);
0796         right.push_back(headingRight);
0797         linesWithHunkHeading.push_back({lineA, i, /*unused*/ false});
0798         lineA++;
0799         lineB++;
0800 
0801         hunkChangedLinesA.clear();
0802         hunkChangedLinesB.clear();
0803 
0804         int srcLine = oldRange.first;
0805         const int oldCount = oldRange.second;
0806 
0807         int tgtLine = newRange.first;
0808         const int newCount = newRange.second;
0809         maxLineNoFound = qMax(qMax(srcLine + oldCount, tgtLine + newCount), maxLineNoFound);
0810 
0811         for (int j = i + 1; j < text.size(); j++) {
0812             QString l = text.at(j);
0813             if (l.startsWith(QLatin1Char(' '))) {
0814                 // Insert dummy lines when left/right are unequal
0815                 balanceHunkLines(left, right, lineA, lineB, lineNumsA, lineNumsB);
0816                 markInlineDiffs(hunkChangedLinesA, hunkChangedLinesB, leftHlts, rightHlts);
0817 
0818                 l = l.mid(1);
0819                 left.push_back(l);
0820                 right.push_back(l);
0821                 //                 lineNo++;
0822                 lineNumsA.push_back(srcLine++);
0823                 lineNumsB.push_back(tgtLine++);
0824                 lineA++;
0825                 lineB++;
0826             } else if (l.startsWith(QLatin1Char('+'))) {
0827                 //                 qDebug() << "- line";
0828                 l = l.mid(1);
0829                 LineHighlight h;
0830                 h.line = lineB;
0831                 h.added = true;
0832                 h.changes.push_back({0, Change::FullBlock});
0833                 rightHlts.push_back(h);
0834                 lineNumsB.push_back(tgtLine++);
0835                 lineToRawDiffLine.push_back({lineB, j, true});
0836                 right.push_back(l);
0837 
0838                 hunkChangedLinesB.emplace_back(l, lineB, h.added);
0839 
0840                 //                 lineNo++;
0841                 lineB++;
0842             } else if (l.startsWith(QLatin1Char('-'))) {
0843                 l = l.mid(1);
0844                 //                 qDebug() << "+ line: " << l;
0845                 LineHighlight h;
0846                 h.line = lineA;
0847                 h.added = false;
0848                 h.changes.push_back({0, Change::FullBlock});
0849 
0850                 leftHlts.push_back(h);
0851                 lineNumsA.push_back(srcLine++);
0852                 left.push_back(l);
0853                 hunkChangedLinesA.emplace_back(l, lineA, h.added);
0854                 lineToRawDiffLine.push_back({lineA, j, false});
0855 
0856                 lineA++;
0857             } else if (l.startsWith(QStringLiteral("@@ ")) && HUNK_HEADER_RE.match(l).hasMatch()) {
0858                 i = j - 1;
0859 
0860                 markInlineDiffs(hunkChangedLinesA, hunkChangedLinesB, leftHlts, rightHlts);
0861 
0862                 // add line number for current line
0863                 lineNumsA.push_back(-1);
0864                 lineNumsB.push_back(-1);
0865 
0866                 // add new line
0867                 left.push_back(QString());
0868                 right.push_back(QString());
0869                 lineA += 1;
0870                 lineB += 1;
0871                 break;
0872             } else if (l.startsWith(QStringLiteral("diff --git "))) {
0873                 balanceHunkLines(left, right, lineA, lineB, lineNumsA, lineNumsB);
0874                 // Start of a new file
0875                 markInlineDiffs(hunkChangedLinesA, hunkChangedLinesB, leftHlts, rightHlts);
0876                 // add new line
0877                 lineNumsA.push_back(-1);
0878                 lineNumsB.push_back(-1);
0879                 left.push_back(QString());
0880                 right.push_back(QString());
0881                 lineA += 1;
0882                 lineB += 1;
0883                 break;
0884             }
0885             if (j + 1 >= text.size()) {
0886                 markInlineDiffs(hunkChangedLinesA, hunkChangedLinesB, leftHlts, rightHlts);
0887                 i = j; // ensure outer loop also exits after this
0888             }
0889         }
0890     }
0891 
0892     balanceHunkLines(left, right, lineA, lineB, lineNumsA, lineNumsB);
0893 
0894     QString leftText = left.join(QLatin1Char('\n'));
0895     QString rightText = right.join(QLatin1Char('\n'));
0896 
0897     Q_ASSERT(lineA == lineB && left.size() == right.size() && lineNumsA.size() == lineNumsB.size());
0898 
0899     m_left->appendData(leftHlts);
0900     m_right->appendData(rightHlts);
0901     m_left->appendPlainText(leftText);
0902     m_right->appendPlainText(rightText);
0903     m_left->setLineNumberData(lineNumsA, {}, maxLineNoFound);
0904     m_right->setLineNumberData(lineNumsB, {}, maxLineNoFound);
0905     m_lineToDiffHunkLine.insert(m_lineToDiffHunkLine.end(), linesWithHunkHeading.begin(), linesWithHunkHeading.end());
0906     m_lineToRawDiffLine.insert(m_lineToRawDiffLine.end(), lineToRawDiffLine.begin(), lineToRawDiffLine.end());
0907     m_linesWithFileName.insert(m_linesWithFileName.end(), linesWithFileName.begin(), linesWithFileName.end());
0908 
0909     const auto defs = defsForFileExtensions(fileExtensions);
0910     leftHl->setDefinition(defs.front());
0911     rightHl->setDefinition(defs.front());
0912 }
0913 
0914 void DiffWidget::parseAndShowDiffUnified(const QByteArray &raw)
0915 {
0916     //     printf("show diff:\n%s\n================================", raw.constData());
0917     const QStringList text = QString::fromUtf8(raw).replace(QStringLiteral("\r\n"), QStringLiteral("\n")).split(QLatin1Char('\n'), Qt::SkipEmptyParts);
0918 
0919     static const QRegularExpression HUNK_HEADER_RE(QStringLiteral("^@@ -([0-9,]+) \\+([0-9,]+) @@(.*)"));
0920     static const QRegularExpression DIFF_FILENAME_RE(QStringLiteral("^[-+]{3} [ab]/(.*)"));
0921 
0922     // Actual lines that will get added to the text editor
0923     QStringList lines;
0924 
0925     // Highlighting data for modified lines
0926     std::vector<LineHighlight> hlts;
0927 
0928     // Line numbers that will be shown in the editor
0929     std::vector<int> lineNumsA;
0930     std::vector<int> lineNumsB;
0931 
0932     // Changed lines of hunk, used to determine differences between two lines
0933     HunkChangedLines hunkChangedLinesA;
0934     HunkChangedLines hunkChangedLinesB;
0935 
0936     // viewLine => rawDiffLine
0937     std::vector<ViewLineToDiffLine> lineToRawDiffLine;
0938     // for Folding/stage/unstage hunk
0939     std::vector<ViewLineToDiffLine> linesWithHunkHeading;
0940     // Lines containing filename
0941     std::vector<int> linesWithFileName;
0942 
0943     QSet<QString> fileExtensions;
0944     QString srcFile;
0945     QString tgtFile;
0946 
0947     int maxLineNoFound = 0;
0948     int lineNo = 0;
0949 
0950     for (int i = 0; i < text.size(); ++i) {
0951         const QString &line = text.at(i);
0952         auto match = DIFF_FILENAME_RE.match(line);
0953         if ((match.hasMatch() || line == QStringLiteral("--- /dev/null")) && i + 1 < text.size()) {
0954             srcFile = match.hasMatch() ? match.captured(1) : QString();
0955             if (!srcFile.isEmpty()) {
0956                 fileExtensions.insert(QFileInfo(srcFile).suffix());
0957             }
0958             auto match = DIFF_FILENAME_RE.match(text.at(i + 1));
0959 
0960             if (match.hasMatch() || text.at(i + 1) == QStringLiteral("--- /dev/null")) {
0961                 tgtFile = match.hasMatch() ? match.captured(1) : QString();
0962                 if (!tgtFile.isEmpty()) {
0963                     fileExtensions.insert(QFileInfo(tgtFile).suffix());
0964                 }
0965             }
0966             i++;
0967 
0968             if (m_params.flags.testFlag(DiffParams::ShowFileName)) {
0969                 if (srcFile.isEmpty() && !tgtFile.isEmpty()) {
0970                     lines.append(i18n("New file %1", Utils::fileNameFromPath(tgtFile)));
0971                 } else if (!srcFile.isEmpty() && tgtFile.isEmpty()) {
0972                     lines.append(i18n("Deleted file %1", Utils::fileNameFromPath(srcFile)));
0973                 } else if (!srcFile.isEmpty() && !tgtFile.isEmpty()) {
0974                     lines.append(QStringLiteral("%1 → %2").arg(Utils::fileNameFromPath(srcFile), Utils::fileNameFromPath(tgtFile)));
0975                 }
0976                 lineNumsA.push_back(-1);
0977                 lineNumsB.push_back(-1);
0978                 linesWithFileName.push_back(lineNo);
0979                 lineNo++;
0980             }
0981             continue;
0982         }
0983 
0984         match = HUNK_HEADER_RE.match(line);
0985         if (!match.hasMatch())
0986             continue;
0987 
0988         const auto oldRange = parseRange(match.captured(1));
0989         const auto newRange = parseRange(match.captured(2));
0990         //         const QString headingLeft = QStringLiteral("@@ ") + match.captured(1) + match.captured(3) /* + QStringLiteral(" ") + srcFile*/;
0991         //         const QString headingRight = QStringLiteral("@@ ") + match.captured(2) + match.captured(3) /* + QStringLiteral(" ") + tgtFile*/;
0992 
0993         lines.push_back(line);
0994         lineNumsA.push_back(-1);
0995         lineNumsB.push_back(-1);
0996         linesWithHunkHeading.push_back({lineNo, i, false});
0997         lineNo++;
0998 
0999         hunkChangedLinesA.clear();
1000         hunkChangedLinesB.clear();
1001 
1002         int srcLine = oldRange.first;
1003         const int oldCount = oldRange.second;
1004 
1005         int tgtLine = newRange.first;
1006         const int newCount = newRange.second;
1007         maxLineNoFound = qMax(qMax(srcLine + oldCount, tgtLine + newCount), maxLineNoFound);
1008 
1009         for (int j = i + 1; j < text.size(); j++) {
1010             QString l = text.at(j);
1011             if (l.startsWith(QLatin1Char(' '))) {
1012                 markInlineDiffs(hunkChangedLinesA, hunkChangedLinesB, hlts, hlts);
1013 
1014                 l = l.mid(1);
1015                 lines.append(l);
1016                 lineNumsA.push_back(srcLine++);
1017                 lineNumsB.push_back(tgtLine++);
1018                 lineNo++;
1019                 //                 lineB++;
1020             } else if (l.startsWith(QLatin1Char('+'))) {
1021                 //                 qDebug() << "- line";
1022                 l = l.mid(1);
1023                 LineHighlight h;
1024                 h.line = lineNo;
1025                 h.added = true;
1026                 h.changes.push_back({0, Change::FullBlock});
1027                 lines.append(l);
1028                 hlts.push_back(h);
1029                 lineNumsA.push_back(-1);
1030                 lineNumsB.push_back(tgtLine++);
1031                 lineToRawDiffLine.push_back({lineNo, j, false});
1032 
1033                 hunkChangedLinesB.emplace_back(l, lineNo, h.added);
1034                 lineNo++;
1035             } else if (l.startsWith(QLatin1Char('-'))) {
1036                 l = l.mid(1);
1037                 LineHighlight h;
1038                 h.line = lineNo;
1039                 h.added = false;
1040                 h.changes.push_back({0, Change::FullBlock});
1041 
1042                 hlts.push_back(h);
1043                 lineNumsA.push_back(srcLine++);
1044                 lineNumsB.push_back(-1);
1045                 lines.append(l);
1046                 hunkChangedLinesA.emplace_back(l, lineNo, h.added);
1047                 lineToRawDiffLine.push_back({lineNo, j, false});
1048 
1049                 lineNo++;
1050             } else if (l.startsWith(QStringLiteral("@@ ")) && HUNK_HEADER_RE.match(l).hasMatch()) {
1051                 i = j - 1;
1052                 markInlineDiffs(hunkChangedLinesA, hunkChangedLinesB, hlts, hlts);
1053                 // add line number for current line
1054                 lineNumsA.push_back(-1);
1055                 lineNumsB.push_back(-1);
1056 
1057                 // add new line
1058                 lines.append(QString());
1059                 lineNo += 1;
1060                 break;
1061             } else if (l.startsWith(QStringLiteral("diff --git "))) {
1062                 // Start of a new file
1063                 markInlineDiffs(hunkChangedLinesA, hunkChangedLinesB, hlts, hlts);
1064                 // add new line
1065                 lines.append(QString());
1066                 lineNumsA.push_back(-1);
1067                 lineNumsB.push_back(-1);
1068                 lineNo += 1;
1069                 break;
1070             }
1071             if (j + 1 >= text.size()) {
1072                 markInlineDiffs(hunkChangedLinesA, hunkChangedLinesB, hlts, hlts);
1073                 i = j; // ensure outer loop also exits after this
1074             }
1075         }
1076     }
1077 
1078     m_left->appendData(hlts);
1079     m_left->appendPlainText(lines.join(QLatin1Char('\n')));
1080     m_left->setLineNumberData(lineNumsA, lineNumsB, maxLineNoFound);
1081     m_lineToDiffHunkLine.insert(m_lineToDiffHunkLine.end(), linesWithHunkHeading.begin(), linesWithHunkHeading.end());
1082     m_lineToRawDiffLine.insert(m_lineToRawDiffLine.end(), lineToRawDiffLine.begin(), lineToRawDiffLine.end());
1083     m_linesWithFileName.insert(m_linesWithFileName.end(), linesWithFileName.begin(), linesWithFileName.end());
1084 
1085     const auto defs = defsForFileExtensions(fileExtensions);
1086     leftHl->setDefinition(defs.front());
1087 }
1088 
1089 static QString commitInfoFromDiff(const QByteArray &raw)
1090 {
1091     if (!raw.startsWith("commit ")) {
1092         return {};
1093     }
1094     int commitEnd = raw.indexOf("diff --git");
1095     if (commitEnd == -1) {
1096         return {};
1097     }
1098     return QString::fromUtf8(raw.mid(0, commitEnd).trimmed());
1099 }
1100 
1101 void DiffWidget::openDiff(const QByteArray &raw)
1102 {
1103     if ((m_params.flags & DiffParams::ShowCommitInfo) && m_style != DiffStyle::Raw) {
1104         m_toolbar->setShowCommitActionVisible(true);
1105         m_commitInfo->setPlainText(commitInfoFromDiff(raw));
1106         if (m_toolbar->showCommitInfo()) {
1107             m_commitInfo->show();
1108         }
1109     } else {
1110         m_commitInfo->hide();
1111         m_toolbar->setShowCommitActionVisible(false);
1112     }
1113 
1114     // Fallback to raw mode if parsing fails
1115     auto fallback = [&] {
1116         handleStyleChange(Raw);
1117         m_left->setPlainText(QString::fromUtf8(raw));
1118         leftHl->setDefinition(KTextEditor::Editor::instance()->repository().definitionForName(QStringLiteral("Diff")));
1119     };
1120 
1121     if (m_style == SideBySide) {
1122         parseAndShowDiff(raw);
1123         if (m_left->document()->isEmpty() && m_right->document()->isEmpty() && !raw.isEmpty()) {
1124             fallback();
1125         }
1126     } else if (m_style == Unified) {
1127         parseAndShowDiffUnified(raw);
1128         if (m_left->document()->isEmpty() && !raw.isEmpty()) {
1129             fallback();
1130         }
1131     } else if (m_style == Raw) {
1132         m_left->setPlainText(QString::fromUtf8(raw));
1133         leftHl->setDefinition(KTextEditor::Editor::instance()->repository().definitionForName(QStringLiteral("Diff")));
1134     }
1135     m_rawDiff = raw;
1136 
1137     if (m_rawDiff.isEmpty()) {
1138         m_left->setPlainText(i18n("No differences found"));
1139         m_right->setPlainText(i18n("No differences found"));
1140     }
1141 
1142     QMetaObject::invokeMethod(
1143         this,
1144         [this] {
1145             m_left->verticalScrollBar()->setValue(0);
1146             m_right->verticalScrollBar()->setValue(0);
1147             m_blockShowEvent = false;
1148         },
1149         Qt::QueuedConnection);
1150 }
1151 
1152 void DiffWidget::onError(const QByteArray &error, int)
1153 {
1154     if (!error.isEmpty()) {
1155         Utils::showMessage(QString::fromUtf8(error), gitIcon(), i18n("Diff"), MessageType::Warning);
1156     }
1157     //     printf("Got error: \n%s\n==============\n", error.constData());
1158 }
1159 
1160 bool DiffWidget::isHunk(const int line) const
1161 {
1162     return std::any_of(m_lineToDiffHunkLine.begin(), m_lineToDiffHunkLine.end(), [line](const ViewLineToDiffLine l) {
1163         return l.line == line;
1164     });
1165 }
1166 
1167 int DiffWidget::hunkLineCount(int hunkLine)
1168 {
1169     for (size_t i = 0; i < m_lineToDiffHunkLine.size(); ++i) {
1170         const auto h = m_lineToDiffHunkLine.at(i);
1171         if (h.line == hunkLine) {
1172             // last hunk?
1173             if (i + 1 >= m_lineToDiffHunkLine.size()) {
1174                 return -1;
1175             }
1176             auto nextHunk = m_lineToDiffHunkLine.at(i + 1);
1177             auto count = nextHunk.line - h.line;
1178             count -= 1; // one separator line is ignored
1179             return count;
1180         }
1181     }
1182 
1183     return 0;
1184 }
1185 
1186 void DiffWidget::jumpToNextFile()
1187 {
1188     const int block = m_left->firstVisibleBlockNumber();
1189     int nextFileLineNo = 0;
1190     for (const auto &i : m_linesWithFileName) {
1191         if (i > block) {
1192             nextFileLineNo = i;
1193             break;
1194         }
1195     }
1196 
1197     QScopedValueRollback r(m_stopScrollSync, true);
1198     m_left->scrollToBlock(nextFileLineNo, true);
1199     if (m_style == SideBySide) {
1200         m_right->scrollToBlock(nextFileLineNo, true);
1201     }
1202 }
1203 
1204 void DiffWidget::jumpToPrevFile()
1205 {
1206     const int block = m_left->firstVisibleBlockNumber();
1207     int prevFileLineNo = 0;
1208     for (auto i = m_linesWithFileName.crbegin(); i != m_linesWithFileName.crend(); ++i) {
1209         if (*i < block) {
1210             prevFileLineNo = *i;
1211             break;
1212         }
1213     }
1214 
1215     QScopedValueRollback r(m_stopScrollSync, true);
1216     m_left->scrollToBlock(prevFileLineNo, true);
1217     if (m_style == SideBySide) {
1218         m_right->scrollToBlock(prevFileLineNo, true);
1219     }
1220 }
1221 
1222 void DiffWidget::jumpToNextHunk()
1223 {
1224     const int block = m_left->firstVisibleBlockNumber();
1225     int nextHunkLineNo = 0;
1226     for (const auto &i : m_lineToDiffHunkLine) {
1227         if (i.line > block) {
1228             nextHunkLineNo = i.line;
1229             break;
1230         }
1231     }
1232 
1233     QScopedValueRollback r(m_stopScrollSync, true);
1234     m_left->scrollToBlock(nextHunkLineNo, true);
1235     if (m_style == SideBySide) {
1236         m_right->scrollToBlock(nextHunkLineNo, true);
1237     }
1238 }
1239 
1240 void DiffWidget::jumpToPrevHunk()
1241 {
1242     const int block = m_left->firstVisibleBlockNumber();
1243     int prevHunkLineNo = 0;
1244     for (auto i = m_lineToDiffHunkLine.crbegin(); i != m_lineToDiffHunkLine.crend(); ++i) {
1245         if (i->line < block) {
1246             prevHunkLineNo = i->line;
1247             break;
1248         }
1249     }
1250 
1251     QScopedValueRollback r(m_stopScrollSync, true);
1252     m_left->scrollToBlock(prevHunkLineNo, true);
1253     if (m_style == SideBySide) {
1254         m_right->scrollToBlock(prevHunkLineNo, true);
1255     }
1256 }
1257 
1258 #include "diffwidget.moc"
1259 #include "moc_diffwidget.cpp"