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 ¶ms) 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 ¶ms) 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"