diff --git a/src/plugins/languageclient/client.cpp b/src/plugins/languageclient/client.cpp index c4d72eb873d..ecbd6b68786 100644 --- a/src/plugins/languageclient/client.cpp +++ b/src/plugins/languageclient/client.cpp @@ -1579,6 +1579,12 @@ void Client::initializeCallback(const InitializeRequest::Response &initResponse) } } + if (const BaseSettings *settings = LanguageClientManager::settingForClient(this)) { + const QJsonValue configuration = settings->configuration(); + if (!configuration.isNull()) + updateConfiguration(configuration); + } + for (TextEditor::TextDocument *doc : m_postponedDocuments) openDocument(doc); m_postponedDocuments.clear(); diff --git a/src/plugins/languageclient/languageclientsettings.cpp b/src/plugins/languageclient/languageclientsettings.cpp index e994fefb498..4b7391a71ec 100644 --- a/src/plugins/languageclient/languageclientsettings.cpp +++ b/src/plugins/languageclient/languageclientsettings.cpp @@ -37,6 +37,9 @@ #include #include +#include +#include + #include #include #include @@ -75,6 +78,7 @@ constexpr char startupBehaviorKey[] = "startupBehavior"; constexpr char mimeTypeKey[] = "mimeType"; constexpr char filePatternKey[] = "filePattern"; constexpr char initializationOptionsKey[] = "initializationOptions"; +constexpr char configurationKey[] = "configuration"; constexpr char executableKey[] = "executable"; constexpr char argumentsKey[] = "arguments"; constexpr char settingsGroupKey[] = "LanguageClient"; @@ -523,6 +527,16 @@ QJsonObject BaseSettings::initializationOptions() const expand(m_initializationOptions).toUtf8()).object(); } +QJsonValue BaseSettings::configuration() const +{ + const QJsonDocument document = QJsonDocument::fromJson(m_configuration.toUtf8()); + if (document.isArray()) + return document.array(); + if (document.isObject()) + return document.object(); + return {}; +} + bool BaseSettings::applyFromSettingsWidget(QWidget *widget) { bool changed = false; @@ -593,6 +607,7 @@ QVariantMap BaseSettings::toMap() const map.insert(mimeTypeKey, m_languageFilter.mimeTypes); map.insert(filePatternKey, m_languageFilter.filePattern); map.insert(initializationOptionsKey, m_initializationOptions); + map.insert(configurationKey, m_configuration); return map; } @@ -607,6 +622,7 @@ void BaseSettings::fromMap(const QVariantMap &map) m_languageFilter.filePattern = map[filePatternKey].toStringList(); m_languageFilter.filePattern.removeAll(QString()); // remove empty entries m_initializationOptions = map[initializationOptionsKey].toString(); + m_configuration = map[configurationKey].toString(); } static LanguageClientSettingsPage &settingsPage() @@ -1025,4 +1041,39 @@ bool LanguageFilter::operator!=(const LanguageFilter &other) const return this->filePattern != other.filePattern || this->mimeTypes != other.mimeTypes; } +TextEditor::BaseTextEditor *jsonEditor() +{ + using namespace TextEditor; + BaseTextEditor *editor = PlainTextEditorFactory::createPlainTextEditor(); + TextDocument *document = editor->textDocument(); + TextEditorWidget *widget = editor->editorWidget(); + widget->configureGenericHighlighter(Utils::mimeTypeForName("application/json")); + widget->setLineNumbersVisible(false); + widget->setMarksVisible(false); + widget->setRevisionsVisible(false); + widget->setCodeFoldingSupported(false); + QObject::connect(document, &TextDocument::contentsChanged, widget, [document](){ + const Utils::Id jsonMarkId("LanguageClient.JsonTextMarkId"); + qDeleteAll( + Utils::filtered(document->marks(), Utils::equal(&TextMark::category, jsonMarkId))); + const QString content = document->plainText().trimmed(); + if (content.isEmpty()) + return; + QJsonParseError error; + QJsonDocument::fromJson(content.toUtf8(), &error); + if (error.error == QJsonParseError::NoError) + return; + const Utils::OptionalLineColumn lineColumn + = Utils::Text::convertPosition(document->document(), error.offset); + if (!lineColumn.has_value()) + return; + auto mark = new TextMark(Utils::FilePath(), lineColumn->line, jsonMarkId); + mark->setLineAnnotation(error.errorString()); + mark->setColor(Utils::Theme::CodeModel_Error_TextMarkColor); + mark->setIcon(Utils::Icons::CODEMODEL_ERROR.icon()); + document->addMark(mark); + }); + return editor; +} + } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientsettings.h b/src/plugins/languageclient/languageclientsettings.h index 03a9cf9f84a..d88e00430de 100644 --- a/src/plugins/languageclient/languageclientsettings.h +++ b/src/plugins/languageclient/languageclientsettings.h @@ -51,6 +51,7 @@ class FancyLineEdit; namespace Core { class IDocument; } namespace ProjectExplorer { class Project; } +namespace TextEditor { class BaseTextEditor; } namespace LanguageClient { @@ -88,8 +89,10 @@ public: StartBehavior m_startBehavior = RequiresFile; LanguageFilter m_languageFilter; QString m_initializationOptions; + QString m_configuration; QJsonObject initializationOptions() const; + QJsonValue configuration() const; virtual bool applyFromSettingsWidget(QWidget *widget); virtual QWidget *createSettingsWidget(QWidget *parent = nullptr) const; @@ -212,4 +215,6 @@ private: QLineEdit *m_arguments = nullptr; }; +LANGUAGECLIENT_EXPORT TextEditor::BaseTextEditor *jsonEditor(); + } // namespace LanguageClient diff --git a/src/plugins/python/pythonlanguageclient.cpp b/src/plugins/python/pythonlanguageclient.cpp index 86906317ea9..f39bf0b30c7 100644 --- a/src/plugins/python/pythonlanguageclient.cpp +++ b/src/plugins/python/pythonlanguageclient.cpp @@ -31,20 +31,26 @@ #include "pythonutils.h" #include +#include #include #include #include +#include #include #include #include #include +#include #include +#include #include #include +#include +#include #include #include @@ -167,6 +173,154 @@ static PythonLanguageServerState checkPythonLanguageServer(const FilePath &pytho return {PythonLanguageServerState::CanNotBeInstalled, FilePath()}; } +static const QStringList &plugins() +{ + static const QStringList plugins{"flake8", + "jedi_completion", + "jedi_definition", + "jedi_hover", + "jedi_references", + "jedi_signature_help", + "jedi_symbols", + "mccabe", + "pycodestyle", + "pydocstyle", + "pyflakes", + "pylint", + "rope_completion", + "yapf"}; + return plugins; +} + +class PylsConfigureDialog : public QDialog +{ + Q_DECLARE_TR_FUNCTIONS(PylsConfigureDialog) +public: + PylsConfigureDialog() + : QDialog(Core::ICore::dialogParent()) + , m_editor(jsonEditor()) + , m_advancedLabel(new QLabel) + , m_pluginsGroup(new QGroupBox(tr("Plugins:"))) + { + auto mainLayout = new QVBoxLayout; + + auto pluginsLayout = new QGridLayout; + m_pluginsGroup->setLayout(pluginsLayout); + int i = 0; + for (const QString &plugin : plugins()) { + auto checkBox = new QCheckBox(plugin, this); + connect(checkBox, &QCheckBox::toggled, this, [this, plugin](bool enabled) { + updatePluginEnabled(enabled, plugin); + }); + m_checkBoxes[plugin] = checkBox; + pluginsLayout->addWidget(checkBox, i / 4, i % 4); + ++i; + } + mainLayout->addWidget(m_pluginsGroup); + + const QString labelText = tr( + "For a complete list of avilable options, consult the Python LSP Server configuration documentation."); + + m_advancedLabel->setText(labelText); + m_advancedLabel->setOpenExternalLinks(true); + mainLayout->addWidget(m_advancedLabel); + mainLayout->addWidget(m_editor->editorWidget(), 1); + + setAdvanced(false); + + mainLayout->addStretch(); + + auto buttons = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + + auto advanced = new QPushButton(tr("Advanced")); + advanced->setCheckable(true); + advanced->setChecked(false); + buttons->addButton(advanced, QDialogButtonBox::ActionRole); + + connect(advanced, + &QPushButton::toggled, + this, + &PylsConfigureDialog::setAdvanced); + + connect(buttons->button(QDialogButtonBox::Cancel), + &QPushButton::clicked, + this, + &QDialog::reject); + + connect(buttons->button(QDialogButtonBox::Ok), + &QPushButton::clicked, + this, + &QDialog::accept); + + mainLayout->addWidget(buttons); + setLayout(mainLayout); + + resize(640, 480); + } + + void setConfiguration(const QString &configuration) + { + m_editor->textDocument()->setPlainText(configuration); + updateCheckboxes(); + } + + QString configuration() const { return m_editor->textDocument()->plainText(); } + +private: + void setAdvanced(bool advanced) + { + m_editor->editorWidget()->setVisible(advanced); + m_advancedLabel->setVisible(advanced); + m_pluginsGroup->setVisible(!advanced); + updateCheckboxes(); + } + + void updateCheckboxes() + { + const QJsonDocument document = QJsonDocument::fromJson( + m_editor->textDocument()->plainText().toUtf8()); + if (document.isObject()) { + const QJsonObject pluginsObject + = document.object()["pylsp"].toObject()["plugins"].toObject(); + for (const QString &plugin : plugins()) { + auto checkBox = m_checkBoxes[plugin]; + if (!checkBox) + continue; + const QJsonValue enabled = pluginsObject[plugin].toObject()["enabled"]; + if (!enabled.isBool()) + checkBox->setCheckState(Qt::PartiallyChecked); + else + checkBox->setCheckState(enabled.toBool(false) ? Qt::Checked : Qt::Unchecked); + } + } + } + + void updatePluginEnabled(bool enabled, const QString &plugin) + { + QJsonDocument document = QJsonDocument::fromJson( + m_editor->textDocument()->plainText().toUtf8()); + if (document.isNull()) + return; + QJsonObject config = document.object(); + QJsonObject pylsp = config["pylsp"].toObject(); + QJsonObject plugins = pylsp["plugins"].toObject(); + QJsonObject pluginValue = plugins[plugin].toObject(); + pluginValue.insert("enabled", enabled); + plugins.insert(plugin, pluginValue); + pylsp.insert("plugins", plugins); + config.insert("pylsp", pylsp); + document.setObject(config); + m_editor->textDocument()->setPlainText(QString::fromUtf8(document.toJson())); + } + + QMap m_checkBoxes; + TextEditor::BaseTextEditor *m_editor = nullptr; + QLabel *m_advancedLabel = nullptr; + QGroupBox *m_pluginsGroup = nullptr; +}; + class PyLSSettingsWidget : public QWidget { Q_DECLARE_TR_FUNCTIONS(PyLSSettingsWidget) @@ -175,6 +329,8 @@ public: : QWidget(parent) , m_name(new QLineEdit(settings->m_name, this)) , m_interpreter(new QComboBox(this)) + , m_configure(new QPushButton(tr("Configure..."), this)) + , m_configuration(settings->m_configuration) { int row = 0; auto *mainLayout = new QGridLayout; @@ -191,10 +347,14 @@ public: mainLayout->addWidget(m_interpreter, row, 1); setLayout(mainLayout); + mainLayout->addWidget(m_configure, ++row, 0); + connect(PythonSettings::instance(), &PythonSettings::interpretersChanged, this, &PyLSSettingsWidget::updateInterpreters); + + connect(m_configure, &QPushButton::clicked, this, &PyLSSettingsWidget::showConfigureDialog); } void updateInterpreters(const QList &interpreters, const QString &defaultId) @@ -216,10 +376,21 @@ public: QString name() const { return m_name->text(); } QString interpreterId() const { return m_interpreter->currentData().toString(); } + QString configuration() const { return m_configuration; } private: + void showConfigureDialog() + { + PylsConfigureDialog dialog; + dialog.setConfiguration(m_configuration); + if (dialog.exec() == QDialog::Accepted) + m_configuration = dialog.configuration(); + } + QLineEdit *m_name = nullptr; QComboBox *m_interpreter = nullptr; + QPushButton *m_configure = nullptr; + QString m_configuration; }; PyLSSettings::PyLSSettings() @@ -229,6 +400,8 @@ PyLSSettings::PyLSSettings() m_startBehavior = RequiresFile; m_languageFilter.mimeTypes = QStringList(Constants::C_PY_MIMETYPE); m_arguments = "-m pylsp"; + const QJsonDocument config(defaultConfiguration()); + m_configuration = QString::fromUtf8(config.toJson()); } bool PyLSSettings::isValid() const @@ -248,6 +421,10 @@ QVariantMap PyLSSettings::toMap() const void PyLSSettings::fromMap(const QVariantMap &map) { StdIOSettings::fromMap(map); + if (m_configuration.isEmpty()) { + const QJsonDocument config(defaultConfiguration()); + m_configuration = QString::fromUtf8(config.toJson()); + } setInterpreter(map[interpreterKey].toString()); } @@ -262,6 +439,15 @@ bool PyLSSettings::applyFromSettingsWidget(QWidget *widget) changed |= m_interpreterId != pylswidget->interpreterId(); setInterpreter(pylswidget->interpreterId()); + if (m_configuration != pylswidget->configuration()) { + m_configuration = pylswidget->configuration(); + if (!changed) { // if only the settings configuration changed just send an update + const QVector clients = LanguageClientManager::clientForSetting(this); + for (Client *client : clients) + client->updateConfiguration(configuration()); + } + } + return changed; } @@ -290,6 +476,36 @@ Client *PyLSSettings::createClient(BaseClientInterface *interface) const return new Client(interface); } +QJsonObject PyLSSettings::defaultConfiguration() +{ + static QJsonObject configuration; + if (configuration.isEmpty()) { + QJsonObject enabled; + enabled.insert("enabled", true); + QJsonObject disabled; + disabled.insert("enabled", false); + QJsonObject plugins; + plugins.insert("flake8", disabled); + plugins.insert("jedi_completion", enabled); + plugins.insert("jedi_definition", enabled); + plugins.insert("jedi_hover", enabled); + plugins.insert("jedi_references", enabled); + plugins.insert("jedi_signature_help", enabled); + plugins.insert("jedi_symbols", enabled); + plugins.insert("mccabe", disabled); + plugins.insert("pycodestyle", disabled); + plugins.insert("pydocstyle", disabled); + plugins.insert("pyflakes", enabled); + plugins.insert("pylint", disabled); + plugins.insert("rope_completion", enabled); + plugins.insert("yapf", enabled); + QJsonObject pylsp; + pylsp.insert("plugins", plugins); + configuration.insert("pylsp", pylsp); + } + return configuration; +} + PyLSConfigureAssistant *PyLSConfigureAssistant::instance() { static auto *instance = new PyLSConfigureAssistant(PythonPlugin::instance()); diff --git a/src/plugins/python/pythonlanguageclient.h b/src/plugins/python/pythonlanguageclient.h index 67e0e5f104d..97cb00c311d 100644 --- a/src/plugins/python/pythonlanguageclient.h +++ b/src/plugins/python/pythonlanguageclient.h @@ -57,6 +57,8 @@ public: LanguageClient::Client *createClient(LanguageClient::BaseClientInterface *interface) const final; private: + static QJsonObject defaultConfiguration(); + QString m_interpreterId; PyLSSettings(const PyLSSettings &other) = default;