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"