LSP: implement call hierarchy

Fixes: QTCREATORBUG-11660
Change-Id: I006872ba598a807f1f9f16d134fe9ce4fe5dd09d
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
This commit is contained in:
David Schulz
2022-12-06 15:16:41 +01:00
parent ac5db86129
commit 887db6b419
14 changed files with 494 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ add_qtc_plugin(LanguageClient
PUBLIC_DEPENDS LanguageServerProtocol Qt::Core app_version
PLUGIN_DEPENDS ProjectExplorer Core TextEditor
SOURCES
callhierarchy.cpp callhierarchy.h
client.cpp client.h
diagnosticmanager.cpp diagnosticmanager.h
documentsymbolcache.cpp documentsymbolcache.h

View File

@@ -0,0 +1,291 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "callhierarchy.h"
#include "languageclientmanager.h"
#include "languageclienttr.h"
#include <QToolButton>
#include <coreplugin/editormanager/editormanager.h>
#include <languageserverprotocol/callhierarchy.h>
#include <texteditor/texteditor.h>
#include <utils/delegates.h>
#include <utils/navigationtreeview.h>
#include <utils/treemodel.h>
#include <utils/utilsicons.h>
using namespace Utils;
using namespace TextEditor;
using namespace LanguageServerProtocol;
namespace LanguageClient {
const char CALL_HIERARCHY_FACTORY_ID[] = "LanguageClient.CallHierarchy";
namespace {
enum Direction { Incoming, Outgoing };
enum {
AnnotationRole = Qt::UserRole + 1,
LinkRole
};
}
class CallHierarchyItem : public TreeItem
{
public:
CallHierarchyItem(const LanguageServerProtocol::CallHierarchyItem &item,
const Direction direction,
Client *client)
: m_item(item)
, m_direction(direction)
, m_client(client)
{
}
QVariant data(int column, int role) const override
{
switch (role) {
case Qt::DecorationRole:
return symbolIcon(int(m_item.symbolKind()));
case Qt::DisplayRole:
return m_item.name();
case LinkRole: {
if (!m_client)
return QVariant();
const Position start = m_item.selectionRange().start();
return QVariant::fromValue(
Link(m_client->serverUriToHostPath(m_item.uri()), start.line(), start.character()));
}
case AnnotationRole:
if (const std::optional<QString> detail = m_item.detail())
return *detail;
return {};
default:
return TreeItem::data(column, role);
}
}
bool canFetchMore() const override
{
return m_client && !m_fetchedChildren;
}
void fetchMore() override
{
m_fetchedChildren = true;
if (!m_client)
return;
CallHierarchyCallsParams params;
params.setItem(m_item);
if (m_direction == Incoming) {
CallHierarchyIncomingCallsRequest request(params);
request.setResponseCallback(
[this](const CallHierarchyIncomingCallsRequest::Response &response) {
const std::optional<LanguageClientArray<CallHierarchyIncomingCall>> result
= response.result();
if (result && !result->isNull()) {
for (const CallHierarchyIncomingCall &item : result->toList()) {
if (item.isValid())
appendChild(new CallHierarchyItem(item.from(), m_direction, m_client));
}
}
if (!hasChildren())
update();
});
m_client->sendMessage(request);
} else {
CallHierarchyOutgoingCallsRequest request(params);
request.setResponseCallback(
[this](const CallHierarchyOutgoingCallsRequest::Response &response) {
const std::optional<LanguageClientArray<CallHierarchyOutgoingCall>> result
= response.result();
if (result && !result->isNull()) {
for (const CallHierarchyOutgoingCall &item : result->toList()) {
if (item.isValid())
appendChild(new CallHierarchyItem(item.to(), m_direction, m_client));
}
}
if (!hasChildren())
update();
});
m_client->sendMessage(request);
}
}
protected:
const LanguageServerProtocol::CallHierarchyItem m_item;
const Direction m_direction;
bool m_fetchedChildren = false;
QPointer<Client> m_client;
};
class CallHierarchyDirectionItem : public CallHierarchyItem
{
public:
CallHierarchyDirectionItem(const LanguageServerProtocol::CallHierarchyItem &item,
const Direction direction,
Client *client)
: CallHierarchyItem(item, direction, client)
{}
QVariant data(int column, int role) const override
{
if (role == Qt::DisplayRole)
return m_direction == Incoming ? Tr::tr("Incoming") : Tr::tr("Outgoing");
return CallHierarchyItem::data(column, role);
}
};
class CallHierarchyRootItem : public TreeItem
{
public:
CallHierarchyRootItem(const LanguageServerProtocol::CallHierarchyItem &item, Client *client)
: m_item(item)
{
appendChild(new CallHierarchyDirectionItem(m_item, Incoming, client));
appendChild(new CallHierarchyDirectionItem(m_item, Outgoing, client));
}
QVariant data(int column, int role) const override
{
switch (role) {
case Qt::DecorationRole:
return symbolIcon(int(m_item.symbolKind()));
case Qt::DisplayRole:
return m_item.name();
default:
return TreeItem::data(column, role);
}
}
private:
const LanguageServerProtocol::CallHierarchyItem m_item;
};
class CallHierarchy : public QWidget
{
public:
CallHierarchy() : m_view(new NavigationTreeView(this))
{
m_delegate.setDelimiter(" ");
m_delegate.setAnnotationRole(AnnotationRole);
m_view->setModel(&m_model);
m_view->setActivationMode(SingleClickActivation);
m_view->setItemDelegate(&m_delegate);
setLayout(new QVBoxLayout);
layout()->addWidget(m_view);
layout()->setContentsMargins(0, 0, 0, 0);
layout()->setSpacing(0);
connect(m_view, &NavigationTreeView::activated, this, &CallHierarchy::onItemActivated);
}
void onItemActivated(const QModelIndex &index)
{
const auto link = index.data(LinkRole).value<Utils::Link>();
if (link.hasValidTarget())
Core::EditorManager::openEditorAt(link);
}
void updateHierarchyAtCursorPosition();
void handlePrepareResponse(Client *client,
const PrepareCallHierarchyRequest::Response &response);
AnnotatedItemDelegate m_delegate;
NavigationTreeView *m_view;
TreeModel<TreeItem, CallHierarchyRootItem, CallHierarchyDirectionItem, CallHierarchyItem> m_model;
};
void CallHierarchy::updateHierarchyAtCursorPosition()
{
m_model.clear();
BaseTextEditor *editor = BaseTextEditor::currentTextEditor();
if (!editor)
return;
Client *client = LanguageClientManager::clientForFilePath(editor->document()->filePath());
if (!client)
return;
const QString methodName = PrepareCallHierarchyRequest::methodName;
std::optional<bool> registered = client->dynamicCapabilities().isRegistered(methodName);
bool supported = registered.value_or(false);
const Core::IDocument *document = editor->document();
if (registered) {
if (supported) {
const QJsonValue &options = client->dynamicCapabilities().option(methodName);
const TextDocumentRegistrationOptions docOptions(options);
supported = docOptions.filterApplies(document->filePath(),
Utils::mimeTypeForName(document->mimeType()));
}
} else {
supported = client->capabilities().callHierarchyProvider().has_value();
}
if (!supported)
return;
TextDocumentPositionParams params;
params.setTextDocument(TextDocumentIdentifier(client->hostPathToServerUri(document->filePath())));
params.setPosition(Position(editor->editorWidget()->textCursor()));
PrepareCallHierarchyRequest request(params);
request.setResponseCallback([this, client = QPointer<Client>(client)](
const PrepareCallHierarchyRequest::Response &response) {
handlePrepareResponse(client, response);
});
client->sendMessage(request);
}
void CallHierarchy::handlePrepareResponse(Client *client,
const PrepareCallHierarchyRequest::Response &response)
{
if (!client)
return;
const std::optional<PrepareCallHierarchyRequest::Response::Error> error = response.error();
if (error)
client->log(*error);
const std::optional<LanguageClientArray<LanguageServerProtocol::CallHierarchyItem>>
result = response.result();
if (result && !result->isNull()) {
for (const LanguageServerProtocol::CallHierarchyItem &item : result->toList()) {
auto newItem = new CallHierarchyRootItem(item, client);
m_model.rootItem()->appendChild(newItem);
m_view->expand(newItem->index());
newItem->forChildrenAtLevel(1, [&](const TreeItem *child) {
m_view->expand(child->index());
});
}
}
}
CallHierarchyFactory::CallHierarchyFactory()
{
setDisplayName(tr("Call Hierarchy"));
setPriority(650);
setId(CALL_HIERARCHY_FACTORY_ID);
}
Core::NavigationView CallHierarchyFactory::createWidget()
{
auto h = new CallHierarchy;
h->updateHierarchyAtCursorPosition();
Icons::RELOAD_TOOLBAR.icon();
auto button = new QToolButton;
button->setIcon(Icons::RELOAD_TOOLBAR.icon());
connect(button, &QToolButton::clicked, [h](){
h->updateHierarchyAtCursorPosition();
});
return {h,{button}};
}
} // namespace LanguageClient

View File

@@ -0,0 +1,20 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include <coreplugin/inavigationwidgetfactory.h>
#pragma once
namespace LanguageClient {
class CallHierarchyFactory : public Core::INavigationWidgetFactory
{
Q_OBJECT
public:
CallHierarchyFactory();
Core::NavigationView createWidget() override;
};
} // namespace LanguageClient

View File

@@ -482,6 +482,7 @@ static ClientCapabilities generateClientCapabilities()
tokens.setTokenModifiers({"declaration", "definition"});
tokens.setFormats({"relative"});
documentCapabilities.setSemanticTokens(tokens);
documentCapabilities.setCallHierarchy(allowDynamicRegistration);
capabilities.setTextDocument(documentCapabilities);
WindowClientClientCapabilities window;

View File

@@ -20,6 +20,8 @@ QtcPlugin {
Depends { name: "app_version_header" }
files: [
"callhierarchy.cpp",
"callhierarchy.h",
"client.cpp",
"client.h",
"diagnosticmanager.cpp",

View File

@@ -3,9 +3,8 @@
#pragma once
#include "languageclientmanager.h"
#include "languageclientoutline.h"
#include "languageclientsettings.h"
#include "callhierarchy.h"
#include <extensionsystem/iplugin.h>
@@ -29,6 +28,7 @@ private:
private:
LanguageClientOutlineWidgetFactory m_outlineFactory;
CallHierarchyFactory m_callHierarchyFactory;
#ifdef WITH_TESTS
private slots: