File indexing completed on 2024-05-12 09:55:20
0001 /* 0002 SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 #include "rainbowparens_plugin.h" 0007 0008 #include <QGradient> 0009 #include <QIcon> 0010 #include <QLabel> 0011 #include <QPainter> 0012 #include <QScopeGuard> 0013 #include <QTimer> 0014 #include <QVBoxLayout> 0015 0016 #include <KConfigGroup> 0017 #include <KLocalizedString> 0018 #include <KPluginFactory> 0019 #include <KSharedConfig> 0020 #include <KTextEditor/Document> 0021 #include <KTextEditor/View> 0022 0023 constexpr int numberOfColors = 5; 0024 0025 K_PLUGIN_FACTORY_WITH_JSON(RainbowParenPluginFactory, "rainbowparens_plugin.json", registerPlugin<RainbowParenPlugin>();) 0026 0027 RainbowParenPlugin::RainbowParenPlugin(QObject *parent, const QVariantList &) 0028 : KTextEditor::Plugin(parent) 0029 { 0030 readConfig(); 0031 } 0032 0033 KTextEditor::ConfigPage *RainbowParenPlugin::configPage(int number, QWidget *parent) 0034 { 0035 if (number == 0) { 0036 return new RainbowParenConfigPage(parent, this); 0037 } 0038 return nullptr; 0039 } 0040 0041 void RainbowParenPlugin::readConfig() 0042 { 0043 if (attrs.empty()) { 0044 attrs.resize(5); 0045 for (auto &attr : attrs) { 0046 attr = new KTextEditor::Attribute; 0047 } 0048 } 0049 0050 KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ColoredBrackets")); 0051 QColor br = config.readEntry("color1", QStringLiteral("#1275ef")); 0052 attrs[0]->setForeground(br); 0053 br = config.readEntry("color2", QStringLiteral("#f83c1f")); 0054 attrs[1]->setForeground(br); 0055 br = config.readEntry("color3", QStringLiteral("#9dba1e")); 0056 attrs[2]->setForeground(br); 0057 br = config.readEntry("color4", QStringLiteral("#e219e2")); 0058 attrs[3]->setForeground(br); 0059 br = config.readEntry("color5", QStringLiteral("#37d21c")); 0060 attrs[4]->setForeground(br); 0061 } 0062 0063 QObject *RainbowParenPlugin::createView(KTextEditor::MainWindow *mainWindow) 0064 { 0065 return new RainbowParenPluginView(this, mainWindow); 0066 } 0067 0068 RainbowParenPluginView::RainbowParenPluginView(RainbowParenPlugin *plugin, KTextEditor::MainWindow *mainWin) 0069 : QObject(plugin) 0070 , m_plugin(plugin) 0071 , m_mainWindow(mainWin) 0072 { 0073 connect(mainWin, &KTextEditor::MainWindow::viewChanged, this, &RainbowParenPluginView::viewChanged); 0074 QTimer::singleShot(50, this, [this] { 0075 if (auto *view = m_mainWindow->activeView()) { 0076 rehighlight(view); 0077 } 0078 }); 0079 m_rehighlightTimer.setInterval(200); 0080 m_rehighlightTimer.setSingleShot(true); 0081 m_rehighlightTimer.callOnTimeout(this, [this] { 0082 if (m_activeView) { 0083 rehighlight(m_activeView); 0084 } 0085 }); 0086 } 0087 0088 static void getSavedRangesForDoc(std::vector<RainbowParenPluginView::SavedRanges> &ranges, 0089 std::vector<std::unique_ptr<KTextEditor::MovingRange>> &outRanges, 0090 KTextEditor::Document *d) 0091 { 0092 auto it = std::find_if(ranges.begin(), ranges.end(), [d](const RainbowParenPluginView::SavedRanges &r) { 0093 return r.doc == d; 0094 }); 0095 if (it != ranges.end()) { 0096 outRanges = std::move(it->ranges); 0097 ranges.erase(it); 0098 } 0099 } 0100 0101 void RainbowParenPluginView::viewChanged(KTextEditor::View *view) 0102 { 0103 if (!view) { 0104 return; 0105 } 0106 0107 // disconnect and clear previous doc stuff 0108 if (m_activeView) { 0109 disconnect(m_activeView, &KTextEditor::View::verticalScrollPositionChanged, this, &RainbowParenPluginView::onScrollChanged); 0110 0111 auto doc = m_activeView->document(); 0112 disconnect(doc, &KTextEditor::Document::aboutToDeleteMovingInterfaceContent, this, &RainbowParenPluginView::clearRanges); 0113 disconnect(doc, &KTextEditor::Document::aboutToInvalidateMovingInterfaceContent, this, &RainbowParenPluginView::clearRanges); 0114 disconnect(doc, &KTextEditor::Document::textInserted, this, &RainbowParenPluginView::onTextInserted); 0115 disconnect(doc, &KTextEditor::Document::textRemoved, this, &RainbowParenPluginView::onTextRemoved); 0116 0117 // Remove if we already have this view's ranges saved 0118 clearSavedRangesForDoc(doc); 0119 0120 // save these ranges so that if we are in a split view etc, we don't 0121 // loose the bracket coloring 0122 SavedRanges saved; 0123 saved.doc = doc; 0124 saved.ranges = std::move(ranges); 0125 savedRanges.push_back(std::move(saved)); 0126 if (savedRanges.size() > 4) { 0127 savedRanges.erase(savedRanges.begin()); 0128 } 0129 0130 // clear ranges for this doc if it gets closed 0131 connect(doc, &KTextEditor::Document::aboutToDeleteMovingInterfaceContent, this, &RainbowParenPluginView::clearSavedRangesForDoc, Qt::UniqueConnection); 0132 connect(doc, 0133 &KTextEditor::Document::aboutToInvalidateMovingInterfaceContent, 0134 this, 0135 &RainbowParenPluginView::clearSavedRangesForDoc, 0136 Qt::UniqueConnection); 0137 } 0138 0139 ranges.clear(); 0140 m_activeView = view; 0141 0142 // get any existing ranges for this view 0143 getSavedRangesForDoc(savedRanges, ranges, m_activeView->document()); 0144 0145 connect(view, &KTextEditor::View::verticalScrollPositionChanged, this, &RainbowParenPluginView::onScrollChanged, Qt::UniqueConnection); 0146 0147 auto doc = m_activeView->document(); 0148 connect(doc, &KTextEditor::Document::aboutToDeleteMovingInterfaceContent, this, &RainbowParenPluginView::clearRanges, Qt::UniqueConnection); 0149 connect(doc, &KTextEditor::Document::aboutToInvalidateMovingInterfaceContent, this, &RainbowParenPluginView::clearRanges, Qt::UniqueConnection); 0150 connect(doc, &KTextEditor::Document::textInserted, this, &RainbowParenPluginView::onTextInserted, Qt::UniqueConnection); 0151 connect(doc, &KTextEditor::Document::textRemoved, this, &RainbowParenPluginView::onTextRemoved, Qt::UniqueConnection); 0152 0153 rehighlight(m_activeView); 0154 } 0155 0156 static void onTextChanged(RainbowParenPluginView *p, const QString &text) 0157 { 0158 auto isBracket = [](QChar c) { 0159 return c == QLatin1Char('{') || c == QLatin1Char('(') || c == QLatin1Char(')') || c == QLatin1Char('}') || c == QLatin1Char('[') 0160 || c == QLatin1Char(']'); 0161 }; 0162 if (text.size() > 100) { 0163 p->requestRehighlight(); 0164 return; 0165 } 0166 0167 for (auto c : text) { 0168 if (isBracket(c)) { 0169 p->requestRehighlight(); 0170 break; 0171 } 0172 } 0173 } 0174 0175 void RainbowParenPluginView::onTextInserted(KTextEditor::Document *, KTextEditor::Cursor, const QString &text) 0176 { 0177 onTextChanged(this, text); 0178 } 0179 0180 void RainbowParenPluginView::onTextRemoved(KTextEditor::Document *, KTextEditor::Range, const QString &text) 0181 { 0182 onTextChanged(this, text); 0183 } 0184 0185 void RainbowParenPluginView::onScrollChanged() 0186 { 0187 requestRehighlight(50); 0188 } 0189 0190 void RainbowParenPluginView::requestRehighlight(int delay) 0191 { 0192 if (!m_rehighlightTimer.isActive()) { 0193 m_rehighlightTimer.start(delay); 0194 } 0195 } 0196 0197 void RainbowParenPluginView::clearRanges(KTextEditor::Document *) 0198 { 0199 ranges.clear(); 0200 } 0201 0202 void RainbowParenPluginView::clearSavedRangesForDoc(KTextEditor::Document *doc) 0203 { 0204 auto it = std::find_if(savedRanges.begin(), savedRanges.end(), [doc](const RainbowParenPluginView::SavedRanges &r) { 0205 return r.doc == doc; 0206 }); 0207 if (it != savedRanges.end()) { 0208 savedRanges.erase(it); 0209 } 0210 } 0211 0212 static bool isCommentOrString(KTextEditor::Document *doc, int line, int col) 0213 { 0214 const auto style = doc->defaultStyleAt({line, col}); 0215 return style == KSyntaxHighlighting::Theme::TextStyle::Comment || style == KSyntaxHighlighting::Theme::TextStyle::Char 0216 || style == KSyntaxHighlighting::Theme::TextStyle::String; 0217 } 0218 0219 using ColoredBracket = std::unique_ptr<KTextEditor::MovingRange>; 0220 using ColoredBracketPair = std::pair<std::unique_ptr<KTextEditor::MovingRange>, std::unique_ptr<KTextEditor::MovingRange>>; 0221 0222 /** 0223 * Helper function to find if we have @p open and @p close already in oldRanges so we 0224 * can reuse it 0225 */ 0226 static ColoredBracketPair existingColoredBracketForPos(std::vector<ColoredBracket> &oldRanges, KTextEditor::Cursor open, KTextEditor::Cursor close) 0227 { 0228 bool openFound = false; 0229 bool closeFound = false; 0230 int sIdx = 0; 0231 int eIdx = 0; 0232 int i = 0; 0233 for (const auto &range : oldRanges) { 0234 if (!openFound && range->start() == open) { 0235 openFound = true; 0236 sIdx = i; 0237 } else if (!closeFound && range->start() == close) { 0238 closeFound = true; 0239 eIdx = i; 0240 } 0241 if (openFound && closeFound) { 0242 ColoredBracketPair ret = {std::move(oldRanges[sIdx]), std::move(oldRanges[eIdx])}; 0243 oldRanges.erase(std::remove(oldRanges.begin(), oldRanges.end(), nullptr), oldRanges.end()); 0244 return ret; 0245 } 0246 i++; 0247 } 0248 return {nullptr, nullptr}; 0249 } 0250 0251 /** 0252 * This function contains the entirety of the algorithm 0253 * 0254 * How it works (or supposed to): 0255 * - Get current view line range (our viewport) 0256 * - Collect all bracket pairs in it 0257 * - Color them 0258 * 0259 * We check for vertical scrolling + text insertion to redo everything. 0260 * This allows us to do a lot less work and still be able to do coloring 0261 * of brackets. 0262 */ 0263 void RainbowParenPluginView::rehighlight(KTextEditor::View *view) 0264 { 0265 // we only care about lines that are in the viewport 0266 int start = view->firstDisplayedLine(); 0267 int end = view->lastDisplayedLine(); 0268 if (end < start) { 0269 qWarning() << "RainbowParenPluginView: Unexpected end < start"; 0270 return; 0271 } 0272 0273 // if the lines are folded we can get really big range 0274 if (end - start > 800) { 0275 end = start + 800; 0276 } 0277 0278 /** The brackets that we support for now **/ 0279 constexpr int totalBracketTypes = 3; 0280 static constexpr std::array<QChar, totalBracketTypes> opens = {QLatin1Char('{'), QLatin1Char('('), QLatin1Char('[')}; 0281 static constexpr std::array<QChar, totalBracketTypes> closes = {QLatin1Char('}'), QLatin1Char(')'), QLatin1Char(']')}; 0282 0283 // Check if @p c matches any opener 0284 auto matchOpen = [](QChar c) { 0285 for (auto o : opens) { 0286 if (o == c) { 0287 return true; 0288 } 0289 } 0290 return false; 0291 }; 0292 // Check if @p c matches any closer 0293 auto matchClose = [](QChar c) { 0294 for (auto o : closes) { 0295 if (o == c) { 0296 return true; 0297 } 0298 } 0299 return false; 0300 }; 0301 // Check at which index @p c is in openers 0302 auto indexOfOpener = [](QChar c) { 0303 for (int i = 0; i < (int)opens.size(); ++i) { 0304 if (c == opens[i]) { 0305 return i; 0306 } 0307 } 0308 return -1; 0309 }; 0310 0311 // A struct representing an opening bracket 0312 struct Opener { 0313 QChar bracketChar; 0314 KTextEditor::Cursor pos; 0315 }; 0316 0317 // A struct representing a bracket pair (open, close) 0318 struct BracketPair { 0319 BracketPair(KTextEditor::Cursor oo, KTextEditor::Cursor cc) 0320 : opener(oo) 0321 , closer(cc) 0322 { 0323 } 0324 KTextEditor::Cursor opener; 0325 KTextEditor::Cursor closer; 0326 }; 0327 0328 // contains all final bracket pairs of current viewport 0329 std::vector<BracketPair> parens; 0330 // the stack we use to build "parens" 0331 std::vector<Opener> bracketStack; 0332 0333 KTextEditor::Document *doc = view->document(); 0334 0335 for (int l = start; l <= end; ++l) { 0336 const QString line = doc->line(l); 0337 for (int c = 0; c < line.length(); ++c) { 0338 if (isCommentOrString(doc, l, c)) { 0339 continue; 0340 } 0341 0342 if (matchOpen(line.at(c))) { 0343 Opener o; 0344 o.bracketChar = line.at(c); 0345 o.pos = {l, c}; 0346 bracketStack.push_back(o); 0347 } else if (matchClose(line.at(c))) { 0348 if (!bracketStack.empty()) { 0349 auto opener = bracketStack.back(); 0350 int idx = indexOfOpener(opener.bracketChar); 0351 if (idx == -1) { 0352 continue; 0353 } 0354 QChar closerChar = closes[idx]; 0355 0356 if (closerChar != line.at(c)) { 0357 continue; 0358 } 0359 0360 parens.push_back({opener.pos, {l, c}}); 0361 bracketStack.pop_back(); 0362 } 0363 } 0364 } 0365 } 0366 0367 // We reuse ranges completely if there was no change but the user 0368 // was only scrolling. This allows the colors to stay somewhat stable 0369 // and not change all the time 0370 auto oldRanges = std::move(ranges); 0371 0372 if (parens.empty()) 0373 return; 0374 0375 // sort by start paren 0376 // Necessary so that we can get alternating colors for brackets 0377 // on the same line 0378 std::stable_sort(parens.begin(), parens.end(), [](const auto &a, const auto &b) { 0379 return a.opener < b.opener; 0380 }); 0381 0382 auto onSameLine = [](KTextEditor::Cursor open, KTextEditor::Cursor close) { 0383 return open.line() == close.line(); 0384 }; 0385 0386 size_t idx = 0; 0387 size_t color = m_lastUserColor; 0388 int lastParenLine = 0; 0389 const auto &attrs = m_plugin->colorsList(); 0390 for (auto p : parens) { 0391 // scope guard to ensure we always update stuff for every iteration 0392 auto updater = qScopeGuard([&idx, &lastParenLine, p] { 0393 idx++; 0394 lastParenLine = p.opener.line(); 0395 }); 0396 0397 auto cur1 = p.opener; 0398 cur1.setColumn(cur1.column() + 1); 0399 0400 // no '()' or '{}' highlighting 0401 if (cur1 == p.closer) { 0402 continue; 0403 } 0404 0405 // open/close brackets on same line? 0406 if (lastParenLine != p.opener.line() && onSameLine(p.opener, p.closer)) { 0407 // check next position, 0408 // if the next opener's line is not the same as current opener's line 0409 // then it is on some other line which means we don't need to highlight 0410 // this pair of brackets 0411 if (idx + 1 < parens.size() && parens.at(idx + 1).opener.line() != p.opener.line()) { 0412 continue; 0413 } 0414 } 0415 0416 // find if we already highlighted this bracket 0417 auto [existingStart, existingEnd] = existingColoredBracketForPos(oldRanges, p.opener, p.closer); 0418 if (existingStart && existingEnd) { 0419 ranges.push_back(std::move(existingStart)); 0420 ranges.push_back(std::move(existingEnd)); 0421 auto attrib = ranges.back()->attribute(); 0422 auto it = std::find(attrs.begin(), attrs.end(), attrib); 0423 auto prevColor = color; 0424 color = std::distance(attrs.begin(), it) + 1; 0425 color = prevColor == color ? color + 1 : color; 0426 continue; 0427 } 0428 0429 std::unique_ptr<KTextEditor::MovingRange> r(doc->newMovingRange({p.opener, cur1})); 0430 r->setAttribute(attrs[color % numberOfColors]); 0431 0432 auto cur2 = p.closer; 0433 cur2.setColumn(cur2.column() + 1); 0434 std::unique_ptr<KTextEditor::MovingRange> r2(doc->newMovingRange({p.closer, cur2})); 0435 r2->setAttribute(attrs[color % numberOfColors]); 0436 0437 ranges.push_back(std::move(r)); 0438 ranges.push_back(std::move(r2)); 0439 0440 color++; 0441 } 0442 m_lastUserColor = color; 0443 oldRanges.clear(); 0444 } 0445 0446 RainbowParenConfigPage::RainbowParenConfigPage(QWidget *parent, RainbowParenPlugin *plugin) 0447 : KTextEditor::ConfigPage(parent) 0448 , m_plugin(plugin) 0449 { 0450 auto layout = new QVBoxLayout(this); 0451 layout->setContentsMargins({}); 0452 0453 auto label = new QLabel(this); 0454 label->setText(i18n("Choose colors that will be used for bracket coloring:")); 0455 label->setWordWrap(true); 0456 layout->addWidget(label); 0457 0458 for (auto &btn : m_btns) { 0459 auto hl = new QHBoxLayout; 0460 hl->addWidget(&btn); 0461 hl->addStretch(); 0462 hl->setContentsMargins({}); 0463 layout->addLayout(hl); 0464 btn.setMinimumWidth(150); 0465 connect(&btn, &KColorButton::changed, this, &RainbowParenConfigPage::changed); 0466 } 0467 layout->addStretch(); 0468 0469 reset(); 0470 } 0471 0472 QString RainbowParenConfigPage::name() const 0473 { 0474 return i18n("Colored Brackets"); 0475 } 0476 0477 QString RainbowParenConfigPage::fullName() const 0478 { 0479 return i18n("Colored Brackets Settings"); 0480 } 0481 0482 static QIcon ColoredBracketsIcon(const QWidget *_this) 0483 { 0484 QPixmap p; 0485 { 0486 QRect r(QPoint(0, 0), _this->devicePixelRatioF() * QSize(16, 16)); 0487 QPixmap pix(r.size()); 0488 pix.fill(Qt::transparent); 0489 QPainter paint(&pix); 0490 if (paint.fontMetrics().height() > r.height()) { 0491 auto f = paint.font(); 0492 f.setPixelSize(14); 0493 paint.setFont(f); 0494 } 0495 paint.drawText(r, Qt::AlignCenter, QStringLiteral("{.}")); 0496 paint.end(); 0497 p = pix; 0498 } 0499 0500 QPainter paint(&p); 0501 paint.setCompositionMode(QPainter::CompositionMode_SourceIn); 0502 paint.fillRect(p.rect(), QBrush(QGradient(QGradient::Preset::FruitBlend))); 0503 paint.end(); 0504 return p; 0505 } 0506 0507 QIcon RainbowParenConfigPage::icon() const 0508 { 0509 if (m_icon.isNull()) { 0510 const_cast<RainbowParenConfigPage *>(this)->m_icon = ColoredBracketsIcon(this); 0511 } 0512 return m_icon; 0513 } 0514 0515 void RainbowParenConfigPage::apply() 0516 { 0517 KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("ColoredBrackets")); 0518 config.writeEntry("color1", m_btns[0].color().name(QColor::HexRgb)); 0519 config.writeEntry("color2", m_btns[1].color().name(QColor::HexRgb)); 0520 config.writeEntry("color3", m_btns[2].color().name(QColor::HexRgb)); 0521 config.writeEntry("color4", m_btns[3].color().name(QColor::HexRgb)); 0522 config.writeEntry("color5", m_btns[4].color().name(QColor::HexRgb)); 0523 config.sync(); 0524 Q_EMIT m_plugin->readConfig(); 0525 } 0526 0527 void RainbowParenConfigPage::reset() 0528 { 0529 int i = 0; 0530 for (const auto &attr : m_plugin->colorsList()) { 0531 m_btns[i++].setColor(attr->foreground().color()); 0532 } 0533 } 0534 0535 void RainbowParenConfigPage::defaults() 0536 { 0537 } 0538 0539 #include "moc_rainbowparens_plugin.cpp" 0540 #include "rainbowparens_plugin.moc"