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

0001 /*
0002     SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include "lspclientpluginview.h"
0008 #include "diagnostics/diagnosticview.h"
0009 #include "gotosymboldialog.h"
0010 #include "inlayhints.h"
0011 #include "lspclientcompletion.h"
0012 #include "lspclienthover.h"
0013 #include "lspclientplugin.h"
0014 #include "lspclientservermanager.h"
0015 #include "lspclientsymbolview.h"
0016 #include "lspclientutils.h"
0017 #include "texthint/KateTextHintManager.h"
0018 
0019 #include "lspclient_debug.h"
0020 
0021 #include <KAcceleratorManager>
0022 #include <KActionCollection>
0023 #include <KActionMenu>
0024 #include <KConfigGroup>
0025 #include <KLocalizedString>
0026 #include <KStandardAction>
0027 #include <KXMLGUIFactory>
0028 
0029 #include <KTextEditor/Document>
0030 #include <KTextEditor/MainWindow>
0031 #include <KTextEditor/Message>
0032 #include <KTextEditor/SessionConfigInterface>
0033 #include <KTextEditor/View>
0034 #include <KXMLGUIClient>
0035 
0036 #include <ktexteditor/editor.h>
0037 #include <ktexteditor/movingrange.h>
0038 #include <ktexteditor_version.h>
0039 
0040 #include <QAction>
0041 #include <QApplication>
0042 #include <QClipboard>
0043 #include <QDateTime>
0044 #include <QFileInfo>
0045 #include <QHBoxLayout>
0046 #include <QHeaderView>
0047 #include <QInputDialog>
0048 #include <QJsonObject>
0049 #include <QKeyEvent>
0050 #include <QKeySequence>
0051 #include <QMenu>
0052 #include <QMessageBox>
0053 #include <QPainter>
0054 #include <QPlainTextEdit>
0055 #include <QPushButton>
0056 #include <QScopeGuard>
0057 #include <QSet>
0058 #include <QStandardItem>
0059 #include <QStringDecoder>
0060 #include <QStyledItemDelegate>
0061 #include <QTimer>
0062 #include <QTreeView>
0063 #include <unordered_map>
0064 #include <utility>
0065 
0066 #include <drawing_utils.h>
0067 #include <ktexteditor_utils.h>
0068 
0069 namespace RangeData
0070 {
0071 enum {
0072     // preserve UserRole for generic use where needed
0073     FileUrlRole = Qt::UserRole + 1,
0074     RangeRole,
0075     KindRole,
0076 };
0077 
0078 class KindEnum
0079 {
0080 public:
0081     enum _kind {
0082         Text = static_cast<int>(LSPDocumentHighlightKind::Text),
0083         Read = static_cast<int>(LSPDocumentHighlightKind::Read),
0084         Write = static_cast<int>(LSPDocumentHighlightKind::Write),
0085     };
0086 
0087     KindEnum(int v)
0088     {
0089         m_value = _kind(v);
0090     }
0091 
0092     KindEnum(LSPDocumentHighlightKind hl)
0093         : KindEnum(static_cast<_kind>(hl))
0094     {
0095     }
0096 
0097     KindEnum(LSPDiagnosticSeverity sev)
0098         : KindEnum(_kind(10 + static_cast<int>(sev)))
0099     {
0100     }
0101 
0102     operator _kind()
0103     {
0104         return m_value;
0105     }
0106 
0107 private:
0108     _kind m_value;
0109 };
0110 
0111 static constexpr KTextEditor::Document::MarkTypes markType = KTextEditor::Document::markType31;
0112 }
0113 
0114 KTextEditor::Document *findDocument(KTextEditor::MainWindow *mainWindow, const QUrl &url)
0115 {
0116     auto views = mainWindow->views();
0117     for (const auto v : views) {
0118         auto doc = v->document();
0119         if (doc && doc->url() == url) {
0120             return doc;
0121         }
0122     }
0123     return nullptr;
0124 }
0125 
0126 // helper to read lines from unopened documents
0127 // lightweight and does not require additional symbols
0128 class FileLineReader
0129 {
0130     QFile file;
0131     int lastLineNo = -1;
0132     QString lastLine;
0133 
0134 public:
0135     FileLineReader(const QUrl &url)
0136         : file(url.toLocalFile())
0137     {
0138         file.open(QIODevice::ReadOnly);
0139     }
0140 
0141     // called with non-descending lineno
0142     QString line(int lineno)
0143     {
0144         if (lineno == lastLineNo) {
0145             return lastLine;
0146         }
0147         while (file.isOpen() && !file.atEnd()) {
0148             auto line = file.readLine();
0149             if (++lastLineNo == lineno) {
0150                 auto toUtf16 = QStringDecoder(QStringDecoder::Utf8);
0151                 QString text = toUtf16(line);
0152                 if (toUtf16.hasError()) {
0153                     text = QString::fromLatin1(line);
0154                 }
0155 
0156                 text = text.trimmed();
0157                 lastLine = text;
0158                 return text;
0159             }
0160         }
0161         return QString();
0162     }
0163 };
0164 
0165 class CloseAllowedMessageBox : public QMessageBox
0166 {
0167 public:
0168     using QMessageBox::QMessageBox;
0169 
0170     void closeEvent(QCloseEvent *) override
0171     {
0172     }
0173 };
0174 
0175 class LocationTreeDelegate : public QStyledItemDelegate
0176 {
0177 public:
0178     LocationTreeDelegate(QObject *parent, const QFont &font)
0179         : QStyledItemDelegate(parent)
0180         , m_monoFont(font)
0181     {
0182     }
0183 
0184     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0185     {
0186         auto options = option;
0187         initStyleOption(&options, index);
0188 
0189         painter->save();
0190 
0191         QString text = index.data().toString();
0192 
0193         options.text = QString(); // clear old text
0194         options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
0195 
0196         QList<QTextLayout::FormatRange> formats;
0197         if (!index.parent().isValid()) {
0198             int lastSlash = text.lastIndexOf(QLatin1Char('/'));
0199             if (lastSlash != -1) {
0200                 QTextCharFormat fmt;
0201                 fmt.setFontWeight(QFont::Bold);
0202                 formats.append({lastSlash + 1, int(text.length() - (lastSlash + 1)), fmt});
0203             }
0204         } else {
0205             // mind translation; let's hope/assume the colon survived
0206             int nextColon = text.indexOf(QLatin1Char(':'), 0);
0207             if (nextColon != 1 && nextColon < text.size()) {
0208                 nextColon = text.indexOf(QLatin1Char(':'), nextColon + 1);
0209             }
0210             if (nextColon != -1) {
0211                 QTextCharFormat fmt;
0212                 fmt.setFont(m_monoFont);
0213                 int codeStart = nextColon + 1;
0214                 formats.append({codeStart, int(text.length() - codeStart), fmt});
0215             }
0216         }
0217 
0218         /** There might be an icon? Make sure to not draw over it **/
0219         auto textRectX = options.widget->style()->subElementRect(QStyle::SE_ItemViewItemText, &options, options.widget).x();
0220         auto width = textRectX - options.rect.x();
0221         painter->translate(width, 0);
0222 
0223         Utils::paintItemViewText(painter, text, options, formats);
0224 
0225         painter->restore();
0226     }
0227 
0228 private:
0229     QFont m_monoFont;
0230 };
0231 
0232 /**
0233  * @brief This is just a helper class that provides "underline" on Ctrl + click
0234  */
0235 class CtrlHoverFeedback : public QObject
0236 {
0237     Q_OBJECT
0238 public:
0239     void highlight(KTextEditor::View *activeView)
0240     {
0241         // sanity checksQString
0242         if (!activeView) {
0243             return;
0244         }
0245 
0246         auto doc = activeView->document();
0247         if (!doc) {
0248             return;
0249         }
0250 
0251         if (!w) {
0252             return;
0253         }
0254 
0255         w->setCursor(Qt::PointingHandCursor);
0256 
0257         // underline the hovered word
0258         auto &mr = docs[doc];
0259         if (mr) {
0260             mr->setRange(range);
0261         } else {
0262             mr.reset(doc->newMovingRange(range));
0263             connect(doc, &KTextEditor::Document::aboutToInvalidateMovingInterfaceContent, this, &CtrlHoverFeedback::clearMovingRange, Qt::UniqueConnection);
0264             connect(doc, &KTextEditor::Document::aboutToDeleteMovingInterfaceContent, this, &CtrlHoverFeedback::clearMovingRange, Qt::UniqueConnection);
0265         }
0266 
0267         static KTextEditor::Attribute::Ptr attr;
0268         if (!attr) {
0269             attr = new KTextEditor::Attribute;
0270             attr->setUnderlineStyle(QTextCharFormat::SingleUnderline);
0271         }
0272         mr->setAttribute(attr);
0273     }
0274 
0275     void clear(KTextEditor::View *activeView)
0276     {
0277         if (activeView) {
0278             auto doc = activeView->document();
0279             auto it = docs.find(doc);
0280             if (it != docs.end()) {
0281                 auto &mr = it->second;
0282                 if (mr) {
0283                     mr->setRange(KTextEditor::Range::invalid());
0284                 }
0285             }
0286         }
0287         if (w && w->cursor() != Qt::IBeamCursor) {
0288             w->setCursor(Qt::IBeamCursor);
0289         }
0290         w.clear();
0291     }
0292 
0293     void setRangeAndWidget(const KTextEditor::Range &r, QWidget *wid)
0294     {
0295         range = r;
0296         w = wid;
0297     }
0298 
0299     bool isValid() const
0300     {
0301         return !w.isNull();
0302     }
0303 
0304 private:
0305     Q_SLOT void clearMovingRange(KTextEditor::Document *doc)
0306     {
0307         if (doc) {
0308             auto it = docs.find(doc);
0309             if (it != docs.end()) {
0310                 docs.erase(it);
0311             }
0312         }
0313     }
0314 
0315 private:
0316     QPointer<QWidget> w;
0317     std::unordered_map<KTextEditor::Document *, std::unique_ptr<KTextEditor::MovingRange>> docs;
0318     KTextEditor::Range range;
0319 };
0320 
0321 class LSPClientPluginViewImpl : public QObject, public KXMLGUIClient
0322 {
0323     Q_OBJECT
0324 
0325     typedef LSPClientPluginViewImpl self_type;
0326 
0327     LSPClientPlugin *m_plugin;
0328     KTextEditor::MainWindow *m_mainWindow;
0329     std::shared_ptr<LSPClientServerManager> m_serverManager;
0330     std::unique_ptr<LSPClientCompletion> m_completion;
0331     KateTextHintProvider m_textHintprovider;
0332     std::unique_ptr<LSPClientHover> m_hover;
0333     std::unique_ptr<LSPClientSymbolView> m_symbolView;
0334 
0335     QPointer<QAction> m_findDef;
0336     QPointer<QAction> m_findDecl;
0337     QPointer<QAction> m_findTypeDef;
0338     QPointer<QAction> m_findRef;
0339     QPointer<QAction> m_findImpl;
0340     QPointer<QAction> m_triggerHighlight;
0341     QPointer<QAction> m_triggerSymbolInfo;
0342     QPointer<QAction> m_triggerGotoSymbol;
0343     QPointer<QAction> m_triggerFormat;
0344     QPointer<QAction> m_triggerRename;
0345     QPointer<QAction> m_expandSelection;
0346     QPointer<QAction> m_shrinkSelection;
0347     QPointer<QAction> m_complDocOn;
0348     QPointer<QAction> m_signatureHelp;
0349     QPointer<QAction> m_refDeclaration;
0350     QPointer<QAction> m_complParens;
0351     QPointer<QAction> m_autoHover;
0352     QPointer<QAction> m_onTypeFormatting;
0353     QPointer<QAction> m_incrementalSync;
0354     QPointer<QAction> m_highlightGoto;
0355     QPointer<QAction> m_diagnostics;
0356     QPointer<QAction> m_messages;
0357     QPointer<QAction> m_closeDynamic;
0358     QPointer<QAction> m_restartServer;
0359     QPointer<QAction> m_restartAll;
0360     QPointer<QAction> m_switchSourceHeader;
0361     QPointer<QAction> m_expandMacro;
0362     QPointer<QAction> m_memoryUsage;
0363     QPointer<QAction> m_inlayHints;
0364     QPointer<KActionMenu> m_requestCodeAction;
0365 
0366     QList<QAction *> m_contextMenuActions;
0367 
0368     // toolview
0369     std::unique_ptr<QWidget> m_toolView;
0370     QPointer<QTabWidget> m_tabWidget;
0371     // applied search ranges
0372     typedef QMultiHash<KTextEditor::Document *, KTextEditor::MovingRange *> RangeCollection;
0373     RangeCollection m_ranges;
0374     // applied marks
0375     typedef QSet<KTextEditor::Document *> DocumentCollection;
0376     DocumentCollection m_marks;
0377     // modelis either owned by tree added to tabwidget or owned here
0378     std::unique_ptr<QStandardItemModel> m_ownedModel;
0379     // in either case, the model that directs applying marks/ranges
0380     QPointer<QStandardItemModel> m_markModel;
0381     // goto definition and declaration jump list is more a menu than a
0382     // search result, so let's not keep adding new tabs for those
0383     // previous tree for definition result
0384     QPointer<QTreeView> m_defTree;
0385     // ... and for declaration
0386     QPointer<QTreeView> m_declTree;
0387     // ... and for type definition
0388     QPointer<QTreeView> m_typeDefTree;
0389 
0390     // views on which completions have been registered
0391     QList<KTextEditor::View *> m_completionViews;
0392 
0393     // outstanding request
0394     LSPClientServer::RequestHandle m_handle;
0395     // timeout on request
0396     bool m_req_timeout = false;
0397 
0398     // accept incoming applyEdit
0399     bool m_accept_edit = false;
0400     // characters to trigger format request
0401     QList<QChar> m_onTypeFormattingTriggers;
0402 
0403     // ongoing workDoneProgress
0404     // list of (enhanced server token, progress begin)
0405     QList<std::pair<QString, LSPWorkDoneProgressParams>> m_workDoneProgress;
0406 
0407     CtrlHoverFeedback m_ctrlHoverFeedback = {};
0408 
0409     SemanticHighlighter m_semHighlightingManager;
0410     InlayHintsManager m_inlayHintsHandler;
0411 
0412     class LSPDiagnosticProvider : public DiagnosticsProvider
0413     {
0414     public:
0415         LSPDiagnosticProvider(KTextEditor::MainWindow *mainWin, LSPClientPluginViewImpl *lsp)
0416             : DiagnosticsProvider(mainWin, lsp)
0417             , m_lsp(lsp)
0418         {
0419             name = i18n("LSP");
0420         }
0421 
0422         QJsonObject suppressions(KTextEditor::Document *doc) const override
0423         {
0424             auto config = m_lsp->m_serverManager->findServerConfig(doc);
0425             if (config.isObject()) {
0426                 auto config_o = config.toObject();
0427                 return config_o.value(QStringLiteral("suppressions")).toObject();
0428             }
0429             return {};
0430         }
0431 
0432     private:
0433         LSPClientPluginViewImpl *m_lsp;
0434     };
0435 
0436     LSPDiagnosticProvider m_diagnosticProvider;
0437 
0438 public:
0439     LSPClientPluginViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, std::shared_ptr<LSPClientServerManager> serverManager)
0440         : QObject(mainWin)
0441         , m_plugin(plugin)
0442         , m_mainWindow(mainWin)
0443         , m_serverManager(std::move(serverManager))
0444         , m_completion(LSPClientCompletion::new_(m_serverManager))
0445         , m_textHintprovider(m_mainWindow, this)
0446         , m_hover(LSPClientHover::new_(m_serverManager, &m_textHintprovider))
0447         , m_symbolView(LSPClientSymbolView::new_(plugin, mainWin, m_serverManager))
0448         , m_semHighlightingManager(m_serverManager)
0449         , m_inlayHintsHandler(m_serverManager, this)
0450         , m_diagnosticProvider(mainWin, this)
0451     {
0452         KXMLGUIClient::setComponentName(QStringLiteral("lspclient"), i18n("LSP Client"));
0453         setXMLFile(QStringLiteral("ui.rc"));
0454 
0455         connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::updateState);
0456         connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &self_type::handleEsc);
0457         connect(m_serverManager.get(), &LSPClientServerManager::serverChanged, this, &self_type::onServerChanged);
0458         connect(m_plugin, &LSPClientPlugin::showMessage, this, &self_type::onShowMessage);
0459         connect(m_serverManager.get(), &LSPClientServerManager::serverShowMessage, this, &self_type::onMessage);
0460         connect(m_serverManager.get(), &LSPClientServerManager::serverLogMessage, this, [this](LSPClientServer *server, LSPLogMessageParams params) {
0461             switch (params.type) {
0462             case LSPMessageType::Error:
0463                 params.message.prepend(QStringLiteral("[Error] "));
0464                 break;
0465             case LSPMessageType::Warning:
0466                 params.message.prepend(QStringLiteral("[Warn] "));
0467                 break;
0468             case LSPMessageType::Info:
0469                 params.message.prepend(QStringLiteral("[Info] "));
0470                 break;
0471             case LSPMessageType::Log:
0472                 break;
0473             }
0474             params.type = LSPMessageType::Log;
0475             onMessage(server, params);
0476         });
0477         connect(m_serverManager.get(), &LSPClientServerManager::serverWorkDoneProgress, this, &self_type::onWorkDoneProgress);
0478         connect(m_serverManager.get(), &LSPClientServerManager::showMessageRequest, this, &self_type::showMessageRequest);
0479 
0480         m_findDef = actionCollection()->addAction(QStringLiteral("lspclient_find_definition"), this, &self_type::goToDefinition);
0481         m_findDef->setText(i18n("Go to Definition"));
0482         m_findDecl = actionCollection()->addAction(QStringLiteral("lspclient_find_declaration"), this, &self_type::goToDeclaration);
0483         m_findDecl->setText(i18n("Go to Declaration"));
0484         m_findTypeDef = actionCollection()->addAction(QStringLiteral("lspclient_find_type_definition"), this, &self_type::goToTypeDefinition);
0485         m_findTypeDef->setText(i18n("Go to Type Definition"));
0486         m_findRef = actionCollection()->addAction(QStringLiteral("lspclient_find_references"), this, &self_type::findReferences);
0487         m_findRef->setText(i18n("Find References"));
0488         actionCollection()->setDefaultShortcut(m_findRef, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_L));
0489         m_findImpl = actionCollection()->addAction(QStringLiteral("lspclient_find_implementations"), this, &self_type::findImplementation);
0490         m_findImpl->setText(i18n("Find Implementations"));
0491         m_triggerHighlight = actionCollection()->addAction(QStringLiteral("lspclient_highlight"), this, &self_type::highlight);
0492         m_triggerHighlight->setText(i18n("Highlight"));
0493         m_triggerSymbolInfo = actionCollection()->addAction(QStringLiteral("lspclient_symbol_info"), this, &self_type::symbolInfo);
0494         m_triggerSymbolInfo->setText(i18n("Symbol Info"));
0495         m_triggerGotoSymbol = actionCollection()->addAction(QStringLiteral("lspclient_goto_workspace_symbol"), this, &self_type::gotoWorkSpaceSymbol);
0496         m_triggerGotoSymbol->setText(i18n("Search and Go to Symbol"));
0497         actionCollection()->setDefaultShortcut(m_triggerGotoSymbol, QKeySequence(Qt::ALT | Qt::CTRL | Qt::Key_P));
0498         m_triggerFormat = actionCollection()->addAction(QStringLiteral("lspclient_format"), this, [this]() {
0499             format();
0500         });
0501         m_triggerFormat->setText(i18n("Format"));
0502         actionCollection()->setDefaultShortcut(m_triggerFormat, QKeySequence(QStringLiteral("Ctrl+T, F"), QKeySequence::PortableText));
0503         m_triggerRename = actionCollection()->addAction(QStringLiteral("lspclient_rename"), this, &self_type::rename);
0504         m_triggerRename->setText(i18n("Rename"));
0505         actionCollection()->setDefaultShortcut(m_triggerRename, QKeySequence(Qt::Key_F2));
0506         m_expandSelection = actionCollection()->addAction(QStringLiteral("lspclient_expand_selection"), this, &self_type::expandSelection);
0507         m_expandSelection->setText(i18n("Expand Selection"));
0508         actionCollection()->setDefaultShortcut(m_expandSelection, QKeySequence(Qt::CTRL | Qt::Key_2));
0509         m_shrinkSelection = actionCollection()->addAction(QStringLiteral("lspclient_shrink_selection"), this, &self_type::shrinkSelection);
0510         m_shrinkSelection->setText(i18n("Shrink Selection"));
0511         actionCollection()->setDefaultShortcut(m_shrinkSelection, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_2));
0512         m_switchSourceHeader = actionCollection()->addAction(QStringLiteral("lspclient_clangd_switchheader"), this, &self_type::clangdSwitchSourceHeader);
0513         m_switchSourceHeader->setText(i18n("Switch Source Header"));
0514         actionCollection()->setDefaultShortcut(m_switchSourceHeader, Qt::Key_F12);
0515         m_expandMacro = actionCollection()->addAction(QStringLiteral("lspclient_rust_analyzer_expand_macro"), this, &self_type::rustAnalyzerExpandMacro);
0516         m_expandMacro->setText(i18n("Expand Macro"));
0517         m_requestCodeAction = actionCollection()->add<KActionMenu>(QStringLiteral("lspclient_code_action"));
0518         m_requestCodeAction->setText(i18n("Code Action"));
0519         QList<QKeySequence> scuts;
0520         scuts << QKeySequence(Qt::ALT | Qt::Key_Return) << QKeySequence(Qt::ALT | Qt::Key_Enter);
0521         actionCollection()->setDefaultShortcuts(m_requestCodeAction, scuts);
0522         connect(m_requestCodeAction, &QWidgetAction::triggered, this, [this] {
0523             auto view = m_mainWindow->activeView();
0524             if (m_requestCodeAction && view) {
0525                 const QPoint p = view->cursorPositionCoordinates();
0526                 m_requestCodeAction->menu()->exec(view->mapToGlobal(p));
0527             }
0528         });
0529         connect(m_requestCodeAction->menu(), &QMenu::aboutToShow, this, &self_type::requestCodeAction);
0530 
0531         // general options
0532         m_complDocOn = actionCollection()->addAction(QStringLiteral("lspclient_completion_doc"), this, &self_type::displayOptionChanged);
0533         m_complDocOn->setText(i18n("Show selected completion documentation"));
0534         m_complDocOn->setCheckable(true);
0535         m_signatureHelp = actionCollection()->addAction(QStringLiteral("lspclient_signature_help"), this, &self_type::displayOptionChanged);
0536         m_signatureHelp->setText(i18n("Enable signature help with auto completion"));
0537         m_signatureHelp->setCheckable(true);
0538         m_refDeclaration = actionCollection()->addAction(QStringLiteral("lspclient_references_declaration"), this, &self_type::displayOptionChanged);
0539         m_refDeclaration->setText(i18n("Include declaration in references"));
0540         m_refDeclaration->setCheckable(true);
0541         m_complParens = actionCollection()->addAction(QStringLiteral("lspclient_completion_parens"), this, &self_type::displayOptionChanged);
0542         m_complParens->setText(i18n("Add parentheses upon function completion"));
0543         m_complParens->setCheckable(true);
0544         m_autoHover = actionCollection()->addAction(QStringLiteral("lspclient_auto_hover"), this, &self_type::displayOptionChanged);
0545         m_autoHover->setText(i18n("Show hover information"));
0546         m_autoHover->setCheckable(true);
0547         m_onTypeFormatting = actionCollection()->addAction(QStringLiteral("lspclient_type_formatting"), this, &self_type::displayOptionChanged);
0548         m_onTypeFormatting->setText(i18n("Format on typing"));
0549         m_onTypeFormatting->setCheckable(true);
0550         m_incrementalSync = actionCollection()->addAction(QStringLiteral("lspclient_incremental_sync"), this, &self_type::displayOptionChanged);
0551         m_incrementalSync->setText(i18n("Incremental document synchronization"));
0552         m_incrementalSync->setCheckable(true);
0553         m_highlightGoto = actionCollection()->addAction(QStringLiteral("lspclient_highlight_goto"), this, &self_type::displayOptionChanged);
0554         m_highlightGoto->setText(i18n("Highlight goto location"));
0555         m_highlightGoto->setCheckable(true);
0556         m_inlayHints = actionCollection()->addAction(QStringLiteral("lspclient_inlay_hint"), this, [this](bool checked) {
0557             if (!checked) {
0558                 m_inlayHintsHandler.disable();
0559             }
0560             displayOptionChanged();
0561         });
0562         m_inlayHints->setCheckable(true);
0563         m_inlayHints->setText(i18n("Show Inlay Hints"));
0564 
0565         // diagnostics
0566         m_diagnostics = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics"), this, &self_type::displayOptionChanged);
0567         m_diagnostics->setText(i18n("Show Diagnostics Notifications"));
0568         m_diagnostics->setCheckable(true);
0569 
0570         // messages
0571         m_messages = actionCollection()->addAction(QStringLiteral("lspclient_messages"), this, &self_type::displayOptionChanged);
0572         m_messages->setText(i18n("Show Messages"));
0573         m_messages->setCheckable(true);
0574 
0575         // extra
0576         m_memoryUsage = actionCollection()->addAction(QStringLiteral("lspclient_clangd_memoryusage"), this, &self_type::clangdMemoryUsage);
0577         m_memoryUsage->setText(i18n("Server Memory Usage"));
0578 
0579         // server control and misc actions
0580         m_closeDynamic = actionCollection()->addAction(QStringLiteral("lspclient_close_dynamic"), this, &self_type::closeDynamic);
0581         m_closeDynamic->setText(i18n("Close All Dynamic Reference Tabs"));
0582         m_restartServer = actionCollection()->addAction(QStringLiteral("lspclient_restart_server"), this, &self_type::restartCurrent);
0583         m_restartServer->setText(i18n("Restart LSP Server"));
0584         m_restartAll = actionCollection()->addAction(QStringLiteral("lspclient_restart_all"), this, &self_type::restartAll);
0585         m_restartAll->setText(i18n("Restart All LSP Servers"));
0586 
0587         auto addSeparator = [this]() {
0588             auto *sep1 = new QAction(this);
0589             sep1->setSeparator(true);
0590             m_contextMenuActions << sep1;
0591         };
0592 
0593         m_contextMenuActions << m_requestCodeAction;
0594         addSeparator();
0595 
0596         QAction *goToAction = new QAction(i18n("Go To"));
0597         QMenu *goTo = new QMenu();
0598         goToAction->setMenu(goTo);
0599         goTo->addActions({m_findDecl, m_findDef, m_findTypeDef, m_switchSourceHeader});
0600 
0601         m_contextMenuActions << goToAction;
0602         addSeparator();
0603         m_contextMenuActions << m_findRef;
0604         m_contextMenuActions << m_triggerRename;
0605         addSeparator();
0606 
0607         QAction *lspOtherAction = new QAction(i18n("LSP Client"));
0608         QMenu *lspOther = new QMenu();
0609         lspOtherAction->setMenu(lspOther);
0610         lspOther->addAction(m_findImpl);
0611         lspOther->addAction(m_triggerHighlight);
0612         lspOther->addAction(m_triggerGotoSymbol);
0613         lspOther->addAction(m_expandMacro);
0614         lspOther->addAction(m_triggerFormat);
0615         lspOther->addAction(m_triggerSymbolInfo);
0616         lspOther->addSeparator();
0617         lspOther->addAction(m_closeDynamic);
0618         lspOther->addSeparator();
0619         lspOther->addAction(m_restartServer);
0620         lspOther->addAction(m_restartAll);
0621 
0622         // more options
0623         auto moreOptions = new KActionMenu(i18n("More options"), this);
0624         lspOther->addSeparator();
0625         lspOther->addAction(moreOptions);
0626         moreOptions->addAction(m_complDocOn);
0627         moreOptions->addAction(m_signatureHelp);
0628         moreOptions->addAction(m_refDeclaration);
0629         moreOptions->addAction(m_complParens);
0630         moreOptions->addAction(m_autoHover);
0631         moreOptions->addAction(m_onTypeFormatting);
0632         moreOptions->addAction(m_incrementalSync);
0633         moreOptions->addAction(m_highlightGoto);
0634         moreOptions->addAction(m_inlayHints);
0635         moreOptions->addSeparator();
0636         moreOptions->addAction(m_diagnostics);
0637         moreOptions->addAction(m_messages);
0638         moreOptions->addSeparator();
0639         moreOptions->addAction(m_memoryUsage);
0640 
0641         m_contextMenuActions << lspOtherAction;
0642         addSeparator();
0643 
0644         // sync with plugin settings if updated
0645         connect(m_plugin, &LSPClientPlugin::update, this, &self_type::configUpdated);
0646 
0647         m_diagnosticProvider.setObjectName(QStringLiteral("LSPDiagnosticProvider"));
0648         connect(&m_diagnosticProvider, &DiagnosticsProvider::requestFixes, this, &self_type::fixDiagnostic);
0649         connect(&m_textHintprovider, &KateTextHintProvider::textHintRequested, this, &self_type::onTextHint);
0650 
0651         connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, &self_type::onViewCreated);
0652 
0653         connect(this, &self_type::ctrlClickDefRecieved, this, &self_type::onCtrlMouseMove);
0654 
0655         configUpdated();
0656         updateState();
0657 
0658         m_mainWindow->guiFactory()->addClient(this);
0659     }
0660 
0661     void initToolView()
0662     {
0663         if (m_tabWidget || m_toolView) {
0664             return;
0665         }
0666         // toolview
0667         m_toolView.reset(m_mainWindow->createToolView(m_plugin,
0668                                                       QStringLiteral("kate_lspclient"),
0669                                                       KTextEditor::MainWindow::Bottom,
0670                                                       QIcon::fromTheme(QStringLiteral("format-text-code")),
0671                                                       i18n("LSP")));
0672         m_tabWidget = new QTabWidget(m_toolView.get());
0673         m_toolView->layout()->addWidget(m_tabWidget);
0674         m_tabWidget->setFocusPolicy(Qt::NoFocus);
0675         m_tabWidget->setTabsClosable(true);
0676         KAcceleratorManager::setNoAccel(m_tabWidget);
0677         connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &self_type::tabCloseRequested);
0678         connect(m_tabWidget, &QTabWidget::currentChanged, this, &self_type::tabChanged);
0679     }
0680 
0681     void onViewCreated(KTextEditor::View *view)
0682     {
0683         if (view && view->focusProxy()) {
0684             view->focusProxy()->installEventFilter(this);
0685         }
0686     }
0687 
0688     KTextEditor::View *viewFromWidget(QWidget *widget)
0689     {
0690         if (widget) {
0691             return qobject_cast<KTextEditor::View *>(widget->parent());
0692         }
0693         return nullptr;
0694     }
0695 
0696     /**
0697      * @brief normal word range queried from doc->wordRangeAt() will not include
0698      * full header from a #include line. For example in line "#include <abc/some>"
0699      * we get either abc or some. But for Ctrl + click we need to highlight it as
0700      * one thing, so this function expands the range from wordRangeAt() to do that.
0701      */
0702     static void expandToFullHeaderRange(KTextEditor::Range &range, QStringView lineText)
0703     {
0704         auto expandRangeTo = [lineText, &range](QChar c, int startPos) {
0705             int end = lineText.indexOf(c, startPos);
0706             if (end > -1) {
0707                 auto startC = range.start();
0708                 startC.setColumn(startPos);
0709                 auto endC = range.end();
0710                 endC.setColumn(end);
0711                 range.setStart(startC);
0712                 range.setEnd(endC);
0713             }
0714         };
0715 
0716         int angleBracketPos = lineText.indexOf(QLatin1Char('<'), 7);
0717         if (angleBracketPos > -1) {
0718             expandRangeTo(QLatin1Char('>'), angleBracketPos + 1);
0719         } else {
0720             int startPos = lineText.indexOf(QLatin1Char('"'), 7);
0721             if (startPos > -1) {
0722                 expandRangeTo(QLatin1Char('"'), startPos + 1);
0723             }
0724         }
0725     }
0726 
0727     bool eventFilter(QObject *obj, QEvent *event) override
0728     {
0729         if (!obj->isWidgetType()) {
0730             return QObject::eventFilter(obj, event);
0731         }
0732 
0733         // common stuff that we need for both events
0734         auto viewInternal = static_cast<QWidget *>(obj);
0735         KTextEditor::View *v = viewFromWidget(viewInternal);
0736         if (!v) {
0737             return false;
0738         }
0739 
0740         if (event->type() == QEvent::Leave) {
0741             if (m_ctrlHoverFeedback.isValid()) {
0742                 m_ctrlHoverFeedback.clear(v);
0743             }
0744             return QObject::eventFilter(obj, event);
0745         }
0746 
0747         if (event->type() != QEvent::MouseButtonPress && event->type() != QEvent::MouseMove) {
0748             // we are only concerned with mouse events for now :)
0749             return QObject::eventFilter(obj, event);
0750         }
0751 
0752         auto mouseEvent = static_cast<QMouseEvent *>(event);
0753         const auto coords = viewInternal->mapTo(v, mouseEvent->pos());
0754         const auto cur = v->coordinatesToCursor(coords);
0755         // there isn't much we can do now, just bail out
0756         // if we have selection, and the click is on the selection ignore
0757         // the event as we might block actions like ctrl-drag of text
0758         if (!cur.isValid() || v->selectionRange().contains(cur)) {
0759             return false;
0760         }
0761 
0762         // The user pressed Ctrl + Click
0763         if (event->type() == QEvent::MouseButtonPress) {
0764             if (mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() == Qt::ControlModifier) {
0765                 // must set cursor else we will be jumping somewhere else!!
0766                 v->setCursorPosition(cur);
0767                 m_ctrlHoverFeedback.clear(v);
0768                 goToDefinition();
0769             }
0770         }
0771         // The user is hovering with Ctrl pressed
0772         else if (event->type() == QEvent::MouseMove) {
0773             if (mouseEvent->modifiers() == Qt::ControlModifier) {
0774                 auto doc = v->document();
0775                 auto range = doc->wordRangeAt(cur);
0776                 if (!range.isEmpty()) {
0777                     // check if we are in #include
0778                     // and expand the word range
0779                     auto lineText = doc->line(range.start().line());
0780                     if (lineText.startsWith(QLatin1String("#include")) && range.start().column() > 7) {
0781                         expandToFullHeaderRange(range, lineText);
0782                     }
0783 
0784                     m_ctrlHoverFeedback.setRangeAndWidget(range, viewInternal);
0785                     // this will not go anywhere actually, but just signal whether we have a definition
0786                     // Also, please rethink very hard if you are going to reuse this method. It's made
0787                     // only for Ctrl+Hover
0788                     processCtrlMouseHover(cur);
0789                 } else {
0790                     // if there is no word, unset the cursor and remove the highlight
0791                     m_ctrlHoverFeedback.clear(v);
0792                 }
0793             } else {
0794                 // simple mouse move, make sure to unset the cursor
0795                 // and remove the highlight
0796                 m_ctrlHoverFeedback.clear(v);
0797             }
0798         }
0799 
0800         return false;
0801     }
0802 
0803     ~LSPClientPluginViewImpl() override
0804     {
0805         m_mainWindow->guiFactory()->removeClient(this);
0806 
0807         // unregister all code-completion providers, else we might crash
0808         for (auto view : qAsConst(m_completionViews)) {
0809             view->unregisterCompletionModel(m_completion.get());
0810         }
0811 
0812         clearAllLocationMarks();
0813     }
0814 
0815     void configureTreeView(QTreeView *treeView)
0816     {
0817         treeView->setHeaderHidden(true);
0818         treeView->setFocusPolicy(Qt::NoFocus);
0819         treeView->setLayoutDirection(Qt::LeftToRight);
0820         treeView->setSortingEnabled(false);
0821         treeView->setEditTriggers(QAbstractItemView::NoEditTriggers);
0822 
0823         // styling
0824         treeView->setItemDelegate(new LocationTreeDelegate(treeView, Utils::editorFont()));
0825 
0826         // context menu
0827         treeView->setContextMenuPolicy(Qt::CustomContextMenu);
0828         auto menu = new QMenu(treeView);
0829         menu->addAction(i18n("Expand All"), treeView, &QTreeView::expandAll);
0830         menu->addAction(i18n("Collapse All"), treeView, &QTreeView::collapseAll);
0831         auto h = [treeView, menu](const QPoint &p) {
0832             menu->popup(treeView->viewport()->mapToGlobal(p));
0833         };
0834         connect(treeView, &QTreeView::customContextMenuRequested, h);
0835     }
0836 
0837     void displayOptionChanged()
0838     {
0839         m_serverManager->setIncrementalSync(m_incrementalSync->isChecked());
0840         // use snippets if and only if parentheses are requested
0841         auto &clientCaps = m_serverManager->clientCapabilities();
0842         auto snippetSupport = m_complParens->isChecked();
0843         if (clientCaps.snippetSupport != snippetSupport) {
0844             clientCaps.snippetSupport = snippetSupport;
0845             // restart servers to make it apply
0846             // (not likely frequently toggled)
0847             restartAll();
0848         }
0849         updateState();
0850     }
0851 
0852     void configUpdated()
0853     {
0854         if (m_complDocOn) {
0855             m_complDocOn->setChecked(m_plugin->m_complDoc);
0856         }
0857         if (m_signatureHelp) {
0858             m_signatureHelp->setChecked(m_plugin->m_signatureHelp);
0859         }
0860         if (m_refDeclaration) {
0861             m_refDeclaration->setChecked(m_plugin->m_refDeclaration);
0862         }
0863         if (m_complParens) {
0864             m_complParens->setChecked(m_plugin->m_complParens);
0865         }
0866         if (m_autoHover) {
0867             m_autoHover->setChecked(m_plugin->m_autoHover);
0868         }
0869         if (m_onTypeFormatting) {
0870             m_onTypeFormatting->setChecked(m_plugin->m_onTypeFormatting);
0871         }
0872         if (m_incrementalSync) {
0873             m_incrementalSync->setChecked(m_plugin->m_incrementalSync);
0874         }
0875         if (m_highlightGoto) {
0876             m_highlightGoto->setChecked(m_plugin->m_highlightGoto);
0877         }
0878         if (m_diagnostics) {
0879             m_diagnostics->setChecked(m_plugin->m_diagnostics);
0880         }
0881         if (m_messages) {
0882             m_messages->setChecked(m_plugin->m_messages);
0883         }
0884         if (m_completion) {
0885             m_completion->setAutoImport(m_plugin->m_autoImport);
0886         }
0887         if (m_inlayHints) {
0888             m_inlayHints->setChecked(m_plugin->m_inlayHints);
0889         }
0890         displayOptionChanged();
0891     }
0892 
0893     void restartCurrent()
0894     {
0895         KTextEditor::View *activeView = m_mainWindow->activeView();
0896         auto server = m_serverManager->findServer(activeView);
0897         if (server) {
0898             m_serverManager->restart(server.get());
0899         }
0900     }
0901 
0902     void restartAll()
0903     {
0904         m_serverManager->restart(nullptr);
0905     }
0906 
0907     static void clearMarks(KTextEditor::Document *doc, RangeCollection &ranges, DocumentCollection &docs, uint markType)
0908     {
0909         if (docs.contains(doc)) {
0910             const QHash<int, KTextEditor::Mark *> marks = doc->marks();
0911             QHashIterator<int, KTextEditor::Mark *> i(marks);
0912             while (i.hasNext()) {
0913                 i.next();
0914                 if (i.value()->type & markType) {
0915                     doc->removeMark(i.value()->line, markType);
0916                 }
0917             }
0918             docs.remove(doc);
0919         }
0920 
0921         for (auto it = ranges.find(doc); it != ranges.end() && it.key() == doc;) {
0922             delete it.value();
0923             it = ranges.erase(it);
0924         }
0925     }
0926 
0927     static void clearMarks(RangeCollection &ranges, DocumentCollection &docs, uint markType)
0928     {
0929         while (!ranges.empty()) {
0930             clearMarks(ranges.begin().key(), ranges, docs, markType);
0931         }
0932     }
0933 
0934     Q_SLOT void clearAllMarks(KTextEditor::Document *doc)
0935     {
0936         clearMarks(doc, m_ranges, m_marks, RangeData::markType);
0937     }
0938 
0939     void clearAllLocationMarks()
0940     {
0941         clearMarks(m_ranges, m_marks, RangeData::markType);
0942         // no longer add any again
0943         m_ownedModel.reset();
0944         m_markModel.clear();
0945     }
0946 
0947     void addMarks(KTextEditor::Document *doc, QStandardItem *item, RangeCollection *ranges, DocumentCollection *docs)
0948     {
0949         Q_ASSERT(item);
0950 
0951         // only consider enabled items
0952         if (!(item->flags() & Qt::ItemIsEnabled)) {
0953             return;
0954         }
0955 
0956         auto url = item->data(RangeData::FileUrlRole).toUrl();
0957         // document url could end up empty while in intermediate reload state
0958         // (and then it might match a parent item with no RangeData at all)
0959         if (url != doc->url() || url.isEmpty()) {
0960             return;
0961         }
0962 
0963         KTextEditor::Range range = item->data(RangeData::RangeRole).value<LSPRange>();
0964         if (!range.isValid() || range.isEmpty()) {
0965             return;
0966         }
0967         auto line = range.start().line();
0968         RangeData::KindEnum kind = RangeData::KindEnum(item->data(RangeData::KindRole).toInt());
0969 
0970         KTextEditor::Attribute::Ptr attr;
0971 
0972         bool enabled = false;
0973         switch (kind) {
0974         case RangeData::KindEnum::Text: {
0975             // well, it's a bit like searching for something, so re-use that color
0976             static KTextEditor::Attribute::Ptr searchAttr;
0977             if (!searchAttr) {
0978                 searchAttr = new KTextEditor::Attribute();
0979                 const auto theme = KTextEditor::Editor::instance()->theme();
0980                 QColor rangeColor = theme.editorColor(KSyntaxHighlighting::Theme::SearchHighlight);
0981                 searchAttr->setBackground(rangeColor);
0982                 searchAttr->setForeground(QBrush(theme.textColor(KSyntaxHighlighting::Theme::Normal)));
0983             }
0984             attr = searchAttr;
0985             enabled = true;
0986             break;
0987         }
0988         // FIXME are there any symbolic/configurable ways to pick these colors?
0989         case RangeData::KindEnum::Read: {
0990             static KTextEditor::Attribute::Ptr greenAttr;
0991             if (!greenAttr) {
0992                 const auto theme = KTextEditor::Editor::instance()->theme();
0993                 greenAttr = new KTextEditor::Attribute();
0994                 greenAttr->setBackground(Qt::green);
0995                 greenAttr->setForeground(QBrush(theme.textColor(KSyntaxHighlighting::Theme::Normal)));
0996             }
0997             attr = greenAttr;
0998             enabled = true;
0999             break;
1000         }
1001         case RangeData::KindEnum::Write: {
1002             static KTextEditor::Attribute::Ptr redAttr;
1003             if (!redAttr) {
1004                 const auto theme = KTextEditor::Editor::instance()->theme();
1005                 redAttr = new KTextEditor::Attribute();
1006                 redAttr->setBackground(Qt::red);
1007                 redAttr->setForeground(QBrush(theme.textColor(KSyntaxHighlighting::Theme::Normal)));
1008             }
1009             attr = redAttr;
1010             enabled = true;
1011             break;
1012         }
1013         }
1014 
1015         if (!attr) {
1016             qWarning() << "Unexpected null attr";
1017         }
1018 
1019         // highlight the range
1020         if (enabled && ranges && attr) {
1021             KTextEditor::MovingRange *mr = doc->newMovingRange(range);
1022             mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection
1023             mr->setAttribute(attr);
1024             mr->setAttributeOnlyForViews(true);
1025             ranges->insert(doc, mr);
1026         }
1027 
1028         auto *iface = doc;
1029         KTextEditor::Document::MarkTypes markType = RangeData::markType;
1030         // add match mark for range
1031         switch (markType) {
1032         case RangeData::markType:
1033             iface->setMarkDescription(markType, i18n("RangeHighLight"));
1034             iface->setMarkIcon(markType, QIcon());
1035             enabled = true;
1036             break;
1037         default:
1038             Q_ASSERT(false);
1039             break;
1040         }
1041         if (enabled && docs) {
1042             iface->addMark(line, markType);
1043             docs->insert(doc);
1044         }
1045 
1046         connect(doc, &KTextEditor::Document::aboutToInvalidateMovingInterfaceContent, this, &self_type::clearAllMarks, Qt::UniqueConnection);
1047         connect(doc, &KTextEditor::Document::aboutToDeleteMovingInterfaceContent, this, &self_type::clearAllMarks, Qt::UniqueConnection);
1048 
1049         // reload might save/restore marks before/after above signals, so let's clear before that
1050         connect(doc, &KTextEditor::Document::aboutToReload, this, &self_type::clearAllMarks, Qt::UniqueConnection);
1051     }
1052 
1053     void addMarksRec(KTextEditor::Document *doc, QStandardItem *item, RangeCollection *ranges, DocumentCollection *docs)
1054     {
1055         Q_ASSERT(item);
1056         addMarks(doc, item, ranges, docs);
1057         for (int i = 0; i < item->rowCount(); ++i) {
1058             addMarksRec(doc, item->child(i), ranges, docs);
1059         }
1060     }
1061 
1062     void addMarks(KTextEditor::Document *doc, QStandardItemModel *treeModel, RangeCollection &ranges, DocumentCollection &docs)
1063     {
1064         // check if already added
1065         auto oranges = ranges.contains(doc) ? nullptr : &ranges;
1066         auto odocs = docs.contains(doc) ? nullptr : &docs;
1067 
1068         if (!oranges && !odocs) {
1069             return;
1070         }
1071 
1072         Q_ASSERT(treeModel);
1073         addMarksRec(doc, treeModel->invisibleRootItem(), oranges, odocs);
1074     }
1075 
1076     void goToDocumentLocation(const QUrl &uri, const KTextEditor::Range &location)
1077     {
1078         int line = location.start().line();
1079         int column = location.start().column();
1080         KTextEditor::View *activeView = m_mainWindow->activeView();
1081         if (!activeView || uri.isEmpty() || line < 0 || column < 0) {
1082             return;
1083         }
1084 
1085         KTextEditor::Document *document = activeView->document();
1086         KTextEditor::Cursor cdef(line, column);
1087 
1088         KTextEditor::View *targetView = nullptr;
1089         if (document && uri == document->url()) {
1090             targetView = activeView;
1091         } else {
1092             targetView = m_mainWindow->openUrl(uri);
1093         }
1094         if (targetView) {
1095             // save current position for location history
1096             Utils::addPositionToHistory(activeView->document()->url(), activeView->cursorPosition(), m_mainWindow);
1097             // save the position to which we are jumping in location history
1098             Utils::addPositionToHistory(targetView->document()->url(), cdef, m_mainWindow);
1099             targetView->setCursorPosition(cdef);
1100             highlightLandingLocation(targetView, location);
1101         }
1102     }
1103 
1104     /**
1105      * @brief give a short 1sec temporary highlight where you land
1106      */
1107     void highlightLandingLocation(KTextEditor::View *view, const KTextEditor::Range &location)
1108     {
1109         if (!m_highlightGoto || !m_highlightGoto->isChecked()) {
1110             return;
1111         }
1112         auto doc = view->document();
1113         if (!doc) {
1114             return;
1115         }
1116         auto mr = doc->newMovingRange(location);
1117         KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute);
1118         attr->setUnderlineStyle(QTextCharFormat::SingleUnderline);
1119         mr->setView(view);
1120         mr->setAttribute(attr);
1121         QTimer::singleShot(1000, doc, [mr] {
1122             mr->setRange(KTextEditor::Range::invalid());
1123             delete mr;
1124         });
1125     }
1126 
1127     static QModelIndex getPrimaryModelIndex(QModelIndex index)
1128     {
1129         // in case of a multiline diagnostics item, a split secondary line has no data set
1130         // so we need to go up to the primary parent item
1131         if (!index.data(RangeData::RangeRole).isValid() && index.parent().data(RangeData::RangeRole).isValid()) {
1132             return index.parent();
1133         }
1134         return index;
1135     }
1136 
1137     void goToItemLocation(const QModelIndex &_index)
1138     {
1139         auto index = getPrimaryModelIndex(_index);
1140         auto url = index.data(RangeData::FileUrlRole).toUrl();
1141         auto start = index.data(RangeData::RangeRole).value<LSPRange>();
1142         goToDocumentLocation(url, start);
1143     }
1144 
1145     void fixDiagnostic(const QUrl url, const Diagnostic &diagnostic, const QVariant &data)
1146     {
1147         KTextEditor::View *activeView = m_mainWindow->activeView();
1148         QPointer<KTextEditor::Document> document = activeView->document();
1149         auto server = m_serverManager->findServer(activeView);
1150         if (!server || !document) {
1151             return;
1152         }
1153 
1154         auto executeCodeAction = [this, server](LSPCodeAction action, std::shared_ptr<LSPClientRevisionSnapshot> snapshot) {
1155             // apply edit before command
1156             applyWorkspaceEdit(action.edit, snapshot.get());
1157             executeServerCommand(server, action.command);
1158         };
1159 
1160         // only engage action if active document matches diagnostic document
1161         if (url != document->url()) {
1162             return;
1163         }
1164 
1165         // store some things to find item safely later on
1166         // QPersistentModelIndex pindex(index);
1167         std::shared_ptr<LSPClientRevisionSnapshot> snapshot(m_serverManager->snapshot(server.get()));
1168         auto h = [url, snapshot, executeCodeAction, this, data](const QList<LSPCodeAction> &actions) {
1169             // add actions below diagnostic item
1170             QList<DiagnosticFix> fixes;
1171             for (const auto &action : actions) {
1172                 DiagnosticFix fix;
1173                 fix.fixTitle = action.title;
1174                 fix.fixCallback = [executeCodeAction, action, snapshot] {
1175                     executeCodeAction(action, snapshot);
1176                 };
1177                 fixes << fix;
1178             }
1179             Q_EMIT m_diagnosticProvider.fixesAvailable(fixes, data);
1180         };
1181 
1182         auto range = activeView->selectionRange();
1183         if (!range.isValid()) {
1184             range = {activeView->cursorPosition(), activeView->cursorPosition()};
1185         }
1186         server->documentCodeAction(url, range, {}, {diagnostic}, this, h);
1187     }
1188 
1189     bool tabCloseRequested(int index)
1190     {
1191         auto widget = m_tabWidget->widget(index);
1192         if (m_markModel && widget == m_markModel->parent()) {
1193             clearAllLocationMarks();
1194         }
1195         delete widget;
1196 
1197         if (m_tabWidget->count() == 0) {
1198             m_toolView.release()->deleteLater();
1199         }
1200 
1201         return true;
1202     }
1203 
1204     void tabChanged(int index)
1205     {
1206         // reset to regular foreground
1207         m_tabWidget->tabBar()->setTabTextColor(index, QColor());
1208     }
1209 
1210     void closeDynamic()
1211     {
1212         if (m_tabWidget) {
1213             for (int i = 0; i < m_tabWidget->count();) {
1214                 // if so deemed suitable, tab will be spared and not closed
1215                 if (!tabCloseRequested(i)) {
1216                     ++i;
1217                 }
1218             }
1219         }
1220     }
1221 
1222     // local helper to overcome some differences in LSP types
1223     struct RangeItem {
1224         QUrl uri;
1225         LSPRange range;
1226         LSPDocumentHighlightKind kind;
1227 
1228         bool isValid() const
1229         {
1230             return uri.isValid() && range.isValid();
1231         }
1232     };
1233 
1234     static bool compareRangeItem(const RangeItem &a, const RangeItem &b)
1235     {
1236         return (a.uri < b.uri) || ((a.uri == b.uri) && a.range < b.range);
1237     }
1238 
1239     // provide Qt::DisplayRole (text) line lazily;
1240     // only find line's text content when so requested
1241     // This may then involve opening reading some file, at which time
1242     // all items for that file will be resolved in one go.
1243     struct LineItem : public QStandardItem {
1244         KTextEditor::MainWindow *m_mainWindow;
1245 
1246         LineItem(KTextEditor::MainWindow *mainWindow)
1247             : m_mainWindow(mainWindow)
1248         {
1249         }
1250 
1251         QVariant data(int role = Qt::UserRole + 1) const override
1252         {
1253             auto rootItem = this->parent();
1254             if (role != Qt::DisplayRole || !rootItem) {
1255                 return QStandardItem::data(role);
1256             }
1257 
1258             auto line = data(Qt::UserRole);
1259             // either of these mean we tried to obtain line already
1260             if (line.isValid() || rootItem->data(RangeData::KindRole).toBool()) {
1261                 return QStandardItem::data(role).toString().append(line.toString());
1262             }
1263 
1264             KTextEditor::Document *doc = nullptr;
1265             std::unique_ptr<FileLineReader> fr;
1266             for (int i = 0; i < rootItem->rowCount(); i++) {
1267                 auto child = rootItem->child(i);
1268                 if (i == 0) {
1269                     auto url = child->data(RangeData::FileUrlRole).toUrl();
1270                     doc = findDocument(m_mainWindow, url);
1271                     if (!doc) {
1272                         fr.reset(new FileLineReader(url));
1273                     }
1274                 }
1275                 auto lineno = child->data(RangeData::RangeRole).value<LSPRange>().start().line();
1276                 auto line = doc ? doc->line(lineno) : fr->line(lineno);
1277                 child->setData(line, Qt::UserRole);
1278             }
1279 
1280             // mark as processed
1281             rootItem->setData(RangeData::KindRole, true);
1282 
1283             // should work ok
1284             return data(role);
1285         }
1286     };
1287 
1288     static void
1289     fillItemRoles(QStandardItem *item, const QUrl &url, const LSPRange _range, RangeData::KindEnum kind, const LSPClientRevisionSnapshot *snapshot = nullptr)
1290     {
1291         auto range = snapshot ? transformRange(url, *snapshot, _range) : _range;
1292         item->setData(QVariant(url), RangeData::FileUrlRole);
1293         QVariant vrange;
1294         vrange.setValue(range);
1295         item->setData(vrange, RangeData::RangeRole);
1296         item->setData(static_cast<int>(kind), RangeData::KindRole);
1297     }
1298 
1299     QString getProjectBaseDir()
1300     {
1301         QObject *project = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin"));
1302         if (project) {
1303             auto baseDir = project->property("projectBaseDir").toString();
1304             if (!baseDir.endsWith(QLatin1Char('/'))) {
1305                 return baseDir + QLatin1Char('/');
1306             }
1307 
1308             return baseDir;
1309         }
1310 
1311         return {};
1312     }
1313 
1314     QString shortenPath(QString projectBaseDir, QString url)
1315     {
1316         if (!projectBaseDir.isEmpty() && url.startsWith(projectBaseDir)) {
1317             return url.mid(projectBaseDir.length());
1318         }
1319 
1320         return url;
1321     }
1322 
1323     void makeTree(const QList<RangeItem> &locations, const LSPClientRevisionSnapshot *snapshot)
1324     {
1325         // group by url, assuming input is suitably sorted that way
1326         auto treeModel = new QStandardItemModel();
1327         treeModel->setColumnCount(1);
1328 
1329         QString baseDir = getProjectBaseDir();
1330         QUrl lastUrl;
1331         QStandardItem *parent = nullptr;
1332         for (const auto &loc : locations) {
1333             // ensure we create a parent, if not already there (bug 427270) or we have a different url
1334             if (!parent || loc.uri != lastUrl) {
1335                 if (parent) {
1336                     parent->setText(QStringLiteral("%1: %2").arg(shortenPath(baseDir, lastUrl.toLocalFile())).arg(parent->rowCount()));
1337                 }
1338                 lastUrl = loc.uri;
1339                 parent = new QStandardItem();
1340                 treeModel->appendRow(parent);
1341             }
1342             auto item = new LineItem(m_mainWindow);
1343             parent->appendRow(item);
1344             // add partial display data; line will be added by item later on
1345             item->setText(i18n("Line: %1: ", loc.range.start().line() + 1));
1346             fillItemRoles(item, loc.uri, loc.range, loc.kind, snapshot);
1347         }
1348         if (parent) {
1349             parent->setText(QStringLiteral("%1: %2").arg(shortenPath(baseDir, lastUrl.toLocalFile())).arg(parent->rowCount()));
1350         }
1351 
1352         // plain heuristic; mark for auto-expand all when safe and/or useful to do so
1353         if (treeModel->rowCount() <= 2 || locations.size() <= 20) {
1354             treeModel->invisibleRootItem()->setData(true, RangeData::KindRole);
1355         }
1356 
1357         m_ownedModel.reset(treeModel);
1358         m_markModel = treeModel;
1359     }
1360 
1361     void showTree(const QString &title, QPointer<QTreeView> *targetTree)
1362     {
1363         // create toolview if not present
1364         if (!m_tabWidget) {
1365             initToolView();
1366         }
1367 
1368         // clean up previous target if any
1369         if (targetTree && *targetTree) {
1370             int index = m_tabWidget->indexOf(*targetTree);
1371             if (index >= 0) {
1372                 tabCloseRequested(index);
1373             }
1374         }
1375 
1376         // setup view
1377         auto treeView = new QTreeView();
1378         configureTreeView(treeView);
1379 
1380         // transfer model from owned to tree and that in turn to tabwidget
1381         auto treeModel = m_ownedModel.release();
1382         treeView->setModel(treeModel);
1383         treeModel->setParent(treeView);
1384         int index = m_tabWidget->addTab(treeView, title);
1385         connect(treeView, &QTreeView::clicked, this, &self_type::goToItemLocation);
1386 
1387         if (treeModel->invisibleRootItem()->data(RangeData::KindRole).toBool()) {
1388             treeView->expandAll();
1389         }
1390 
1391         // track for later cleanup
1392         if (targetTree) {
1393             *targetTree = treeView;
1394         }
1395 
1396         // activate the resulting tab
1397         m_tabWidget->setCurrentIndex(index);
1398         m_mainWindow->showToolView(m_toolView.get());
1399     }
1400 
1401     void showMessage(const QString &text, KTextEditor::Message::MessageType level)
1402     {
1403         KTextEditor::View *view = m_mainWindow->activeView();
1404         if (!view || !view->document()) {
1405             return;
1406         }
1407 
1408         auto kmsg = new KTextEditor::Message(text, level);
1409         kmsg->setPosition(KTextEditor::Message::BottomInView);
1410         kmsg->setAutoHide(500);
1411         kmsg->setView(view);
1412         view->document()->postMessage(kmsg);
1413     }
1414 
1415     void handleEsc(QEvent *e)
1416     {
1417         if (!m_mainWindow) {
1418             return;
1419         }
1420 
1421         QKeyEvent *k = static_cast<QKeyEvent *>(e);
1422         if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) {
1423             if (!m_ranges.empty()) {
1424                 clearAllLocationMarks();
1425             } else if (m_toolView && m_toolView->isVisible()) {
1426                 m_mainWindow->hideToolView(m_toolView.get());
1427             }
1428         }
1429     }
1430 
1431     template<typename Handler>
1432     using LocationRequest = std::function<
1433         LSPClientServer::RequestHandle(LSPClientServer &, const QUrl &document, const LSPPosition &pos, const QObject *context, const Handler &h)>;
1434 
1435     template<typename Handler>
1436     void positionRequest(const LocationRequest<Handler> &req,
1437                          const Handler &h,
1438                          std::unique_ptr<LSPClientRevisionSnapshot> *snapshot = nullptr,
1439                          KTextEditor::Cursor cur = KTextEditor::Cursor::invalid())
1440     {
1441         KTextEditor::View *activeView = m_mainWindow->activeView();
1442         auto server = m_serverManager->findServer(activeView);
1443         if (!server) {
1444             return;
1445         }
1446 
1447         // track revision if requested
1448         if (snapshot) {
1449             snapshot->reset(m_serverManager->snapshot(server.get()));
1450         }
1451 
1452         KTextEditor::Cursor cursor = cur.isValid() ? cur : activeView->cursorPosition();
1453 
1454         clearAllLocationMarks();
1455         m_req_timeout = false;
1456         QTimer::singleShot(1000, this, [this] {
1457             m_req_timeout = true;
1458         });
1459         m_handle.cancel() = req(*server, activeView->document()->url(), {cursor.line(), cursor.column()}, this, h);
1460     }
1461 
1462     QString currentWord()
1463     {
1464         KTextEditor::View *activeView = m_mainWindow->activeView();
1465         if (activeView) {
1466             KTextEditor::Cursor cursor = activeView->cursorPosition();
1467             return activeView->document()->wordAt(cursor);
1468         } else {
1469             return QString();
1470         }
1471     }
1472 
1473     Q_SIGNAL void ctrlClickDefRecieved(const RangeItem &range);
1474 
1475     Q_SLOT void onCtrlMouseMove(const RangeItem &range)
1476     {
1477         if (range.isValid()) {
1478             if (m_ctrlHoverFeedback.isValid()) {
1479                 m_ctrlHoverFeedback.highlight(m_mainWindow->activeView());
1480             }
1481         }
1482     }
1483 
1484     // some template and function type trickery here, but at least that buck stops here then ...
1485     template<typename ReplyEntryType, bool doshow = true, typename HandlerType = ReplyHandler<QList<ReplyEntryType>>>
1486     void processLocations(const QString &title,
1487                           const typename utils::identity<LocationRequest<HandlerType>>::type &req,
1488                           bool onlyshow,
1489                           const std::function<RangeItem(const ReplyEntryType &)> &itemConverter,
1490                           QPointer<QTreeView> *targetTree = nullptr)
1491     {
1492         // no capture for move only using initializers available (yet), so shared outer type
1493         // the additional level of indirection is so it can be 'filled-in' after lambda creation
1494         std::shared_ptr<std::unique_ptr<LSPClientRevisionSnapshot>> s(new std::unique_ptr<LSPClientRevisionSnapshot>);
1495         auto h = [this, title, onlyshow, itemConverter, targetTree, s](const QList<ReplyEntryType> &defs) {
1496             if (defs.count() == 0) {
1497                 showMessage(i18n("No results"), KTextEditor::Message::Information);
1498             } else {
1499                 // convert to helper type
1500                 QList<RangeItem> ranges;
1501                 ranges.reserve(defs.size());
1502                 for (const auto &def : defs) {
1503                     ranges.push_back(itemConverter(def));
1504                 }
1505                 // ... so we can sort it also
1506                 std::stable_sort(ranges.begin(), ranges.end(), compareRangeItem);
1507                 makeTree(ranges, s.get()->get());
1508 
1509                 // assuming that reply ranges refer to revision when submitted
1510                 // (not specified anyway in protocol/reply)
1511                 if (defs.count() > 1 || onlyshow) {
1512                     showTree(title, targetTree);
1513                 }
1514                 // it's not nice to jump to some location if we are too late
1515                 if (!m_req_timeout && !onlyshow) {
1516                     // assuming here that the first location is the best one
1517                     const auto &item = itemConverter(defs.at(0));
1518                     goToDocumentLocation(item.uri, item.range);
1519                     // forego mark and such if only a single destination
1520                     if (defs.count() == 1) {
1521                         clearAllLocationMarks();
1522                     }
1523                 }
1524                 // update marks
1525                 updateMarks();
1526             }
1527         };
1528 
1529         positionRequest<HandlerType>(req, h, s.get());
1530     }
1531 
1532     void processCtrlMouseHover(const KTextEditor::Cursor &cursor)
1533     {
1534         auto h = [this](const QList<LSPLocation> &defs) {
1535             if (defs.isEmpty()) {
1536                 return;
1537             } else {
1538                 const auto item = locationToRangeItem(defs.at(0));
1539                 Q_EMIT this->ctrlClickDefRecieved(item);
1540                 return;
1541             }
1542         };
1543 
1544         using Handler = std::function<void(const QList<LSPLocation> &)>;
1545         auto request = &LSPClientServer::documentDefinition;
1546         positionRequest<Handler>(request, h, nullptr, cursor);
1547     }
1548 
1549     static RangeItem locationToRangeItem(const LSPLocation &loc)
1550     {
1551         return {loc.uri, loc.range, LSPDocumentHighlightKind::Text};
1552     }
1553 
1554     void goToDefinition()
1555     {
1556         auto title = i18nc("@title:tab", "Definition: %1", currentWord());
1557         processLocations<LSPLocation>(title, &LSPClientServer::documentDefinition, false, &self_type::locationToRangeItem, &m_defTree);
1558     }
1559 
1560     void goToDeclaration()
1561     {
1562         auto title = i18nc("@title:tab", "Declaration: %1", currentWord());
1563         processLocations<LSPLocation>(title, &LSPClientServer::documentDeclaration, false, &self_type::locationToRangeItem, &m_declTree);
1564     }
1565 
1566     void goToTypeDefinition()
1567     {
1568         auto title = i18nc("@title:tab", "Type Definition: %1", currentWord());
1569         processLocations<LSPLocation>(title, &LSPClientServer::documentTypeDefinition, false, &self_type::locationToRangeItem, &m_typeDefTree);
1570     }
1571 
1572     void findReferences()
1573     {
1574         auto title = i18nc("@title:tab", "References: %1", currentWord());
1575         bool decl = m_refDeclaration->isChecked();
1576         // clang-format off
1577         auto req = [decl](LSPClientServer &server, const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentDefinitionReplyHandler &h)
1578         { return server.documentReferences(document, pos, decl, context, h); };
1579         // clang-format on
1580 
1581         processLocations<LSPLocation>(title, req, true, &self_type::locationToRangeItem);
1582     }
1583 
1584     void findImplementation()
1585     {
1586         auto title = i18nc("@title:tab", "Implementation: %1", currentWord());
1587         processLocations<LSPLocation>(title, &LSPClientServer::documentImplementation, true, &self_type::locationToRangeItem);
1588     }
1589 
1590     void highlight()
1591     {
1592         // determine current url to capture and use later on
1593         QUrl url;
1594         const KTextEditor::View *viewForRequest(m_mainWindow->activeView());
1595         if (viewForRequest && viewForRequest->document()) {
1596             url = viewForRequest->document()->url();
1597         }
1598 
1599         auto title = i18nc("@title:tab", "Highlight: %1", currentWord());
1600         auto converter = [url](const LSPDocumentHighlight &hl) {
1601             return RangeItem{url, hl.range, hl.kind};
1602         };
1603 
1604         processLocations<LSPDocumentHighlight, false>(title, &LSPClientServer::documentHighlight, true, converter);
1605     }
1606 
1607     void symbolInfo()
1608     {
1609         // trigger manually the normally automagic hover
1610         if (auto activeView = m_mainWindow->activeView()) {
1611             m_hover->showTextHint(activeView, activeView->cursorPosition(), true);
1612         }
1613     }
1614 
1615     void requestCodeAction()
1616     {
1617         if (!m_requestCodeAction)
1618             return;
1619         m_requestCodeAction->menu()->clear();
1620 
1621         KTextEditor::View *activeView = m_mainWindow->activeView();
1622         if (!activeView) {
1623             m_requestCodeAction->menu()->addAction(i18n("No Actions"))->setEnabled(false);
1624             return;
1625         }
1626 
1627         KTextEditor::Document *document = activeView->document();
1628         auto server = m_serverManager->findServer(activeView);
1629         auto range = activeView->selectionRange();
1630         if (!range.isValid()) {
1631             range = activeView->document()->wordRangeAt(activeView->cursorPosition());
1632         }
1633         if (!server || !document || !range.isValid()) {
1634             m_requestCodeAction->menu()->addAction(i18n("No Actions"))->setEnabled(false);
1635             return;
1636         }
1637 
1638         QPointer<QAction> loadingAction = m_requestCodeAction->menu()->addAction(i18n("Loading..."));
1639         loadingAction->setEnabled(false);
1640 
1641         // store some things to find item safely later on
1642         std::shared_ptr<LSPClientRevisionSnapshot> snapshot(m_serverManager->snapshot(server.get()));
1643         auto h = [this, snapshot, server, loadingAction](const QList<LSPCodeAction> &actions) {
1644             auto menu = m_requestCodeAction->menu();
1645             // clearing menu also hides it, and so added actions end up not shown
1646             if (actions.isEmpty()) {
1647                 menu->addAction(i18n("No Actions"))->setEnabled(false);
1648             }
1649             for (const auto &action : actions) {
1650                 auto text = action.kind.size() ? QStringLiteral("[%1] %2").arg(action.kind).arg(action.title) : action.title;
1651                 menu->addAction(text, this, [this, action, snapshot, server]() {
1652                     applyWorkspaceEdit(action.edit, snapshot.get());
1653                     executeServerCommand(server, action.command);
1654                 });
1655             }
1656             if (loadingAction) {
1657                 menu->removeAction(loadingAction);
1658             }
1659         };
1660         server->documentCodeAction(document->url(), range, {}, {}, this, h);
1661     }
1662 
1663     void executeServerCommand(std::shared_ptr<LSPClientServer> server, const LSPCommand &command)
1664     {
1665         if (!command.command.isEmpty()) {
1666             // accept edit requests that may be sent to execute command
1667             m_accept_edit = true;
1668             // but only for a short time
1669             QTimer::singleShot(2000, this, [this] {
1670                 m_accept_edit = false;
1671             });
1672             server->executeCommand(command);
1673         }
1674     }
1675 
1676     static void applyEdits(KTextEditor::Document *doc, const LSPClientRevisionSnapshot *snapshot, const QList<LSPTextEdit> &edits)
1677     {
1678         ::applyEdits(doc, snapshot, edits);
1679     }
1680 
1681     void applyEdits(const QUrl &url, const LSPClientRevisionSnapshot *snapshot, const QList<LSPTextEdit> &edits)
1682     {
1683         auto document = findDocument(m_mainWindow, url);
1684         if (!document) {
1685             KTextEditor::View *view = m_mainWindow->openUrl(url);
1686             if (view) {
1687                 document = view->document();
1688             }
1689         }
1690         applyEdits(document, snapshot, edits);
1691     }
1692 
1693     void applyWorkspaceEdit(const LSPWorkspaceEdit &edit, const LSPClientRevisionSnapshot *snapshot)
1694     {
1695         auto currentView = m_mainWindow->activeView();
1696         // edits may be in changes or documentChanges
1697         // the latter is handled in a sneaky way, but not announced in capabilities
1698         for (auto it = edit.changes.begin(); it != edit.changes.end(); ++it) {
1699             applyEdits(it.key(), snapshot, it.value());
1700         }
1701         // ... as/though the document version is not (yet) taken into account
1702         for (auto &change : edit.documentChanges) {
1703             applyEdits(change.textDocument.uri, snapshot, change.edits);
1704         }
1705         if (currentView) {
1706             m_mainWindow->activateView(currentView->document());
1707         }
1708     }
1709 
1710     void onApplyEdit(const LSPApplyWorkspaceEditParams &edit, const ApplyEditReplyHandler &h, bool &handled)
1711     {
1712         if (handled) {
1713             return;
1714         }
1715         handled = true;
1716 
1717         if (m_accept_edit) {
1718             qCInfo(LSPCLIENT) << "applying edit" << edit.label;
1719             applyWorkspaceEdit(edit.edit, nullptr);
1720         } else {
1721             qCInfo(LSPCLIENT) << "ignoring edit";
1722         }
1723         h({m_accept_edit, QString()});
1724     }
1725 
1726     template<typename Collection>
1727     void checkEditResult(const Collection &c)
1728     {
1729         if (c.empty()) {
1730             showMessage(i18n("No edits"), KTextEditor::Message::Information);
1731         }
1732     }
1733 
1734     void delayCancelRequest(LSPClientServer::RequestHandle &&h, int timeout_ms = 4000)
1735     {
1736         QTimer::singleShot(timeout_ms, this, [h]() mutable {
1737             h.cancel();
1738         });
1739     }
1740 
1741     void format(QChar lastChar = QChar(), bool save = false)
1742     {
1743         KTextEditor::View *activeView = m_mainWindow->activeView();
1744         QPointer<KTextEditor::Document> document = activeView ? activeView->document() : nullptr;
1745         auto server = m_serverManager->findServer(activeView);
1746         if (!server || !document) {
1747             return;
1748         }
1749 
1750         int tabSize = 4;
1751         bool insertSpaces = true;
1752         tabSize = document->configValue(QStringLiteral("tab-width")).toInt();
1753         insertSpaces = document->configValue(QStringLiteral("replace-tabs")).toBool();
1754 
1755         // sigh, no move initialization capture ...
1756         // (again) assuming reply ranges wrt revisions submitted at this time
1757         std::shared_ptr<LSPClientRevisionSnapshot> snapshot(m_serverManager->snapshot(server.get()));
1758         auto h = [this, document, snapshot, lastChar, save](const QList<LSPTextEdit> &edits) {
1759             if (lastChar.isNull()) {
1760                 checkEditResult(edits);
1761             }
1762             if (document) {
1763                 // Must clear formatting triggers here otherwise on applying edits we
1764                 // might end up triggering formatting again ending up in an infinite loop
1765                 auto savedTriggers = m_onTypeFormattingTriggers;
1766                 m_onTypeFormattingTriggers.clear();
1767                 applyEdits(document, snapshot.get(), edits);
1768                 m_onTypeFormattingTriggers = savedTriggers;
1769                 if (save) {
1770                     disconnect(document, &KTextEditor::Document::documentSavedOrUploaded, this, &self_type::formatOnSave);
1771                     document->documentSave();
1772                     connect(document, &KTextEditor::Document::documentSavedOrUploaded, this, &self_type::formatOnSave);
1773                 }
1774             }
1775         };
1776 
1777         auto options = LSPFormattingOptions{tabSize, insertSpaces, QJsonObject()};
1778         auto handle = !lastChar.isNull()
1779             ? server->documentOnTypeFormatting(document->url(), activeView->cursorPosition(), lastChar, options, this, h)
1780             : (activeView->selection() ? server->documentRangeFormatting(document->url(), activeView->selectionRange(), options, this, h)
1781                                        : server->documentFormatting(document->url(), options, this, h));
1782         delayCancelRequest(std::move(handle));
1783     }
1784 
1785     void rename()
1786     {
1787         KTextEditor::View *activeView = m_mainWindow->activeView();
1788         QPointer<KTextEditor::Document> document = activeView ? activeView->document() : nullptr;
1789         auto server = m_serverManager->findServer(activeView);
1790         if (!server || !document) {
1791             return;
1792         }
1793 
1794         QString wordUnderCursor = document->wordAt(activeView->cursorPosition());
1795         if (wordUnderCursor.isEmpty()) {
1796             return;
1797         }
1798 
1799         bool ok = false;
1800         // results are typically (too) limited
1801         // due to server implementation or limited view/scope
1802         // so let's add a disclaimer that it's not our fault
1803         QString newName = QInputDialog::getText(activeView,
1804                                                 i18nc("@title:window", "Rename"),
1805                                                 i18nc("@label:textbox", "New name (caution: not all references may be replaced)"),
1806                                                 QLineEdit::Normal,
1807                                                 wordUnderCursor,
1808                                                 &ok);
1809         if (!ok) {
1810             return;
1811         }
1812 
1813         std::shared_ptr<LSPClientRevisionSnapshot> snapshot(m_serverManager->snapshot(server.get()));
1814         auto h = [this, snapshot](const LSPWorkspaceEdit &edit) {
1815             if (edit.documentChanges.empty()) {
1816                 checkEditResult(edit.changes);
1817             }
1818             applyWorkspaceEdit(edit, snapshot.get());
1819         };
1820         auto handle = server->documentRename(document->url(), activeView->cursorPosition(), newName, this, h);
1821         delayCancelRequest(std::move(handle));
1822     }
1823 
1824     void expandSelection()
1825     {
1826         changeSelection(true);
1827     }
1828 
1829     void shrinkSelection()
1830     {
1831         changeSelection(false);
1832     }
1833 
1834     void changeSelection(bool expand)
1835     {
1836         KTextEditor::View *activeView = m_mainWindow->activeView();
1837         QPointer<KTextEditor::Document> document = activeView ? activeView->document() : nullptr;
1838         auto server = m_serverManager->findServer(activeView);
1839         if (!server || !document) {
1840             return;
1841         }
1842 
1843         auto h = [this, activeView, expand](const QList<std::shared_ptr<LSPSelectionRange>> &reply) {
1844             if (reply.isEmpty()) {
1845                 showMessage(i18n("No results"), KTextEditor::Message::Information);
1846             }
1847 
1848             auto cursors = activeView->cursorPositions();
1849 
1850             if (cursors.size() != reply.size()) {
1851                 showMessage(i18n("Not enough results"), KTextEditor::Message::Information);
1852             }
1853 
1854             auto selections = activeView->selectionRanges();
1855             QList<KTextEditor::Range> ret;
1856 
1857             for (int i = 0; i < cursors.size(); i++) {
1858                 const auto &lspSelectionRange = reply.at(i);
1859 
1860                 if (lspSelectionRange) {
1861                     LSPRange currentRange = selections.isEmpty() || !selections.at(i).isValid() ? LSPRange(cursors.at(i), cursors.at(i)) : selections.at(i);
1862 
1863                     auto resultRange = findNextSelection(lspSelectionRange, currentRange, expand);
1864                     ret.append(resultRange);
1865                 } else {
1866                     ret.append(KTextEditor::Range::invalid());
1867                 }
1868             }
1869 
1870             activeView->setSelections(ret);
1871         };
1872 
1873         auto handle = server->selectionRange(document->url(), activeView->cursorPositions(), this, h);
1874         delayCancelRequest(std::move(handle));
1875     }
1876 
1877     static LSPRange findNextSelection(std::shared_ptr<LSPSelectionRange> selectionRange, const LSPRange &current, bool expand)
1878     {
1879         if (expand) {
1880             while (selectionRange && !selectionRange->range.contains(current)) {
1881                 selectionRange = selectionRange->parent;
1882             }
1883 
1884             if (selectionRange) {
1885                 if (selectionRange->range != current) {
1886                     return selectionRange->range;
1887                 } else if (selectionRange->parent) {
1888                     return selectionRange->parent->range;
1889                 }
1890             }
1891         } else {
1892             std::shared_ptr<LSPSelectionRange> previous = nullptr;
1893 
1894             while (selectionRange && current.contains(selectionRange->range) && current != selectionRange->range) {
1895                 previous = selectionRange;
1896                 selectionRange = selectionRange->parent;
1897             }
1898 
1899             if (previous) {
1900                 return previous->range;
1901             }
1902         }
1903 
1904         return LSPRange::invalid();
1905     }
1906 
1907     void clangdSwitchSourceHeader()
1908     {
1909         KTextEditor::View *activeView = m_mainWindow->activeView();
1910         KTextEditor::Document *document = activeView->document();
1911         auto server = m_serverManager->findServer(activeView);
1912         if (!server || !document) {
1913             return;
1914         }
1915 
1916         auto h = [this](const QString &reply) {
1917             if (!reply.isEmpty()) {
1918                 m_mainWindow->openUrl(QUrl(reply));
1919             } else {
1920                 showMessage(i18n("Corresponding Header/Source not found"), KTextEditor::Message::Information);
1921             }
1922         };
1923         server->clangdSwitchSourceHeader(document->url(), this, h);
1924     }
1925 
1926     void clangdMemoryUsage()
1927     {
1928         KTextEditor::View *activeView = m_mainWindow->activeView();
1929         auto server = m_serverManager->findServer(activeView);
1930         if (!server)
1931             return;
1932 
1933         auto h = [this](const QString &reply) {
1934             auto view = m_mainWindow->openUrl(QUrl());
1935             if (view) {
1936                 auto doc = view->document();
1937                 doc->setText(reply);
1938                 // position at top
1939                 view->setCursorPosition({0, 0});
1940                 // adjust mode
1941                 const QString mode = QStringLiteral("JSON");
1942                 doc->setHighlightingMode(mode);
1943                 doc->setMode(mode);
1944                 // no save file dialog when closing
1945                 doc->setModified(false);
1946             }
1947         };
1948         server->clangdMemoryUsage(this, h);
1949     }
1950 
1951     void rustAnalyzerExpandMacro()
1952     {
1953         KTextEditor::View *activeView = m_mainWindow->activeView();
1954         auto server = m_serverManager->findServer(activeView);
1955         if (!server)
1956             return;
1957 
1958         auto position = activeView->cursorPosition();
1959         QPointer<KTextEditor::View> v(activeView);
1960         auto h = [this, v, position](const LSPExpandedMacro &reply) {
1961             if (v && !reply.expansion.isEmpty()) {
1962                 m_textHintprovider.showTextHint(reply.expansion, TextHintMarkupKind::PlainText, position);
1963             } else {
1964                 showMessage(i18n("No results"), KTextEditor::Message::Information);
1965             }
1966         };
1967         server->rustAnalyzerExpandMacro(this, activeView->document()->url(), position, h);
1968     }
1969 
1970     void gotoWorkSpaceSymbol()
1971     {
1972         KTextEditor::View *activeView = m_mainWindow->activeView();
1973         auto server = m_serverManager->findServer(activeView);
1974         if (!server) {
1975             return;
1976         }
1977         GotoSymbolHUDDialog dialog(m_mainWindow, server);
1978         dialog.openDialog();
1979     }
1980 
1981     static QStandardItem *getItem(const QStandardItemModel &model, const QUrl &url)
1982     {
1983         // local file in custom role, Qt::DisplayRole might have additional elements
1984         auto l = model.match(model.index(0, 0, QModelIndex()), Qt::UserRole, url.toLocalFile(), 1, Qt::MatchExactly);
1985         if (l.length()) {
1986             return model.itemFromIndex(l.at(0));
1987         }
1988         return nullptr;
1989     }
1990 
1991     static QStandardItem *getItem(const QStandardItem *topItem, KTextEditor::Cursor pos, bool onlyLine)
1992     {
1993         QStandardItem *targetItem = nullptr;
1994         if (topItem) {
1995             int count = topItem->rowCount();
1996             int first = 0, last = count;
1997             // let's not run wild on a linear search in a flood of diagnostics
1998             if (count > 50) {
1999                 // instead, let's *assume* sorted and use binary search to get closer
2000                 // it probably is sorted, so it should work out
2001                 // if not, at least we tried (without spending/wasting more on sorting)
2002                 auto getLine = [topItem, count](int index) {
2003                     Q_ASSERT(index >= 0);
2004                     Q_ASSERT(index < count);
2005                     auto range = topItem->child(index)->data(RangeData::RangeRole).value<LSPRange>();
2006                     return range.start().line();
2007                 };
2008                 int first = 0, last = count - 1;
2009                 int target = pos.line();
2010                 while (first + 1 < last) {
2011                     int middle = first + (last - first) / 2;
2012                     Q_ASSERT(first != middle);
2013                     Q_ASSERT(middle != last);
2014                     if (getLine(middle) < target) {
2015                         first = middle;
2016                     } else {
2017                         last = middle;
2018                     }
2019                 }
2020             }
2021             for (int i = first; i < last; ++i) {
2022                 auto item = topItem->child(i);
2023                 if (!(item->flags() & Qt::ItemIsEnabled)) {
2024                     continue;
2025                 }
2026                 auto range = item->data(RangeData::RangeRole).value<LSPRange>();
2027                 if ((onlyLine && pos.line() == range.start().line()) || (range.contains(pos))) {
2028                     targetItem = item;
2029                     break;
2030                 }
2031             }
2032         }
2033         return targetItem;
2034     }
2035 
2036     void onDiagnostics(const LSPPublishDiagnosticsParams &diagnostics)
2037     {
2038         if (m_diagnostics->isChecked()) {
2039             Q_EMIT m_diagnosticProvider.diagnosticsAdded(diagnostics);
2040         }
2041     }
2042 
2043     void onServerChanged()
2044     {
2045         m_diagnosticProvider.requestClearSuppressions(&m_diagnosticProvider);
2046         updateState();
2047     }
2048 
2049     void onTextHint(KTextEditor::View *view, const KTextEditor::Cursor &position)
2050     {
2051         // only trigger generic hover if no diagnostic to show;
2052         // avoids interference by generic hover info
2053         // (which is likely not so useful in this case/position anyway)
2054         if (m_autoHover && m_autoHover->isChecked()) {
2055             m_hover->showTextHint(view, position, false);
2056         }
2057     }
2058 
2059     KTextEditor::View *viewForUrl(const QUrl &url) const
2060     {
2061         const auto views = m_mainWindow->views();
2062         for (auto *view : views) {
2063             if (view->document()->url() == url) {
2064                 return view;
2065             }
2066         }
2067         return nullptr;
2068     }
2069 
2070     void addMessage(LSPMessageType level, const QString &category, const QString &msg, const QString &token = {})
2071     {
2072         // skip messaging if not enabled
2073         if (!m_messages->isChecked()) {
2074             return;
2075         }
2076 
2077         // use generic output view
2078         QVariantMap genericMessage;
2079         genericMessage.insert(QStringLiteral("category"), category);
2080         genericMessage.insert(QStringLiteral("text"), msg);
2081 
2082         // translate level to the type keys
2083         QString type;
2084         switch (level) {
2085         case LSPMessageType::Error:
2086             type = QStringLiteral("Error");
2087             break;
2088         case LSPMessageType::Warning:
2089             type = QStringLiteral("Warning");
2090             break;
2091         case LSPMessageType::Info:
2092             type = QStringLiteral("Info");
2093             break;
2094         case LSPMessageType::Log:
2095             type = QStringLiteral("Log");
2096             break;
2097         }
2098         genericMessage.insert(QStringLiteral("type"), type);
2099 
2100         if (!token.isEmpty()) {
2101             genericMessage.insert(QStringLiteral("token"), token);
2102         }
2103 
2104         // host application will handle these message for us, including auto-show settings
2105         Utils::showMessage(genericMessage, m_mainWindow);
2106     }
2107 
2108     // params type is same for show or log and is treated the same way
2109     void onMessage(LSPClientServer *server, const LSPLogMessageParams &params)
2110     {
2111         // determine server description
2112         auto message = params.message;
2113         if (server) {
2114             message = QStringLiteral("%1\n%2").arg(LSPClientServerManager::serverDescription(server), message);
2115         }
2116         addMessage(params.type, i18nc("@info", "LSP Server"), message);
2117     }
2118 
2119     void onWorkDoneProgress(LSPClientServer *server, const LSPWorkDoneProgressParams &params)
2120     {
2121         // provided token is/should be unique (from server perspective)
2122         // but we are dealing with multiple servers, so let's make a combined token
2123         const auto token = QStringLiteral("%1:%2").arg((quintptr)server).arg(params.token.toString());
2124         // find title in matching begin entry (if any)
2125         QString title;
2126         // plain search
2127         // could have used a map, but now we can discard the oldest one if needed
2128         int index = -1;
2129         for (int i = 0; i < m_workDoneProgress.size(); ++i) {
2130             auto &e = m_workDoneProgress.at(i);
2131             if (e.first == token) {
2132                 index = i;
2133                 title = e.second.value.title;
2134                 break;
2135             }
2136         }
2137         if (index < 0) {
2138             if (m_workDoneProgress.size() > 10) {
2139                 m_workDoneProgress.pop_front();
2140             }
2141             m_workDoneProgress.push_back({token, params});
2142         } else if (params.value.kind == LSPWorkDoneProgressKind::End) {
2143             m_workDoneProgress.remove(index);
2144         }
2145         // title (likely) only in initial message
2146         if (title.isEmpty()) {
2147             title = params.value.title;
2148         }
2149 
2150         const auto percent = params.value.percentage;
2151         QString msg = title;
2152         if (!percent.has_value()) {
2153             msg.append(QLatin1String(" ")).append(params.value.message);
2154         } else {
2155             msg.append(QStringLiteral(" [%1%] ").arg(percent.value(), 3));
2156             msg.append(params.value.message);
2157         }
2158 
2159         addMessage(LSPMessageType::Info, i18nc("@info", "LSP Server"), msg, token);
2160     }
2161 
2162     void onShowMessage(KTextEditor::Message::MessageType level, const QString &msg)
2163     {
2164         // translate level
2165         LSPMessageType lvl = LSPMessageType::Log;
2166         using KMessage = KTextEditor::Message;
2167         switch (level) {
2168         case KMessage::Error:
2169             lvl = LSPMessageType::Error;
2170             break;
2171         case KMessage::Warning:
2172             lvl = LSPMessageType::Warning;
2173             break;
2174         case KMessage::Information:
2175             lvl = LSPMessageType::Info;
2176             break;
2177         case KMessage::Positive:
2178             lvl = LSPMessageType::Log;
2179             break;
2180         }
2181 
2182         addMessage(lvl, i18nc("@info", "LSP Client"), msg);
2183     }
2184 
2185     void onTextChanged(KTextEditor::Document *doc)
2186     {
2187         KTextEditor::View *activeView = m_mainWindow->activeView();
2188         if (!activeView || activeView->document() != doc) {
2189             return;
2190         }
2191 
2192         if (m_plugin->m_semanticHighlighting) {
2193             m_semHighlightingManager.doSemanticHighlighting(activeView, true);
2194         }
2195 
2196         if (m_onTypeFormattingTriggers.empty()) {
2197             return;
2198         }
2199 
2200         // NOTE the intendation mode should probably be set to None,
2201         // so as not to experience unpleasant interference
2202         auto cursor = activeView->cursorPosition();
2203         QChar lastChar = cursor.column() == 0 ? QChar::fromLatin1('\n') : doc->characterAt({cursor.line(), cursor.column() - 1});
2204         if (m_onTypeFormattingTriggers.contains(lastChar)) {
2205             format(lastChar);
2206         }
2207     }
2208 
2209     void formatOnSave(KTextEditor::Document *doc, bool)
2210     {
2211         // only trigger for active doc
2212         auto activeView = m_mainWindow->activeView();
2213         if (activeView && activeView->document() == doc) {
2214             format({}, true);
2215         }
2216     }
2217 
2218     void updateState()
2219     {
2220         KTextEditor::View *activeView = m_mainWindow->activeView();
2221 
2222         auto doc = activeView ? activeView->document() : nullptr;
2223         auto server = m_serverManager->findServer(activeView);
2224         bool defEnabled = false, declEnabled = false, typeDefEnabled = false, refEnabled = false, implEnabled = false;
2225         bool hoverEnabled = false, highlightEnabled = false, codeActionEnabled = false;
2226         bool formatEnabled = false;
2227         bool renameEnabled = false;
2228         bool selectionRangeEnabled = false;
2229         bool isClangd = false;
2230         bool isRustAnalyzer = false;
2231         bool formatOnSave = false;
2232 
2233         if (server) {
2234             const auto &caps = server->capabilities();
2235             defEnabled = caps.definitionProvider;
2236             declEnabled = caps.declarationProvider;
2237             typeDefEnabled = caps.typeDefinitionProvider;
2238             refEnabled = caps.referencesProvider;
2239             implEnabled = caps.implementationProvider;
2240             hoverEnabled = caps.hoverProvider;
2241             highlightEnabled = caps.documentHighlightProvider;
2242             formatEnabled = caps.documentFormattingProvider || caps.documentRangeFormattingProvider;
2243             renameEnabled = caps.renameProvider;
2244             codeActionEnabled = caps.codeActionProvider;
2245             selectionRangeEnabled = caps.selectionRangeProvider;
2246             formatOnSave = formatEnabled && m_plugin->m_fmtOnSave;
2247 
2248             connect(server.get(), &LSPClientServer::publishDiagnostics, this, &self_type::onDiagnostics, Qt::UniqueConnection);
2249             connect(server.get(), &LSPClientServer::applyEdit, this, &self_type::onApplyEdit, Qt::UniqueConnection);
2250 
2251             // update format trigger characters
2252             const auto &fmt = caps.documentOnTypeFormattingProvider;
2253             if (fmt.provider && m_onTypeFormatting->isChecked()) {
2254                 m_onTypeFormattingTriggers = fmt.triggerCharacters;
2255             } else {
2256                 m_onTypeFormattingTriggers.clear();
2257             }
2258             // and monitor for such
2259             if (doc) {
2260                 connect(doc, &KTextEditor::Document::textChanged, this, &self_type::onTextChanged, Qt::UniqueConnection);
2261                 connect(doc, &KTextEditor::Document::reloaded, this, &self_type::updateState, Qt::UniqueConnection);
2262             }
2263             // only consider basename (full path may have been custom specified)
2264             auto lspServer = QFileInfo(server->cmdline().front()).fileName();
2265             isClangd = lspServer == QStringLiteral("clangd");
2266             isRustAnalyzer = lspServer == QStringLiteral("rust-analyzer");
2267 
2268             const bool semHighlightingEnabled = m_plugin->m_semanticHighlighting;
2269             if (semHighlightingEnabled) {
2270                 m_semHighlightingManager.doSemanticHighlighting(activeView, false);
2271             }
2272 
2273             connect(activeView, &KTextEditor::View::contextMenuAboutToShow, this, &self_type::prepareContextMenu, Qt::UniqueConnection);
2274 
2275             if (caps.inlayHintProvider && m_inlayHints->isChecked()) {
2276                 m_inlayHintsHandler.setActiveView(activeView);
2277             }
2278         }
2279 
2280         if (m_findDef) {
2281             m_findDef->setEnabled(defEnabled);
2282         }
2283         if (m_findDecl) {
2284             m_findDecl->setEnabled(declEnabled);
2285         }
2286         if (m_findTypeDef) {
2287             m_findTypeDef->setEnabled(typeDefEnabled);
2288         }
2289         if (m_findRef) {
2290             m_findRef->setEnabled(refEnabled);
2291         }
2292         if (m_findImpl) {
2293             m_findImpl->setEnabled(implEnabled);
2294         }
2295         if (m_triggerHighlight) {
2296             m_triggerHighlight->setEnabled(highlightEnabled);
2297         }
2298         if (m_triggerSymbolInfo) {
2299             m_triggerSymbolInfo->setEnabled(hoverEnabled);
2300         }
2301         if (m_triggerFormat) {
2302             m_triggerFormat->setEnabled(formatEnabled);
2303         }
2304         if (m_triggerRename) {
2305             m_triggerRename->setEnabled(renameEnabled);
2306         }
2307         if (m_complDocOn) {
2308             m_complDocOn->setEnabled(server != nullptr);
2309         }
2310         if (m_restartServer) {
2311             m_restartServer->setEnabled(server != nullptr);
2312         }
2313         if (m_requestCodeAction) {
2314             m_requestCodeAction->setEnabled(codeActionEnabled);
2315         }
2316         if (m_expandSelection) {
2317             m_expandSelection->setEnabled(selectionRangeEnabled);
2318         }
2319         if (m_shrinkSelection) {
2320             m_shrinkSelection->setEnabled(selectionRangeEnabled);
2321         }
2322         m_switchSourceHeader->setEnabled(isClangd);
2323         m_switchSourceHeader->setVisible(isClangd);
2324         m_memoryUsage->setEnabled(isClangd);
2325         m_memoryUsage->setVisible(isClangd);
2326         m_expandMacro->setEnabled(isRustAnalyzer);
2327         m_expandMacro->setVisible(isRustAnalyzer);
2328 
2329         // update completion with relevant server
2330         m_completion->setServer(server);
2331         if (m_complDocOn) {
2332             m_completion->setSelectedDocumentation(m_complDocOn->isChecked());
2333         }
2334         if (m_signatureHelp) {
2335             m_completion->setSignatureHelp(m_signatureHelp->isChecked());
2336         }
2337         if (m_complParens) {
2338             m_completion->setCompleteParens(m_complParens->isChecked());
2339         }
2340         updateCompletion(activeView, server.get());
2341 
2342         // update hover with relevant server
2343         m_hover->setServer(server && server->capabilities().hoverProvider ? server : nullptr);
2344 
2345         updateMarks(doc);
2346 
2347         if (formatOnSave) {
2348             auto t = Qt::ConnectionType(Qt::UniqueConnection | Qt::QueuedConnection);
2349             connect(activeView->document(), &KTextEditor::Document::documentSavedOrUploaded, this, &self_type::formatOnSave, t);
2350         }
2351 
2352         // connect for cleanup stuff
2353         if (activeView) {
2354             connect(activeView, &KTextEditor::View::destroyed, this, &self_type::viewDestroyed, Qt::UniqueConnection);
2355         }
2356     }
2357 
2358     void prepareContextMenu(KTextEditor::View *view, QMenu *menu)
2359     {
2360         Q_UNUSED(view);
2361 
2362         // make sure the parent is set
2363         for (auto *act : m_contextMenuActions) {
2364             act->setParent(menu);
2365         }
2366 
2367         QAction *insertBefore = nullptr;
2368         // the name is used in KXMLGUI as object name
2369         // we want to insert before the cut action to ensure the KTE spelling menu is still on top
2370         const auto cutName = KStandardAction::name(KStandardAction::StandardAction::Cut);
2371 
2372         for (auto *act : menu->actions()) {
2373             if (act->objectName() == cutName) {
2374                 insertBefore = act;
2375                 break;
2376             }
2377         }
2378 
2379         if (!insertBefore) {
2380             Q_ASSERT(!menu->actions().isEmpty());
2381             insertBefore = menu->actions().first();
2382         }
2383 
2384         // insert lsp actions at the beginning of the menu
2385         menu->insertActions(insertBefore, m_contextMenuActions);
2386 
2387         connect(menu, &QMenu::aboutToHide, this, &self_type::cleanUpContextMenu, Qt::UniqueConnection);
2388     }
2389 
2390     void cleanUpContextMenu()
2391     {
2392         // We need to remove our list or they will accumulated on next show event
2393         for (auto *act : m_contextMenuActions) {
2394             qobject_cast<QWidget *>(act->parent())->removeAction(act);
2395         }
2396     }
2397 
2398     void updateMarks(KTextEditor::Document *doc = nullptr)
2399     {
2400         if (!doc) {
2401             KTextEditor::View *activeView = m_mainWindow->activeView();
2402             doc = activeView ? activeView->document() : nullptr;
2403         }
2404 
2405         // update marks if applicable
2406         if (m_markModel && doc) {
2407             addMarks(doc, m_markModel, m_ranges, m_marks);
2408         }
2409     }
2410 
2411     void viewDestroyed(QObject *view)
2412     {
2413         m_completionViews.removeAll(view);
2414     }
2415 
2416     void updateCompletion(KTextEditor::View *view, LSPClientServer *server)
2417     {
2418         if (!view) {
2419             return;
2420         }
2421 
2422         bool registered = m_completionViews.contains(view);
2423 
2424         if (!registered && server && server->capabilities().completionProvider.provider) {
2425             qCInfo(LSPCLIENT) << "registering cci";
2426             view->registerCompletionModel(m_completion.get());
2427             m_completionViews.append(view);
2428         }
2429 
2430         if (registered && !server) {
2431             qCInfo(LSPCLIENT) << "unregistering cci";
2432             view->unregisterCompletionModel(m_completion.get());
2433             m_completionViews.removeAll(view);
2434         }
2435     }
2436 
2437     Q_INVOKABLE QAbstractItemModel *documentSymbolsModel()
2438     {
2439         return m_symbolView->documentSymbolsModel();
2440     }
2441 
2442     void showMessageRequest(const LSPShowMessageParams &message,
2443                             const QList<LSPMessageRequestAction> &actions,
2444                             const std::function<void()> chooseNothing,
2445                             bool &handled)
2446     {
2447         if (handled) {
2448             return;
2449         }
2450 
2451         handled = true;
2452 
2453         CloseAllowedMessageBox box(m_mainWindow->window());
2454         box.setWindowTitle(i18n("Question from LSP server"));
2455         box.setText(message.message);
2456         switch (message.type) {
2457         case (LSPMessageType::Error):
2458             box.setIcon(QMessageBox::Critical);
2459             break;
2460         case (LSPMessageType::Warning):
2461             box.setIcon(QMessageBox::Warning);
2462             break;
2463         case (LSPMessageType::Info):
2464             box.setIcon(QMessageBox::Question);
2465             break;
2466         case (LSPMessageType::Log):
2467             box.setIcon(QMessageBox::Question);
2468             break;
2469         }
2470 
2471         std::map<QAbstractButton *, std::function<void()>> onClick;
2472         for (auto &action : actions) {
2473             QString escaped = action.title;
2474             escaped.replace(QLatin1Char('&'), QLatin1String("&&"));
2475             QAbstractButton *button = box.addButton(escaped, QMessageBox::AcceptRole);
2476             onClick[button] = action.choose;
2477         }
2478         box.exec();
2479         if (actions.empty() || box.clickedButton() == nullptr) {
2480             chooseNothing();
2481         } else {
2482             onClick[box.clickedButton()]();
2483         }
2484     }
2485 };
2486 
2487 QObject *LSPClientPluginView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, std::shared_ptr<LSPClientServerManager> manager)
2488 {
2489     return new LSPClientPluginViewImpl(plugin, mainWin, manager);
2490 }
2491 
2492 #include "lspclientpluginview.moc"