// Copyright (C) 2019 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "lspinspector.h" #include "client.h" #include "languageclientmanager.h" #include "languageclientsettings.h" #include "languageclienttr.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace LanguageServerProtocol; using namespace Utils; namespace LanguageClient { class JsonTreeItemDelegate : public QStyledItemDelegate { public: QString displayText(const QVariant &value, const QLocale &) const override { QString result = value.toString(); if (result.size() == 1) { switch (result.at(0).toLatin1()) { case '\n': return QString("\\n"); case '\t': return QString("\\t"); case '\r': return QString("\\r"); } } return result; } }; using JsonModel = Utils::TreeModel; JsonModel *createJsonModel(const QString &displayName, const QJsonValue &value) { if (value.isNull()) return nullptr; auto root = new Utils::JsonTreeItem(displayName, value); if (root->canFetchMore()) root->fetchMore(); auto model = new JsonModel(root); model->setHeader({{"Name"}, {"Value"}, {"Type"}}); return model; } QTreeView *createJsonTreeView() { auto view = new QTreeView; view->setContextMenuPolicy(Qt::ActionsContextMenu); auto action = new QAction(Tr::tr("Expand All"), view); QObject::connect(action, &QAction::triggered, view, &QTreeView::expandAll); view->addAction(action); view->setAlternatingRowColors(true); view->header()->setSectionResizeMode(QHeaderView::ResizeToContents); view->setItemDelegate(new JsonTreeItemDelegate); return view; } QTreeView *createJsonTreeView(const QString &displayName, const QJsonValue &value) { auto view = createJsonTreeView(); view->setModel(createJsonModel(displayName, value)); return view; } class MessageDetailWidget : public QGroupBox { public: MessageDetailWidget(); void setMessage(const LspLogMessage &message); void clear(); private: QTreeView *m_jsonTree = nullptr; }; class LspCapabilitiesWidget : public QWidget { public: LspCapabilitiesWidget(); void setCapabilities(const Capabilities &serverCapabilities); private: void updateOptionsView(const QString &method); DynamicCapabilities m_dynamicCapabilities; QTreeView *m_capabilitiesView = nullptr; QListWidget *m_dynamicCapabilitiesView = nullptr; QTreeView *m_dynamicOptionsView = nullptr; QGroupBox *m_dynamicCapabilitiesGroup = nullptr; }; LspCapabilitiesWidget::LspCapabilitiesWidget() { auto mainLayout = new QHBoxLayout; auto group = new QGroupBox(Tr::tr("Capabilities:")); QLayout *layout = new QHBoxLayout; m_capabilitiesView = createJsonTreeView(); layout->addWidget(m_capabilitiesView); group->setLayout(layout); mainLayout->addWidget(group); m_dynamicCapabilitiesGroup = new QGroupBox(Tr::tr("Dynamic Capabilities:")); layout = new QVBoxLayout; auto label = new QLabel(Tr::tr("Method:")); layout->addWidget(label); m_dynamicCapabilitiesView = new QListWidget(); layout->addWidget(m_dynamicCapabilitiesView); label = new QLabel(Tr::tr("Options:")); layout->addWidget(label); m_dynamicOptionsView = createJsonTreeView(); layout->addWidget(m_dynamicOptionsView); m_dynamicCapabilitiesGroup->setLayout(layout); mainLayout->addWidget(m_dynamicCapabilitiesGroup); setLayout(mainLayout); connect(m_dynamicCapabilitiesView, &QListWidget::currentTextChanged, this, &LspCapabilitiesWidget::updateOptionsView); } void LspCapabilitiesWidget::setCapabilities(const Capabilities &serverCapabilities) { m_capabilitiesView->setModel( createJsonModel(Tr::tr("Server Capabilities"), QJsonObject(serverCapabilities.capabilities))); m_dynamicCapabilities = serverCapabilities.dynamicCapabilities; const QStringList &methods = m_dynamicCapabilities.registeredMethods(); if (methods.isEmpty()) { m_dynamicCapabilitiesGroup->hide(); return; } m_dynamicCapabilitiesGroup->show(); m_dynamicCapabilitiesView->clear(); m_dynamicCapabilitiesView->addItems(methods); } void LspCapabilitiesWidget::updateOptionsView(const QString &method) { QAbstractItemModel *oldModel = m_dynamicOptionsView->model(); m_dynamicOptionsView->setModel(createJsonModel(method, m_dynamicCapabilities.option(method))); delete oldModel; } class LspLogWidget : public Core::MiniSplitter { public: LspLogWidget(); void addMessage(const LspLogMessage &message); void setMessages(const std::list &messages); void saveLog(); MessageDetailWidget *m_clientDetails = nullptr; QListView *m_messages = nullptr; MessageDetailWidget *m_serverDetails = nullptr; Utils::ListModel m_model; private: void currentMessageChanged(const QModelIndex &index); void selectMatchingMessage(const LspLogMessage &message); }; static QVariant messageData(const LspLogMessage &message, int, int role) { if (role == Qt::DisplayRole) return message.displayText(); if (role == Qt::TextAlignmentRole) return message.sender == LspLogMessage::ClientMessage ? Qt::AlignLeft : Qt::AlignRight; return {}; } LspLogWidget::LspLogWidget() { setOrientation(Qt::Horizontal); m_clientDetails = new MessageDetailWidget; m_clientDetails->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_clientDetails->setTitle(Tr::tr("Client Message")); addWidget(m_clientDetails); setStretchFactor(0, 1); m_model.setDataAccessor(&messageData); m_messages = new QListView; m_messages->setModel(&m_model); m_messages->setAlternatingRowColors(true); m_model.setHeader({Tr::tr("Messages")}); m_messages->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); m_messages->setSelectionMode(QAbstractItemView::MultiSelection); addWidget(m_messages); setStretchFactor(1, 0); m_serverDetails = new MessageDetailWidget; m_serverDetails->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_serverDetails->setTitle(Tr::tr("Server Message")); addWidget(m_serverDetails); setStretchFactor(2, 1); connect(m_messages->selectionModel(), &QItemSelectionModel::currentChanged, this, &LspLogWidget::currentMessageChanged); } void LspLogWidget::currentMessageChanged(const QModelIndex &index) { m_messages->clearSelection(); if (!index.isValid()) { m_clientDetails->clear(); m_serverDetails->clear(); return; } LspLogMessage message = m_model.itemAt(index.row())->itemData; if (message.sender == LspLogMessage::ClientMessage) m_clientDetails->setMessage(message); else m_serverDetails->setMessage(message); selectMatchingMessage(message); } static bool matches(LspLogMessage::MessageSender sender, const MessageId &id, const LspLogMessage &message) { if (message.sender != sender) return false; return message.id() == id; } void LspLogWidget::selectMatchingMessage(const LspLogMessage &message) { MessageId id = message.id(); if (!id.isValid()) return; LspLogMessage::MessageSender sender = message.sender == LspLogMessage::ServerMessage ? LspLogMessage::ClientMessage : LspLogMessage::ServerMessage; LspLogMessage *matchingMessage = m_model.findData( [&](const LspLogMessage &message) { return matches(sender, id, message); }); if (!matchingMessage) return; auto index = m_model.findIndex( [&](const LspLogMessage &message) { return &message == matchingMessage; }); m_messages->selectionModel()->select(index, QItemSelectionModel::Select); if (matchingMessage->sender == LspLogMessage::ServerMessage) m_serverDetails->setMessage(*matchingMessage); else m_clientDetails->setMessage(*matchingMessage); } void LspLogWidget::addMessage(const LspLogMessage &message) { m_model.appendItem(message); } void LspLogWidget::setMessages(const std::list &messages) { m_model.clear(); for (const LspLogMessage &message : messages) m_model.appendItem(message); } void LspLogWidget::saveLog() { QString contents; QTextStream stream(&contents); m_model.forAllData([&](const LspLogMessage &message) { stream << message.time.toString("hh:mm:ss.zzz") << ' '; stream << (message.sender == LspLogMessage::ClientMessage ? QString{"Client"} : QString{"Server"}); stream << '\n'; stream << QJsonDocument(message.message.toJsonObject()).toJson(); stream << "\n\n"; }); const FilePath filePath = FileUtils::getSaveFilePath(Tr::tr("Log File")); if (filePath.isEmpty()) return; FileSaver saver(filePath, QIODevice::Text); saver.write(contents.toUtf8()); if (const Result<> res = saver.finalize(); !res) { FileUtils::showError(res.error()); saveLog(); } } class LspInspectorWidget : public QDialog { public: explicit LspInspectorWidget(LspInspector *inspector); void selectClient(const QString &clientName); private: void addMessage(const QString &clientName, const LspLogMessage &message); void updateCapabilities(const QString &clientName); void currentClientChanged(const QString &clientName); LspLogWidget *log() const; LspCapabilitiesWidget *capabilities() const; LspInspector *const m_inspector = nullptr; LspLogWidget *m_logWidget = nullptr; LspCapabilitiesWidget *m_capWidget = nullptr; QTabWidget *m_tabWidget = nullptr; const int m_numFixedTabs = 2; QComboBox *m_clients = nullptr; }; void LspInspector::show(const QString &defaultClient) { if (!m_currentWidget) { auto widget = new LspInspectorWidget(this); connect(widget, &LspInspectorWidget::finished, this, &LspInspector::onInspectorClosed); Core::ICore::registerWindow(widget, Core::Context("LanguageClient.Inspector")); m_currentWidget = widget; } else { QApplication::setActiveWindow(m_currentWidget); } if (!defaultClient.isEmpty()) static_cast(m_currentWidget)->selectClient(defaultClient); m_currentWidget->show(); } void LspInspector::log(const LspLogMessage::MessageSender sender, const QString &clientName, const JsonRpcMessage &message) { std::list &clientLog = m_logs[clientName]; while (clientLog.size() >= static_cast(m_logSize)) clientLog.pop_front(); clientLog.push_back({sender, QTime::currentTime(), message}); emit newMessage(clientName, clientLog.back()); } void LspInspector::clientInitialized(const QString &clientName, const ServerCapabilities &capabilities) { m_capabilities[clientName].capabilities = capabilities; m_capabilities[clientName].dynamicCapabilities.reset(); emit capabilitiesUpdated(clientName); } void LspInspector::updateCapabilities(const QString &clientName, const DynamicCapabilities &dynamicCapabilities) { m_capabilities[clientName].dynamicCapabilities = dynamicCapabilities; emit capabilitiesUpdated(clientName); } std::list LspInspector::messages(const QString &clientName) const { return m_logs.value(clientName); } Capabilities LspInspector::capabilities(const QString &clientName) const { return m_capabilities.value(clientName); } QList LspInspector::clients() const { return m_logs.keys(); } void LspInspector::onInspectorClosed() { m_currentWidget->deleteLater(); m_currentWidget = nullptr; } static QString sendMessage(Client *client, const QString &msg) { if (!client) return Tr::tr("No client selected"); QString parseError; BaseMessage baseMsg; QByteArray asUtf8 = msg.toUtf8(); QBuffer buf; buf.open(QIODevice::WriteOnly); buf.write(QString("Content-Length: %1\r\n\r\n").arg(asUtf8.size()).toUtf8()); buf.write(asUtf8); buf.close(); buf.open(QIODevice::ReadOnly); BaseMessage::parse(&buf, parseError, baseMsg); if (!parseError.isEmpty()) return parseError; auto rpcMessage = JsonRpcMessage(baseMsg); if (!rpcMessage.parseError().isEmpty()) return rpcMessage.parseError(); client->sendMessage(rpcMessage, Client::SendDocUpdates::Send, LanguageClient::Schedule::Delayed); return {}; } LspInspectorWidget::LspInspectorWidget(LspInspector *inspector) : m_inspector(inspector) { setWindowTitle(Tr::tr("Language Client Inspector")); connect(inspector, &LspInspector::newMessage, this, &LspInspectorWidget::addMessage); connect(inspector, &LspInspector::capabilitiesUpdated, this, &LspInspectorWidget::updateCapabilities); connect(Core::ICore::instance(), &Core::ICore::coreAboutToClose, this, &QWidget::close); m_clients = new QComboBox; m_clients->addItem(Tr::tr("