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"