File indexing completed on 2025-01-05 05:14:51

0001 /*
0002 SPDX-FileCopyrightText: 2021 Hamed Masafi <hamed.masfi@gmail.com>
0003 
0004 SPDX-License-Identifier: GPL-3.0-or-later
0005 */
0006 
0007 #include "codeeditor.h"
0008 #include "codeeditorsidebar.h"
0009 #include "kommitwidgetsglobaloptions.h"
0010 #include "libkommitwidgets_appdebug.h"
0011 
0012 #include <KSyntaxHighlighting/Definition>
0013 #include <KSyntaxHighlighting/FoldingRegion>
0014 #include <KSyntaxHighlighting/SyntaxHighlighter>
0015 #include <KSyntaxHighlighting/Theme>
0016 
0017 #include <QApplication>
0018 #include <QFontDatabase>
0019 #include <QLabel>
0020 #include <QPainter>
0021 #include <QPalette>
0022 
0023 #include <QtMath>
0024 
0025 class LIBKOMMITWIDGETS_EXPORT SegmentData : public QTextBlockUserData
0026 {
0027 public:
0028     SegmentData(Diff::Segment *segment, int lineNumber, bool empty = false);
0029     void setSegment(Diff::Segment *newSegment);
0030 
0031     Diff::Segment *segment() const;
0032     int mLineNumber{1};
0033     bool mIsEmpty{false};
0034 
0035 private:
0036     Diff::Segment *mSegment{nullptr};
0037 };
0038 
0039 SegmentData::SegmentData(Diff::Segment *segment, int lineNumber, bool empty)
0040     : mLineNumber(lineNumber)
0041     , mIsEmpty(empty)
0042     , mSegment(segment)
0043 {
0044 }
0045 
0046 Diff::Segment *SegmentData::segment() const
0047 {
0048     return mSegment;
0049 }
0050 
0051 void SegmentData::setSegment(Diff::Segment *newSegment)
0052 {
0053     mSegment = newSegment;
0054 }
0055 
0056 CodeEditor::CodeEditor(QWidget *parent)
0057     : QPlainTextEdit(parent)
0058     , mHighlighter(new KSyntaxHighlighting::SyntaxHighlighter(document()))
0059     , mSideBar(new CodeEditorSidebar(this))
0060     , mTitleBar(new QLabel(this))
0061 {
0062     setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
0063     setWordWrapMode(QTextOption::NoWrap);
0064 
0065     setTheme((palette().color(QPalette::Base).lightness() < 128) ? mRepository.defaultTheme(KSyntaxHighlighting::Repository::DarkTheme)
0066                                                                  : mRepository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme));
0067 
0068     connect(this, &QPlainTextEdit::blockCountChanged, this, &CodeEditor::updateViewPortGeometry);
0069     connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::updateSidebarArea);
0070     connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEditor::highlightCurrentLine);
0071 
0072     highlightCurrentLine();
0073 
0074     QTextBlockFormat normalFormat, addedFormat, removedFormat, changedFormat, highlightFormat, emptyFormat, oddFormat, evenFormat;
0075 
0076     addedFormat.setBackground(KommitWidgetsGlobalOptions::instance()->statucColor(Git::ChangeStatus::Added));
0077     removedFormat.setBackground(KommitWidgetsGlobalOptions::instance()->statucColor(Git::ChangeStatus::Removed));
0078     changedFormat.setBackground(KommitWidgetsGlobalOptions::instance()->statucColor(Git::ChangeStatus::Modified));
0079     highlightFormat.setBackground(Qt::yellow);
0080     emptyFormat.setBackground(Qt::gray);
0081     oddFormat.setBackground(QColor(200, 150, 150, 100));
0082     evenFormat.setBackground(QColor(150, 200, 150, 100));
0083     //    normalFormat.setBackground(Qt::lightGray);
0084 
0085     mFormats.insert(Added, addedFormat);
0086     mFormats.insert(Removed, removedFormat);
0087     mFormats.insert(Unchanged, normalFormat);
0088     mFormats.insert(Edited, changedFormat);
0089     mFormats.insert(HighLight, highlightFormat);
0090     mFormats.insert(Empty, emptyFormat);
0091     mFormats.insert(Odd, oddFormat);
0092     mFormats.insert(Even, evenFormat);
0093 
0094     setLineWrapMode(QPlainTextEdit::NoWrap);
0095 
0096     mTitleBar->setAlignment(Qt::AlignCenter);
0097     mTitlebarDefaultHeight = mTitleBar->fontMetrics().height() + 4;
0098     updateViewPortGeometry();
0099 }
0100 
0101 CodeEditor::~CodeEditor() = default;
0102 
0103 void CodeEditor::resizeEvent(QResizeEvent *event)
0104 {
0105     QPlainTextEdit::resizeEvent(event);
0106     updateViewPortGeometry();
0107 }
0108 
0109 void CodeEditor::mousePressEvent(QMouseEvent *event)
0110 {
0111     QPlainTextEdit::mousePressEvent(event);
0112 }
0113 
0114 void CodeEditor::setTheme(const KSyntaxHighlighting::Theme &theme)
0115 {
0116     auto pal = qApp->palette();
0117     if (theme.isValid()) {
0118         pal.setColor(QPalette::Base, theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor));
0119         pal.setColor(QPalette::Highlight, theme.editorColor(KSyntaxHighlighting::Theme::TextSelection));
0120     }
0121     setPalette(pal);
0122 
0123     mHighlighter->setTheme(theme);
0124     mHighlighter->rehighlight();
0125     highlightCurrentLine();
0126 
0127     mTitleBar->setPalette(pal);
0128     mTitleBar->setStyleSheet(QStringLiteral("border: 1px solid %1; border-width: 0 0 1 0;")
0129                                  .arg(QColor(theme.editorColor(KSyntaxHighlighting::Theme::IconBorder)).darker(200).name()));
0130 }
0131 
0132 int CodeEditor::sidebarWidth() const
0133 {
0134     auto longestText = std::max_element(mBlocksData.begin(), mBlocksData.end(), [](BlockData *d1, BlockData *d2) {
0135         return d1->extraText.size() < d2->extraText.size();
0136     });
0137     int count = int(std::log10(blockCount() + 1));
0138     if (longestText != mBlocksData.end())
0139         count += longestText.value()->extraText.size() + 3;
0140     return 4 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * count + fontMetrics().lineSpacing();
0141 }
0142 
0143 int CodeEditor::titlebarHeight() const
0144 {
0145     return mShowTitlebar ? mTitlebarDefaultHeight : 0;
0146 }
0147 
0148 void CodeEditor::sidebarPaintEvent(QPaintEvent *event)
0149 {
0150     QPainter painter(mSideBar);
0151     painter.fillRect(event->rect(), mHighlighter->theme().editorColor(KSyntaxHighlighting::Theme::IconBorder));
0152 
0153     auto block = firstVisibleBlock();
0154     auto blockNumber = block.blockNumber();
0155     int top = blockBoundingGeometry(block).translated(contentOffset()).top();
0156     int bottom = top + blockBoundingRect(block).height();
0157     const int currentBlockNumber = textCursor().blockNumber();
0158 
0159     const auto foldingMarkerSize = fontMetrics().lineSpacing();
0160     int lineNumber{0};
0161 
0162     while (block.isValid() && top <= event->rect().bottom()) {
0163         if (block.isVisible() && bottom >= event->rect().top()) {
0164             QBrush bg;
0165             if (blockNumber >= mCurrentSegment.first && blockNumber <= mCurrentSegment.second)
0166                 bg = Qt::yellow;
0167             else
0168                 bg = document()->findBlockByNumber(blockNumber).blockFormat().background();
0169             painter.fillRect(QRect{0, top, mSideBar->width() - 1, fontMetrics().height()}, bg);
0170 
0171             painter.setPen(mHighlighter->theme().editorColor((blockNumber == currentBlockNumber) ? KSyntaxHighlighting::Theme::CurrentLineNumber
0172                                                                                                  : KSyntaxHighlighting::Theme::LineNumbers));
0173             auto data = mBlocksData.value(block, nullptr);
0174             lineNumber = data ? data->lineNumber : -1;
0175 
0176             if (data && !data->extraText.isEmpty()) {
0177                 painter.drawText(0, top, mSideBar->width() - 2 - foldingMarkerSize, fontMetrics().height(), Qt::AlignLeft, data->extraText);
0178             }
0179             if (lineNumber != -1) {
0180                 const auto number = QString::number(lineNumber);
0181                 painter.drawText(0, top, mSideBar->width() - 2 - (mShowFoldMarks ? foldingMarkerSize : 9), fontMetrics().height(), Qt::AlignRight, number);
0182             }
0183         }
0184 
0185         // folding marker
0186         if (mShowFoldMarks && block.isVisible() && isFoldable(block)) {
0187             QPolygonF polygon;
0188             if (isFolded(block)) {
0189                 polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.25);
0190                 polygon << QPointF(foldingMarkerSize * 0.4, foldingMarkerSize * 0.75);
0191                 polygon << QPointF(foldingMarkerSize * 0.8, foldingMarkerSize * 0.5);
0192             } else {
0193                 polygon << QPointF(foldingMarkerSize * 0.25, foldingMarkerSize * 0.4);
0194                 polygon << QPointF(foldingMarkerSize * 0.75, foldingMarkerSize * 0.4);
0195                 polygon << QPointF(foldingMarkerSize * 0.5, foldingMarkerSize * 0.8);
0196             }
0197             painter.save();
0198             painter.setRenderHint(QPainter::Antialiasing);
0199             painter.setPen(Qt::NoPen);
0200             painter.setBrush(QColor(mHighlighter->theme().editorColor(KSyntaxHighlighting::Theme::CodeFolding)));
0201             painter.translate(mSideBar->width() - foldingMarkerSize, top);
0202             painter.drawPolygon(polygon);
0203             painter.restore();
0204         }
0205 
0206         block = block.next();
0207         if (!block.isValid())
0208             break;
0209         top = bottom;
0210         bottom = top + blockBoundingRect(block).height();
0211         ++blockNumber;
0212     }
0213 }
0214 
0215 CodeEditor::BlockData *CodeEditor::currentBlockData() const
0216 {
0217     return mBlocksData.value(textCursor().block(), nullptr);
0218 }
0219 
0220 void CodeEditor::updateViewPortGeometry()
0221 {
0222     auto th = this->titlebarHeight();
0223     setViewportMargins(this->sidebarWidth(), th, 0, 0);
0224     const auto r = contentsRect();
0225     mSideBar->setGeometry(QRect(r.left(), r.top() + th, sidebarWidth(), r.height()));
0226 
0227     if (th)
0228         mTitleBar->setGeometry(QRect(r.left(), r.top(), r.width(), th));
0229     mTitleBar->setVisible(th);
0230 }
0231 
0232 void CodeEditor::updateSidebarArea(const QRect &rect, int dy)
0233 {
0234     if (dy)
0235         mSideBar->scroll(0, dy);
0236     else
0237         mSideBar->update(0, rect.y(), mSideBar->width(), rect.height());
0238 }
0239 
0240 void CodeEditor::highlightCurrentLine()
0241 {
0242     QTextEdit::ExtraSelection selection;
0243     auto color = QColor(mHighlighter->theme().editorColor(KSyntaxHighlighting::Theme::CurrentLine));
0244     color.setAlpha(220);
0245     selection.format.setBackground(color);
0246     selection.format.setProperty(QTextFormat::FullWidthSelection, true);
0247     selection.cursor = textCursor();
0248     selection.cursor.clearSelection();
0249 
0250     QList<QTextEdit::ExtraSelection> extraSelections;
0251     extraSelections.append(selection);
0252     setExtraSelections(extraSelections);
0253 }
0254 
0255 QTextBlock CodeEditor::blockAtPosition(int y) const
0256 {
0257     auto block = firstVisibleBlock();
0258     if (!block.isValid())
0259         return {};
0260 
0261     auto top = blockBoundingGeometry(block).translated(contentOffset()).top();
0262     int bottom = top + blockBoundingRect(block).height();
0263     do {
0264         if (top <= y && y <= bottom)
0265             return block;
0266         block = block.next();
0267         top = bottom;
0268         bottom = top + blockBoundingRect(block).height();
0269     } while (block.isValid());
0270     return {};
0271 }
0272 
0273 bool CodeEditor::isFoldable(const QTextBlock &block) const
0274 {
0275     return mHighlighter->startsFoldingRegion(block);
0276 }
0277 
0278 bool CodeEditor::isFolded(const QTextBlock &block) const
0279 {
0280     if (!block.isValid())
0281         return false;
0282     const auto nextBlock = block.next();
0283     if (!nextBlock.isValid())
0284         return false;
0285     return !nextBlock.isVisible();
0286 }
0287 
0288 void CodeEditor::toggleFold(const QTextBlock &startBlock)
0289 {
0290     if (!mShowFoldMarks)
0291         return;
0292 
0293     // we also want to fold the last line of the region, therefore the ".next()"
0294     const auto endBlock = mHighlighter->findFoldingRegionEnd(startBlock).next();
0295 
0296     if (isFolded(startBlock)) {
0297         // unfold
0298         auto block = startBlock.next();
0299         while (block.isValid() && !block.isVisible()) {
0300             block.setVisible(true);
0301             block.setLineCount(block.layout()->lineCount());
0302             block = block.next();
0303         }
0304 
0305     } else {
0306         // fold
0307         auto block = startBlock.next();
0308         while (block.isValid() && block != endBlock) {
0309             block.setVisible(false);
0310             block.setLineCount(0);
0311             block = block.next();
0312         }
0313     }
0314 
0315     // redraw document
0316     document()->markContentsDirty(startBlock.position(), endBlock.position() - startBlock.position() + 1);
0317 
0318     // update scrollbars
0319     Q_EMIT document()->documentLayout()->documentSizeChanged(document()->documentLayout()->documentSize());
0320 }
0321 
0322 bool CodeEditor::showFoldMarks() const
0323 {
0324     return mShowFoldMarks;
0325 }
0326 
0327 void CodeEditor::setShowFoldMarks(bool newShowFoldMarks)
0328 {
0329     mShowFoldMarks = newShowFoldMarks;
0330 }
0331 
0332 bool CodeEditor::showTitleBar() const
0333 {
0334     return mShowTitlebar;
0335 }
0336 
0337 void CodeEditor::setShowTitleBar(bool newShowTitleBar)
0338 {
0339     mShowTitlebar = newShowTitleBar;
0340     mTitleBar->setVisible(newShowTitleBar);
0341     updateViewPortGeometry();
0342 }
0343 
0344 QString CodeEditor::title() const
0345 {
0346     return mTitleBar->text();
0347 }
0348 
0349 void CodeEditor::setTitle(const QString &title)
0350 {
0351     mTitleBar->setText(title);
0352     updateViewPortGeometry();
0353 }
0354 
0355 void CodeEditor::paintEvent(QPaintEvent *e)
0356 {
0357     QPlainTextEdit::paintEvent(e);
0358 
0359     //    QPainter p(viewport());
0360     //    for (auto i = _lines.begin(); i != _lines.end(); ++i) {
0361     ////        auto b = document()->findBlockByLineNumber(i.key());
0362     //        auto rc = blockBoundingGeometry(i.key());
0363     //        rc.moveTop(rc.top() - 2);
0364     //        rc.setBottom(rc.top() + 2);
0365     //        p.fillRect(rc, _formats.value(i.value()).background());
0366     //    }
0367     viewport()->update();
0368 }
0369 
0370 int CodeEditor::lineNumberOfBlock(const QTextBlock &block) const
0371 {
0372     auto b = mBlocksData.value(block, nullptr);
0373     return b ? b->lineNumber : -1;
0374 }
0375 
0376 void CodeEditor::setHighlighting(const QString &fileName)
0377 {
0378     const auto def = mRepository.definitionForFileName(fileName);
0379     mHighlighter->setDefinition(def);
0380     mTitleBar->setText(fileName);
0381 }
0382 
0383 void CodeEditor::append(const QString &code, CodeEditor::BlockType type, Diff::Segment *segment)
0384 {
0385     auto t = textCursor();
0386 
0387     if (mSegments.size()) {
0388         t.insertBlock();
0389     }
0390 
0391     if (!code.isEmpty())
0392         t.insertText(code);
0393 
0394     mSegments.insert(t.block().blockNumber(), segment);
0395     t.setBlockFormat(mFormats.value(type));
0396 
0397     if (type != Empty)
0398         mBlocksData.insert(t.block(), new BlockData{++mLastLineNumber, segment, type});
0399 }
0400 
0401 int CodeEditor::append(const QString &code, const QColor &backgroundColor)
0402 {
0403     auto t = textCursor();
0404 
0405     if (mSegments.size())
0406         t.insertBlock();
0407 
0408     QTextCursor c(t.block());
0409     c.insertText(code);
0410     QTextBlockFormat fmt;
0411     fmt.setBackground(backgroundColor);
0412     t.setBlockFormat(fmt);
0413     mSegments.insert(t.block().blockNumber(), nullptr);
0414 
0415     mBlocksData.insert(t.block(), new BlockData{++mLastLineNumber, nullptr, mLastLineNumber ? BlockType::Odd : BlockType::Even});
0416     mLastOddEven = !mLastOddEven;
0417 
0418     return t.block().blockNumber();
0419 }
0420 
0421 void CodeEditor::append(const QStringList &code, CodeEditor::BlockType type, Diff::Segment *segment, int size)
0422 {
0423     for (auto &e : code)
0424         append(e, type, segment);
0425     for (int var = 0; var < size - code.size(); ++var)
0426         append(QString(), Empty, segment);
0427 }
0428 
0429 int CodeEditor::append(const QString &code, CodeEditor::BlockType type, BlockData *data)
0430 {
0431     auto t = textCursor();
0432 
0433     if (mSegments.size())
0434         t.insertBlock();
0435 
0436     QTextCursor c(t.block());
0437     c.insertText(code);
0438 
0439     t.setBlockFormat(mFormats.value(type));
0440     data->lineNumber = ++mLastLineNumber;
0441 
0442     mSegments.insert(t.block().blockNumber(), nullptr);
0443     mBlocksData.insert(t.block(), data);
0444 
0445     return t.block().blockNumber();
0446 }
0447 
0448 QPair<int, int> CodeEditor::blockArea(int from, int to)
0449 {
0450     auto firstBlock = document()->findBlockByLineNumber(from);
0451     auto secondBlock = document()->findBlockByLineNumber(to);
0452     //    qCDebug(KOMMIT_LOG) << from << " to " << to << firstBlock.text() << secondBlock.text();
0453 
0454     int top = qRound(blockBoundingGeometry(firstBlock).translated(contentOffset()).top());
0455     int bottom;
0456 
0457     if (to == -1)
0458         bottom = top + 1;
0459     else
0460         bottom = qRound(blockBoundingGeometry(secondBlock).translated(contentOffset()).bottom());
0461 
0462     return qMakePair(top, bottom);
0463 }
0464 
0465 QPair<int, int> CodeEditor::visibleLines() const
0466 {
0467     auto block = firstVisibleBlock();
0468     auto ret = qMakePair(block.blockNumber(), 0);
0469 
0470     //    while (block.isVisible() && block.isValid()) {
0471     //        ret.second++;// = mSegmentsLineNumbers.value(block);
0472     //        block = block.next();
0473     //    }
0474     ret.second = (height() - titlebarHeight()) / blockBoundingRect(block).height();
0475     return ret;
0476 }
0477 
0478 int CodeEditor::currentLineNumber() const
0479 {
0480     return textCursor().block().firstLineNumber();
0481 }
0482 
0483 void CodeEditor::gotoLineNumber(int lineNumber)
0484 {
0485     const QTextBlock block = document()->findBlockByLineNumber(lineNumber);
0486 
0487     if (block.isValid()) {
0488         QTextCursor cursor(block);
0489         setTextCursor(cursor);
0490     }
0491 }
0492 
0493 void CodeEditor::gotoSegment(Diff::Segment *segment)
0494 {
0495     for (auto i = mSegments.begin(); i != mSegments.end(); i++) {
0496         if (i.value() == segment) {
0497             QTextBlock block = document()->findBlockByLineNumber(i.key());
0498 
0499             if (block.isValid()) {
0500                 QTextCursor cursor(block);
0501                 setTextCursor(cursor);
0502             }
0503             return;
0504         }
0505     }
0506 }
0507 
0508 void CodeEditor::mouseReleaseEvent(QMouseEvent *event)
0509 {
0510     Q_EMIT blockSelected();
0511     QPlainTextEdit::mouseReleaseEvent(event);
0512 }
0513 
0514 Diff::Segment *CodeEditor::currentSegment() const
0515 {
0516     return mSegments.value(textCursor().block().blockNumber(), nullptr);
0517 }
0518 
0519 void CodeEditor::highlightSegment(Diff::Segment *segment)
0520 {
0521     mCurrentSegment = qMakePair(-1, -1);
0522     for (auto i = mSegments.begin(); i != mSegments.end(); i++) {
0523         if (i.value() == segment) {
0524             if (mCurrentSegment.first == -1)
0525                 mCurrentSegment.first = i.key();
0526             //            auto block = document()->findBlockByNumber(i.key());
0527 
0528             //            QTextCursor cursor(block);
0529             ////            cursor.setBlockFormat(_formats.value(HighLight));
0530             //            setTextCursor(cursor);
0531             //            return;
0532         } else if (mCurrentSegment.first != -1) {
0533             mCurrentSegment.second = i.key() - 1;
0534             break;
0535         }
0536     }
0537     //    _currentSegment = segment;
0538     mSideBar->update();
0539     qCDebug(KOMMIT_WIDGETS_LOG()) << mCurrentSegment;
0540     return;
0541     qCDebug(KOMMIT_WIDGETS_LOG()) << "Segment not found";
0542 }
0543 
0544 void CodeEditor::clearAll()
0545 {
0546     mSegments.clear();
0547     qDeleteAll(mSegments);
0548     clear();
0549     mLastLineNumber = 0;
0550     auto tmp = mBlocksData.values();
0551     qDeleteAll(tmp);
0552     mBlocksData.clear();
0553 }
0554 
0555 CodeEditor::BlockData::BlockData(int lineNumber, Diff::Segment *segment, CodeEditor::BlockType type)
0556     : lineNumber{lineNumber}
0557     , segment{segment}
0558     , type{type}
0559 {
0560 }
0561 
0562 #include "moc_codeeditor.cpp"