diff --git a/addons/lspclient/lspclientsymbolview.cpp b/addons/lspclient/lspclientsymbolview.cpp index ba9874404..96f45e59b 100644 --- a/addons/lspclient/lspclientsymbolview.cpp +++ b/addons/lspclient/lspclientsymbolview.cpp @@ -1,539 +1,539 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts Copyright (C) 2019 Christoph Cullmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "lspclientsymbolview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include class LSPClientViewTrackerImpl : public LSPClientViewTracker { Q_OBJECT typedef LSPClientViewTrackerImpl self_type; LSPClientPlugin *m_plugin; KTextEditor::MainWindow *m_mainWindow; // timers to delay some todo's QTimer m_changeTimer; int m_change; QTimer m_motionTimer; int m_motion; int m_oldCursorLine = -1; public: LSPClientViewTrackerImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, int change_ms, int motion_ms) : m_plugin(plugin) , m_mainWindow(mainWin) , m_change(change_ms) , m_motion(motion_ms) { // get updated m_changeTimer.setSingleShot(true); auto ch = [this]() { emit newState(m_mainWindow->activeView(), TextChanged); }; connect(&m_changeTimer, &QTimer::timeout, this, ch); m_motionTimer.setSingleShot(true); auto mh = [this]() { emit newState(m_mainWindow->activeView(), LineChanged); }; connect(&m_motionTimer, &QTimer::timeout, this, mh); // track views connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::viewChanged); } void viewChanged(KTextEditor::View *view) { m_motionTimer.stop(); m_changeTimer.stop(); if (view) { if (m_motion) { connect(view, &KTextEditor::View::cursorPositionChanged, this, &self_type::cursorPositionChanged, Qt::UniqueConnection); } if (m_change > 0 && view->document()) { connect(view->document(), &KTextEditor::Document::textChanged, this, &self_type::textChanged, Qt::UniqueConnection); } emit newState(view, ViewChanged); m_oldCursorLine = view->cursorPosition().line(); } } void textChanged() { m_motionTimer.stop(); m_changeTimer.start(m_change); } void cursorPositionChanged(KTextEditor::View *view, const KTextEditor::Cursor &newPosition) { if (m_changeTimer.isActive()) { // change trumps motion return; } if (view && newPosition.line() != m_oldCursorLine) { m_oldCursorLine = newPosition.line(); m_motionTimer.start(m_motion); } } }; LSPClientViewTracker *LSPClientViewTracker::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, int change_ms, int motion_ms) { return new LSPClientViewTrackerImpl(plugin, mainWin, change_ms, motion_ms); } /* * Instantiates and manages the symbol outline toolview. */ class LSPClientSymbolViewImpl : public QObject, public LSPClientSymbolView { Q_OBJECT typedef LSPClientSymbolViewImpl self_type; LSPClientPlugin *m_plugin; KTextEditor::MainWindow *m_mainWindow; QSharedPointer m_serverManager; QScopedPointer m_toolview; // parent ownership QPointer m_symbols; QPointer m_filter; QScopedPointer m_popup; // initialized/updated from plugin settings // managed by context menu later on // parent ownership QAction *m_detailsOn; QAction *m_expandOn; QAction *m_treeOn; QAction *m_sortOn; // view tracking QScopedPointer m_viewTracker; // outstanding request LSPClientServer::RequestHandle m_handle; // cached outline models struct ModelData { KTextEditor::Document *document; qint64 revision; std::shared_ptr model; }; QList m_models; // max number to cache static constexpr int MAX_MODELS = 10; // last outline model we constructed std::shared_ptr m_outline; // filter model, setup once QSortFilterProxyModel m_filterModel; // cached icons for model const QIcon m_icon_pkg = QIcon::fromTheme(QStringLiteral("code-block")); const QIcon m_icon_class = QIcon::fromTheme(QStringLiteral("code-class")); const QIcon m_icon_typedef = QIcon::fromTheme(QStringLiteral("code-typedef")); const QIcon m_icon_function = QIcon::fromTheme(QStringLiteral("code-function")); const QIcon m_icon_var = QIcon::fromTheme(QStringLiteral("code-variable")); public: LSPClientSymbolViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer manager) : m_plugin(plugin) , m_mainWindow(mainWin) , m_serverManager(std::move(manager)) , m_outline(new QStandardItemModel()) { 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 QTreeView(m_toolview.data()); 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); // setup filter line edit m_filter = new KLineEdit(m_toolview.data()); 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); m_symbols->setEditTriggers(QAbstractItemView::NoEditTriggers); m_symbols->setAllColumnsShowFocus(true); // 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_filterModel.setRecursiveFilteringEnabled(true); 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)); m_treeOn = m_popup->addAction(i18n("Tree Mode"), this, &self_type::displayOptionChanged); m_treeOn->setCheckable(true); m_expandOn = m_popup->addAction(i18n("Automatically Expand Tree"), this, &self_type::displayOptionChanged); m_expandOn->setCheckable(true); m_sortOn = m_popup->addAction(i18n("Sort Alphabetically"), this, &self_type::displayOptionChanged); m_sortOn->setCheckable(true); 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"), 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); // get updated m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 500, 100)); connect(m_viewTracker.data(), &LSPClientViewTracker::newState, this, &self_type::onViewState); connect(m_serverManager.data(), &LSPClientServerManager::serverChanged, this, [this]() { refresh(false); }); // limit cached models; will not go beyond capacity set here m_models.reserve(MAX_MODELS + 1); // initial trigger of symbols view update configUpdated(); } void displayOptionChanged() { m_expandOn->setEnabled(m_treeOn->isChecked()); refresh(false); } void configUpdated() { m_treeOn->setChecked(m_plugin->m_symbolTree); m_detailsOn->setChecked(m_plugin->m_symbolDetails); m_expandOn->setChecked(m_plugin->m_symbolExpand); m_sortOn->setChecked(m_plugin->m_symbolSort); displayOptionChanged(); } void showContextMenu(const QPoint &) { m_popup->popup(QCursor::pos(), m_treeOn); } void onViewState(KTextEditor::View *, LSPClientViewTracker::State newState) { switch (newState) { case LSPClientViewTracker::ViewChanged: refresh(true); break; case LSPClientViewTracker::TextChanged: refresh(false); break; case LSPClientViewTracker::LineChanged: updateCurrentTreeItem(); break; } } void makeNodes(const QList &symbols, bool tree, bool show_detail, QStandardItemModel *model, QStandardItem *parent, bool &details) { const 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().cacheKey() == m_icon_function.cacheKey()) continue; icon = &m_icon_var; } auto node = new QStandardItem(); if (parent && tree) parent->appendRow(node); else model->appendRow(node); if (!symbol.detail.isEmpty()) details = true; auto detail = show_detail ? symbol.detail : QString(); node->setText(symbol.name + detail); node->setIcon(*icon); node->setData(QVariant::fromValue(symbol.range), Qt::UserRole); // recurse children makeNodes(symbol.children, tree, show_detail, model, node, details); } } void onDocumentSymbols(const QList &outline) { onDocumentSymbolsOrProblem(outline, QString(), true); } void onDocumentSymbolsOrProblem(const QList &outline, const QString &problem = QString(), bool cache = false) { if (!m_symbols) return; // construct new model for data auto newModel = std::make_shared(); // if we have some problem, just report that, else construct model bool details = false; if (problem.isEmpty()) { makeNodes(outline, m_treeOn->isChecked(), m_detailsOn->isChecked(), newModel.get(), nullptr, details); if (cache) { // last request has been placed at head of model list Q_ASSERT(!m_models.isEmpty()); m_models[0].model = newModel; } } else { newModel->appendRow(new QStandardItem(problem)); } // cache detail info with model newModel->invisibleRootItem()->setData(details); // fixup headers QStringList headers {i18n("Symbols")}; newModel->setHorizontalHeaderLabels(headers); setModel(newModel); } void setModel(const std::shared_ptr &newModel) { Q_ASSERT(newModel); // update filter model, do this before the assignment below deletes the old model! m_filterModel.setSourceModel(newModel.get()); // delete old outline if there, keep our new one alive m_outline = newModel; // fixup sorting if (m_sortOn->isChecked()) { m_symbols->setSortingEnabled(true); m_symbols->sortByColumn(0, Qt::AscendingOrder); } else { m_symbols->sortByColumn(-1, Qt::AscendingOrder); } // handle auto-expansion if (m_expandOn->isChecked()) { m_symbols->expandAll(); } // recover detail info from model data bool details = newModel->invisibleRootItem()->data().toBool(); // disable detail setting if no such info available // (as an indication there is nothing to show anyway) m_detailsOn->setEnabled(details); // hide detail column if not needed/wanted bool showDetails = m_detailsOn->isChecked() && details; m_symbols->setColumnHidden(1, !showDetails); // current item tracking updateCurrentTreeItem(); } void refresh(bool clear) { // cancel old request! m_handle.cancel(); // check if we have some server for the current view => trigger request auto view = m_mainWindow->activeView(); if (auto server = m_serverManager->findServer(view)) { // clear current model in any case // this avoids that we show stuff not matching the current view // but let's only do it if needed, e.g. when changing view // so as to avoid unhealthy flickering in other cases if (clear) { onDocumentSymbolsOrProblem(QList(), QString(), false); } // check (valid) cache auto doc = view->document(); auto revision = m_serverManager->revision(doc); auto it = m_models.begin(); for (; it != m_models.end(); ++it) { if (it->document == doc) { break; } } if (it != m_models.end()) { // move to most recently used head m_models.move(it - m_models.begin(), 0); auto &model = m_models.front(); // re-use if possible // reloaded document recycles revision number, so avoid stale cache // (clear := view switch) if (revision == model.revision && model.model && (clear || revision > 0)) { setModel(model.model); return; } it->revision = revision; } else { m_models.insert(0, {doc, revision, nullptr}); if (m_models.size() > MAX_MODELS) { m_models.pop_back(); } } - server->documentSymbols(view->document()->url(), this, utils::mem_fun(&self_type::onDocumentSymbols, this)); + m_handle = server->documentSymbols(view->document()->url(), this, utils::mem_fun(&self_type::onDocumentSymbols, this)); return; } // else: inform that no server is there onDocumentSymbolsOrProblem(QList(), i18n("No LSP server for this document.")); } QStandardItem *getCurrentItem(QStandardItem *item, int line) { // first traverse the child items to have deepest match! // only do this if our stuff is expanded if (item == m_outline->invisibleRootItem() || m_symbols->isExpanded(m_filterModel.mapFromSource(m_outline->indexFromItem(item)))) { for (int i = 0; i < item->rowCount(); i++) { if (auto citem = getCurrentItem(item->child(i), line)) { return citem; } } } // does the line match our item? return item->data(Qt::UserRole).value().overlapsLine(line) ? item : nullptr; } void updateCurrentTreeItem() { KTextEditor::View *editView = m_mainWindow->activeView(); if (!editView || !m_symbols) { return; } /** * get item if any */ QStandardItem *item = getCurrentItem(m_outline->invisibleRootItem(), editView->cursorPositionVirtual().line()); if (!item) { return; } /** * select it */ QModelIndex index = m_filterModel.mapFromSource(m_outline->indexFromItem(item)); m_symbols->scrollTo(index); m_symbols->selectionModel()->setCurrentIndex(index, QItemSelectionModel::Clear | QItemSelectionModel::Select); } void goToSymbol(const QModelIndex &index) { KTextEditor::View *kv = m_mainWindow->activeView(); const auto range = index.data(Qt::UserRole).value(); if (kv && range.isValid()) { kv->setCursorPosition(range.start()); } } private Q_SLOTS: /** * React on filter change * @param filterText new filter text */ void filterTextChanged(const QString &filterText) { if (!m_symbols) { return; } /** * filter */ m_filterModel.setFilterFixedString(filterText); /** * expand */ if (!filterText.isEmpty()) { QTimer::singleShot(100, m_symbols, &QTreeView::expandAll); } } }; QObject *LSPClientSymbolView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, QSharedPointer manager) { return new LSPClientSymbolViewImpl(plugin, mainWin, std::move(manager)); } #include "lspclientsymbolview.moc"