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 &params)
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 &params)
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