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(" <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"