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

0001 /*
0002     SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 #include "diffeditor.h"
0006 #include "difflinenumarea.h"
0007 #include "diffwidget.h"
0008 #include "ktexteditor_utils.h"
0009 
0010 #include <QMenu>
0011 #include <QPainter>
0012 #include <QPainterPath>
0013 #include <QScrollBar>
0014 #include <QTextBlock>
0015 
0016 #include <KLocalizedString>
0017 #include <KSyntaxHighlighting/Format>
0018 #include <KSyntaxHighlighting/State>
0019 #include <KTextEditor/Editor>
0020 
0021 DiffSyntaxHighlighter::DiffSyntaxHighlighter(QTextDocument *parent, DiffWidget *diffWidget)
0022     : KSyntaxHighlighting::SyntaxHighlighter(parent)
0023     , m_diffWidget(diffWidget)
0024 {
0025 }
0026 
0027 void DiffSyntaxHighlighter::applyFormat(int offset, int length, const KSyntaxHighlighting::Format &format)
0028 {
0029     if (format.textStyle() == KSyntaxHighlighting::Theme::TextStyle::Error) {
0030         return;
0031     }
0032     KSyntaxHighlighting::SyntaxHighlighter::applyFormat(offset, length, format);
0033 }
0034 
0035 void DiffSyntaxHighlighter::highlightBlock(const QString &text)
0036 {
0037     // Delete user data i.e., the stored state in the block
0038     // when we encounter a hunk to avoid issues like everything
0039     // is commented because previous hunk ended with an unclosed
0040     // comment block
0041     // do this only if not anyways first block, there user data is not existing
0042     if (currentBlock().position() > 0 && m_diffWidget->isHunk(currentBlock().blockNumber())) {
0043         // ownership of the data is in the block, just reset it by assigning a new dummy data
0044         currentBlock().previous().setUserData(new QTextBlockUserData);
0045     }
0046     KSyntaxHighlighting::SyntaxHighlighter::highlightBlock(text);
0047 }
0048 
0049 DiffEditor::DiffEditor(DiffParams::Flags f, QWidget *parent)
0050     : QPlainTextEdit(parent)
0051     , m_lineNumArea(new LineNumArea(this))
0052     , m_diffWidget(static_cast<DiffWidget *>(parent))
0053     , m_flags(f)
0054 {
0055     setFrameStyle(QFrame::NoFrame);
0056 
0057     auto updateEditorColors = [this](KTextEditor::Editor *e) {
0058         if (!e)
0059             return;
0060         using namespace KSyntaxHighlighting;
0061         auto theme = e->theme();
0062         auto bg = QColor::fromRgba(theme.editorColor(Theme::EditorColorRole::BackgroundColor));
0063         auto fg = QColor::fromRgba(theme.textColor(Theme::TextStyle::Normal));
0064         auto sel = QColor::fromRgba(theme.editorColor(Theme::EditorColorRole::TextSelection));
0065         hunkSeparatorColor = QColor::fromRgba(theme.textColor(Theme::TextStyle::Keyword));
0066         auto pal = palette();
0067         pal.setColor(QPalette::Base, bg);
0068         pal.setColor(QPalette::Text, fg);
0069         // set a small alpha to be able to see the red/green bg
0070         // with selection
0071         if (fg.alpha() == 255) {
0072             sel.setAlphaF(0.8f);
0073         }
0074         pal.setColor(QPalette::Highlight, sel);
0075         pal.setColor(QPalette::HighlightedText, fg);
0076         setFont(Utils::editorFont());
0077         setPalette(pal);
0078 
0079         updateDiffColors(bg.lightness() < 127);
0080     };
0081     connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateEditorColors);
0082     updateEditorColors(KTextEditor::Editor::instance());
0083 
0084     connect(verticalScrollBar(), &QScrollBar::valueChanged, this, [this] {
0085         m_lineNumArea->update();
0086     });
0087     connect(this, &QPlainTextEdit::cursorPositionChanged, this, [this] {
0088         m_lineNumArea->update();
0089     });
0090     connect(document(), &QTextDocument::blockCountChanged, this, &DiffEditor::updateLineNumberAreaWidth);
0091     connect(this, &QPlainTextEdit::updateRequest, this, &DiffEditor::updateLineNumberArea);
0092 
0093     setReadOnly(true);
0094 
0095     m_timeLine.setDuration(1000);
0096     m_timeLine.setFrameRange(0, 250);
0097     connect(&m_timeLine, &QTimeLine::frameChanged, this, [this](int) {
0098         if (!m_animateTextRect.isNull()) {
0099             viewport()->update(m_animateTextRect);
0100         }
0101     });
0102     connect(&m_timeLine, &QTimeLine::finished, this, [this] {
0103         auto r = m_animateTextRect;
0104         m_animateTextRect = QRect();
0105         viewport()->update(r);
0106     });
0107 }
0108 
0109 void DiffEditor::updateDiffColors(bool darkMode)
0110 {
0111     red1 = darkMode ? QColor(Qt::red).darker(120) : QColor(Qt::red).lighter(140);
0112     red1.setAlphaF(0.1f);
0113     green1 = darkMode ? QColor(Qt::green).darker(140) : QColor(Qt::green).lighter(140);
0114     green1.setAlphaF(0.1f);
0115 
0116     red2 = darkMode ? QColor(Qt::red).darker(80) : QColor(Qt::darkRed).lighter(120);
0117     red2.setAlphaF(0.20f);
0118     green2 = darkMode ? QColor(Qt::green).darker(110) : QColor(Qt::darkGreen).lighter(120);
0119     green2.setAlphaF(0.20f);
0120 }
0121 
0122 void DiffEditor::scrollToBlock(int block, bool flashBlock)
0123 {
0124     if (flashBlock) {
0125         if (m_timeLine.state() == QTimeLine::Running) {
0126             m_timeLine.stop();
0127             auto r = m_animateTextRect;
0128             m_animateTextRect = QRect();
0129             viewport()->update(r);
0130         }
0131     }
0132     int lineNo = 0;
0133     for (int i = 0; i < block; ++i) {
0134         lineNo += document()->findBlockByNumber(i).lineCount();
0135     }
0136     verticalScrollBar()->setValue(lineNo);
0137 
0138     if (flashBlock) {
0139         QTextBlock b = document()->findBlockByNumber(block);
0140         m_animateTextRect = blockBoundingGeometry(b).translated(contentOffset()).toRect();
0141         m_timeLine.start();
0142     }
0143 }
0144 
0145 void DiffEditor::resizeEvent(QResizeEvent *event)
0146 {
0147     QPlainTextEdit::resizeEvent(event);
0148     updateLineNumAreaGeometry();
0149 }
0150 
0151 KTextEditor::Range DiffEditor::selectionRange() const
0152 {
0153     const auto cursor = textCursor();
0154     if (!cursor.hasSelection())
0155         return KTextEditor::Range::invalid();
0156 
0157     QTextCursor start = cursor;
0158     start.setPosition(qMin(cursor.selectionStart(), cursor.selectionEnd()));
0159     QTextCursor end = cursor;
0160     end.setPosition(qMax(cursor.selectionStart(), cursor.selectionEnd()));
0161 
0162     const int startLine = start.blockNumber();
0163     const int endLine = end.blockNumber();
0164     const int startColumn = start.selectionStart() - start.block().position();
0165     const int endColumn = end.selectionEnd() - end.block().position();
0166     return {startLine, startColumn, endLine, endColumn};
0167 }
0168 
0169 void DiffEditor::contextMenuEvent(QContextMenuEvent *e)
0170 {
0171     // Follow KTextEditor behaviour
0172     if (!textCursor().hasSelection()) {
0173         setTextCursor(cursorForPosition(e->pos()));
0174     }
0175 
0176     auto menu = createStandardContextMenu();
0177     QAction *before = nullptr;
0178     if (!menu->actions().isEmpty())
0179         before = menu->actions().constFirst();
0180 
0181     {
0182         auto a = new QAction(i18n("Change Style"), this);
0183         auto styleMenu = new QMenu(this);
0184         styleMenu->addAction(i18n("Side By Side"), this, [this] {
0185             Q_EMIT switchStyle(SideBySide);
0186         });
0187         styleMenu->addAction(i18n("Unified"), this, [this] {
0188             Q_EMIT switchStyle(Unified);
0189         });
0190         styleMenu->addAction(i18n("Raw"), this, [this] {
0191             Q_EMIT switchStyle(Raw);
0192         });
0193         a->setMenu(styleMenu);
0194         menu->insertAction(before, a);
0195     }
0196 
0197     addStageUnstageDiscardActions(menu);
0198 
0199     menu->exec(viewport()->mapToGlobal(e->pos()));
0200 }
0201 
0202 void DiffEditor::addStageUnstageDiscardActions(QMenu *menu)
0203 {
0204     const auto selection = selectionRange();
0205     const int lineCount = !selection.isValid() ? 1 : selection.numberOfLines() + 1;
0206 
0207     int startLine = textCursor().blockNumber();
0208     int endLine = startLine;
0209     if (selection.isValid()) {
0210         startLine = selection.start().line();
0211         endLine = selection.end().line();
0212     }
0213 
0214     QAction *before = nullptr;
0215     if (!menu->actions().isEmpty())
0216         before = menu->actions().constFirst();
0217 
0218     if (m_flags.testFlag(DiffParams::Flag::ShowStage)) {
0219         auto a = new QAction(i18np("Stage Line", "Stage Lines", lineCount), this);
0220         connect(a, &QAction::triggered, this, [=] {
0221             Q_EMIT actionTriggered(this, startLine, endLine, (int)Line, DiffParams::Flag::ShowStage);
0222         });
0223         menu->insertAction(before, a);
0224         a = new QAction(i18n("Stage Hunk"), this);
0225         connect(a, &QAction::triggered, this, [=] {
0226             Q_EMIT actionTriggered(this, startLine, endLine, (int)Hunk, DiffParams::Flag::ShowStage);
0227         });
0228         menu->insertAction(before, a);
0229     }
0230     if (m_flags.testFlag(DiffParams::Flag::ShowDiscard)) {
0231         auto a = new QAction(i18np("Discard Line", "Discard Lines", lineCount), this);
0232         connect(a, &QAction::triggered, this, [=] {
0233             Q_EMIT actionTriggered(this, startLine, endLine, (int)Line, DiffParams::Flag::ShowDiscard);
0234         });
0235         menu->insertAction(before, a);
0236         a = new QAction(i18n("Discard Hunk"), this);
0237         connect(a, &QAction::triggered, this, [=] {
0238             Q_EMIT actionTriggered(this, startLine, endLine, (int)Hunk, DiffParams::Flag::ShowDiscard);
0239         });
0240         menu->insertAction(before, a);
0241     }
0242     if (m_flags.testFlag(DiffParams::Flag::ShowUnstage)) {
0243         auto a = new QAction(i18np("Unstage Line", "Unstage Lines", lineCount), this);
0244         connect(a, &QAction::triggered, this, [=] {
0245             Q_EMIT actionTriggered(this, startLine, endLine, (int)Line, DiffParams::Flag::ShowUnstage);
0246         });
0247         menu->insertAction(before, a);
0248         a = new QAction(i18n("Unstage Hunk"), this);
0249         connect(a, &QAction::triggered, this, [=] {
0250             Q_EMIT actionTriggered(this, startLine, endLine, (int)Hunk, DiffParams::Flag::ShowUnstage);
0251         });
0252         menu->insertAction(before, a);
0253     }
0254 }
0255 
0256 void DiffEditor::updateLineNumberArea(const QRect &rect, int dy)
0257 {
0258     if (dy)
0259         m_lineNumArea->scroll(0, dy);
0260     else
0261         m_lineNumArea->update(0, rect.y(), m_lineNumArea->sizeHint().width(), rect.height());
0262 
0263     updateLineNumAreaGeometry();
0264 
0265     if (rect.contains(viewport()->rect())) {
0266         updateLineNumberAreaWidth(0);
0267     }
0268 }
0269 
0270 void DiffEditor::updateLineNumAreaGeometry()
0271 {
0272     const auto contentsRect = this->contentsRect();
0273     const QRect newGeometry = {contentsRect.left(), contentsRect.top(), m_lineNumArea->sizeHint().width(), contentsRect.height()};
0274     auto oldGeometry = m_lineNumArea->geometry();
0275     if (newGeometry != oldGeometry) {
0276         m_lineNumArea->setGeometry(newGeometry);
0277     }
0278 }
0279 
0280 void DiffEditor::updateLineNumberAreaWidth(int)
0281 {
0282     QSignalBlocker blocker(this);
0283     const auto oldMargins = viewportMargins();
0284     const int width = m_lineNumArea->sizeHint().width();
0285     const auto newMargins = QMargins{width, oldMargins.top(), oldMargins.right(), oldMargins.bottom()};
0286 
0287     if (newMargins != oldMargins) {
0288         setViewportMargins(newMargins);
0289     }
0290 }
0291 
0292 void DiffEditor::paintEvent(QPaintEvent *e)
0293 {
0294     QPainter p(viewport());
0295     QPointF offset(contentOffset());
0296     QTextBlock block = firstVisibleBlock();
0297     const auto cursorBlock = textCursor().blockNumber();
0298     const auto cursorPos = textCursor().positionInBlock();
0299     const auto viewportRect = viewport()->rect();
0300 
0301     while (block.isValid()) {
0302         const QRectF r = blockBoundingRect(block).translated(offset);
0303         auto layout = block.layout();
0304 
0305         auto hl = highlightingForLine(block.blockNumber());
0306         if (block.isVisible() && hl && layout) {
0307             const auto changes = hl->changes;
0308             for (auto c : changes) {
0309                 // full line background is colored
0310                 p.fillRect(r, hl->added ? green1 : red1);
0311                 if (c.len == Change::FullBlock) {
0312                     continue;
0313                 }
0314 
0315                 QTextLine sl = layout->lineForTextPosition(c.pos);
0316                 QTextLine el = layout->lineForTextPosition(c.pos + c.len);
0317                 // color any word diffs
0318                 if (!sl.isValid() || !el.isValid()) {
0319                     continue;
0320                 }
0321                 if (sl.isValid() && sl.lineNumber() == el.lineNumber()) {
0322                     int sx = sl.cursorToX(c.pos);
0323                     int ex = el.cursorToX(c.pos + c.len);
0324                     QRectF r = sl.naturalTextRect();
0325                     r.setLeft(sx);
0326                     r.setRight(ex);
0327                     r.moveTop(offset.y() + (p.fontMetrics().lineSpacing() * sl.lineNumber()));
0328                     p.fillRect(r, hl->added ? green2 : red2);
0329                 } else {
0330                     QPainterPath path;
0331                     int i = sl.lineNumber() + 1;
0332                     int end = el.lineNumber();
0333                     QRectF rect = sl.naturalTextRect();
0334                     rect.setLeft(sl.cursorToX(c.pos));
0335                     rect.moveTop(offset.y() + (sl.height() * sl.lineNumber()));
0336                     path.addRect(rect);
0337                     for (; i <= end; ++i) {
0338                         auto line = layout->lineAt(i);
0339                         rect = line.naturalTextRect();
0340                         rect.moveTop(offset.y() + (p.fontMetrics().lineSpacing() * line.lineNumber()));
0341                         if (i == end) {
0342                             rect.setRight(el.cursorToX(c.pos + c.len));
0343                         }
0344                         path.addRect(rect);
0345                     }
0346                     p.fillPath(path, hl->added ? green2 : red2);
0347                 }
0348             }
0349         }
0350 
0351         if (isHunkLine(block.blockNumber())) {
0352             p.save();
0353             QPen pen;
0354             pen.setColor(hunkSeparatorColor);
0355             pen.setWidthF(1.1);
0356             p.setPen(pen);
0357             p.setBrush(Qt::NoBrush);
0358             p.drawLine(r.topLeft(), r.topRight());
0359             p.drawLine(r.bottomLeft(), r.bottomRight());
0360             p.restore();
0361         }
0362 
0363         if (m_diffWidget->isFileNameLine(block.blockNumber())) {
0364             p.save();
0365             QPen pen;
0366             pen.setColor(hunkSeparatorColor);
0367             pen.setWidthF(1.1);
0368             p.setPen(pen);
0369             p.setBrush(Qt::NoBrush);
0370             auto rCopy = r;
0371             rCopy.setRight(block.layout()->lineAt(0).naturalTextRect().right() + 4);
0372             rCopy.setLeft(rCopy.left() - 2);
0373             p.drawRect(rCopy);
0374             p.restore();
0375         }
0376 
0377         if (m_animateTextRect.contains(offset.toPoint())) {
0378             QColor c(Qt::red);
0379             c.setAlpha(m_timeLine.currentFrame());
0380             p.fillRect(m_animateTextRect, c);
0381         }
0382 
0383         if (block.blockNumber() == cursorBlock && block.layout()) {
0384             block.layout()->drawCursor(&p, {0., r.y()}, cursorPos, 2);
0385         }
0386 
0387         offset.ry() += r.height();
0388         if (offset.y() > viewportRect.height()) {
0389             break;
0390         }
0391         block = block.next();
0392     }
0393 
0394     QPlainTextEdit::paintEvent(e);
0395 }
0396 
0397 const LineHighlight *DiffEditor::highlightingForLine(int line)
0398 {
0399     auto it = std::find_if(m_data.cbegin(), m_data.cend(), [line](LineHighlight hl) {
0400         return hl.line == line;
0401     });
0402     return it == m_data.cend() ? nullptr : &(*it);
0403 }
0404 
0405 void DiffEditor::setLineNumberData(std::vector<int> lineNosA, std::vector<int> lineNosB, int maxLineNum)
0406 {
0407     m_lineNumArea->setLineNumData(std::move(lineNosA), std::move(lineNosB));
0408     m_lineNumArea->setMaxLineNum(maxLineNum);
0409     updateLineNumberAreaWidth(0);
0410 }
0411 
0412 bool DiffEditor::isHunkLine(int line) const
0413 {
0414     return m_diffWidget->isHunk(line);
0415 }
0416 
0417 bool DiffEditor::isHunkFolded(int blockNumber)
0418 {
0419     Q_ASSERT(isHunkLine(blockNumber));
0420     const auto block = document()->findBlockByNumber(blockNumber).next();
0421     return block.isValid() && !block.isVisible();
0422 }
0423 
0424 void DiffEditor::toggleFoldHunk(int blockNumber)
0425 {
0426     Q_ASSERT(isHunkLine(blockNumber));
0427     int count = m_diffWidget->hunkLineCount(blockNumber);
0428     if (count == 0) {
0429         return;
0430     }
0431     if (count == -1) {
0432         count = blockCount() - blockNumber;
0433     }
0434     if (count <= 0) {
0435         return;
0436     }
0437 
0438     auto block = document()->findBlockByNumber(blockNumber).next();
0439     int i = 0;
0440     bool visible = !block.isVisible();
0441     while (true) {
0442         i++;
0443         if (i == count || !block.isValid()) {
0444             break;
0445         }
0446         block.setVisible(visible);
0447         block = block.next();
0448     }
0449 
0450     viewport()->update();
0451     m_lineNumArea->update();
0452 }
0453 
0454 int DiffEditor::firstVisibleLineNumber() const
0455 {
0456     const int block = firstVisibleBlockNumber();
0457     const int last = document()->blockCount();
0458     for (int i = block; i < last; ++i) {
0459         int lineNo = m_lineNumArea->lineNumForBlock(i);
0460         if (lineNo != -1) {
0461             return lineNo;
0462         }
0463     }
0464     return -1;
0465 }
0466 
0467 void DiffEditor::scrollToLineNumber(int lineNo)
0468 {
0469     const int block = m_lineNumArea->blockForLineNum(lineNo);
0470     if (block != -1) {
0471         scrollToBlock(block);
0472     }
0473 }
0474 
0475 #include "moc_diffeditor.cpp"