diff --git a/src/plugins/languageclient/CMakeLists.txt b/src/plugins/languageclient/CMakeLists.txt index abddb9cd968..9d04e7df1b7 100644 --- a/src/plugins/languageclient/CMakeLists.txt +++ b/src/plugins/languageclient/CMakeLists.txt @@ -19,5 +19,6 @@ add_qtc_plugin(LanguageClient languageclientutils.cpp languageclientutils.h languageclient_global.h locatorfilter.cpp locatorfilter.h + lsplogger.cpp lsplogger.h semantichighlightsupport.cpp semantichighlightsupport.h ) diff --git a/src/plugins/languageclient/client.cpp b/src/plugins/languageclient/client.cpp index bb74a7bcab3..80b156216d5 100644 --- a/src/plugins/languageclient/client.cpp +++ b/src/plugins/languageclient/client.cpp @@ -262,6 +262,9 @@ void Client::initialize() }); // directly send data otherwise the state check would fail; initRequest.registerResponseHandler(&m_responseHandlers); + LanguageClientManager::logBaseMessage(LspLogMessage::ClientMessage, + name(), + initRequest.toBaseMessage()); m_clientInterface->sendMessage(initRequest.toBaseMessage()); m_state = InitializeRequested; } @@ -334,6 +337,9 @@ void Client::sendContent(const IContent &content) QString error; if (!QTC_GUARD(content.isValid(&error))) Core::MessageManager::write(error); + LanguageClientManager::logBaseMessage(LspLogMessage::ClientMessage, + name(), + content.toBaseMessage()); m_clientInterface->sendMessage(content.toBaseMessage()); } @@ -931,6 +937,7 @@ void Client::setError(const QString &message) void Client::handleMessage(const BaseMessage &message) { + LanguageClientManager::logBaseMessage(LspLogMessage::ServerMessage, name(), message); if (auto handler = m_contentHandler[message.mimeType]) { QString parseError; handler(message.content, message.codec, parseError, diff --git a/src/plugins/languageclient/languageclient.pro b/src/plugins/languageclient/languageclient.pro index 5db41008d1c..f6a2930d71a 100644 --- a/src/plugins/languageclient/languageclient.pro +++ b/src/plugins/languageclient/languageclient.pro @@ -19,6 +19,7 @@ HEADERS += \ languageclientsettings.h \ languageclientutils.h \ locatorfilter.h \ + lsplogger.h \ semantichighlightsupport.h @@ -38,6 +39,7 @@ SOURCES += \ languageclientsettings.cpp \ languageclientutils.cpp \ locatorfilter.cpp \ + lsplogger.cpp \ semantichighlightsupport.cpp RESOURCES += \ diff --git a/src/plugins/languageclient/languageclient.qbs b/src/plugins/languageclient/languageclient.qbs index ce1583381de..0502921b9ef 100644 --- a/src/plugins/languageclient/languageclient.qbs +++ b/src/plugins/languageclient/languageclient.qbs @@ -46,6 +46,8 @@ QtcPlugin { "languageclientutils.h", "locatorfilter.cpp", "locatorfilter.h", + "lsplogger.cpp", + "lsplogger.h", "semantichighlightsupport.cpp", "semantichighlightsupport.h", ] diff --git a/src/plugins/languageclient/languageclientmanager.cpp b/src/plugins/languageclient/languageclientmanager.cpp index 27e74683132..b2c65f511b7 100644 --- a/src/plugins/languageclient/languageclientmanager.cpp +++ b/src/plugins/languageclient/languageclientmanager.cpp @@ -340,6 +340,20 @@ void LanguageClientManager::reOpenDocumentWithClient(TextEditor::TextDocument *d client->activateDocument(document); } +void LanguageClientManager::logBaseMessage(const LspLogMessage::MessageSender sender, + const QString &clientName, + const BaseMessage &message) +{ + instance()->m_logger.log(sender, clientName, message); +} + +void LanguageClientManager::showLogger() +{ + QWidget *loggerWidget = instance()->m_logger.createWidget(); + loggerWidget->setAttribute(Qt::WA_DeleteOnClose); + loggerWidget->show(); +} + QVector LanguageClientManager::reachableClients() { return Utils::filtered(m_clients, &Client::reachable); diff --git a/src/plugins/languageclient/languageclientmanager.h b/src/plugins/languageclient/languageclientmanager.h index fbf5322f1cc..ae30540f44b 100644 --- a/src/plugins/languageclient/languageclientmanager.h +++ b/src/plugins/languageclient/languageclientmanager.h @@ -29,6 +29,7 @@ #include "languageclient_global.h" #include "languageclientsettings.h" #include "locatorfilter.h" +#include "lsplogger.h" #include @@ -84,6 +85,11 @@ public: static Client *clientForUri(const LanguageServerProtocol::DocumentUri &uri); static void reOpenDocumentWithClient(TextEditor::TextDocument *document, Client *client); + static void logBaseMessage(const LspLogMessage::MessageSender sender, + const QString &clientName, + const LanguageServerProtocol::BaseMessage &message); + static void showLogger(); + signals: void shutdownFinished(); @@ -118,5 +124,6 @@ private: WorkspaceLocatorFilter m_workspaceLocatorFilter; WorkspaceClassLocatorFilter m_workspaceClassLocatorFilter; WorkspaceMethodLocatorFilter m_workspaceMethodLocatorFilter; + LspLogger m_logger; }; } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientutils.cpp b/src/plugins/languageclient/languageclientutils.cpp index 9cc1ad488fa..72bb0157576 100644 --- a/src/plugins/languageclient/languageclientutils.cpp +++ b/src/plugins/languageclient/languageclientutils.cpp @@ -239,6 +239,9 @@ void updateEditorToolBar(Core::IEditor *editor) QObject::connect(action, &QAction::triggered, reopen); } menu->addActions(clientsGroup->actions()); + menu->addAction("Language Client Logs", []() { + LanguageClientManager::showLogger(); + }); menu->addAction("Manage...", []() { Core::ICore::showOptionsDialog(Constants::LANGUAGECLIENT_SETTINGS_PAGE); }); diff --git a/src/plugins/languageclient/lsplogger.cpp b/src/plugins/languageclient/lsplogger.cpp new file mode 100644 index 00000000000..76a50a138fe --- /dev/null +++ b/src/plugins/languageclient/lsplogger.cpp @@ -0,0 +1,364 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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 "lsplogger.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace LanguageServerProtocol; + +namespace LanguageClient { + +class MessageDetailWidget : public QGroupBox +{ +public: + MessageDetailWidget(); + + void setMessage(const BaseMessage &message); + void clear(); + +private: + QLabel *m_contentLength = nullptr; + QLabel *m_mimeType = nullptr; +}; + +class LspLoggerWidget : public QDialog +{ + Q_DECLARE_TR_FUNCTIONS(LspLoggerWidget) +public: + explicit LspLoggerWidget(LspLogger *logger); + +private: + void addMessage(const QString &clientName, const LspLogMessage &message); + void setCurrentClient(const QString &clientName); + void currentMessageChanged(const QModelIndex &index); + void selectMatchingMessage(LspLogMessage::MessageSender sender, const QJsonValue &id); + void saveLog(); + + LspLogger *m_logger = nullptr; + QListWidget *m_clients = nullptr; + MessageDetailWidget *m_clientDetails = nullptr; + QListView *m_messages = nullptr; + MessageDetailWidget *m_serverDetails = nullptr; + Utils::ListModel m_model; +}; + +QWidget *LspLogger::createWidget() +{ + return new LspLoggerWidget(this); +} + +void LspLogger::log(const LspLogMessage::MessageSender sender, + const QString &clientName, + const BaseMessage &message) +{ + QLinkedList &clientLog = m_logs[clientName]; + auto delta = clientLog.size() - m_logSize + 1; + if (delta > 0) + clientLog.erase(clientLog.begin(), clientLog.begin() + delta); + m_logs[clientName].append({sender, QTime::currentTime(), message}); + emit newMessage(clientName, m_logs[clientName].last()); +} + +QLinkedList LspLogger::messages(const QString &clientName) const +{ + return m_logs[clientName]; +} + +QList LspLogger::clients() const +{ + return m_logs.keys(); +} + +static QVariant messageData(const LspLogMessage &message, int, int role) +{ + if (role == Qt::DisplayRole) { + QString result = message.time.toString("hh:mm:ss.zzz") + '\n'; + if (message.message.mimeType == JsonRpcMessageHandler::jsonRpcMimeType()) { + QString error; + auto json = JsonRpcMessageHandler::toJsonObject(message.message.content, + message.message.codec, + error); + result += json.value(QString{methodKey}).toString(json.value(QString{idKey}).toString()); + } else { + result += message.message.codec->toUnicode(message.message.content); + } + return result; + } + if (role == Qt::TextAlignmentRole) + return message.sender == LspLogMessage::ClientMessage ? Qt::AlignLeft : Qt::AlignRight; + return {}; +} + +LspLoggerWidget::LspLoggerWidget(LspLogger *logger) + : m_logger(logger) +{ + setWindowTitle(tr("Language Client Log")); + + connect(logger, &LspLogger::newMessage, this, &LspLoggerWidget::addMessage); + connect(Core::ICore::instance(), &Core::ICore::coreAboutToClose, this, &QWidget::close); + + m_clients = new QListWidget; + m_clients->addItems(logger->clients()); + connect(m_clients, &QListWidget::currentTextChanged, this, &LspLoggerWidget::setCurrentClient); + m_clients->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::MinimumExpanding); + + m_clientDetails = new MessageDetailWidget; + m_clientDetails->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + m_clientDetails->setTitle(tr("Client Message")); + m_serverDetails = new MessageDetailWidget; + m_serverDetails->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + m_serverDetails->setTitle(tr("Server Message")); + + m_model.setDataAccessor(&messageData); + m_messages = new QListView; + m_messages->setModel(&m_model); + m_messages->setAlternatingRowColors(true); + m_model.setHeader({tr("Messages")}); + connect(m_messages->selectionModel(), + &QItemSelectionModel::currentChanged, + this, + &LspLoggerWidget::currentMessageChanged); + m_messages->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); + m_messages->setSelectionMode(QAbstractItemView::MultiSelection); + + auto layout = new QVBoxLayout; + setLayout(layout); + auto splitter = new Core::MiniSplitter; + splitter->setOrientation(Qt::Horizontal); + splitter->addWidget(m_clients); + splitter->addWidget(m_clientDetails); + splitter->addWidget(m_messages); + splitter->addWidget(m_serverDetails); + splitter->setStretchFactor(0, 0); + splitter->setStretchFactor(1, 1); + splitter->setStretchFactor(2, 1); + splitter->setStretchFactor(3, 1); + layout->addWidget(splitter); + + auto buttonBox = new QDialogButtonBox(this); + buttonBox->setStandardButtons(QDialogButtonBox::Save | QDialogButtonBox::Close); + layout->addWidget(buttonBox); + + // save + connect(buttonBox, &QDialogButtonBox::accepted, this, &LspLoggerWidget::saveLog); + + // close + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + resize(1024, 768); +} + +void LspLoggerWidget::addMessage(const QString &clientName, const LspLogMessage &message) +{ + if (m_clients->findItems(clientName, Qt::MatchExactly).isEmpty()) + m_clients->addItem(clientName); + if (clientName != m_clients->currentItem()->text()) + return; + m_model.appendItem(message); +} + +void LspLoggerWidget::setCurrentClient(const QString &clientName) +{ + m_model.clear(); + for (const LspLogMessage &message : m_logger->messages(clientName)) + m_model.appendItem(message); +} + +void LspLoggerWidget::currentMessageChanged(const QModelIndex &index) +{ + m_messages->clearSelection(); + if (!index.isValid()) + return; + LspLogMessage selectedMessage = m_model.itemAt(index.row())->itemData; + BaseMessage message = selectedMessage.message; + if (selectedMessage.sender == LspLogMessage::ClientMessage) + m_clientDetails->setMessage(message); + else + m_serverDetails->setMessage(message); + if (message.mimeType == JsonRpcMessageHandler::jsonRpcMimeType()) { + QString error; + QJsonValue id = JsonRpcMessageHandler::toJsonObject(message.content, message.codec, error) + .value(idKey); + if (!id.isUndefined()) { + selectMatchingMessage(selectedMessage.sender == LspLogMessage::ClientMessage + ? LspLogMessage::ServerMessage + : LspLogMessage::ClientMessage, + id); + } + } +} + +static bool matches(LspLogMessage::MessageSender sender, + const QJsonValue &id, + const LspLogMessage &message) +{ + if (message.sender != sender) + return false; + if (message.message.mimeType != JsonRpcMessageHandler::jsonRpcMimeType()) + return false; + QString error; + auto json = JsonRpcMessageHandler::toJsonObject(message.message.content, + message.message.codec, + error); + return json.value(QString{idKey}) == id; +} + +void LspLoggerWidget::selectMatchingMessage(LspLogMessage::MessageSender sender, + const QJsonValue &id) +{ + LspLogMessage *matchingMessage = m_model.findData( + [&](const LspLogMessage &message) { return matches(sender, id, message); }); + if (!matchingMessage) + return; + auto item = m_model.findItemByData( + [&](const LspLogMessage &message) { return &message == matchingMessage; }); + + m_messages->selectionModel()->select(m_model.indexForItem(item), QItemSelectionModel::Select); + if (matchingMessage->sender == LspLogMessage::ServerMessage) + m_serverDetails->setMessage(matchingMessage->message); + else + m_clientDetails->setMessage(matchingMessage->message); +} + +void LspLoggerWidget::saveLog() +{ + QString contents; + QTextStream stream(&contents); + m_model.forItems([&](const LspLogMessage &message) { + stream << message.time.toString("hh:mm:ss.zzz") << ' '; + stream << (message.sender == LspLogMessage::ClientMessage ? QString{"Client"} + : QString{"Server"}); + stream << '\n'; + stream << message.message.codec->toUnicode(message.message.content); + stream << "\n\n"; + }); + + const QString fileName = QFileDialog::getSaveFileName(this, tr("Log File")); + if (fileName.isEmpty()) + return; + Utils::FileSaver saver(fileName, QIODevice::Text); + saver.write(contents.toUtf8()); + if (!saver.finalize(this)) + saveLog(); +} + +MessageDetailWidget::MessageDetailWidget() +{ + auto layout = new QFormLayout; + setLayout(layout); + + m_contentLength = new QLabel; + m_mimeType = new QLabel; + + layout->addRow("Content Length:", m_contentLength); + layout->addRow("MIME Type:", m_mimeType); +} + +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; + } +}; + +void MessageDetailWidget::setMessage(const BaseMessage &message) +{ + m_contentLength->setText(QString::number(message.contentLength)); + m_mimeType->setText(QString::fromLatin1(message.mimeType)); + + QWidget *newContentWidget = nullptr; + if (message.mimeType == JsonRpcMessageHandler::jsonRpcMimeType()) { + QString error; + auto json = JsonRpcMessageHandler::toJsonObject(message.content, message.codec, error); + if (json.isEmpty()) { + newContentWidget = new QLabel(error); + } else { + auto root = new Utils::JsonTreeItem("content", json); + if (root->canFetchMore()) + root->fetchMore(); + + auto model = new Utils::TreeModel(root); + model->setHeader({{"Name"}, {"Value"}, {"Type"}}); + auto view = new QTreeView; + view->setModel(model); + view->setAlternatingRowColors(true); + view->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + view->setItemDelegate(new JsonTreeItemDelegate); + newContentWidget = view; + } + } else { + auto edit = new QPlainTextEdit(); + edit->setReadOnly(true); + edit->setPlainText(message.codec->toUnicode(message.content)); + newContentWidget = edit; + } + auto formLayout = static_cast(layout()); + if (formLayout->rowCount() > 2) + formLayout->removeRow(2); + formLayout->setWidget(2, QFormLayout::SpanningRole, newContentWidget); +} + +void MessageDetailWidget::clear() +{ + m_contentLength->setText({}); + m_mimeType->setText({}); + auto formLayout = static_cast(layout()); + if (formLayout->rowCount() > 2) + formLayout->removeRow(2); +} + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/lsplogger.h b/src/plugins/languageclient/lsplogger.h new file mode 100644 index 00000000000..c07a161737d --- /dev/null +++ b/src/plugins/languageclient/lsplogger.h @@ -0,0 +1,69 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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. +** +****************************************************************************/ + +#pragma once + +#include +#include +#include + +#include + +class QWidget; + +namespace LanguageClient { + +struct LspLogMessage +{ + enum MessageSender { ClientMessage, ServerMessage } sender; + QTime time; + LanguageServerProtocol::BaseMessage message; +}; + +class LspLogger : public QObject +{ + Q_OBJECT +public: + LspLogger() {} + + QWidget *createWidget(); + + + void log(const LspLogMessage::MessageSender sender, + const QString &clientName, + const LanguageServerProtocol::BaseMessage &message); + + QLinkedList messages(const QString &clientName) const; + QList clients() const; + +signals: + void newMessage(const QString &clientName, const LspLogMessage &message); + +private: + QMap> m_logs; + int m_logSize = 100; // default log size if no widget is currently visible +}; + +} // namespace LanguageClient