File indexing completed on 2024-04-28 05:49:10

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"