diff --git a/addons/lspclient/lspclientconfigpage.cpp b/addons/lspclient/lspclientconfigpage.cpp index 1550b2f61..2b23814df 100644 --- a/addons/lspclient/lspclientconfigpage.cpp +++ b/addons/lspclient/lspclientconfigpage.cpp @@ -1,236 +1,246 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts 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 "lspclientconfigpage.h" #include "lspclientplugin.h" #include "ui_lspconfigwidget.h" #include #include #include #include #include #include #include LSPClientConfigPage::LSPClientConfigPage(QWidget *parent, LSPClientPlugin *plugin) : KTextEditor::ConfigPage(parent) , m_plugin(plugin) { ui = new Ui::LspConfigWidget(); ui->setupUi(this); // fix-up our two text edits to be proper JSON file editors for (auto textEdit : {ui->userConfig, static_cast(ui->defaultConfig)}) { // setup JSON highlighter for the default json stuff auto highlighter = new KSyntaxHighlighting::SyntaxHighlighter(textEdit->document()); highlighter->setDefinition(m_repository.definitionForFileName(QStringLiteral("settings.json"))); // we want mono-spaced font textEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); // we want to have the proper theme for the current palette const auto theme = (palette().color(QPalette::Base).lightness() < 128) ? m_repository.defaultTheme(KSyntaxHighlighting::Repository::DarkTheme) : m_repository.defaultTheme(KSyntaxHighlighting::Repository::LightTheme); auto pal = qApp->palette(); if (theme.isValid()) { pal.setColor(QPalette::Base, theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor)); pal.setColor(QPalette::Highlight, theme.editorColor(KSyntaxHighlighting::Theme::TextSelection)); } textEdit->setPalette(pal); highlighter->setTheme(theme); } // setup default json settings QFile defaultConfigFile(QStringLiteral(":/lspclient/settings.json")); defaultConfigFile.open(QIODevice::ReadOnly); Q_ASSERT(defaultConfigFile.isOpen()); ui->defaultConfig->setPlainText(QString::fromUtf8(defaultConfigFile.readAll())); // setup default config path as placeholder to show user where it is ui->edtConfigPath->setPlaceholderText(m_plugin->m_defaultConfigPath.toLocalFile()); reset(); for (const auto &cb : {ui->chkSymbolDetails, ui->chkSymbolExpand, ui->chkSymbolSort, ui->chkSymbolTree, ui->chkComplDoc, ui->chkRefDeclaration, ui->chkDiagnostics, ui->chkDiagnosticsMark, + ui->chkMessages, ui->chkOnTypeFormatting, ui->chkIncrementalSync, ui->chkSemanticHighlighting, ui->chkAutoHover}) connect(cb, &QCheckBox::toggled, this, &LSPClientConfigPage::changed); + connect(ui->comboMessagesSwitch, static_cast(&QComboBox::currentIndexChanged), this, [this](int) { changed(); }); connect(ui->edtConfigPath, &KUrlRequester::textChanged, this, &LSPClientConfigPage::configUrlChanged); connect(ui->edtConfigPath, &KUrlRequester::urlSelected, this, &LSPClientConfigPage::configUrlChanged); connect(ui->userConfig, &QTextEdit::textChanged, this, &LSPClientConfigPage::configTextChanged); // custom control logic auto h = [this]() { bool enabled = ui->chkDiagnostics->isChecked(); ui->chkDiagnosticsHighlight->setEnabled(enabled); ui->chkDiagnosticsMark->setEnabled(enabled); + enabled = ui->chkMessages->isChecked(); + ui->comboMessagesSwitch->setEnabled(enabled); }; connect(this, &LSPClientConfigPage::changed, this, h); } LSPClientConfigPage::~LSPClientConfigPage() { delete ui; } QString LSPClientConfigPage::name() const { return QString(i18n("LSP Client")); } QString LSPClientConfigPage::fullName() const { return QString(i18n("LSP Client")); } QIcon LSPClientConfigPage::icon() const { return QIcon::fromTheme(QLatin1String("code-context")); } void LSPClientConfigPage::apply() { m_plugin->m_symbolDetails = ui->chkSymbolDetails->isChecked(); m_plugin->m_symbolTree = ui->chkSymbolTree->isChecked(); m_plugin->m_symbolExpand = ui->chkSymbolExpand->isChecked(); m_plugin->m_symbolSort = ui->chkSymbolSort->isChecked(); m_plugin->m_complDoc = ui->chkComplDoc->isChecked(); m_plugin->m_refDeclaration = ui->chkRefDeclaration->isChecked(); m_plugin->m_diagnostics = ui->chkDiagnostics->isChecked(); m_plugin->m_diagnosticsHighlight = ui->chkDiagnosticsHighlight->isChecked(); m_plugin->m_diagnosticsMark = ui->chkDiagnosticsMark->isChecked(); m_plugin->m_autoHover = ui->chkAutoHover->isChecked(); m_plugin->m_onTypeFormatting = ui->chkOnTypeFormatting->isChecked(); m_plugin->m_incrementalSync = ui->chkIncrementalSync->isChecked(); m_plugin->m_semanticHighlighting = ui->chkSemanticHighlighting->isChecked(); + m_plugin->m_messages = ui->chkMessages->isChecked(); + m_plugin->m_messagesAutoSwitch = ui->comboMessagesSwitch->currentIndex(); + m_plugin->m_configPath = ui->edtConfigPath->url(); // own scope to ensure file is flushed before we signal below in writeConfig! { QFile configFile(m_plugin->configPath().toLocalFile()); configFile.open(QIODevice::WriteOnly); if (configFile.isOpen()) { configFile.write(ui->userConfig->toPlainText().toUtf8()); } } m_plugin->writeConfig(); } void LSPClientConfigPage::reset() { ui->chkSymbolDetails->setChecked(m_plugin->m_symbolDetails); ui->chkSymbolTree->setChecked(m_plugin->m_symbolTree); ui->chkSymbolExpand->setChecked(m_plugin->m_symbolExpand); ui->chkSymbolSort->setChecked(m_plugin->m_symbolSort); ui->chkComplDoc->setChecked(m_plugin->m_complDoc); ui->chkRefDeclaration->setChecked(m_plugin->m_refDeclaration); ui->chkDiagnostics->setChecked(m_plugin->m_diagnostics); ui->chkDiagnosticsHighlight->setChecked(m_plugin->m_diagnosticsHighlight); ui->chkDiagnosticsMark->setChecked(m_plugin->m_diagnosticsMark); ui->chkAutoHover->setChecked(m_plugin->m_autoHover); ui->chkOnTypeFormatting->setChecked(m_plugin->m_onTypeFormatting); ui->chkIncrementalSync->setChecked(m_plugin->m_incrementalSync); ui->chkSemanticHighlighting->setChecked(m_plugin->m_semanticHighlighting); + ui->chkMessages->setChecked(m_plugin->m_messages); + ui->comboMessagesSwitch->setCurrentIndex(m_plugin->m_messagesAutoSwitch); + ui->edtConfigPath->setUrl(m_plugin->m_configPath); readUserConfig(m_plugin->configPath().toLocalFile()); } void LSPClientConfigPage::defaults() { reset(); } void LSPClientConfigPage::readUserConfig(const QString &fileName) { QFile configFile(fileName); configFile.open(QIODevice::ReadOnly); if (configFile.isOpen()) { ui->userConfig->setPlainText(QString::fromUtf8(configFile.readAll())); } else { ui->userConfig->clear(); } updateConfigTextErrorState(); } void LSPClientConfigPage::updateConfigTextErrorState() { const auto data = ui->userConfig->toPlainText().toUtf8(); if (data.isEmpty()) { ui->userConfigError->setText(i18n("No JSON data to validate.")); return; } // check json validity QJsonParseError error; auto json = QJsonDocument::fromJson(data, &error); if (error.error == QJsonParseError::NoError) { if (json.isObject()) { ui->userConfigError->setText(i18n("JSON data is valid.")); } else { ui->userConfigError->setText(i18n("JSON data is invalid: no JSON object")); } } else { ui->userConfigError->setText(i18n("JSON data is invalid: %1", error.errorString())); } } void LSPClientConfigPage::configTextChanged() { // check for errors updateConfigTextErrorState(); // remember changed changed(); } void LSPClientConfigPage::configUrlChanged() { // re-read config readUserConfig(ui->edtConfigPath->url().isEmpty() ? m_plugin->m_defaultConfigPath.toLocalFile() : ui->edtConfigPath->url().toLocalFile()); // remember changed changed(); } diff --git a/addons/lspclient/lspclientplugin.cpp b/addons/lspclient/lspclientplugin.cpp index 2aa63eaad..9cc3d1ae7 100644 --- a/addons/lspclient/lspclientplugin.cpp +++ b/addons/lspclient/lspclientplugin.cpp @@ -1,144 +1,150 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts 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 "lspclientplugin.h" #include "lspclientconfigpage.h" #include "lspclientpluginview.h" #include "lspclient_debug.h" #include #include #include #include #include #include static const QString CONFIG_LSPCLIENT {QStringLiteral("lspclient")}; static const QString CONFIG_SYMBOL_DETAILS {QStringLiteral("SymbolDetails")}; static const QString CONFIG_SYMBOL_TREE {QStringLiteral("SymbolTree")}; static const QString CONFIG_SYMBOL_EXPAND {QStringLiteral("SymbolExpand")}; static const QString CONFIG_SYMBOL_SORT {QStringLiteral("SymbolSort")}; static const QString CONFIG_COMPLETION_DOC {QStringLiteral("CompletionDocumentation")}; static const QString CONFIG_REFERENCES_DECLARATION {QStringLiteral("ReferencesDeclaration")}; static const QString CONFIG_AUTO_HOVER {QStringLiteral("AutoHover")}; static const QString CONFIG_TYPE_FORMATTING {QStringLiteral("TypeFormatting")}; static const QString CONFIG_INCREMENTAL_SYNC {QStringLiteral("IncrementalSync")}; static const QString CONFIG_DIAGNOSTICS {QStringLiteral("Diagnostics")}; static const QString CONFIG_DIAGNOSTICS_HIGHLIGHT {QStringLiteral("DiagnosticsHighlight")}; static const QString CONFIG_DIAGNOSTICS_MARK {QStringLiteral("DiagnosticsMark")}; +static const QString CONFIG_MESSAGES {QStringLiteral("Messages")}; +static const QString CONFIG_MESSAGES_AUTO_SWITCH {QStringLiteral("MessagesAutoSwitch")}; static const QString CONFIG_SERVER_CONFIG {QStringLiteral("ServerConfiguration")}; static const QString CONFIG_SEMANTIC_HIGHLIGHTING {QStringLiteral("SemanticHighlighting")}; K_PLUGIN_FACTORY_WITH_JSON(LSPClientPluginFactory, "lspclientplugin.json", registerPlugin();) LSPClientPlugin::LSPClientPlugin(QObject *parent, const QList &) : KTextEditor::Plugin(parent) , m_settingsPath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + QStringLiteral("/lspclient")) , m_defaultConfigPath(QUrl::fromLocalFile(m_settingsPath + QStringLiteral("/settings.json"))) { // ensure settings path exist, for e.g. local settings.json QDir().mkpath(m_settingsPath); /** * handle plugin verbosity * the m_debugMode will be used to e.g. set debug level for started clangd, too */ m_debugMode = (qgetenv("LSPCLIENT_DEBUG") == QByteArray("1")); if (!m_debugMode) { QLoggingCategory::setFilterRules(QStringLiteral("katelspclientplugin.debug=false\nkatelspclientplugin.info=false")); } else { QLoggingCategory::setFilterRules(QStringLiteral("katelspclientplugin.debug=true\nkatelspclientplugin.info=true")); } readConfig(); } LSPClientPlugin::~LSPClientPlugin() { } QObject *LSPClientPlugin::createView(KTextEditor::MainWindow *mainWindow) { return LSPClientPluginView::new_(this, mainWindow); } int LSPClientPlugin::configPages() const { return 1; } KTextEditor::ConfigPage *LSPClientPlugin::configPage(int number, QWidget *parent) { if (number != 0) { return nullptr; } return new LSPClientConfigPage(parent, this); } void LSPClientPlugin::readConfig() { KConfigGroup config(KSharedConfig::openConfig(), CONFIG_LSPCLIENT); m_symbolDetails = config.readEntry(CONFIG_SYMBOL_DETAILS, false); m_symbolTree = config.readEntry(CONFIG_SYMBOL_TREE, true); m_symbolExpand = config.readEntry(CONFIG_SYMBOL_EXPAND, true); m_symbolSort = config.readEntry(CONFIG_SYMBOL_SORT, false); m_complDoc = config.readEntry(CONFIG_COMPLETION_DOC, true); m_refDeclaration = config.readEntry(CONFIG_REFERENCES_DECLARATION, true); m_autoHover = config.readEntry(CONFIG_AUTO_HOVER, true); m_onTypeFormatting = config.readEntry(CONFIG_TYPE_FORMATTING, false); m_incrementalSync = config.readEntry(CONFIG_INCREMENTAL_SYNC, false); m_diagnostics = config.readEntry(CONFIG_DIAGNOSTICS, true); m_diagnosticsHighlight = config.readEntry(CONFIG_DIAGNOSTICS_HIGHLIGHT, true); m_diagnosticsMark = config.readEntry(CONFIG_DIAGNOSTICS_MARK, true); + m_messages = config.readEntry(CONFIG_MESSAGES, true); + m_messagesAutoSwitch = config.readEntry(CONFIG_MESSAGES_AUTO_SWITCH, 1); m_configPath = config.readEntry(CONFIG_SERVER_CONFIG, QUrl()); m_semanticHighlighting = config.readEntry(CONFIG_SEMANTIC_HIGHLIGHTING, false); emit update(); } void LSPClientPlugin::writeConfig() const { KConfigGroup config(KSharedConfig::openConfig(), CONFIG_LSPCLIENT); config.writeEntry(CONFIG_SYMBOL_DETAILS, m_symbolDetails); config.writeEntry(CONFIG_SYMBOL_TREE, m_symbolTree); config.writeEntry(CONFIG_SYMBOL_EXPAND, m_symbolExpand); config.writeEntry(CONFIG_SYMBOL_SORT, m_symbolSort); config.writeEntry(CONFIG_COMPLETION_DOC, m_complDoc); config.writeEntry(CONFIG_REFERENCES_DECLARATION, m_refDeclaration); config.writeEntry(CONFIG_AUTO_HOVER, m_autoHover); config.writeEntry(CONFIG_TYPE_FORMATTING, m_onTypeFormatting); config.writeEntry(CONFIG_INCREMENTAL_SYNC, m_incrementalSync); config.writeEntry(CONFIG_DIAGNOSTICS, m_diagnostics); config.writeEntry(CONFIG_DIAGNOSTICS_HIGHLIGHT, m_diagnosticsHighlight); config.writeEntry(CONFIG_DIAGNOSTICS_MARK, m_diagnosticsMark); + config.writeEntry(CONFIG_MESSAGES, m_messages); + config.writeEntry(CONFIG_MESSAGES_AUTO_SWITCH, m_messagesAutoSwitch); config.writeEntry(CONFIG_SERVER_CONFIG, m_configPath); config.writeEntry(CONFIG_SEMANTIC_HIGHLIGHTING, m_semanticHighlighting); emit update(); } #include "lspclientplugin.moc" diff --git a/addons/lspclient/lspclientplugin.h b/addons/lspclient/lspclientplugin.h index e57444efd..5dfc36e22 100644 --- a/addons/lspclient/lspclientplugin.h +++ b/addons/lspclient/lspclientplugin.h @@ -1,87 +1,89 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts 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. */ #ifndef LSPCLIENTPLUGIN_H #define LSPCLIENTPLUGIN_H #include #include #include #include class LSPClientPlugin : public KTextEditor::Plugin { Q_OBJECT public: explicit LSPClientPlugin(QObject *parent = nullptr, const QList & = QList()); ~LSPClientPlugin() override; QObject *createView(KTextEditor::MainWindow *mainWindow) override; int configPages() const override; KTextEditor::ConfigPage *configPage(int number = 0, QWidget *parent = nullptr) override; void readConfig(); void writeConfig() const; // path for local setting files, auto-created on load const QString m_settingsPath; // default config path const QUrl m_defaultConfigPath; // settings bool m_symbolDetails; bool m_symbolExpand; bool m_symbolTree; bool m_symbolSort; bool m_complDoc; bool m_refDeclaration; bool m_diagnostics; bool m_diagnosticsHighlight; bool m_diagnosticsMark; + bool m_messages; + int m_messagesAutoSwitch; bool m_autoHover; bool m_onTypeFormatting; bool m_incrementalSync; QUrl m_configPath; bool m_semanticHighlighting; // debug mode? bool m_debugMode = false; // get current config path QUrl configPath() const { return m_configPath.isEmpty() ? m_defaultConfigPath : m_configPath; } private: Q_SIGNALS: // signal settings update void update() const; }; #endif diff --git a/addons/lspclient/lspclientpluginview.cpp b/addons/lspclient/lspclientpluginview.cpp index a7437357b..1c848fd40 100644 --- a/addons/lspclient/lspclientpluginview.cpp +++ b/addons/lspclient/lspclientpluginview.cpp @@ -1,1837 +1,1971 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts 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 "lspclientpluginview.h" #include "lspclientcompletion.h" #include "lspclienthover.h" #include "lspclientplugin.h" #include "lspclientservermanager.h" #include "lspclientsymbolview.h" #include "lspclient_debug.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 +#include #include #include #include #include #include #include namespace RangeData { enum { // preserve UserRole for generic use where needed FileUrlRole = Qt::UserRole + 1, RangeRole, KindRole, }; class KindEnum { public: enum _kind { Text = static_cast(LSPDocumentHighlightKind::Text), Read = static_cast(LSPDocumentHighlightKind::Read), Write = static_cast(LSPDocumentHighlightKind::Write), Error = 10 + static_cast(LSPDiagnosticSeverity::Error), Warning = 10 + static_cast(LSPDiagnosticSeverity::Warning), Information = 10 + static_cast(LSPDiagnosticSeverity::Information), Hint = 10 + static_cast(LSPDiagnosticSeverity::Hint), Related }; KindEnum(int v) { m_value = _kind(v); } KindEnum(LSPDocumentHighlightKind hl) : KindEnum(static_cast<_kind>(hl)) { } KindEnum(LSPDiagnosticSeverity sev) : KindEnum(_kind(10 + static_cast(sev))) { } operator _kind() { return m_value; } private: _kind m_value; }; static constexpr KTextEditor::MarkInterface::MarkTypes markType = KTextEditor::MarkInterface::markType31; static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagError = KTextEditor::MarkInterface::Error; static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagWarning = KTextEditor::MarkInterface::Warning; static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagOther = KTextEditor::MarkInterface::markType30; static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagAll = KTextEditor::MarkInterface::MarkTypes(markTypeDiagError | markTypeDiagWarning | markTypeDiagOther); } static QIcon diagnosticsIcon(LSPDiagnosticSeverity severity) { // clang-format off #define RETURN_CACHED_ICON(name, fallbackname) \ { \ static QIcon icon(QIcon::fromTheme(QStringLiteral(name), \ QIcon::fromTheme(QStringLiteral(fallbackname)))); \ return icon; \ } // clang-format on switch (severity) { case LSPDiagnosticSeverity::Error: RETURN_CACHED_ICON("data-error", "dialog-error") case LSPDiagnosticSeverity::Warning: RETURN_CACHED_ICON("data-warning", "dialog-warning") case LSPDiagnosticSeverity::Information: case LSPDiagnosticSeverity::Hint: RETURN_CACHED_ICON("data-information", "dialog-information") default: break; } return QIcon(); } static QIcon codeActionIcon() { static QIcon icon(QIcon::fromTheme(QStringLiteral("insert-text"))); return icon; } KTextEditor::Document *findDocument(KTextEditor::MainWindow *mainWindow, const QUrl &url) { auto views = mainWindow->views(); for (const auto v : views) { auto doc = v->document(); if (doc && doc->url() == url) return doc; } return nullptr; } // helper to read lines from unopened documents // lightweight and does not require additional symbols class FileLineReader { QFile file; int lastLineNo = -1; QString lastLine; public: FileLineReader(const QUrl &url) : file(url.path()) { file.open(QIODevice::ReadOnly); } // called with non-descending lineno QString line(int lineno) { if (lineno == lastLineNo) { return lastLine; } while (file.isOpen() && !file.atEnd()) { auto line = file.readLine(); if (++lastLineNo == lineno) { QTextCodec::ConverterState state; QTextCodec *codec = QTextCodec::codecForName("UTF-8"); QString text = codec->toUnicode(line.constData(), line.size(), &state); if (state.invalidChars > 0) { text = QString::fromLatin1(line); } while (text.size() && text.at(text.size() - 1).isSpace()) text.chop(1); lastLine = text; return text; } } return QString(); } }; class LSPClientActionView : public QObject { Q_OBJECT typedef LSPClientActionView self_type; LSPClientPlugin *m_plugin; KTextEditor::MainWindow *m_mainWindow; KXMLGUIClient *m_client; QSharedPointer m_serverManager; QScopedPointer m_viewTracker; QScopedPointer m_completion; QScopedPointer m_hover; QScopedPointer m_symbolView; QPointer m_findDef; QPointer m_findDecl; QPointer m_findRef; QPointer m_triggerHighlight; QPointer m_triggerHover; QPointer m_triggerFormat; QPointer m_triggerRename; QPointer m_complDocOn; QPointer m_refDeclaration; QPointer m_autoHover; QPointer m_onTypeFormatting; QPointer m_incrementalSync; QPointer m_diagnostics; QPointer m_diagnosticsHighlight; QPointer m_diagnosticsMark; QPointer m_diagnosticsSwitch; QPointer m_diagnosticsCloseNon; + QPointer m_messages; + QPointer m_messagesAutoSwitch; + QPointer m_messagesSwitch; QPointer m_restartServer; QPointer m_restartAll; // toolview QScopedPointer m_toolView; QPointer m_tabWidget; // applied search ranges typedef QMultiHash RangeCollection; RangeCollection m_ranges; QHash>> m_semanticHighlightRanges; // applied marks typedef QSet DocumentCollection; DocumentCollection m_marks; // modelis either owned by tree added to tabwidget or owned here QScopedPointer m_ownedModel; // in either case, the model that directs applying marks/ranges QPointer m_markModel; // goto definition and declaration jump list is more a menu than a // search result, so let's not keep adding new tabs for those // previous tree for definition result QPointer m_defTree; // ... and for declaration QPointer m_declTree; // diagnostics tab QPointer m_diagnosticsTree; // tree widget is either owned here or by tab QScopedPointer m_diagnosticsTreeOwn; QScopedPointer m_diagnosticsModel; // diagnostics ranges RangeCollection m_diagnosticsRanges; // and marks DocumentCollection m_diagnosticsMarks; + using MessagesWidget = QPlainTextEdit; + // messages tab + QPointer m_messagesView; + // widget is either owned here or by tab + QScopedPointer m_messagesViewOwn; + // views on which completions have been registered QSet m_completionViews; // views on which hovers have been registered QSet m_hoverViews; // outstanding request LSPClientServer::RequestHandle m_handle; // timeout on request bool m_req_timeout = false; // accept incoming applyEdit bool m_accept_edit = false; // characters to trigger format request QVector m_onTypeFormattingTriggers; KActionCollection *actionCollection() const { return m_client->actionCollection(); } public: LSPClientActionView(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, KXMLGUIClient *client, QSharedPointer serverManager) : QObject(mainWin) , m_plugin(plugin) , m_mainWindow(mainWin) , m_client(client) , m_serverManager(std::move(serverManager)) , m_completion(LSPClientCompletion::new_(m_serverManager)) , m_hover(LSPClientHover::new_(m_serverManager)) , m_symbolView(LSPClientSymbolView::new_(plugin, mainWin, m_serverManager)) { connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::updateState); connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &self_type::handleEsc); connect(m_serverManager.data(), &LSPClientServerManager::serverChanged, this, &self_type::updateState); + connect(m_serverManager.data(), &LSPClientServerManager::showMessage, this, &self_type::onShowMessage); m_findDef = actionCollection()->addAction(QStringLiteral("lspclient_find_definition"), this, &self_type::goToDefinition); m_findDef->setText(i18n("Go to Definition")); m_findDecl = actionCollection()->addAction(QStringLiteral("lspclient_find_declaration"), this, &self_type::goToDeclaration); m_findDecl->setText(i18n("Go to Declaration")); m_findRef = actionCollection()->addAction(QStringLiteral("lspclient_find_references"), this, &self_type::findReferences); m_findRef->setText(i18n("Find References")); m_triggerHighlight = actionCollection()->addAction(QStringLiteral("lspclient_highlight"), this, &self_type::highlight); m_triggerHighlight->setText(i18n("Highlight")); // perhaps hover suggests to do so on mouse-over, // but let's just use a (convenient) action/shortcut for it m_triggerHover = actionCollection()->addAction(QStringLiteral("lspclient_hover"), this, &self_type::hover); m_triggerHover->setText(i18n("Hover")); m_triggerFormat = actionCollection()->addAction(QStringLiteral("lspclient_format"), this, &self_type::format); m_triggerFormat->setText(i18n("Format")); m_triggerRename = actionCollection()->addAction(QStringLiteral("lspclient_rename"), this, &self_type::rename); m_triggerRename->setText(i18n("Rename")); // general options m_complDocOn = actionCollection()->addAction(QStringLiteral("lspclient_completion_doc"), this, &self_type::displayOptionChanged); m_complDocOn->setText(i18n("Show selected completion documentation")); m_complDocOn->setCheckable(true); m_refDeclaration = actionCollection()->addAction(QStringLiteral("lspclient_references_declaration"), this, &self_type::displayOptionChanged); m_refDeclaration->setText(i18n("Include declaration in references")); m_refDeclaration->setCheckable(true); m_autoHover = actionCollection()->addAction(QStringLiteral("lspclient_auto_hover"), this, &self_type::displayOptionChanged); m_autoHover->setText(i18n("Show hover information")); m_autoHover->setCheckable(true); m_onTypeFormatting = actionCollection()->addAction(QStringLiteral("lspclient_type_formatting"), this, &self_type::displayOptionChanged); m_onTypeFormatting->setText(i18n("Format on typing")); m_onTypeFormatting->setCheckable(true); m_incrementalSync = actionCollection()->addAction(QStringLiteral("lspclient_incremental_sync"), this, &self_type::displayOptionChanged); m_incrementalSync->setText(i18n("Incremental document synchronization")); m_incrementalSync->setCheckable(true); // diagnostics m_diagnostics = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics"), this, &self_type::displayOptionChanged); m_diagnostics->setText(i18n("Show diagnostics notifications")); m_diagnostics->setCheckable(true); m_diagnosticsHighlight = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_highlight"), this, &self_type::displayOptionChanged); m_diagnosticsHighlight->setText(i18n("Show diagnostics highlights")); m_diagnosticsHighlight->setCheckable(true); m_diagnosticsMark = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_mark"), this, &self_type::displayOptionChanged); m_diagnosticsMark->setText(i18n("Show diagnostics marks")); m_diagnosticsMark->setCheckable(true); m_diagnosticsSwitch = actionCollection()->addAction(QStringLiteral("lspclient_diagnostic_switch"), this, &self_type::switchToDiagnostics); m_diagnosticsSwitch->setText(i18n("Switch to diagnostics tab")); m_diagnosticsCloseNon = actionCollection()->addAction(QStringLiteral("lspclient_diagnostic_close_non"), this, &self_type::closeNonDiagnostics); m_diagnosticsCloseNon->setText(i18n("Close all non-diagnostics tabs")); + // messages + m_messages = actionCollection()->addAction(QStringLiteral("lspclient_messages"), this, &self_type::displayOptionChanged); + m_messages->setText(i18n("Show messages")); + m_messages->setCheckable(true); + m_messagesAutoSwitch = new KSelectAction(i18n("Switch to messages tab upon message level"), this); + actionCollection()->addAction(QStringLiteral("lspclient_messages_auto_switch"), m_messagesAutoSwitch); + const QStringList list {i18nc("@info", "Never"), i18nc("@info", "Error"), i18nc("@info", "Warning"), i18nc("@info", "Information"), i18nc("@info", "Log")}; + m_messagesAutoSwitch->setItems(list); + m_messagesSwitch = actionCollection()->addAction(QStringLiteral("lspclient_messages_switch"), this, &self_type::switchToMessages); + m_messagesSwitch->setText(i18n("Switch to messages tab")); + // server control m_restartServer = actionCollection()->addAction(QStringLiteral("lspclient_restart_server"), this, &self_type::restartCurrent); m_restartServer->setText(i18n("Restart LSP Server")); m_restartAll = actionCollection()->addAction(QStringLiteral("lspclient_restart_all"), this, &self_type::restartAll); m_restartAll->setText(i18n("Restart All LSP Servers")); // popup menu auto menu = new KActionMenu(i18n("LSP Client"), this); actionCollection()->addAction(QStringLiteral("popup_lspclient"), menu); menu->addAction(m_findDef); menu->addAction(m_findDecl); menu->addAction(m_findRef); menu->addAction(m_triggerHighlight); menu->addAction(m_triggerHover); menu->addAction(m_triggerFormat); menu->addAction(m_triggerRename); menu->addSeparator(); menu->addAction(m_complDocOn); menu->addAction(m_refDeclaration); menu->addAction(m_autoHover); menu->addAction(m_onTypeFormatting); menu->addAction(m_incrementalSync); menu->addSeparator(); menu->addAction(m_diagnostics); menu->addAction(m_diagnosticsHighlight); menu->addAction(m_diagnosticsMark); menu->addAction(m_diagnosticsSwitch); menu->addAction(m_diagnosticsCloseNon); menu->addSeparator(); + menu->addAction(m_messages); + menu->addAction(m_messagesAutoSwitch); + menu->addAction(m_messagesSwitch); + menu->addSeparator(); menu->addAction(m_restartServer); menu->addAction(m_restartAll); // sync with plugin settings if updated connect(m_plugin, &LSPClientPlugin::update, this, &self_type::configUpdated); // toolview m_toolView.reset(mainWin->createToolView(plugin, QStringLiteral("kate_lspclient"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("application-x-ms-dos-executable")), i18n("LSP Client"))); m_tabWidget = new QTabWidget(m_toolView.data()); m_toolView->layout()->addWidget(m_tabWidget); m_tabWidget->setFocusPolicy(Qt::NoFocus); m_tabWidget->setTabsClosable(true); KAcceleratorManager::setNoAccel(m_tabWidget); connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &self_type::tabCloseRequested); + connect(m_tabWidget, &QTabWidget::currentChanged, this, &self_type::tabChanged); // diagnostics tab m_diagnosticsTree = new QTreeView(); m_diagnosticsTree->setAlternatingRowColors(true); m_diagnosticsTreeOwn.reset(m_diagnosticsTree); m_diagnosticsModel.reset(new QStandardItemModel()); m_diagnosticsModel->setColumnCount(1); m_diagnosticsTree->setModel(m_diagnosticsModel.data()); configureTreeView(m_diagnosticsTree); connect(m_diagnosticsTree, &QTreeView::clicked, this, &self_type::goToItemLocation); connect(m_diagnosticsTree, &QTreeView::doubleClicked, this, &self_type::triggerCodeAction); + // messages tab + m_messagesView = new QPlainTextEdit(); + m_messagesView->setMaximumBlockCount(100); + m_messagesView->setReadOnly(true); + m_messagesViewOwn.reset(m_messagesView); + // track position in view to sync diagnostics list m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 0, 500)); connect(m_viewTracker.data(), &LSPClientViewTracker::newState, this, &self_type::onViewState); configUpdated(); updateState(); } ~LSPClientActionView() override { // unregister all code-completion providers, else we might crash for (auto view : qAsConst(m_completionViews)) { qobject_cast(view)->unregisterCompletionModel(m_completion.data()); } // unregister all text-hint providers, else we might crash for (auto view : qAsConst(m_hoverViews)) { qobject_cast(view)->unregisterTextHintProvider(m_hover.data()); } clearAllLocationMarks(); clearAllDiagnosticsMarks(); } void configureTreeView(QTreeView *treeView) { treeView->setHeaderHidden(true); treeView->setFocusPolicy(Qt::NoFocus); treeView->setLayoutDirection(Qt::LeftToRight); treeView->setSortingEnabled(false); treeView->setEditTriggers(QAbstractItemView::NoEditTriggers); // context menu treeView->setContextMenuPolicy(Qt::CustomContextMenu); auto menu = new QMenu(treeView); menu->addAction(i18n("Expand All"), treeView, &QTreeView::expandAll); menu->addAction(i18n("Collapse All"), treeView, &QTreeView::collapseAll); auto h = [menu](const QPoint &) { menu->popup(QCursor::pos()); }; connect(treeView, &QTreeView::customContextMenuRequested, h); } void displayOptionChanged() { m_diagnosticsHighlight->setEnabled(m_diagnostics->isChecked()); m_diagnosticsMark->setEnabled(m_diagnostics->isChecked()); - auto index = m_tabWidget->indexOf(m_diagnosticsTree); + // messages tab should go first + int messagesIndex = m_tabWidget->indexOf(m_messagesView); + if (m_messages->isChecked() && m_messagesViewOwn) { + m_tabWidget->insertTab(0, m_messagesView, i18nc("@title:tab", "Messages")); + messagesIndex = 0; + m_messagesViewOwn.take(); + } else if (!m_messages->isChecked() && !m_messagesViewOwn) { + m_messagesViewOwn.reset(m_messagesView); + m_tabWidget->removeTab(messagesIndex); + messagesIndex = -1; + } + // diagnstics tab next + int diagnosticsIndex = m_tabWidget->indexOf(m_diagnosticsTree); // setTabEnabled may still show it ... so let's be more forceful if (m_diagnostics->isChecked() && m_diagnosticsTreeOwn) { m_diagnosticsTreeOwn.take(); - m_tabWidget->insertTab(0, m_diagnosticsTree, i18nc("@title:tab", "Diagnostics")); + m_tabWidget->insertTab(messagesIndex + 1, m_diagnosticsTree, i18nc("@title:tab", "Diagnostics")); } else if (!m_diagnostics->isChecked() && !m_diagnosticsTreeOwn) { m_diagnosticsTreeOwn.reset(m_diagnosticsTree); - m_tabWidget->removeTab(index); + m_tabWidget->removeTab(diagnosticsIndex); } m_diagnosticsSwitch->setEnabled(m_diagnostics->isChecked()); m_serverManager->setIncrementalSync(m_incrementalSync->isChecked()); updateState(); } void configUpdated() { if (m_complDocOn) m_complDocOn->setChecked(m_plugin->m_complDoc); if (m_refDeclaration) m_refDeclaration->setChecked(m_plugin->m_refDeclaration); if (m_autoHover) m_autoHover->setChecked(m_plugin->m_autoHover); if (m_onTypeFormatting) m_onTypeFormatting->setChecked(m_plugin->m_onTypeFormatting); if (m_incrementalSync) m_incrementalSync->setChecked(m_plugin->m_incrementalSync); if (m_diagnostics) m_diagnostics->setChecked(m_plugin->m_diagnostics); if (m_diagnosticsHighlight) m_diagnosticsHighlight->setChecked(m_plugin->m_diagnosticsHighlight); if (m_diagnosticsMark) m_diagnosticsMark->setChecked(m_plugin->m_diagnosticsMark); + if (m_messages) + m_messages->setChecked(m_plugin->m_messages); + if (m_messagesAutoSwitch) + m_messagesAutoSwitch->setCurrentItem(m_plugin->m_messagesAutoSwitch); displayOptionChanged(); } void restartCurrent() { KTextEditor::View *activeView = m_mainWindow->activeView(); auto server = m_serverManager->findServer(activeView); if (server) m_serverManager->restart(server.data()); } void restartAll() { m_serverManager->restart(nullptr); } static void clearMarks(KTextEditor::Document *doc, RangeCollection &ranges, DocumentCollection &docs, uint markType) { KTextEditor::MarkInterface *iface = docs.contains(doc) ? qobject_cast(doc) : nullptr; if (iface) { const QHash marks = iface->marks(); QHashIterator i(marks); while (i.hasNext()) { i.next(); if (i.value()->type & markType) { iface->removeMark(i.value()->line, markType); } } docs.remove(doc); } for (auto it = ranges.find(doc); it != ranges.end() && it.key() == doc;) { delete it.value(); it = ranges.erase(it); } } static void clearMarks(RangeCollection &ranges, DocumentCollection &docs, uint markType) { while (!ranges.empty()) { clearMarks(ranges.begin().key(), ranges, docs, markType); } } Q_SLOT void clearAllMarks(KTextEditor::Document *doc) { clearMarks(doc, m_ranges, m_marks, RangeData::markType); clearMarks(doc, m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll); } void clearAllLocationMarks() { clearMarks(m_ranges, m_marks, RangeData::markType); // no longer add any again m_ownedModel.reset(); m_markModel.clear(); } void clearAllDiagnosticsMarks() { clearMarks(m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll); } void addMarks(KTextEditor::Document *doc, QStandardItem *item, RangeCollection *ranges, DocumentCollection *docs) { Q_ASSERT(item); KTextEditor::MovingInterface *miface = qobject_cast(doc); Q_ASSERT(miface); #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,69,0) KTextEditor::MarkInterfaceV2 *iface = qobject_cast(doc); #else KTextEditor::MarkInterface *iface = qobject_cast(doc); #endif Q_ASSERT(iface); KTextEditor::View *activeView = m_mainWindow->activeView(); KTextEditor::ConfigInterface *ciface = qobject_cast(activeView); auto url = item->data(RangeData::FileUrlRole).toUrl(); if (url != doc->url()) return; KTextEditor::Range range = item->data(RangeData::RangeRole).value(); auto line = range.start().line(); RangeData::KindEnum kind = RangeData::KindEnum(item->data(RangeData::KindRole).toInt()); KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); bool enabled = m_diagnostics && m_diagnostics->isChecked() && m_diagnosticsHighlight && m_diagnosticsHighlight->isChecked(); KTextEditor::MarkInterface::MarkTypes markType = RangeData::markType; switch (kind) { case RangeData::KindEnum::Text: { // well, it's a bit like searching for something, so re-use that color QColor rangeColor = Qt::yellow; if (ciface) { rangeColor = ciface->configValue(QStringLiteral("search-highlight-color")).value(); } attr->setBackground(rangeColor); enabled = true; break; } // FIXME are there any symbolic/configurable ways to pick these colors? case RangeData::KindEnum::Read: attr->setBackground(Qt::green); enabled = true; break; case RangeData::KindEnum::Write: attr->setBackground(Qt::red); enabled = true; break; // use underlining for diagnostics to avoid lots of fancy flickering case RangeData::KindEnum::Error: markType = RangeData::markTypeDiagError; attr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); attr->setUnderlineColor(Qt::red); break; case RangeData::KindEnum::Warning: markType = RangeData::markTypeDiagWarning; attr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); attr->setUnderlineColor(QColor(255, 128, 0)); break; case RangeData::KindEnum::Information: case RangeData::KindEnum::Hint: case RangeData::KindEnum::Related: markType = RangeData::markTypeDiagOther; attr->setUnderlineStyle(QTextCharFormat::DashUnderline); attr->setUnderlineColor(Qt::blue); break; } if (activeView) { attr->setForeground(activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color()); } // highlight the range if (enabled && ranges) { KTextEditor::MovingRange *mr = miface->newMovingRange(range); mr->setAttribute(attr); mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection mr->setAttributeOnlyForViews(true); ranges->insert(doc, mr); } // add match mark for range const int ps = 32; bool handleClick = true; enabled = m_diagnostics && m_diagnostics->isChecked() && m_diagnosticsMark && m_diagnosticsMark->isChecked(); switch (markType) { case RangeData::markType: iface->setMarkDescription(markType, i18n("RangeHighLight")); #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,69,0) iface->setMarkIcon(markType, QIcon()); #else iface->setMarkPixmap(markType, QIcon().pixmap(0, 0)); #endif handleClick = false; enabled = true; break; case RangeData::markTypeDiagError: iface->setMarkDescription(markType, i18n("Error")); #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,69,0) iface->setMarkIcon(markType, diagnosticsIcon(LSPDiagnosticSeverity::Error)); #else iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Error).pixmap(ps, ps)); #endif break; case RangeData::markTypeDiagWarning: iface->setMarkDescription(markType, i18n("Warning")); #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,69,0) iface->setMarkIcon(markType, diagnosticsIcon(LSPDiagnosticSeverity::Warning)); #else iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Warning).pixmap(ps, ps)); #endif break; case RangeData::markTypeDiagOther: iface->setMarkDescription(markType, i18n("Information")); #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,69,0) iface->setMarkIcon(markType, diagnosticsIcon(LSPDiagnosticSeverity::Information)); #else iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Information).pixmap(ps, ps)); #endif break; default: Q_ASSERT(false); break; } if (enabled && docs) { iface->addMark(line, markType); docs->insert(doc); } // ensure runtime match connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearAllMarks(KTextEditor::Document *)), Qt::UniqueConnection); connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearAllMarks(KTextEditor::Document *)), Qt::UniqueConnection); if (handleClick) { connect(doc, SIGNAL(markClicked(KTextEditor::Document *, KTextEditor::Mark, bool &)), this, SLOT(onMarkClicked(KTextEditor::Document *, KTextEditor::Mark, bool &)), Qt::UniqueConnection); } } void addMarksRec(KTextEditor::Document *doc, QStandardItem *item, RangeCollection *ranges, DocumentCollection *docs) { Q_ASSERT(item); addMarks(doc, item, ranges, docs); for (int i = 0; i < item->rowCount(); ++i) { addMarksRec(doc, item->child(i), ranges, docs); } } void addMarks(KTextEditor::Document *doc, QStandardItemModel *treeModel, RangeCollection &ranges, DocumentCollection &docs) { // check if already added auto oranges = ranges.contains(doc) ? nullptr : &ranges; auto odocs = docs.contains(doc) ? nullptr : &docs; if (!oranges && !odocs) return; Q_ASSERT(treeModel); addMarksRec(doc, treeModel->invisibleRootItem(), oranges, odocs); } void goToDocumentLocation(const QUrl &uri, int line, int column) { KTextEditor::View *activeView = m_mainWindow->activeView(); if (!activeView || uri.isEmpty() || line < 0 || column < 0) return; KTextEditor::Document *document = activeView->document(); KTextEditor::Cursor cdef(line, column); if (document && uri == document->url()) { activeView->setCursorPosition(cdef); } else { KTextEditor::View *view = m_mainWindow->openUrl(uri); if (view) { view->setCursorPosition(cdef); } } } void goToItemLocation(const QModelIndex &index) { auto url = index.data(RangeData::FileUrlRole).toUrl(); auto start = index.data(RangeData::RangeRole).value().start(); goToDocumentLocation(url, start.line(), start.column()); } // custom item subclass that captures additional attributes; // a bit more convenient than the variant/role way struct DiagnosticItem : public QStandardItem { LSPDiagnostic m_diagnostic; LSPCodeAction m_codeAction; QSharedPointer m_snapshot; DiagnosticItem(const LSPDiagnostic &d) : m_diagnostic(d) { } DiagnosticItem(const LSPCodeAction &c, QSharedPointer s) : m_codeAction(c) , m_snapshot(std::move(s)) { m_diagnostic.range = LSPRange::invalid(); } bool isCodeAction() { return !m_diagnostic.range.isValid() && m_codeAction.title.size(); } }; // double click on: // diagnostic item -> request and add actions (below item) // code action -> perform action (literal edit and/or execute command) // (execution of command may lead to an applyEdit request from server) void triggerCodeAction(const QModelIndex &index) { KTextEditor::View *activeView = m_mainWindow->activeView(); QPointer document = activeView->document(); auto server = m_serverManager->findServer(activeView); auto it = dynamic_cast(m_diagnosticsModel->itemFromIndex(index)); if (!server || !document || !it) return; // click on an action ? if (it->isCodeAction()) { auto &action = it->m_codeAction; // apply edit before command applyWorkspaceEdit(action.edit, it->m_snapshot.data()); auto &command = action.command; if (command.command.size()) { // accept edit requests that may be sent to execute command m_accept_edit = true; // but only for a short time QTimer::singleShot(2000, this, [this] { m_accept_edit = false; }); server->executeCommand(command.command, command.arguments); } // diagnostics are likely updated soon, but might be clicked again in meantime // so clear once executed, so not executed again action.edit.changes.clear(); action.command.command.clear(); return; } // only engage action if // * active document matches diagnostic document // * if really clicked a diagnostic item // (which is the case as it != nullptr and not a code action) // * if no code action invoked and added already // (note; related items are also children) auto url = it->data(RangeData::FileUrlRole).toUrl(); if (url != document->url() || it->data(Qt::UserRole).toBool()) return; // store some things to find item safely later on QPersistentModelIndex pindex(index); QSharedPointer snapshot(m_serverManager->snapshot(server.data())); auto h = [this, url, snapshot, pindex](const QList &actions) { if (!pindex.isValid()) return; auto child = m_diagnosticsModel->itemFromIndex(pindex); if (!child) return; // add actions below diagnostic item for (const auto &action : actions) { auto item = new DiagnosticItem(action, snapshot); child->appendRow(item); auto text = action.kind.size() ? QStringLiteral("[%1] %2").arg(action.kind).arg(action.title) : action.title; item->setData(text, Qt::DisplayRole); item->setData(codeActionIcon(), Qt::DecorationRole); } m_diagnosticsTree->setExpanded(child->index(), true); // mark actions added child->setData(true, Qt::UserRole); }; auto range = activeView->selectionRange(); if (!range.isValid()) { range = document->documentRange(); } server->documentCodeAction(url, range, {}, {it->m_diagnostic}, this, h); } void tabCloseRequested(int index) { auto widget = m_tabWidget->widget(index); - if (widget != m_diagnosticsTree) { + if (widget != m_diagnosticsTree && widget != m_messagesView) { if (m_markModel && widget == m_markModel->parent()) { clearAllLocationMarks(); } delete widget; } } + void tabChanged(int index) + { + // reset to regular foreground + m_tabWidget->tabBar()->setTabTextColor(index, QColor()); + } + void switchToDiagnostics() { m_tabWidget->setCurrentWidget(m_diagnosticsTree); m_mainWindow->showToolView(m_toolView.data()); } + void switchToMessages() + { + m_tabWidget->setCurrentWidget(m_messagesView); + m_mainWindow->showToolView(m_toolView.data()); + } + void closeNonDiagnostics() { for (int i = 0; i < m_tabWidget->count();) { if (m_tabWidget->widget(i) != m_diagnosticsTree) { tabCloseRequested(i); } else { ++i; } } } // local helper to overcome some differences in LSP types struct RangeItem { QUrl uri; LSPRange range; LSPDocumentHighlightKind kind; }; static bool compareRangeItem(const RangeItem &a, const RangeItem &b) { return (a.uri < b.uri) || ((a.uri == b.uri) && a.range < b.range); } // provide Qt::DisplayRole (text) line lazily; // only find line's text content when so requested // This may then involve opening reading some file, at which time // all items for that file will be resolved in one go. struct LineItem : public QStandardItem { KTextEditor::MainWindow *m_mainWindow; LineItem(KTextEditor::MainWindow *mainWindow) : m_mainWindow(mainWindow) { } QVariant data(int role = Qt::UserRole + 1) const override { auto rootItem = this->parent(); if (role != Qt::DisplayRole || !rootItem) { return QStandardItem::data(role); } auto line = data(Qt::UserRole); // either of these mean we tried to obtain line already if (line.isValid() || rootItem->data(RangeData::KindRole).toBool()) { return QStandardItem::data(role).toString().append(line.toString()); } KTextEditor::Document *doc = nullptr; QScopedPointer fr; for (int i = 0; i < rootItem->rowCount(); i++) { auto child = rootItem->child(i); if (i == 0) { auto url = child->data(RangeData::FileUrlRole).toUrl(); doc = findDocument(m_mainWindow, url); if (!doc) { fr.reset(new FileLineReader(url)); } } auto lineno = child->data(RangeData::RangeRole).value().start().line(); auto line = doc ? doc->line(lineno) : fr->line(lineno); child->setData(line, Qt::UserRole); } // mark as processed rootItem->setData(RangeData::KindRole, true); // should work ok return data(role); } }; LSPRange transformRange(const QUrl &url, const LSPClientRevisionSnapshot &snapshot, const LSPRange &range) { KTextEditor::MovingInterface *miface; qint64 revision; auto result = range; snapshot.find(url, miface, revision); if (miface) { miface->transformRange(result, KTextEditor::MovingRange::DoNotExpand, KTextEditor::MovingRange::AllowEmpty, revision); } return result; } void fillItemRoles(QStandardItem *item, const QUrl &url, const LSPRange _range, RangeData::KindEnum kind, const LSPClientRevisionSnapshot *snapshot = nullptr) { auto range = snapshot ? transformRange(url, *snapshot, _range) : _range; item->setData(QVariant(url), RangeData::FileUrlRole); QVariant vrange; vrange.setValue(range); item->setData(vrange, RangeData::RangeRole); item->setData(static_cast(kind), RangeData::KindRole); } void makeTree(const QVector &locations, const LSPClientRevisionSnapshot *snapshot) { // group by url, assuming input is suitably sorted that way auto treeModel = new QStandardItemModel(); treeModel->setColumnCount(1); QUrl lastUrl; QStandardItem *parent = nullptr; for (const auto &loc : locations) { if (loc.uri != lastUrl) { if (parent) { parent->setText(QStringLiteral("%1: %2").arg(lastUrl.path()).arg(parent->rowCount())); } lastUrl = loc.uri; parent = new QStandardItem(); treeModel->appendRow(parent); } auto item = new LineItem(m_mainWindow); parent->appendRow(item); // add partial display data; line will be added by item later on item->setText(i18n("Line: %1: ", loc.range.start().line() + 1)); fillItemRoles(item, loc.uri, loc.range, loc.kind, snapshot); } if (parent) parent->setText(QStringLiteral("%1: %2").arg(lastUrl.path()).arg(parent->rowCount())); // plain heuristic; mark for auto-expand all when safe and/or useful to do so if (treeModel->rowCount() <= 2 || locations.size() <= 20) { treeModel->invisibleRootItem()->setData(true, RangeData::KindRole); } m_ownedModel.reset(treeModel); m_markModel = treeModel; } void showTree(const QString &title, QPointer *targetTree) { // clean up previous target if any if (targetTree && *targetTree) { int index = m_tabWidget->indexOf(*targetTree); if (index >= 0) tabCloseRequested(index); } // setup view auto treeView = new QTreeView(); configureTreeView(treeView); // transfer model from owned to tree and that in turn to tabwidget auto treeModel = m_ownedModel.take(); treeView->setModel(treeModel); treeModel->setParent(treeView); int index = m_tabWidget->addTab(treeView, title); connect(treeView, &QTreeView::clicked, this, &self_type::goToItemLocation); if (treeModel->invisibleRootItem()->data(RangeData::KindRole).toBool()) { treeView->expandAll(); } // track for later cleanup if (targetTree) *targetTree = treeView; // activate the resulting tab m_tabWidget->setCurrentIndex(index); m_mainWindow->showToolView(m_toolView.data()); } void showMessage(const QString &text, KTextEditor::Message::MessageType level) { KTextEditor::View *view = m_mainWindow->activeView(); if (!view || !view->document()) return; auto kmsg = new KTextEditor::Message(text, level); kmsg->setPosition(KTextEditor::Message::BottomInView); kmsg->setAutoHide(500); kmsg->setView(view); view->document()->postMessage(kmsg); } void handleEsc(QEvent *e) { if (!m_mainWindow) return; QKeyEvent *k = static_cast(e); if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { if (!m_ranges.empty()) { clearAllLocationMarks(); } else if (m_toolView->isVisible()) { m_mainWindow->hideToolView(m_toolView.data()); } } } template using LocationRequest = std::function; template void positionRequest(const LocationRequest &req, const Handler &h, QScopedPointer *snapshot = nullptr) { KTextEditor::View *activeView = m_mainWindow->activeView(); auto server = m_serverManager->findServer(activeView); if (!server) return; // track revision if requested if (snapshot) { snapshot->reset(m_serverManager->snapshot(server.data())); } KTextEditor::Cursor cursor = activeView->cursorPosition(); clearAllLocationMarks(); m_req_timeout = false; QTimer::singleShot(1000, this, [this] { m_req_timeout = true; }); m_handle.cancel() = req(*server, activeView->document()->url(), {cursor.line(), cursor.column()}, this, h); } QString currentWord() { KTextEditor::View *activeView = m_mainWindow->activeView(); if (activeView) { KTextEditor::Cursor cursor = activeView->cursorPosition(); return activeView->document()->wordAt(cursor); } else { return QString(); } } // some template and function type trickery here, but at least that buck stops here then ... template>> void processLocations(const QString &title, const typename utils::identity>::type &req, bool onlyshow, const std::function &itemConverter, QPointer *targetTree = nullptr) { // no capture for move only using initializers available (yet), so shared outer type // the additional level of indirection is so it can be 'filled-in' after lambda creation QSharedPointer> s(new QScopedPointer); auto h = [this, title, onlyshow, itemConverter, targetTree, s](const QList &defs) { if (defs.count() == 0) { showMessage(i18n("No results"), KTextEditor::Message::Information); } else { // convert to helper type QVector ranges; ranges.reserve(defs.size()); for (const auto &def : defs) { ranges.push_back(itemConverter(def)); } // ... so we can sort it also std::stable_sort(ranges.begin(), ranges.end(), compareRangeItem); makeTree(ranges, s.data()->data()); // assuming that reply ranges refer to revision when submitted // (not specified anyway in protocol/reply) if (defs.count() > 1 || onlyshow) { showTree(title, targetTree); } // it's not nice to jump to some location if we are too late if (!m_req_timeout && !onlyshow) { // assuming here that the first location is the best one const auto &item = itemConverter(defs.at(0)); const auto &pos = item.range.start(); goToDocumentLocation(item.uri, pos.line(), pos.column()); // forego mark and such if only a single destination if (defs.count() == 1) { clearAllLocationMarks(); } } // update marks updateState(); } }; positionRequest(req, h, s.data()); } static RangeItem locationToRangeItem(const LSPLocation &loc) { return {loc.uri, loc.range, LSPDocumentHighlightKind::Text}; } void goToDefinition() { auto title = i18nc("@title:tab", "Definition: %1", currentWord()); processLocations(title, &LSPClientServer::documentDefinition, false, &self_type::locationToRangeItem, &m_defTree); } void goToDeclaration() { auto title = i18nc("@title:tab", "Declaration: %1", currentWord()); processLocations(title, &LSPClientServer::documentDeclaration, false, &self_type::locationToRangeItem, &m_declTree); } void findReferences() { auto title = i18nc("@title:tab", "References: %1", currentWord()); bool decl = m_refDeclaration->isChecked(); // clang-format off auto req = [decl](LSPClientServer &server, const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentDefinitionReplyHandler &h) { return server.documentReferences(document, pos, decl, context, h); }; // clang-format on processLocations(title, req, true, &self_type::locationToRangeItem); } void highlight() { // determine current url to capture and use later on QUrl url; const KTextEditor::View *viewForRequest(m_mainWindow->activeView()); if (viewForRequest && viewForRequest->document()) { url = viewForRequest->document()->url(); } auto title = i18nc("@title:tab", "Highlight: %1", currentWord()); auto converter = [url](const LSPDocumentHighlight &hl) { return RangeItem {url, hl.range, hl.kind}; }; processLocations(title, &LSPClientServer::documentHighlight, true, converter); } void hover() { // trigger manually the normally automagic hover if (auto activeView = m_mainWindow->activeView()) { m_hover->textHint(activeView, activeView->cursorPosition()); } } void applyEdits(KTextEditor::Document *doc, const LSPClientRevisionSnapshot *snapshot, const QList &edits) { KTextEditor::MovingInterface *miface = qobject_cast(doc); Q_ASSERT(miface); // NOTE: // server might be pretty sloppy wrt edits (e.g. python-language-server) // e.g. send one edit for the whole document rather than 'surgical edits' // and that even when requesting format for a limited selection // ... but then we are but a client and do as we are told // all-in-all a low priority feature // all coordinates in edits are wrt original document, // so create moving ranges that will adjust to preceding edits as they are applied QVector ranges; for (const auto &edit : edits) { auto range = snapshot ? transformRange(doc->url(), *snapshot, edit.range) : edit.range; KTextEditor::MovingRange *mr = miface->newMovingRange(range); ranges.append(mr); } // now make one transaction (a.o. for one undo) and apply in sequence { KTextEditor::Document::EditingTransaction transaction(doc); for (int i = 0; i < ranges.length(); ++i) { doc->replaceText(ranges.at(i)->toRange(), edits.at(i).newText); } } qDeleteAll(ranges); } void applyWorkspaceEdit(const LSPWorkspaceEdit &edit, const LSPClientRevisionSnapshot *snapshot) { auto currentView = m_mainWindow->activeView(); for (auto it = edit.changes.begin(); it != edit.changes.end(); ++it) { auto document = findDocument(m_mainWindow, it.key()); if (!document) { KTextEditor::View *view = m_mainWindow->openUrl(it.key()); if (view) { document = view->document(); } } applyEdits(document, snapshot, it.value()); } if (currentView) { m_mainWindow->activateView(currentView->document()); } } void onApplyEdit(const LSPApplyWorkspaceEditParams &edit, const ApplyEditReplyHandler &h, bool &handled) { if (handled) return; handled = true; if (m_accept_edit) { qCInfo(LSPCLIENT) << "applying edit" << edit.label; applyWorkspaceEdit(edit.edit, nullptr); } else { qCInfo(LSPCLIENT) << "ignoring edit"; } h({m_accept_edit, QString()}); } template void checkEditResult(const Collection &c) { if (c.empty()) { showMessage(i18n("No edits"), KTextEditor::Message::Information); } } void delayCancelRequest(LSPClientServer::RequestHandle &&h, int timeout_ms = 4000) { QTimer::singleShot(timeout_ms, this, [h]() mutable { h.cancel(); }); } void format(QChar lastChar = QChar()) { KTextEditor::View *activeView = m_mainWindow->activeView(); QPointer document = activeView->document(); auto server = m_serverManager->findServer(activeView); if (!server || !document) return; int tabSize = 4; bool insertSpaces = true; auto cfgiface = qobject_cast(document); Q_ASSERT(cfgiface); tabSize = cfgiface->configValue(QStringLiteral("tab-width")).toInt(); insertSpaces = cfgiface->configValue(QStringLiteral("replace-tabs")).toBool(); // sigh, no move initialization capture ... // (again) assuming reply ranges wrt revisions submitted at this time QSharedPointer snapshot(m_serverManager->snapshot(server.data())); auto h = [this, document, snapshot, lastChar](const QList &edits) { if (lastChar.isNull()) { checkEditResult(edits); } if (document) { applyEdits(document, snapshot.data(), edits); } }; auto options = LSPFormattingOptions {tabSize, insertSpaces, QJsonObject()}; auto handle = !lastChar.isNull() ? server->documentOnTypeFormatting(document->url(), activeView->cursorPosition(), lastChar, options, this, h) : (activeView->selection() ? server->documentRangeFormatting(document->url(), activeView->selectionRange(), options, this, h) : server->documentFormatting(document->url(), options, this, h)); delayCancelRequest(std::move(handle)); } void rename() { KTextEditor::View *activeView = m_mainWindow->activeView(); QPointer document = activeView->document(); auto server = m_serverManager->findServer(activeView); if (!server || !document) return; bool ok = false; // results are typically (too) limited // due to server implementation or limited view/scope // so let's add a disclaimer that it's not our fault QString newName = QInputDialog::getText(activeView, i18nc("@title:window", "Rename"), i18nc("@label:textbox", "New name (caution: not all references may be replaced)"), QLineEdit::Normal, QString(), &ok); if (!ok) { return; } QSharedPointer snapshot(m_serverManager->snapshot(server.data())); auto h = [this, snapshot](const LSPWorkspaceEdit &edit) { checkEditResult(edit.changes); applyWorkspaceEdit(edit, snapshot.data()); }; auto handle = server->documentRename(document->url(), activeView->cursorPosition(), newName, this, h); delayCancelRequest(std::move(handle)); } static QStandardItem *getItem(const QStandardItemModel &model, const QUrl &url) { auto l = model.findItems(url.path()); if (l.length()) { return l.at(0); } return nullptr; } // select/scroll to diagnostics item for document and (optionally) line bool syncDiagnostics(KTextEditor::Document *document, int line, bool allowTop, bool doShow) { if (!m_diagnosticsTree) return false; auto hint = QAbstractItemView::PositionAtTop; QStandardItem *targetItem = nullptr; QStandardItem *topItem = getItem(*m_diagnosticsModel, document->url()); if (topItem) { int count = topItem->rowCount(); // let's not run wild on a linear search in a flood of diagnostics // user is already in enough trouble as it is ;-) if (count > 50) count = 0; for (int i = 0; i < count; ++i) { auto item = topItem->child(i); int itemline = item->data(RangeData::RangeRole).value().start().line(); if (line == itemline && m_diagnosticsTree) { targetItem = item; hint = QAbstractItemView::PositionAtCenter; break; } } } if (!targetItem && allowTop) { targetItem = topItem; } if (targetItem) { m_diagnosticsTree->blockSignals(true); m_diagnosticsTree->scrollTo(targetItem->index(), hint); m_diagnosticsTree->setCurrentIndex(targetItem->index()); m_diagnosticsTree->blockSignals(false); if (doShow) { m_tabWidget->setCurrentWidget(m_diagnosticsTree); m_mainWindow->showToolView(m_toolView.data()); } } return targetItem != nullptr; } void onViewState(KTextEditor::View *view, LSPClientViewTracker::State newState) { if (!view || !view->document()) return; // select top item on view change, // but otherwise leave selection unchanged if no match switch (newState) { case LSPClientViewTracker::ViewChanged: syncDiagnostics(view->document(), view->cursorPosition().line(), true, false); break; case LSPClientViewTracker::LineChanged: syncDiagnostics(view->document(), view->cursorPosition().line(), false, false); break; default: // should not happen break; } } Q_SLOT void onMarkClicked(KTextEditor::Document *document, KTextEditor::Mark mark, bool &handled) { // no action if no mark was sprinkled here if (m_diagnosticsMarks.contains(document) && syncDiagnostics(document, mark.line, false, true)) { handled = true; } } void onDiagnostics(const LSPPublishDiagnosticsParams &diagnostics) { if (!m_diagnosticsTree) return; QStandardItemModel *model = m_diagnosticsModel.data(); QStandardItem *topItem = getItem(*m_diagnosticsModel, diagnostics.uri); if (!topItem) { // no need to create an empty one if (diagnostics.diagnostics.empty()) { return; } topItem = new QStandardItem(); model->appendRow(topItem); topItem->setText(diagnostics.uri.path()); } else { topItem->setRowCount(0); } for (const auto &diag : diagnostics.diagnostics) { auto item = new DiagnosticItem(diag); topItem->appendRow(item); QString source; if (diag.source.length()) { source = QStringLiteral("[%1] ").arg(diag.source); } item->setData(diagnosticsIcon(diag.severity), Qt::DecorationRole); item->setText(source + diag.message); fillItemRoles(item, diagnostics.uri, diag.range, diag.severity); const auto &relatedInfo = diag.relatedInformation; for (const auto &related : relatedInfo) { if (related.location.uri.isEmpty()) { continue; } auto relatedItemMessage = new QStandardItem(); fillItemRoles(relatedItemMessage, related.location.uri, related.location.range, RangeData::KindEnum::Related); auto basename = QFileInfo(related.location.uri.path()).fileName(); auto location = QStringLiteral("%1:%2").arg(basename).arg(related.location.range.start().line()); relatedItemMessage->setText(QStringLiteral("[%1] %2").arg(location).arg(related.message)); relatedItemMessage->setData(diagnosticsIcon(LSPDiagnosticSeverity::Information), Qt::DecorationRole); item->appendRow({relatedItemMessage}); m_diagnosticsTree->setExpanded(item->index(), true); } } // TODO perhaps add some custom delegate that only shows 1 line // and only the whole text when item selected ?? m_diagnosticsTree->setExpanded(topItem->index(), true); m_diagnosticsTree->setRowHidden(topItem->row(), QModelIndex(), topItem->rowCount() == 0); m_diagnosticsTree->scrollTo(topItem->index(), QAbstractItemView::PositionAtTop); updateState(); } KTextEditor::View *viewForUrl(const QUrl &url) const { for (auto *view : m_mainWindow->views()) { if (view->document()->url() == url) return view; } return nullptr; } + void addMessage(LSPMessageType level, const QString &header, const QString &msg) + { + if (!m_messagesView) + return; + + QString lvl = i18nc("@info", "Unknown"); + switch (level) { + case LSPMessageType::Error: + lvl = i18nc("@info", "Error"); + break; + case LSPMessageType::Warning: + lvl = i18nc("@info", "Warning"); + break; + case LSPMessageType::Info: + lvl = i18nc("@info", "Information"); + break; + case LSPMessageType::Log: + lvl = i18nc("@info", "Log"); + break; + } + + // let's consider this expert info and use ISO date + auto now = QDateTime::currentDateTime().toString(Qt::ISODate); + auto text = QStringLiteral("[%1] [%2] [%3]\n%4\n").arg(now).arg(lvl).arg(header).arg(msg.trimmed()); + m_messagesView->appendPlainText(text); + + if (static_cast(level) <= m_messagesAutoSwitch->currentItem()) { + switchToMessages(); + } else { + // show arrival of new message + auto index = m_tabWidget->indexOf(m_messagesView); + if (m_tabWidget->currentIndex() != index) + m_tabWidget->tabBar()->setTabTextColor(index, Qt::gray); + } + } + + // params type is same for show or log and is treated the same way + void onMessage(const LSPLogMessageParams ¶ms) + { + // determine server description + auto server = dynamic_cast(sender()); + auto desc = i18nc("@info", "LSP Server"); + if (server) + desc += QStringLiteral(": %1").arg(LSPClientServerManager::serverDescription(server)); + addMessage(params.type, desc, params.message); + } + + void onShowMessage(KTextEditor::Message::MessageType level, const QString &msg) + { + // translate level + LSPMessageType lvl = LSPMessageType::Log; + using KMessage = KTextEditor::Message; + switch (level) { + case KMessage::Error: + lvl = LSPMessageType::Error; + break; + case KMessage::Warning: + lvl = LSPMessageType::Warning; + break; + case KMessage::Information: + lvl = LSPMessageType::Info; + break; + case KMessage::Positive: + lvl = LSPMessageType::Log; + break; + } + + addMessage(lvl, i18nc("@info", "LSP Client"), msg); + } + Q_SLOT void clearSemanticHighlighting(KTextEditor::Document *document) { auto &documentRanges = m_semanticHighlightRanges[document]; for (const auto &lineRanges : documentRanges) qDeleteAll(lineRanges); documentRanges.clear(); } void onSemanticHighlighting(const LSPSemanticHighlightingParams ¶ms) { auto *view = viewForUrl(params.textDocument.uri); if (!view) { qCWarning(LSPCLIENT) << "failed to find view for uri" << params.textDocument.uri; return; } auto server = m_serverManager->findServer(view); if (!server) { qCWarning(LSPCLIENT) << "failed to find server for view" << params.textDocument.uri; return; } auto *document = view->document(); auto *miface = qobject_cast(document); Q_ASSERT(miface); // TODO: translate between locked revision, if possible? auto version = params.textDocument.version; if (version == -1) { // use version from disk version = miface->lastSavedRevision(); if (version == -1) { // never saved version = miface->revision(); } } if (version != miface->revision()) { qCWarning(LSPCLIENT) << "discarding highlighting, versions don't match:" << params.textDocument.version << version << miface->revision(); return; } // ensure runtime match connect(document, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearSemanticHighlighting(KTextEditor::Document *)), Qt::UniqueConnection); connect(document, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearSemanticHighlighting(KTextEditor::Document *)), Qt::UniqueConnection); // TODO: make schema attributes accessible via some new interface, // or at least add configuration to the lsp plugin config // FIXME: static attributes break if one e.g. switches the color scheme on the fly! auto attributeForScopes = [view](const QVector &scopes) -> KTextEditor::Attribute::Ptr { for (const auto &scope : scopes) { if (scope == QLatin1String("entity.name.function.method.cpp")) { static KTextEditor::Attribute::Ptr attr; if (!attr) { attr = view->defaultStyleAttribute(KTextEditor::dsFunction); attr.detach(); attr->setForeground(Qt::darkYellow); attr->setFontItalic(true); } return attr; } else if(scope == QLatin1String("entity.name.function.cpp")) { static KTextEditor::Attribute::Ptr attr; if (!attr) { attr = view->defaultStyleAttribute(KTextEditor::dsFunction); attr.detach(); attr->setForeground(Qt::darkYellow); } return attr; } else if (scope == QLatin1String("variable.other.cpp")) { static KTextEditor::Attribute::Ptr attr; if (!attr) { attr = view->defaultStyleAttribute(KTextEditor::dsVariable); attr.detach(); attr->setForeground(Qt::darkCyan); } return attr; } else if (scope == QLatin1String("variable.other.field.cpp")) { static KTextEditor::Attribute::Ptr attr; if (!attr) { attr = view->defaultStyleAttribute(KTextEditor::dsVariable); attr.detach(); attr->setForeground(Qt::darkCyan); attr->setFontItalic(true); } return attr; } else if (scope == QLatin1String("entity.name.type.enum.cpp")) { static KTextEditor::Attribute::Ptr attr; if (!attr) { attr = view->defaultStyleAttribute(KTextEditor::dsConstant); attr.detach(); attr->setForeground(Qt::darkMagenta); } return attr; } else if (scope == QLatin1String("variable.other.enummember.cpp")) { static KTextEditor::Attribute::Ptr attr; if (!attr) { attr = view->defaultStyleAttribute(KTextEditor::dsConstant); attr.detach(); attr->setForeground(Qt::darkMagenta); attr->setFontItalic(true); } return attr; } else if (scope == QLatin1String("entity.name.type.class.cpp") || scope == QLatin1String("entity.name.type.template.cpp")) { static KTextEditor::Attribute::Ptr attr; if (!attr) { attr = view->defaultStyleAttribute(KTextEditor::dsDataType); attr.detach(); attr->setForeground(Qt::darkMagenta); } return attr; } else if (scope == QLatin1String("entity.name.namespace.cpp")) { static KTextEditor::Attribute::Ptr attr; if (!attr) { attr = view->defaultStyleAttribute(KTextEditor::dsDataType); attr.detach(); attr->setForeground(Qt::darkGreen); attr->setFontItalic(true); } return attr; } } return {}; }; // TODO: we should try to recycle the moving ranges instead of recreating them all the time const auto scopes = server->capabilities().semanticHighlightingProvider.scopes; //qDebug() << params.textDocument.uri << scopes; auto &documentRanges = m_semanticHighlightRanges[document]; QSet handledLines; for (const auto &line : params.lines) { handledLines.insert(line.line); auto &lineRanges = documentRanges[line.line]; qDeleteAll(lineRanges); lineRanges.clear(); //qDebug() << "line:" << line.line; for (const auto &token : line.tokens) { //qDebug() << "token:" << token.character << token.length << token.scope << scopes.value(token.scope); auto attribute = attributeForScopes(scopes.value(token.scope)); if (!attribute) continue; const auto columnStart = static_cast(token.character); const auto columnEnd = columnStart + static_cast(token.length); constexpr auto expand = KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight; auto *range = miface->newMovingRange({line.line, columnStart, line.line, columnEnd}, expand, KTextEditor::MovingRange::InvalidateIfEmpty); range->setAttribute(attribute); } } // clear lines that got removed or commented out for (auto it = documentRanges.begin(); it != documentRanges.end();) { if (!handledLines.contains(it.key())) { qDeleteAll(it.value()); it = documentRanges.erase(it); } else { ++it; } } } void onDocumentUrlChanged(KTextEditor::Document *doc) { // url already changed by this time and new url not useful (void)doc; // note; url also changes when closed // spec says; // if a language has a project system, diagnostics are not cleared by *server* // but in either case (url change or close); remove lingering diagnostics // collect active urls QSet fpaths; for (const auto &view : m_mainWindow->views()) { if (auto doc = view->document()) { fpaths.insert(doc->url().path()); } } // check and clear defunct entries const auto &model = *m_diagnosticsModel; for (int i = 0; i < model.rowCount(); ++i) { auto item = model.item(i); if (item && !fpaths.contains(item->text())) { item->setRowCount(0); if (m_diagnosticsTree) { m_diagnosticsTree->setRowHidden(item->row(), QModelIndex(), true); } } } } void onTextChanged(KTextEditor::Document *doc) { if (m_onTypeFormattingTriggers.empty()) return; KTextEditor::View *activeView = m_mainWindow->activeView(); if (!activeView || activeView->document() != doc) return; // NOTE the intendation mode should probably be set to None, // so as not to experience unpleasant interference auto cursor = activeView->cursorPosition(); QChar lastChar = cursor.column() == 0 ? QChar::fromLatin1('\n') : doc->characterAt({cursor.line(), cursor.column() - 1}); if (m_onTypeFormattingTriggers.contains(lastChar)) { format(lastChar); } } void updateState() { KTextEditor::View *activeView = m_mainWindow->activeView(); auto doc = activeView ? activeView->document() : nullptr; auto server = m_serverManager->findServer(activeView); bool defEnabled = false, declEnabled = false, refEnabled = false; bool hoverEnabled = false, highlightEnabled = false; bool formatEnabled = false; bool renameEnabled = false; if (server) { const auto &caps = server->capabilities(); defEnabled = caps.definitionProvider; // FIXME no real official protocol way to detect, so enable anyway declEnabled = caps.declarationProvider || true; refEnabled = caps.referencesProvider; hoverEnabled = caps.hoverProvider; highlightEnabled = caps.documentHighlightProvider; formatEnabled = caps.documentFormattingProvider || caps.documentRangeFormattingProvider; renameEnabled = caps.renameProvider; connect(server.data(), &LSPClientServer::publishDiagnostics, this, &self_type::onDiagnostics, Qt::UniqueConnection); + connect(server.data(), &LSPClientServer::showMessage, this, &self_type::onMessage, Qt::UniqueConnection); + connect(server.data(), &LSPClientServer::logMessage, this, &self_type::onMessage, Qt::UniqueConnection); connect(server.data(), &LSPClientServer::semanticHighlighting, this, &self_type::onSemanticHighlighting, Qt::UniqueConnection); connect(server.data(), &LSPClientServer::applyEdit, this, &self_type::onApplyEdit, Qt::UniqueConnection); // update format trigger characters const auto &fmt = caps.documentOnTypeFormattingProvider; if (fmt.provider && m_onTypeFormatting->isChecked()) { m_onTypeFormattingTriggers = fmt.triggerCharacters; } else { m_onTypeFormattingTriggers.clear(); } // and monitor for such if (doc) { connect(doc, &KTextEditor::Document::textChanged, this, &self_type::onTextChanged, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::documentUrlChanged, this, &self_type::onDocumentUrlChanged, Qt::UniqueConnection); } } if (m_findDef) m_findDef->setEnabled(defEnabled); if (m_findDecl) m_findDecl->setEnabled(declEnabled); if (m_findRef) m_findRef->setEnabled(refEnabled); if (m_triggerHighlight) m_triggerHighlight->setEnabled(highlightEnabled); if (m_triggerHover) m_triggerHover->setEnabled(hoverEnabled); if (m_triggerFormat) m_triggerFormat->setEnabled(formatEnabled); if (m_triggerRename) m_triggerRename->setEnabled(renameEnabled); if (m_complDocOn) m_complDocOn->setEnabled(server); if (m_restartServer) m_restartServer->setEnabled(server); // update completion with relevant server m_completion->setServer(server); if (m_complDocOn) m_completion->setSelectedDocumentation(m_complDocOn->isChecked()); updateCompletion(activeView, server.data()); // update hover with relevant server m_hover->setServer(server); updateHover(activeView, (m_autoHover && m_autoHover->isChecked()) ? server.data() : nullptr); // update marks if applicable if (m_markModel && doc) addMarks(doc, m_markModel, m_ranges, m_marks); if (m_diagnosticsModel && doc) { clearMarks(doc, m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll); addMarks(doc, m_diagnosticsModel.data(), m_diagnosticsRanges, m_diagnosticsMarks); } // connect for cleanup stuff if (activeView) connect(activeView, &KTextEditor::View::destroyed, this, &self_type::viewDestroyed, Qt::UniqueConnection); } void viewDestroyed(QObject *view) { m_completionViews.remove(static_cast(view)); m_hoverViews.remove(static_cast(view)); } void updateCompletion(KTextEditor::View *view, LSPClientServer *server) { if (!view) { return; } bool registered = m_completionViews.contains(view); KTextEditor::CodeCompletionInterface *cci = qobject_cast(view); Q_ASSERT(cci); if (!registered && server && server->capabilities().completionProvider.provider) { qCInfo(LSPCLIENT) << "registering cci"; cci->registerCompletionModel(m_completion.data()); m_completionViews.insert(view); } if (registered && !server) { qCInfo(LSPCLIENT) << "unregistering cci"; cci->unregisterCompletionModel(m_completion.data()); m_completionViews.remove(view); } } void updateHover(KTextEditor::View *view, LSPClientServer *server) { if (!view) { return; } bool registered = m_hoverViews.contains(view); KTextEditor::TextHintInterface *cci = qobject_cast(view); Q_ASSERT(cci); if (!registered && server && server->capabilities().hoverProvider) { qCInfo(LSPCLIENT) << "registering cci"; cci->registerTextHintProvider(m_hover.data()); m_hoverViews.insert(view); } if (registered && !server) { qCInfo(LSPCLIENT) << "unregistering cci"; cci->unregisterTextHintProvider(m_hover.data()); m_hoverViews.remove(view); } } }; class LSPClientPluginViewImpl : public QObject, public KXMLGUIClient { Q_OBJECT typedef LSPClientPluginViewImpl self_type; KTextEditor::MainWindow *m_mainWindow; QSharedPointer m_serverManager; QScopedPointer m_actionView; public: LSPClientPluginViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) : QObject(mainWin) , m_mainWindow(mainWin) , m_serverManager(LSPClientServerManager::new_(plugin, mainWin)) , m_actionView(new LSPClientActionView(plugin, mainWin, this, m_serverManager)) { KXMLGUIClient::setComponentName(QStringLiteral("lspclient"), i18n("LSP Client")); setXMLFile(QStringLiteral("ui.rc")); m_mainWindow->guiFactory()->addClient(this); } ~LSPClientPluginViewImpl() override { // minimize/avoid some surprises; // safe construction/destruction by separate (helper) objects; // signals are auto-disconnected when high-level "view" objects are broken down // so it only remains to clean up lowest level here then prior to removal m_actionView.reset(); m_serverManager.reset(); m_mainWindow->guiFactory()->removeClient(this); } }; QObject *LSPClientPluginView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) { return new LSPClientPluginViewImpl(plugin, mainWin); } #include "lspclientpluginview.moc" diff --git a/addons/lspclient/lspclientservermanager.cpp b/addons/lspclient/lspclientservermanager.cpp index ec788860e..623fb90a7 100644 --- a/addons/lspclient/lspclientservermanager.cpp +++ b/addons/lspclient/lspclientservermanager.cpp @@ -1,825 +1,819 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts 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. */ /* see plugins.docbook lspclient-configuration * for client configuration documentation */ #include "lspclientservermanager.h" #include "lspclient_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // helper to find a proper root dir for the given document & file name that indicate the root dir static QString rootForDocumentAndRootIndicationFileName(KTextEditor::Document *document, const QString &rootIndicationFileName) { // search only feasible if document is local file if (!document->url().isLocalFile()) { return QString(); } // search root upwards QDir dir(QFileInfo(document->url().toLocalFile()).absolutePath()); QSet seenDirectories; while (!seenDirectories.contains(dir.absolutePath())) { // update guard seenDirectories.insert(dir.absolutePath()); // the file that indicates the root dir is there => all fine if (dir.exists(rootIndicationFileName)) { return dir.absolutePath(); } // else: cd up, if possible or abort if (!dir.cdUp()) { break; } } // no root found, bad luck return QString(); } #include // local helper; // recursively merge top json top onto bottom json static QJsonObject merge(const QJsonObject &bottom, const QJsonObject &top) { QJsonObject result; for (auto item = top.begin(); item != top.end(); item++) { const auto &key = item.key(); if (item.value().isObject()) { result.insert(key, merge(bottom.value(key).toObject(), item.value().toObject())); } else { result.insert(key, item.value()); } } // parts only in bottom for (auto item = bottom.begin(); item != bottom.end(); item++) { if (!result.contains(item.key())) { result.insert(item.key(), item.value()); } } return result; } // helper guard to handle revision (un)lock struct RevisionGuard { QPointer m_doc; KTextEditor::MovingInterface *m_movingInterface = nullptr; qint64 m_revision = -1; RevisionGuard(KTextEditor::Document *doc = nullptr) : m_doc(doc) , m_movingInterface(qobject_cast(doc)) { Q_ASSERT(m_movingInterface); m_revision = m_movingInterface->revision(); m_movingInterface->lockRevision(m_revision); } // really only need/allow this one (out of 5) RevisionGuard(RevisionGuard &&other) : RevisionGuard(nullptr) { std::swap(m_doc, other.m_doc); std::swap(m_movingInterface, other.m_movingInterface); std::swap(m_revision, other.m_revision); } void release() { m_movingInterface = nullptr; m_revision = -1; } ~RevisionGuard() { // NOTE: hopefully the revision is still valid at this time if (m_doc && m_movingInterface && m_revision >= 0) { m_movingInterface->unlockRevision(m_revision); } } }; class LSPClientRevisionSnapshotImpl : public LSPClientRevisionSnapshot { Q_OBJECT typedef LSPClientRevisionSnapshotImpl self_type; // std::map has more relaxed constraints on value_type std::map m_guards; Q_SLOT void clearRevisions(KTextEditor::Document *doc) { for (auto &item : m_guards) { if (item.second.m_doc == doc) { item.second.release(); } } } public: void add(KTextEditor::Document *doc) { Q_ASSERT(doc); // make sure revision is cleared when needed and no longer used (to unlock or otherwise) // see e.g. implementation in katetexthistory.cpp and assert's in place there auto conn = connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearRevisions(KTextEditor::Document *))); Q_ASSERT(conn); conn = connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearRevisions(KTextEditor::Document *))); Q_ASSERT(conn); m_guards.emplace(doc->url(), doc); } void find(const QUrl &url, KTextEditor::MovingInterface *&miface, qint64 &revision) const override { auto it = m_guards.find(url); if (it != m_guards.end()) { miface = it->second.m_movingInterface; revision = it->second.m_revision; } else { miface = nullptr; revision = -1; } } }; // helper class to sync document changes to LSP server class LSPClientServerManagerImpl : public LSPClientServerManager { Q_OBJECT typedef LSPClientServerManagerImpl self_type; struct ServerInfo { QSharedPointer server; // config specified server url QString url; QTime started; int failcount = 0; // pending settings to be submitted QJsonValue settings; }; struct DocumentInfo { QSharedPointer server; KTextEditor::MovingInterface *movingInterface; QUrl url; qint64 version; bool open : 1; bool modified : 1; // used for incremental update (if non-empty) QList changes; }; LSPClientPlugin *m_plugin; KTextEditor::MainWindow *m_mainWindow; // merged default and user config QJsonObject m_serverConfig; // root -> (mode -> server) QMap> m_servers; QHash m_docs; bool m_incrementalSync = false; // highlightingModeRegex => language id std::vector> m_highlightingModeRegexToLanguageId; // cache of highlighting mode => language id, to avoid massive regex matching QHash m_highlightingModeToLanguageIdCache; typedef QVector> ServerList; public: LSPClientServerManagerImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) : m_plugin(plugin) , m_mainWindow(mainWin) { connect(plugin, &LSPClientPlugin::update, this, &self_type::updateServerConfig); QTimer::singleShot(100, this, &self_type::updateServerConfig); } ~LSPClientServerManagerImpl() override { // stop everything as we go down // several stages; // stage 1; request shutdown of all servers (in parallel) // (give that some time) // stage 2; send TERM // stage 3; send KILL // stage 1 QEventLoop q; QTimer t; connect(&t, &QTimer::timeout, &q, &QEventLoop::quit); /* some msleep are used below which is somewhat BAD as it blocks/hangs * the mainloop, however there is not much alternative: * + running an inner mainloop leads to event processing, * which could trigger an unexpected sequence of 'events' * such as (re)loading plugin that is currently still unloading * (consider scenario of fast-clicking enable/disable of LSP plugin) * + could reduce or forego the sleep, but that increases chances * on an unclean shutdown of LSP server, which may or may not * be able to handle that properly (so let's try and be a polite * client and try to avoid that to some degree) * So we are left with a minor sleep compromise ... */ int count = 0; for (const auto &el : m_servers) { for (const auto &si : el) { auto &s = si.server; if (!s) continue; disconnect(s.data(), nullptr, this, nullptr); if (s->state() != LSPClientServer::State::None) { auto handler = [&q, &count, s]() { if (s->state() != LSPClientServer::State::None) { if (--count == 0) { q.quit(); } } }; connect(s.data(), &LSPClientServer::stateChanged, this, handler); ++count; s->stop(-1, -1); } } } QThread::msleep(500); // stage 2 and 3 count = 0; for (count = 0; count < 2; ++count) { for (const auto &el : m_servers) { for (const auto &si : el) { auto &s = si.server; if (!s) continue; s->stop(count == 0 ? 1 : -1, count == 0 ? -1 : 1); } } QThread::msleep(100); } } // map (highlight)mode to lsp languageId QString languageId(const QString &mode) { // query cache first const auto cacheIt = m_highlightingModeToLanguageIdCache.find(mode); if (cacheIt != m_highlightingModeToLanguageIdCache.end()) return cacheIt.value(); // match via regexes + cache result for (auto it : m_highlightingModeRegexToLanguageId) { if (it.first.match(mode).hasMatch()) { m_highlightingModeToLanguageIdCache[mode] = it.second; return it.second; } } // else: we have no matching server! m_highlightingModeToLanguageIdCache[mode] = QString(); return QString(); } void setIncrementalSync(bool inc) override { m_incrementalSync = inc; } QSharedPointer findServer(KTextEditor::Document *document, bool updatedoc = true) override { if (!document || document->url().isEmpty()) return nullptr; auto it = m_docs.find(document); auto server = it != m_docs.end() ? it->server : nullptr; if (!server) { if ((server = _findServer(document))) trackDocument(document, server); } if (server && updatedoc) update(server.data(), false); return server; } QSharedPointer findServer(KTextEditor::View *view, bool updatedoc = true) override { return view ? findServer(view->document(), updatedoc) : nullptr; } // restart a specific server or all servers if server == nullptr void restart(LSPClientServer *server) override { ServerList servers; // find entry for server(s) and move out for (auto &m : m_servers) { for (auto it = m.begin(); it != m.end();) { if (!server || it->server.data() == server) { servers.push_back(it->server); it = m.erase(it); } else { ++it; } } } restart(servers); } qint64 revision(KTextEditor::Document *doc) override { auto it = m_docs.find(doc); return it != m_docs.end() ? it->version : -1; } LSPClientRevisionSnapshot *snapshot(LSPClientServer *server) override { auto result = new LSPClientRevisionSnapshotImpl; for (auto it = m_docs.begin(); it != m_docs.end(); ++it) { if (it->server == server) { // sync server to latest revision that will be recorded update(it.key(), false); result->add(it.key()); } } return result; } private: void showMessage(const QString &msg, KTextEditor::Message::MessageType level) { - KTextEditor::View *view = m_mainWindow->activeView(); - if (!view || !view->document()) - return; - - auto kmsg = new KTextEditor::Message(xi18nc("@info", "LSP Client: %1", msg), level); - kmsg->setPosition(KTextEditor::Message::AboveView); - kmsg->setAutoHide(5000); - kmsg->setAutoHideMode(KTextEditor::Message::Immediate); - kmsg->setView(view); - view->document()->postMessage(kmsg); + // inform interested view(er) which will decide how/where to show + emit LSPClientServerManager::showMessage(level, msg); } // caller ensures that servers are no longer present in m_servers void restart(const ServerList &servers) { // close docs for (const auto &server : servers) { // controlling server here, so disable usual state tracking response disconnect(server.data(), nullptr, this, nullptr); for (auto it = m_docs.begin(); it != m_docs.end();) { auto &item = it.value(); if (item.server == server) { // no need to close if server not in proper state if (server->state() != LSPClientServer::State::Running) { item.open = false; } it = _close(it, true); } else { ++it; } } } // helper captures servers auto stopservers = [servers](int t, int k) { for (const auto &server : servers) { server->stop(t, k); } }; // trigger server shutdown now stopservers(-1, -1); // initiate delayed stages (TERM and KILL) // async, so give a bit more time QTimer::singleShot(2 * TIMEOUT_SHUTDOWN, this, [stopservers]() { stopservers(1, -1); }); QTimer::singleShot(4 * TIMEOUT_SHUTDOWN, this, [stopservers]() { stopservers(-1, 1); }); // as for the start part // trigger interested parties, which will again request a server as needed // let's delay this; less chance for server instances to trip over each other QTimer::singleShot(6 * TIMEOUT_SHUTDOWN, this, [this]() { emit serverChanged(); }); } void onStateChanged(LSPClientServer *server) { if (server->state() == LSPClientServer::State::Running) { // send settings if pending for (auto &m : m_servers) { for (auto &si : m) { if (si.server.data() == server && !si.settings.isUndefined()) { server->didChangeConfiguration(si.settings); } } } // clear for normal operation emit serverChanged(); } else if (server->state() == LSPClientServer::State::None) { // went down // find server info to see how bad this is // if this is an occasional termination/crash ... ok then // if this happens quickly (bad/missing server, wrong cmdline/config), then no restart QSharedPointer sserver; QString url; bool retry = true; for (auto &m : m_servers) { for (auto &si : m) { if (si.server.data() == server) { url = si.url; if (si.started.secsTo(QTime::currentTime()) < 60) { ++si.failcount; } // clear the entry, which will be re-filled if needed // otherwise, leave it in place as a dead mark not to re-create one in _findServer if (si.failcount < 2) { std::swap(sserver, si.server); } else { sserver = si.server; retry = false; } } } } auto action = retry ? i18n("Restarting") : i18n("NOT Restarting"); showMessage(i18n("Server terminated unexpectedly ... %1 [%2] [homepage: %3] ", action, server->cmdline().join(QLatin1Char(' ')), url), KTextEditor::Message::Warning); if (sserver) { // sserver might still be in m_servers // but since it died already bringing it down will have no (ill) effect restart({sserver}); } } } QSharedPointer _findServer(KTextEditor::Document *document) { // compute the LSP standardized language id, none found => no change auto langId = languageId(document->highlightingMode()); if (langId.isEmpty()) return nullptr; QObject *projectView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin")); const auto projectBase = QDir(projectView ? projectView->property("projectBaseDir").toString() : QString()); const auto &projectMap = projectView ? projectView->property("projectMap").toMap() : QVariantMap(); // merge with project specific auto projectConfig = QJsonDocument::fromVariant(projectMap).object().value(QStringLiteral("lspclient")).toObject(); auto serverConfig = merge(m_serverConfig, projectConfig); // locate server config QJsonValue config; QSet used; // reduce langId auto realLangId = langId; while (true) { qCInfo(LSPCLIENT) << "language id " << langId; used << langId; config = serverConfig.value(QStringLiteral("servers")).toObject().value(langId); if (config.isObject()) { const auto &base = config.toObject().value(QStringLiteral("use")).toString(); // basic cycle detection if (!base.isEmpty() && !used.contains(base)) { langId = base; continue; } } break; } if (!config.isObject()) return nullptr; // merge global settings serverConfig = merge(serverConfig.value(QStringLiteral("global")).toObject(), config.toObject()); QString rootpath; auto rootv = serverConfig.value(QStringLiteral("root")); if (rootv.isString()) { auto sroot = rootv.toString(); if (QDir::isAbsolutePath(sroot)) { rootpath = sroot; } else if (!projectBase.isEmpty()) { rootpath = QDir(projectBase).absoluteFilePath(sroot); } } /** * no explicit set root dir? search for a matching root based on some name filters * this is required for some LSP servers like rls that don't handle that on their own like * clangd does */ if (rootpath.isEmpty()) { const auto fileNamesForDetection = serverConfig.value(QStringLiteral("rootIndicationFileNames")); if (fileNamesForDetection.isArray()) { // we try each file name alternative in the listed order // this allows to have preferences for (auto name : fileNamesForDetection.toArray()) { if (name.isString()) { rootpath = rootForDocumentAndRootIndicationFileName(document, name.toString()); if (!rootpath.isEmpty()) { break; } } } } } // last fallback: home directory if (rootpath.isEmpty()) { rootpath = QDir::homePath(); } auto root = QUrl::fromLocalFile(rootpath); auto &serverinfo = m_servers[root][langId]; auto &server = serverinfo.server; if (!server) { QStringList cmdline; // choose debug command line for debug mode, fallback to command auto vcmdline = serverConfig.value(m_plugin->m_debugMode ? QStringLiteral("commandDebug") : QStringLiteral("command")); if (vcmdline.isUndefined()) { vcmdline = serverConfig.value(QStringLiteral("command")); } auto scmdline = vcmdline.toString(); if (!scmdline.isEmpty()) { cmdline = scmdline.split(QLatin1Char(' ')); } else { for (const auto &c : vcmdline.toArray()) { cmdline.push_back(c.toString()); } } if (cmdline.length() > 0) { server.reset(new LSPClientServer(cmdline, root, realLangId, serverConfig.value(QStringLiteral("initializationOptions")))); connect(server.data(), &LSPClientServer::stateChanged, this, &self_type::onStateChanged, Qt::UniqueConnection); if (!server->start(m_plugin)) { showMessage(i18n("Failed to start server: %1", cmdline.join(QLatin1Char(' '))), KTextEditor::Message::Error); + } else { + showMessage(i18n("Started server %2: %1", cmdline.join(QLatin1Char(' ')), serverDescription(server.data())), KTextEditor::Message::Positive); } serverinfo.settings = serverConfig.value(QStringLiteral("settings")); serverinfo.started = QTime::currentTime(); serverinfo.url = serverConfig.value(QStringLiteral("url")).toString(); // leave failcount as-is } } return (server && server->state() == LSPClientServer::State::Running) ? server : nullptr; } void updateServerConfig() { // default configuration, compiled into plugin resource, reading can't fail QFile defaultConfigFile(QStringLiteral(":/lspclient/settings.json")); defaultConfigFile.open(QIODevice::ReadOnly); Q_ASSERT(defaultConfigFile.isOpen()); m_serverConfig = QJsonDocument::fromJson(defaultConfigFile.readAll()).object(); // consider specified configuration if existing const auto configPath = m_plugin->configPath().toLocalFile(); if (!configPath.isEmpty() && QFile::exists(configPath)) { QFile f(configPath); if (f.open(QIODevice::ReadOnly)) { const auto data = f.readAll(); if (!data.isEmpty()) { QJsonParseError error; auto json = QJsonDocument::fromJson(data, &error); if (error.error == QJsonParseError::NoError) { if (json.isObject()) { m_serverConfig = merge(m_serverConfig, json.object()); } else { showMessage(i18n("Failed to parse server configuration '%1': no JSON object", configPath), KTextEditor::Message::Error); } } else { showMessage(i18n("Failed to parse server configuration '%1': %2", configPath, error.errorString()), KTextEditor::Message::Error); } } } else { showMessage(i18n("Failed to read server configuration: %1", configPath), KTextEditor::Message::Error); } } // build regex of highlightingMode => language id m_highlightingModeRegexToLanguageId.clear(); m_highlightingModeToLanguageIdCache.clear(); const auto servers = m_serverConfig.value(QLatin1String("servers")).toObject(); for (auto it = servers.begin(); it != servers.end(); ++it) { // get highlighting mode regex for this server, if not set, fallback to just the name QString highlightingModeRegex = it.value().toObject().value(QLatin1String("highlightingModeRegex")).toString(); if (highlightingModeRegex.isEmpty()) { highlightingModeRegex = it.key(); } m_highlightingModeRegexToLanguageId.emplace_back(QRegularExpression(highlightingModeRegex, QRegularExpression::CaseInsensitiveOption), it.key()); } // we could (but do not) perform restartAll here; // for now let's leave that up to user // but maybe we do have a server now where not before, so let's signal emit serverChanged(); } void trackDocument(KTextEditor::Document *doc, const QSharedPointer &server) { auto it = m_docs.find(doc); if (it == m_docs.end()) { KTextEditor::MovingInterface *miface = qobject_cast(doc); it = m_docs.insert(doc, {server, miface, doc->url(), 0, false, false, {}}); // track document connect(doc, &KTextEditor::Document::documentUrlChanged, this, &self_type::untrack, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::highlightingModeChanged, this, &self_type::untrack, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::aboutToClose, this, &self_type::untrack, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::destroyed, this, &self_type::untrack, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::textChanged, this, &self_type::onTextChanged, Qt::UniqueConnection); // in case of incremental change connect(doc, &KTextEditor::Document::textInserted, this, &self_type::onTextInserted, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::textRemoved, this, &self_type::onTextRemoved, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::lineWrapped, this, &self_type::onLineWrapped, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::lineUnwrapped, this, &self_type::onLineUnwrapped, Qt::UniqueConnection); } else { it->server = server; } } decltype(m_docs)::iterator _close(decltype(m_docs)::iterator it, bool remove) { if (it != m_docs.end()) { if (it->open) { // release server side (use url as registered with) (it->server)->didClose(it->url); it->open = false; } if (remove) { disconnect(it.key(), nullptr, this, nullptr); it = m_docs.erase(it); } } return it; } void _close(KTextEditor::Document *doc, bool remove) { auto it = m_docs.find(doc); if (it != m_docs.end()) { _close(it, remove); } } void untrack(QObject *doc) { _close(qobject_cast(doc), true); emit serverChanged(); } void close(KTextEditor::Document *doc) { _close(doc, false); } void update(const decltype(m_docs)::iterator &it, bool force) { auto doc = it.key(); if (it != m_docs.end() && it->server) { it->version = it->movingInterface->revision(); if (!m_incrementalSync) { it->changes.clear(); } if (it->open) { if (it->modified || force) { (it->server)->didChange(it->url, it->version, (it->changes.empty()) ? doc->text() : QString(), it->changes); } } else { (it->server)->didOpen(it->url, it->version, languageId(doc->highlightingMode()), doc->text()); it->open = true; } it->modified = false; it->changes.clear(); } } void update(KTextEditor::Document *doc, bool force) override { update(m_docs.find(doc), force); } void update(LSPClientServer *server, bool force) { for (auto it = m_docs.begin(); it != m_docs.end(); ++it) { if (it->server == server) { update(it, force); } } } void onTextChanged(KTextEditor::Document *doc) { auto it = m_docs.find(doc); if (it != m_docs.end()) { it->modified = true; } } DocumentInfo *getDocumentInfo(KTextEditor::Document *doc) { if (!m_incrementalSync) return nullptr; auto it = m_docs.find(doc); if (it != m_docs.end() && it->server) { const auto &caps = it->server->capabilities(); if (caps.textDocumentSync == LSPDocumentSyncKind::Incremental) { return &(*it); } } return nullptr; } void onTextInserted(KTextEditor::Document *doc, const KTextEditor::Cursor &position, const QString &text) { auto info = getDocumentInfo(doc); if (info) { info->changes.push_back({LSPRange {position, position}, text}); } } void onTextRemoved(KTextEditor::Document *doc, const KTextEditor::Range &range, const QString &text) { (void)text; auto info = getDocumentInfo(doc); if (info) { info->changes.push_back({range, QString()}); } } void onLineWrapped(KTextEditor::Document *doc, const KTextEditor::Cursor &position) { // so a 'newline' has been inserted at position // could have been UNIX style or other kind, let's ask the document auto text = doc->text({position, {position.line() + 1, 0}}); onTextInserted(doc, position, text); } void onLineUnwrapped(KTextEditor::Document *doc, int line) { // lines line-1 and line got replaced by current content of line-1 Q_ASSERT(line > 0); auto info = getDocumentInfo(doc); if (info) { LSPRange oldrange {{line - 1, 0}, {line + 1, 0}}; LSPRange newrange {{line - 1, 0}, {line, 0}}; auto text = doc->text(newrange); info->changes.push_back({oldrange, text}); } } }; QSharedPointer LSPClientServerManager::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) { return QSharedPointer(new LSPClientServerManagerImpl(plugin, mainWin)); } #include "lspclientservermanager.moc" diff --git a/addons/lspclient/lspclientservermanager.h b/addons/lspclient/lspclientservermanager.h index c99eb600a..5552d1d4f 100644 --- a/addons/lspclient/lspclientservermanager.h +++ b/addons/lspclient/lspclientservermanager.h @@ -1,91 +1,105 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts 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. */ #ifndef LSPCLIENTSERVERMANAGER_H #define LSPCLIENTSERVERMANAGER_H #include "lspclientplugin.h" #include "lspclientserver.h" +#include + #include namespace KTextEditor { class MainWindow; class Document; class View; class MovingInterface; } class LSPClientRevisionSnapshot; /* * A helper class that manages LSP servers in relation to a KTextDocument. * That is, spin up a server for a document when so requested, and then * monitor when the server is up (or goes down) and the document (for changes). * Those changes may then be synced to the server when so requested (e.g. prior * to another component performing an LSP request for a document). * So, other than managing servers, it also manages the document-server * relationship (and document), what's in a name ... */ class LSPClientServerManager : public QObject { Q_OBJECT public: // factory method; private implementation by interface static QSharedPointer new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin); virtual QSharedPointer findServer(KTextEditor::Document *document, bool updatedoc = true) = 0; virtual QSharedPointer findServer(KTextEditor::View *view, bool updatedoc = true) = 0; virtual void update(KTextEditor::Document *doc, bool force) = 0; virtual void restart(LSPClientServer *server) = 0; virtual void setIncrementalSync(bool inc) = 0; // latest sync'ed revision of doc (-1 if N/A) virtual qint64 revision(KTextEditor::Document *doc) = 0; // lock all relevant documents' current revision and sync that to server // locks are released when returned snapshot is delete'd virtual LSPClientRevisionSnapshot *snapshot(LSPClientServer *server) = 0; + // helper method providing descriptive label for a server + static QString serverDescription(LSPClientServer *server) + { + if (server) { + auto root = server->root().toLocalFile(); + return QStringLiteral("%1@%2").arg(server->langId()).arg(root); + } else { + return {}; + } + } + public: Q_SIGNALS: void serverChanged(); + void showMessage(KTextEditor::Message::MessageType level, const QString &msg); }; class LSPClientRevisionSnapshot : public QObject { Q_OBJECT public: // find a locked revision for url in snapshot virtual void find(const QUrl &url, KTextEditor::MovingInterface *&miface, qint64 &revision) const = 0; }; #endif diff --git a/addons/lspclient/lspconfigwidget.ui b/addons/lspclient/lspconfigwidget.ui index bdde9afe8..66d779d13 100644 --- a/addons/lspclient/lspconfigwidget.ui +++ b/addons/lspclient/lspconfigwidget.ui @@ -1,240 +1,287 @@ LspConfigWidget 0 0 758 907 0 0 0 0 0 Client Settings General Options Show selected completion documentation Include declaration in references Show hover information Format on typing Incremental document synchronization Enable semantic highlighting Show diagnostics notifications Add highlights Add markers + + + + + + Show messages + + + + + + + Switch to messages tab upon level + + + + + + + + Never + + + + + Error + + + + + Warning + + + + + Information + + + + + Log + + + + + + Symbol Outline Options Display symbol details Tree mode outline Automatically expand nodes in tree mode Sort symbols alphabetically Qt::Vertical 20 40 User Server Settings Settings File: QTextEdit::NoWrap false Default Server Settings QTextEdit::NoWrap false KUrlRequester QWidget
kurlrequester.h
diff --git a/addons/lspclient/ui.rc b/addons/lspclient/ui.rc index 72281015b..8b0d67c85 100644 --- a/addons/lspclient/ui.rc +++ b/addons/lspclient/ui.rc @@ -1,35 +1,39 @@ - + LSP Client + + + +