diff --git a/addons/lspclient/CMakeLists.txt b/addons/lspclient/CMakeLists.txt --- a/addons/lspclient/CMakeLists.txt +++ b/addons/lspclient/CMakeLists.txt @@ -29,6 +29,8 @@ target_link_libraries(lspclientplugin KF5::TextEditor KF5::XmlGui + KF5::ItemViews + KF5::ItemModels ) install(TARGETS lspclientplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor) diff --git a/addons/lspclient/lspclientprotocol.h b/addons/lspclient/lspclientprotocol.h --- a/addons/lspclient/lspclientprotocol.h +++ b/addons/lspclient/lspclientprotocol.h @@ -160,17 +160,19 @@ struct LSPSymbolInformation { - LSPSymbolInformation(const QString & _name, LSPSymbolKind _kind, - LSPRange _range, const QString & _detail = QString()) + LSPSymbolInformation(const QString & _name = QString(), LSPSymbolKind _kind = LSPSymbolKind::File, + LSPRange _range = LSPRange::invalid(), const QString & _detail = QString()) : name(_name), detail(_detail), kind(_kind), range(_range) {} QString name; QString detail; LSPSymbolKind kind; LSPRange range; - QList children; }; +// allow QVariant to use our own type +Q_DECLARE_METATYPE(LSPSymbolInformation) + enum class LSPCompletionItemKind { Text = 1, diff --git a/addons/lspclient/lspclientserver.h b/addons/lspclient/lspclientserver.h --- a/addons/lspclient/lspclientserver.h +++ b/addons/lspclient/lspclientserver.h @@ -29,8 +29,10 @@ #include #include #include +#include #include +#include namespace utils { @@ -66,7 +68,7 @@ template using ReplyHandler = std::function; -using DocumentSymbolsReplyHandler = ReplyHandler>; +using DocumentSymbolsReplyHandler = ReplyHandler>; using DocumentDefinitionReplyHandler = ReplyHandler>; using DocumentHighlightReplyHandler = ReplyHandler>; using DocumentHoverReplyHandler = ReplyHandler; diff --git a/addons/lspclient/lspclientserver.cpp b/addons/lspclient/lspclientserver.cpp --- a/addons/lspclient/lspclientserver.cpp +++ b/addons/lspclient/lspclientserver.cpp @@ -29,8 +29,15 @@ #include #include #include +#include #include #include +#include +#include + +#include + +#include static const QString MEMBER_ID = QStringLiteral("id"); static const QString MEMBER_METHOD = QStringLiteral("method"); @@ -697,7 +704,66 @@ return ret; } -static QList +/** + * QStandardItem that creates icon on demand + */ +class StandardItemWithIcon : public QStandardItem +{ +public: + /** + * construct new item with given icon + * @param iconName icon name, if any + */ + StandardItemWithIcon(const QString &iconName) + : m_iconName(iconName) + { + } + + /** + * Overwritten data methode for on-demand icon creation and co. + * @param role role to get data for + * @return data for role + */ + QVariant data(int role = Qt::UserRole + 1) const override + { + if (role == Qt::DecorationRole) { + /** + * this should only happen in main thread + * the background thread should only construct this elements and fill data + * but never query gui stuff! + */ + Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); + + // no icon name => just no icon + if (m_iconName.isEmpty()) + return QIcon(); + + // return cached icon, if already there + if (m_icon) { + return *m_icon; + } + + // else: init icon cache + m_icon.reset(new QIcon(QIcon::fromTheme(m_iconName))); + return *m_icon; + } + + return QStandardItem::data(role); + } + +private: + /** + * icon name + */ + const QString m_iconName; + + /** + * cached icon + */ + mutable std::unique_ptr m_icon; +}; + +static std::shared_ptr parseDocumentSymbols(const QJsonValue & result) { // the reply could be old SymbolInformation[] or new (hierarchical) DocumentSymbol[] @@ -708,36 +774,117 @@ // * if a name is defined/declared several times and then used as a parent, // then it is the last instance that is used as a parent - QList ret; - QMap index; + auto newModel = std::make_shared(); + + QMap index; + + std::function parseSymbol = + [&] (const QJsonObject & symbol, QStandardItem *parent) { + // try to parse location info, skip stuff without locations + const auto& location = symbol.value(MEMBER_LOCATION).toObject(); + const auto& mrange = symbol.contains(MEMBER_RANGE) ? + symbol.value(MEMBER_RANGE) : location.value(MEMBER_RANGE); + const auto range = parseRange(mrange.toObject()); + if (!isPositionValid(range.start()) || !isPositionValid(range.end())) { + return; + } - std::function parseSymbol = - [&] (const QJsonObject & symbol, LSPSymbolInformation *parent) { // if flat list, try to find parent by name if (!parent) { auto container = symbol.value(QStringLiteral("containerName")).toString(); parent = index.value(container, nullptr); } - auto list = parent ? &parent->children : &ret; - auto name = symbol.value(QStringLiteral("name")).toString(); - auto kind = (LSPSymbolKind) symbol.value(MEMBER_KIND).toInt(); - const auto& location = symbol.value(MEMBER_LOCATION).toObject(); - const auto& mrange = symbol.contains(MEMBER_RANGE) ? - symbol.value(MEMBER_RANGE) : location.value(MEMBER_RANGE); - auto range = parseRange(mrange.toObject()); - if (isPositionValid(range.start()) && isPositionValid(range.end())) { - list->push_back({name, kind, range}); - index[name] = &list->back(); - // proceed recursively - for (const auto &child : symbol.value(QStringLiteral("children")).toArray()) - parseSymbol(child.toObject(), &list->back()); + + // determine type specific infos + const auto kind = (LSPSymbolKind) symbol.value(MEMBER_KIND).toInt(); + bool deleteIfNoChildren = false; + QString iconName; + switch (kind) { + case LSPSymbolKind::File: + case LSPSymbolKind::Module: + case LSPSymbolKind::Namespace: + case LSPSymbolKind::Package: + deleteIfNoChildren = true; + iconName = QStringLiteral("code-block"); + break; + case LSPSymbolKind::Class: + case LSPSymbolKind::Interface: + iconName = QStringLiteral("code-class"); + break; + case LSPSymbolKind::Enum: + iconName = QStringLiteral("code-typedef"); + break; + case LSPSymbolKind::Method: + case LSPSymbolKind::Function: + case LSPSymbolKind::Constructor: + iconName = QStringLiteral("code-function"); + break; + // all others considered/assumed Variable + case LSPSymbolKind::Variable: + case LSPSymbolKind::Constant: + case LSPSymbolKind::String: + case LSPSymbolKind::Number: + case LSPSymbolKind::Property: + case LSPSymbolKind::Field: + default: + // skip local variable + // property, field, etc unlikely in such case anyway + if (parent) { + const auto parentKind = parent->data(Qt::UserRole).value().kind; + if (parentKind == LSPSymbolKind::Method || parentKind == LSPSymbolKind::Function || parentKind == LSPSymbolKind::Constructor) { + return; + } + } + iconName = QStringLiteral("code-variable"); + } + + // new standard item will our structured data as user-data + auto newItem = new StandardItemWithIcon(iconName); + const auto name = symbol.value(QStringLiteral("name")).toString(); + const auto details = symbol.value(QStringLiteral("details")).toString(); + newItem->setData(QVariant::fromValue(LSPSymbolInformation{name, kind, range, details}), Qt::UserRole); + + // setup enough info that a normal view can display our items + newItem->setText(name); + + // proceed recursively + for (const auto &child : symbol.value(QStringLiteral("children")).toArray()) { + parseSymbol(child.toObject(), newItem); } - }; + // shall we remove this if it had no children? + if (deleteIfNoChildren && newItem->rowCount() == 0) { + delete newItem; + return; + } + + // add extra column for details if set + QList items{newItem}; + if (!details.isEmpty()) { + items << new QStandardItem(details); + } + + // child element or toplevel thing in model + if (parent) + parent->appendRow(items); + else + newModel->appendRow(items); + + // map name => item for parent discovery + index[name] = newItem; + }; for (const auto& info : result.toArray()) { parseSymbol(info.toObject(), nullptr); } - return ret; + + // adjust headers + QStringList headers{i18n("Symbol")}; + if (newModel->columnCount() == 2) { + headers << i18n("Details"); + } + newModel->setHorizontalHeaderLabels(headers); + + return newModel; } static QList diff --git a/addons/lspclient/lspclientsymbolview.cpp b/addons/lspclient/lspclientsymbolview.cpp --- a/addons/lspclient/lspclientsymbolview.cpp +++ b/addons/lspclient/lspclientsymbolview.cpp @@ -20,18 +20,21 @@ #include "lspclientsymbolview.h" +#include #include +#include #include #include #include #include -#include +#include #include #include #include #include +#include class LSPClientViewTrackerImpl : public LSPClientViewTracker { @@ -123,14 +126,9 @@ QSharedPointer m_serverManager; QScopedPointer m_toolview; // parent ownership - QPointer m_symbols; + QPointer m_symbols; + QPointer m_filter; QScopedPointer m_popup; - // icons used in tree representation - QIcon m_icon_pkg; - QIcon m_icon_class; - QIcon m_icon_typedef; - QIcon m_icon_function; - QIcon m_icon_var; // initialized/updated from plugin settings // managed by context menu later on // parent ownership @@ -142,43 +140,49 @@ QScopedPointer m_viewTracker; // outstanding request LSPClientServer::RequestHandle m_handle; + // last outline we got + std::shared_ptr m_outline; + // filter model, setup once + KRecursiveFilterProxyModel m_filterModel; public: LSPClientSymbolViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer manager) - : m_plugin(plugin), m_mainWindow(mainWin), m_serverManager(manager) + : m_plugin(plugin), m_mainWindow(mainWin), m_serverManager(manager), m_outline(new QStandardItemModel()) { - m_icon_pkg = QIcon::fromTheme(QStringLiteral("code-block")); - m_icon_class = QIcon::fromTheme(QStringLiteral("code-class")); - m_icon_typedef = QIcon::fromTheme(QStringLiteral("code-typedef")); - m_icon_function = QIcon::fromTheme(QStringLiteral("code-function")); - m_icon_var = QIcon::fromTheme(QStringLiteral("code-variable")); - m_toolview.reset(m_mainWindow->createToolView(plugin, QStringLiteral("lspclient_symbol_outline"), KTextEditor::MainWindow::Right, QIcon::fromTheme(QStringLiteral("code-context")), i18n("LSP Client Symbol Outline"))); - m_symbols = new QTreeWidget(m_toolview.get()); + m_symbols = new QTreeView(m_toolview.get()); m_symbols->setFocusPolicy(Qt::NoFocus); m_symbols->setLayoutDirection(Qt::LeftToRight); m_toolview->layout()->setContentsMargins(0, 0, 0, 0); m_toolview->layout()->addWidget(m_symbols); m_toolview->layout()->setSpacing(0); - QStringList titles; - titles << i18nc("@title:column", "Symbols") << i18nc("@title:column", "Position"); - m_symbols->setColumnCount(3); - m_symbols->setHeaderLabels(titles); - m_symbols->setColumnHidden(1, true); - m_symbols->setColumnHidden(2, true); + // setup filter line edit + m_filter = new KLineEdit(m_toolview.get()); + m_toolview->layout()->addWidget(m_filter); + m_filter->setPlaceholderText(i18n("Filter...")); + m_filter->setClearButtonEnabled(true); + connect(m_filter, &KLineEdit::textChanged, this, &self_type::filterTextChanged); + m_symbols->setContextMenuPolicy(Qt::CustomContextMenu); m_symbols->setIndentation(10); - connect(m_symbols, &QTreeWidget::itemClicked, this, &self_type::goToSymbol); - connect(m_symbols, &QTreeWidget::customContextMenuRequested, this, &self_type::showContextMenu); - connect(m_symbols, &QTreeWidget::itemExpanded, this, &self_type::updateCurrentTreeItem); - connect(m_symbols, &QTreeWidget::itemCollapsed, this, &self_type::updateCurrentTreeItem); + // init filter model once, later we only swap the source model! + QItemSelectionModel *m = m_symbols->selectionModel(); + m_filterModel.setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filterModel.setSortCaseSensitivity(Qt::CaseInsensitive); + m_filterModel.setSourceModel(m_outline.get()); + m_symbols->setModel(&m_filterModel); + delete m; + + connect(m_symbols, &QTreeView::customContextMenuRequested, this, &self_type::showContextMenu); + connect(m_symbols, &QTreeView::activated, this, &self_type::goToSymbol); + connect(m_symbols, &QTreeView::clicked, this, &self_type::goToSymbol); // context menu m_popup.reset(new QMenu(m_symbols)); @@ -191,8 +195,8 @@ m_detailsOn = m_popup->addAction(i18n("Show Details"), this, &self_type::displayOptionChanged); m_detailsOn->setCheckable(true); m_popup->addSeparator(); - m_popup->addAction(i18n("Expand All"), this, &self_type::expandAll); - m_popup->addAction(i18n("Collapse All"), this, &self_type::collapseAll); + m_popup->addAction(i18n("Expand All"), m_symbols.data(), &QTreeView::expandAll); + m_popup->addAction(i18n("Collapse All"), m_symbols.data(), &QTreeView::collapseAll); // sync with plugin settings if updated connect(m_plugin, &LSPClientPlugin::update, this, &self_type::configUpdated); @@ -202,7 +206,7 @@ connect(m_viewTracker.get(), &LSPClientViewTracker::newState, this, &self_type::onViewState); connect(m_serverManager.get(), &LSPClientServerManager::serverChanged, this, &self_type::refresh); - // initial trigger + // initial trigger of symbols view update configUpdated(); } @@ -239,119 +243,57 @@ } } - void makeNodes(const QList & symbols, bool tree, - bool show_detail, QTreeWidget * widget, QTreeWidgetItem * parent, - int * details) - { - QIcon *icon = nullptr; - - for (const auto& symbol: symbols) { - switch (symbol.kind) { - case LSPSymbolKind::File: - case LSPSymbolKind::Module: - case LSPSymbolKind::Namespace: - case LSPSymbolKind::Package: - if (symbol.children.count() == 0) - continue; - icon = &m_icon_pkg; - break; - case LSPSymbolKind::Class: - case LSPSymbolKind::Interface: - icon = &m_icon_class; - break; - case LSPSymbolKind::Enum: - icon = &m_icon_typedef; - break; - case LSPSymbolKind::Method: - case LSPSymbolKind::Function: - case LSPSymbolKind::Constructor: - icon = &m_icon_function; - break; - // all others considered/assumed Variable - case LSPSymbolKind::Variable: - case LSPSymbolKind::Constant: - case LSPSymbolKind::String: - case LSPSymbolKind::Number: - case LSPSymbolKind::Property: - case LSPSymbolKind::Field: - default: - // skip local variable - // property, field, etc unlikely in such case anyway - if (parent && parent->icon(0).cacheKey() == m_icon_function.cacheKey()) - continue; - icon = &m_icon_var; - } - auto node = parent && tree ? - new QTreeWidgetItem(parent) : new QTreeWidgetItem(widget); - if (!symbol.detail.isEmpty() && details) - ++details; - auto detail = show_detail ? symbol.detail : QStringLiteral(""); - node->setText(0, symbol.name + detail); - node->setIcon(0, *icon); - node->setText(1, QString::number(symbol.range.start().line(), 10)); - node->setText(2, QString::number(symbol.range.end().line(), 10)); - // recurse children - makeNodes(symbol.children, tree, show_detail, widget, node, details); - } - } - - void onDocumentSymbols(const QList & outline) + void onDocumentSymbols(std::shared_ptr outline) { if (!m_symbols) return; - // populate with sort disabled - Qt::SortOrder sortOrder = m_symbols->header()->sortIndicatorOrder(); - m_symbols->clear(); - m_symbols->setSortingEnabled(false); - int details = 0; - - makeNodes(outline, m_treeOn->isChecked(), m_detailsOn->isChecked(), - m_symbols, nullptr, &details); - if (m_symbols->topLevelItemCount() == 0) { - QTreeWidgetItem *node = new QTreeWidgetItem(m_symbols); - node->setText(0, i18n("No outline items")); - } - if (m_expandOn->isChecked()) - expandAll(); - // disable detail setting if no such info available - // (as an indication there is nothing to show anyway) - if (!details) - m_detailsOn->setEnabled(false); - if (m_sortOn->isChecked()) { - m_symbols->setSortingEnabled(true); - m_symbols->sortItems(0, sortOrder); + // update filter model, do this before the assignment below deletes the old model! + m_filterModel.setSourceModel(outline.get()); + + // delete old outline if there, keep our new one alive + m_outline = outline; + + m_symbols->setSortingEnabled(m_sortOn->isChecked()); + + // hide detail column if not wanted + m_symbols->setColumnHidden(1, !m_detailsOn->isChecked()); + + // handle auto-expansion + if (m_expandOn->isChecked()) { + m_symbols->expandAll(); } + // current item tracking updateCurrentTreeItem(); } void refresh() { + // clear current model in any case + // this avoids that we show stuff not matching the current view + onDocumentSymbols(std::make_shared()); + m_handle.cancel(); auto view = m_mainWindow->activeView(); auto server = m_serverManager->findServer(view); if (server) { server->documentSymbols(view->document()->url(), this, utils::mem_fun(&self_type::onDocumentSymbols, this)); - } else if (m_symbols) { - m_symbols->clear(); - QTreeWidgetItem *node = new QTreeWidgetItem(m_symbols); - node->setText(0, i18n("No server available")); } } - QTreeWidgetItem* getCurrentItem(QTreeWidgetItem * item, int line) + QStandardItem* getCurrentItem(QStandardItem * item, int line) { - for (int i = 0; i < item->childCount(); i++) { + for (int i = 0; i < item->rowCount(); i++) { auto citem = getCurrentItem(item->child(i), line); if (citem) return citem; } - int lstart = item->data(1, Qt::DisplayRole).toInt(); - int lend = item->data(2, Qt::DisplayRole).toInt(); - if (lstart <= line && line <= lend) + // get meta data + + if (item->data(Qt::UserRole).value().range.containsLine(line)) return item; return nullptr; @@ -364,56 +306,53 @@ return; } - int currLine = editView->cursorPositionVirtual().line(); - auto item = getCurrentItem(m_symbols->invisibleRootItem(), currLine); - - // go up until a non-expanded item is found - // (the others were collapsed for some reason ...) - - while (item) { - auto parent = item->parent(); - if (parent && !parent->isExpanded()) { - item = parent; - } else { - break; - } + /** + * get item if any + */ + QStandardItem *item = getCurrentItem(m_outline->invisibleRootItem(), editView->cursorPositionVirtual().line()); + if (!item) { + return; } - m_symbols->blockSignals(true); - m_symbols->setCurrentItem(item); - m_symbols->blockSignals(false); + /** + * select it + */ + QModelIndex index = m_filterModel.mapFromSource(m_outline->indexFromItem(item)); + m_symbols->scrollTo(index, QAbstractItemView::EnsureVisible); + m_symbols->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Clear | QItemSelectionModel::Select); } - void expandAll() + void goToSymbol(const QModelIndex &index) { - if (!m_symbols) - return; - - QTreeWidgetItemIterator it(m_symbols, QTreeWidgetItemIterator::HasChildren); - while (*it) { - m_symbols->expandItem(*it); - ++it; - } + KTextEditor::View *kv = m_mainWindow->activeView(); + const auto range = index.data(Qt::UserRole).value().range; + if (kv && range.isValid()) { + kv->setCursorPosition(range.start()); + } } - void collapseAll() +private Q_SLOTS: + /** + * React on filter change + * @param filterText new filter text + */ + void filterTextChanged(const QString &filterText) { - if (!m_symbols) + if (!m_symbols) { return; + } - QTreeWidgetItemIterator it(m_symbols, QTreeWidgetItemIterator::HasChildren); - while (*it) { - m_symbols->collapseItem(*it); - ++it; - } - } + /** + * filter + */ + m_filterModel.setFilterFixedString(filterText); - void goToSymbol(QTreeWidgetItem *it) - { - KTextEditor::View *kv = m_mainWindow->activeView(); - if (kv && it && !it->text(1).isEmpty()) { - kv->setCursorPosition(KTextEditor::Cursor(it->text(1).toInt(nullptr, 10), 0)); - } + /** + * expand + */ + if (!filterText.isEmpty()) { + QTimer::singleShot(100, m_symbols, &QTreeView::expandAll); + } } }; diff --git a/addons/lspclient/tests/lsptestapp.cpp b/addons/lspclient/tests/lsptestapp.cpp --- a/addons/lspclient/tests/lsptestapp.cpp +++ b/addons/lspclient/tests/lsptestapp.cpp @@ -61,8 +61,8 @@ QString content = in.readAll(); lsp.didOpen(document, 0, content); - auto ds_h = [&q] (const QList & syms) { - std::cout << "symbol count: " << syms.length() << std::endl; + auto ds_h = [&q] (std::shared_ptr syms) { + std::cout << "symbol count: " << (syms ? syms->rowCount() : 0) << std::endl; q.quit(); }; lsp.documentSymbols(document, &app, ds_h);