1 /*
2     SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
3 
4     SPDX-License-Identifier: MIT
5 */
6 
7 #include "lspclientconfigpage.h"
8 #include "lspclientplugin.h"
9 #include "ui_lspconfigwidget.h"
10 
11 #include <KLocalizedString>
12 
13 #include <KSyntaxHighlighting/Definition>
14 #include <KSyntaxHighlighting/Repository>
15 #include <KSyntaxHighlighting/SyntaxHighlighter>
16 #include <KSyntaxHighlighting/Theme>
17 
18 #include <KTextEditor/Editor>
19 
20 #include <QJsonDocument>
21 #include <QJsonParseError>
22 #include <QPalette>
23 
LSPClientConfigPage(QWidget * parent,LSPClientPlugin * plugin)24 LSPClientConfigPage::LSPClientConfigPage(QWidget *parent, LSPClientPlugin *plugin)
25     : KTextEditor::ConfigPage(parent)
26     , m_plugin(plugin)
27 {
28     ui = new Ui::LspConfigWidget();
29     ui->setupUi(this);
30 
31     // fix-up our two text edits to be proper JSON file editors
32     updateHighlighters();
33 
34     // ensure we update the highlighters if the repository is updated or theme is changed
35     connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::repositoryReloaded, this, &LSPClientConfigPage::updateHighlighters);
36     connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, &LSPClientConfigPage::updateHighlighters);
37 
38     // setup default json settings
39     QFile defaultConfigFile(QStringLiteral(":/lspclient/settings.json"));
40     defaultConfigFile.open(QIODevice::ReadOnly);
41     Q_ASSERT(defaultConfigFile.isOpen());
42     ui->defaultConfig->setPlainText(QString::fromUtf8(defaultConfigFile.readAll()));
43 
44     // setup default config path as placeholder to show user where it is
45     ui->edtConfigPath->setPlaceholderText(m_plugin->m_defaultConfigPath.toLocalFile());
46 
47     reset();
48 
49     for (const auto &cb : {ui->chkSymbolDetails,
50                            ui->chkSymbolExpand,
51                            ui->chkSymbolSort,
52                            ui->chkSymbolTree,
53                            ui->chkComplDoc,
54                            ui->chkRefDeclaration,
55                            ui->chkComplParens,
56                            ui->chkDiagnostics,
57                            ui->chkDiagnosticsMark,
58                            ui->chkDiagnosticsHover,
59                            ui->chkMessages,
60                            ui->chkOnTypeFormatting,
61                            ui->chkIncrementalSync,
62                            ui->chkHighlightGoto,
63                            ui->chkSemanticHighlighting,
64                            ui->chkAutoHover,
65                            ui->chkSignatureHelp}) {
66         connect(cb, &QCheckBox::toggled, this, &LSPClientConfigPage::changed);
67     }
68     auto ch = [this](int) {
69         this->changed();
70     };
71     connect(ui->spinDiagnosticsSize, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, ch);
72     connect(ui->edtConfigPath, &KUrlRequester::textChanged, this, &LSPClientConfigPage::configUrlChanged);
73     connect(ui->edtConfigPath, &KUrlRequester::urlSelected, this, &LSPClientConfigPage::configUrlChanged);
74 
75     auto cfgh = [this](int position, int added, int removed) {
76         Q_UNUSED(position);
77         // discard format change
78         // (e.g. due to syntax highlighting)
79         if (added || removed) {
80             configTextChanged();
81         }
82     };
83     connect(ui->userConfig->document(), &QTextDocument::contentsChange, this, cfgh);
84 
85     // custom control logic
86     auto h = [this]() {
87         bool enabled = ui->chkDiagnostics->isChecked();
88         ui->chkDiagnosticsHighlight->setEnabled(enabled);
89         ui->chkDiagnosticsMark->setEnabled(enabled);
90         ui->chkDiagnosticsHover->setEnabled(enabled);
91         enabled = enabled && ui->chkDiagnosticsHover->isChecked();
92         ui->spinDiagnosticsSize->setEnabled(enabled);
93         enabled = ui->chkMessages->isChecked();
94     };
95     connect(this, &LSPClientConfigPage::changed, this, h);
96 }
97 
~LSPClientConfigPage()98 LSPClientConfigPage::~LSPClientConfigPage()
99 {
100     delete ui;
101 }
102 
name() const103 QString LSPClientConfigPage::name() const
104 {
105     return QString(i18n("LSP Client"));
106 }
107 
fullName() const108 QString LSPClientConfigPage::fullName() const
109 {
110     return QString(i18n("LSP Client"));
111 }
112 
icon() const113 QIcon LSPClientConfigPage::icon() const
114 {
115     return QIcon::fromTheme(QLatin1String("code-context"));
116 }
117 
apply()118 void LSPClientConfigPage::apply()
119 {
120     m_plugin->m_symbolDetails = ui->chkSymbolDetails->isChecked();
121     m_plugin->m_symbolTree = ui->chkSymbolTree->isChecked();
122     m_plugin->m_symbolExpand = ui->chkSymbolExpand->isChecked();
123     m_plugin->m_symbolSort = ui->chkSymbolSort->isChecked();
124 
125     m_plugin->m_complDoc = ui->chkComplDoc->isChecked();
126     m_plugin->m_refDeclaration = ui->chkRefDeclaration->isChecked();
127     m_plugin->m_complParens = ui->chkComplParens->isChecked();
128 
129     m_plugin->m_diagnostics = ui->chkDiagnostics->isChecked();
130     m_plugin->m_diagnosticsHighlight = ui->chkDiagnosticsHighlight->isChecked();
131     m_plugin->m_diagnosticsMark = ui->chkDiagnosticsMark->isChecked();
132     m_plugin->m_diagnosticsHover = ui->chkDiagnosticsHover->isChecked();
133     m_plugin->m_diagnosticsSize = ui->spinDiagnosticsSize->value();
134 
135     m_plugin->m_autoHover = ui->chkAutoHover->isChecked();
136     m_plugin->m_onTypeFormatting = ui->chkOnTypeFormatting->isChecked();
137     m_plugin->m_incrementalSync = ui->chkIncrementalSync->isChecked();
138     m_plugin->m_highlightGoto = ui->chkHighlightGoto->isChecked();
139     m_plugin->m_semanticHighlighting = ui->chkSemanticHighlighting->isChecked();
140     m_plugin->m_signatureHelp = ui->chkSignatureHelp->isChecked();
141 
142     m_plugin->m_messages = ui->chkMessages->isChecked();
143 
144     m_plugin->m_configPath = ui->edtConfigPath->url();
145 
146     // own scope to ensure file is flushed before we signal below in writeConfig!
147     {
148         QFile configFile(m_plugin->configPath().toLocalFile());
149         configFile.open(QIODevice::WriteOnly);
150         if (configFile.isOpen()) {
151             configFile.write(ui->userConfig->toPlainText().toUtf8());
152         }
153     }
154 
155     m_plugin->writeConfig();
156 }
157 
reset()158 void LSPClientConfigPage::reset()
159 {
160     ui->chkSymbolDetails->setChecked(m_plugin->m_symbolDetails);
161     ui->chkSymbolTree->setChecked(m_plugin->m_symbolTree);
162     ui->chkSymbolExpand->setChecked(m_plugin->m_symbolExpand);
163     ui->chkSymbolSort->setChecked(m_plugin->m_symbolSort);
164 
165     ui->chkComplDoc->setChecked(m_plugin->m_complDoc);
166     ui->chkRefDeclaration->setChecked(m_plugin->m_refDeclaration);
167     ui->chkComplParens->setChecked(m_plugin->m_complParens);
168 
169     ui->chkDiagnostics->setChecked(m_plugin->m_diagnostics);
170     ui->chkDiagnosticsHighlight->setChecked(m_plugin->m_diagnosticsHighlight);
171     ui->chkDiagnosticsMark->setChecked(m_plugin->m_diagnosticsMark);
172     ui->chkDiagnosticsHover->setChecked(m_plugin->m_diagnosticsHover);
173     ui->spinDiagnosticsSize->setValue(m_plugin->m_diagnosticsSize);
174 
175     ui->chkAutoHover->setChecked(m_plugin->m_autoHover);
176     ui->chkOnTypeFormatting->setChecked(m_plugin->m_onTypeFormatting);
177     ui->chkIncrementalSync->setChecked(m_plugin->m_incrementalSync);
178     ui->chkHighlightGoto->setChecked(m_plugin->m_highlightGoto);
179     ui->chkSemanticHighlighting->setChecked(m_plugin->m_semanticHighlighting);
180     ui->chkSignatureHelp->setChecked(m_plugin->m_signatureHelp);
181 
182     ui->chkMessages->setChecked(m_plugin->m_messages);
183 
184     ui->edtConfigPath->setUrl(m_plugin->m_configPath);
185 
186     readUserConfig(m_plugin->configPath().toLocalFile());
187 }
188 
defaults()189 void LSPClientConfigPage::defaults()
190 {
191     reset();
192 }
193 
readUserConfig(const QString & fileName)194 void LSPClientConfigPage::readUserConfig(const QString &fileName)
195 {
196     QFile configFile(fileName);
197     configFile.open(QIODevice::ReadOnly);
198     if (configFile.isOpen()) {
199         ui->userConfig->setPlainText(QString::fromUtf8(configFile.readAll()));
200     } else {
201         ui->userConfig->clear();
202     }
203 
204     updateConfigTextErrorState();
205 }
206 
updateConfigTextErrorState()207 void LSPClientConfigPage::updateConfigTextErrorState()
208 {
209     const auto data = ui->userConfig->toPlainText().toUtf8();
210     if (data.isEmpty()) {
211         ui->userConfigError->setText(i18n("No JSON data to validate."));
212         return;
213     }
214 
215     // check json validity
216     QJsonParseError error{};
217     auto json = QJsonDocument::fromJson(data, &error);
218     if (error.error == QJsonParseError::NoError) {
219         if (json.isObject()) {
220             ui->userConfigError->setText(i18n("JSON data is valid."));
221         } else {
222             ui->userConfigError->setText(i18n("JSON data is invalid: no JSON object"));
223         }
224     } else {
225         ui->userConfigError->setText(i18n("JSON data is invalid: %1", error.errorString()));
226     }
227 }
228 
configTextChanged()229 void LSPClientConfigPage::configTextChanged()
230 {
231     // check for errors
232     updateConfigTextErrorState();
233 
234     // remember changed
235     changed();
236 }
237 
configUrlChanged()238 void LSPClientConfigPage::configUrlChanged()
239 {
240     // re-read config
241     readUserConfig(ui->edtConfigPath->url().isEmpty() ? m_plugin->m_defaultConfigPath.toLocalFile() : ui->edtConfigPath->url().toLocalFile());
242 
243     // remember changed
244     changed();
245 }
246 
updateHighlighters()247 void LSPClientConfigPage::updateHighlighters()
248 {
249     for (auto textEdit : {ui->userConfig, static_cast<QTextEdit *>(ui->defaultConfig)}) {
250         // setup JSON highlighter for the default json stuff
251         auto highlighter = new KSyntaxHighlighting::SyntaxHighlighter(textEdit->document());
252         highlighter->setDefinition(KTextEditor::Editor::instance()->repository().definitionForFileName(QStringLiteral("settings.json")));
253 
254         // we want mono-spaced font
255         textEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
256 
257         // we want to have the proper theme for the current palette
258         const auto theme = KTextEditor::Editor::instance()->theme();
259         auto pal = qApp->palette();
260         pal.setColor(QPalette::Base, QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor)));
261         pal.setColor(QPalette::Highlight, QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::TextSelection)));
262         textEdit->setPalette(pal);
263         highlighter->setTheme(theme);
264         highlighter->rehighlight();
265     }
266 }
267