/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "languageclientoutline.h" #include "languageclientmanager.h" #include "languageclientutils.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace LanguageServerProtocol; namespace LanguageClient { class LanguageClientOutlineItem : public Utils::TypedTreeItem { public: LanguageClientOutlineItem() = default; LanguageClientOutlineItem(const SymbolInformation &info) : m_name(info.name()) , m_range(info.location().range()) , m_type(info.kind()) { } LanguageClientOutlineItem(const DocumentSymbol &info, const SymbolStringifier &stringifier) : m_name(info.name()) , m_detail(info.detail().value_or(QString())) , m_range(info.range()) , m_symbolStringifier(stringifier) , m_type(info.kind()) { for (const DocumentSymbol &child : info.children().value_or(QList())) appendChild(new LanguageClientOutlineItem(child, stringifier)); } // TreeItem interface QVariant data(int column, int role) const override { switch (role) { case Qt::DecorationRole: return symbolIcon(m_type); case Qt::DisplayRole: return m_symbolStringifier ? m_symbolStringifier(static_cast(m_type), m_name, m_detail) : m_name; default: return Utils::TreeItem::data(column, role); } } Range range() const { return m_range; } Position pos() const { return m_range.start(); } bool contains(const Position &pos) const { return m_range.contains(pos); } private: QString m_name; QString m_detail; Range m_range; SymbolStringifier m_symbolStringifier; int m_type = -1; }; class LanguageClientOutlineModel : public Utils::TreeModel { public: using Utils::TreeModel::TreeModel; void setInfo(const QList &info) { clear(); for (const SymbolInformation &symbol : info) rootItem()->appendChild(new LanguageClientOutlineItem(symbol)); } void setInfo(const QList &info) { clear(); for (const DocumentSymbol &symbol : info) rootItem()->appendChild(new LanguageClientOutlineItem(symbol, m_symbolStringifier)); } void setSymbolStringifier(const SymbolStringifier &stringifier) { m_symbolStringifier = stringifier; } private: SymbolStringifier m_symbolStringifier; }; class LanguageClientOutlineWidget : public TextEditor::IOutlineWidget { public: LanguageClientOutlineWidget(Client *client, TextEditor::BaseTextEditor *editor); // IOutlineWidget interface public: QList filterMenuActions() const override; void setCursorSynchronization(bool syncWithCursor) override; void setSorted(bool) override; bool isSorted() const override; void restoreSettings(const QVariantMap &map) override; QVariantMap settings() const override; private: void handleResponse(const DocumentUri &uri, const DocumentSymbolsResult &response); void updateTextCursor(const QModelIndex &proxyIndex); void updateSelectionInTree(const QTextCursor ¤tCursor); void onItemActivated(const QModelIndex &index); QPointer m_client; QPointer m_editor; LanguageClientOutlineModel m_model; QSortFilterProxyModel m_proxyModel; Utils::TreeView m_view; DocumentUri m_uri; bool m_sync = false; bool m_sorted = false; }; LanguageClientOutlineWidget::LanguageClientOutlineWidget(Client *client, TextEditor::BaseTextEditor *editor) : m_client(client) , m_editor(editor) , m_view(this) , m_uri(DocumentUri::fromFilePath(editor->textDocument()->filePath())) { connect(client->documentSymbolCache(), &DocumentSymbolCache::gotSymbols, this, &LanguageClientOutlineWidget::handleResponse); connect(client, &Client::documentUpdated, this, [this](TextEditor::TextDocument *document) { if (m_client && m_uri == DocumentUri::fromFilePath(document->filePath())) m_client->documentSymbolCache()->requestSymbols(m_uri, Schedule::Delayed); }); client->documentSymbolCache()->requestSymbols(m_uri, Schedule::Delayed); auto *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); layout->addWidget(Core::ItemViewFind::createSearchableWrapper(&m_view)); setLayout(layout); m_model.setSymbolStringifier(m_client->symbolStringifier()); m_proxyModel.setSourceModel(&m_model); m_view.setModel(&m_proxyModel); m_view.setHeaderHidden(true); m_view.setExpandsOnDoubleClick(false); m_view.setFrameStyle(QFrame::NoFrame); connect(&m_view, &QAbstractItemView::activated, this, &LanguageClientOutlineWidget::onItemActivated); connect(m_editor->editorWidget(), &TextEditor::TextEditorWidget::cursorPositionChanged, this, [this](){ if (m_sync) updateSelectionInTree(m_editor->textCursor()); }); } QList LanguageClientOutlineWidget::filterMenuActions() const { return {}; } void LanguageClientOutlineWidget::setCursorSynchronization(bool syncWithCursor) { m_sync = syncWithCursor; if (m_sync && m_editor) updateSelectionInTree(m_editor->textCursor()); } void LanguageClientOutlineWidget::setSorted(bool sorted) { m_sorted = sorted; m_proxyModel.sort(sorted ? 0 : -1); } bool LanguageClientOutlineWidget::isSorted() const { return m_sorted; } void LanguageClientOutlineWidget::restoreSettings(const QVariantMap &map) { setSorted(map.value(QString("LspOutline.Sort"), false).toBool()); } QVariantMap LanguageClientOutlineWidget::settings() const { return {{QString("LspOutline.Sort"), m_sorted}}; } void LanguageClientOutlineWidget::handleResponse(const DocumentUri &uri, const DocumentSymbolsResult &result) { if (uri != m_uri) return; if (Utils::holds_alternative>(result)) m_model.setInfo(Utils::get>(result)); else if (Utils::holds_alternative>(result)) m_model.setInfo(Utils::get>(result)); else m_model.clear(); // The list has changed, update the current items updateSelectionInTree(m_editor->textCursor()); } void LanguageClientOutlineWidget::updateTextCursor(const QModelIndex &proxyIndex) { LanguageClientOutlineItem *item = m_model.itemForIndex(m_proxyModel.mapToSource(proxyIndex)); const Position &pos = item->pos(); // line has to be 1 based, column 0 based! m_editor->editorWidget()->gotoLine(pos.line() + 1, pos.character(), true, true); } static LanguageClientOutlineItem *itemForCursor(const LanguageClientOutlineModel &m_model, const QTextCursor &cursor) { const Position pos(cursor); LanguageClientOutlineItem *result = nullptr; m_model.forAllItems([&](LanguageClientOutlineItem *candidate){ if (!candidate->contains(pos)) return; if (result && candidate->range().contains(result->range())) return; // skip item if the range is equal or bigger than the previous found range result = candidate; }); return result; } void LanguageClientOutlineWidget::updateSelectionInTree(const QTextCursor ¤tCursor) { if (LanguageClientOutlineItem *item = itemForCursor(m_model, currentCursor)) { const QModelIndex index = m_proxyModel.mapFromSource(m_model.indexForItem(item)); m_view.selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); m_view.scrollTo(index); } else { m_view.clearSelection(); } } void LanguageClientOutlineWidget::onItemActivated(const QModelIndex &index) { if (!index.isValid() || !m_editor) return; updateTextCursor(index); m_editor->widget()->setFocus(); } bool LanguageClientOutlineWidgetFactory::clientSupportsDocumentSymbols( const Client *client, const TextEditor::TextDocument *doc) { if (!client) return false; DynamicCapabilities dc = client->dynamicCapabilities(); if (dc.isRegistered(DocumentSymbolsRequest::methodName).value_or(false)) { TextDocumentRegistrationOptions options(dc.option(DocumentSymbolsRequest::methodName)); return !options.isValid() || options.filterApplies(doc->filePath(), Utils::mimeTypeForName(doc->mimeType())); } const Utils::optional> &provider = client->capabilities().documentSymbolProvider(); if (!provider.has_value()) return false; if (Utils::holds_alternative(*provider)) return Utils::get(*provider); return true; } bool LanguageClientOutlineWidgetFactory::supportsEditor(Core::IEditor *editor) const { auto doc = qobject_cast(editor->document()); if (!doc) return false; return clientSupportsDocumentSymbols(LanguageClientManager::clientForDocument(doc), doc); } TextEditor::IOutlineWidget *LanguageClientOutlineWidgetFactory::createWidget(Core::IEditor *editor) { auto textEditor = qobject_cast(editor); QTC_ASSERT(textEditor, return nullptr); Client *client = LanguageClientManager::clientForDocument(textEditor->textDocument()); if (!client || !clientSupportsDocumentSymbols(client, textEditor->textDocument())) return nullptr; return new LanguageClientOutlineWidget(client, textEditor); } class OutlineComboBox : public Utils::TreeViewComboBox { public: OutlineComboBox(Client *client, TextEditor::BaseTextEditor *editor); private: void updateModel(const DocumentUri &resultUri, const DocumentSymbolsResult &result); void updateEntry(); void activateEntry(); void documentUpdated(TextEditor::TextDocument *document); LanguageClientOutlineModel m_model; QPointer m_client; TextEditor::TextEditorWidget *m_editorWidget; const DocumentUri m_uri; }; Utils::TreeViewComboBox *LanguageClientOutlineWidgetFactory::createComboBox(Client *client, Core::IEditor *editor) { auto textEditor = qobject_cast(editor); QTC_ASSERT(textEditor, return nullptr); TextEditor::TextDocument *document = textEditor->textDocument(); if (!client || !clientSupportsDocumentSymbols(client, document)) return nullptr; return new OutlineComboBox(client, textEditor); } OutlineComboBox::OutlineComboBox(Client *client, TextEditor::BaseTextEditor *editor) : m_client(client) , m_editorWidget(editor->editorWidget()) , m_uri(DocumentUri::fromFilePath(editor->document()->filePath())) { m_model.setSymbolStringifier(client->symbolStringifier()); setModel(&m_model); setMinimumContentsLength(13); QSizePolicy policy = sizePolicy(); policy.setHorizontalPolicy(QSizePolicy::Expanding); setSizePolicy(policy); setMaxVisibleItems(40); connect(client->documentSymbolCache(), &DocumentSymbolCache::gotSymbols, this, &OutlineComboBox::updateModel); connect(client, &Client::documentUpdated, this, &OutlineComboBox::documentUpdated); connect(m_editorWidget, &TextEditor::TextEditorWidget::cursorPositionChanged, this, &OutlineComboBox::updateEntry); connect(this, QOverload::of(&QComboBox::activated), this, &OutlineComboBox::activateEntry); documentUpdated(editor->textDocument()); } void OutlineComboBox::updateModel(const DocumentUri &resultUri, const DocumentSymbolsResult &result) { if (m_uri != resultUri) return; if (Utils::holds_alternative>(result)) m_model.setInfo(Utils::get>(result)); else if (Utils::holds_alternative>(result)) m_model.setInfo(Utils::get>(result)); else m_model.clear(); view()->expandAll(); // The list has changed, update the current item updateEntry(); } void OutlineComboBox::updateEntry() { if (LanguageClientOutlineItem *item = itemForCursor(m_model, m_editorWidget->textCursor())) setCurrentIndex(m_model.indexForItem(item)); } void OutlineComboBox::activateEntry() { const QModelIndex modelIndex = view()->currentIndex(); if (modelIndex.isValid()) { const Position &pos = m_model.itemForIndex(modelIndex)->pos(); Core::EditorManager::cutForwardNavigationHistory(); Core::EditorManager::addCurrentPositionToNavigationHistory(); // line has to be 1 based, column 0 based! m_editorWidget->gotoLine(pos.line() + 1, pos.character(), true, true); emit m_editorWidget->activateEditor(); } } void OutlineComboBox::documentUpdated(TextEditor::TextDocument *document) { if (document == m_editorWidget->textDocument()) m_client->documentSymbolCache()->requestSymbols(m_uri, Schedule::Delayed); } } // namespace LanguageClient