1 /*
2     SPDX-FileCopyrightText: 2014-2019 Dominik Haumann <dhaumann@kde.org>
3     SPDX-FileCopyrightText: 2020 Waqar Ahmed <waqar.17a@gmail.com>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 #include "gotosymbolwidget.h"
8 #include "gotoglobalsymbolmodel.h"
9 #include "gotosymbolmodel.h"
10 #include "gotosymboltreeview.h"
11 #include "kate_ctags_view.h"
12 #include "tags.h"
13 
14 #include <QCoreApplication>
15 #include <QKeyEvent>
16 #include <QLineEdit>
17 #include <QPainter>
18 #include <QPropertyAnimation>
19 #include <QScrollBar>
20 #include <QSortFilterProxyModel>
21 #include <QStyledItemDelegate>
22 #include <QTextDocument>
23 #include <QVBoxLayout>
24 
25 #include <KTextEditor/MainWindow>
26 #include <KTextEditor/Message>
27 #include <KTextEditor/View>
28 
29 class QuickOpenFilterProxyModel : public QSortFilterProxyModel
30 {
31 public:
QuickOpenFilterProxyModel(QObject * parent=nullptr)32     QuickOpenFilterProxyModel(QObject *parent = nullptr)
33         : QSortFilterProxyModel(parent)
34     {
35     }
36 
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const37     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
38     {
39         const QString fileName = sourceModel()->index(sourceRow, 0, sourceParent).data().toString();
40         for (const QString &str : m_filterStrings) {
41             if (!fileName.contains(str, Qt::CaseInsensitive)) {
42                 return false;
43             }
44         }
45         return true;
46     }
47 
filterStrings() const48     QStringList filterStrings() const
49     {
50         return m_filterStrings;
51     }
52 
53 public Q_SLOTS:
setFilterText(const QString & text)54     void setFilterText(const QString &text)
55     {
56         m_filterStrings = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
57 
58         invalidateFilter();
59     }
60 
61 private:
62     QStringList m_filterStrings;
63 };
64 
65 class GotoStyleDelegate : public QStyledItemDelegate
66 {
67 public:
GotoStyleDelegate(QObject * parent=nullptr)68     GotoStyleDelegate(QObject *parent = nullptr)
69         : QStyledItemDelegate(parent)
70     {
71     }
72 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const73     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
74     {
75         QStyleOptionViewItem options = option;
76         initStyleOption(&options, index);
77 
78         QTextDocument doc;
79 
80         QString str = index.data().toString();
81         for (const auto &string : m_filterStrings) {
82             // FIXME: This will skip the letter 'b' if the string
83             // has only one letter so that we don't match inside
84             // <b> tags.
85             if (string == QLatin1String("b")) {
86                 continue;
87             }
88             const QRegularExpression re(QStringLiteral("(") + QRegularExpression::escape(string) + QStringLiteral(")"),
89                                         QRegularExpression::CaseInsensitiveOption);
90             str.replace(re, QStringLiteral("<b>\\1</b>"));
91         }
92 
93         auto file = index.data(GotoGlobalSymbolModel::FileUrl).toString();
94         // this will be empty for local symbol mode
95         if (!file.isEmpty()) {
96             str += QStringLiteral(" &nbsp;<span style=\"color: gray;\">") + QFileInfo(file).fileName() + QStringLiteral("</span>");
97         }
98 
99         doc.setHtml(str);
100         doc.setDocumentMargin(2);
101 
102         painter->save();
103 
104         // paint background
105         if (option.state & QStyle::State_Selected) {
106             painter->fillRect(option.rect, option.palette.highlight());
107         } else {
108             painter->fillRect(option.rect, option.palette.base());
109         }
110 
111         options.text = QString(); // clear old text
112         options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
113 
114         // draw text
115         painter->translate(option.rect.x(), option.rect.y());
116         if (index.column() == 0) {
117             painter->translate(25, 0);
118         }
119         doc.drawContents(painter);
120 
121         painter->restore();
122     }
123 
124 public Q_SLOTS:
setFilterStrings(const QString & text)125     void setFilterStrings(const QString &text)
126     {
127         m_filterStrings = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
128     }
129 
130 private:
131     QStringList m_filterStrings;
132 };
133 
GotoSymbolWidget(KTextEditor::MainWindow * mainWindow,KateCTagsView * pluginView,QWidget * widget)134 GotoSymbolWidget::GotoSymbolWidget(KTextEditor::MainWindow *mainWindow, KateCTagsView *pluginView, QWidget *widget)
135     : QWidget(widget)
136     , ctagsPluginView(pluginView)
137     , m_mainWindow(mainWindow)
138     , oldPos(-1, -1)
139 {
140     setWindowFlags(Qt::FramelessWindowHint);
141 
142     mode = Local;
143 
144     m_treeView = new GotoSymbolTreeView(mainWindow, this);
145     m_styleDelegate = new GotoStyleDelegate(this);
146     m_treeView->setItemDelegate(m_styleDelegate);
147     m_lineEdit = new QLineEdit(this);
148 
149     setFocusProxy(m_lineEdit);
150 
151     m_proxyModel = new QuickOpenFilterProxyModel(this);
152     m_proxyModel->setSortRole(Qt::DisplayRole);
153     m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
154     m_proxyModel->setFilterRole(Qt::DisplayRole);
155     m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
156     m_proxyModel->setFilterKeyColumn(0);
157 
158     m_symbolsModel = new GotoSymbolModel(this);
159     m_globalSymbolsModel = new GotoGlobalSymbolModel(this);
160 
161     m_proxyModel->setSourceModel(m_symbolsModel);
162     m_treeView->setModel(m_proxyModel);
163 
164     connect(m_lineEdit, &QLineEdit::textChanged, m_proxyModel, &QuickOpenFilterProxyModel::setFilterText);
165     connect(m_lineEdit, &QLineEdit::textChanged, m_styleDelegate, &GotoStyleDelegate::setFilterStrings);
166     connect(m_lineEdit, &QLineEdit::textChanged, this, [this]() {
167         m_treeView->viewport()->update();
168     });
169     connect(m_lineEdit, &QLineEdit::textChanged, this, &GotoSymbolWidget::loadGlobalSymbols);
170     connect(m_lineEdit, &QLineEdit::returnPressed, this, &GotoSymbolWidget::slotReturnPressed);
171 
172     connect(m_treeView, &QTreeView::activated, this, &GotoSymbolWidget::slotReturnPressed);
173     connect(m_proxyModel, &QSortFilterProxyModel::rowsInserted, this, &GotoSymbolWidget::reselectFirst);
174     connect(m_proxyModel, &QSortFilterProxyModel::rowsRemoved, this, &GotoSymbolWidget::reselectFirst);
175 
176     QVBoxLayout *layout = new QVBoxLayout();
177     layout->setSpacing(0);
178     layout->setContentsMargins(4, 4, 4, 4);
179     layout->addWidget(m_lineEdit);
180     layout->addWidget(m_treeView);
181     setLayout(layout);
182 
183     m_treeView->installEventFilter(this);
184     m_lineEdit->installEventFilter(this);
185 }
186 
eventFilter(QObject * obj,QEvent * event)187 bool GotoSymbolWidget::eventFilter(QObject *obj, QEvent *event)
188 {
189     if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
190         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
191         if (obj == m_lineEdit) {
192             const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
193                 || (keyEvent->key() == Qt::Key_PageDown);
194             if (forward2list) {
195                 QCoreApplication::sendEvent(m_treeView, event);
196                 return true;
197             }
198 
199             if (keyEvent->key() == Qt::Key_Escape) {
200                 if (oldPos.isValid()) {
201                     m_mainWindow->activeView()->setCursorPosition(oldPos);
202                 }
203                 m_lineEdit->clear();
204                 keyEvent->accept();
205                 hide();
206                 return true;
207             }
208         } else {
209             const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
210                 && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
211             if (forward2input) {
212                 QCoreApplication::sendEvent(m_lineEdit, event);
213                 return true;
214             }
215         }
216     }
217 
218     else if (event->type() == QEvent::FocusOut && !(m_lineEdit->hasFocus() || m_treeView->hasFocus())) {
219         m_lineEdit->clear();
220         hide();
221         return true;
222     }
223 
224     return QWidget::eventFilter(obj, event);
225 }
226 
showSymbols(const QString & filePath)227 void GotoSymbolWidget::showSymbols(const QString &filePath)
228 {
229     changeMode(Local);
230     oldPos = m_mainWindow->activeView()->cursorPosition();
231     m_symbolsModel->refresh(filePath);
232     updateViewGeometry();
233     reselectFirst();
234 }
235 
showGlobalSymbols(const QString & tagFilePath)236 void GotoSymbolWidget::showGlobalSymbols(const QString &tagFilePath)
237 {
238     changeMode(Global);
239     m_tagFile = tagFilePath;
240     updateViewGeometry();
241 }
242 
loadGlobalSymbols(const QString & text)243 void GotoSymbolWidget::loadGlobalSymbols(const QString &text)
244 {
245     if (m_tagFile.isEmpty() || !QFileInfo::exists(m_tagFile) || !QFileInfo(m_tagFile).isFile()) {
246         Tags::TagEntry e(i18n("Tags file not found. Please generate one manually or using the CTags plugin"), QString(), QString(), QString());
247         m_globalSymbolsModel->setSymbolsData({e});
248         return;
249     }
250 
251     if (text.length() < 3 || mode == Local) {
252         return;
253     }
254 
255     QString currentWord = text;
256     Tags::TagList list = Tags::getPartialMatchesNoi8n(m_tagFile, currentWord);
257 
258     if (list.isEmpty()) {
259         return;
260     }
261 
262     m_globalSymbolsModel->setSymbolsData(std::move(list));
263     updateViewGeometry();
264     reselectFirst();
265 }
266 
slotReturnPressed()267 void GotoSymbolWidget::slotReturnPressed()
268 {
269     const auto idx = m_proxyModel->index(m_treeView->currentIndex().row(), 0);
270     if (!idx.isValid()) {
271         return;
272     }
273 
274     if (mode == Global) {
275         QString tag = idx.data(Qt::UserRole).toString();
276         QString pattern = idx.data(GotoGlobalSymbolModel::Pattern).toString();
277         QString file = idx.data(GotoGlobalSymbolModel::FileUrl).toString();
278         bool fileFound = true;
279 
280         QFileInfo fi(file);
281         QString url;
282         // if the file doesn't exist, try to load it using project base dir
283         if (!fi.exists()) {
284             fileFound = false;
285             QObject *projectView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin"));
286             QString ret = projectView ? projectView->property("projectBaseDir").toString() : QString();
287             if (!ret.isEmpty() && !ret.endsWith(QLatin1Char('/'))) {
288                 ret.append(QLatin1Char('/'));
289             }
290             url = ret + file;
291             fi.setFile(url);
292 
293             // check again
294             // not found? use tagFile path as base path
295             if (!fi.exists()) {
296                 url.clear();
297                 fi.setFile(m_tagFile);
298                 QString path = fi.absolutePath();
299                 url = path + QStringLiteral("/") + file;
300 
301                 fi.setFile(url);
302                 if (fi.exists()) {
303                     fileFound = true;
304                 }
305             } else {
306                 fileFound = true;
307             }
308         } else {
309             url = file;
310         }
311 
312         if (fileFound) {
313             ctagsPluginView->jumpToTag(url, pattern, tag);
314         } else {
315             QString msg = i18n("File for '%1' not found.", tag);
316             auto message = new KTextEditor::Message(msg, KTextEditor::Message::MessageType::Error);
317             if (auto view = m_mainWindow->activeView()) {
318                 view->document()->postMessage(message);
319             }
320         }
321 
322     } else {
323         int line = idx.data(Qt::UserRole).toInt();
324 
325         // try to find the start position of this tag
326         // and put the cursor there
327         QString tag = idx.data().toString();
328         QString textLine = m_mainWindow->activeView()->document()->line(--line);
329         int col = textLine.indexOf(tag.midRef(0, 4));
330         col = col >= 0 ? col : 0;
331         KTextEditor::Cursor c(line, col);
332 
333         m_mainWindow->activeView()->setCursorPosition(c);
334     }
335 
336     // block signals, so that rowsInserted isn't emitted causing us to loose position
337     const QSignalBlocker blocker(m_proxyModel);
338 
339     m_lineEdit->clear();
340     hide();
341 }
342 
changeMode(GotoSymbolWidget::Mode newMode)343 void GotoSymbolWidget::changeMode(GotoSymbolWidget::Mode newMode)
344 {
345     mode = newMode;
346     if (mode == Global) {
347         m_proxyModel->setSourceModel(m_globalSymbolsModel);
348         m_treeView->setGlobalMode(true);
349     } else if (mode == Local) {
350         m_proxyModel->setSourceModel(m_symbolsModel);
351         m_treeView->setGlobalMode(false);
352     }
353 }
354 
updateViewGeometry()355 void GotoSymbolWidget::updateViewGeometry()
356 {
357     QWidget *window = m_mainWindow->window();
358     const QSize centralSize = window->size();
359 
360     // width: 2.4 of editor, height: 1/2 of editor
361     const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
362 
363     const int rowHeight = m_treeView->sizeHintForRow(0) == -1 ? 0 : m_treeView->sizeHintForRow(0);
364 
365     int frameWidth = this->frameSize().width();
366     frameWidth = frameWidth > centralSize.width() / 2.4 ? centralSize.width() / 2.4 : frameWidth;
367 
368     const int width = viewMaxSize.width();
369 
370     const int rowCount = mode == Global ? m_globalSymbolsModel->rowCount() : m_symbolsModel->rowCount();
371 
372     const QSize viewSize(width, std::min(std::max(rowHeight * rowCount + 2 * frameWidth, rowHeight * 6), viewMaxSize.height()));
373 
374     // Position should be central over the editor area, so map to global from
375     // parent of central widget since the view is positioned in global coords
376     const QPoint centralWidgetPos = window->parentWidget() ? window->mapToGlobal(window->pos()) : window->pos();
377     const int xPos = std::max(0, centralWidgetPos.x() + (centralSize.width() - viewSize.width()) / 2);
378     const int yPos = std::max(0, centralWidgetPos.y() + (centralSize.height() - viewSize.height()) * 1 / 4);
379 
380     move(xPos, yPos);
381 
382     QPropertyAnimation *animation = new QPropertyAnimation(this, "size");
383     animation->setDuration(150);
384     animation->setStartValue(this->size());
385     animation->setEndValue(viewSize);
386 
387     animation->start(QPropertyAnimation::DeleteWhenStopped);
388 }
389 
reselectFirst()390 void GotoSymbolWidget::reselectFirst()
391 {
392     QModelIndex index = m_proxyModel->index(0, 0);
393     if (index.isValid()) {
394         m_treeView->setCurrentIndex(index);
395     }
396 }
397