diff --git a/addons/lspclient/lspclientplugin.cpp b/addons/lspclient/lspclientplugin.cpp index 96f6a7599..a7356dd61 100644 --- a/addons/lspclient/lspclientplugin.cpp +++ b/addons/lspclient/lspclientplugin.cpp @@ -1,114 +1,125 @@ /* 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 "lspclientpluginview.h" #include "lspclientconfigpage.h" +#include "lspclient_debug.h" + #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_DIAGNOSTICS { QStringLiteral("Diagnostics") }; static const QString CONFIG_DIAGNOSTICS_HIGHLIGHT { QStringLiteral("DiagnosticsHighlight") }; static const QString CONFIG_DIAGNOSTICS_MARK { QStringLiteral("DiagnosticsMark") }; static const QString CONFIG_SERVER_CONFIG { QStringLiteral("ServerConfiguration") }; K_PLUGIN_FACTORY_WITH_JSON(LSPClientPluginFactory, "lspclientplugin.json", registerPlugin();) LSPClientPlugin::LSPClientPlugin(QObject *parent, const QList &) : KTextEditor::Plugin(parent) { + /** + * 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")); + } + 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_diagnostics = config.readEntry(CONFIG_DIAGNOSTICS, true); m_diagnosticsHighlight = config.readEntry(CONFIG_DIAGNOSTICS_HIGHLIGHT, true); m_diagnosticsMark = config.readEntry(CONFIG_DIAGNOSTICS_MARK, true); m_configPath = config.readEntry(CONFIG_SERVER_CONFIG, QUrl()); 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_DIAGNOSTICS, m_diagnostics); config.writeEntry(CONFIG_DIAGNOSTICS_HIGHLIGHT, m_diagnosticsHighlight); config.writeEntry(CONFIG_DIAGNOSTICS_MARK, m_diagnosticsMark); config.writeEntry(CONFIG_SERVER_CONFIG, m_configPath); emit update(); } #include "lspclientplugin.moc" diff --git a/addons/lspclient/lspclientplugin.h b/addons/lspclient/lspclientplugin.h index 010a880fc..d2772a9b3 100644 --- a/addons/lspclient/lspclientplugin.h +++ b/addons/lspclient/lspclientplugin.h @@ -1,68 +1,71 @@ /* 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; // 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; QUrl m_configPath; + // debug mode? + bool m_debugMode = false; + private: Q_SIGNALS: // signal settings update void update() const; }; #endif diff --git a/addons/lspclient/lspclientservermanager.cpp b/addons/lspclient/lspclientservermanager.cpp index 0d78eadc9..40223ebea 100644 --- a/addons/lspclient/lspclientservermanager.cpp +++ b/addons/lspclient/lspclientservermanager.cpp @@ -1,685 +1,685 @@ /* 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. */ /* * Some explanation here on server configuration JSON, pending such ending up * in real user level documentation ... * * The default configuration in JSON format is roughly as follows; { "global": { "root": null, }, "servers": { "Python": { "command": "python3 -m pyls --check-parent-process" }, "C": { "command": "clangd -log=verbose --background-index" }, "C++": { "use": "C++" } } } * (the "command" can be an array or a string, which is then split into array) * * From the above, the gist is presumably clear. In addition, each server * entry object may also have an "initializationOptions" entry, which is passed * along to the server as part of the 'initialize' method. A clangd-specific * HACK^Hfeature uses this to add "compilationDatabasePath". * * Various stages of override/merge are applied; * + user configuration (loaded from file) overrides (internal) default configuration * + "lspclient" entry in projectMap overrides the above * + the resulting "global" entry is used to supplement (not override) any server entry * * One server instance is used per (root, servertype) combination. * If "root" is not specified, it default to the $HOME directory. If it is * specified as an absolute path, then it used as-is, otherwise it is relative * to the projectBase. For any document, the resulting "root" then determines * whether or not a separate instance is needed. If so, the "root" is passed * as rootUri/rootPath. * * In general, it is recommended to leave root unspecified, as it is not that * important for a server (your mileage may vary though). Fewer instances * are obviously more efficient, and they also have a 'wider' view than * the view of many separate instances. */ #include "lspclientservermanager.h" #include "lspclient_debug.h" #include #include #include #include #include #include #include #include #include #include #include #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)), m_revision(-1) { if (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); /* NOTE: * The implementation (at least one) of range translation comes down to * katetexthistory.cpp. While there are asserts in place that check * for a valid revision parameter, those checks are *only* in assert * and will otherwise result in nasty index access. So it is vital * that a valid revision is always given. However, there is no way * to check for a valid revision using the interface only, and documentation * refers to (dangerous) effects of clear() and reload() (that are not * part of the interface, neither clearly of Document interface). * So it then becomes crucial that the following signal covers all * cases where a revision might be invalidated. * Even if so, it would be preferably being able to check for a valid/known * revision at any other time by other means (e.g. interface method). */ auto conn = connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(clearRevisions(KTextEditor::Document*))); Q_ASSERT(conn); #if 0 // disable until confirmation/clarification on the above m_guards.emplace(doc->url(), doc); #endif } 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 DocumentInfo { QSharedPointer server; KTextEditor::MovingInterface *movingInterface; QUrl url; qint64 version; bool open; bool modified; }; LSPClientPlugin *m_plugin; KTextEditor::MainWindow *m_mainWindow; // merged default and user config QJsonObject m_serverConfig; // root -> (mode -> server) QMap>> m_servers; QHash m_docs; 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() { // 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); auto run = [&q, &t] (int ms) { t.setSingleShot(true); t.start(ms); q.exec(); }; int count = 0; for (const auto & el: m_servers) { for (const auto & s: el) { disconnect(s.get(), 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.get(), &LSPClientServer::stateChanged, this, handler); ++count; s->stop(-1, -1); } } } run(500); // stage 2 and 3 count = 0; for (count = 0; count < 2; ++count) { for (const auto & el: m_servers) { for (const auto & s: el) { s->stop(count == 0 ? 1 : -1, count == 0 ? -1 : 1); } } run(100); } } 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.get(), 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->get() == server) { servers.push_back(*it); it = m.erase(it); } else { ++it; } } } restart(servers); } virtual 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); } // 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.get(), 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) { // clear for normal operation emit serverChanged(); } else if (server->state() == LSPClientServer::State::None) { // went down showMessage(i18n("Server terminated unexpectedly: %1", server->cmdline().join(QLatin1Char(' '))), KTextEditor::Message::Warning); restart(server); } } QJsonValue applyCustomInit(const QJsonValue & init, const QStringList & cmdline, const QUrl & root, QObject * projectView) { (void)root; // clangd specific if (cmdline.at(0).indexOf(QStringLiteral("clangd")) >= 0) { const auto& projectMap = projectView ? projectView->property("projectMap").toMap() : QVariantMap(); // prefer the build directory, if set, e.g. for CMake generated .kateproject files auto buildDir = projectMap.value(QStringLiteral("build")).toMap().value(QStringLiteral("directory")).toString(); // fallback to base directory of .kateproject file if build doesn't contain a compilation database if (buildDir.isEmpty() || !QFile::exists(buildDir + QStringLiteral("/compile_commands.json"))) { buildDir = projectView ? projectView->property("projectBaseDir").toString() : QString(); } auto obinit = init.toObject(); // NOTE: // alternatively, use symlink to have clang locate the db by parent dir search // which allows it to handle multiple compilation db // (since this custom way only allows passing it to a specific instance) // FIXME perhaps also use workspace/didChangeConfiguration to update compilation db ?? // (but symlink is then simpler for now ;-) ) // ... at which time need a nicer way to involve server specific interventions obinit[QStringLiteral("compilationDatabasePath")] = buildDir; return obinit; } return init; } QSharedPointer _findServer(KTextEditor::Document *document) { 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(); auto mode = document->highlightingMode(); // 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; while (true) { qCInfo(LSPCLIENT) << "mode " << mode; used << mode; config = serverConfig.value(QStringLiteral("servers")).toObject().value(mode); if (config.isObject()) { const auto & base = config.toObject().value(QStringLiteral("use")).toString(); // basic cycle detection if (!base.isEmpty() && !used.contains(base)) { mode = 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); } } if (rootpath.isEmpty()) { rootpath = QDir::homePath(); } auto root = QUrl::fromLocalFile(rootpath); auto server = m_servers.value(root).value(mode); if (!server) { QStringList cmdline; auto 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) { auto&& init = serverConfig.value(QStringLiteral("initializationOptions")); init = applyCustomInit(init, cmdline, root, projectView); server.reset(new LSPClientServer(cmdline, root, init)); m_servers[root][mode] = server; connect(server.get(), &LSPClientServer::stateChanged, this, &self_type::onStateChanged, Qt::UniqueConnection); if (!server->start()) { showMessage(i18n("Failed to start server: %1", cmdline.join(QLatin1Char(' '))), KTextEditor::Message::Error); } } } return (server && server->state() == LSPClientServer::State::Running) ? server : nullptr; } void updateServerConfig() { // default configuration auto makeServerConfig = [] (const QString & cmdline) { return QJsonObject { { QStringLiteral("command"), cmdline } }; }; static auto defaultConfig = QJsonObject { { QStringLiteral("servers"), QJsonObject { { QStringLiteral("Python"), makeServerConfig(QStringLiteral("python3 -m pyls --check-parent-process")) }, { QStringLiteral("C"), - makeServerConfig(QStringLiteral("clangd -log=verbose --background-index")) }, + makeServerConfig(QStringLiteral("clangd -log=%1 --background-index").arg(m_plugin->m_debugMode ? QStringLiteral("verbose") : QStringLiteral("error"))) }, { QStringLiteral("C++"), QJsonObject { { QStringLiteral("use"), QStringLiteral("C") } } } } } }; m_serverConfig = defaultConfig; // consider specified configuration const auto& configPath = m_plugin->m_configPath.path(); if (!configPath.isEmpty()) { QFile f(configPath); if (f.open(QIODevice::ReadOnly)) { auto data = f.readAll(); auto json = QJsonDocument::fromJson(data); if (json.isObject()) { m_serverConfig = merge(m_serverConfig, json.object()); } else { showMessage(i18n("Failed to parse server configuration: %1", configPath), KTextEditor::Message::Error); } } else { showMessage(i18n("Failed to read server configuration: %1", configPath), KTextEditor::Message::Error); } } // 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, 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, true}); // 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); } 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) { if (it->movingInterface) { it->version = it->movingInterface->revision(); } else if (it->modified) { ++it->version; } if (it->open) { if (it->modified || force) { (it->server)->didChange(it->url, it->version, doc->text()); it->modified = false; } } else { (it->server)->didOpen(it->url, it->version, doc->text()); it->open = true; } } } 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; } } }; QSharedPointer LSPClientServerManager::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) { return QSharedPointer(new LSPClientServerManagerImpl(plugin, mainWin)); } #include "lspclientservermanager.moc"