diff --git a/addons/lspclient/lspclientcompletion.cpp b/addons/lspclient/lspclientcompletion.cpp index 08577d7cd..b62fcd2b2 100644 --- a/addons/lspclient/lspclientcompletion.cpp +++ b/addons/lspclient/lspclientcompletion.cpp @@ -1,337 +1,337 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "lspclientcompletion.h" #include "lspclientplugin.h" #include "lspclient_debug.h" #include #include #include #include #include #include #include #define RETURN_CACHED_ICON(name) \ { \ static QIcon icon(QIcon::fromTheme(QStringLiteral(name))); \ return icon; \ } static QIcon kind_icon(LSPCompletionItemKind kind) { switch (kind) { case LSPCompletionItemKind::Method: case LSPCompletionItemKind::Function: case LSPCompletionItemKind::Constructor: RETURN_CACHED_ICON("code-function") case LSPCompletionItemKind::Variable: RETURN_CACHED_ICON("code-variable") case LSPCompletionItemKind::Class: case LSPCompletionItemKind::Interface: case LSPCompletionItemKind::Struct: RETURN_CACHED_ICON("code-class"); case LSPCompletionItemKind::Module: RETURN_CACHED_ICON("code-block"); case LSPCompletionItemKind::Field: case LSPCompletionItemKind::Property: // align with symbolview RETURN_CACHED_ICON("code-variable"); case LSPCompletionItemKind::Enum: case LSPCompletionItemKind::EnumMember: RETURN_CACHED_ICON("enum"); default: break; } return QIcon(); } static KTextEditor::CodeCompletionModel::CompletionProperty kind_property(LSPCompletionItemKind kind) { using CompletionProperty = KTextEditor::CodeCompletionModel::CompletionProperty; auto p = CompletionProperty::NoProperty; switch (kind) { case LSPCompletionItemKind::Method: case LSPCompletionItemKind::Function: case LSPCompletionItemKind::Constructor: p = CompletionProperty::Function; break; case LSPCompletionItemKind::Variable: p = CompletionProperty::Variable; break; case LSPCompletionItemKind::Class: case LSPCompletionItemKind::Interface: p = CompletionProperty::Class; break; case LSPCompletionItemKind::Struct: p = CompletionProperty::Class; break; case LSPCompletionItemKind::Module: p = CompletionProperty::Namespace; break; case LSPCompletionItemKind::Enum: case LSPCompletionItemKind::EnumMember: p = CompletionProperty::Enum; break; default: break; } return p; } struct LSPClientCompletionItem : public LSPCompletionItem { int argumentHintDepth = 0; QString prefix; QString postfix; LSPClientCompletionItem(const LSPCompletionItem &item) : LSPCompletionItem(item) { // transform for later display // sigh, remove (leading) whitespace (looking at clangd here) // could skip the [] if empty detail, but it is a handy watermark anyway ;-) label = QString(label.simplified() + QStringLiteral(" [") + detail.simplified() + QStringLiteral("]")); } LSPClientCompletionItem(const LSPSignatureInformation &sig, int activeParameter, const QString &_sortText) { argumentHintDepth = 1; documentation = sig.documentation; label = sig.label; sortText = _sortText; // transform into prefix, name, suffix if active if (activeParameter >= 0 && activeParameter < sig.parameters.length()) { const auto ¶m = sig.parameters.at(activeParameter); if (param.start >= 0 && param.start < label.length() && param.end >= 0 && param.end < label.length() && param.start < param.end) { prefix = label.mid(0, param.start); postfix = label.mid(param.end); label = label.mid(param.start, param.end - param.start); } } } }; static bool compare_match(const LSPCompletionItem &a, const LSPCompletionItem& b) { return a.sortText < b.sortText; } class LSPClientCompletionImpl : public LSPClientCompletion { Q_OBJECT typedef LSPClientCompletionImpl self_type; QSharedPointer m_manager; QSharedPointer m_server; bool m_selectedDocumentation = false; QVector m_triggersCompletion; QVector m_triggersSignature; bool m_triggerSignature = false; QList m_matches; LSPClientServer::RequestHandle m_handle, m_handleSig; public: LSPClientCompletionImpl(QSharedPointer manager) : LSPClientCompletion(nullptr), m_manager(std::move(manager)), m_server(nullptr) { } void setServer(QSharedPointer server) override { m_server = server; if (m_server) { const auto &caps = m_server->capabilities(); m_triggersCompletion = caps.completionProvider.triggerCharacters; m_triggersSignature = caps.signatureHelpProvider.triggerCharacters; } else { m_triggersCompletion.clear(); m_triggersSignature.clear(); } } - virtual void setSelectedDocumentation(bool s) override { m_selectedDocumentation = s; } + void setSelectedDocumentation(bool s) override { m_selectedDocumentation = s; } QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid() || index.row() >= m_matches.size()) { return QVariant(); } const auto &match = m_matches.at(index.row()); if (role == Qt::DisplayRole) { if (index.column() == KTextEditor::CodeCompletionModel::Name) { return match.label; } else if (index.column() == KTextEditor::CodeCompletionModel::Prefix) { return match.prefix; } else if (index.column() == KTextEditor::CodeCompletionModel::Postfix) { return match.postfix; } } else if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) { return kind_icon(match.kind); } else if (role == KTextEditor::CodeCompletionModel::CompletionRole) { return kind_property(match.kind); } else if (role == KTextEditor::CodeCompletionModel::ArgumentHintDepth) { return match.argumentHintDepth; } else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) { // (ab)use depth to indicate sort order return index.row(); } else if (role == KTextEditor::CodeCompletionModel::IsExpandable) { return !match.documentation.value.isEmpty(); } else if (role == KTextEditor::CodeCompletionModel::ExpandingWidget && !match.documentation.value.isEmpty()) { // probably plaintext, but let's show markdown as-is for now // FIXME better presentation of markdown return match.documentation.value; } else if (role == KTextEditor::CodeCompletionModel::ItemSelected && !match.argumentHintDepth && !match.documentation.value.isEmpty() && m_selectedDocumentation) { return match.documentation.value; } return QVariant(); } bool shouldStartCompletion(KTextEditor::View *view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position) override { qCInfo(LSPCLIENT) << "should start " << userInsertion << insertedText; if (!userInsertion || !m_server || insertedText.isEmpty()) { return false; } // covers most already ... bool complete = CodeCompletionModelControllerInterface::shouldStartCompletion( view, insertedText, userInsertion, position); QChar lastChar = insertedText.at(insertedText.count() - 1); m_triggerSignature = false; complete = complete || m_triggersCompletion.contains(lastChar); if (m_triggersSignature.contains(lastChar)) { complete = true; m_triggerSignature = true; } return complete; } void completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it) override { Q_UNUSED(it) qCInfo(LSPCLIENT) << "completion invoked" << m_server; // maybe use WaitForReset ?? // but more complex and already looks good anyway auto handler = [this](const QList & compl) { beginResetModel(); qCInfo(LSPCLIENT) << "adding completions " << compl.size(); for (const auto &item : compl) m_matches.push_back(item); std::stable_sort(m_matches.begin(), m_matches.end(), compare_match); setRowCount(m_matches.size()); endResetModel(); }; auto sigHandler = [this](const LSPSignatureHelp &sig) { beginResetModel(); qCInfo(LSPCLIENT) << "adding signatures " << sig.signatures.size(); int index = 0; for (const auto &item : sig.signatures) { int sortIndex = 10 + index; int active = -1; if (index == sig.activeSignature) { sortIndex = 0; active = sig.activeParameter; } // trick active first, others after that m_matches.push_back( { item, active, QString(QStringLiteral("%1").arg(sortIndex, 3, 10)) }); ++index; } std::stable_sort(m_matches.begin(), m_matches.end(), compare_match); setRowCount(m_matches.size()); endResetModel(); }; beginResetModel(); m_matches.clear(); auto document = view->document(); if (m_server && document) { // the default range is determined based on a reasonable identifier (word) // which is generally fine and nice, but let's pass actual cursor position // (which may be within this typical range) auto position = view->cursorPosition(); auto cursor = qMax(range.start(), qMin(range.end(), position)); m_manager->update(document, false); if (!m_triggerSignature) { m_handle = m_server->documentCompletion( document->url(), { cursor.line(), cursor.column() }, this, handler); } m_handleSig = m_server->signatureHelp( document->url(), { cursor.line(), cursor.column() }, this, sigHandler); } setRowCount(m_matches.size()); endResetModel(); } void executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const override { if (index.row() < m_matches.size()) view->document()->replaceText(word, m_matches.at(index.row()).insertText); } void aborted(KTextEditor::View *view) override { Q_UNUSED(view); beginResetModel(); m_matches.clear(); m_handle.cancel(); m_handleSig.cancel(); m_triggerSignature = false; endResetModel(); } }; LSPClientCompletion *LSPClientCompletion::new_(QSharedPointer manager) { return new LSPClientCompletionImpl(std::move(manager)); } #include "lspclientcompletion.moc" diff --git a/addons/lspclient/lspclientpluginview.cpp b/addons/lspclient/lspclientpluginview.cpp index 620c9959e..9e833cabb 100644 --- a/addons/lspclient/lspclientpluginview.cpp +++ b/addons/lspclient/lspclientpluginview.cpp @@ -1,1707 +1,1707 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "lspclientpluginview.h" #include "lspclientsymbolview.h" #include "lspclientplugin.h" #include "lspclientservermanager.h" #include "lspclientcompletion.h" #include "lspclienthover.h" #include "lspclient_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace RangeData { enum { // preserve UserRole for generic use where needed FileUrlRole = Qt::UserRole + 1, RangeRole, KindRole, }; class KindEnum { public: enum _kind { Text = (int)LSPDocumentHighlightKind::Text, Read = (int)LSPDocumentHighlightKind::Read, Write = (int)LSPDocumentHighlightKind::Write, Error = 10 + (int)LSPDiagnosticSeverity::Error, Warning = 10 + (int)LSPDiagnosticSeverity::Warning, Information = 10 + (int)LSPDiagnosticSeverity::Information, Hint = 10 + (int)LSPDiagnosticSeverity::Hint, Related }; KindEnum(int v) { m_value = (_kind)v; } KindEnum(LSPDocumentHighlightKind hl) : KindEnum((_kind)(hl)) {} KindEnum(LSPDiagnosticSeverity sev) : KindEnum(_kind(10 + (int)sev)) {} operator _kind() { return m_value; } private: _kind m_value; }; static constexpr KTextEditor::MarkInterface::MarkTypes markType = KTextEditor::MarkInterface::markType31; static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagError = KTextEditor::MarkInterface::Error; static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagWarning = KTextEditor::MarkInterface::Warning; static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagOther = KTextEditor::MarkInterface::markType30; static constexpr KTextEditor::MarkInterface::MarkTypes markTypeDiagAll = KTextEditor::MarkInterface::MarkTypes(markTypeDiagError | markTypeDiagWarning | markTypeDiagOther); } static QIcon diagnosticsIcon(LSPDiagnosticSeverity severity) { #define RETURN_CACHED_ICON(name) \ { \ static QIcon icon(QIcon::fromTheme(QStringLiteral(name))); \ return icon; \ } switch (severity) { case LSPDiagnosticSeverity::Error: RETURN_CACHED_ICON("dialog-error") case LSPDiagnosticSeverity::Warning: RETURN_CACHED_ICON("dialog-warning") case LSPDiagnosticSeverity::Information: case LSPDiagnosticSeverity::Hint: RETURN_CACHED_ICON("dialog-information") default: break; } return QIcon(); } static QIcon codeActionIcon() { static QIcon icon(QIcon::fromTheme(QStringLiteral("insert-text"))); return icon; } KTextEditor::Document *findDocument(KTextEditor::MainWindow *mainWindow, const QUrl &url) { auto views = mainWindow->views(); for (const auto v : views) { auto doc = v->document(); if (doc && doc->url() == url) return doc; } return nullptr; } // helper to read lines from unopened documents // lightweight and does not require additional symbols class FileLineReader { QFile file; int lastLineNo = -1; QString lastLine; public: FileLineReader(const QUrl &url) : file(url.path()) { file.open(QIODevice::ReadOnly); } // called with non-descending lineno QString line(int lineno) { if (lineno == lastLineNo) { return lastLine; } while (file.isOpen() && !file.atEnd()) { auto line = file.readLine(); if (++lastLineNo == lineno) { QTextCodec::ConverterState state; QTextCodec *codec = QTextCodec::codecForName("UTF-8"); QString text = codec->toUnicode(line.constData(), line.size(), &state); if (state.invalidChars > 0) { text = QString::fromLatin1(line); } while (text.size() && text.at(text.size() - 1).isSpace()) text.chop(1); lastLine = text; return text; } } return QString(); } }; class LSPClientActionView : public QObject { Q_OBJECT typedef LSPClientActionView self_type; LSPClientPlugin *m_plugin; KTextEditor::MainWindow *m_mainWindow; KXMLGUIClient *m_client; QSharedPointer m_serverManager; QScopedPointer m_viewTracker; QScopedPointer m_completion; QScopedPointer m_hover; QScopedPointer m_symbolView; QPointer m_findDef; QPointer m_findDecl; QPointer m_findRef; QPointer m_triggerHighlight; QPointer m_triggerHover; QPointer m_triggerFormat; QPointer m_triggerRename; QPointer m_complDocOn; QPointer m_refDeclaration; QPointer m_autoHover; QPointer m_onTypeFormatting; QPointer m_incrementalSync; QPointer m_diagnostics; QPointer m_diagnosticsHighlight; QPointer m_diagnosticsMark; QPointer m_diagnosticsSwitch; QPointer m_diagnosticsCloseNon; QPointer m_restartServer; QPointer m_restartAll; // toolview QScopedPointer m_toolView; QPointer m_tabWidget; // applied search ranges typedef QMultiHash RangeCollection; RangeCollection m_ranges; // applied marks typedef QSet DocumentCollection; DocumentCollection m_marks; // modelis either owned by tree added to tabwidget or owned here QScopedPointer m_ownedModel; // in either case, the model that directs applying marks/ranges QPointer m_markModel; // goto definition and declaration jump list is more a menu than a // search result, so let's not keep adding new tabs for those // previous tree for definition result QPointer m_defTree; // ... and for declaration QPointer m_declTree; // diagnostics tab QPointer m_diagnosticsTree; // tree widget is either owned here or by tab QScopedPointer m_diagnosticsTreeOwn; QScopedPointer m_diagnosticsModel; // diagnostics ranges RangeCollection m_diagnosticsRanges; // and marks DocumentCollection m_diagnosticsMarks; // views on which completions have been registered QSet m_completionViews; // views on which hovers have been registered QSet m_hoverViews; // outstanding request LSPClientServer::RequestHandle m_handle; // timeout on request bool m_req_timeout = false; // accept incoming applyEdit bool m_accept_edit = false; // characters to trigger format request QVector m_onTypeFormattingTriggers; KActionCollection *actionCollection() const { return m_client->actionCollection(); } public: LSPClientActionView(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin, KXMLGUIClient *client, QSharedPointer serverManager) : QObject(mainWin), m_plugin(plugin), m_mainWindow(mainWin), m_client(client), m_serverManager(std::move(serverManager)), m_completion(LSPClientCompletion::new_(m_serverManager)), m_hover(LSPClientHover::new_(m_serverManager)), m_symbolView(LSPClientSymbolView::new_(plugin, mainWin, m_serverManager)) { connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &self_type::updateState); connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &self_type::handleEsc); connect(m_serverManager.data(), &LSPClientServerManager::serverChanged, this, &self_type::updateState); m_findDef = actionCollection()->addAction(QStringLiteral("lspclient_find_definition"), this, &self_type::goToDefinition); m_findDef->setText(i18n("Go to Definition")); m_findDecl = actionCollection()->addAction(QStringLiteral("lspclient_find_declaration"), this, &self_type::goToDeclaration); m_findDecl->setText(i18n("Go to Declaration")); m_findRef = actionCollection()->addAction(QStringLiteral("lspclient_find_references"), this, &self_type::findReferences); m_findRef->setText(i18n("Find References")); m_triggerHighlight = actionCollection()->addAction(QStringLiteral("lspclient_highlight"), this, &self_type::highlight); m_triggerHighlight->setText(i18n("Highlight")); // perhaps hover suggests to do so on mouse-over, // but let's just use a (convenient) action/shortcut for it m_triggerHover = actionCollection()->addAction(QStringLiteral("lspclient_hover"), this, &self_type::hover); m_triggerHover->setText(i18n("Hover")); m_triggerFormat = actionCollection()->addAction(QStringLiteral("lspclient_format"), this, &self_type::format); m_triggerFormat->setText(i18n("Format")); m_triggerRename = actionCollection()->addAction(QStringLiteral("lspclient_rename"), this, &self_type::rename); m_triggerRename->setText(i18n("Rename")); // general options m_complDocOn = actionCollection()->addAction(QStringLiteral("lspclient_completion_doc"), this, &self_type::displayOptionChanged); m_complDocOn->setText(i18n("Show selected completion documentation")); m_complDocOn->setCheckable(true); m_refDeclaration = actionCollection()->addAction(QStringLiteral("lspclient_references_declaration"), this, &self_type::displayOptionChanged); m_refDeclaration->setText(i18n("Include declaration in references")); m_refDeclaration->setCheckable(true); m_autoHover = actionCollection()->addAction(QStringLiteral("lspclient_auto_hover"), this, &self_type::displayOptionChanged); m_autoHover->setText(i18n("Show hover information")); m_autoHover->setCheckable(true); m_onTypeFormatting = actionCollection()->addAction(QStringLiteral("lspclient_type_formatting"), this, &self_type::displayOptionChanged); m_onTypeFormatting->setText(i18n("Format on typing")); m_onTypeFormatting->setCheckable(true); m_incrementalSync = actionCollection()->addAction(QStringLiteral("lspclient_incremental_sync"), this, &self_type::displayOptionChanged); m_incrementalSync->setText(i18n("Incremental document synchronization")); m_incrementalSync->setCheckable(true); // diagnostics m_diagnostics = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics"), this, &self_type::displayOptionChanged); m_diagnostics->setText(i18n("Show diagnostics notifications")); m_diagnostics->setCheckable(true); m_diagnosticsHighlight = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_highlight"), this, &self_type::displayOptionChanged); m_diagnosticsHighlight->setText(i18n("Show diagnostics highlights")); m_diagnosticsHighlight->setCheckable(true); m_diagnosticsMark = actionCollection()->addAction(QStringLiteral("lspclient_diagnostics_mark"), this, &self_type::displayOptionChanged); m_diagnosticsMark->setText(i18n("Show diagnostics marks")); m_diagnosticsMark->setCheckable(true); m_diagnosticsSwitch = actionCollection()->addAction(QStringLiteral("lspclient_diagnostic_switch"), this, &self_type::switchToDiagnostics); m_diagnosticsSwitch->setText(i18n("Switch to diagnostics tab")); m_diagnosticsCloseNon = actionCollection()->addAction(QStringLiteral("lspclient_diagnostic_close_non"), this, &self_type::closeNonDiagnostics); m_diagnosticsCloseNon->setText(i18n("Close all non-diagnostics tabs")); // server control m_restartServer = actionCollection()->addAction(QStringLiteral("lspclient_restart_server"), this, &self_type::restartCurrent); m_restartServer->setText(i18n("Restart LSP Server")); m_restartAll = actionCollection()->addAction(QStringLiteral("lspclient_restart_all"), this, &self_type::restartAll); m_restartAll->setText(i18n("Restart All LSP Servers")); // popup menu auto menu = new KActionMenu(i18n("LSP Client"), this); actionCollection()->addAction(QStringLiteral("popup_lspclient"), menu); menu->addAction(m_findDef); menu->addAction(m_findDecl); menu->addAction(m_findRef); menu->addAction(m_triggerHighlight); menu->addAction(m_triggerHover); menu->addAction(m_triggerFormat); menu->addAction(m_triggerRename); menu->addSeparator(); menu->addAction(m_complDocOn); menu->addAction(m_refDeclaration); menu->addAction(m_autoHover); menu->addAction(m_onTypeFormatting); menu->addAction(m_incrementalSync); menu->addSeparator(); menu->addAction(m_diagnostics); menu->addAction(m_diagnosticsHighlight); menu->addAction(m_diagnosticsMark); menu->addAction(m_diagnosticsSwitch); menu->addAction(m_diagnosticsCloseNon); menu->addSeparator(); menu->addAction(m_restartServer); menu->addAction(m_restartAll); // sync with plugin settings if updated connect(m_plugin, &LSPClientPlugin::update, this, &self_type::configUpdated); // toolview m_toolView.reset(mainWin->createToolView( plugin, QStringLiteral("kate_lspclient"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("application-x-ms-dos-executable")), i18n("LSP Client"))); m_tabWidget = new QTabWidget(m_toolView.data()); m_toolView->layout()->addWidget(m_tabWidget); m_tabWidget->setFocusPolicy(Qt::NoFocus); m_tabWidget->setTabsClosable(true); KAcceleratorManager::setNoAccel(m_tabWidget); connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &self_type::tabCloseRequested); // diagnostics tab m_diagnosticsTree = new QTreeView(); configureTreeView(m_diagnosticsTree); m_diagnosticsTree->setAlternatingRowColors(true); m_diagnosticsTreeOwn.reset(m_diagnosticsTree); m_diagnosticsModel.reset(new QStandardItemModel()); m_diagnosticsModel->setColumnCount(2); m_diagnosticsTree->setModel(m_diagnosticsModel.data()); connect(m_diagnosticsTree, &QTreeView::clicked, this, &self_type::goToItemLocation); connect(m_diagnosticsTree, &QTreeView::doubleClicked, this, &self_type::triggerCodeAction); // track position in view to sync diagnostics list m_viewTracker.reset(LSPClientViewTracker::new_(plugin, mainWin, 0, 500)); connect(m_viewTracker.data(), &LSPClientViewTracker::newState, this, &self_type::onViewState); configUpdated(); updateState(); } - ~LSPClientActionView() + ~LSPClientActionView() override { // unregister all code-completion providers, else we might crash for (auto view : qAsConst(m_completionViews)) { qobject_cast(view)->unregisterCompletionModel( m_completion.data()); } // unregister all text-hint providers, else we might crash for (auto view : qAsConst(m_hoverViews)) { qobject_cast(view)->unregisterTextHintProvider( m_hover.data()); } clearAllLocationMarks(); clearAllDiagnosticsMarks(); } void configureTreeView(QTreeView *treeView) { treeView->setHeaderHidden(true); treeView->setFocusPolicy(Qt::NoFocus); treeView->setLayoutDirection(Qt::LeftToRight); treeView->setSortingEnabled(false); treeView->setEditTriggers(QAbstractItemView::NoEditTriggers); } void displayOptionChanged() { m_diagnosticsHighlight->setEnabled(m_diagnostics->isChecked()); m_diagnosticsMark->setEnabled(m_diagnostics->isChecked()); auto index = m_tabWidget->indexOf(m_diagnosticsTree); // setTabEnabled may still show it ... so let's be more forceful if (m_diagnostics->isChecked() && m_diagnosticsTreeOwn) { m_diagnosticsTreeOwn.take(); m_tabWidget->insertTab(0, m_diagnosticsTree, i18nc("@title:tab", "Diagnostics")); } else if (!m_diagnostics->isChecked() && !m_diagnosticsTreeOwn) { m_diagnosticsTreeOwn.reset(m_diagnosticsTree); m_tabWidget->removeTab(index); } m_diagnosticsSwitch->setEnabled(m_diagnostics->isChecked()); m_serverManager->setIncrementalSync(m_incrementalSync->isChecked()); updateState(); } void configUpdated() { if (m_complDocOn) m_complDocOn->setChecked(m_plugin->m_complDoc); if (m_refDeclaration) m_refDeclaration->setChecked(m_plugin->m_refDeclaration); if (m_autoHover) m_autoHover->setChecked(m_plugin->m_autoHover); if (m_onTypeFormatting) m_onTypeFormatting->setChecked(m_plugin->m_onTypeFormatting); if (m_incrementalSync) m_incrementalSync->setChecked(m_plugin->m_incrementalSync); if (m_diagnostics) m_diagnostics->setChecked(m_plugin->m_diagnostics); if (m_diagnosticsHighlight) m_diagnosticsHighlight->setChecked(m_plugin->m_diagnosticsHighlight); if (m_diagnosticsMark) m_diagnosticsMark->setChecked(m_plugin->m_diagnosticsMark); displayOptionChanged(); } void restartCurrent() { KTextEditor::View *activeView = m_mainWindow->activeView(); auto server = m_serverManager->findServer(activeView); if (server) m_serverManager->restart(server.data()); } void restartAll() { m_serverManager->restart(nullptr); } static void clearMarks(KTextEditor::Document *doc, RangeCollection &ranges, DocumentCollection &docs, uint markType) { KTextEditor::MarkInterface *iface = docs.contains(doc) ? qobject_cast(doc) : nullptr; if (iface) { const QHash marks = iface->marks(); QHashIterator i(marks); while (i.hasNext()) { i.next(); if (i.value()->type & markType) { iface->removeMark(i.value()->line, markType); } } docs.remove(doc); } for (auto it = ranges.find(doc); it != ranges.end() && it.key() == doc;) { delete it.value(); it = ranges.erase(it); } } static void clearMarks(RangeCollection &ranges, DocumentCollection &docs, uint markType) { while (!ranges.empty()) { clearMarks(ranges.begin().key(), ranges, docs, markType); } } Q_SLOT void clearAllMarks(KTextEditor::Document *doc) { clearMarks(doc, m_ranges, m_marks, RangeData::markType); clearMarks(doc, m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll); } void clearAllLocationMarks() { clearMarks(m_ranges, m_marks, RangeData::markType); // no longer add any again m_ownedModel.reset(); m_markModel.clear(); } void clearAllDiagnosticsMarks() { clearMarks(m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll); } void addMarks(KTextEditor::Document *doc, QStandardItem *item, RangeCollection *ranges, DocumentCollection *docs) { Q_ASSERT(item); KTextEditor::MovingInterface *miface = qobject_cast(doc); KTextEditor::MarkInterface *iface = qobject_cast(doc); KTextEditor::View *activeView = m_mainWindow->activeView(); KTextEditor::ConfigInterface *ciface = qobject_cast(activeView); if (!miface || !iface) return; auto url = item->data(RangeData::FileUrlRole).toUrl(); if (url != doc->url()) return; KTextEditor::Range range = item->data(RangeData::RangeRole).value(); auto line = range.start().line(); RangeData::KindEnum kind = (RangeData::KindEnum)item->data(RangeData::KindRole).toInt(); KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); bool enabled = m_diagnostics && m_diagnostics->isChecked() && m_diagnosticsHighlight && m_diagnosticsHighlight->isChecked(); KTextEditor::MarkInterface::MarkTypes markType = RangeData::markType; switch (kind) { case RangeData::KindEnum::Text: { // well, it's a bit like searching for something, so re-use that color QColor rangeColor = Qt::yellow; if (ciface) { rangeColor = ciface->configValue(QStringLiteral("search-highlight-color")) .value(); } attr->setBackground(rangeColor); enabled = true; break; } // FIXME are there any symbolic/configurable ways to pick these colors? case RangeData::KindEnum::Read: attr->setBackground(Qt::green); enabled = true; break; case RangeData::KindEnum::Write: attr->setBackground(Qt::red); enabled = true; break; // use underlining for diagnostics to avoid lots of fancy flickering case RangeData::KindEnum::Error: markType = RangeData::markTypeDiagError; attr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); attr->setUnderlineColor(Qt::red); break; case RangeData::KindEnum::Warning: markType = RangeData::markTypeDiagWarning; attr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline); attr->setUnderlineColor(QColor(255, 128, 0)); break; case RangeData::KindEnum::Information: case RangeData::KindEnum::Hint: case RangeData::KindEnum::Related: markType = RangeData::markTypeDiagOther; attr->setUnderlineStyle(QTextCharFormat::DashUnderline); attr->setUnderlineColor(Qt::blue); break; } if (activeView) { attr->setForeground( activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color()); } // highlight the range if (enabled && ranges) { KTextEditor::MovingRange *mr = miface->newMovingRange(range); mr->setAttribute(attr); mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection mr->setAttributeOnlyForViews(true); ranges->insert(doc, mr); } // add match mark for range const int ps = 32; bool handleClick = true; enabled = m_diagnostics && m_diagnostics->isChecked() && m_diagnosticsMark && m_diagnosticsMark->isChecked(); switch (markType) { case RangeData::markType: iface->setMarkDescription(markType, i18n("RangeHighLight")); iface->setMarkPixmap(markType, QIcon().pixmap(0, 0)); handleClick = false; enabled = true; break; case RangeData::markTypeDiagError: iface->setMarkDescription(markType, i18n("Error")); iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Error).pixmap(ps, ps)); break; case RangeData::markTypeDiagWarning: iface->setMarkDescription(markType, i18n("Warning")); iface->setMarkPixmap(markType, diagnosticsIcon(LSPDiagnosticSeverity::Warning).pixmap(ps, ps)); break; case RangeData::markTypeDiagOther: iface->setMarkDescription(markType, i18n("Information")); iface->setMarkPixmap( markType, diagnosticsIcon(LSPDiagnosticSeverity::Information).pixmap(ps, ps)); break; default: Q_ASSERT(false); break; } if (enabled && docs) { iface->addMark(line, markType); docs->insert(doc); } // ensure runtime match connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearAllMarks(KTextEditor::Document *)), Qt::UniqueConnection); connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearAllMarks(KTextEditor::Document *)), Qt::UniqueConnection); if (handleClick) { connect(doc, SIGNAL(markClicked(KTextEditor::Document *, KTextEditor::Mark, bool &)), this, SLOT(onMarkClicked(KTextEditor::Document *, KTextEditor::Mark, bool &)), Qt::UniqueConnection); } } void addMarksRec(KTextEditor::Document *doc, QStandardItem *item, RangeCollection *ranges, DocumentCollection *docs) { Q_ASSERT(item); addMarks(doc, item, ranges, docs); for (int i = 0; i < item->rowCount(); ++i) { addMarksRec(doc, item->child(i), ranges, docs); } } void addMarks(KTextEditor::Document *doc, QStandardItemModel *treeModel, RangeCollection &ranges, DocumentCollection &docs) { // check if already added auto oranges = ranges.contains(doc) ? nullptr : &ranges; auto odocs = docs.contains(doc) ? nullptr : &docs; if (!oranges && !odocs) return; Q_ASSERT(treeModel); addMarksRec(doc, treeModel->invisibleRootItem(), oranges, odocs); } void goToDocumentLocation(const QUrl &uri, int line, int column) { KTextEditor::View *activeView = m_mainWindow->activeView(); if (!activeView || uri.isEmpty() || line < 0 || column < 0) return; KTextEditor::Document *document = activeView->document(); KTextEditor::Cursor cdef(line, column); if (document && uri == document->url()) { activeView->setCursorPosition(cdef); } else { KTextEditor::View *view = m_mainWindow->openUrl(uri); if (view) { view->setCursorPosition(cdef); } } } void goToItemLocation(const QModelIndex &index) { auto url = index.data(RangeData::FileUrlRole).toUrl(); auto start = index.data(RangeData::RangeRole).value().start(); goToDocumentLocation(url, start.line(), start.column()); } // custom item subclass that captures additional attributes; // a bit more convenient than the variant/role way struct DiagnosticItem : public QStandardItem { LSPDiagnostic m_diagnostic; LSPCodeAction m_codeAction; QSharedPointer m_snapshot; DiagnosticItem(const LSPDiagnostic &d) : m_diagnostic(d) {} DiagnosticItem(const LSPCodeAction &c, QSharedPointer s) : m_codeAction(c), m_snapshot(std::move(s)) { m_diagnostic.range = LSPRange::invalid(); } bool isCodeAction() { return !m_diagnostic.range.isValid() && m_codeAction.title.size(); } }; // double click on: // diagnostic item -> request and add actions (below item) // code action -> perform action (literal edit and/or execute command) // (execution of command may lead to an applyEdit request from server) void triggerCodeAction(const QModelIndex &index) { KTextEditor::View *activeView = m_mainWindow->activeView(); QPointer document = activeView->document(); auto server = m_serverManager->findServer(activeView); auto it = dynamic_cast(m_diagnosticsModel->itemFromIndex(index)); if (!server || !document || !it) return; // click on an action ? if (it->isCodeAction()) { auto &action = it->m_codeAction; // apply edit before command applyWorkspaceEdit(action.edit, it->m_snapshot.data()); auto &command = action.command; if (command.command.size()) { // accept edit requests that may be sent to execute command m_accept_edit = true; // but only for a short time QTimer::singleShot(2000, this, [this] { m_accept_edit = false; }); server->executeCommand(command.command, command.arguments); } // diagnostics are likely updated soon, but might be clicked again in meantime // so clear once executed, so not executed again action.edit.changes.clear(); action.command.command.clear(); return; } // only engage action if // * active document matches diagnostic document // * if really clicked a diagnostic item // (which is the case as it != nullptr and not a code action) // * if no code action invoked and added already // (note; related items are also children) auto url = it->data(RangeData::FileUrlRole).toUrl(); if (url != document->url() || it->data(Qt::UserRole).toBool()) return; // store some things to find item safely later on QPersistentModelIndex pindex(index); QSharedPointer snapshot( m_serverManager->snapshot(server.data())); auto h = [this, url, snapshot, pindex](const QList& actions) { if (!pindex.isValid()) return; auto child = m_diagnosticsModel->itemFromIndex(pindex); if (!child) return; // add actions below diagnostic item for (const auto &action : actions) { auto item = new DiagnosticItem(action, snapshot); child->appendRow(item); auto text = action.kind.size() ? QStringLiteral("[%1] %2").arg(action.kind).arg(action.title) : action.title; item->setData(text, Qt::DisplayRole); item->setData(codeActionIcon(), Qt::DecorationRole); } m_diagnosticsTree->setExpanded(child->index(), true); // mark actions added child->setData(true, Qt::UserRole); }; auto range = activeView->selectionRange(); if (!range.isValid()) { range = document->documentRange(); } server->documentCodeAction(url, range, {}, { it->m_diagnostic }, this, h); } void tabCloseRequested(int index) { auto widget = m_tabWidget->widget(index); if (widget != m_diagnosticsTree) { if (m_markModel && widget == m_markModel->parent()) { clearAllLocationMarks(); } delete widget; } } void switchToDiagnostics() { m_tabWidget->setCurrentWidget(m_diagnosticsTree); m_mainWindow->showToolView(m_toolView.data()); } void closeNonDiagnostics() { for (int i = 0; i < m_tabWidget->count();) { if (m_tabWidget->widget(i) != m_diagnosticsTree) { tabCloseRequested(i); } else { ++i; } } } // local helper to overcome some differences in LSP types struct RangeItem { QUrl uri; LSPRange range; LSPDocumentHighlightKind kind; }; static bool compareRangeItem(const RangeItem &a, const RangeItem &b) { return (a.uri < b.uri) || ((a.uri == b.uri) && a.range < b.range); } // provide Qt::DisplayRole (text) line lazily; // only find line's text content when so requested // This may then involve opening reading some file, at which time // all items for that file will be resolved in one go. struct LineItem : public QStandardItem { KTextEditor::MainWindow *m_mainWindow; LineItem(KTextEditor::MainWindow *mainWindow) : m_mainWindow(mainWindow) {} QVariant data(int role = Qt::UserRole + 1) const override { auto rootItem = this->parent(); if (role != Qt::DisplayRole || !rootItem) { return QStandardItem::data(role); } auto line = data(Qt::UserRole); // either of these mean we tried to obtain line already if (line.isValid() || rootItem->data(RangeData::KindRole).toBool()) { return QStandardItem::data(role).toString().append(line.toString()); } KTextEditor::Document *doc = nullptr; QScopedPointer fr; for (int i = 0; i < rootItem->rowCount(); i++) { auto child = rootItem->child(i); if (i == 0) { auto url = child->data(RangeData::FileUrlRole).toUrl(); doc = findDocument(m_mainWindow, url); if (!doc) { fr.reset(new FileLineReader(url)); } } auto lineno = child->data(RangeData::RangeRole).value().start().line(); auto line = doc ? doc->line(lineno) : fr->line(lineno); child->setData(line, Qt::UserRole); } // mark as processed rootItem->setData(RangeData::KindRole, true); // should work ok return data(role); } }; LSPRange transformRange(const QUrl &url, const LSPClientRevisionSnapshot &snapshot, const LSPRange &range) { KTextEditor::MovingInterface *miface; qint64 revision; auto result = range; snapshot.find(url, miface, revision); if (miface) { miface->transformRange(result, KTextEditor::MovingRange::DoNotExpand, KTextEditor::MovingRange::AllowEmpty, revision); } return result; } void fillItemRoles(QStandardItem *item, const QUrl &url, const LSPRange _range, RangeData::KindEnum kind, const LSPClientRevisionSnapshot *snapshot = nullptr) { auto range = snapshot ? transformRange(url, *snapshot, _range) : _range; item->setData(QVariant(url), RangeData::FileUrlRole); QVariant vrange; vrange.setValue(range); item->setData(vrange, RangeData::RangeRole); item->setData((int)kind, RangeData::KindRole); } void makeTree(const QVector &locations, const LSPClientRevisionSnapshot *snapshot) { // group by url, assuming input is suitably sorted that way auto treeModel = new QStandardItemModel(); treeModel->setColumnCount(1); QUrl lastUrl; QStandardItem *parent = nullptr; for (const auto &loc : locations) { if (loc.uri != lastUrl) { if (parent) { parent->setText( QStringLiteral("%1: %2").arg(lastUrl.path()).arg(parent->rowCount())); } lastUrl = loc.uri; parent = new QStandardItem(); treeModel->appendRow(parent); } auto item = new LineItem(m_mainWindow); parent->appendRow(item); // add partial display data; line will be added by item later on item->setText(i18n("Line: %1: ", loc.range.start().line() + 1)); fillItemRoles(item, loc.uri, loc.range, loc.kind, snapshot); } if (parent) parent->setText(QStringLiteral("%1: %2").arg(lastUrl.path()).arg(parent->rowCount())); // plain heuristic; mark for auto-expand all when safe and/or useful to do so if (treeModel->rowCount() <= 2 || locations.size() <= 20) { treeModel->invisibleRootItem()->setData(true, RangeData::KindRole); } m_ownedModel.reset(treeModel); m_markModel = treeModel; } void showTree(const QString &title, QPointer *targetTree) { // clean up previous target if any if (targetTree && *targetTree) { int index = m_tabWidget->indexOf(*targetTree); if (index >= 0) tabCloseRequested(index); } // setup view auto treeView = new QTreeView(); configureTreeView(treeView); // transfer model from owned to tree and that in turn to tabwidget auto treeModel = m_ownedModel.take(); treeView->setModel(treeModel); treeModel->setParent(treeView); int index = m_tabWidget->addTab(treeView, title); connect(treeView, &QTreeView::clicked, this, &self_type::goToItemLocation); if (treeModel->invisibleRootItem()->data(RangeData::KindRole).toBool()) { treeView->expandAll(); } // track for later cleanup if (targetTree) *targetTree = treeView; // activate the resulting tab m_tabWidget->setCurrentIndex(index); m_mainWindow->showToolView(m_toolView.data()); } void showMessage(const QString &text, KTextEditor::Message::MessageType level) { KTextEditor::View *view = m_mainWindow->activeView(); if (!view || !view->document()) return; auto kmsg = new KTextEditor::Message(text, level); kmsg->setPosition(KTextEditor::Message::BottomInView); kmsg->setAutoHide(500); kmsg->setView(view); view->document()->postMessage(kmsg); } void handleEsc(QEvent *e) { if (!m_mainWindow) return; QKeyEvent *k = static_cast(e); if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { if (!m_ranges.empty()) { clearAllLocationMarks(); } else if (m_toolView->isVisible()) { m_mainWindow->hideToolView(m_toolView.data()); } } } template using LocationRequest = std::function; template void positionRequest(const LocationRequest &req, const Handler &h, QScopedPointer *snapshot = nullptr) { KTextEditor::View *activeView = m_mainWindow->activeView(); auto server = m_serverManager->findServer(activeView); if (!server) return; // track revision if requested if (snapshot) { snapshot->reset(m_serverManager->snapshot(server.data())); } KTextEditor::Cursor cursor = activeView->cursorPosition(); clearAllLocationMarks(); m_req_timeout = false; QTimer::singleShot(1000, this, [this] { m_req_timeout = true; }); m_handle.cancel() = req(*server, activeView->document()->url(), { cursor.line(), cursor.column() }, this, h); } QString currentWord() { KTextEditor::View *activeView = m_mainWindow->activeView(); if (activeView) { KTextEditor::Cursor cursor = activeView->cursorPosition(); return activeView->document()->wordAt(cursor); } else { return QString(); } } // some template and function type trickery here, but at least that buck stops here then ... template>> void processLocations(const QString &title, const typename utils::identity>::type &req, bool onlyshow, const std::function &itemConverter, QPointer *targetTree = nullptr) { // no capture for move only using initializers available (yet), so shared outer type // the additional level of indirection is so it can be 'filled-in' after lambda creation QSharedPointer> s( new QScopedPointer); auto h = [this, title, onlyshow, itemConverter, targetTree, s](const QList &defs) { if (defs.count() == 0) { showMessage(i18n("No results"), KTextEditor::Message::Information); } else { // convert to helper type QVector ranges; ranges.reserve(defs.size()); for (const auto &def : defs) { ranges.push_back(itemConverter(def)); } // ... so we can sort it also std::stable_sort(ranges.begin(), ranges.end(), compareRangeItem); makeTree(ranges, s.data()->data()); // assuming that reply ranges refer to revision when submitted // (not specified anyway in protocol/reply) if (defs.count() > 1 || onlyshow) { showTree(title, targetTree); } // it's not nice to jump to some location if we are too late if (!m_req_timeout && !onlyshow) { // assuming here that the first location is the best one const auto &item = itemConverter(defs.at(0)); const auto &pos = item.range.start(); goToDocumentLocation(item.uri, pos.line(), pos.column()); } // update marks updateState(); } }; positionRequest(req, h, s.data()); } static RangeItem locationToRangeItem(const LSPLocation &loc) { return { loc.uri, loc.range, LSPDocumentHighlightKind::Text }; } void goToDefinition() { auto title = i18nc("@title:tab", "Definition: %1", currentWord()); processLocations(title, &LSPClientServer::documentDefinition, false, &self_type::locationToRangeItem, &m_defTree); } void goToDeclaration() { auto title = i18nc("@title:tab", "Declaration: %1", currentWord()); processLocations(title, &LSPClientServer::documentDeclaration, false, &self_type::locationToRangeItem, &m_declTree); } void findReferences() { auto title = i18nc("@title:tab", "References: %1", currentWord()); bool decl = m_refDeclaration->isChecked(); auto req = [decl](LSPClientServer &server, const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentDefinitionReplyHandler &h) { return server.documentReferences(document, pos, decl, context, h); }; processLocations(title, req, true, &self_type::locationToRangeItem); } void highlight() { // determine current url to capture and use later on QUrl url; const KTextEditor::View *viewForRequest(m_mainWindow->activeView()); if (viewForRequest && viewForRequest->document()) { url = viewForRequest->document()->url(); } auto title = i18nc("@title:tab", "Highlight: %1", currentWord()); auto converter = [url](const LSPDocumentHighlight &hl) { return RangeItem { url, hl.range, hl.kind }; }; processLocations(title, &LSPClientServer::documentHighlight, true, converter); } void hover() { // trigger manually the normally automagic hover if (auto activeView = m_mainWindow->activeView()) { m_hover->textHint(activeView, activeView->cursorPosition()); } } void applyEdits(KTextEditor::Document *doc, const LSPClientRevisionSnapshot *snapshot, const QList &edits) { KTextEditor::MovingInterface *miface = qobject_cast(doc); if (!miface) return; // NOTE: // server might be pretty sloppy wrt edits (e.g. python-language-server) // e.g. send one edit for the whole document rather than 'surgical edits' // and that even when requesting format for a limited selection // ... but then we are but a client and do as we are told // all-in-all a low priority feature // all coordinates in edits are wrt original document, // so create moving ranges that will adjust to preceding edits as they are applied QVector ranges; for (const auto &edit : edits) { auto range = snapshot ? transformRange(doc->url(), *snapshot, edit.range) : edit.range; KTextEditor::MovingRange *mr = miface->newMovingRange(range); ranges.append(mr); } // now make one transaction (a.o. for one undo) and apply in sequence { KTextEditor::Document::EditingTransaction transaction(doc); for (int i = 0; i < ranges.length(); ++i) { doc->replaceText(ranges.at(i)->toRange(), edits.at(i).newText); } } qDeleteAll(ranges); } void applyWorkspaceEdit(const LSPWorkspaceEdit &edit, const LSPClientRevisionSnapshot *snapshot) { auto currentView = m_mainWindow->activeView(); for (auto it = edit.changes.begin(); it != edit.changes.end(); ++it) { auto document = findDocument(m_mainWindow, it.key()); if (!document) { KTextEditor::View *view = m_mainWindow->openUrl(it.key()); if (view) { document = view->document(); } } applyEdits(document, snapshot, it.value()); } if (currentView) { m_mainWindow->activateView(currentView->document()); } } void onApplyEdit(const LSPApplyWorkspaceEditParams &edit, const ApplyEditReplyHandler &h, bool &handled) { if (handled) return; handled = true; if (m_accept_edit) { qCInfo(LSPCLIENT) << "applying edit" << edit.label; applyWorkspaceEdit(edit.edit, nullptr); } else { qCInfo(LSPCLIENT) << "ignoring edit"; } h({ m_accept_edit, QString() }); } template void checkEditResult(const Collection &c) { if (c.size() == 0) { showMessage(i18n("No edits"), KTextEditor::Message::Information); } } void delayCancelRequest(LSPClientServer::RequestHandle &&h, int timeout_ms = 4000) { QTimer::singleShot(timeout_ms, this, [h]() mutable { h.cancel(); }); } void format(QChar lastChar = QChar()) { KTextEditor::View *activeView = m_mainWindow->activeView(); QPointer document = activeView->document(); auto server = m_serverManager->findServer(activeView); if (!server || !document) return; int tabSize = 4; bool insertSpaces = true; auto cfgiface = qobject_cast(document); if (cfgiface) { tabSize = cfgiface->configValue(QStringLiteral("tab-width")).toInt(); insertSpaces = cfgiface->configValue(QStringLiteral("replace-tabs")).toBool(); } // sigh, no move initialization capture ... // (again) assuming reply ranges wrt revisions submitted at this time QSharedPointer snapshot( m_serverManager->snapshot(server.data())); auto h = [this, document, snapshot, lastChar](const QList &edits) { if (lastChar.isNull()) { checkEditResult(edits); } if (document) { applyEdits(document, snapshot.data(), edits); } }; auto options = LSPFormattingOptions { tabSize, insertSpaces, QJsonObject() }; auto handle = !lastChar.isNull() ? server->documentOnTypeFormatting(document->url(), activeView->cursorPosition(), lastChar, options, this, h) : (activeView->selection() ? server->documentRangeFormatting( document->url(), activeView->selectionRange(), options, this, h) : server->documentFormatting(document->url(), options, this, h)); delayCancelRequest(std::move(handle)); } void rename() { KTextEditor::View *activeView = m_mainWindow->activeView(); QPointer document = activeView->document(); auto server = m_serverManager->findServer(activeView); if (!server || !document) return; bool ok = false; // results are typically (too) limited // due to server implementation or limited view/scope // so let's add a disclaimer that it's not our fault QString newName = QInputDialog::getText( activeView, i18nc("@title:window", "Rename"), i18nc("@label:textbox", "New name (caution: not all references may be replaced)"), QLineEdit::Normal, QString(), &ok); if (!ok) { return; } QSharedPointer snapshot( m_serverManager->snapshot(server.data())); auto h = [this, snapshot](const LSPWorkspaceEdit &edit) { checkEditResult(edit.changes); applyWorkspaceEdit(edit, snapshot.data()); }; auto handle = server->documentRename(document->url(), activeView->cursorPosition(), newName, this, h); delayCancelRequest(std::move(handle)); } static QStandardItem *getItem(const QStandardItemModel &model, const QUrl &url) { auto l = model.findItems(url.path()); if (l.length()) { return l.at(0); } return nullptr; } // select/scroll to diagnostics item for document and (optionally) line bool syncDiagnostics(KTextEditor::Document *document, int line, bool allowTop, bool doShow) { if (!m_diagnosticsTree) return false; auto hint = QAbstractItemView::PositionAtTop; QStandardItem *targetItem = nullptr; QStandardItem *topItem = getItem(*m_diagnosticsModel, document->url()); if (topItem) { int count = topItem->rowCount(); // let's not run wild on a linear search in a flood of diagnostics // user is already in enough trouble as it is ;-) if (count > 50) count = 0; for (int i = 0; i < count; ++i) { auto item = topItem->child(i); int itemline = item->data(RangeData::RangeRole).value().start().line(); if (line == itemline && m_diagnosticsTree) { targetItem = item; hint = QAbstractItemView::PositionAtCenter; break; } } } if (!targetItem && allowTop) { targetItem = topItem; } if (targetItem) { m_diagnosticsTree->blockSignals(true); m_diagnosticsTree->scrollTo(targetItem->index(), hint); m_diagnosticsTree->setCurrentIndex(targetItem->index()); m_diagnosticsTree->blockSignals(false); if (doShow) { m_tabWidget->setCurrentWidget(m_diagnosticsTree); m_mainWindow->showToolView(m_toolView.data()); } } return targetItem != nullptr; } void onViewState(KTextEditor::View *view, LSPClientViewTracker::State newState) { if (!view || !view->document()) return; // select top item on view change, // but otherwise leave selection unchanged if no match switch (newState) { case LSPClientViewTracker::ViewChanged: syncDiagnostics(view->document(), view->cursorPosition().line(), true, false); break; case LSPClientViewTracker::LineChanged: syncDiagnostics(view->document(), view->cursorPosition().line(), false, false); break; default: // should not happen break; } } Q_SLOT void onMarkClicked(KTextEditor::Document *document, KTextEditor::Mark mark, bool &handled) { // no action if no mark was sprinkled here if (m_diagnosticsMarks.contains(document) && syncDiagnostics(document, mark.line, false, true)) { handled = true; } } void onDiagnostics(const LSPPublishDiagnosticsParams &diagnostics) { if (!m_diagnosticsTree) return; QStandardItemModel *model = m_diagnosticsModel.data(); QStandardItem *topItem = getItem(*m_diagnosticsModel, diagnostics.uri); if (!topItem) { // no need to create an empty one if (diagnostics.diagnostics.size() == 0) { return; } topItem = new QStandardItem(); model->appendRow(topItem); topItem->setText(diagnostics.uri.path()); } else { topItem->setRowCount(0); } for (const auto &diag : diagnostics.diagnostics) { auto item = new DiagnosticItem(diag); topItem->appendRow(item); QString source; if (diag.source.length()) { source = QStringLiteral("[%1] ").arg(diag.source); } item->setData(diagnosticsIcon(diag.severity), Qt::DecorationRole); item->setText(source + diag.message); fillItemRoles(item, diagnostics.uri, diag.range, diag.severity); const auto &related = diag.relatedInformation; if (!related.location.uri.isEmpty()) { auto relatedItemMessage = new QStandardItem(); relatedItemMessage->setText(related.message); fillItemRoles(relatedItemMessage, related.location.uri, related.location.range, RangeData::KindEnum::Related); auto relatedItemPath = new QStandardItem(); auto basename = QFileInfo(related.location.uri.path()).fileName(); relatedItemPath->setText(QStringLiteral("%1:%2").arg(basename).arg( related.location.range.start().line())); item->appendRow({ relatedItemMessage, relatedItemPath }); m_diagnosticsTree->setExpanded(item->index(), true); } } // TODO perhaps add some custom delegate that only shows 1 line // and only the whole text when item selected ?? m_diagnosticsTree->setExpanded(topItem->index(), true); m_diagnosticsTree->setRowHidden(topItem->row(), QModelIndex(), topItem->rowCount() == 0); m_diagnosticsTree->scrollTo(topItem->index(), QAbstractItemView::PositionAtTop); auto header = m_diagnosticsTree->header(); header->setStretchLastSection(false); header->setMinimumSectionSize(0); header->setSectionResizeMode(0, QHeaderView::Stretch); header->setSectionResizeMode(1, QHeaderView::ResizeToContents); updateState(); } void onDocumentUrlChanged(KTextEditor::Document *doc) { // url already changed by this time and new url not useufl (void)doc; // note; url also changes when closed // spec says; // if a language has a project system, diagnostics are not cleared by *server* // but in either case (url change or close); remove lingering diagnostics // collect active urls QSet fpaths; for (const auto &view : m_mainWindow->views()) { if (auto doc = view->document()) { fpaths.insert(doc->url().path()); } } // check and clear defunct entries const auto &model = *m_diagnosticsModel; for (int i = 0; i < model.rowCount(); ++i) { auto item = model.item(i); if (item && !fpaths.contains(item->text())) { item->setRowCount(0); if (m_diagnosticsTree) { m_diagnosticsTree->setRowHidden(item->row(), QModelIndex(), true); } } } } void onTextChanged(KTextEditor::Document *doc) { if (m_onTypeFormattingTriggers.size() == 0) return; KTextEditor::View *activeView = m_mainWindow->activeView(); if (!activeView || activeView->document() != doc) return; // NOTE the intendation mode should probably be set to None, // so as not to experience unpleasant interference auto cursor = activeView->cursorPosition(); QChar lastChar = cursor.column() == 0 ? QChar::fromLatin1('\n') : doc->characterAt({ cursor.line(), cursor.column() - 1 }); if (m_onTypeFormattingTriggers.contains(lastChar)) { format(lastChar); } } void updateState() { KTextEditor::View *activeView = m_mainWindow->activeView(); auto doc = activeView ? activeView->document() : nullptr; auto server = m_serverManager->findServer(activeView); bool defEnabled = false, declEnabled = false, refEnabled = false; bool hoverEnabled = false, highlightEnabled = false; bool formatEnabled = false; bool renameEnabled = false; if (server) { const auto &caps = server->capabilities(); defEnabled = caps.definitionProvider; // FIXME no real official protocol way to detect, so enable anyway declEnabled = caps.declarationProvider || true; refEnabled = caps.referencesProvider; hoverEnabled = caps.hoverProvider; highlightEnabled = caps.documentHighlightProvider; formatEnabled = caps.documentFormattingProvider || caps.documentRangeFormattingProvider; renameEnabled = caps.renameProvider; connect(server.data(), &LSPClientServer::publishDiagnostics, this, &self_type::onDiagnostics, Qt::UniqueConnection); connect(server.data(), &LSPClientServer::applyEdit, this, &self_type::onApplyEdit, Qt::UniqueConnection); // update format trigger characters const auto &fmt = caps.documentOnTypeFormattingProvider; if (fmt.provider && m_onTypeFormatting->isChecked()) { m_onTypeFormattingTriggers = fmt.triggerCharacters; } else { m_onTypeFormattingTriggers.clear(); } // and monitor for such if (doc) { connect(doc, &KTextEditor::Document::textChanged, this, &self_type::onTextChanged, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::documentUrlChanged, this, &self_type::onDocumentUrlChanged, Qt::UniqueConnection); } } if (m_findDef) m_findDef->setEnabled(defEnabled); if (m_findDecl) m_findDecl->setEnabled(declEnabled); if (m_findRef) m_findRef->setEnabled(refEnabled); if (m_triggerHighlight) m_triggerHighlight->setEnabled(highlightEnabled); if (m_triggerHover) m_triggerHover->setEnabled(hoverEnabled); if (m_triggerFormat) m_triggerFormat->setEnabled(formatEnabled); if (m_triggerRename) m_triggerRename->setEnabled(renameEnabled); if (m_complDocOn) m_complDocOn->setEnabled(server); if (m_restartServer) m_restartServer->setEnabled(server); // update completion with relevant server m_completion->setServer(server); if (m_complDocOn) m_completion->setSelectedDocumentation(m_complDocOn->isChecked()); updateCompletion(activeView, server.data()); // update hover with relevant server m_hover->setServer((m_autoHover && m_autoHover->isChecked()) ? server : nullptr); updateHover(activeView, server.data()); // update marks if applicable if (m_markModel && doc) addMarks(doc, m_markModel, m_ranges, m_marks); if (m_diagnosticsModel && doc) { clearMarks(doc, m_diagnosticsRanges, m_diagnosticsMarks, RangeData::markTypeDiagAll); addMarks(doc, m_diagnosticsModel.data(), m_diagnosticsRanges, m_diagnosticsMarks); } // connect for cleanup stuff if (activeView) connect(activeView, &KTextEditor::View::destroyed, this, &self_type::viewDestroyed, Qt::UniqueConnection); } void viewDestroyed(QObject *view) { m_completionViews.remove(static_cast(view)); m_hoverViews.remove(static_cast(view)); } void updateCompletion(KTextEditor::View *view, LSPClientServer *server) { bool registered = m_completionViews.contains(view); KTextEditor::CodeCompletionInterface *cci = qobject_cast(view); if (!cci) { return; } if (!registered && server && server->capabilities().completionProvider.provider) { qCInfo(LSPCLIENT) << "registering cci"; cci->registerCompletionModel(m_completion.data()); m_completionViews.insert(view); } if (registered && !server) { qCInfo(LSPCLIENT) << "unregistering cci"; cci->unregisterCompletionModel(m_completion.data()); m_completionViews.remove(view); } } void updateHover(KTextEditor::View *view, LSPClientServer *server) { bool registered = m_hoverViews.contains(view); KTextEditor::TextHintInterface *cci = qobject_cast(view); if (!cci) { return; } if (!registered && server && server->capabilities().hoverProvider) { qCInfo(LSPCLIENT) << "registering cci"; cci->registerTextHintProvider(m_hover.data()); m_hoverViews.insert(view); } if (registered && !server) { qCInfo(LSPCLIENT) << "unregistering cci"; cci->unregisterTextHintProvider(m_hover.data()); m_hoverViews.remove(view); } } }; class LSPClientPluginViewImpl : public QObject, public KXMLGUIClient { Q_OBJECT typedef LSPClientPluginViewImpl self_type; KTextEditor::MainWindow *m_mainWindow; QSharedPointer m_serverManager; QScopedPointer m_actionView; public: LSPClientPluginViewImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) : QObject(mainWin), m_mainWindow(mainWin), m_serverManager(LSPClientServerManager::new_(plugin, mainWin)), m_actionView(new LSPClientActionView(plugin, mainWin, this, m_serverManager)) { KXMLGUIClient::setComponentName(QStringLiteral("lspclient"), i18n("LSP Client")); setXMLFile(QStringLiteral("ui.rc")); m_mainWindow->guiFactory()->addClient(this); } - ~LSPClientPluginViewImpl() + ~LSPClientPluginViewImpl() override { // minimize/avoid some surprises; // safe construction/destruction by separate (helper) objects; // signals are auto-disconnected when high-level "view" objects are broken down // so it only remains to clean up lowest level here then prior to removal m_actionView.reset(); m_serverManager.reset(); m_mainWindow->guiFactory()->removeClient(this); } }; QObject *LSPClientPluginView::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) { return new LSPClientPluginViewImpl(plugin, mainWin); } #include "lspclientpluginview.moc" diff --git a/addons/lspclient/lspclientserver.h b/addons/lspclient/lspclientserver.h index c8fb2b00b..2e8156243 100644 --- a/addons/lspclient/lspclientserver.h +++ b/addons/lspclient/lspclientserver.h @@ -1,182 +1,182 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef LSPCLIENTSERVER_H #define LSPCLIENTSERVER_H #include "lspclientprotocol.h" #include #include #include #include #include #include #include #include namespace utils { // template helper // function bind helpers template inline std::function mem_fun(R (T::*pm)(Args...), Tp object) { return [object, pm](Args... args) { return (object->*pm)(std::forward(args)...); }; } template inline std::function mem_fun(R (T::*pm)(Args...) const, Tp object) { return [object, pm](Args... args) { return (object->*pm)(std::forward(args)...); }; } // prevent argument deduction template struct identity { typedef T type; }; } // namespace utils static const int TIMEOUT_SHUTDOWN = 200; template using ReplyHandler = std::function; using DocumentSymbolsReplyHandler = ReplyHandler>; using DocumentDefinitionReplyHandler = ReplyHandler>; using DocumentHighlightReplyHandler = ReplyHandler>; using DocumentHoverReplyHandler = ReplyHandler; using DocumentCompletionReplyHandler = ReplyHandler>; using SignatureHelpReplyHandler = ReplyHandler; using FormattingReplyHandler = ReplyHandler>; using CodeActionReplyHandler = ReplyHandler>; using WorkspaceEditReplyHandler = ReplyHandler; using ApplyEditReplyHandler = ReplyHandler; class LSPClientServer : public QObject { Q_OBJECT public: enum class State { None, Started, Running, Shutdown }; class LSPClientServerPrivate; class RequestHandle { friend class LSPClientServerPrivate; QPointer m_server; int m_id = -1; public: RequestHandle &cancel() { if (m_server) m_server->cancel(m_id); return *this; } }; LSPClientServer(const QStringList &server, const QUrl &root, const QJsonValue &init = QJsonValue()); - ~LSPClientServer(); + ~LSPClientServer() override; // server management // request start bool start(); // request shutdown/stop // if to_xxx >= 0 -> send signal if not exit'ed after timeout void stop(int to_term_ms, int to_kill_ms); int cancel(int id); // properties const QStringList &cmdline() const; State state() const; Q_SIGNAL void stateChanged(LSPClientServer *server); const LSPServerCapabilities &capabilities() const; // language RequestHandle documentSymbols(const QUrl &document, const QObject *context, const DocumentSymbolsReplyHandler &h); RequestHandle documentDefinition(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentDefinitionReplyHandler &h); RequestHandle documentDeclaration(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentDefinitionReplyHandler &h); RequestHandle documentHighlight(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentHighlightReplyHandler &h); RequestHandle documentHover(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentHoverReplyHandler &h); RequestHandle documentReferences(const QUrl &document, const LSPPosition &pos, bool decl, const QObject *context, const DocumentDefinitionReplyHandler &h); RequestHandle documentCompletion(const QUrl &document, const LSPPosition &pos, const QObject *context, const DocumentCompletionReplyHandler &h); RequestHandle signatureHelp(const QUrl &document, const LSPPosition &pos, const QObject *context, const SignatureHelpReplyHandler &h); RequestHandle documentFormatting(const QUrl &document, const LSPFormattingOptions &options, const QObject *context, const FormattingReplyHandler &h); RequestHandle documentRangeFormatting(const QUrl &document, const LSPRange &range, const LSPFormattingOptions &options, const QObject *context, const FormattingReplyHandler &h); RequestHandle documentOnTypeFormatting(const QUrl &document, const LSPPosition &pos, QChar lastChar, const LSPFormattingOptions &options, const QObject *context, const FormattingReplyHandler &h); RequestHandle documentRename(const QUrl &document, const LSPPosition &pos, const QString& newName, const QObject *context, const WorkspaceEditReplyHandler &h); RequestHandle documentCodeAction(const QUrl &document, const LSPRange &range, const QList &kinds, QList diagnostics, const QObject *context, const CodeActionReplyHandler &h); void executeCommand(const QString &command, const QJsonValue &args); // sync void didOpen(const QUrl &document, int version, const QString &langId, const QString &text); // only 1 of text or changes should be non-empty and is considered void didChange(const QUrl &document, int version, const QString &text, const QList &changes = {}); void didSave(const QUrl &document, const QString &text); void didClose(const QUrl &document); // notifcation = signal Q_SIGNALS: void publishDiagnostics(const LSPPublishDiagnosticsParams &); // request = signal void applyEdit(const LSPApplyWorkspaceEditParams &req, const ApplyEditReplyHandler &h, bool &handled); private: // pimpl data holder LSPClientServerPrivate *const d; }; #endif diff --git a/addons/lspclient/lspclientservermanager.cpp b/addons/lspclient/lspclientservermanager.cpp index 078f67587..4fb41a2e5 100644 --- a/addons/lspclient/lspclientservermanager.cpp +++ b/addons/lspclient/lspclientservermanager.cpp @@ -1,825 +1,825 @@ /* SPDX-License-Identifier: MIT Copyright (C) 2019 Mark Nauwelaerts Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * Some explanation here on server configuration JSON, pending such ending up * in real user level documentation ... * * The default configuration in JSON format is roughly as follows; { "global": { "root": null, }, "servers": { "Python": { "command": "python3 -m pyls --check-parent-process" }, "C": { "command": "clangd -log=verbose --background-index" }, "C++": { "use": "C++" } } } * (the "command" can be an array or a string, which is then split into array) * * From the above, the gist is presumably clear. In addition, each server * entry object may also have an "initializationOptions" entry, which is passed * along to the server as part of the 'initialize' method. A clangd-specific * HACK^Hfeature uses this to add "compilationDatabasePath". * * Various stages of override/merge are applied; * + user configuration (loaded from file) overrides (internal) default configuration * + "lspclient" entry in projectMap overrides the above * + the resulting "global" entry is used to supplement (not override) any server entry * * One server instance is used per (root, servertype) combination. * If "root" is not specified, it default to the $HOME directory. If it is * specified as an absolute path, then it used as-is, otherwise it is relative * to the projectBase. For any document, the resulting "root" then determines * whether or not a separate instance is needed. If so, the "root" is passed * as rootUri/rootPath. * * In general, it is recommended to leave root unspecified, as it is not that * important for a server (your mileage may vary though). Fewer instances * are obviously more efficient, and they also have a 'wider' view than * the view of many separate instances. */ #include "lspclientservermanager.h" #include "lspclient_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // helper to find a proper root dir for the given document & file name that indicate the root dir static QString rootForDocumentAndRootIndicationFileName(KTextEditor::Document *document, const QString &rootIndicationFileName) { // search only feasible if document is local file if (!document->url().isLocalFile()) { return QString(); } // search root upwards QDir dir(QFileInfo(document->url().toLocalFile()).absolutePath()); QSet seenDirectories; while (!seenDirectories.contains(dir.absolutePath())) { // update guard seenDirectories.insert(dir.absolutePath()); // the file that indicates the root dir is there => all fine if (dir.exists(rootIndicationFileName)) { return dir.absolutePath(); } // else: cd up, if possible or abort if (!dir.cdUp()) { break; } } // no root found, bad luck return QString(); } #include // local helper; // recursively merge top json top onto bottom json static QJsonObject merge(const QJsonObject &bottom, const QJsonObject &top) { QJsonObject result; for (auto item = top.begin(); item != top.end(); item++) { const auto &key = item.key(); if (item.value().isObject()) { result.insert(key, merge(bottom.value(key).toObject(), item.value().toObject())); } else { result.insert(key, item.value()); } } // parts only in bottom for (auto item = bottom.begin(); item != bottom.end(); item++) { if (!result.contains(item.key())) { result.insert(item.key(), item.value()); } } return result; } // map (highlight)mode to lsp languageId static QString languageId(const QString &mode) { // lookup in cache first static QHash m; auto it = m.find(mode); if (it != m.end()) { return *it; } /** * try to normalize the highlighting name * - lower-case * - transform some special characters */ QString result = mode.toLower(); result = result.replace(QStringLiteral("++"), QStringLiteral("pp")); result = result.replace(QStringLiteral("#"), QStringLiteral("sharp")); /** * we still need to take care of some languages that have "very" different names than the normalized lsp names */ if (result.contains(QLatin1String("fortran"))) { result = QStringLiteral("fortran"); } /** * now, cache the resulting mapping and be done */ m[mode] = result; return result; } // helper guard to handle revision (un)lock struct RevisionGuard { QPointer m_doc; KTextEditor::MovingInterface *m_movingInterface = nullptr; qint64 m_revision = -1; RevisionGuard(KTextEditor::Document *doc = nullptr) : m_doc(doc), m_movingInterface(qobject_cast(doc)) { if (m_movingInterface) { m_revision = m_movingInterface->revision(); m_movingInterface->lockRevision(m_revision); } } // really only need/allow this one (out of 5) RevisionGuard(RevisionGuard &&other) : RevisionGuard(nullptr) { std::swap(m_doc, other.m_doc); std::swap(m_movingInterface, other.m_movingInterface); std::swap(m_revision, other.m_revision); } void release() { m_movingInterface = nullptr; m_revision = -1; } ~RevisionGuard() { // NOTE: hopefully the revision is still valid at this time if (m_doc && m_movingInterface && m_revision >= 0) { m_movingInterface->unlockRevision(m_revision); } } }; class LSPClientRevisionSnapshotImpl : public LSPClientRevisionSnapshot { Q_OBJECT typedef LSPClientRevisionSnapshotImpl self_type; // std::map has more relaxed constraints on value_type std::map m_guards; Q_SLOT void clearRevisions(KTextEditor::Document *doc) { for (auto &item : m_guards) { if (item.second.m_doc == doc) { item.second.release(); } } } public: void add(KTextEditor::Document *doc) { Q_ASSERT(doc); // make sure revision is cleared when needed and no longer used (to unlock or otherwise) // see e.g. implementation in katetexthistory.cpp and assert's in place there auto conn = connect( doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearRevisions(KTextEditor::Document *))); Q_ASSERT(conn); conn = connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearRevisions(KTextEditor::Document *))); Q_ASSERT(conn); m_guards.emplace(doc->url(), doc); } void find(const QUrl &url, KTextEditor::MovingInterface *&miface, qint64 &revision) const override { auto it = m_guards.find(url); if (it != m_guards.end()) { miface = it->second.m_movingInterface; revision = it->second.m_revision; } else { miface = nullptr; revision = -1; } } }; // helper class to sync document changes to LSP server class LSPClientServerManagerImpl : public LSPClientServerManager { Q_OBJECT typedef LSPClientServerManagerImpl self_type; struct DocumentInfo { QSharedPointer server; KTextEditor::MovingInterface *movingInterface; QUrl url; qint64 version; bool open : 1; bool modified : 1; // used for incremental update (if non-empty) QList changes; }; LSPClientPlugin *m_plugin; KTextEditor::MainWindow *m_mainWindow; // merged default and user config QJsonObject m_serverConfig; // root -> (mode -> server) QMap>> m_servers; QHash m_docs; bool m_incrementalSync = false; typedef QVector> ServerList; public: LSPClientServerManagerImpl(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) : m_plugin(plugin), m_mainWindow(mainWin) { connect(plugin, &LSPClientPlugin::update, this, &self_type::updateServerConfig); QTimer::singleShot(100, this, &self_type::updateServerConfig); } - ~LSPClientServerManagerImpl() + ~LSPClientServerManagerImpl() override { // stop everything as we go down // several stages; // stage 1; request shutdown of all servers (in parallel) // (give that some time) // stage 2; send TERM // stage 3; send KILL // stage 1 QEventLoop q; QTimer t; connect(&t, &QTimer::timeout, &q, &QEventLoop::quit); auto run = [&q, &t](int ms) { t.setSingleShot(true); t.start(ms); q.exec(); }; int count = 0; for (const auto &el : m_servers) { for (const auto &s : el) { disconnect(s.data(), nullptr, this, nullptr); if (s->state() != LSPClientServer::State::None) { auto handler = [&q, &count, s]() { if (s->state() != LSPClientServer::State::None) { if (--count == 0) { q.quit(); } } }; connect(s.data(), &LSPClientServer::stateChanged, this, handler); ++count; s->stop(-1, -1); } } } run(500); // stage 2 and 3 count = 0; for (count = 0; count < 2; ++count) { for (const auto &el : m_servers) { for (const auto &s : el) { s->stop(count == 0 ? 1 : -1, count == 0 ? -1 : 1); } } run(100); } } void setIncrementalSync(bool inc) override { m_incrementalSync = inc; } QSharedPointer findServer(KTextEditor::Document *document, bool updatedoc = true) override { if (!document || document->url().isEmpty()) return nullptr; auto it = m_docs.find(document); auto server = it != m_docs.end() ? it->server : nullptr; if (!server) { if ((server = _findServer(document))) trackDocument(document, server); } if (server && updatedoc) update(server.data(), false); return server; } QSharedPointer findServer(KTextEditor::View *view, bool updatedoc = true) override { return view ? findServer(view->document(), updatedoc) : nullptr; } // restart a specific server or all servers if server == nullptr void restart(LSPClientServer *server) override { ServerList servers; // find entry for server(s) and move out for (auto &m : m_servers) { for (auto it = m.begin(); it != m.end();) { if (!server || it->data() == server) { servers.push_back(*it); it = m.erase(it); } else { ++it; } } } restart(servers); } - virtual qint64 revision(KTextEditor::Document *doc) override + qint64 revision(KTextEditor::Document *doc) override { auto it = m_docs.find(doc); return it != m_docs.end() ? it->version : -1; } - virtual LSPClientRevisionSnapshot *snapshot(LSPClientServer *server) override + LSPClientRevisionSnapshot *snapshot(LSPClientServer *server) override { auto result = new LSPClientRevisionSnapshotImpl; for (auto it = m_docs.begin(); it != m_docs.end(); ++it) { if (it->server == server) { // sync server to latest revision that will be recorded update(it.key(), false); result->add(it.key()); } } return result; } private: void showMessage(const QString &msg, KTextEditor::Message::MessageType level) { KTextEditor::View *view = m_mainWindow->activeView(); if (!view || !view->document()) return; auto kmsg = new KTextEditor::Message(xi18nc("@info", "LSP Client: %1", msg), level); kmsg->setPosition(KTextEditor::Message::AboveView); kmsg->setAutoHide(5000); kmsg->setAutoHideMode(KTextEditor::Message::Immediate); kmsg->setView(view); view->document()->postMessage(kmsg); } // caller ensures that servers are no longer present in m_servers void restart(const ServerList &servers) { // close docs for (const auto &server : servers) { // controlling server here, so disable usual state tracking response disconnect(server.data(), nullptr, this, nullptr); for (auto it = m_docs.begin(); it != m_docs.end();) { auto &item = it.value(); if (item.server == server) { // no need to close if server not in proper state if (server->state() != LSPClientServer::State::Running) { item.open = false; } it = _close(it, true); } else { ++it; } } } // helper captures servers auto stopservers = [servers](int t, int k) { for (const auto &server : servers) { server->stop(t, k); } }; // trigger server shutdown now stopservers(-1, -1); // initiate delayed stages (TERM and KILL) // async, so give a bit more time QTimer::singleShot(2 * TIMEOUT_SHUTDOWN, this, [stopservers]() { stopservers(1, -1); }); QTimer::singleShot(4 * TIMEOUT_SHUTDOWN, this, [stopservers]() { stopservers(-1, 1); }); // as for the start part // trigger interested parties, which will again request a server as needed // let's delay this; less chance for server instances to trip over each other QTimer::singleShot(6 * TIMEOUT_SHUTDOWN, this, [this]() { emit serverChanged(); }); } void onStateChanged(LSPClientServer *server) { if (server->state() == LSPClientServer::State::Running) { // clear for normal operation emit serverChanged(); } else if (server->state() == LSPClientServer::State::None) { // went down showMessage(i18n("Server terminated unexpectedly: %1", server->cmdline().join(QLatin1Char(' '))), KTextEditor::Message::Warning); restart(server); } } QSharedPointer _findServer(KTextEditor::Document *document) { QObject *projectView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin")); const auto projectBase = QDir(projectView ? projectView->property("projectBaseDir").toString() : QString()); const auto &projectMap = projectView ? projectView->property("projectMap").toMap() : QVariantMap(); // compute the LSP standardized language id auto langId = languageId(document->highlightingMode()); // merge with project specific auto projectConfig = QJsonDocument::fromVariant(projectMap) .object() .value(QStringLiteral("lspclient")) .toObject(); auto serverConfig = merge(m_serverConfig, projectConfig); // locate server config QJsonValue config; QSet used; while (true) { qCInfo(LSPCLIENT) << "language id " << langId; used << langId; config = serverConfig.value(QStringLiteral("servers")).toObject().value(langId); if (config.isObject()) { const auto &base = config.toObject().value(QStringLiteral("use")).toString(); // basic cycle detection if (!base.isEmpty() && !used.contains(base)) { langId = base; continue; } } break; } if (!config.isObject()) return nullptr; // merge global settings serverConfig = merge(serverConfig.value(QStringLiteral("global")).toObject(), config.toObject()); QString rootpath; auto rootv = serverConfig.value(QStringLiteral("root")); if (rootv.isString()) { auto sroot = rootv.toString(); if (QDir::isAbsolutePath(sroot)) { rootpath = sroot; } else if (!projectBase.isEmpty()) { rootpath = QDir(projectBase).absoluteFilePath(sroot); } } /** * no explicit set root dir? search for a matching root based on some name filters * this is required for some LSP servers like rls that don't handle that on their own like * clangd does */ if (rootpath.isEmpty()) { const auto fileNamesForDetection = serverConfig.value(QStringLiteral("rootIndicationFileNames")); if (fileNamesForDetection.isArray()) { // we try each file name alternative in the listed order // this allows to have preferences for (auto name : fileNamesForDetection.toArray()) { if (name.isString()) { rootpath = rootForDocumentAndRootIndicationFileName(document, name.toString()); if (!rootpath.isEmpty()) { break; } } } } } // last fallback: home directory if (rootpath.isEmpty()) { rootpath = QDir::homePath(); } auto root = QUrl::fromLocalFile(rootpath); auto server = m_servers.value(root).value(langId); if (!server) { QStringList cmdline; // choose debug command line for debug mode, fallback to command auto vcmdline = serverConfig.value(m_plugin->m_debugMode ? QStringLiteral("commandDebug") : QStringLiteral("command")); if (vcmdline.isUndefined()) { vcmdline = serverConfig.value(QStringLiteral("command")); } auto scmdline = vcmdline.toString(); if (!scmdline.isEmpty()) { cmdline = scmdline.split(QLatin1Char(' ')); } else { for (const auto &c : vcmdline.toArray()) { cmdline.push_back(c.toString()); } } if (cmdline.length() > 0) { server.reset(new LSPClientServer( cmdline, root, serverConfig.value(QStringLiteral("initializationOptions")))); m_servers[root][langId] = server; connect(server.data(), &LSPClientServer::stateChanged, this, &self_type::onStateChanged, Qt::UniqueConnection); if (!server->start()) { showMessage(i18n("Failed to start server: %1", cmdline.join(QLatin1Char(' '))), KTextEditor::Message::Error); } } } return (server && server->state() == LSPClientServer::State::Running) ? server : nullptr; } void updateServerConfig() { // default configuration, compiled into plugin resource, reading can't fail QFile defaultConfigFile(QStringLiteral(":/lspclient/settings.json")); defaultConfigFile.open(QIODevice::ReadOnly); Q_ASSERT(defaultConfigFile.isOpen()); m_serverConfig = QJsonDocument::fromJson(defaultConfigFile.readAll()).object(); // consider specified configuration const auto &configPath = m_plugin->m_configPath.path(); if (!configPath.isEmpty()) { QFile f(configPath); if (f.open(QIODevice::ReadOnly)) { auto data = f.readAll(); auto json = QJsonDocument::fromJson(data); if (json.isObject()) { m_serverConfig = merge(m_serverConfig, json.object()); } else { showMessage(i18n("Failed to parse server configuration: %1", configPath), KTextEditor::Message::Error); } } else { showMessage(i18n("Failed to read server configuration: %1", configPath), KTextEditor::Message::Error); } } // we could (but do not) perform restartAll here; // for now let's leave that up to user // but maybe we do have a server now where not before, so let's signal emit serverChanged(); } void trackDocument(KTextEditor::Document *doc, const QSharedPointer& server) { auto it = m_docs.find(doc); if (it == m_docs.end()) { KTextEditor::MovingInterface *miface = qobject_cast(doc); it = m_docs.insert(doc, { server, miface, doc->url(), 0, false, false, {} }); // track document connect(doc, &KTextEditor::Document::documentUrlChanged, this, &self_type::untrack, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::highlightingModeChanged, this, &self_type::untrack, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::aboutToClose, this, &self_type::untrack, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::destroyed, this, &self_type::untrack, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::textChanged, this, &self_type::onTextChanged, Qt::UniqueConnection); // in case of incremental change connect(doc, &KTextEditor::Document::textInserted, this, &self_type::onTextInserted, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::textRemoved, this, &self_type::onTextRemoved, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::lineWrapped, this, &self_type::onLineWrapped, Qt::UniqueConnection); connect(doc, &KTextEditor::Document::lineUnwrapped, this, &self_type::onLineUnwrapped, Qt::UniqueConnection); } else { it->server = server; } } decltype(m_docs)::iterator _close(decltype(m_docs)::iterator it, bool remove) { if (it != m_docs.end()) { if (it->open) { // release server side (use url as registered with) (it->server)->didClose(it->url); it->open = false; } if (remove) { disconnect(it.key(), nullptr, this, nullptr); it = m_docs.erase(it); } } return it; } void _close(KTextEditor::Document *doc, bool remove) { auto it = m_docs.find(doc); if (it != m_docs.end()) { _close(it, remove); } } void untrack(QObject *doc) { _close(qobject_cast(doc), true); emit serverChanged(); } void close(KTextEditor::Document *doc) { _close(doc, false); } void update(const decltype(m_docs)::iterator &it, bool force) { auto doc = it.key(); if (it != m_docs.end() && it->server) { if (it->movingInterface) { it->version = it->movingInterface->revision(); } else if (it->modified) { ++it->version; } if (!m_incrementalSync) { it->changes.clear(); } if (it->open) { if (it->modified || force) { (it->server) ->didChange(it->url, it->version, (it->changes.size() == 0) ? doc->text() : QString(), it->changes); } } else { (it->server) ->didOpen(it->url, it->version, languageId(doc->highlightingMode()), doc->text()); it->open = true; } it->modified = false; it->changes.clear(); } } void update(KTextEditor::Document *doc, bool force) override { update(m_docs.find(doc), force); } void update(LSPClientServer *server, bool force) { for (auto it = m_docs.begin(); it != m_docs.end(); ++it) { if (it->server == server) { update(it, force); } } } void onTextChanged(KTextEditor::Document *doc) { auto it = m_docs.find(doc); if (it != m_docs.end()) { it->modified = true; } } DocumentInfo *getDocumentInfo(KTextEditor::Document *doc) { if (!m_incrementalSync) return nullptr; auto it = m_docs.find(doc); if (it != m_docs.end() && it->server) { const auto &caps = it->server->capabilities(); if (caps.textDocumentSync == LSPDocumentSyncKind::Incremental) { return &(*it); } } return nullptr; } void onTextInserted(KTextEditor::Document *doc, const KTextEditor::Cursor &position, const QString &text) { auto info = getDocumentInfo(doc); if (info) { info->changes.push_back({ LSPRange { position, position }, text }); } } void onTextRemoved(KTextEditor::Document *doc, const KTextEditor::Range &range, const QString &text) { (void)text; auto info = getDocumentInfo(doc); if (info) { info->changes.push_back({ range, QString() }); } } void onLineWrapped(KTextEditor::Document *doc, const KTextEditor::Cursor &position) { // so a 'newline' has been inserted at position // could have been UNIX style or other kind, let's ask the document auto text = doc->text({ position, { position.line() + 1, 0 } }); onTextInserted(doc, position, text); } void onLineUnwrapped(KTextEditor::Document *doc, int line) { // lines line-1 and line got replaced by current content of line-1 Q_ASSERT(line > 0); auto info = getDocumentInfo(doc); if (info) { LSPRange oldrange { { line - 1, 0 }, { line + 1, 0 } }; LSPRange newrange { { line - 1, 0 }, { line, 0 } }; auto text = doc->text(newrange); info->changes.push_back({ oldrange, text }); } } }; QSharedPointer LSPClientServerManager::new_(LSPClientPlugin *plugin, KTextEditor::MainWindow *mainWin) { return QSharedPointer(new LSPClientServerManagerImpl(plugin, mainWin)); } #include "lspclientservermanager.moc" diff --git a/addons/project/kateprojectcodeanalysistool.h b/addons/project/kateprojectcodeanalysistool.h index 6e436ac8a..1a6eee652 100644 --- a/addons/project/kateprojectcodeanalysistool.h +++ b/addons/project/kateprojectcodeanalysistool.h @@ -1,138 +1,138 @@ /* This file is part of the Kate project. * * Copyright (C) 2017 Héctor Mesa Jiménez * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_PROJECT_CODE_ANALYSIS_TOOL_H #define KATE_PROJECT_CODE_ANALYSIS_TOOL_H #include #include #include "kateproject.h" /** * Information provider for a code analysis tool */ class KateProjectCodeAnalysisTool: public QObject { Q_OBJECT protected: explicit KateProjectCodeAnalysisTool(QObject *parent = nullptr); /** * Current project */ KateProject *m_project = nullptr; public: - virtual ~KateProjectCodeAnalysisTool(); + ~KateProjectCodeAnalysisTool() override; /** * bind to this project * @param project project this tool will analyze */ virtual void setProject(KateProject *project); /** * @return tool descriptive name */ virtual QString name() const = 0; /** * @return tool short description */ virtual QString description() const = 0; /** * @returns a string containing the file extensions this * tool should be run, separated by '|', * e.g. "cpp|cxx" * NOTE that this is used directly as part of a regular expression. * If more flexibility is required this method probably will change */ virtual QString fileExtensions() const = 0; /** * filter relevant files * @param files set of files in project * @return relevant files that can be analyzed */ virtual QStringList filter(const QStringList &files) const = 0; /** * @return tool path */ virtual QString path() const = 0; /** * @return arguments required for the tool * NOTE that this method is not const because here setActualFilesCount might be called */ virtual QStringList arguments() = 0; /** * @return warning message when the tool is not installed */ virtual QString notInstalledMessage() const = 0; /** * parse output line * @param line * @return file, line, severity, message */ virtual QStringList parseLine(const QString &line) const = 0; /** * Tells the tool runner if the returned process exit code * was a successful one. * * The default implementation returns true on exitCode 0. * * Override this method for a tool that use a non-zero exit code * e.g. if the processing itself was successful but not all files * had no linter errors. */ virtual bool isSuccessfulExitCode(int exitCode) const; /** * @return messages passed to the tool through stdin * This is used when the files are not passed as arguments to the tool. * * NOTE that this method is not const because here setActualFilesCount might be called */ virtual QString stdinMessages() = 0; /** * @returns the number of files to be processed after the filter * has been applied */ int getActualFilesCount() const; /** * To be called by derived classes */ void setActualFilesCount(int count); private: int m_filesCount = 0; }; Q_DECLARE_METATYPE(KateProjectCodeAnalysisTool*) #endif // KATE_PROJECT_CODE_ANALYSIS_TOOL_H diff --git a/addons/project/tools/kateprojectcodeanalysistoolcppcheck.h b/addons/project/tools/kateprojectcodeanalysistoolcppcheck.h index cc93abe54..404ee6b5d 100644 --- a/addons/project/tools/kateprojectcodeanalysistoolcppcheck.h +++ b/addons/project/tools/kateprojectcodeanalysistoolcppcheck.h @@ -1,55 +1,55 @@ /* This file is part of the Kate project. * * Copyright (C) 2017 Héctor Mesa Jiménez * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_PROJECT_CODE_ANALYSIS_TOOL_CPPCHECK_H #define KATE_PROJECT_CODE_ANALYSIS_TOOL_CPPCHECK_H #include "../kateprojectcodeanalysistool.h" /** * Information provider for cppcheck */ class KateProjectCodeAnalysisToolCppcheck: public KateProjectCodeAnalysisTool { public: explicit KateProjectCodeAnalysisToolCppcheck(QObject *parent = nullptr); - virtual ~KateProjectCodeAnalysisToolCppcheck() override; + ~KateProjectCodeAnalysisToolCppcheck() override; - virtual QString name() const override; + QString name() const override; - virtual QString description() const override; + QString description() const override; - virtual QString fileExtensions() const override; + QString fileExtensions() const override; virtual QStringList filter(const QStringList &files) const override; - virtual QString path() const override; + QString path() const override; - virtual QStringList arguments() override; + QStringList arguments() override; - virtual QString notInstalledMessage() const override; + QString notInstalledMessage() const override; - virtual QStringList parseLine(const QString &line) const override; + QStringList parseLine(const QString &line) const override; - virtual QString stdinMessages() override; + QString stdinMessages() override; }; #endif // KATE_PROJECT_CODE_ANALYSIS_TOOL_CPPCHECK_H diff --git a/addons/project/tools/kateprojectcodeanalysistoolflake8.h b/addons/project/tools/kateprojectcodeanalysistoolflake8.h index 26a37db5d..822cfa22d 100644 --- a/addons/project/tools/kateprojectcodeanalysistoolflake8.h +++ b/addons/project/tools/kateprojectcodeanalysistoolflake8.h @@ -1,55 +1,55 @@ /* This file is part of the Kate project. * * Copyright (C) 2017 Héctor Mesa Jiménez * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KATE_PROJECT_CODE_ANALYSIS_TOOL_FLAKE8_H #define KATE_PROJECT_CODE_ANALYSIS_TOOL_FLAKE8_H #include "../kateprojectcodeanalysistool.h" /** * Information provider for flake8 */ class KateProjectCodeAnalysisToolFlake8: public KateProjectCodeAnalysisTool { public: explicit KateProjectCodeAnalysisToolFlake8(QObject *parent = nullptr); - virtual ~KateProjectCodeAnalysisToolFlake8() override; + ~KateProjectCodeAnalysisToolFlake8() override; - virtual QString name() const override; + QString name() const override; - virtual QString description() const override; + QString description() const override; - virtual QString fileExtensions() const override; + QString fileExtensions() const override; virtual QStringList filter(const QStringList &files) const override; - virtual QString path() const override; + QString path() const override; - virtual QStringList arguments() override; + QStringList arguments() override; - virtual QString notInstalledMessage() const override; + QString notInstalledMessage() const override; - virtual QStringList parseLine(const QString &line) const override; + QStringList parseLine(const QString &line) const override; - virtual QString stdinMessages() override; + QString stdinMessages() override; }; #endif // KATE_PROJECT_CODE_ANALYSIS_TOOL_FLAKE8_H diff --git a/addons/project/tools/kateprojectcodeanalysistoolshellcheck.h b/addons/project/tools/kateprojectcodeanalysistoolshellcheck.h index cb837817b..9ec1b7df0 100644 --- a/addons/project/tools/kateprojectcodeanalysistoolshellcheck.h +++ b/addons/project/tools/kateprojectcodeanalysistoolshellcheck.h @@ -1,54 +1,54 @@ /* This file is part of the Kate project. * * Copyright (C) 2018 Gregor Mi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #pragma once #include "../kateprojectcodeanalysistool.h" /** * Information provider for shellcheck */ class KateProjectCodeAnalysisToolShellcheck : public KateProjectCodeAnalysisTool { public: explicit KateProjectCodeAnalysisToolShellcheck(QObject *parent = nullptr); - virtual ~KateProjectCodeAnalysisToolShellcheck() override; + ~KateProjectCodeAnalysisToolShellcheck() override; - virtual QString name() const override; + QString name() const override; - virtual QString description() const override; + QString description() const override; - virtual QString fileExtensions() const override; + QString fileExtensions() const override; virtual QStringList filter(const QStringList &files) const override; - virtual QString path() const override; + QString path() const override; - virtual QStringList arguments() override; + QStringList arguments() override; - virtual QString notInstalledMessage() const override; + QString notInstalledMessage() const override; - virtual QStringList parseLine(const QString &line) const override; + QStringList parseLine(const QString &line) const override; - virtual bool isSuccessfulExitCode(int exitCode) const override; + bool isSuccessfulExitCode(int exitCode) const override; - virtual QString stdinMessages() override; + QString stdinMessages() override; }; diff --git a/addons/tabswitcher/tabswitcherfilesmodel.h b/addons/tabswitcher/tabswitcherfilesmodel.h index 73ac97456..f60e3e562 100644 --- a/addons/tabswitcher/tabswitcherfilesmodel.h +++ b/addons/tabswitcher/tabswitcherfilesmodel.h @@ -1,115 +1,115 @@ /* This file is part of the KDE project Copyright (C) 2018 Gregor Mi This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KTEXTEDITOR_TAB_SWITCHER_FILES_MODEL_H #define KTEXTEDITOR_TAB_SWITCHER_FILES_MODEL_H #include #include #include namespace KTextEditor { class Document; } namespace detail { /** * Represents one item in the table view of the tab switcher. */ class FilenameListItem { public: FilenameListItem(KTextEditor::Document* doc); KTextEditor::Document *document; QIcon icon() const; QString documentName() const; QString fullPath() const; /** * calculated from documentName and fullPath */ QString displayPathPrefix; }; using FilenameList = std::vector; class TabswitcherFilesModel : public QAbstractTableModel { Q_OBJECT public: explicit TabswitcherFilesModel(QObject *parent = nullptr); - virtual ~TabswitcherFilesModel() = default; + ~TabswitcherFilesModel() override = default; bool insertDocument(int row, KTextEditor::Document * document); bool removeDocument(KTextEditor::Document * document); /** * Clears all data from the model */ void clear(); /** * NOTE: The returned pointer will become invalid as soon as the underlying vector changes. */ KTextEditor::Document * item(int row) const; /** * Move the document to row 0. */ void raiseDocument(KTextEditor::Document * document); /* * Use this method to update all items. * This is typically needed when a document name changes, since then the prefix paths change, * so all items need an update. */ void updateItems(); /** * Reimplemented to return the column count of top-level items. */ int columnCount(const QModelIndex & parent = QModelIndex()) const override; /** * Reimplemented to return the top-level row count. */ int rowCount(const QModelIndex & parent = QModelIndex()) const override; /** * Returns the data for the requested model index. */ QVariant data(const QModelIndex & index, int role) const override; /** * Reimplemented to remove the specified rows. * The paret is always ignored since this is a table model. */ bool removeRows(int row, int count, const QModelIndex & parent = QModelIndex()) override; private: FilenameList data_; }; QString longestCommonPrefix(std::vector const & strs); } #endif // KTEXTEDITOR_TAB_SWITCHER_FILES_MODEL_H diff --git a/addons/tabswitcher/tests/tstestapp.h b/addons/tabswitcher/tests/tstestapp.h index f711b628a..ebf77d36b 100644 --- a/addons/tabswitcher/tests/tstestapp.h +++ b/addons/tabswitcher/tests/tstestapp.h @@ -1,37 +1,37 @@ /* This file is part of the KDE project * * Copyright (C) 2018 Gregor Mi * Copyright (C) 2019 Dominik Haumann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #pragma once #include #include class TsTestApp : public QMainWindow { Q_OBJECT public: explicit TsTestApp(QWidget *parent = nullptr); - ~TsTestApp(); + ~TsTestApp() override; private: class Impl; std::unique_ptr impl_; }; diff --git a/kwrite/kwriteapplication.h b/kwrite/kwriteapplication.h index ac87804a2..164cc075c 100644 --- a/kwrite/kwriteapplication.h +++ b/kwrite/kwriteapplication.h @@ -1,63 +1,63 @@ /* This file is part of the KDE project Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 2001 Anders Lund This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KWRITE_APPLICATION_H #define KWRITE_APPLICATION_H #include #include #include #include class KWrite; class KWriteApplication : public QObject { Q_OBJECT public: KWriteApplication(); - ~KWriteApplication(); + ~KWriteApplication() override; void addDocument(KTextEditor::Document *doc) { m_documents.append(doc); } void removeDocument(KTextEditor::Document *doc) { m_documents.removeAll(doc); } void removeWindow(KWrite *kwrite) { m_kwrites.removeAll(kwrite); } bool noWindows() { return m_kwrites.isEmpty(); } KWrite *newWindow(KTextEditor::Document *doc = nullptr); void restore(); void saveProperties(KConfig *config); public Q_SLOTS: QList documents() { return m_documents; } bool quit(); KTextEditor::MainWindow *activeMainWindow(); QList mainWindows(); bool closeDocument(KTextEditor::Document *document); private: KTextEditor::Application *m_application; QList m_documents; QList m_kwrites; }; #endif