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

0001 /*
0002     SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
0003     SPDX-FileCopyrightText: 2019 Christoph Cullmann <cullmann@kde.org>
0004 
0005     SPDX-License-Identifier: MIT
0006 */
0007 
0008 #include "lspclientsymbolview.h"
0009 
0010 #include <KFuzzyMatcher>
0011 #include <KLineEdit>
0012 #include <KLocalizedString>
0013 #include <QSortFilterProxyModel>
0014 
0015 #include <KTextEditor/Document>
0016 #include <KTextEditor/Editor>
0017 #include <KTextEditor/MainWindow>
0018 #include <KTextEditor/View>
0019 
0020 #include <QFrame>
0021 #include <QHBoxLayout>
0022 #include <QHeaderView>
0023 #include <QIdentityProxyModel>
0024 #include <QMenu>
0025 #include <QPointer>
0026 #include <QStandardItemModel>
0027 #include <QTimer>
0028 #include <QTreeView>
0029 
0030 #include <drawing_utils.h>
0031 #include <memory>
0032 #include <utility>
0033 
0034 // TODO: Make this globally available in shared/
0035 enum SymbolViewRoles { SymbolRange = Qt::UserRole, ScoreRole, IsPlaceholder };
0036 
0037 class LSPClientViewTrackerImpl : public LSPClientViewTracker
0038 {
0039     Q_OBJECT
0040 
0041     typedef LSPClientViewTrackerImpl self_type;
0042 
0043     LSPClientPlugin *m_plugin;
0044     KTextEditor::MainWindow *m_mainWindow;
0045     // timers to delay some todo's
0046     QTimer m_changeTimer;
0047     int m_change;
0048     QTimer m_motionTimer;
0049     int m_motion;
0050     int m_oldCursorLine = -1;
0051 
0052 public:
0053     LSPClientViewTrackerImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, int change_ms, int motion_ms)
0054         : m_plugin(plugin)
0055         , m_mainWindow(mainWin)
0056         , m_change(change_ms)
0057         , m_motion(motion_ms)
0058     {
0059         Q_UNUSED(m_plugin);
0060         // get updated
0061         m_changeTimer.setSingleShot(true);
0062         auto ch = [this]() {
0063             Q_EMIT newState(m_mainWindow->activeView(), TextChanged);
0064         };
0065         connect(&m_changeTimer, &QTimer::timeout, this, ch);
0066 
0067         m_motionTimer.setSingleShot(true);
0068         auto mh = [this]() {
0069             Q_EMIT newState(m_mainWindow->activeView(), LineChanged);
0070         };
0071         connect(&m_motionTimer, &QTimer::timeout, this, mh);
0072 
0073         // track views
0074         connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::viewChanged);
0075     }
0076 
0077     void viewChanged(KTextEditor::View *view)
0078     {
0079         m_motionTimer.stop();
0080         m_changeTimer.stop();
0081 
0082         if (view) {
0083             if (m_motion) {
0084                 connect(view, &KTextEditor::View::cursorPositionChanged, this, &self_type::cursorPositionChanged, Qt::UniqueConnection);
0085             }
0086             if (m_change > 0 && view->document()) {
0087                 connect(view->document(), &KTextEditor::Document::textChanged, this, &self_type::textChanged, Qt::UniqueConnection);
0088             }
0089             Q_EMIT newState(view, ViewChanged);
0090             m_oldCursorLine = view->cursorPosition().line();
0091         }
0092     }
0093 
0094     void textChanged()
0095     {
0096         m_motionTimer.stop();
0097         m_changeTimer.start(m_change);
0098     }
0099 
0100     void cursorPositionChanged(KTextEditor::View *view, const KTextEditor::Cursor &newPosition)
0101     {
0102         if (m_changeTimer.isActive()) {
0103             // change trumps motion
0104             return;
0105         }
0106 
0107         if (view && newPosition.line() != m_oldCursorLine) {
0108             m_oldCursorLine = newPosition.line();
0109             m_motionTimer.start(m_motion);
0110         }
0111     }
0112 };
0113 
0114 LSPClientViewTracker *LSPClientViewTracker::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, int change_ms, int motion_ms)
0115 {
0116     return new LSPClientViewTrackerImpl(plugin, mainWin, change_ms, motion_ms);
0117 }
0118 
0119 class LSPClientSymbolViewFilterProxyModel : public QSortFilterProxyModel
0120 {
0121 public:
0122     LSPClientSymbolViewFilterProxyModel(QObject *parent = nullptr)
0123         : QSortFilterProxyModel(parent)
0124     {
0125     }
0126 
0127     void setFilterString(const QString &string)
0128     {
0129         beginResetModel();
0130         m_pattern = string;
0131         endResetModel();
0132     }
0133 
0134 protected:
0135     bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
0136     {
0137         // make sure to honour configured sort-order (if no scoring applies)
0138         if (m_pattern.isEmpty()) {
0139             return QSortFilterProxyModel::lessThan(sourceLeft, sourceRight);
0140         }
0141 
0142         const int l = sourceLeft.data(SymbolViewRoles::ScoreRole).toInt();
0143         const int r = sourceRight.data(SymbolViewRoles::ScoreRole).toInt();
0144         return l < r;
0145     }
0146 
0147     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
0148     {
0149         if (m_pattern.isEmpty()) {
0150             return true;
0151         }
0152 
0153         const auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
0154         const QString symbol = idx.data().toString();
0155         const auto res = KFuzzyMatcher::match(m_pattern, symbol);
0156         sourceModel()->setData(idx, res.score, SymbolViewRoles::ScoreRole);
0157         return res.matched;
0158     }
0159 
0160 private:
0161     QString m_pattern;
0162 };
0163 
0164 class SymbolViewProxyModel : public QIdentityProxyModel
0165 {
0166     Q_OBJECT
0167 public:
0168     using QIdentityProxyModel::QIdentityProxyModel;
0169 
0170     int columnCount(const QModelIndex &) const override
0171     {
0172         return 1;
0173     }
0174 };
0175 
0176 /*
0177  * Instantiates and manages the symbol outline toolview.
0178  */
0179 class LSPClientSymbolViewImpl : public QObject, public LSPClientSymbolView
0180 {
0181     Q_OBJECT
0182 
0183     typedef LSPClientSymbolViewImpl self_type;
0184 
0185     LSPClientPlugin *m_plugin;
0186     KTextEditor::MainWindow *m_mainWindow;
0187     std::shared_ptr<LSPClientServerManager> m_serverManager;
0188     std::unique_ptr<QWidget> m_toolview;
0189     // parent ownership
0190     QPointer<QTreeView> m_symbols;
0191     QPointer<KLineEdit> m_filter;
0192     std::unique_ptr<QMenu> m_popup;
0193     // initialized/updated from plugin settings
0194     // managed by context menu later on
0195     // parent ownership
0196     QAction *m_detailsOn;
0197     QAction *m_expandOn;
0198     QAction *m_treeOn;
0199     QAction *m_sortOn;
0200     // view tracking
0201     std::unique_ptr<LSPClientViewTracker> m_viewTracker;
0202     // outstanding request
0203     LSPClientServer::RequestHandle m_handle;
0204     // magic request tracking cookie
0205     int m_requestCnt = 0;
0206     // cached outline models
0207     struct ModelData {
0208         QPointer<KTextEditor::Document> document;
0209         qint64 revision;
0210         std::shared_ptr<QStandardItemModel> model;
0211     };
0212     QList<ModelData> m_models;
0213     // max number to cache
0214     static constexpr int MAX_MODELS = 10;
0215     // last outline model we constructed
0216     std::shared_ptr<QStandardItemModel> m_outline;
0217     // filter model, setup once
0218     LSPClientSymbolViewFilterProxyModel m_filterModel;
0219 
0220     SymbolViewProxyModel *m_identityModel;
0221 
0222     // cached icons for model
0223     QIcon m_icon_pkg = QIcon::fromTheme(QStringLiteral("code-block"));
0224     QIcon m_icon_class = QIcon::fromTheme(QStringLiteral("code-class"));
0225     QIcon m_icon_typedef = QIcon::fromTheme(QStringLiteral("code-typedef"));
0226     QIcon m_icon_function = QIcon::fromTheme(QStringLiteral("code-function"));
0227     QIcon m_icon_var = QIcon::fromTheme(QStringLiteral("code-variable"));
0228 
0229 public:
0230     LSPClientSymbolViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, std::shared_ptr<LSPClientServerManager> manager)
0231         : m_plugin(plugin)
0232         , m_mainWindow(mainWin)
0233         , m_serverManager(std::move(manager))
0234         , m_outline(new QStandardItemModel())
0235         , m_identityModel(new SymbolViewProxyModel(this))
0236     {
0237         m_toolview.reset(m_mainWindow->createToolView(plugin,
0238                                                       QStringLiteral("lspclient_symbol_outline"),
0239                                                       KTextEditor::MainWindow::Left,
0240                                                       QIcon::fromTheme(QStringLiteral("quickopen-class")),
0241                                                       i18n("Symbol Outline")));
0242 
0243         m_symbols = new QTreeView(m_toolview.get());
0244         m_symbols->setFocusPolicy(Qt::NoFocus);
0245         m_symbols->setLayoutDirection(Qt::LeftToRight);
0246         m_toolview->layout()->setContentsMargins(0, 0, 0, 0);
0247 
0248         auto separator = new QFrame(m_toolview.get());
0249         separator->setFrameShape(QFrame::HLine);
0250         separator->setEnabled(false);
0251         m_toolview->layout()->addWidget(separator);
0252 
0253         m_toolview->layout()->addWidget(m_symbols);
0254         m_toolview->layout()->setSpacing(0);
0255 
0256         // setup filter line edit
0257         m_filter = new KLineEdit(m_toolview.get());
0258         m_toolview->layout()->addWidget(m_filter);
0259         m_filter->setPlaceholderText(i18n("Filter..."));
0260         m_filter->setClearButtonEnabled(true);
0261         m_filter->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
0262         connect(m_filter, &KLineEdit::textChanged, this, &self_type::filterTextChanged);
0263 
0264         m_symbols->setContextMenuPolicy(Qt::CustomContextMenu);
0265         m_symbols->setIndentation(10);
0266         m_symbols->setEditTriggers(QAbstractItemView::NoEditTriggers);
0267         m_symbols->setAllColumnsShowFocus(true);
0268 
0269         // init filter model once, later we only swap the source model!
0270         QItemSelectionModel *m = m_symbols->selectionModel();
0271         m_filterModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
0272         m_filterModel.setSortCaseSensitivity(Qt::CaseInsensitive);
0273         m_filterModel.setSourceModel(m_outline.get());
0274         m_filterModel.setRecursiveFilteringEnabled(true);
0275         m_symbols->setModel(&m_filterModel);
0276         delete m;
0277 
0278         m_identityModel->setSourceModel(m_outline.get());
0279 
0280         connect(m_symbols, &QTreeView::customContextMenuRequested, this, &self_type::showContextMenu);
0281         connect(m_symbols, &QTreeView::activated, this, &self_type::goToSymbol);
0282         connect(m_symbols, &QTreeView::clicked, this, &self_type::goToSymbol);
0283 
0284         // context menu
0285         m_popup.reset(new QMenu(m_symbols));
0286         m_treeOn = m_popup->addAction(i18n("Tree Mode"), this, &self_type::displayOptionChanged);
0287         m_treeOn->setCheckable(true);
0288         m_expandOn = m_popup->addAction(i18n("Automatically Expand Tree"), this, &self_type::displayOptionChanged);
0289         m_expandOn->setCheckable(true);
0290         m_sortOn = m_popup->addAction(i18n("Sort Alphabetically"), this, &self_type::displayOptionChanged);
0291         m_sortOn->setCheckable(true);
0292         m_detailsOn = m_popup->addAction(i18n("Show Details"), this, &self_type::displayOptionChanged);
0293         m_detailsOn->setCheckable(true);
0294         m_popup->addSeparator();
0295         m_popup->addAction(i18n("Expand All"), m_symbols.data(), &QTreeView::expandAll);
0296         m_popup->addAction(i18n("Collapse All"), m_symbols.data(), &QTreeView::collapseAll);
0297 
0298         // sync with plugin settings if updated
0299         connect(m_plugin, &LSPClientPlugin::update, this, &self_type::configUpdated);
0300 
0301         // get updated
0302         m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 500, 100));
0303         connect(m_viewTracker.get(), &LSPClientViewTracker::newState, this, &self_type::onViewState);
0304         connect(m_serverManager.get(), &LSPClientServerManager::serverChanged, this, [this]() {
0305             refresh(false, false);
0306         });
0307 
0308         // limit cached models; will not go beyond capacity set here
0309         m_models.reserve(MAX_MODELS + 1);
0310 
0311         // recolor icons
0312         QObject::connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, [this](KTextEditor::Editor *e) {
0313             colorIcons(e);
0314         });
0315         colorIcons(KTextEditor::Editor::instance());
0316 
0317         // initial trigger of symbols view update
0318         configUpdated();
0319     }
0320 
0321     void displayOptionChanged()
0322     {
0323         m_expandOn->setEnabled(m_treeOn->isChecked());
0324         refresh(false, false);
0325     }
0326 
0327     void configUpdated()
0328     {
0329         m_treeOn->setChecked(m_plugin->m_symbolTree);
0330         m_detailsOn->setChecked(m_plugin->m_symbolDetails);
0331         m_expandOn->setChecked(m_plugin->m_symbolExpand);
0332         m_sortOn->setChecked(m_plugin->m_symbolSort);
0333         displayOptionChanged();
0334     }
0335 
0336     void showContextMenu(const QPoint &pos)
0337     {
0338         m_popup->popup(m_symbols->viewport()->mapToGlobal(pos), m_treeOn);
0339     }
0340 
0341     void onViewState(KTextEditor::View *, LSPClientViewTracker::State newState)
0342     {
0343         switch (newState) {
0344         case LSPClientViewTracker::ViewChanged:
0345             refresh(true);
0346             break;
0347         case LSPClientViewTracker::TextChanged:
0348             refresh(false);
0349             break;
0350         case LSPClientViewTracker::LineChanged:
0351             updateCurrentTreeItem();
0352             break;
0353         }
0354     }
0355 
0356     void makeNodes(const std::list<LSPSymbolInformation> &symbols, bool tree, bool show_detail, QStandardItemModel *model, QStandardItem *parent, bool &details)
0357     {
0358         const QIcon *icon = nullptr;
0359         for (const auto &symbol : symbols) {
0360             switch (symbol.kind) {
0361             case LSPSymbolKind::File:
0362             case LSPSymbolKind::Module:
0363             case LSPSymbolKind::Namespace:
0364             case LSPSymbolKind::Package:
0365                 if (symbol.children.empty()) {
0366                     continue;
0367                 }
0368                 icon = &m_icon_pkg;
0369                 break;
0370             case LSPSymbolKind::Class:
0371             case LSPSymbolKind::Interface:
0372                 icon = &m_icon_class;
0373                 break;
0374             case LSPSymbolKind::Enum:
0375                 icon = &m_icon_typedef;
0376                 break;
0377             case LSPSymbolKind::Method:
0378             case LSPSymbolKind::Function:
0379             case LSPSymbolKind::Constructor:
0380                 icon = &m_icon_function;
0381                 break;
0382             // all others considered/assumed Variable
0383             case LSPSymbolKind::Variable:
0384             case LSPSymbolKind::Constant:
0385             case LSPSymbolKind::String:
0386             case LSPSymbolKind::Number:
0387             case LSPSymbolKind::Property:
0388             case LSPSymbolKind::Field:
0389             default:
0390                 // skip local variable
0391                 // property, field, etc unlikely in such case anyway
0392                 if (parent && parent->icon().cacheKey() == m_icon_function.cacheKey()) {
0393                     continue;
0394                 }
0395                 icon = &m_icon_var;
0396             }
0397 
0398             auto node = new QStandardItem();
0399             auto line = new QStandardItem();
0400             if (parent && tree) {
0401                 parent->appendRow({node, line});
0402             } else {
0403                 model->appendRow({node, line});
0404             }
0405 
0406             if (!symbol.detail.isEmpty()) {
0407                 details = true;
0408             }
0409             auto detail = show_detail && !symbol.detail.isEmpty() ? QStringLiteral(" [%1]").arg(symbol.detail) : QString();
0410             node->setText(symbol.name + detail);
0411             node->setIcon(*icon);
0412             node->setData(QVariant::fromValue<KTextEditor::Range>(symbol.range), SymbolViewRoles::SymbolRange);
0413             static const QChar prefix = QChar::fromLatin1('0');
0414             line->setText(QStringLiteral("%1").arg(symbol.range.start().line(), 7, 10, prefix));
0415             // recurse children
0416             makeNodes(symbol.children, tree, show_detail, model, node, details);
0417         }
0418     }
0419 
0420     void onDocumentSymbols(const std::list<LSPSymbolInformation> &outline)
0421     {
0422         onDocumentSymbolsOrProblem(outline, QString(), true);
0423     }
0424 
0425     void onDocumentSymbolsOrProblem(const std::list<LSPSymbolInformation> &outline, const QString &problem = QString(), bool cache = false)
0426     {
0427         if (!m_symbols) {
0428             return;
0429         }
0430 
0431         // construct new model for data
0432         auto newModel = std::make_shared<QStandardItemModel>();
0433 
0434         // if we have some problem, just report that, else construct model
0435         bool details = false;
0436         if (problem.isEmpty()) {
0437             makeNodes(outline, m_treeOn->isChecked(), m_detailsOn->isChecked(), newModel.get(), nullptr, details);
0438             if (cache) {
0439                 // last request has been placed at head of model list
0440                 Q_ASSERT(!m_models.isEmpty());
0441                 m_models[0].model = newModel;
0442             }
0443         } else {
0444             auto item = new QStandardItem(problem);
0445             item->setData(true, SymbolViewRoles::IsPlaceholder);
0446             newModel->appendRow(item);
0447         }
0448 
0449         // cache detail info with model
0450         newModel->invisibleRootItem()->setData(details);
0451 
0452         // fixup headers
0453         QStringList headers{i18n("Symbols")};
0454         newModel->setHorizontalHeaderLabels(headers);
0455 
0456         setModel(newModel);
0457     }
0458 
0459     void setModel(const std::shared_ptr<QStandardItemModel> &newModel)
0460     {
0461         Q_ASSERT(newModel);
0462 
0463         // update filter model, do this before the assignment below deletes the old model!
0464         m_filterModel.setSourceModel(newModel.get());
0465 
0466         // delete old outline if there, keep our new one alive
0467         m_outline = newModel;
0468 
0469         // fixup sorting
0470         if (m_sortOn->isChecked()) {
0471             m_symbols->setSortingEnabled(true);
0472             m_symbols->sortByColumn(0, Qt::AscendingOrder);
0473             m_symbols->header()->setSectionsClickable(true);
0474         } else {
0475             // most servers provide items in reasonable file/input order
0476             // however sadly not all, so let's sort by hidden line number column to make sure
0477             m_symbols->setSortingEnabled(true);
0478             m_symbols->sortByColumn(1, Qt::AscendingOrder);
0479             m_symbols->header()->setSectionsClickable(false);
0480         }
0481         // no need to show internal info
0482         m_symbols->setColumnHidden(1, true);
0483 
0484         // handle auto-expansion
0485         if (m_expandOn->isChecked()) {
0486             m_symbols->expandAll();
0487         }
0488 
0489         // recover detail info from model data
0490         bool details = newModel->invisibleRootItem()->data().toBool();
0491 
0492         // disable detail setting if no such info available
0493         // (as an indication there is nothing to show anyway)
0494         m_detailsOn->setEnabled(details);
0495 
0496         // current item tracking
0497         updateCurrentTreeItem();
0498 
0499         m_identityModel->setSourceModel(m_outline.get());
0500     }
0501 
0502     void refresh(bool clear, bool allow_cache = true, int retry = 0)
0503     {
0504         // cancel old request!
0505         m_handle.cancel();
0506 
0507         // check if we have some server for the current view => trigger request
0508         auto view = m_mainWindow->activeView();
0509         if (auto server = m_serverManager->findServer(view)) {
0510             // clear current model in any case
0511             // this avoids that we show stuff not matching the current view
0512             // but let's only do it if needed, e.g. when changing view
0513             // so as to avoid unhealthy flickering in other cases
0514             if (clear) {
0515                 onDocumentSymbolsOrProblem({}, QString(), false);
0516             }
0517 
0518             // check (valid) cache
0519             auto doc = view->document();
0520             auto revision = m_serverManager->revision(doc);
0521             auto it = m_models.begin();
0522             for (; it != m_models.end();) {
0523                 if (it->document == doc) {
0524                     break;
0525                 }
0526                 if (!it->document) {
0527                     it = m_models.erase(it);
0528                     continue;
0529                 }
0530                 ++it;
0531             }
0532             if (it != m_models.end()) {
0533                 // move to most recently used head
0534                 m_models.move(it - m_models.begin(), 0);
0535                 auto &model = m_models.front();
0536                 // re-use if possible
0537                 // reloaded document recycles revision number, so avoid stale cache
0538                 // (clear := view switch)
0539                 if (revision == model.revision && model.model && (clear || revision > 0) && allow_cache) {
0540                     setModel(model.model);
0541                     return;
0542                 }
0543                 it->revision = revision;
0544             } else {
0545                 m_models.insert(0, {doc, revision, nullptr});
0546                 if (m_models.size() > MAX_MODELS) {
0547                     m_models.pop_back();
0548                 }
0549             }
0550 
0551             // a cancelled request or modified content might result in error response,
0552             // so arrange to process it as an error rather than an empty result,
0553             // since the latter would (temporarily) clear the symbol outline
0554             // and lead to flicker until the next/final request has a proper result again
0555             auto oldRequestCnt = ++m_requestCnt;
0556             auto eh = [this, clear, retry, oldRequestCnt](const LSPResponseError &err) {
0557                 switch (err.code) {
0558                 case LSPErrorCode::ContentModified:
0559                 case LSPErrorCode::RequestCancelled:
0560                     break;
0561                 default:
0562                     // also try to avoid flicker here
0563                     // never mind the request if another one has already been launched
0564                     // but if this is the last request standing, go for retry
0565                     if (m_requestCnt == oldRequestCnt) {
0566                         if (retry < 4) {
0567                             // if we got here, cache was not used
0568                             refresh(clear, false, retry + 1);
0569                         } else {
0570                             // clear old/stale situation and show that the server has lost track
0571                             onDocumentSymbols({});
0572                         }
0573                     }
0574                     break;
0575                 }
0576             };
0577 
0578             m_handle = server->documentSymbols(doc->url(), this, utils::mem_fun(&self_type::onDocumentSymbols, this), eh);
0579 
0580             return;
0581         }
0582 
0583         // else: inform that no server is there
0584         onDocumentSymbolsOrProblem({}, i18n("No LSP server for this document."));
0585     }
0586 
0587     // returns (covering item, closest child of covering item after line)
0588     // if distance non-null, then (output) *distance:
0589     //  = irrelevant if first element of return value is non-null
0590     //  < 0 if line is after item range
0591     //  > 0 if line is before item range = distance from line to item range start
0592     std::pair<QStandardItem *, QStandardItem *> getCurrentItem(QStandardItem *item, int line, int *distance = nullptr)
0593     {
0594         // first traverse the child items to have deepest match!
0595         // only do this if our stuff is expanded
0596         QStandardItem *minItem = nullptr;
0597         if (item == m_outline->invisibleRootItem() || m_symbols->isExpanded(m_filterModel.mapFromSource(m_outline->indexFromItem(item)))) {
0598             int minDistance = std::numeric_limits<int>::max();
0599             for (int i = 0; i < item->rowCount(); i++) {
0600                 int dist = 0;
0601                 auto child = item->child(i);
0602                 auto citem = getCurrentItem(child, line, &dist);
0603                 if (citem.first) {
0604                     return citem;
0605                 } else if (dist > 0 && dist < minDistance) {
0606                     minDistance = dist;
0607                     minItem = child;
0608                 }
0609             }
0610         }
0611 
0612         // does the line match our item?
0613         auto range = item->data(SymbolViewRoles::SymbolRange).value<KTextEditor::Range>();
0614         if (range.overlapsLine(line)) {
0615             return {item, minItem};
0616         } else {
0617             if (distance) {
0618                 auto startline = range.start().line();
0619                 *distance = line < startline ? startline - line : -1;
0620             }
0621             return {nullptr, minItem};
0622         }
0623     }
0624 
0625     void updateCurrentTreeItem()
0626     {
0627         KTextEditor::View *editView = m_mainWindow->activeView();
0628         if (!editView || !m_symbols) {
0629             return;
0630         }
0631 
0632         /**
0633          * get item if any
0634          */
0635         auto items = getCurrentItem(m_outline->invisibleRootItem(), editView->cursorPositionVirtual().line());
0636         if (!items.first) {
0637             return;
0638         }
0639 
0640         /**
0641          * select it;
0642          * however, in a typical (e.g. class/namespace) case, the areas between (e.g. method) children
0643          * select the parent (class) as item.  To aid navigation in such case, the closest (following)
0644          * child is also considered (as the parent item may in fact be quite far away in the symbol tree).
0645          */
0646         QModelIndex coverIndex = m_filterModel.mapFromSource(m_outline->indexFromItem(items.first));
0647         QModelIndex closestIndex = m_filterModel.mapFromSource(m_outline->indexFromItem(items.second ? items.second : items.first));
0648         // select covering (parent) item
0649         m_symbols->selectionModel()->setCurrentIndex(coverIndex, QItemSelectionModel::Clear | QItemSelectionModel::Select);
0650         // add child to selection and move view there
0651         m_symbols->selectionModel()->setCurrentIndex(closestIndex, QItemSelectionModel::Select);
0652         m_symbols->scrollTo(closestIndex);
0653     }
0654 
0655     void goToSymbol(const QModelIndex &index)
0656     {
0657         KTextEditor::View *kv = m_mainWindow->activeView();
0658         const auto range = index.data(SymbolViewRoles::SymbolRange).value<KTextEditor::Range>();
0659         if (kv && range.isValid()) {
0660             kv->setCursorPosition(range.start());
0661         }
0662     }
0663 
0664     QAbstractItemModel *documentSymbolsModel() override
0665     {
0666         return m_identityModel;
0667     }
0668 
0669 private:
0670     void colorIcons(KTextEditor::Editor *e)
0671     {
0672         using KSyntaxHighlighting::Theme;
0673         auto theme = e->theme();
0674         auto varColor = QColor::fromRgba(theme.textColor(Theme::Variable));
0675         m_icon_var = Utils::colorIcon(m_icon_var, varColor);
0676 
0677         auto typeColor = QColor::fromRgba(theme.textColor(Theme::DataType));
0678         m_icon_class = Utils::colorIcon(m_icon_class, typeColor);
0679 
0680         auto enColor = QColor::fromRgba(theme.textColor(Theme::Constant));
0681         m_icon_typedef = Utils::colorIcon(m_icon_typedef, enColor);
0682 
0683         auto funcColor = QColor::fromRgba(theme.textColor(Theme::Function));
0684         m_icon_function = Utils::colorIcon(m_icon_function, funcColor);
0685 
0686         auto blockColor = QColor::fromRgba(theme.textColor(Theme::Import));
0687         m_icon_pkg = Utils::colorIcon(m_icon_pkg, blockColor);
0688     }
0689 
0690 private Q_SLOTS:
0691     /**
0692      * React on filter change
0693      * @param filterText new filter text
0694      */
0695     void filterTextChanged(const QString &filterText)
0696     {
0697         if (!m_symbols) {
0698             return;
0699         }
0700 
0701         /**
0702          * filter
0703          */
0704         m_filterModel.setFilterString(filterText);
0705 
0706         /**
0707          * expand
0708          */
0709         if (!filterText.isEmpty()) {
0710             QTimer::singleShot(100, m_symbols, &QTreeView::expandAll);
0711         }
0712     }
0713 };
0714 
0715 LSPClientSymbolView *LSPClientSymbolView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, std::shared_ptr<LSPClientServerManager> manager)
0716 {
0717     return new LSPClientSymbolViewImpl(plugin, mainWin, std::move(manager));
0718 }
0719 
0720 LSPClientSymbolView::~LSPClientSymbolView() = default;
0721 
0722 #include "lspclientsymbolview.moc"
0723 #include "moc_lspclientsymbolview.cpp"