File indexing completed on 2024-05-05 05:51:37

0001 /*
0002     SPDX-FileCopyrightText: 2014-2019 Dominik Haumann <dhaumann@kde.org>
0003     SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 #include "gotosymbolwidget.h"
0008 #include "gotoglobalsymbolmodel.h"
0009 #include "gotosymbolmodel.h"
0010 #include "gotosymboltreeview.h"
0011 #include "kate_ctags_view.h"
0012 #include "tags.h"
0013 
0014 #include <QCoreApplication>
0015 #include <QKeyEvent>
0016 #include <QLineEdit>
0017 #include <QPainter>
0018 #include <QPropertyAnimation>
0019 #include <QSortFilterProxyModel>
0020 #include <QStyledItemDelegate>
0021 #include <QTextDocument>
0022 #include <QVBoxLayout>
0023 
0024 #include <KTextEditor/Document>
0025 #include <KTextEditor/MainWindow>
0026 #include <KTextEditor/Message>
0027 #include <KTextEditor/View>
0028 
0029 class CtagsGotoSymbolProxyModel : public QSortFilterProxyModel
0030 {
0031 public:
0032     explicit CtagsGotoSymbolProxyModel(QObject *parent = nullptr)
0033         : QSortFilterProxyModel(parent)
0034     {
0035     }
0036 
0037     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
0038     {
0039         const QString fileName = sourceModel()->index(sourceRow, 0, sourceParent).data().toString();
0040         for (const QString &str : m_filterStrings) {
0041             if (!fileName.contains(str, Qt::CaseInsensitive)) {
0042                 return false;
0043             }
0044         }
0045         return true;
0046     }
0047 
0048     QStringList filterStrings() const
0049     {
0050         return m_filterStrings;
0051     }
0052 
0053 public Q_SLOTS:
0054     void setFilterText(const QString &text)
0055     {
0056         m_filterStrings = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
0057 
0058         invalidateFilter();
0059     }
0060 
0061 private:
0062     QStringList m_filterStrings;
0063 };
0064 
0065 class GotoStyleDelegate : public QStyledItemDelegate
0066 {
0067 public:
0068     explicit GotoStyleDelegate(QObject *parent = nullptr)
0069         : QStyledItemDelegate(parent)
0070     {
0071     }
0072 
0073     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0074     {
0075         QStyleOptionViewItem options = option;
0076         initStyleOption(&options, index);
0077 
0078         QTextDocument doc;
0079 
0080         QString str = index.data().toString();
0081         for (const auto &string : m_filterStrings) {
0082             // FIXME: This will skip the letter 'b' if the string
0083             // has only one letter so that we don't match inside
0084             // <b> tags.
0085             if (string == QLatin1String("b")) {
0086                 continue;
0087             }
0088             const QRegularExpression re(QStringLiteral("(") + QRegularExpression::escape(string) + QStringLiteral(")"),
0089                                         QRegularExpression::CaseInsensitiveOption);
0090             str.replace(re, QStringLiteral("<b>\\1</b>"));
0091         }
0092 
0093         auto file = index.data(GotoGlobalSymbolModel::FileUrl).toString();
0094         // this will be empty for local symbol mode
0095         if (!file.isEmpty()) {
0096             str += QStringLiteral(" &nbsp;<span style=\"color: gray;\">") + QFileInfo(file).fileName() + QStringLiteral("</span>");
0097         }
0098 
0099         doc.setHtml(str);
0100         doc.setDocumentMargin(2);
0101 
0102         painter->save();
0103 
0104         // paint background
0105         if (option.state & QStyle::State_Selected) {
0106             painter->fillRect(option.rect, option.palette.highlight());
0107         } else {
0108             painter->fillRect(option.rect, option.palette.base());
0109         }
0110 
0111         options.text = QString(); // clear old text
0112         options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
0113 
0114         // draw text
0115         painter->translate(option.rect.x(), option.rect.y());
0116         if (index.column() == 0) {
0117             painter->translate(25, 0);
0118         }
0119         doc.drawContents(painter);
0120 
0121         painter->restore();
0122     }
0123 
0124 public Q_SLOTS:
0125     void setFilterStrings(const QString &text)
0126     {
0127         m_filterStrings = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
0128     }
0129 
0130 private:
0131     QStringList m_filterStrings;
0132 };
0133 
0134 GotoSymbolWidget::GotoSymbolWidget(KTextEditor::MainWindow *mainWindow, KateCTagsView *pluginView, QWidget *widget)
0135     : QWidget(widget)
0136     , ctagsPluginView(pluginView)
0137     , m_mainWindow(mainWindow)
0138     , oldPos(-1, -1)
0139 {
0140     setWindowFlags(Qt::FramelessWindowHint);
0141 
0142     mode = Local;
0143 
0144     m_treeView = new GotoSymbolTreeView(mainWindow, this);
0145     m_styleDelegate = new GotoStyleDelegate(this);
0146     m_treeView->setItemDelegate(m_styleDelegate);
0147     m_lineEdit = new QLineEdit(this);
0148 
0149     setFocusProxy(m_lineEdit);
0150 
0151     m_proxyModel = new CtagsGotoSymbolProxyModel(this);
0152     m_proxyModel->setSortRole(Qt::DisplayRole);
0153     m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
0154     m_proxyModel->setFilterRole(Qt::DisplayRole);
0155     m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0156     m_proxyModel->setFilterKeyColumn(0);
0157 
0158     m_symbolsModel = new GotoSymbolModel(this);
0159     m_globalSymbolsModel = new GotoGlobalSymbolModel(this);
0160 
0161     m_proxyModel->setSourceModel(m_symbolsModel);
0162     m_treeView->setModel(m_proxyModel);
0163 
0164     connect(m_lineEdit, &QLineEdit::textChanged, m_proxyModel, &CtagsGotoSymbolProxyModel::setFilterText);
0165     connect(m_lineEdit, &QLineEdit::textChanged, m_styleDelegate, &GotoStyleDelegate::setFilterStrings);
0166     connect(m_lineEdit, &QLineEdit::textChanged, this, [this]() {
0167         m_treeView->viewport()->update();
0168     });
0169     connect(m_lineEdit, &QLineEdit::textChanged, this, &GotoSymbolWidget::loadGlobalSymbols);
0170     connect(m_lineEdit, &QLineEdit::returnPressed, this, &GotoSymbolWidget::slotReturnPressed);
0171 
0172     connect(m_treeView, &QTreeView::activated, this, &GotoSymbolWidget::slotReturnPressed);
0173     connect(m_proxyModel, &QSortFilterProxyModel::rowsInserted, this, &GotoSymbolWidget::reselectFirst);
0174     connect(m_proxyModel, &QSortFilterProxyModel::rowsRemoved, this, &GotoSymbolWidget::reselectFirst);
0175 
0176     QVBoxLayout *layout = new QVBoxLayout();
0177     layout->setSpacing(0);
0178     layout->setContentsMargins(4, 4, 4, 4);
0179     layout->addWidget(m_lineEdit);
0180     layout->addWidget(m_treeView);
0181     setLayout(layout);
0182 
0183     m_treeView->installEventFilter(this);
0184     m_lineEdit->installEventFilter(this);
0185 }
0186 
0187 bool GotoSymbolWidget::eventFilter(QObject *obj, QEvent *event)
0188 {
0189     if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
0190         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0191         if (obj == m_lineEdit) {
0192             const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
0193                 || (keyEvent->key() == Qt::Key_PageDown);
0194             if (forward2list) {
0195                 QCoreApplication::sendEvent(m_treeView, event);
0196                 return true;
0197             }
0198 
0199             if (keyEvent->key() == Qt::Key_Escape) {
0200                 if (oldPos.isValid()) {
0201                     m_mainWindow->activeView()->setCursorPosition(oldPos);
0202                 }
0203                 m_lineEdit->clear();
0204                 keyEvent->accept();
0205                 hide();
0206                 return true;
0207             }
0208         } else {
0209             const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
0210                 && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
0211             if (forward2input) {
0212                 QCoreApplication::sendEvent(m_lineEdit, event);
0213                 return true;
0214             }
0215         }
0216     }
0217 
0218     else if (event->type() == QEvent::FocusOut && !(m_lineEdit->hasFocus() || m_treeView->hasFocus())) {
0219         m_lineEdit->clear();
0220         hide();
0221         return true;
0222     }
0223 
0224     return QWidget::eventFilter(obj, event);
0225 }
0226 
0227 void GotoSymbolWidget::showSymbols(const QString &filePath)
0228 {
0229     changeMode(Local);
0230     oldPos = m_mainWindow->activeView()->cursorPosition();
0231     m_symbolsModel->refresh(filePath);
0232     updateViewGeometry();
0233     reselectFirst();
0234 }
0235 
0236 void GotoSymbolWidget::showGlobalSymbols(const QString &tagFilePath)
0237 {
0238     changeMode(Global);
0239     m_tagFile = tagFilePath;
0240     updateViewGeometry();
0241 }
0242 
0243 void GotoSymbolWidget::loadGlobalSymbols(const QString &text)
0244 {
0245     if (m_tagFile.isEmpty() || !QFileInfo::exists(m_tagFile) || !QFileInfo(m_tagFile).isFile()) {
0246         Tags::TagEntry e(i18n("Tags file not found. Please generate one manually or using the CTags plugin"), QString(), QString(), QString());
0247         m_globalSymbolsModel->setSymbolsData({e});
0248         return;
0249     }
0250 
0251     if (text.length() < 3 || mode == Local) {
0252         return;
0253     }
0254 
0255     QString currentWord = text;
0256     Tags::TagList list = Tags::getPartialMatchesNoi8n(m_tagFile, currentWord);
0257 
0258     if (list.isEmpty()) {
0259         return;
0260     }
0261 
0262     m_globalSymbolsModel->setSymbolsData(std::move(list));
0263     updateViewGeometry();
0264     reselectFirst();
0265 }
0266 
0267 void GotoSymbolWidget::slotReturnPressed()
0268 {
0269     const auto idx = m_proxyModel->index(m_treeView->currentIndex().row(), 0);
0270     if (!idx.isValid()) {
0271         return;
0272     }
0273 
0274     if (mode == Global) {
0275         QString tag = idx.data(Qt::UserRole).toString();
0276         QString pattern = idx.data(GotoGlobalSymbolModel::Pattern).toString();
0277         QString file = idx.data(GotoGlobalSymbolModel::FileUrl).toString();
0278         bool fileFound = true;
0279 
0280         QFileInfo fi(file);
0281         QString url;
0282         // if the file doesn't exist, try to load it using project base dir
0283         if (!fi.exists()) {
0284             fileFound = false;
0285             QObject *projectView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin"));
0286             QString ret = projectView ? projectView->property("projectBaseDir").toString() : QString();
0287             if (!ret.isEmpty() && !ret.endsWith(QLatin1Char('/'))) {
0288                 ret.append(QLatin1Char('/'));
0289             }
0290             url = ret + file;
0291             fi.setFile(url);
0292 
0293             // check again
0294             // not found? use tagFile path as base path
0295             if (!fi.exists()) {
0296                 url.clear();
0297                 fi.setFile(m_tagFile);
0298                 QString path = fi.absolutePath();
0299                 url = path + QStringLiteral("/") + file;
0300 
0301                 fi.setFile(url);
0302                 if (fi.exists()) {
0303                     fileFound = true;
0304                 }
0305             } else {
0306                 fileFound = true;
0307             }
0308         } else {
0309             url = file;
0310         }
0311 
0312         if (fileFound) {
0313             ctagsPluginView->jumpToTag(url, pattern, tag);
0314         } else {
0315             QString msg = i18n("File for '%1' not found.", tag);
0316             auto message = new KTextEditor::Message(msg, KTextEditor::Message::MessageType::Error);
0317             if (auto view = m_mainWindow->activeView()) {
0318                 view->document()->postMessage(message);
0319             }
0320         }
0321 
0322     } else {
0323         int line = idx.data(Qt::UserRole).toInt();
0324 
0325         // try to find the start position of this tag
0326         // and put the cursor there
0327         const QString tag = idx.data().toString();
0328         const QString textLine = m_mainWindow->activeView()->document()->line(--line);
0329         int col = textLine.indexOf(QStringView(tag).mid(0, 4));
0330         col = col >= 0 ? col : 0;
0331         KTextEditor::Cursor c(line, col);
0332 
0333         m_mainWindow->activeView()->setCursorPosition(c);
0334     }
0335 
0336     // block signals, so that rowsInserted isn't emitted causing us to loose position
0337     const QSignalBlocker blocker(m_proxyModel);
0338 
0339     m_lineEdit->clear();
0340     hide();
0341 }
0342 
0343 void GotoSymbolWidget::changeMode(GotoSymbolWidget::Mode newMode)
0344 {
0345     mode = newMode;
0346     if (mode == Global) {
0347         m_proxyModel->setSourceModel(m_globalSymbolsModel);
0348         m_treeView->setGlobalMode(true);
0349     } else if (mode == Local) {
0350         m_proxyModel->setSourceModel(m_symbolsModel);
0351         m_treeView->setGlobalMode(false);
0352     }
0353 }
0354 
0355 void GotoSymbolWidget::updateViewGeometry()
0356 {
0357     QWidget *window = m_mainWindow->window();
0358     const QSize centralSize = window->size();
0359 
0360     // width: 2.4 of editor, height: 1/2 of editor
0361     const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
0362 
0363     const int rowHeight = m_treeView->sizeHintForRow(0) == -1 ? 0 : m_treeView->sizeHintForRow(0);
0364 
0365     int frameWidth = this->frameSize().width();
0366     frameWidth = frameWidth > centralSize.width() / 2.4 ? centralSize.width() / 2.4 : frameWidth;
0367 
0368     const int width = viewMaxSize.width();
0369 
0370     const int rowCount = mode == Global ? m_globalSymbolsModel->rowCount() : m_symbolsModel->rowCount();
0371 
0372     const QSize viewSize(width, std::min(std::max(rowHeight * rowCount + 2 * frameWidth, rowHeight * 6), viewMaxSize.height()));
0373 
0374     // Position should be central over the editor area, so map to global from
0375     // parent of central widget since the view is positioned in global coords
0376     const QPoint centralWidgetPos = window->parent() ? window->mapToGlobal(window->pos()) : window->pos();
0377     const int xPos = std::max(0, centralWidgetPos.x() + (centralSize.width() - viewSize.width()) / 2);
0378     const int yPos = std::max(0, centralWidgetPos.y() + (centralSize.height() - viewSize.height()) * 1 / 4);
0379 
0380     move(xPos, yPos);
0381 
0382     QPropertyAnimation *animation = new QPropertyAnimation(this, "size");
0383     animation->setDuration(150);
0384     animation->setStartValue(this->size());
0385     animation->setEndValue(viewSize);
0386 
0387     animation->start(QPropertyAnimation::DeleteWhenStopped);
0388 }
0389 
0390 void GotoSymbolWidget::reselectFirst()
0391 {
0392     QModelIndex index = m_proxyModel->index(0, 0);
0393     if (index.isValid()) {
0394         m_treeView->setCurrentIndex(index);
0395     }
0396 }
0397 
0398 #include "moc_gotosymbolwidget.cpp"