File indexing completed on 2024-04-28 05:49:00

0001 /*
0002     SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 /* see plugins.docbook lspclient-configuration
0008  * for client configuration documentation
0009  */
0010 
0011 #include "lspclientservermanager.h"
0012 
0013 #include "hostprocess.h"
0014 #include "ktexteditor_utils.h"
0015 #include "lspclient_debug.h"
0016 
0017 #include <KLocalizedString>
0018 #include <KTextEditor/Application>
0019 #include <KTextEditor/Document>
0020 #include <KTextEditor/Editor>
0021 #include <KTextEditor/MainWindow>
0022 #include <KTextEditor/View>
0023 
0024 #include <QDir>
0025 #include <QFileInfo>
0026 #include <QJsonDocument>
0027 #include <QJsonObject>
0028 #include <QJsonParseError>
0029 #include <QRegularExpression>
0030 #include <QStandardPaths>
0031 #include <QThread>
0032 #include <QTime>
0033 #include <QTimer>
0034 
0035 #include <json_utils.h>
0036 
0037 // sadly no common header for plugins to include this from
0038 // unless we do come up with such one
0039 typedef QMap<QString, QString> QStringMap;
0040 Q_DECLARE_METATYPE(QStringMap)
0041 
0042 // helper to find a proper root dir for the given document & file name/pattern that indicates the root dir
0043 static QString findRootForDocument(KTextEditor::Document *document, const QStringList &rootIndicationFileNames, const QStringList &rootIndicationFilePatterns)
0044 {
0045     // skip search if nothing there to look at
0046     if (rootIndicationFileNames.isEmpty() && rootIndicationFilePatterns.isEmpty()) {
0047         return QString();
0048     }
0049 
0050     // search only feasible if document is local file
0051     if (!document->url().isLocalFile()) {
0052         return QString();
0053     }
0054 
0055     // search root upwards
0056     QDir dir(QFileInfo(document->url().toLocalFile()).absolutePath());
0057     QSet<QString> seenDirectories;
0058     while (!seenDirectories.contains(dir.absolutePath())) {
0059         // update guard
0060         seenDirectories.insert(dir.absolutePath());
0061 
0062         // the file that indicates the root dir is there => all fine
0063         for (const auto &fileName : rootIndicationFileNames) {
0064             if (dir.exists(fileName)) {
0065                 return dir.absolutePath();
0066             }
0067         }
0068 
0069         // look for matching file patterns, if any
0070         if (!rootIndicationFilePatterns.isEmpty()) {
0071             dir.setNameFilters(rootIndicationFilePatterns);
0072             if (!dir.entryList().isEmpty()) {
0073                 return dir.absolutePath();
0074             }
0075         }
0076 
0077         // else: cd up, if possible or abort
0078         if (!dir.cdUp()) {
0079             break;
0080         }
0081     }
0082 
0083     // no root found, bad luck
0084     return QString();
0085 }
0086 
0087 static QStringList indicationDataToStringList(const QJsonValue &indicationData)
0088 {
0089     if (indicationData.isArray()) {
0090         QStringList indications;
0091         for (auto indication : indicationData.toArray()) {
0092             if (indication.isString()) {
0093                 indications << indication.toString();
0094             }
0095         }
0096 
0097         return indications;
0098     }
0099 
0100     return {};
0101 }
0102 
0103 static LSPClientServer::TriggerCharactersOverride parseTriggerOverride(const QJsonValue &json)
0104 {
0105     LSPClientServer::TriggerCharactersOverride adjust;
0106     if (json.isObject()) {
0107         auto ob = json.toObject();
0108         for (const auto &c : ob.value(QStringLiteral("exclude")).toString()) {
0109             adjust.exclude.push_back(c);
0110         }
0111         for (const auto &c : ob.value(QStringLiteral("include")).toString()) {
0112             adjust.include.push_back(c);
0113         }
0114     }
0115     return adjust;
0116 }
0117 
0118 #include <memory>
0119 
0120 // helper guard to handle revision (un)lock
0121 struct RevisionGuard {
0122     QPointer<KTextEditor::Document> m_doc;
0123     qint64 m_revision = -1;
0124 
0125     RevisionGuard(KTextEditor::Document *doc = nullptr)
0126         : m_doc(doc)
0127     {
0128         m_revision = doc->revision();
0129         doc->lockRevision(m_revision);
0130     }
0131 
0132     // really only need/allow this one (out of 5)
0133     RevisionGuard(RevisionGuard &&other)
0134         : RevisionGuard(nullptr)
0135     {
0136         std::swap(m_doc, other.m_doc);
0137         std::swap(m_revision, other.m_revision);
0138     }
0139 
0140     void release()
0141     {
0142         m_revision = -1;
0143     }
0144 
0145     ~RevisionGuard()
0146     {
0147         // NOTE: hopefully the revision is still valid at this time
0148         if (m_doc && m_revision >= 0) {
0149             m_doc->unlockRevision(m_revision);
0150         }
0151     }
0152 };
0153 
0154 class LSPClientRevisionSnapshotImpl : public LSPClientRevisionSnapshot
0155 {
0156     Q_OBJECT
0157 
0158     typedef LSPClientRevisionSnapshotImpl self_type;
0159 
0160     // std::map has more relaxed constraints on value_type
0161     std::map<QUrl, RevisionGuard> m_guards;
0162 
0163     Q_SLOT
0164     void clearRevisions(KTextEditor::Document *doc)
0165     {
0166         for (auto &item : m_guards) {
0167             if (item.second.m_doc == doc) {
0168                 item.second.release();
0169             }
0170         }
0171     }
0172 
0173 public:
0174     void add(KTextEditor::Document *doc)
0175     {
0176         Q_ASSERT(doc);
0177 
0178         // make sure revision is cleared when needed and no longer used (to unlock or otherwise)
0179         // see e.g. implementation in katetexthistory.cpp and assert's in place there
0180         connect(doc, &KTextEditor::Document::aboutToInvalidateMovingInterfaceContent, this, &self_type::clearRevisions);
0181         connect(doc, &KTextEditor::Document::aboutToDeleteMovingInterfaceContent, this, &self_type::clearRevisions);
0182         m_guards.emplace(doc->url(), doc);
0183     }
0184 
0185     void find(const QUrl &url, KTextEditor::Document *&doc, qint64 &revision) const override
0186     {
0187         auto it = m_guards.find(url);
0188         if (it != m_guards.end()) {
0189             doc = it->second.m_doc;
0190             revision = it->second.m_revision;
0191         } else {
0192             doc = nullptr;
0193             revision = -1;
0194         }
0195     }
0196 };
0197 
0198 static const QString PROJECT_PLUGIN{QStringLiteral("kateprojectplugin")};
0199 
0200 // helper class to sync document changes to LSP server
0201 class LSPClientServerManagerImpl : public LSPClientServerManager
0202 {
0203     Q_OBJECT
0204 
0205     typedef LSPClientServerManagerImpl self_type;
0206 
0207     struct ServerInfo {
0208         std::shared_ptr<LSPClientServer> server;
0209         // config specified server url
0210         QString url;
0211         QTime started;
0212         int failcount = 0;
0213         // pending settings to be submitted
0214         QJsonValue settings;
0215         // use of workspace folders allowed
0216         bool useWorkspace = false;
0217     };
0218 
0219     struct DocumentInfo {
0220         std::shared_ptr<LSPClientServer> server;
0221         // merged server config as obtain from various sources
0222         QJsonObject config;
0223         KTextEditor::Document *doc;
0224         QUrl url;
0225         qint64 version;
0226         bool open : 1;
0227         bool modified : 1;
0228         // used for incremental update (if non-empty)
0229         QList<LSPTextDocumentContentChangeEvent> changes;
0230     };
0231 
0232     LSPClientPlugin *m_plugin;
0233     QPointer<QObject> m_projectPlugin;
0234     // merged default and user config
0235     QJsonObject m_serverConfig;
0236     // root -> (mode -> server)
0237     QMap<QUrl, QMap<QString, ServerInfo>> m_servers;
0238     QHash<KTextEditor::Document *, DocumentInfo> m_docs;
0239     bool m_incrementalSync = false;
0240     LSPClientCapabilities m_clientCapabilities;
0241 
0242     // highlightingModeRegex => language id
0243     std::vector<std::pair<QRegularExpression, QString>> m_highlightingModeRegexToLanguageId;
0244     // cache of highlighting mode => language id, to avoid massive regex matching
0245     QHash<QString, QString> m_highlightingModeToLanguageIdCache;
0246     // whether to pass the language id (key) to server when opening document
0247     // most either do not care about the id, or can find out themselves
0248     // (and might get confused if we pass a not so accurate one)
0249     QHash<QString, bool> m_documentLanguageId;
0250     typedef QList<std::shared_ptr<LSPClientServer>> ServerList;
0251 
0252     // Servers which were not found to be installed. We use this
0253     // variable to avoid warning more than once
0254     QSet<QString> m_failedToFindServers;
0255 
0256 public:
0257     LSPClientServerManagerImpl(LSPClientPlugin *plugin)
0258         : m_plugin(plugin)
0259     {
0260         connect(plugin, &LSPClientPlugin::update, this, &self_type::updateServerConfig);
0261         QTimer::singleShot(100, this, &self_type::updateServerConfig);
0262 
0263         // stay tuned on project situation
0264         auto app = KTextEditor::Editor::instance()->application();
0265         auto h = [this](const QString &name, KTextEditor::Plugin *plugin) {
0266             if (name == PROJECT_PLUGIN) {
0267                 m_projectPlugin = plugin;
0268                 monitorProjects(plugin);
0269             }
0270         };
0271         connect(app, &KTextEditor::Application::pluginCreated, this, h);
0272         auto projectPlugin = app->plugin(PROJECT_PLUGIN);
0273         m_projectPlugin = projectPlugin;
0274         monitorProjects(projectPlugin);
0275     }
0276 
0277     ~LSPClientServerManagerImpl() override
0278     {
0279         // stop everything as we go down
0280         // several stages;
0281         // stage 1; request shutdown of all servers (in parallel)
0282         // (give that some time)
0283         // stage 2; send TERM
0284         // stage 3; send KILL
0285 
0286         // stage 1
0287 
0288         /* some msleep are used below which is somewhat BAD as it blocks/hangs
0289          * the mainloop, however there is not much alternative:
0290          * + running an inner mainloop leads to event processing,
0291          *   which could trigger an unexpected sequence of 'events'
0292          *   such as (re)loading plugin that is currently still unloading
0293          *   (consider scenario of fast-clicking enable/disable of LSP plugin)
0294          * + could reduce or forego the sleep, but that increases chances
0295          *   on an unclean shutdown of LSP server, which may or may not
0296          *   be able to handle that properly (so let's try and be a polite
0297          *   client and try to avoid that to some degree)
0298          * So we are left with a minor sleep compromise ...
0299          */
0300 
0301         int count = 0;
0302         for (const auto &el : qAsConst(m_servers)) {
0303             for (const auto &si : el) {
0304                 auto &s = si.server;
0305                 if (!s) {
0306                     continue;
0307                 }
0308                 disconnect(s.get(), nullptr, this, nullptr);
0309                 if (s->state() != LSPClientServer::State::None) {
0310                     ++count;
0311                     s->stop(-1, -1);
0312                 }
0313             }
0314         }
0315         if (count) {
0316             QThread::msleep(500);
0317         } else {
0318             return;
0319         }
0320 
0321         // stage 2 and 3
0322         count = 0;
0323         for (count = 0; count < 2; ++count) {
0324             bool wait = false;
0325             for (const auto &el : qAsConst(m_servers)) {
0326                 for (const auto &si : el) {
0327                     auto &s = si.server;
0328                     if (!s) {
0329                         continue;
0330                     }
0331                     wait = true;
0332                     s->stop(count == 0 ? 1 : -1, count == 0 ? -1 : 1);
0333                 }
0334             }
0335             if (wait && count == 0) {
0336                 QThread::msleep(100);
0337             }
0338         }
0339     }
0340 
0341     // map (highlight)mode to lsp languageId
0342     QString _languageId(const QString &mode)
0343     {
0344         // query cache first
0345         const auto cacheIt = m_highlightingModeToLanguageIdCache.find(mode);
0346         if (cacheIt != m_highlightingModeToLanguageIdCache.end()) {
0347             return cacheIt.value();
0348         }
0349 
0350         // match via regexes + cache result
0351         for (const auto &it : m_highlightingModeRegexToLanguageId) {
0352             if (it.first.match(mode).hasMatch()) {
0353                 m_highlightingModeToLanguageIdCache[mode] = it.second;
0354                 return it.second;
0355             }
0356         }
0357 
0358         // else: we have no matching server!
0359         m_highlightingModeToLanguageIdCache[mode] = QString();
0360         return QString();
0361     }
0362 
0363     QString languageId(KTextEditor::Document *doc)
0364     {
0365         if (!doc) {
0366             return {};
0367         }
0368 
0369         // prefer the mode over the highlighting to allow to
0370         // use known a highlighting with existing LSP
0371         // for a mode for a new language and own LSP
0372         // see bug 474887
0373         if (const auto langId = _languageId(doc->mode()); !langId.isEmpty()) {
0374             return langId;
0375         }
0376 
0377         return _languageId(doc->highlightingMode());
0378     }
0379 
0380     QObject *projectPluginView(KTextEditor::MainWindow *mainWindow)
0381     {
0382         return mainWindow->pluginView(PROJECT_PLUGIN);
0383     }
0384 
0385     QString documentLanguageId(KTextEditor::Document *doc)
0386     {
0387         auto langId = languageId(doc);
0388         const auto it = m_documentLanguageId.find(langId);
0389         // FIXME ?? perhaps use default false
0390         // most servers can find out much better on their own
0391         // (though it would actually have to be confirmed as such)
0392         bool useId = true;
0393         if (it != m_documentLanguageId.end()) {
0394             useId = it.value();
0395         }
0396 
0397         return useId ? langId : QString();
0398     }
0399 
0400     void setIncrementalSync(bool inc) override
0401     {
0402         m_incrementalSync = inc;
0403     }
0404 
0405     LSPClientCapabilities &clientCapabilities() override
0406     {
0407         return m_clientCapabilities;
0408     }
0409 
0410     std::shared_ptr<LSPClientServer> findServer(KTextEditor::View *view, bool updatedoc = true) override
0411     {
0412         if (!view) {
0413             return nullptr;
0414         }
0415 
0416         auto document = view->document();
0417         if (!document || document->url().isEmpty()) {
0418             return nullptr;
0419         }
0420 
0421         auto it = m_docs.find(document);
0422         auto server = it != m_docs.end() ? it->server : nullptr;
0423         if (!server) {
0424             QJsonObject serverConfig;
0425             if ((server = _findServer(view, document, serverConfig))) {
0426                 trackDocument(document, server, serverConfig);
0427             }
0428         }
0429 
0430         if (server && updatedoc) {
0431             update(server.get(), false);
0432         }
0433         return server;
0434     }
0435 
0436     virtual QJsonValue findServerConfig(KTextEditor::Document *document) override
0437     {
0438         // check if document has been seen/processed by now
0439         auto it = m_docs.find(document);
0440         auto config = it != m_docs.end() ? QJsonValue(it->config) : QJsonValue::Null;
0441         return config;
0442     }
0443 
0444     // restart a specific server or all servers if server == nullptr
0445     void restart(LSPClientServer *server) override
0446     {
0447         ServerList servers;
0448         // find entry for server(s) and move out
0449         for (auto &m : m_servers) {
0450             for (auto it = m.begin(); it != m.end();) {
0451                 if (!server || it->server.get() == server) {
0452                     servers.push_back(it->server);
0453                     it = m.erase(it);
0454                 } else {
0455                     ++it;
0456                 }
0457             }
0458         }
0459         restart(servers, server == nullptr);
0460     }
0461 
0462     qint64 revision(KTextEditor::Document *doc) override
0463     {
0464         auto it = m_docs.find(doc);
0465         return it != m_docs.end() ? it->version : -1;
0466     }
0467 
0468     LSPClientRevisionSnapshot *snapshot(LSPClientServer *server) override
0469     {
0470         auto result = new LSPClientRevisionSnapshotImpl;
0471         for (auto it = m_docs.begin(); it != m_docs.end(); ++it) {
0472             if (it->server.get() == server) {
0473                 // sync server to latest revision that will be recorded
0474                 update(it.key(), false);
0475                 result->add(it.key());
0476             }
0477         }
0478         return result;
0479     }
0480 
0481 private:
0482     void showMessage(const QString &msg, KTextEditor::Message::MessageType level)
0483     {
0484         // inform interested view(er) which will decide how/where to show
0485         Q_EMIT m_plugin->showMessage(level, msg);
0486     }
0487 
0488     // caller ensures that servers are no longer present in m_servers
0489     void restart(const ServerList &servers, bool reload = false)
0490     {
0491         // close docs
0492         for (const auto &server : servers) {
0493             if (!server) {
0494                 continue;
0495             }
0496             // controlling server here, so disable usual state tracking response
0497             disconnect(server.get(), nullptr, this, nullptr);
0498             for (auto it = m_docs.begin(); it != m_docs.end();) {
0499                 auto &item = it.value();
0500                 if (item.server == server) {
0501                     // no need to close if server not in proper state
0502                     if (server->state() != LSPClientServer::State::Running) {
0503                         item.open = false;
0504                     }
0505                     it = _close(it, true);
0506                 } else {
0507                     ++it;
0508                 }
0509             }
0510         }
0511 
0512         // helper captures servers
0513         auto stopservers = [servers](int t, int k) {
0514             for (const auto &server : servers) {
0515                 if (server) {
0516                     server->stop(t, k);
0517                 }
0518             }
0519         };
0520 
0521         // trigger server shutdown now
0522         stopservers(-1, -1);
0523 
0524         // initiate delayed stages (TERM and KILL)
0525         // async, so give a bit more time
0526         QTimer::singleShot(2 * TIMEOUT_SHUTDOWN, this, [stopservers]() {
0527             stopservers(1, -1);
0528         });
0529         QTimer::singleShot(4 * TIMEOUT_SHUTDOWN, this, [stopservers]() {
0530             stopservers(-1, 1);
0531         });
0532 
0533         // as for the start part
0534         // trigger interested parties, which will again request a server as needed
0535         // let's delay this; less chance for server instances to trip over each other
0536         QTimer::singleShot(6 * TIMEOUT_SHUTDOWN, this, [this, reload]() {
0537             // this may be a good time to refresh server config
0538             if (reload) {
0539                 // will also trigger as mentioned above
0540                 updateServerConfig();
0541             } else {
0542                 Q_EMIT serverChanged();
0543             }
0544         });
0545     }
0546 
0547     void onStateChanged(LSPClientServer *server)
0548     {
0549         if (server->state() == LSPClientServer::State::Running) {
0550             // send settings if pending
0551             ServerInfo *info = nullptr;
0552             for (auto &m : m_servers) {
0553                 for (auto &si : m) {
0554                     if (si.server.get() == server) {
0555                         info = &si;
0556                         break;
0557                     }
0558                 }
0559             }
0560             if (info && !info->settings.isUndefined()) {
0561                 server->didChangeConfiguration(info->settings);
0562             }
0563             // provide initial workspace folder situation
0564             // this is done here because the folder notification pre-dates
0565             // the workspaceFolders property in 'initialize'
0566             // there is also no way to know whether the server supports that
0567             // and in fact some servers do "support workspace folders" (e.g. notification)
0568             // but do not know about the 'initialize' property
0569             // so, in summary, the notification is used here rather than the property
0570             const auto &caps = server->capabilities();
0571             if (caps.workspaceFolders.changeNotifications && info && info->useWorkspace) {
0572                 if (auto folders = currentWorkspaceFolders(); !folders.isEmpty()) {
0573                     server->didChangeWorkspaceFolders(folders, {});
0574                 }
0575             }
0576             // clear for normal operation
0577             Q_EMIT serverChanged();
0578         } else if (server->state() == LSPClientServer::State::None) {
0579             // went down
0580             // find server info to see how bad this is
0581             // if this is an occasional termination/crash ... ok then
0582             // if this happens quickly (bad/missing server, wrong cmdline/config), then no restart
0583             std::shared_ptr<LSPClientServer> sserver;
0584             QString url;
0585             bool retry = true;
0586             for (auto &m : m_servers) {
0587                 for (auto &si : m) {
0588                     if (si.server.get() == server) {
0589                         url = si.url;
0590                         if (si.started.secsTo(QTime::currentTime()) < 60) {
0591                             ++si.failcount;
0592                         }
0593                         // clear the entry, which will be re-filled if needed
0594                         // otherwise, leave it in place as a dead mark not to re-create one in _findServer
0595                         if (si.failcount < 2) {
0596                             std::swap(sserver, si.server);
0597                         } else {
0598                             sserver = si.server;
0599                             retry = false;
0600                         }
0601                     }
0602                 }
0603             }
0604             auto action = retry ? i18n("Restarting") : i18n("NOT Restarting");
0605             showMessage(i18n("Server terminated unexpectedly ... %1 [%2] [homepage: %3] ", action, server->cmdline().join(QLatin1Char(' ')), url),
0606                         KTextEditor::Message::Warning);
0607             if (sserver) {
0608                 // sserver might still be in m_servers
0609                 // but since it died already bringing it down will have no (ill) effect
0610                 restart({sserver});
0611             }
0612         }
0613     }
0614 
0615     std::shared_ptr<LSPClientServer> _findServer(KTextEditor::View *view, KTextEditor::Document *document, QJsonObject &mergedConfig)
0616     {
0617         // compute the LSP standardized language id, none found => no change
0618         auto langId = languageId(document);
0619         if (langId.isEmpty()) {
0620             return nullptr;
0621         }
0622 
0623         // get project plugin infos if available
0624         const auto projectBase = Utils::projectBaseDirForDocument(document);
0625         const auto projectMap = Utils::projectMapForDocument(document);
0626 
0627         // merge with project specific
0628         auto projectConfig = QJsonDocument::fromVariant(projectMap).object().value(QStringLiteral("lspclient")).toObject();
0629         auto serverConfig = json::merge(m_serverConfig, projectConfig);
0630 
0631         // locate server config
0632         QJsonValue config;
0633         QSet<QString> used;
0634         // reduce langId
0635         auto realLangId = langId;
0636         while (true) {
0637             qCInfo(LSPCLIENT) << "language id " << langId;
0638             used << langId;
0639             config = serverConfig.value(QStringLiteral("servers")).toObject().value(langId);
0640             if (config.isObject()) {
0641                 const auto &base = config.toObject().value(QStringLiteral("use")).toString();
0642                 // basic cycle detection
0643                 if (!base.isEmpty() && !used.contains(base)) {
0644                     langId = base;
0645                     continue;
0646                 }
0647             }
0648             break;
0649         }
0650 
0651         if (!config.isObject()) {
0652             return nullptr;
0653         }
0654 
0655         // merge global settings
0656         serverConfig = json::merge(serverConfig.value(QStringLiteral("global")).toObject(), config.toObject());
0657 
0658         // used for variable substitution in the sequl
0659         // NOTE that also covers a form of environment substitution using %{ENV:XYZ}
0660         auto editor = KTextEditor::Editor::instance();
0661 
0662         std::optional<QString> rootpath;
0663         const auto rootv = serverConfig.value(QStringLiteral("root"));
0664         if (rootv.isString()) {
0665             auto sroot = rootv.toString();
0666             sroot = editor->expandText(sroot, view);
0667             if (QDir::isAbsolutePath(sroot)) {
0668                 rootpath = sroot;
0669             } else if (!projectBase.isEmpty()) {
0670                 rootpath = QDir(projectBase).absoluteFilePath(sroot);
0671             } else if (sroot.isEmpty()) {
0672                 // empty root; so we are convinced the server can handle null rootUri
0673                 rootpath = QString();
0674             } else if (const auto url = document->url(); url.isValid() && url.isLocalFile()) {
0675                 // likewise, but use safer traditional approach and specify rootUri
0676                 rootpath = QDir(QFileInfo(url.toLocalFile()).absolutePath()).absoluteFilePath(sroot);
0677             }
0678         }
0679 
0680         /**
0681          * no explicit set root dir? search for a matching root based on some name filters
0682          * this is required for some LSP servers like rls that don't handle that on their own like
0683          * clangd does
0684          */
0685         if (!rootpath) {
0686             const auto fileNamesForDetection = indicationDataToStringList(serverConfig.value(QStringLiteral("rootIndicationFileNames")));
0687             const auto filePatternsForDetection = indicationDataToStringList(serverConfig.value(QStringLiteral("rootIndicationFilePatterns")));
0688             const auto root = findRootForDocument(document, fileNamesForDetection, filePatternsForDetection);
0689             if (!root.isEmpty()) {
0690                 rootpath = root;
0691             }
0692         }
0693 
0694         // just in case ... ensure normalized result
0695         if (rootpath && !rootpath->isEmpty()) {
0696             auto cpath = QFileInfo(*rootpath).canonicalFilePath();
0697             if (!cpath.isEmpty()) {
0698                 rootpath = cpath;
0699             }
0700         }
0701 
0702         // is it actually safe/reasonable to use workspaces?
0703         // in practice, (at this time) servers do do not quite consider or support all that
0704         // so in that regard workspace folders represents a bit of "spec endulgance"
0705         // (along with quite some other aspects for that matter)
0706         //
0707         // if a server was/is able to handle a "generic root",
0708         //   let's assume it is safe to consider workspace folders if it explicitly claims such support
0709         // if, however, an explicit root was/is necessary,
0710         //   let's assume not safe
0711         // in either case, let configuration explicitly specify this
0712         bool useWorkspace = serverConfig.value(QStringLiteral("useWorkspace")).toBool(!rootpath ? true : false);
0713 
0714         // last fallback: home directory
0715         if (!rootpath) {
0716             rootpath = QDir::homePath();
0717         }
0718 
0719         auto root = rootpath && !rootpath->isEmpty() ? QUrl::fromLocalFile(*rootpath) : QUrl();
0720         auto &serverinfo = m_servers[root][langId];
0721         auto &server = serverinfo.server;
0722 
0723         // maybe there is a server with other root that is workspace capable
0724         if (!server && useWorkspace) {
0725             for (const auto &l : qAsConst(m_servers)) {
0726                 // for (auto it = l.begin(); it != l.end(); ++it) {
0727                 auto it = l.find(langId);
0728                 if (it != l.end()) {
0729                     if (auto oserver = it->server) {
0730                         const auto &caps = oserver->capabilities();
0731                         if (caps.workspaceFolders.supported && caps.workspaceFolders.changeNotifications && it->useWorkspace) {
0732                             // so this server can handle workspace folders and should know about project root
0733                             server = oserver;
0734                             break;
0735                         }
0736                     }
0737                 }
0738             }
0739         }
0740 
0741         QStringList cmdline;
0742         if (!server) {
0743             // need to find command line for server
0744             // choose debug command line for debug mode, fallback to command
0745             auto vcmdline = serverConfig.value(m_plugin->m_debugMode ? QStringLiteral("commandDebug") : QStringLiteral("command"));
0746             if (vcmdline.isUndefined()) {
0747                 vcmdline = serverConfig.value(QStringLiteral("command"));
0748             }
0749 
0750             auto scmdline = vcmdline.toString();
0751             if (!scmdline.isEmpty()) {
0752                 cmdline = scmdline.split(QLatin1Char(' '));
0753             } else {
0754                 const auto cmdOpts = vcmdline.toArray();
0755                 for (const auto &c : cmdOpts) {
0756                     cmdline.push_back(c.toString());
0757                 }
0758             }
0759 
0760             // some more expansion and substitution
0761             // unlikely to be used here, but anyway
0762             for (auto &e : cmdline) {
0763                 e = editor->expandText(e, view);
0764             }
0765         }
0766 
0767         if (cmdline.length() > 0) {
0768             // always update some info
0769             // (even if eventually no server found/started)
0770             serverinfo.settings = serverConfig.value(QStringLiteral("settings"));
0771             serverinfo.started = QTime::currentTime();
0772             serverinfo.url = serverConfig.value(QStringLiteral("url")).toString();
0773             // leave failcount as-is
0774             serverinfo.useWorkspace = useWorkspace;
0775 
0776             // ensure we always only take the server executable from the PATH or user defined paths
0777             // QProcess will take the executable even just from current working directory without this => BAD
0778             auto cmd = safeExecutableName(cmdline[0]);
0779 
0780             // optionally search in supplied path(s)
0781             const auto vpath = serverConfig.value(QStringLiteral("path")).toArray();
0782             if (cmd.isEmpty() && !vpath.isEmpty()) {
0783                 // collect and expand in case home dir or other (environment) variable reference is used
0784                 QStringList path;
0785                 for (const auto &e : vpath) {
0786                     auto p = e.toString();
0787                     p = editor->expandText(p, view);
0788                     path.push_back(p);
0789                 }
0790                 cmd = safeExecutableName(cmdline[0], path);
0791             }
0792 
0793             // we can only start the stuff if we did find the binary in the paths
0794             if (!cmd.isEmpty()) {
0795                 // use full path to avoid security issues
0796                 cmdline[0] = cmd;
0797             } else {
0798                 if (!m_failedToFindServers.contains(cmdline[0])) {
0799                     m_failedToFindServers.insert(cmdline[0]);
0800                     // we didn't find the server binary at all!
0801                     QString message = i18n("Failed to find server binary: %1", cmdline[0]);
0802                     const auto url = serverConfig.value(QStringLiteral("url")).toString();
0803                     if (!url.isEmpty()) {
0804                         message += QStringLiteral("\n") + i18n("Please check your PATH for the binary");
0805                         message += QStringLiteral("\n") + i18n("See also %1 for installation or details", url);
0806                     }
0807                     showMessage(message, KTextEditor::Message::Warning);
0808                 }
0809                 // clear to cut branch below
0810                 cmdline.clear();
0811             }
0812         }
0813 
0814         // check if allowed to start, function will query user if needed and emit messages
0815         if (cmdline.length() > 0 && !m_plugin->isCommandLineAllowed(cmdline)) {
0816             cmdline.clear();
0817         }
0818 
0819         // made it here with a command line; spin up server
0820         if (cmdline.length() > 0) {
0821             // an empty list is always passed here (or null)
0822             // the initial list is provided/updated using notification after start
0823             // since that is what a server is more aware of
0824             // and should support if it declares workspace folder capable
0825             // (as opposed to the new initialization property)
0826             LSPClientServer::FoldersType folders;
0827             if (useWorkspace) {
0828                 folders = QList<LSPWorkspaceFolder>();
0829             }
0830             // spin up using currently configured client capabilities
0831             auto &caps = m_clientCapabilities;
0832             // extract some more additional config
0833             auto completionOverride = parseTriggerOverride(serverConfig.value(QStringLiteral("completionTriggerCharacters")));
0834             auto signatureOverride = parseTriggerOverride(serverConfig.value(QStringLiteral("signatureTriggerCharacters")));
0835             // request server and setup
0836             server.reset(new LSPClientServer(cmdline,
0837                                              root,
0838                                              realLangId,
0839                                              serverConfig.value(QStringLiteral("initializationOptions")),
0840                                              {folders, caps, completionOverride, signatureOverride}));
0841             connect(server.get(), &LSPClientServer::stateChanged, this, &self_type::onStateChanged, Qt::UniqueConnection);
0842             if (!server->start(m_plugin->m_debugMode)) {
0843                 QString message = i18n("Failed to start server: %1", cmdline.join(QLatin1Char(' ')));
0844                 const auto url = serverConfig.value(QStringLiteral("url")).toString();
0845                 if (!url.isEmpty()) {
0846                     message += QStringLiteral("\n") + i18n("Please check your PATH for the binary");
0847                     message += QStringLiteral("\n") + i18n("See also %1 for installation or details", url);
0848                 }
0849                 showMessage(message, KTextEditor::Message::Warning);
0850             } else {
0851                 showMessage(i18n("Started server %2: %1", cmdline.join(QLatin1Char(' ')), serverDescription(server.get())), KTextEditor::Message::Positive);
0852                 using namespace std::placeholders;
0853                 server->connect(server.get(), &LSPClientServer::logMessage, this, std::bind(&self_type::onMessage, this, true, _1));
0854                 server->connect(server.get(), &LSPClientServer::showMessage, this, std::bind(&self_type::onMessage, this, false, _1));
0855                 server->connect(server.get(), &LSPClientServer::workDoneProgress, this, &self_type::onWorkDoneProgress);
0856                 server->connect(server.get(), &LSPClientServer::workspaceFolders, this, &self_type::onWorkspaceFolders, Qt::UniqueConnection);
0857                 server->connect(server.get(), &LSPClientServer::showMessageRequest, this, &self_type::showMessageRequest);
0858             }
0859         }
0860         // set out param value
0861         mergedConfig = serverConfig;
0862         return (server && server->state() == LSPClientServer::State::Running) ? server : nullptr;
0863     }
0864 
0865     void updateServerConfig()
0866     {
0867         // default configuration, compiled into plugin resource, reading can't fail
0868         QFile defaultConfigFile(QStringLiteral(":/lspclient/settings.json"));
0869         defaultConfigFile.open(QIODevice::ReadOnly);
0870         Q_ASSERT(defaultConfigFile.isOpen());
0871         m_serverConfig = QJsonDocument::fromJson(defaultConfigFile.readAll()).object();
0872 
0873         // consider specified configuration if existing
0874         const auto configPath = m_plugin->configPath().toLocalFile();
0875         if (!configPath.isEmpty() && QFile::exists(configPath)) {
0876             QFile f(configPath);
0877             if (f.open(QIODevice::ReadOnly)) {
0878                 const auto data = f.readAll();
0879                 if (!data.isEmpty()) {
0880                     QJsonParseError error{};
0881                     auto json = QJsonDocument::fromJson(data, &error);
0882                     if (error.error == QJsonParseError::NoError) {
0883                         if (json.isObject()) {
0884                             m_serverConfig = json::merge(m_serverConfig, json.object());
0885                         } else {
0886                             showMessage(i18n("Failed to parse server configuration '%1': no JSON object", configPath), KTextEditor::Message::Error);
0887                         }
0888                     } else {
0889                         showMessage(i18n("Failed to parse server configuration '%1': %2", configPath, error.errorString()), KTextEditor::Message::Error);
0890                     }
0891                 }
0892             } else {
0893                 showMessage(i18n("Failed to read server configuration: %1", configPath), KTextEditor::Message::Error);
0894             }
0895         }
0896 
0897         // build regex of highlightingMode => language id
0898         m_highlightingModeRegexToLanguageId.clear();
0899         m_highlightingModeToLanguageIdCache.clear();
0900         const auto servers = m_serverConfig.value(QLatin1String("servers")).toObject();
0901         for (auto it = servers.begin(); it != servers.end(); ++it) {
0902             // get highlighting mode regex for this server, if not set, fallback to just the name
0903             const auto &server = it.value().toObject();
0904             QString highlightingModeRegex = server.value(QLatin1String("highlightingModeRegex")).toString();
0905             if (highlightingModeRegex.isEmpty()) {
0906                 highlightingModeRegex = it.key();
0907             }
0908             m_highlightingModeRegexToLanguageId.emplace_back(QRegularExpression(highlightingModeRegex, QRegularExpression::CaseInsensitiveOption), it.key());
0909             // should we use the languageId in didOpen
0910             auto docLanguageId = server.value(QLatin1String("documentLanguageId"));
0911             if (docLanguageId.isBool()) {
0912                 m_documentLanguageId[it.key()] = docLanguageId.toBool();
0913             }
0914         }
0915         m_failedToFindServers.clear();
0916 
0917         // we could (but do not) perform restartAll here;
0918         // for now let's leave that up to user
0919         // but maybe we do have a server now where not before, so let's signal
0920         Q_EMIT serverChanged();
0921     }
0922 
0923     void trackDocument(KTextEditor::Document *doc, const std::shared_ptr<LSPClientServer> &server, QJsonObject serverConfig)
0924     {
0925         auto it = m_docs.find(doc);
0926         if (it == m_docs.end()) {
0927             // TODO: Further simplify once we are Qt6
0928             // track document
0929             connect(doc, &KTextEditor::Document::documentUrlChanged, this, &self_type::untrack, Qt::UniqueConnection);
0930             it = m_docs.insert(doc, {server, serverConfig, doc, doc->url(), 0, false, false, {}});
0931             connect(doc, &KTextEditor::Document::highlightingModeChanged, this, &self_type::untrack, Qt::UniqueConnection);
0932             connect(doc, &KTextEditor::Document::aboutToClose, this, &self_type::untrack, Qt::UniqueConnection);
0933             connect(doc, &KTextEditor::Document::destroyed, this, &self_type::untrack, Qt::UniqueConnection);
0934             connect(doc, &KTextEditor::Document::textChanged, this, &self_type::onTextChanged, Qt::UniqueConnection);
0935             connect(doc, &KTextEditor::Document::documentSavedOrUploaded, this, &self_type::onDocumentSaved, Qt::UniqueConnection);
0936             // in case of incremental change
0937             connect(doc, &KTextEditor::Document::textInserted, this, &self_type::onTextInserted, Qt::UniqueConnection);
0938             connect(doc, &KTextEditor::Document::textRemoved, this, &self_type::onTextRemoved, Qt::UniqueConnection);
0939             connect(doc, &KTextEditor::Document::lineWrapped, this, &self_type::onLineWrapped, Qt::UniqueConnection);
0940             connect(doc, &KTextEditor::Document::lineUnwrapped, this, &self_type::onLineUnwrapped, Qt::UniqueConnection);
0941         } else {
0942             it->server = server;
0943         }
0944     }
0945 
0946     decltype(m_docs)::iterator _close(decltype(m_docs)::iterator it, bool remove)
0947     {
0948         if (it != m_docs.end()) {
0949             if (it->open) {
0950                 // release server side (use url as registered with)
0951                 (it->server)->didClose(it->url);
0952                 it->open = false;
0953             }
0954             if (remove) {
0955                 disconnect(it.key(), nullptr, this, nullptr);
0956                 it = m_docs.erase(it);
0957             }
0958         }
0959         return it;
0960     }
0961 
0962     void _close(KTextEditor::Document *doc, bool remove)
0963     {
0964         auto it = m_docs.find(doc);
0965         if (it != m_docs.end()) {
0966             _close(it, remove);
0967         }
0968     }
0969 
0970     void untrack(QObject *doc)
0971     {
0972         _close(qobject_cast<KTextEditor::Document *>(doc), true);
0973         Q_EMIT serverChanged();
0974     }
0975 
0976     void close(KTextEditor::Document *doc)
0977     {
0978         _close(doc, false);
0979     }
0980 
0981     void update(const decltype(m_docs)::iterator &it, bool force)
0982     {
0983         auto doc = it.key();
0984         if (it != m_docs.end() && it->server) {
0985             it->version = it->doc->revision();
0986 
0987             if (!m_incrementalSync) {
0988                 it->changes.clear();
0989             }
0990             if (it->open) {
0991                 if (it->modified || force) {
0992                     (it->server)->didChange(it->url, it->version, (it->changes.empty()) ? doc->text() : QString(), it->changes);
0993                 }
0994             } else {
0995                 (it->server)->didOpen(it->url, it->version, documentLanguageId(doc), doc->text());
0996                 it->open = true;
0997             }
0998             it->modified = false;
0999             it->changes.clear();
1000         }
1001     }
1002 
1003     void update(KTextEditor::Document *doc, bool force) override
1004     {
1005         update(m_docs.find(doc), force);
1006     }
1007 
1008     void update(LSPClientServer *server, bool force)
1009     {
1010         for (auto it = m_docs.begin(); it != m_docs.end(); ++it) {
1011             if (it->server.get() == server) {
1012                 update(it, force);
1013             }
1014         }
1015     }
1016 
1017     void onTextChanged(KTextEditor::Document *doc)
1018     {
1019         auto it = m_docs.find(doc);
1020         if (it != m_docs.end()) {
1021             it->modified = true;
1022         }
1023     }
1024 
1025     DocumentInfo *getDocumentInfo(KTextEditor::Document *doc)
1026     {
1027         if (!m_incrementalSync) {
1028             return nullptr;
1029         }
1030 
1031         auto it = m_docs.find(doc);
1032         if (it != m_docs.end() && it->server) {
1033             const auto &caps = it->server->capabilities();
1034             if (caps.textDocumentSync.change == LSPDocumentSyncKind::Incremental) {
1035                 return &(*it);
1036             }
1037         }
1038         return nullptr;
1039     }
1040 
1041     void onTextInserted(KTextEditor::Document *doc, const KTextEditor::Cursor &position, const QString &text)
1042     {
1043         auto info = getDocumentInfo(doc);
1044         if (info) {
1045             info->changes.push_back({LSPRange{position, position}, text});
1046         }
1047     }
1048 
1049     void onTextRemoved(KTextEditor::Document *doc, const KTextEditor::Range &range, const QString &text)
1050     {
1051         (void)text;
1052         auto info = getDocumentInfo(doc);
1053         if (info) {
1054             info->changes.push_back({range, QString()});
1055         }
1056     }
1057 
1058     void onLineWrapped(KTextEditor::Document *doc, const KTextEditor::Cursor &position)
1059     {
1060         // so a 'newline' has been inserted at position
1061         // could have been UNIX style or other kind, let's ask the document
1062         auto text = doc->text({position, {position.line() + 1, 0}});
1063         onTextInserted(doc, position, text);
1064     }
1065 
1066     void onLineUnwrapped(KTextEditor::Document *doc, int line)
1067     {
1068         // lines line-1 and line got replaced by current content of line-1
1069         Q_ASSERT(line > 0);
1070         auto info = getDocumentInfo(doc);
1071         if (info) {
1072             LSPRange oldrange{{line - 1, 0}, {line + 1, 0}};
1073             LSPRange newrange{{line - 1, 0}, {line, 0}};
1074             auto text = doc->text(newrange);
1075             info->changes.push_back({oldrange, text});
1076         }
1077     }
1078 
1079     void onDocumentSaved(KTextEditor::Document *doc, bool saveAs)
1080     {
1081         if (!saveAs) {
1082             auto it = m_docs.find(doc);
1083             if (it != m_docs.end() && it->server) {
1084                 auto server = it->server;
1085                 const auto &saveOptions = server->capabilities().textDocumentSync.save;
1086                 if (saveOptions) {
1087                     server->didSave(doc->url(), saveOptions->includeText ? doc->text() : QString());
1088                 }
1089             }
1090         }
1091     }
1092 
1093     void onMessage(bool isLog, const LSPLogMessageParams &params)
1094     {
1095         // determine server description
1096         auto server = qobject_cast<LSPClientServer *>(sender());
1097         if (isLog) {
1098             Q_EMIT serverLogMessage(server, params);
1099         } else {
1100             Q_EMIT serverShowMessage(server, params);
1101         }
1102     }
1103 
1104     void onWorkDoneProgress(const LSPWorkDoneProgressParams &params)
1105     {
1106         // determine server description
1107         auto server = qobject_cast<LSPClientServer *>(sender());
1108         Q_EMIT serverWorkDoneProgress(server, params);
1109     }
1110 
1111     static std::pair<QString, QString> getProjectNameDir(const QObject *kateProject)
1112     {
1113         return {kateProject->property("name").toString(), kateProject->property("baseDir").toString()};
1114     }
1115 
1116     QList<LSPWorkspaceFolder> currentWorkspaceFolders()
1117     {
1118         QList<LSPWorkspaceFolder> folders;
1119         if (m_projectPlugin) {
1120             auto projects = m_projectPlugin->property("projects").value<QObjectList>();
1121             for (auto proj : projects) {
1122                 auto props = getProjectNameDir(proj);
1123                 folders.push_back(workspaceFolder(props.second, props.first));
1124             }
1125         }
1126         return folders;
1127     }
1128 
1129     static LSPWorkspaceFolder workspaceFolder(const QString &baseDir, const QString &name)
1130     {
1131         return {QUrl::fromLocalFile(baseDir), name};
1132     }
1133 
1134     void updateWorkspace(bool added, const QObject *project)
1135     {
1136         auto props = getProjectNameDir(project);
1137         auto &name = props.first;
1138         auto &baseDir = props.second;
1139         qCInfo(LSPCLIENT) << "update workspace" << added << baseDir << name;
1140         for (const auto &u : qAsConst(m_servers)) {
1141             for (const auto &si : u) {
1142                 if (auto server = si.server) {
1143                     const auto &caps = server->capabilities();
1144                     if (caps.workspaceFolders.changeNotifications && si.useWorkspace) {
1145                         auto wsfolder = workspaceFolder(baseDir, name);
1146                         QList<LSPWorkspaceFolder> l{wsfolder}, empty;
1147                         server->didChangeWorkspaceFolders(added ? l : empty, added ? empty : l);
1148                     }
1149                 }
1150             }
1151         }
1152     }
1153 
1154     Q_SLOT void onProjectAdded(QObject *project)
1155     {
1156         updateWorkspace(true, project);
1157     }
1158 
1159     Q_SLOT void onProjectRemoved(QObject *project)
1160     {
1161         updateWorkspace(false, project);
1162     }
1163 
1164     void monitorProjects(KTextEditor::Plugin *projectPlugin)
1165     {
1166         if (projectPlugin) {
1167             // clang-format off
1168             auto c = connect(projectPlugin,
1169                         SIGNAL(projectAdded(QObject*)),
1170                         this,
1171                         SLOT(onProjectAdded(QObject*)),
1172                         Qt::UniqueConnection);
1173             c = connect(projectPlugin,
1174                         SIGNAL(projectRemoved(QObject*)),
1175                         this,
1176                         SLOT(onProjectRemoved(QObject*)),
1177                         Qt::UniqueConnection);
1178             // clang-format on
1179         }
1180     }
1181 
1182     void onWorkspaceFolders(const WorkspaceFoldersReplyHandler &h, bool &handled)
1183     {
1184         if (handled) {
1185             return;
1186         }
1187 
1188         auto folders = currentWorkspaceFolders();
1189         h(folders);
1190 
1191         handled = true;
1192     }
1193 };
1194 
1195 std::shared_ptr<LSPClientServerManager> LSPClientServerManager::new_(LSPClientPlugin *plugin)
1196 {
1197     return std::shared_ptr<LSPClientServerManager>(new LSPClientServerManagerImpl(plugin));
1198 }
1199 
1200 #include "lspclientservermanager.moc"
1201 #include "moc_lspclientservermanager.cpp"