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 ¤t, 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 ¶ms) 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 ¶ms) 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"