File indexing completed on 2024-04-28 05:48:56

0001 /*
0002     SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include "lspclientplugin.h"
0008 #include "lspclientconfigpage.h"
0009 #include "lspclientpluginview.h"
0010 #include "lspclientservermanager.h"
0011 
0012 #include "lspclient_debug.h"
0013 
0014 #include <KConfigGroup>
0015 #include <KLocalizedString>
0016 #include <KPluginFactory>
0017 #include <KSharedConfig>
0018 
0019 #include <QApplication>
0020 #include <QDir>
0021 #include <QMessageBox>
0022 #include <QStandardPaths>
0023 #include <QTimer>
0024 
0025 static const QString CONFIG_LSPCLIENT{QStringLiteral("lspclient")};
0026 static const QString CONFIG_SYMBOL_DETAILS{QStringLiteral("SymbolDetails")};
0027 static const QString CONFIG_SYMBOL_TREE{QStringLiteral("SymbolTree")};
0028 static const QString CONFIG_SYMBOL_EXPAND{QStringLiteral("SymbolExpand")};
0029 static const QString CONFIG_SYMBOL_SORT{QStringLiteral("SymbolSort")};
0030 static const QString CONFIG_COMPLETION_DOC{QStringLiteral("CompletionDocumentation")};
0031 static const QString CONFIG_REFERENCES_DECLARATION{QStringLiteral("ReferencesDeclaration")};
0032 static const QString CONFIG_COMPLETION_PARENS{QStringLiteral("CompletionParens")};
0033 static const QString CONFIG_AUTO_HOVER{QStringLiteral("AutoHover")};
0034 static const QString CONFIG_TYPE_FORMATTING{QStringLiteral("TypeFormatting")};
0035 static const QString CONFIG_INCREMENTAL_SYNC{QStringLiteral("IncrementalSync")};
0036 static const QString CONFIG_HIGHLIGHT_GOTO{QStringLiteral("HighlightGoto")};
0037 static const QString CONFIG_DIAGNOSTICS{QStringLiteral("Diagnostics")};
0038 static const QString CONFIG_DIAGNOSTICS_HIGHLIGHT{QStringLiteral("DiagnosticsHighlight")};
0039 static const QString CONFIG_DIAGNOSTICS_MARK{QStringLiteral("DiagnosticsMark")};
0040 static const QString CONFIG_DIAGNOSTICS_HOVER{QStringLiteral("DiagnosticsHover")};
0041 static const QString CONFIG_DIAGNOSTICS_SIZE{QStringLiteral("DiagnosticsSize")};
0042 static const QString CONFIG_MESSAGES{QStringLiteral("Messages")};
0043 static const QString CONFIG_SERVER_CONFIG{QStringLiteral("ServerConfiguration")};
0044 static const QString CONFIG_SEMANTIC_HIGHLIGHTING{QStringLiteral("SemanticHighlighting")};
0045 static const QString CONFIG_SIGNATURE_HELP{QStringLiteral("SignatureHelp")};
0046 static const QString CONFIG_AUTO_IMPORT{QStringLiteral("AutoImport")};
0047 static const QString CONFIG_ALLOWED_COMMANDS{QStringLiteral("AllowedServerCommandLines")};
0048 static const QString CONFIG_BLOCKED_COMMANDS{QStringLiteral("BlockedServerCommandLines")};
0049 static const QString CONFIG_FORMAT_ON_SAVE{QStringLiteral("FormatOnSave")};
0050 static const QString CONFIG_INLAY_HINT{QStringLiteral("InlayHints")};
0051 
0052 K_PLUGIN_FACTORY_WITH_JSON(LSPClientPluginFactory, "lspclientplugin.json", registerPlugin<LSPClientPlugin>();)
0053 
0054 /**
0055  * ensure we don't spam the user with debug output per-default
0056  */
0057 static const bool debug = (qgetenv("LSPCLIENT_DEBUG") == QByteArray("1"));
0058 static QLoggingCategory::CategoryFilter oldCategoryFilter = nullptr;
0059 void myCategoryFilter(QLoggingCategory *category)
0060 {
0061     // deactivate info and debug if not debug mode
0062     if (qstrcmp(category->categoryName(), "katelspclientplugin") == 0) {
0063         category->setEnabled(QtInfoMsg, debug);
0064         category->setEnabled(QtDebugMsg, debug);
0065     } else if (oldCategoryFilter) {
0066         oldCategoryFilter(category);
0067     }
0068 }
0069 
0070 LSPClientPlugin::LSPClientPlugin(QObject *parent, const QVariantList &)
0071     : KTextEditor::Plugin(parent)
0072     , m_settingsPath(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + QStringLiteral("/lspclient"))
0073     , m_defaultConfigPath(QUrl::fromLocalFile(m_settingsPath + QStringLiteral("/settings.json")))
0074     , m_debugMode(debug)
0075 {
0076     // ensure settings path exist, for e.g. local settings.json
0077     QDir().mkpath(m_settingsPath);
0078 
0079     // ensure we don't spam the user with debug messages per default, do this just once
0080     if (!oldCategoryFilter) {
0081         oldCategoryFilter = QLoggingCategory::installFilter(myCategoryFilter);
0082     }
0083 
0084     // apply our config
0085     readConfig();
0086 }
0087 
0088 LSPClientPlugin::~LSPClientPlugin()
0089 {
0090 }
0091 
0092 QObject *LSPClientPlugin::createView(KTextEditor::MainWindow *mainWindow)
0093 {
0094     if (!m_serverManager) {
0095         m_serverManager = LSPClientServerManager::new_(this);
0096     }
0097     return LSPClientPluginView::new_(this, mainWindow, m_serverManager);
0098 }
0099 
0100 int LSPClientPlugin::configPages() const
0101 {
0102     return 1;
0103 }
0104 
0105 KTextEditor::ConfigPage *LSPClientPlugin::configPage(int number, QWidget *parent)
0106 {
0107     if (number != 0) {
0108         return nullptr;
0109     }
0110 
0111     return new LSPClientConfigPage(parent, this);
0112 }
0113 
0114 void LSPClientPlugin::readConfig()
0115 {
0116     KConfigGroup config(KSharedConfig::openConfig(), CONFIG_LSPCLIENT);
0117     m_symbolDetails = config.readEntry(CONFIG_SYMBOL_DETAILS, false);
0118     m_symbolTree = config.readEntry(CONFIG_SYMBOL_TREE, true);
0119     m_symbolExpand = config.readEntry(CONFIG_SYMBOL_EXPAND, true);
0120     m_symbolSort = config.readEntry(CONFIG_SYMBOL_SORT, false);
0121     m_complDoc = config.readEntry(CONFIG_COMPLETION_DOC, true);
0122     m_refDeclaration = config.readEntry(CONFIG_REFERENCES_DECLARATION, true);
0123     m_complParens = config.readEntry(CONFIG_COMPLETION_PARENS, true);
0124     m_autoHover = config.readEntry(CONFIG_AUTO_HOVER, true);
0125     m_onTypeFormatting = config.readEntry(CONFIG_TYPE_FORMATTING, false);
0126     m_incrementalSync = config.readEntry(CONFIG_INCREMENTAL_SYNC, false);
0127     m_highlightGoto = config.readEntry(CONFIG_HIGHLIGHT_GOTO, true);
0128     m_diagnostics = config.readEntry(CONFIG_DIAGNOSTICS, true);
0129     m_messages = config.readEntry(CONFIG_MESSAGES, true);
0130     m_configPath = config.readEntry(CONFIG_SERVER_CONFIG, QUrl());
0131     m_semanticHighlighting = config.readEntry(CONFIG_SEMANTIC_HIGHLIGHTING, true);
0132     m_signatureHelp = config.readEntry(CONFIG_SIGNATURE_HELP, true);
0133     m_autoImport = config.readEntry(CONFIG_AUTO_IMPORT, true);
0134     m_fmtOnSave = config.readEntry(CONFIG_FORMAT_ON_SAVE, false);
0135     m_inlayHints = config.readEntry(CONFIG_INLAY_HINT, false);
0136 
0137     // read allow + block lists as two separate keys, let block always win
0138     const auto allowed = config.readEntry(CONFIG_ALLOWED_COMMANDS, QStringList());
0139     const auto blocked = config.readEntry(CONFIG_BLOCKED_COMMANDS, QStringList());
0140     m_serverCommandLineToAllowedState.clear();
0141     for (const auto &cmd : allowed) {
0142         m_serverCommandLineToAllowedState[cmd] = true;
0143     }
0144     for (const auto &cmd : blocked) {
0145         m_serverCommandLineToAllowedState[cmd] = false;
0146     }
0147 
0148     Q_EMIT update();
0149 }
0150 
0151 void LSPClientPlugin::writeConfig() const
0152 {
0153     KConfigGroup config(KSharedConfig::openConfig(), CONFIG_LSPCLIENT);
0154     config.writeEntry(CONFIG_SYMBOL_DETAILS, m_symbolDetails);
0155     config.writeEntry(CONFIG_SYMBOL_TREE, m_symbolTree);
0156     config.writeEntry(CONFIG_SYMBOL_EXPAND, m_symbolExpand);
0157     config.writeEntry(CONFIG_SYMBOL_SORT, m_symbolSort);
0158     config.writeEntry(CONFIG_COMPLETION_DOC, m_complDoc);
0159     config.writeEntry(CONFIG_REFERENCES_DECLARATION, m_refDeclaration);
0160     config.writeEntry(CONFIG_COMPLETION_PARENS, m_complParens);
0161     config.writeEntry(CONFIG_AUTO_HOVER, m_autoHover);
0162     config.writeEntry(CONFIG_TYPE_FORMATTING, m_onTypeFormatting);
0163     config.writeEntry(CONFIG_INCREMENTAL_SYNC, m_incrementalSync);
0164     config.writeEntry(CONFIG_HIGHLIGHT_GOTO, m_highlightGoto);
0165     config.writeEntry(CONFIG_DIAGNOSTICS, m_diagnostics);
0166     config.writeEntry(CONFIG_MESSAGES, m_messages);
0167     config.writeEntry(CONFIG_SERVER_CONFIG, m_configPath);
0168     config.writeEntry(CONFIG_SEMANTIC_HIGHLIGHTING, m_semanticHighlighting);
0169     config.writeEntry(CONFIG_SIGNATURE_HELP, m_signatureHelp);
0170     config.writeEntry(CONFIG_AUTO_IMPORT, m_autoImport);
0171     config.writeEntry(CONFIG_FORMAT_ON_SAVE, m_fmtOnSave);
0172     config.writeEntry(CONFIG_INLAY_HINT, m_inlayHints);
0173 
0174     // write allow + block lists as two separate keys
0175     QStringList allowed, blocked;
0176     for (const auto &it : m_serverCommandLineToAllowedState) {
0177         if (it.second) {
0178             allowed.push_back(it.first);
0179         } else {
0180             blocked.push_back(it.first);
0181         }
0182     }
0183     config.writeEntry(CONFIG_ALLOWED_COMMANDS, allowed);
0184     config.writeEntry(CONFIG_BLOCKED_COMMANDS, blocked);
0185 
0186     Q_EMIT update();
0187 }
0188 
0189 bool LSPClientPlugin::isCommandLineAllowed(const QStringList &cmdline)
0190 {
0191     // check our allow list
0192     // if we already have stored some value, perfect, just use that one
0193     const QString fullCommandLineString = cmdline.join(QStringLiteral(" "));
0194     if (const auto it = m_serverCommandLineToAllowedState.find(fullCommandLineString); it != m_serverCommandLineToAllowedState.end()) {
0195         return it->second;
0196     }
0197 
0198     // else: queue asking the user async, will ensure we don't have duplicate dialogs
0199     // tell the launching it is forbidden ATM
0200     QTimer::singleShot(0, this, [this, fullCommandLineString]() {
0201         askForCommandLinePermission(fullCommandLineString);
0202     });
0203     return false;
0204 }
0205 
0206 void LSPClientPlugin::askForCommandLinePermission(const QString &fullCommandLineString)
0207 {
0208     // did we already store a new result for the given command line? just use that
0209     if (const auto it = m_serverCommandLineToAllowedState.find(fullCommandLineString); it != m_serverCommandLineToAllowedState.end()) {
0210         // we need to trigger config update if the command is not blocked any longer
0211         if (it->second) {
0212             Q_EMIT update();
0213         }
0214         return;
0215     }
0216 
0217     // is this command line request already open? => skip the new request, dialog is already there
0218     if (!m_currentActiveCommandLineDialogs.insert(fullCommandLineString).second) {
0219         return;
0220     }
0221 
0222     // ask user if the start should be allowed
0223     QPointer<QMessageBox> msgBox(new QMessageBox(QApplication::activeWindow()));
0224     msgBox->setWindowTitle(i18n("LSP server start requested"));
0225     msgBox->setTextFormat(Qt::RichText);
0226     msgBox->setText(
0227         i18n("Do you want the LSP server to be started?<br><br>The full command line is:<br><br><b>%1</b><br><br>The choice can be altered via the config page "
0228              "of the plugin.",
0229              fullCommandLineString.toHtmlEscaped()));
0230     msgBox->setStandardButtons(QMessageBox::Yes | QMessageBox::No);
0231     msgBox->setDefaultButton(QMessageBox::Yes);
0232     const bool allowed = (msgBox->exec() == QMessageBox::Yes);
0233 
0234     // store new configured value
0235     m_serverCommandLineToAllowedState.emplace(fullCommandLineString, allowed);
0236 
0237     // inform the user if it was forbidden! do this here to just emit this once
0238     if (!allowed) {
0239         Q_EMIT showMessage(KTextEditor::Message::Information,
0240                            i18n("User permanently blocked start of: '%1'.\nUse the config page of the plugin to undo this block.", fullCommandLineString));
0241     }
0242 
0243     // purge dialog entry
0244     m_currentActiveCommandLineDialogs.erase(fullCommandLineString);
0245 
0246     // flush config, will trigger update()
0247     writeConfig();
0248 }
0249 
0250 #include "lspclientplugin.moc"
0251 #include "moc_lspclientplugin.cpp"