diff --git a/addons/lspclient/lspclientsymbolview.cpp b/addons/lspclient/lspclientsymbolview.cpp index 12842754f..31850ba2c 100644 --- a/addons/lspclient/lspclientsymbolview.cpp +++ b/addons/lspclient/lspclientsymbolview.cpp @@ -1,480 +1,487 @@ /* 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 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; // last outline model we constructed std::unique_ptr m_outline; // filter model, setup once KRecursiveFilterProxyModel 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(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.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); // 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); 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_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.get(), &LSPClientViewTracker::newState, this, &self_type::onViewState); - connect(m_serverManager.get(), &LSPClientServerManager::serverChanged, this, &self_type::refresh); + connect(m_serverManager.get(), &LSPClientServerManager::serverChanged, + this, [this] () { refresh(false); }); // initial trigger of symbols view update configUpdated(); } void displayOptionChanged() { m_expandOn->setEnabled(m_treeOn->isChecked()); - refresh(); + 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(); + 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()); } void onDocumentSymbolsOrProblem(const QList &outline, const QString &problem = QString()) { if (!m_symbols) return; // construct new model for data auto newModel = new QStandardItemModel(); // 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, nullptr, details); } else { newModel->appendRow(new QStandardItem(problem)); } // fixup headers QStringList headers{i18n("Symbols")}; newModel->setHorizontalHeaderLabels(headers); // update filter model, do this before the assignment below deletes the old model! m_filterModel.setSourceModel(newModel); // delete old outline if there, keep our new one alive m_outline.reset(newModel); // fixup sorting if (m_sortOn->isChecked()) { m_symbols->setSortingEnabled(true); m_symbols->sortByColumn(0); } else { m_symbols->sortByColumn(-1); } // handle auto-expansion if (m_expandOn->isChecked()) { m_symbols->expandAll(); } // 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() + 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 - onDocumentSymbols(QList()); + // but let's only do it if needed, e.g. when changing view + // so as to avoid unhealthy flickering in other cases + if (clear) { + onDocumentSymbols(QList()); + } 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, manager); } #include "lspclientsymbolview.moc"