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"