1 /*
2 SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
3
4 SPDX-License-Identifier: MIT
5 */
6
7 /* see plugins.docbook lspclient-configuration
8 * for client configuration documentation
9 */
10
11 #include "lspclientservermanager.h"
12
13 #include "lspclient_debug.h"
14
15 #include <KLocalizedString>
16 #include <KTextEditor/Document>
17 #include <KTextEditor/Editor>
18 #include <KTextEditor/MainWindow>
19 #include <KTextEditor/MovingInterface>
20 #include <KTextEditor/View>
21
22 #include <QDir>
23 #include <QEventLoop>
24 #include <QFileInfo>
25 #include <QJsonArray>
26 #include <QJsonDocument>
27 #include <QJsonObject>
28 #include <QJsonParseError>
29 #include <QRegularExpression>
30 #include <QStandardPaths>
31 #include <QThread>
32 #include <QTime>
33 #include <QTimer>
34
35 #include <json_utils.h>
36
37 // sadly no common header for plugins to include this from
38 // unless we do come up with such one
39 typedef QMap<QString, QString> QStringMap;
Q_DECLARE_METATYPE(QStringMap)40 Q_DECLARE_METATYPE(QStringMap)
41
42 // helper to find a proper root dir for the given document & file name that indicate the root dir
43 static QString rootForDocumentAndRootIndicationFileName(KTextEditor::Document *document, const QString &rootIndicationFileName)
44 {
45 // search only feasible if document is local file
46 if (!document->url().isLocalFile()) {
47 return QString();
48 }
49
50 // search root upwards
51 QDir dir(QFileInfo(document->url().toLocalFile()).absolutePath());
52 QSet<QString> seenDirectories;
53 while (!seenDirectories.contains(dir.absolutePath())) {
54 // update guard
55 seenDirectories.insert(dir.absolutePath());
56
57 // the file that indicates the root dir is there => all fine
58 if (dir.exists(rootIndicationFileName)) {
59 return dir.absolutePath();
60 }
61
62 // else: cd up, if possible or abort
63 if (!dir.cdUp()) {
64 break;
65 }
66 }
67
68 // no root found, bad luck
69 return QString();
70 }
71
72 #include <memory>
73
74 // helper guard to handle revision (un)lock
75 struct RevisionGuard {
76 QPointer<KTextEditor::Document> m_doc;
77 KTextEditor::MovingInterface *m_movingInterface = nullptr;
78 qint64 m_revision = -1;
79
RevisionGuardRevisionGuard80 RevisionGuard(KTextEditor::Document *doc = nullptr)
81 : m_doc(doc)
82 , m_movingInterface(qobject_cast<KTextEditor::MovingInterface *>(doc))
83 {
84 Q_ASSERT(m_movingInterface);
85 m_revision = m_movingInterface->revision();
86 m_movingInterface->lockRevision(m_revision);
87 }
88
89 // really only need/allow this one (out of 5)
RevisionGuardRevisionGuard90 RevisionGuard(RevisionGuard &&other)
91 : RevisionGuard(nullptr)
92 {
93 std::swap(m_doc, other.m_doc);
94 std::swap(m_movingInterface, other.m_movingInterface);
95 std::swap(m_revision, other.m_revision);
96 }
97
releaseRevisionGuard98 void release()
99 {
100 m_movingInterface = nullptr;
101 m_revision = -1;
102 }
103
~RevisionGuardRevisionGuard104 ~RevisionGuard()
105 {
106 // NOTE: hopefully the revision is still valid at this time
107 if (m_doc && m_movingInterface && m_revision >= 0) {
108 m_movingInterface->unlockRevision(m_revision);
109 }
110 }
111 };
112
113 class LSPClientRevisionSnapshotImpl : public LSPClientRevisionSnapshot
114 {
115 Q_OBJECT
116
117 typedef LSPClientRevisionSnapshotImpl self_type;
118
119 // std::map has more relaxed constraints on value_type
120 std::map<QUrl, RevisionGuard> m_guards;
121
122 Q_SLOT
clearRevisions(KTextEditor::Document * doc)123 void clearRevisions(KTextEditor::Document *doc)
124 {
125 for (auto &item : m_guards) {
126 if (item.second.m_doc == doc) {
127 item.second.release();
128 }
129 }
130 }
131
132 public:
add(KTextEditor::Document * doc)133 void add(KTextEditor::Document *doc)
134 {
135 Q_ASSERT(doc);
136
137 // make sure revision is cleared when needed and no longer used (to unlock or otherwise)
138 // see e.g. implementation in katetexthistory.cpp and assert's in place there
139 // clang-format off
140 auto conn = connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(clearRevisions(KTextEditor::Document*)));
141 Q_ASSERT(conn);
142 conn = connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)), this, SLOT(clearRevisions(KTextEditor::Document*)));
143 Q_ASSERT(conn);
144 // clang-format on
145 m_guards.emplace(doc->url(), doc);
146 }
147
find(const QUrl & url,KTextEditor::MovingInterface * & miface,qint64 & revision) const148 void find(const QUrl &url, KTextEditor::MovingInterface *&miface, qint64 &revision) const override
149 {
150 auto it = m_guards.find(url);
151 if (it != m_guards.end()) {
152 miface = it->second.m_movingInterface;
153 revision = it->second.m_revision;
154 } else {
155 miface = nullptr;
156 revision = -1;
157 }
158 }
159 };
160
161 // helper class to sync document changes to LSP server
162 class LSPClientServerManagerImpl : public LSPClientServerManager
163 {
164 Q_OBJECT
165
166 typedef LSPClientServerManagerImpl self_type;
167
168 struct ServerInfo {
169 QSharedPointer<LSPClientServer> server;
170 // config specified server url
171 QString url;
172 QTime started;
173 int failcount = 0;
174 // pending settings to be submitted
175 QJsonValue settings;
176 // use of workspace folders allowed
177 bool useWorkspace = false;
178 };
179
180 struct DocumentInfo {
181 QSharedPointer<LSPClientServer> server;
182 // merged server config as obtain from various sources
183 QJsonObject config;
184 KTextEditor::MovingInterface *movingInterface;
185 QUrl url;
186 qint64 version;
187 bool open : 1;
188 bool modified : 1;
189 // used for incremental update (if non-empty)
190 QList<LSPTextDocumentContentChangeEvent> changes;
191 };
192
193 LSPClientPlugin *m_plugin;
194 KTextEditor::MainWindow *m_mainWindow;
195 // merged default and user config
196 QJsonObject m_serverConfig;
197 // root -> (mode -> server)
198 QMap<QUrl, QMap<QString, ServerInfo>> m_servers;
199 QHash<KTextEditor::Document *, DocumentInfo> m_docs;
200 bool m_incrementalSync = false;
201
202 // highlightingModeRegex => language id
203 std::vector<std::pair<QRegularExpression, QString>> m_highlightingModeRegexToLanguageId;
204 // cache of highlighting mode => language id, to avoid massive regex matching
205 QHash<QString, QString> m_highlightingModeToLanguageIdCache;
206 // whether to pass the language id (key) to server when opening document
207 // most either do not care about the id, or can find out themselves
208 // (and might get confused if we pass a not so accurate one)
209 QHash<QString, bool> m_documentLanguageId;
210
211 typedef QVector<QSharedPointer<LSPClientServer>> ServerList;
212
213 public:
LSPClientServerManagerImpl(LSPClientPlugin * plugin,KTextEditor::MainWindow * mainWin)214 LSPClientServerManagerImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin)
215 : m_plugin(plugin)
216 , m_mainWindow(mainWin)
217 {
218 connect(plugin, &LSPClientPlugin::update, this, &self_type::updateServerConfig);
219 QTimer::singleShot(100, this, &self_type::updateServerConfig);
220
221 // stay tuned on project situation
222 QObject *projectView = projectPluginView();
223 if (projectView) {
224 // clang-format off
225 connect(projectView,
226 SIGNAL(pluginProjectAdded(QString,QString)),
227 this,
228 SLOT(onProjectAdded(QString,QString)));
229 connect(projectView,
230 SIGNAL(pluginProjectRemoved(QString,QString)),
231 this,
232 SLOT(onProjectRemoved(QString,QString)));
233 // clang-format on
234 }
235 }
236
~LSPClientServerManagerImpl()237 ~LSPClientServerManagerImpl() override
238 {
239 // stop everything as we go down
240 // several stages;
241 // stage 1; request shutdown of all servers (in parallel)
242 // (give that some time)
243 // stage 2; send TERM
244 // stage 3; send KILL
245
246 // stage 1
247
248 /* some msleep are used below which is somewhat BAD as it blocks/hangs
249 * the mainloop, however there is not much alternative:
250 * + running an inner mainloop leads to event processing,
251 * which could trigger an unexpected sequence of 'events'
252 * such as (re)loading plugin that is currently still unloading
253 * (consider scenario of fast-clicking enable/disable of LSP plugin)
254 * + could reduce or forego the sleep, but that increases chances
255 * on an unclean shutdown of LSP server, which may or may not
256 * be able to handle that properly (so let's try and be a polite
257 * client and try to avoid that to some degree)
258 * So we are left with a minor sleep compromise ...
259 */
260
261 int count = 0;
262 for (const auto &el : qAsConst(m_servers)) {
263 for (const auto &si : el) {
264 auto &s = si.server;
265 if (!s) {
266 continue;
267 }
268 disconnect(s.data(), nullptr, this, nullptr);
269 if (s->state() != LSPClientServer::State::None) {
270 ++count;
271 s->stop(-1, -1);
272 }
273 }
274 }
275 if (count) {
276 QThread::msleep(500);
277 } else {
278 return;
279 }
280
281 // stage 2 and 3
282 count = 0;
283 for (count = 0; count < 2; ++count) {
284 bool wait = false;
285 for (const auto &el : qAsConst(m_servers)) {
286 for (const auto &si : el) {
287 auto &s = si.server;
288 if (!s) {
289 continue;
290 }
291 wait = true;
292 s->stop(count == 0 ? 1 : -1, count == 0 ? -1 : 1);
293 }
294 }
295 if (wait && count == 0) {
296 QThread::msleep(100);
297 }
298 }
299 }
300
301 // map (highlight)mode to lsp languageId
languageId(const QString & mode)302 QString languageId(const QString &mode)
303 {
304 // query cache first
305 const auto cacheIt = m_highlightingModeToLanguageIdCache.find(mode);
306 if (cacheIt != m_highlightingModeToLanguageIdCache.end()) {
307 return cacheIt.value();
308 }
309
310 // match via regexes + cache result
311 for (const auto &it : m_highlightingModeRegexToLanguageId) {
312 if (it.first.match(mode).hasMatch()) {
313 m_highlightingModeToLanguageIdCache[mode] = it.second;
314 return it.second;
315 }
316 }
317
318 // else: we have no matching server!
319 m_highlightingModeToLanguageIdCache[mode] = QString();
320 return QString();
321 }
322
projectPluginView()323 QObject *projectPluginView()
324 {
325 return m_mainWindow->pluginView(QStringLiteral("kateprojectplugin"));
326 }
327
documentLanguageId(const QString mode)328 QString documentLanguageId(const QString mode)
329 {
330 auto langId = languageId(mode);
331 const auto it = m_documentLanguageId.find(langId);
332 // FIXME ?? perhaps use default false
333 // most servers can find out much better on their own
334 // (though it would actually have to be confirmed as such)
335 bool useId = true;
336 if (it != m_documentLanguageId.end()) {
337 useId = it.value();
338 }
339
340 return useId ? langId : QString();
341 }
342
setIncrementalSync(bool inc)343 void setIncrementalSync(bool inc) override
344 {
345 m_incrementalSync = inc;
346 }
347
findServer(KTextEditor::View * view,bool updatedoc=true)348 QSharedPointer<LSPClientServer> findServer(KTextEditor::View *view, bool updatedoc = true) override
349 {
350 if (!view) {
351 return nullptr;
352 }
353
354 auto document = view->document();
355 if (!document || document->url().isEmpty()) {
356 return nullptr;
357 }
358
359 auto it = m_docs.find(document);
360 auto server = it != m_docs.end() ? it->server : nullptr;
361 if (!server) {
362 QJsonObject serverConfig;
363 if ((server = _findServer(view, document, serverConfig))) {
364 trackDocument(document, server, serverConfig);
365 }
366 }
367
368 if (server && updatedoc) {
369 update(server.data(), false);
370 }
371 return server;
372 }
373
findServerConfig(KTextEditor::Document * document)374 virtual QJsonValue findServerConfig(KTextEditor::Document *document) override
375 {
376 // check if document has been seen/processed by now
377 auto it = m_docs.find(document);
378 auto config = it != m_docs.end() ? QJsonValue(it->config) : QJsonValue::Null;
379 return config;
380 }
381
382 // restart a specific server or all servers if server == nullptr
restart(LSPClientServer * server)383 void restart(LSPClientServer *server) override
384 {
385 ServerList servers;
386 // find entry for server(s) and move out
387 for (auto &m : m_servers) {
388 for (auto it = m.begin(); it != m.end();) {
389 if (!server || it->server.data() == server) {
390 servers.push_back(it->server);
391 it = m.erase(it);
392 } else {
393 ++it;
394 }
395 }
396 }
397 restart(servers, server == nullptr);
398 }
399
revision(KTextEditor::Document * doc)400 qint64 revision(KTextEditor::Document *doc) override
401 {
402 auto it = m_docs.find(doc);
403 return it != m_docs.end() ? it->version : -1;
404 }
405
snapshot(LSPClientServer * server)406 LSPClientRevisionSnapshot *snapshot(LSPClientServer *server) override
407 {
408 auto result = new LSPClientRevisionSnapshotImpl;
409 for (auto it = m_docs.begin(); it != m_docs.end(); ++it) {
410 if (it->server == server) {
411 // sync server to latest revision that will be recorded
412 update(it.key(), false);
413 result->add(it.key());
414 }
415 }
416 return result;
417 }
418
419 private:
showMessage(const QString & msg,KTextEditor::Message::MessageType level)420 void showMessage(const QString &msg, KTextEditor::Message::MessageType level)
421 {
422 // inform interested view(er) which will decide how/where to show
423 Q_EMIT LSPClientServerManager::showMessage(level, msg);
424 }
425
426 // caller ensures that servers are no longer present in m_servers
restart(const ServerList & servers,bool reload=false)427 void restart(const ServerList &servers, bool reload = false)
428 {
429 // close docs
430 for (const auto &server : servers) {
431 // controlling server here, so disable usual state tracking response
432 disconnect(server.data(), nullptr, this, nullptr);
433 for (auto it = m_docs.begin(); it != m_docs.end();) {
434 auto &item = it.value();
435 if (item.server == server) {
436 // no need to close if server not in proper state
437 if (server->state() != LSPClientServer::State::Running) {
438 item.open = false;
439 }
440 it = _close(it, true);
441 } else {
442 ++it;
443 }
444 }
445 }
446
447 // helper captures servers
448 auto stopservers = [servers](int t, int k) {
449 for (const auto &server : servers) {
450 server->stop(t, k);
451 }
452 };
453
454 // trigger server shutdown now
455 stopservers(-1, -1);
456
457 // initiate delayed stages (TERM and KILL)
458 // async, so give a bit more time
459 QTimer::singleShot(2 * TIMEOUT_SHUTDOWN, this, [stopservers]() {
460 stopservers(1, -1);
461 });
462 QTimer::singleShot(4 * TIMEOUT_SHUTDOWN, this, [stopservers]() {
463 stopservers(-1, 1);
464 });
465
466 // as for the start part
467 // trigger interested parties, which will again request a server as needed
468 // let's delay this; less chance for server instances to trip over each other
469 QTimer::singleShot(6 * TIMEOUT_SHUTDOWN, this, [this, reload]() {
470 // this may be a good time to refresh server config
471 if (reload) {
472 // will also trigger as mentioned above
473 updateServerConfig();
474 } else {
475 Q_EMIT serverChanged();
476 }
477 });
478 }
479
onStateChanged(LSPClientServer * server)480 void onStateChanged(LSPClientServer *server)
481 {
482 if (server->state() == LSPClientServer::State::Running) {
483 // send settings if pending
484 ServerInfo *info = nullptr;
485 for (auto &m : m_servers) {
486 for (auto &si : m) {
487 if (si.server.data() == server) {
488 info = &si;
489 break;
490 }
491 }
492 }
493 if (info && !info->settings.isUndefined()) {
494 server->didChangeConfiguration(info->settings);
495 }
496 // provide initial workspace folder situation
497 // this is done here because the folder notification pre-dates
498 // the workspaceFolders property in 'initialize'
499 // there is also no way to know whether the server supports that
500 // and in fact some servers do "support workspace folders" (e.g. notification)
501 // but do not know about the 'initialize' property
502 // so, in summary, the notification is used here rather than the property
503 const auto &caps = server->capabilities();
504 if (caps.workspaceFolders.changeNotifications && info && info->useWorkspace) {
505 if (auto folders = currentWorkspaceFolders(); !folders.isEmpty()) {
506 server->didChangeWorkspaceFolders(folders, {});
507 }
508 }
509 // clear for normal operation
510 Q_EMIT serverChanged();
511 } else if (server->state() == LSPClientServer::State::None) {
512 // went down
513 // find server info to see how bad this is
514 // if this is an occasional termination/crash ... ok then
515 // if this happens quickly (bad/missing server, wrong cmdline/config), then no restart
516 QSharedPointer<LSPClientServer> sserver;
517 QString url;
518 bool retry = true;
519 for (auto &m : m_servers) {
520 for (auto &si : m) {
521 if (si.server.data() == server) {
522 url = si.url;
523 if (si.started.secsTo(QTime::currentTime()) < 60) {
524 ++si.failcount;
525 }
526 // clear the entry, which will be re-filled if needed
527 // otherwise, leave it in place as a dead mark not to re-create one in _findServer
528 if (si.failcount < 2) {
529 std::swap(sserver, si.server);
530 } else {
531 sserver = si.server;
532 retry = false;
533 }
534 }
535 }
536 }
537 auto action = retry ? i18n("Restarting") : i18n("NOT Restarting");
538 showMessage(i18n("Server terminated unexpectedly ... %1 [%2] [homepage: %3] ", action, server->cmdline().join(QLatin1Char(' ')), url),
539 KTextEditor::Message::Warning);
540 if (sserver) {
541 // sserver might still be in m_servers
542 // but since it died already bringing it down will have no (ill) effect
543 restart({sserver});
544 }
545 }
546 }
547
_findServer(KTextEditor::View * view,KTextEditor::Document * document,QJsonObject & mergedConfig)548 QSharedPointer<LSPClientServer> _findServer(KTextEditor::View *view, KTextEditor::Document *document, QJsonObject &mergedConfig)
549 {
550 // compute the LSP standardized language id, none found => no change
551 auto langId = languageId(document->highlightingMode());
552 if (langId.isEmpty()) {
553 return nullptr;
554 }
555
556 QObject *projectView = projectPluginView();
557 // preserve raw QString value so it can be used and tested that way below
558 const auto projectBase = projectView ? projectView->property("projectBaseDir").toString() : QString();
559 const auto &projectMap = projectView ? projectView->property("projectMap").toMap() : QVariantMap();
560
561 // merge with project specific
562 auto projectConfig = QJsonDocument::fromVariant(projectMap).object().value(QStringLiteral("lspclient")).toObject();
563 auto serverConfig = json::merge(m_serverConfig, projectConfig);
564
565 // locate server config
566 QJsonValue config;
567 QSet<QString> used;
568 // reduce langId
569 auto realLangId = langId;
570 while (true) {
571 qCInfo(LSPCLIENT) << "language id " << langId;
572 used << langId;
573 config = serverConfig.value(QStringLiteral("servers")).toObject().value(langId);
574 if (config.isObject()) {
575 const auto &base = config.toObject().value(QStringLiteral("use")).toString();
576 // basic cycle detection
577 if (!base.isEmpty() && !used.contains(base)) {
578 langId = base;
579 continue;
580 }
581 }
582 break;
583 }
584
585 if (!config.isObject()) {
586 return nullptr;
587 }
588
589 // merge global settings
590 serverConfig = json::merge(serverConfig.value(QStringLiteral("global")).toObject(), config.toObject());
591
592 // used for variable substitution in the sequl
593 // NOTE that also covers a form of environment substitution using %{ENV:XYZ}
594 auto editor = KTextEditor::Editor::instance();
595
596 std::optional<QString> rootpath;
597 const auto rootv = serverConfig.value(QStringLiteral("root"));
598 if (rootv.isString()) {
599 auto sroot = rootv.toString();
600 editor->expandText(sroot, view, sroot);
601 if (QDir::isAbsolutePath(sroot)) {
602 rootpath = sroot;
603 } else if (!projectBase.isEmpty()) {
604 rootpath = QDir(projectBase).absoluteFilePath(sroot);
605 } else if (sroot.isEmpty()) {
606 // empty root; so we are convinced the server can handle null rootUri
607 rootpath = QString();
608 } else if (const auto url = document->url(); url.isValid() && url.isLocalFile()) {
609 // likewise, but use safer traditional approach and specify rootUri
610 rootpath = QDir(QFileInfo(url.toLocalFile()).absolutePath()).absoluteFilePath(sroot);
611 }
612 }
613
614 /**
615 * no explicit set root dir? search for a matching root based on some name filters
616 * this is required for some LSP servers like rls that don't handle that on their own like
617 * clangd does
618 */
619 if (!rootpath) {
620 const auto fileNamesForDetection = serverConfig.value(QStringLiteral("rootIndicationFileNames"));
621 if (fileNamesForDetection.isArray()) {
622 // we try each file name alternative in the listed order
623 // this allows to have preferences
624 const auto fileNames = fileNamesForDetection.toArray();
625 for (auto name : fileNames) {
626 if (name.isString()) {
627 auto root = rootForDocumentAndRootIndicationFileName(document, name.toString());
628 if (!root.isEmpty()) {
629 rootpath = root;
630 break;
631 }
632 }
633 }
634 }
635 }
636
637 // just in case ... ensure normalized result
638 if (rootpath && !rootpath->isEmpty()) {
639 auto cpath = QFileInfo(*rootpath).canonicalFilePath();
640 if (!cpath.isEmpty()) {
641 rootpath = cpath;
642 }
643 }
644
645 // is it actually safe/reasonable to use workspaces?
646 // in practice, (at this time) servers do do not quite consider or support all that
647 // so in that regard workspace folders represents a bit of "spec endulgance"
648 // (along with quite some other aspects for that matter)
649 //
650 // if a server was/is able to handle a "generic root",
651 // let's assume it is safe to consider workspace folders if it explicitly claims such support
652 // if, however, an explicit root was/is necessary,
653 // let's assume not safe
654 // in either case, let configuration explicitly specify this
655 bool useWorkspace = serverConfig.value(QStringLiteral("useWorkspace")).toBool(!rootpath ? true : false);
656
657 // last fallback: home directory
658 if (!rootpath) {
659 rootpath = QDir::homePath();
660 }
661
662 auto root = rootpath && !rootpath->isEmpty() ? QUrl::fromLocalFile(*rootpath) : QUrl();
663 auto &serverinfo = m_servers[root][langId];
664 auto &server = serverinfo.server;
665
666 // maybe there is a server with other root that is workspace capable
667 if (!server && useWorkspace) {
668 for (const auto &l : qAsConst(m_servers)) {
669 // for (auto it = l.begin(); it != l.end(); ++it) {
670 auto it = l.find(langId);
671 if (it != l.end()) {
672 if (auto oserver = it->server) {
673 const auto &caps = oserver->capabilities();
674 if (caps.workspaceFolders.supported && caps.workspaceFolders.changeNotifications && it->useWorkspace) {
675 // so this server can handle workspace folders and should know about project root
676 server = oserver;
677 break;
678 }
679 }
680 }
681 }
682 }
683
684 if (!server) {
685 QStringList cmdline;
686
687 // choose debug command line for debug mode, fallback to command
688 auto vcmdline = serverConfig.value(m_plugin->m_debugMode ? QStringLiteral("commandDebug") : QStringLiteral("command"));
689 if (vcmdline.isUndefined()) {
690 vcmdline = serverConfig.value(QStringLiteral("command"));
691 }
692
693 auto scmdline = vcmdline.toString();
694 if (!scmdline.isEmpty()) {
695 cmdline = scmdline.split(QLatin1Char(' '));
696 } else {
697 const auto cmdOpts = vcmdline.toArray();
698 for (const auto &c : cmdOpts) {
699 cmdline.push_back(c.toString());
700 }
701 }
702
703 // some more expansion and substitution
704 // unlikely to be used here, but anyway
705 for (auto &e : cmdline) {
706 editor->expandText(e, view, e);
707 }
708
709 if (cmdline.length() > 0) {
710 // ensure we always only take the server executable from the PATH or user defined paths
711 // QProcess will take the executable even just from current working directory without this => BAD
712 auto cmd = QStandardPaths::findExecutable(cmdline[0]);
713
714 // optionally search in supplied path(s)
715 const auto vpath = serverConfig.value(QStringLiteral("path")).toArray();
716 if (cmd.isEmpty() && !vpath.isEmpty()) {
717 // collect and expand in case home dir or other (environment) variable reference is used
718 QStringList path;
719 for (const auto &e : vpath) {
720 auto p = e.toString();
721 editor->expandText(p, view, p);
722 path.push_back(p);
723 }
724 cmd = QStandardPaths::findExecutable(cmdline[0], path);
725 }
726
727 // we can only start the stuff if we did find the binary in the paths
728 if (!cmd.isEmpty()) {
729 // use full path to avoid security issues
730 cmdline[0] = cmd;
731
732 // an empty list is always passed here (or null)
733 // the initial list is provided/updated using notification after start
734 // since that is what a server is more aware of
735 // and should support if it declares workspace folder capable
736 // (as opposed to the new initialization property)
737 LSPClientServer::FoldersType folders;
738 if (useWorkspace) {
739 folders = QList<LSPWorkspaceFolder>();
740 }
741 server.reset(new LSPClientServer(cmdline, root, realLangId, serverConfig.value(QStringLiteral("initializationOptions")), folders));
742 connect(server.data(), &LSPClientServer::stateChanged, this, &self_type::onStateChanged, Qt::UniqueConnection);
743 if (!server->start()) {
744 QString message = i18n("Failed to start server: %1", cmdline.join(QLatin1Char(' ')));
745 const auto url = serverConfig.value(QStringLiteral("url")).toString();
746 if (!url.isEmpty()) {
747 message += QStringLiteral("\n") + i18n("Please check your PATH for the binary");
748 message += QStringLiteral("\n") + i18n("See also %1 for installation or details", url);
749 }
750 showMessage(message, KTextEditor::Message::Warning);
751 } else {
752 showMessage(i18n("Started server %2: %1", cmdline.join(QLatin1Char(' ')), serverDescription(server.data())),
753 KTextEditor::Message::Positive);
754 using namespace std::placeholders;
755 server->connect(server.data(), &LSPClientServer::logMessage, this, std::bind(&self_type::onMessage, this, true, _1));
756 server->connect(server.data(), &LSPClientServer::showMessage, this, std::bind(&self_type::onMessage, this, false, _1));
757 server->connect(server.data(), &LSPClientServer::workDoneProgress, this, &self_type::onWorkDoneProgress);
758 server->connect(server.data(), &LSPClientServer::workspaceFolders, this, &self_type::onWorkspaceFolders, Qt::UniqueConnection);
759 }
760 } else {
761 // we didn't find the server binary at all!
762 QString message = i18n("Failed to find server binary: %1", cmdline[0]);
763 const auto url = serverConfig.value(QStringLiteral("url")).toString();
764 if (!url.isEmpty()) {
765 message += QStringLiteral("\n") + i18n("Please check your PATH for the binary");
766 message += QStringLiteral("\n") + i18n("See also %1 for installation or details", url);
767 }
768 showMessage(message, KTextEditor::Message::Warning);
769 }
770
771 serverinfo.settings = serverConfig.value(QStringLiteral("settings"));
772 serverinfo.started = QTime::currentTime();
773 serverinfo.url = serverConfig.value(QStringLiteral("url")).toString();
774 // leave failcount as-is
775 serverinfo.useWorkspace = useWorkspace;
776 }
777 }
778 mergedConfig = serverConfig;
779 return (server && server->state() == LSPClientServer::State::Running) ? server : nullptr;
780 }
781
updateServerConfig()782 void updateServerConfig()
783 {
784 // default configuration, compiled into plugin resource, reading can't fail
785 QFile defaultConfigFile(QStringLiteral(":/lspclient/settings.json"));
786 defaultConfigFile.open(QIODevice::ReadOnly);
787 Q_ASSERT(defaultConfigFile.isOpen());
788 m_serverConfig = QJsonDocument::fromJson(defaultConfigFile.readAll()).object();
789
790 // consider specified configuration if existing
791 const auto configPath = m_plugin->configPath().toLocalFile();
792 if (!configPath.isEmpty() && QFile::exists(configPath)) {
793 QFile f(configPath);
794 if (f.open(QIODevice::ReadOnly)) {
795 const auto data = f.readAll();
796 if (!data.isEmpty()) {
797 QJsonParseError error{};
798 auto json = QJsonDocument::fromJson(data, &error);
799 if (error.error == QJsonParseError::NoError) {
800 if (json.isObject()) {
801 m_serverConfig = json::merge(m_serverConfig, json.object());
802 } else {
803 showMessage(i18n("Failed to parse server configuration '%1': no JSON object", configPath), KTextEditor::Message::Error);
804 }
805 } else {
806 showMessage(i18n("Failed to parse server configuration '%1': %2", configPath, error.errorString()), KTextEditor::Message::Error);
807 }
808 }
809 } else {
810 showMessage(i18n("Failed to read server configuration: %1", configPath), KTextEditor::Message::Error);
811 }
812 }
813
814 // build regex of highlightingMode => language id
815 m_highlightingModeRegexToLanguageId.clear();
816 m_highlightingModeToLanguageIdCache.clear();
817 const auto servers = m_serverConfig.value(QLatin1String("servers")).toObject();
818 for (auto it = servers.begin(); it != servers.end(); ++it) {
819 // get highlighting mode regex for this server, if not set, fallback to just the name
820 const auto &server = it.value().toObject();
821 QString highlightingModeRegex = server.value(QLatin1String("highlightingModeRegex")).toString();
822 if (highlightingModeRegex.isEmpty()) {
823 highlightingModeRegex = it.key();
824 }
825 m_highlightingModeRegexToLanguageId.emplace_back(QRegularExpression(highlightingModeRegex, QRegularExpression::CaseInsensitiveOption), it.key());
826 // should we use the languageId in didOpen
827 auto docLanguageId = server.value(QLatin1String("documentLanguageId"));
828 if (docLanguageId.isBool()) {
829 m_documentLanguageId[it.key()] = docLanguageId.toBool();
830 }
831 }
832
833 // we could (but do not) perform restartAll here;
834 // for now let's leave that up to user
835 // but maybe we do have a server now where not before, so let's signal
836 Q_EMIT serverChanged();
837 }
838
trackDocument(KTextEditor::Document * doc,const QSharedPointer<LSPClientServer> & server,QJsonObject serverConfig)839 void trackDocument(KTextEditor::Document *doc, const QSharedPointer<LSPClientServer> &server, QJsonObject serverConfig)
840 {
841 auto it = m_docs.find(doc);
842 if (it == m_docs.end()) {
843 KTextEditor::MovingInterface *miface = qobject_cast<KTextEditor::MovingInterface *>(doc);
844 it = m_docs.insert(doc, {server, serverConfig, miface, doc->url(), 0, false, false, {}});
845 // track document
846 connect(doc, &KTextEditor::Document::documentUrlChanged, this, &self_type::untrack, Qt::UniqueConnection);
847 connect(doc, &KTextEditor::Document::highlightingModeChanged, this, &self_type::untrack, Qt::UniqueConnection);
848 connect(doc, &KTextEditor::Document::aboutToClose, this, &self_type::untrack, Qt::UniqueConnection);
849 connect(doc, &KTextEditor::Document::destroyed, this, &self_type::untrack, Qt::UniqueConnection);
850 connect(doc, &KTextEditor::Document::textChanged, this, &self_type::onTextChanged, Qt::UniqueConnection);
851 connect(doc, &KTextEditor::Document::documentSavedOrUploaded, this, &self_type::onDocumentSaved, Qt::UniqueConnection);
852 // in case of incremental change
853 connect(doc, &KTextEditor::Document::textInserted, this, &self_type::onTextInserted, Qt::UniqueConnection);
854 connect(doc, &KTextEditor::Document::textRemoved, this, &self_type::onTextRemoved, Qt::UniqueConnection);
855 connect(doc, &KTextEditor::Document::lineWrapped, this, &self_type::onLineWrapped, Qt::UniqueConnection);
856 connect(doc, &KTextEditor::Document::lineUnwrapped, this, &self_type::onLineUnwrapped, Qt::UniqueConnection);
857 } else {
858 it->server = server;
859 }
860 }
861
_close(decltype(m_docs)::iterator it,bool remove)862 decltype(m_docs)::iterator _close(decltype(m_docs)::iterator it, bool remove)
863 {
864 if (it != m_docs.end()) {
865 if (it->open) {
866 // release server side (use url as registered with)
867 (it->server)->didClose(it->url);
868 it->open = false;
869 }
870 if (remove) {
871 disconnect(it.key(), nullptr, this, nullptr);
872 it = m_docs.erase(it);
873 }
874 }
875 return it;
876 }
877
_close(KTextEditor::Document * doc,bool remove)878 void _close(KTextEditor::Document *doc, bool remove)
879 {
880 auto it = m_docs.find(doc);
881 if (it != m_docs.end()) {
882 _close(it, remove);
883 }
884 }
885
untrack(QObject * doc)886 void untrack(QObject *doc)
887 {
888 _close(qobject_cast<KTextEditor::Document *>(doc), true);
889 Q_EMIT serverChanged();
890 }
891
close(KTextEditor::Document * doc)892 void close(KTextEditor::Document *doc)
893 {
894 _close(doc, false);
895 }
896
update(const decltype(m_docs)::iterator & it,bool force)897 void update(const decltype(m_docs)::iterator &it, bool force)
898 {
899 auto doc = it.key();
900 if (it != m_docs.end() && it->server) {
901 it->version = it->movingInterface->revision();
902
903 if (!m_incrementalSync) {
904 it->changes.clear();
905 }
906 if (it->open) {
907 if (it->modified || force) {
908 (it->server)->didChange(it->url, it->version, (it->changes.empty()) ? doc->text() : QString(), it->changes);
909 }
910 } else {
911 (it->server)->didOpen(it->url, it->version, documentLanguageId(doc->highlightingMode()), doc->text());
912 it->open = true;
913 }
914 it->modified = false;
915 it->changes.clear();
916 }
917 }
918
update(KTextEditor::Document * doc,bool force)919 void update(KTextEditor::Document *doc, bool force) override
920 {
921 update(m_docs.find(doc), force);
922 }
923
update(LSPClientServer * server,bool force)924 void update(LSPClientServer *server, bool force)
925 {
926 for (auto it = m_docs.begin(); it != m_docs.end(); ++it) {
927 if (it->server == server) {
928 update(it, force);
929 }
930 }
931 }
932
onTextChanged(KTextEditor::Document * doc)933 void onTextChanged(KTextEditor::Document *doc)
934 {
935 auto it = m_docs.find(doc);
936 if (it != m_docs.end()) {
937 it->modified = true;
938 }
939 }
940
getDocumentInfo(KTextEditor::Document * doc)941 DocumentInfo *getDocumentInfo(KTextEditor::Document *doc)
942 {
943 if (!m_incrementalSync) {
944 return nullptr;
945 }
946
947 auto it = m_docs.find(doc);
948 if (it != m_docs.end() && it->server) {
949 const auto &caps = it->server->capabilities();
950 if (caps.textDocumentSync.change == LSPDocumentSyncKind::Incremental) {
951 return &(*it);
952 }
953 }
954 return nullptr;
955 }
956
onTextInserted(KTextEditor::Document * doc,const KTextEditor::Cursor & position,const QString & text)957 void onTextInserted(KTextEditor::Document *doc, const KTextEditor::Cursor &position, const QString &text)
958 {
959 auto info = getDocumentInfo(doc);
960 if (info) {
961 info->changes.push_back({LSPRange{position, position}, text});
962 }
963 }
964
onTextRemoved(KTextEditor::Document * doc,const KTextEditor::Range & range,const QString & text)965 void onTextRemoved(KTextEditor::Document *doc, const KTextEditor::Range &range, const QString &text)
966 {
967 (void)text;
968 auto info = getDocumentInfo(doc);
969 if (info) {
970 info->changes.push_back({range, QString()});
971 }
972 }
973
onLineWrapped(KTextEditor::Document * doc,const KTextEditor::Cursor & position)974 void onLineWrapped(KTextEditor::Document *doc, const KTextEditor::Cursor &position)
975 {
976 // so a 'newline' has been inserted at position
977 // could have been UNIX style or other kind, let's ask the document
978 auto text = doc->text({position, {position.line() + 1, 0}});
979 onTextInserted(doc, position, text);
980 }
981
onLineUnwrapped(KTextEditor::Document * doc,int line)982 void onLineUnwrapped(KTextEditor::Document *doc, int line)
983 {
984 // lines line-1 and line got replaced by current content of line-1
985 Q_ASSERT(line > 0);
986 auto info = getDocumentInfo(doc);
987 if (info) {
988 LSPRange oldrange{{line - 1, 0}, {line + 1, 0}};
989 LSPRange newrange{{line - 1, 0}, {line, 0}};
990 auto text = doc->text(newrange);
991 info->changes.push_back({oldrange, text});
992 }
993 }
994
onDocumentSaved(KTextEditor::Document * doc,bool saveAs)995 void onDocumentSaved(KTextEditor::Document *doc, bool saveAs)
996 {
997 if (!saveAs) {
998 auto it = m_docs.find(doc);
999 if (it != m_docs.end() && it->server) {
1000 auto server = it->server;
1001 const auto &saveOptions = server->capabilities().textDocumentSync.save;
1002 if (saveOptions) {
1003 server->didSave(doc->url(), saveOptions->includeText ? doc->text() : QString());
1004 }
1005 }
1006 }
1007 }
1008
onMessage(bool isLog,const LSPLogMessageParams & params)1009 void onMessage(bool isLog, const LSPLogMessageParams ¶ms)
1010 {
1011 // determine server description
1012 auto server = dynamic_cast<LSPClientServer *>(sender());
1013 if (isLog) {
1014 Q_EMIT serverLogMessage(server, params);
1015 } else {
1016 Q_EMIT serverShowMessage(server, params);
1017 }
1018 }
1019
onWorkDoneProgress(const LSPWorkDoneProgressParams & params)1020 void onWorkDoneProgress(const LSPWorkDoneProgressParams ¶ms)
1021 {
1022 // determine server description
1023 auto server = dynamic_cast<LSPClientServer *>(sender());
1024 Q_EMIT serverWorkDoneProgress(server, params);
1025 }
1026
currentWorkspaceFolders()1027 QList<LSPWorkspaceFolder> currentWorkspaceFolders()
1028 {
1029 QList<LSPWorkspaceFolder> folders;
1030 auto projectView = projectPluginView();
1031 if (projectView) {
1032 auto projectsMap = projectView->property("allProjects").value<QStringMap>();
1033 for (auto it = projectsMap.begin(); it != projectsMap.end(); ++it) {
1034 folders.push_back(workspaceFolder(it.key(), it.value()));
1035 }
1036 }
1037 return folders;
1038 }
1039
workspaceFolder(const QString & baseDir,const QString & name)1040 static LSPWorkspaceFolder workspaceFolder(const QString &baseDir, const QString &name)
1041 {
1042 return {QUrl::fromLocalFile(baseDir), name};
1043 }
1044
updateWorkspace(bool added,const QString & baseDir,const QString & name)1045 void updateWorkspace(bool added, const QString &baseDir, const QString &name)
1046 {
1047 qCInfo(LSPCLIENT) << "update workspace" << added << baseDir << name;
1048 for (const auto &u : qAsConst(m_servers)) {
1049 for (const auto &si : u) {
1050 if (auto server = si.server) {
1051 const auto &caps = server->capabilities();
1052 if (caps.workspaceFolders.changeNotifications && si.useWorkspace) {
1053 auto wsfolder = workspaceFolder(baseDir, name);
1054 QList<LSPWorkspaceFolder> l{wsfolder}, empty;
1055 server->didChangeWorkspaceFolders(added ? l : empty, added ? empty : l);
1056 }
1057 }
1058 }
1059 }
1060 }
1061
onProjectAdded(QString baseDir,QString name)1062 Q_SLOT void onProjectAdded(QString baseDir, QString name)
1063 {
1064 updateWorkspace(true, baseDir, name);
1065 }
1066
onProjectRemoved(QString baseDir,QString name)1067 Q_SLOT void onProjectRemoved(QString baseDir, QString name)
1068 {
1069 updateWorkspace(false, baseDir, name);
1070 }
1071
onWorkspaceFolders(const WorkspaceFoldersReplyHandler & h,bool & handled)1072 void onWorkspaceFolders(const WorkspaceFoldersReplyHandler &h, bool &handled)
1073 {
1074 if (handled) {
1075 return;
1076 }
1077
1078 auto folders = currentWorkspaceFolders();
1079 h(folders);
1080
1081 handled = true;
1082 }
1083 };
1084
new_(LSPClientPlugin * plugin,KTextEditor::MainWindow * mainWin)1085 QSharedPointer<LSPClientServerManager> LSPClientServerManager::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin)
1086 {
1087 return QSharedPointer<LSPClientServerManager>(new LSPClientServerManagerImpl(plugin, mainWin));
1088 }
1089
1090 #include "lspclientservermanager.moc"
1091