diff --git a/addons/project/CMakeLists.txt b/addons/project/CMakeLists.txt index d08fdf4b4..94afc1905 100644 --- a/addons/project/CMakeLists.txt +++ b/addons/project/CMakeLists.txt @@ -1,96 +1,97 @@ project(kateprojectplugin) find_package(KF5NewStuff ${KF5_DEP_VERSION} REQUIRED) # For KMoreTools # hacks to make LSP stuff from QtCreator compile set(CMAKE_CXX_STANDARD 14) remove_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_FROM_BYTEARRAY -DQT_NO_CAST_TO_ASCII -DQT_NO_SIGNALS_SLOTS_KEYWORDS) kde_enable_exceptions() include_directories( ${CMAKE_CURRENT_BINARY_DIR} lsp ) add_definitions(-DTRANSLATION_DOMAIN=\"kateproject\") set(kateprojectplugin_PART_SRCS fileutil.cpp kateprojectplugin.cpp kateprojectpluginview.cpp kateproject.cpp kateprojectworker.cpp kateprojectitem.cpp kateprojectview.cpp kateprojectviewtree.cpp kateprojecttreeviewcontextmenu.cpp kateprojectinfoview.cpp kateprojectcompletion.cpp kateprojectindex.cpp kateprojectinfoviewindex.cpp kateprojectinfoviewterminal.cpp kateprojectinfoviewcodeanalysis.cpp kateprojectinfoviewnotes.cpp kateprojectconfigpage.cpp kateprojectcodeanalysistool.cpp tools/kateprojectcodeanalysistoolcppcheck.cpp tools/kateprojectcodeanalysistoolflake8.cpp tools/kateprojectcodeanalysistoolshellcheck.cpp tools/kateprojectcodeanalysisselector.cpp # language server protocol from Qt Creator lsp/languageserverprotocol/basemessage.cpp lsp/languageserverprotocol/client.cpp lsp/languageserverprotocol/diagnostics.cpp lsp/languageserverprotocol/jsonobject.cpp lsp/languageserverprotocol/languagefeatures.cpp lsp/languageserverprotocol/lsputils.cpp lsp/languageserverprotocol/servercapabilities.cpp lsp/languageserverprotocol/textsynchronization.cpp lsp/languageserverprotocol/clientcapabilities.cpp lsp/languageserverprotocol/completion.cpp lsp/languageserverprotocol/initializemessages.cpp lsp/languageserverprotocol/jsonrpcmessages.cpp lsp/languageserverprotocol/lsptypes.cpp lsp/languageserverprotocol/messages.cpp lsp/languageserverprotocol/shutdownmessages.cpp lsp/languageserverprotocol/workspace.cpp lsp/utils/environment.cpp lsp/utils/fileutils.cpp lsp/utils/hostosinfo.cpp lsp/utils/qtcassert.cpp lsp/utils/qtcprocess.cpp lsp/utils/savefile.cpp lsp/utils/stringutils.cpp lsp/utils/synchronousprocess.cpp lsp/utils/textutils.cpp lsp/utils/mimetypes/mimedatabase.cpp lsp/utils/mimetypes/mimeglobpattern.cpp lsp/utils/mimetypes/mimemagicrule.cpp lsp/utils/mimetypes/mimemagicrulematcher.cpp lsp/utils/mimetypes/mimeprovider.cpp lsp/utils/mimetypes/mimetype.cpp lsp/utils/mimetypes/mimetypeparser.cpp lsp/languageclient/languageclientinterface.cpp + lsp/languageclient/dynamiccapabilities.cpp lsp/languageclient/client.cpp lsp/languageclient/languageclientmanager.cpp ) # resource for ui file and stuff qt5_add_resources(kateprojectplugin_PART_SRCS plugin.qrc) add_library(kateprojectplugin MODULE ${kateprojectplugin_PART_SRCS}) kcoreaddons_desktop_to_json (kateprojectplugin kateprojectplugin.desktop) target_link_libraries(kateprojectplugin KF5::TextEditor KF5::Parts KF5::I18n KF5::GuiAddons KF5::ItemViews KF5::ItemModels KF5::IconThemes KF5::ThreadWeaver KF5::NewStuff # For KMoreTools ) ########### install files ############### install(TARGETS kateprojectplugin DESTINATION ${PLUGIN_INSTALL_DIR}/ktexteditor ) install( FILES kateproject.example DESTINATION ${DATA_INSTALL_DIR}/kateproject ) ############# unit tests ################ if (BUILD_TESTING) add_subdirectory(autotests) endif() diff --git a/addons/project/kateprojectplugin.h b/addons/project/kateprojectplugin.h index 74a7091c1..652a17081 100644 --- a/addons/project/kateprojectplugin.h +++ b/addons/project/kateprojectplugin.h @@ -1,187 +1,196 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * 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_PLUGIN_H_ #define _KATE_PROJECT_PLUGIN_H_ #include #include #include #include #include #include #include "kateproject.h" #include "kateprojectcompletion.h" #include "lsp/languageclient/languageclientmanager.h" namespace ThreadWeaver { class Queue; } class KateProjectPlugin : public KTextEditor::Plugin { Q_OBJECT public: explicit KateProjectPlugin(QObject *parent = nullptr, const QList & = QList()); ~KateProjectPlugin() override; QObject *createView(KTextEditor::MainWindow *mainWindow) override; int configPages() const override; KTextEditor::ConfigPage *configPage (int number = 0, QWidget *parent = nullptr) override; /** * Create new project for given project filename. * Null pointer if no project can be opened. * File name will be canonicalized! * @param fileName canonicalized file name for the project * @return project or null if not openable */ KateProject *createProjectForFileName(const QString &fileName); /** * Search and open project for given dir, if possible. * Will search upwards for .kateproject file. * Will use internally projectForFileName if project file is found. * @param dir dir to search matching project for * @return project or null if not openable */ KateProject *projectForDir(QDir dir); /** * Search and open project that contains given url, if possible. * Will search upwards for .kateproject file, if the url is a local file. * Will use internally projectForDir. * @param url url to search matching project for * @return project or null if not openable */ KateProject *projectForUrl(const QUrl &url); /** * get list of all current open projects * @return list of all open projects */ QList projects() const { return m_projects; } /** * Get global code completion. * @return global completion object for KTextEditor::View */ KateProjectCompletion *completion() { return &m_completion; } /** * Map current open documents to projects. * @param document document we want to know which project it belongs to * @return project or 0 if none found for this document */ KateProject *projectForDocument(KTextEditor::Document *document) { return m_document2Project.value(document); } void setAutoRepository(bool onGit, bool onSubversion, bool onMercurial); bool autoGit() const; bool autoSubversion() const; bool autoMercurial() const; + /** + * Manager for LSP clients + * Wraps usage of stuff like clangd, ccls, ... + */ + LanguageClient::LanguageClientManager &lspClientManager() + { + return m_lspClientManager; + } + Q_SIGNALS: /** * Signal that a new project got created. * @param project new created project */ void projectCreated(KateProject *project); public Q_SLOTS: /** * New document got created, we need to update our connections * @param document new created document */ void slotDocumentCreated(KTextEditor::Document *document); /** * Document got destroyed. * @param document deleted document */ void slotDocumentDestroyed(QObject *document); /** * Url changed, to auto-load projects */ void slotDocumentUrlChanged(KTextEditor::Document *document); /** * did some project file change? * @param path name of directory that did change */ void slotDirectoryChanged(const QString &path); private: KateProject *createProjectForRepository(const QString &type, const QDir &dir); KateProject *detectGit(const QDir &dir); KateProject *detectSubversion(const QDir &dir); KateProject *detectMercurial(const QDir &dir); void readConfig(); void writeConfig(); private: /** * open plugins, maps project base directory => project */ QList m_projects; /** * filesystem watcher to keep track of all project files * and auto-reload */ QFileSystemWatcher m_fileWatcher; /** * Mapping document => project */ QHash m_document2Project; /** * Project completion */ KateProjectCompletion m_completion; bool m_autoGit : 1; bool m_autoSubversion : 1; bool m_autoMercurial : 1; ThreadWeaver::Queue *m_weaver; /** * Manager for LSP clients * Wraps usage of stuff like clangd, ccls, ... */ LanguageClient::LanguageClientManager m_lspClientManager; }; #endif diff --git a/addons/project/kateprojectpluginview.cpp b/addons/project/kateprojectpluginview.cpp index 3798fef2b..72f307dab 100644 --- a/addons/project/kateprojectpluginview.cpp +++ b/addons/project/kateprojectpluginview.cpp @@ -1,526 +1,536 @@ /* This file is part of the Kate project. * * Copyright (C) 2010 Christoph Cullmann * * 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. */ #include "kateprojectpluginview.h" #include "kateprojectinfoviewindex.h" #include "fileutil.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KateProjectPluginFactory, "kateprojectplugin.json", registerPlugin();) KateProjectPluginView::KateProjectPluginView(KateProjectPlugin *plugin, KTextEditor::MainWindow *mainWin) : QObject(mainWin) , m_plugin(plugin) , m_mainWindow(mainWin) , m_toolView(nullptr) , m_toolInfoView(nullptr) , m_lookupAction(nullptr) { KXMLGUIClient::setComponentName(QStringLiteral("kateproject"), i18n("Kate Project Manager")); setXMLFile(QStringLiteral("ui.rc")); /** * create views for all already existing projects * will create toolviews on demand! */ foreach(KateProject * project, m_plugin->projects()) viewForProject(project); /** * connect to important signals, e.g. for auto project view creation */ connect(m_plugin, &KateProjectPlugin::projectCreated, this, &KateProjectPluginView::viewForProject); connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KateProjectPluginView::slotViewChanged); connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, &KateProjectPluginView::slotViewCreated); /** * connect for all already existing views */ foreach(KTextEditor::View * view, m_mainWindow->views()) slotViewCreated(view); /** * trigger once view change, to highlight right document */ slotViewChanged(); /** * back + forward */ auto a = actionCollection()->addAction(KStandardAction::Back, QStringLiteral("projects_prev_project"), this, SLOT(slotProjectPrev())); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Left)); a = actionCollection()->addAction(KStandardAction::Forward, QStringLiteral("projects_next_project"), this, SLOT(slotProjectNext())); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Right)); a = actionCollection()->addAction(KStandardAction::Goto, QStringLiteral("projects_goto_index"), this, SLOT(slotProjectIndex())); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::ALT | Qt::Key_1)); // popup menu auto popup = new KActionMenu(i18n("Project"), this); actionCollection()->addAction(QStringLiteral("popup_project"), popup); m_lookupAction = popup->menu()->addAction(i18n("Lookup: %1", QString()), this, &KateProjectPluginView::slotProjectIndex); connect(popup->menu(), &QMenu::aboutToShow, this, &KateProjectPluginView::slotContextMenuAboutToShow); /** * add us to gui */ m_mainWindow->guiFactory()->addClient(this); } KateProjectPluginView::~KateProjectPluginView() { /** * cleanup for all views */ foreach(QObject * view, m_textViews) { KTextEditor::CodeCompletionInterface *cci = qobject_cast(view); if (cci) { cci->unregisterCompletionModel(m_plugin->completion()); } } /** * cu toolviews */ delete m_toolView; m_toolView = nullptr; delete m_toolInfoView; m_toolInfoView = nullptr; /** * cu gui client */ m_mainWindow->guiFactory()->removeClient(this); } QPair KateProjectPluginView::viewForProject(KateProject *project) { /** * needs valid project */ Q_ASSERT(project); /** * create toolviews on demand */ if (!m_toolView) { /** * create toolviews */ m_toolView = m_mainWindow->createToolView(m_plugin, QStringLiteral("kateproject"), KTextEditor::MainWindow::Left, QIcon::fromTheme(QStringLiteral("project-open")), i18n("Projects")); m_toolInfoView = m_mainWindow->createToolView(m_plugin, QStringLiteral("kateprojectinfo"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("view-choose")), i18n("Current Project")); /** * create the combo + buttons for the toolViews + stacked widgets */ m_projectsCombo = new QComboBox(m_toolView); m_projectsCombo->setFrame(false); m_reloadButton = new QToolButton(m_toolView); m_reloadButton->setAutoRaise(true); m_reloadButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); QHBoxLayout *layout = new QHBoxLayout(); layout->setSpacing(0); layout->addWidget(m_projectsCombo); layout->addWidget(m_reloadButton); m_toolView->layout()->addItem(layout); m_toolView->layout()->setSpacing(0); m_stackedProjectViews = new QStackedWidget(m_toolView); m_stackedProjectInfoViews = new QStackedWidget(m_toolInfoView); connect(m_projectsCombo, static_cast(&QComboBox::currentIndexChanged), this, &KateProjectPluginView::slotCurrentChanged); connect(m_reloadButton, &QToolButton::clicked, this, &KateProjectPluginView::slotProjectReload); } /** * existing view? */ if (m_project2View.contains(project)) { return m_project2View.value(project); } /** * create new views */ KateProjectView *view = new KateProjectView(this, project); KateProjectInfoView *infoView = new KateProjectInfoView(this, project); /** * attach to toolboxes * first the views, then the combo, that triggers signals */ m_stackedProjectViews->addWidget(view); m_stackedProjectInfoViews->addWidget(infoView); m_stackedProjectInfoViews->setFocusProxy(infoView); m_projectsCombo->addItem(QIcon::fromTheme(QStringLiteral("project-open")), project->name(), project->fileName()); /** * remember and return it */ return (m_project2View[project] = QPair (view, infoView)); } QString KateProjectPluginView::projectFileName() const { // nothing there, skip if (!m_toolView) { return QString(); } QWidget *active = m_stackedProjectViews->currentWidget(); if (!active) { return QString(); } return static_cast(active)->project()->fileName(); } QString KateProjectPluginView::projectName() const { // nothing there, skip if (!m_toolView) { return QString(); } QWidget *active = m_stackedProjectViews->currentWidget(); if (!active) { return QString(); } return static_cast(active)->project()->name(); } QString KateProjectPluginView::projectBaseDir() const { // nothing there, skip if (!m_toolView) { return QString(); } QWidget *active = m_stackedProjectViews->currentWidget(); if (!active) { return QString(); } return static_cast(active)->project()->baseDir(); } QVariantMap KateProjectPluginView::projectMap() const { // nothing there, skip if (!m_toolView) { return QVariantMap(); } QWidget *active = m_stackedProjectViews->currentWidget(); if (!active) { return QVariantMap(); } return static_cast(active)->project()->projectMap(); } QStringList KateProjectPluginView::projectFiles() const { // nothing there, skip if (!m_toolView) { return QStringList(); } KateProjectView *active = static_cast(m_stackedProjectViews->currentWidget()); if (!active) { return QStringList(); } return active->project()->files(); } QString KateProjectPluginView::allProjectsCommonBaseDir() const { auto projects = m_plugin->projects(); if (projects.empty()) { return QString(); } if (projects.size() == 1) { return projects[0]->baseDir(); } QString commonParent1 = FileUtil::commonParent(projects[0]->baseDir(), projects[1]->baseDir()); for (int i = 2; i < projects.size(); i++) { commonParent1 = FileUtil::commonParent(commonParent1, projects[i]->baseDir()); } return commonParent1; } QStringList KateProjectPluginView::allProjectsFiles() const { QStringList fileList; foreach (auto project, m_plugin->projects()) { fileList.append(project->files()); } return fileList; } void KateProjectPluginView::slotViewChanged() { /** * get active view */ KTextEditor::View *activeView = m_mainWindow->activeView(); /** * update pointer, maybe disconnect before */ if (m_activeTextEditorView) { m_activeTextEditorView->document()->disconnect(this); } m_activeTextEditorView = activeView; /** * no current active view, return */ if (!m_activeTextEditorView) { return; } /** * connect to url changed, for auto load */ connect(m_activeTextEditorView->document(), &KTextEditor::Document::documentUrlChanged, this, &KateProjectPluginView::slotDocumentUrlChanged); /** * trigger slot once */ slotDocumentUrlChanged(m_activeTextEditorView->document()); } void KateProjectPluginView::slotCurrentChanged(int index) { // nothing there, skip if (!m_toolView) { return; } /** * trigger change of stacked widgets */ m_stackedProjectViews->setCurrentIndex(index); m_stackedProjectInfoViews->setCurrentIndex(index); /** * open currently selected document */ if (QWidget *current = m_stackedProjectViews->currentWidget()) { static_cast(current)->openSelectedDocument(); } /** * project file name might have changed */ emit projectFileNameChanged(); emit projectMapChanged(); } void KateProjectPluginView::slotDocumentUrlChanged(KTextEditor::Document *document) { /** * abort if empty url or no local path */ if (document->url().isEmpty() || !document->url().isLocalFile()) { return; } /** * search matching project */ KateProject *project = m_plugin->projectForUrl(document->url()); if (!project) { return; } /** * select the file FIRST */ m_project2View.value(project).first->selectFile(document->url().toLocalFile()); /** * get active project view and switch it, if it is for a different project * do this AFTER file selection */ KateProjectView *active = static_cast(m_stackedProjectViews->currentWidget()); if (active != m_project2View.value(project).first) { int index = m_projectsCombo->findData(project->fileName()); if (index >= 0) { m_projectsCombo->setCurrentIndex(index); } } } void KateProjectPluginView::slotViewCreated(KTextEditor::View *view) { /** * connect to destroyed */ connect(view, &KTextEditor::View::destroyed, this, &KateProjectPluginView::slotViewDestroyed); /** * add completion model if possible */ KTextEditor::CodeCompletionInterface *cci = qobject_cast(view); if (cci) { cci->registerCompletionModel(m_plugin->completion()); } /** * remember for this view we need to cleanup! */ m_textViews.insert(view); } void KateProjectPluginView::slotViewDestroyed(QObject *view) { /** * remove remembered views for which we need to cleanup on exit! */ m_textViews.remove(view); } void KateProjectPluginView::slotProjectPrev() { // nothing there, skip if (!m_toolView) { return; } if (!m_projectsCombo->count()) { return; } if (m_projectsCombo->currentIndex() == 0) { m_projectsCombo->setCurrentIndex(m_projectsCombo->count() - 1); } else { m_projectsCombo->setCurrentIndex(m_projectsCombo->currentIndex() - 1); } } void KateProjectPluginView::slotProjectNext() { // nothing there, skip if (!m_toolView) { return; } if (!m_projectsCombo->count()) { return; } if (m_projectsCombo->currentIndex() + 1 == m_projectsCombo->count()) { m_projectsCombo->setCurrentIndex(0); } else { m_projectsCombo->setCurrentIndex(m_projectsCombo->currentIndex() + 1); } } void KateProjectPluginView::slotProjectReload() { // nothing there, skip if (!m_toolView) { return; } /** * force reload if any active project */ if (QWidget *current = m_stackedProjectViews->currentWidget()) { static_cast(current)->project()->reload(true); } } QString KateProjectPluginView::currentWord() const { KTextEditor::View *kv = m_activeTextEditorView; if (!kv) { return QString(); } if (kv->selection() && kv->selectionRange().onSingleLine()) { return kv->selectionText(); } return kv->document()->wordAt(kv->cursorPosition()); } void KateProjectPluginView::slotProjectIndex() { + KTextEditor::View *kv = m_activeTextEditorView; + if (!kv) { + return; + } + + m_plugin->lspClientManager().documentOpened(kv->document()); + + m_plugin->lspClientManager().findLinkAt(kv->document(), kv->cursorPosition(), [](const Utils::Link &) {}); +#if 0 if (!m_toolView) { return; } const QString word = currentWord(); if (!word.isEmpty()) { auto tabView = qobject_cast(m_stackedProjectInfoViews->currentWidget()); if (tabView) { if (auto codeIndex = tabView->findChild()) { tabView->setCurrentWidget(codeIndex); } } m_mainWindow->showToolView(m_toolInfoView); emit projectLookupWord(word); } +#endif } void KateProjectPluginView::slotContextMenuAboutToShow() { const QString word = currentWord(); if (word.isEmpty()) { return; } const QString squeezed = KStringHandler::csqueeze(word, 30); m_lookupAction->setText(i18n("Lookup: %1", squeezed)); } #include "kateprojectpluginview.moc" diff --git a/addons/project/lsp/languageclient/client.cpp b/addons/project/lsp/languageclient/client.cpp index c55344ce0..6f78cae84 100644 --- a/addons/project/lsp/languageclient/client.cpp +++ b/addons/project/lsp/languageclient/client.cpp @@ -1,1210 +1,1222 @@ /**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "client.h" #include "languageclientinterface.h" #include "kateprojectplugin.h" #include "kateproject.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace LanguageServerProtocol; using namespace Utils; namespace LanguageClient { static Q_LOGGING_CATEGORY(LOGLSPCLIENT, "qtc.languageclient.client", QtDebugMsg); Client::Client(KateProjectPlugin *parent, BaseClientInterface *clientInterface) : m_projectPlugin(parent) , m_clientInterface(clientInterface) { m_contentHandler.insert(JsonRpcMessageHandler::jsonRpcMimeType(), &JsonRpcMessageHandler::parseContent); QTC_ASSERT(clientInterface, return); connect(clientInterface, &BaseClientInterface::messageReceived, this, &Client::handleMessage); connect(clientInterface, &BaseClientInterface::error, this, &Client::setError); connect(clientInterface, &BaseClientInterface::finished, this, &Client::finished); } Client::~Client() { } bool Client::start() { return m_clientInterface->start(); } bool Client::reset() { if (!m_restartsLeft) return false; --m_restartsLeft; m_state = Uninitialized; m_responseHandlers.clear(); m_clientInterface->resetBuffer(); m_serverCapabilities = ServerCapabilities(); return true; } static ClientCapabilities generateClientCapabilities() { ClientCapabilities capabilities; WorkspaceClientCapabilities workspaceCapabilities; workspaceCapabilities.setWorkspaceFolders(true); workspaceCapabilities.setApplyEdit(true); DynamicRegistrationCapabilities allowDynamicRegistration; allowDynamicRegistration.setDynamicRegistration(true); workspaceCapabilities.setDidChangeConfiguration(allowDynamicRegistration); workspaceCapabilities.setExecuteCommand(allowDynamicRegistration); capabilities.setWorkspace(workspaceCapabilities); TextDocumentClientCapabilities documentCapabilities; TextDocumentClientCapabilities::SynchronizationCapabilities syncCapabilities; syncCapabilities.setDynamicRegistration(true); syncCapabilities.setWillSave(true); syncCapabilities.setWillSaveWaitUntil(false); syncCapabilities.setDidSave(true); documentCapabilities.setSynchronization(syncCapabilities); SymbolCapabilities symbolCapabilities; SymbolCapabilities::SymbolKindCapabilities symbolKindCapabilities; symbolKindCapabilities.setValueSet( {SymbolKind::File, SymbolKind::Module, SymbolKind::Namespace, SymbolKind::Package, SymbolKind::Class, SymbolKind::Method, SymbolKind::Property, SymbolKind::Field, SymbolKind::Constructor, SymbolKind::Enum, SymbolKind::Interface, SymbolKind::Function, SymbolKind::Variable, SymbolKind::Constant, SymbolKind::String, SymbolKind::Number, SymbolKind::Boolean, SymbolKind::Array, SymbolKind::Object, SymbolKind::Key, SymbolKind::Null, SymbolKind::EnumMember, SymbolKind::Struct, SymbolKind::Event, SymbolKind::Operator, SymbolKind::TypeParameter}); symbolCapabilities.setSymbolKind(symbolKindCapabilities); documentCapabilities.setDocumentSymbol(symbolCapabilities); TextDocumentClientCapabilities::CompletionCapabilities completionCapabilities; completionCapabilities.setDynamicRegistration(true); TextDocumentClientCapabilities::CompletionCapabilities::CompletionItemKindCapabilities completionItemKindCapabilities; completionItemKindCapabilities.setValueSet( {CompletionItemKind::Text, CompletionItemKind::Method, CompletionItemKind::Function, CompletionItemKind::Constructor, CompletionItemKind::Field, CompletionItemKind::Variable, CompletionItemKind::Class, CompletionItemKind::Interface, CompletionItemKind::Module, CompletionItemKind::Property, CompletionItemKind::Unit, CompletionItemKind::Value, CompletionItemKind::Enum, CompletionItemKind::Keyword, CompletionItemKind::Snippet, CompletionItemKind::Color, CompletionItemKind::File, CompletionItemKind::Reference, CompletionItemKind::Folder, CompletionItemKind::EnumMember, CompletionItemKind::Constant, CompletionItemKind::Struct, CompletionItemKind::Event, CompletionItemKind::Operator, CompletionItemKind::TypeParameter}); completionCapabilities.setCompletionItemKind(completionItemKindCapabilities); documentCapabilities.setCompletion(completionCapabilities); TextDocumentClientCapabilities::CodeActionCapabilities codeActionCapabilities; TextDocumentClientCapabilities::CodeActionCapabilities::CodeActionLiteralSupport literalSupport; literalSupport.setCodeActionKind( TextDocumentClientCapabilities::CodeActionCapabilities::CodeActionLiteralSupport:: CodeActionKind(QList{"*"})); codeActionCapabilities.setCodeActionLiteralSupport(literalSupport); documentCapabilities.setCodeAction(codeActionCapabilities); documentCapabilities.setReferences(allowDynamicRegistration); documentCapabilities.setDocumentHighlight(allowDynamicRegistration); documentCapabilities.setDefinition(allowDynamicRegistration); documentCapabilities.setTypeDefinition(allowDynamicRegistration); documentCapabilities.setImplementation(allowDynamicRegistration); capabilities.setTextDocument(documentCapabilities); return capabilities; } void Client::initialize() { QTC_ASSERT(m_clientInterface, return); QTC_ASSERT(m_state == Uninitialized, return); qCDebug(LOGLSPCLIENT) << "initializing language server " << m_displayName; auto initRequest = new InitializeRequest(); // we shall arrive here not before we have some projects around to ensure we have some initial root auto projects = m_projectPlugin->projects(); Q_ASSERT(!projects.isEmpty()); auto params = initRequest->params().value_or(InitializeParams()); params.setCapabilities(generateClientCapabilities()); params.setRootUri(DocumentUri::fromFileName(Utils::FileName::fromString(QFileInfo(QFileInfo(projects.at(0)->fileName()).path()).canonicalFilePath()))); initRequest->setParams(params); QList folders; for (auto project : projects) { folders.push_back(WorkSpaceFolder(QUrl::fromLocalFile(QFileInfo(QFileInfo(project->fileName()).path()).canonicalFilePath()).toString(), project->name())); folders.push_back(WorkSpaceFolder(QUrl::fromLocalFile(QFileInfo(project->baseDir()).canonicalFilePath()).toString(), project->name())); } params.setWorkSpaceFolders(folders); initRequest->setParams(params); initRequest->setResponseCallback([this](const InitializeRequest::Response &initResponse){ intializeCallback(initResponse); }); // directly send data otherwise the state check would fail; initRequest->registerResponseHandler(&m_responseHandlers); m_clientInterface->sendMessage(initRequest->toBaseMessage()); m_state = InitializeRequested; } void Client::intializeCallback(const InitializeRequest::Response &initResponse) { QTC_ASSERT(m_state == InitializeRequested, return); if (optional> error = initResponse.error()) { if (error.value().data().has_value() && error.value().data().value().retry().value_or(false)) { const QString title(tr("Language Server \"%1\" Initialize Error").arg(m_displayName)); auto result = QMessageBox::warning(nullptr, title, error.value().message(), QMessageBox::Retry | QMessageBox::Cancel, QMessageBox::Retry); if (result == QMessageBox::Retry) { m_state = Uninitialized; initialize(); return; } } setError(tr("Initialize error: ") + error.value().message()); emit finished(); return; } const optional &_result = initResponse.result(); if (!_result.has_value()) {// continue on ill formed result log(tr("No initialize result.")); } else { const InitializeResult &result = _result.value(); QStringList error; if (!result.isValid(&error)) // continue on ill formed result log(tr("Initialize result is not valid: ") + error.join("->")); m_serverCapabilities = result.capabilities().value_or(ServerCapabilities()); } qCDebug(LOGLSPCLIENT) << "language server " << m_displayName << " initialized"; m_state = Initialized; sendContent(InitializeNotification()); emit initialized(m_serverCapabilities); } void Client::setError(const QString &message) { log(message); m_state = Error; } void Client::handleMessage(const BaseMessage &message) { if (auto handler = m_contentHandler[message.mimeType]) { QString parseError; handler(message.content, message.codec, parseError, [this](MessageId id, const QByteArray &content, QTextCodec *codec){ this->handleResponse(id, content, codec); }, [this](const QString &method, MessageId id, const IContent *content){ this->handleMethod(method, id, content); }); if (!parseError.isEmpty()) log(parseError); } else { log(tr("Cannot handle content of type: %1").arg(QLatin1String(message.mimeType))); } } void Client::log(const QString &messag) { qDebug() << messag; } void Client::sendContent(const IContent &content) { QTC_ASSERT(m_clientInterface, return); QTC_ASSERT(m_state == Initialized, return); content.registerResponseHandler(&m_responseHandlers); QString error; if (!QTC_GUARD(content.isValid(&error))) qDebug() << error; m_clientInterface->sendMessage(content.toBaseMessage()); } void Client::sendContent(const DocumentUri &uri, const IContent &content) { // if (!m_openedDocument.contains(uri.toFileName())) // return; sendContent(content); } void Client::cancelRequest(const MessageId &id) { m_responseHandlers.remove(id); sendContent(CancelRequest(CancelParameter(id))); } void Client::handleResponse(const MessageId &id, const QByteArray &content, QTextCodec *codec) { if (auto handler = m_responseHandlers[id]) handler(content, codec); } void Client::shutdown() { QTC_ASSERT(m_state == Initialized, emit finished(); return); qCDebug(LOGLSPCLIENT) << "shutdown language server " << m_displayName; ShutdownRequest shutdown; shutdown.setResponseCallback([this](const ShutdownRequest::Response &shutdownResponse){ shutDownCallback(shutdownResponse); }); sendContent(shutdown); m_state = ShutdownRequested; } Client::State Client::state() const { return m_state; } void Client::shutDownCallback(const ShutdownRequest::Response &shutdownResponse) { QTC_ASSERT(m_state == ShutdownRequested, return); QTC_ASSERT(m_clientInterface, return); optional errorValue = shutdownResponse.error(); if (errorValue.has_value()) { ShutdownRequest::Response::Error error = errorValue.value(); qDebug() << error; return; } // directly send data otherwise the state check would fail; m_clientInterface->sendMessage(ExitNotification().toBaseMessage()); qCDebug(LOGLSPCLIENT) << "language server " << m_displayName << " shutdown"; m_state = Shutdown; } void Client::handleMethod(const QString &method, MessageId id, const IContent *content) { #if 0 QStringList error; bool paramsValid = true; if (method == PublishDiagnosticsNotification::methodName) { auto params = dynamic_cast(content)->params().value_or(PublishDiagnosticsParams()); paramsValid = params.isValid(&error); if (paramsValid) handleDiagnostics(params); } else if (method == LogMessageNotification::methodName) { auto params = dynamic_cast(content)->params().value_or(LogMessageParams()); paramsValid = params.isValid(&error); if (paramsValid) log(params, Core::MessageManager::Flash); } else if (method == ShowMessageNotification::methodName) { auto params = dynamic_cast(content)->params().value_or(ShowMessageParams()); paramsValid = params.isValid(&error); if (paramsValid) log(params); } else if (method == ShowMessageRequest::methodName) { auto request = dynamic_cast(content); auto params = request->params().value_or(ShowMessageRequestParams()); paramsValid = params.isValid(&error); if (paramsValid) { showMessageBox(params, request->id()); } else { ShowMessageRequest::Response response(request->id()); ResponseError error; const QString errorMessage = QString("Could not parse ShowMessageRequest parameter of '%1': \"%2\"") .arg(request->id().toString(), QString::fromUtf8(QJsonDocument(params).toJson())); error.setMessage(errorMessage); response.setError(error); sendContent(response); } } else if (method == RegisterCapabilityRequest::methodName) { auto params = dynamic_cast(content)->params().value_or(RegistrationParams()); paramsValid = params.isValid(&error); if (paramsValid) m_dynamicCapabilities.registerCapability(params.registrations()); } else if (method == UnregisterCapabilityRequest::methodName) { auto params = dynamic_cast(content)->params().value_or(UnregistrationParams()); paramsValid = params.isValid(&error); if (paramsValid) m_dynamicCapabilities.unregisterCapability(params.unregistrations()); } else if (method == ApplyWorkspaceEditRequest::methodName) { auto params = dynamic_cast(content)->params().value_or(ApplyWorkspaceEditParams()); paramsValid = params.isValid(&error); if (paramsValid) applyWorkspaceEdit(params.edit()); } else if (method == WorkSpaceFolderRequest::methodName) { WorkSpaceFolderRequest::Response response(dynamic_cast(content)->id()); const QList projects = ProjectExplorer::SessionManager::projects(); WorkSpaceFolderResult result; if (projects.isEmpty()) { result = nullptr; } else { result = Utils::transform(projects, [](ProjectExplorer::Project *project) { return WorkSpaceFolder(project->projectDirectory().toString(), project->displayName()); }); } response.setResult(result); sendContent(response); } else if (id.isValid(&error)) { Response response(id); ResponseError error; error.setCode(ResponseError::MethodNotFound); response.setError(error); sendContent(response); } std::reverse(error.begin(), error.end()); if (!paramsValid) { log(tr("Invalid parameter in \"%1\": %2").arg(method, error.join("->")), Core::MessageManager::Flash); } delete content; #endif } +template +static bool sendTextDocumentPositionParamsRequest(Client *client, + const Request &request, + const DynamicCapabilities &dynamicCapabilities, + const optional &serverCapability) +{ + if (!request.isValid(nullptr)) + return false; + const DocumentUri uri = request.params().value().textDocument().uri(); + const bool supportedFile = client->isSupportedUri(uri); + bool sendMessage = dynamicCapabilities.isRegistered(Request::methodName).value_or(false); + if (sendMessage) { + const TextDocumentRegistrationOptions option(dynamicCapabilities.option(Request::methodName)); + if (option.isValid(nullptr)) + sendMessage = option.filterApplies(FileName::fromString(QUrl(uri).adjusted(QUrl::PreferLocalFile).toString())); + else + sendMessage = supportedFile; + } else { + sendMessage = serverCapability.value_or(sendMessage) && supportedFile; + } + if (sendMessage) + client->sendContent(request); + return sendMessage; +} + +bool Client::findLinkAt(GotoDefinitionRequest &request) +{ + return LanguageClient::sendTextDocumentPositionParamsRequest( + this, request, m_dynamicCapabilities, m_serverCapabilities.definitionProvider()); +} + +bool Client::isSupportedUri(const DocumentUri &uri) const +{ + // FIXME + return true; //m_languagFilter.isSupported(uri.toFileName(), + // Utils::mimeTypeForFile(uri.toFileName().fileName()).name()); +} + +bool Client::openDocument(KTextEditor::Document *document) +{ + +#if 0 + using namespace TextEditor; + if (!isSupportedDocument(document)) + return false; + const FileName &filePath = document->filePath(); + const QString method(DidOpenTextDocumentNotification::methodName); + if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { + if (!registered.value()) + return false; + const TextDocumentRegistrationOptions option( + m_dynamicCapabilities.option(method).toObject()); + if (option.isValid(nullptr) + && !option.filterApplies(filePath, Utils::mimeTypeForName(document->mimeType()))) { + return false; + } + } else if (Utils::optional _sync + = m_serverCapabilities.textDocumentSync()) { + if (auto options = Utils::get_if(&_sync.value())) { + if (!options->openClose().value_or(true)) + return false; + } + } + auto uri = DocumentUri::fromFileName(filePath); + showDiagnostics(uri); + auto textDocument = qobject_cast(document); + TextDocumentItem item; + item.setLanguageId(TextDocumentItem::mimeTypeToLanguageId(document->mimeType())); + item.setUri(uri); + item.setText(QString::fromUtf8(document->contents())); + item.setVersion(textDocument ? textDocument->document()->revision() : 0); + + connect(document, &Core::IDocument::contentsChanged, this, + [this, document](){ + documentContentsChanged(document); + }); + if (textDocument) { + textDocument->completionAssistProvider(); + m_resetAssistProvider << textDocument; + m_completionProvider.setTriggerCharacters( + m_serverCapabilities.completionProvider() + .value_or(ServerCapabilities::CompletionOptions()) + .triggerCharacters() + .value_or(QList())); + textDocument->setCompletionAssistProvider(&m_completionProvider); + textDocument->setQuickFixAssistProvider(&m_quickFixProvider); + connect(textDocument, &QObject::destroyed, this, [this, textDocument]{ + m_resetAssistProvider.remove(textDocument); + }); + } + + m_openedDocument.append(document->filePath()); + sendContent(DidOpenTextDocumentNotification(DidOpenTextDocumentParams(item))); + if (textDocument) + requestDocumentSymbols(textDocument); + +#endif + + TextDocumentItem item; + item.setLanguageId(TextDocumentItem::mimeTypeToLanguageId(QStringLiteral("C++"))); + item.setUri(DocumentUri::fromProtocol(document->url().toString())); + item.setText(document->text()); + item.setVersion(0); + sendContent(DidOpenTextDocumentNotification(DidOpenTextDocumentParams(item))); + + return true; +} + +void Client::closeDocument(const DidCloseTextDocumentParams ¶ms) +{ + sendContent(params.textDocument().uri(), DidCloseTextDocumentNotification(params)); +} + #if 0 static void updateEditorToolBar(QList files) { QList editors = Core::DocumentModel::editorsForDocuments( Utils::transform(files, [](Utils::FileName &file) { return Core::DocumentModel::documentForFilePath(file.toString()); })); for (auto editor : editors) updateEditorToolBar(editor); } static ClientCapabilities generateClientCapabilities() { ClientCapabilities capabilities; WorkspaceClientCapabilities workspaceCapabilities; workspaceCapabilities.setWorkspaceFolders(true); workspaceCapabilities.setApplyEdit(true); DynamicRegistrationCapabilities allowDynamicRegistration; allowDynamicRegistration.setDynamicRegistration(true); workspaceCapabilities.setDidChangeConfiguration(allowDynamicRegistration); workspaceCapabilities.setExecuteCommand(allowDynamicRegistration); capabilities.setWorkspace(workspaceCapabilities); TextDocumentClientCapabilities documentCapabilities; TextDocumentClientCapabilities::SynchronizationCapabilities syncCapabilities; syncCapabilities.setDynamicRegistration(true); syncCapabilities.setWillSave(true); syncCapabilities.setWillSaveWaitUntil(false); syncCapabilities.setDidSave(true); documentCapabilities.setSynchronization(syncCapabilities); SymbolCapabilities symbolCapabilities; SymbolCapabilities::SymbolKindCapabilities symbolKindCapabilities; symbolKindCapabilities.setValueSet( {SymbolKind::File, SymbolKind::Module, SymbolKind::Namespace, SymbolKind::Package, SymbolKind::Class, SymbolKind::Method, SymbolKind::Property, SymbolKind::Field, SymbolKind::Constructor, SymbolKind::Enum, SymbolKind::Interface, SymbolKind::Function, SymbolKind::Variable, SymbolKind::Constant, SymbolKind::String, SymbolKind::Number, SymbolKind::Boolean, SymbolKind::Array, SymbolKind::Object, SymbolKind::Key, SymbolKind::Null, SymbolKind::EnumMember, SymbolKind::Struct, SymbolKind::Event, SymbolKind::Operator, SymbolKind::TypeParameter}); symbolCapabilities.setSymbolKind(symbolKindCapabilities); documentCapabilities.setDocumentSymbol(symbolCapabilities); TextDocumentClientCapabilities::CompletionCapabilities completionCapabilities; completionCapabilities.setDynamicRegistration(true); TextDocumentClientCapabilities::CompletionCapabilities::CompletionItemKindCapabilities completionItemKindCapabilities; completionItemKindCapabilities.setValueSet( {CompletionItemKind::Text, CompletionItemKind::Method, CompletionItemKind::Function, CompletionItemKind::Constructor, CompletionItemKind::Field, CompletionItemKind::Variable, CompletionItemKind::Class, CompletionItemKind::Interface, CompletionItemKind::Module, CompletionItemKind::Property, CompletionItemKind::Unit, CompletionItemKind::Value, CompletionItemKind::Enum, CompletionItemKind::Keyword, CompletionItemKind::Snippet, CompletionItemKind::Color, CompletionItemKind::File, CompletionItemKind::Reference, CompletionItemKind::Folder, CompletionItemKind::EnumMember, CompletionItemKind::Constant, CompletionItemKind::Struct, CompletionItemKind::Event, CompletionItemKind::Operator, CompletionItemKind::TypeParameter}); completionCapabilities.setCompletionItemKind(completionItemKindCapabilities); documentCapabilities.setCompletion(completionCapabilities); TextDocumentClientCapabilities::CodeActionCapabilities codeActionCapabilities; TextDocumentClientCapabilities::CodeActionCapabilities::CodeActionLiteralSupport literalSupport; literalSupport.setCodeActionKind( TextDocumentClientCapabilities::CodeActionCapabilities::CodeActionLiteralSupport:: CodeActionKind(QList{"*"})); codeActionCapabilities.setCodeActionLiteralSupport(literalSupport); documentCapabilities.setCodeAction(codeActionCapabilities); documentCapabilities.setReferences(allowDynamicRegistration); documentCapabilities.setDocumentHighlight(allowDynamicRegistration); documentCapabilities.setDefinition(allowDynamicRegistration); documentCapabilities.setTypeDefinition(allowDynamicRegistration); documentCapabilities.setImplementation(allowDynamicRegistration); capabilities.setTextDocument(documentCapabilities); return capabilities; } -bool Client::openDocument(Core::IDocument *document) -{ - using namespace TextEditor; - if (!isSupportedDocument(document)) - return false; - const FileName &filePath = document->filePath(); - const QString method(DidOpenTextDocumentNotification::methodName); - if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { - if (!registered.value()) - return false; - const TextDocumentRegistrationOptions option( - m_dynamicCapabilities.option(method).toObject()); - if (option.isValid(nullptr) - && !option.filterApplies(filePath, Utils::mimeTypeForName(document->mimeType()))) { - return false; - } - } else if (Utils::optional _sync - = m_serverCapabilities.textDocumentSync()) { - if (auto options = Utils::get_if(&_sync.value())) { - if (!options->openClose().value_or(true)) - return false; - } - } - auto uri = DocumentUri::fromFileName(filePath); - showDiagnostics(uri); - auto textDocument = qobject_cast(document); - TextDocumentItem item; - item.setLanguageId(TextDocumentItem::mimeTypeToLanguageId(document->mimeType())); - item.setUri(uri); - item.setText(QString::fromUtf8(document->contents())); - item.setVersion(textDocument ? textDocument->document()->revision() : 0); - - connect(document, &Core::IDocument::contentsChanged, this, - [this, document](){ - documentContentsChanged(document); - }); - if (textDocument) { - textDocument->completionAssistProvider(); - m_resetAssistProvider << textDocument; - m_completionProvider.setTriggerCharacters( - m_serverCapabilities.completionProvider() - .value_or(ServerCapabilities::CompletionOptions()) - .triggerCharacters() - .value_or(QList())); - textDocument->setCompletionAssistProvider(&m_completionProvider); - textDocument->setQuickFixAssistProvider(&m_quickFixProvider); - connect(textDocument, &QObject::destroyed, this, [this, textDocument]{ - m_resetAssistProvider.remove(textDocument); - }); - } - - m_openedDocument.append(document->filePath()); - sendContent(DidOpenTextDocumentNotification(DidOpenTextDocumentParams(item))); - if (textDocument) - requestDocumentSymbols(textDocument); - - return true; -} - -void Client::closeDocument(const DidCloseTextDocumentParams ¶ms) -{ - sendContent(params.textDocument().uri(), DidCloseTextDocumentNotification(params)); -} - bool Client::documentOpen(const Core::IDocument *document) const { return m_openedDocument.contains(document->filePath()); } void Client::documentContentsSaved(Core::IDocument *document) { if (!m_openedDocument.contains(document->filePath())) return; bool sendMessage = true; bool includeText = false; const QString method(DidSaveTextDocumentNotification::methodName); if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { sendMessage = registered.value(); if (sendMessage) { const TextDocumentSaveRegistrationOptions option( m_dynamicCapabilities.option(method).toObject()); if (option.isValid(nullptr)) { sendMessage = option.filterApplies(document->filePath(), Utils::mimeTypeForName(document->mimeType())); includeText = option.includeText().value_or(includeText); } } } else if (Utils::optional _sync = m_serverCapabilities.textDocumentSync()) { if (auto options = Utils::get_if(&_sync.value())) { if (Utils::optional saveOptions = options->save()) includeText = saveOptions.value().includeText().value_or(includeText); } } if (!sendMessage) return; DidSaveTextDocumentParams params( TextDocumentIdentifier(DocumentUri::fromFileName(document->filePath()))); if (includeText) params.setText(QString::fromUtf8(document->contents())); sendContent(DidSaveTextDocumentNotification(params)); } void Client::documentWillSave(Core::IDocument *document) { const FileName &filePath = document->filePath(); if (!m_openedDocument.contains(filePath)) return; bool sendMessage = true; const QString method(WillSaveTextDocumentNotification::methodName); if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { sendMessage = registered.value(); if (sendMessage) { const TextDocumentRegistrationOptions option(m_dynamicCapabilities.option(method)); if (option.isValid(nullptr)) { sendMessage = option.filterApplies(filePath, Utils::mimeTypeForName(document->mimeType())); } } } else if (Utils::optional _sync = m_serverCapabilities.textDocumentSync()) { if (auto options = Utils::get_if(&_sync.value())) sendMessage = options->willSave().value_or(sendMessage); } if (!sendMessage) return; const WillSaveTextDocumentParams params( TextDocumentIdentifier(DocumentUri::fromFileName(document->filePath()))); sendContent(WillSaveTextDocumentNotification(params)); } void Client::documentContentsChanged(Core::IDocument *document) { if (!m_openedDocument.contains(document->filePath())) return; const QString method(DidChangeTextDocumentNotification::methodName); TextDocumentSyncKind syncKind = m_serverCapabilities.textDocumentSyncKindHelper(); if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { syncKind = registered.value() ? TextDocumentSyncKind::None : TextDocumentSyncKind::Full; if (syncKind != TextDocumentSyncKind::None) { const TextDocumentChangeRegistrationOptions option( m_dynamicCapabilities.option(method).toObject()); syncKind = option.isValid(nullptr) ? option.syncKind() : syncKind; } } auto textDocument = qobject_cast(document); if (syncKind != TextDocumentSyncKind::None) { const auto uri = DocumentUri::fromFileName(document->filePath()); VersionedTextDocumentIdentifier docId(uri); docId.setVersion(textDocument ? textDocument->document()->revision() : 0); const DidChangeTextDocumentParams params(docId, QString::fromUtf8(document->contents())); sendContent(DidChangeTextDocumentNotification(params)); } if (textDocument) { using namespace TextEditor; for (BaseTextEditor *editor : BaseTextEditor::textEditorsForDocument(textDocument)) if (TextEditorWidget *widget = editor->editorWidget()) widget->setRefactorMarkers(RefactorMarker::filterOutType(widget->refactorMarkers(), id())); requestDocumentSymbols(textDocument); } } void Client::registerCapabilities(const QList ®istrations) { m_dynamicCapabilities.registerCapability(registrations); } void Client::unregisterCapabilities(const QList &unregistrations) { m_dynamicCapabilities.unregisterCapability(unregistrations); } -template -static bool sendTextDocumentPositionParamsRequest(Client *client, - const Request &request, - const DynamicCapabilities &dynamicCapabilities, - const optional &serverCapability) -{ - if (!request.isValid(nullptr)) - return false; - const DocumentUri uri = request.params().value().textDocument().uri(); - const bool supportedFile = client->isSupportedUri(uri); - bool sendMessage = dynamicCapabilities.isRegistered(Request::methodName).value_or(false); - if (sendMessage) { - const TextDocumentRegistrationOptions option(dynamicCapabilities.option(Request::methodName)); - if (option.isValid(nullptr)) - sendMessage = option.filterApplies(FileName::fromString(QUrl(uri).adjusted(QUrl::PreferLocalFile).toString())); - else - sendMessage = supportedFile; - } else { - sendMessage = serverCapability.value_or(sendMessage) && supportedFile; - } - if (sendMessage) - client->sendContent(request); - return sendMessage; -} - -bool Client::findLinkAt(GotoDefinitionRequest &request) -{ - return LanguageClient::sendTextDocumentPositionParamsRequest( - this, request, m_dynamicCapabilities, m_serverCapabilities.definitionProvider()); -} - bool Client::findUsages(FindReferencesRequest &request) { return LanguageClient::sendTextDocumentPositionParamsRequest( this, request, m_dynamicCapabilities, m_serverCapabilities.referencesProvider()); } TextEditor::HighlightingResult createHighlightingResult(const SymbolInformation &info) { if (!info.isValid(nullptr)) return {}; const Position &start = info.location().range().start(); return TextEditor::HighlightingResult(start.line() + 1, start.character() + 1, info.name().length(), info.kind()); } void Client::requestDocumentSymbols(TextEditor::TextDocument *document) { // TODO: Do not use this information for highlighting but the overview model return; const FileName &filePath = document->filePath(); bool sendMessage = m_dynamicCapabilities.isRegistered(DocumentSymbolsRequest::methodName).value_or(false); if (sendMessage) { const TextDocumentRegistrationOptions option(m_dynamicCapabilities.option(DocumentSymbolsRequest::methodName)); if (option.isValid(nullptr)) sendMessage = option.filterApplies(filePath, Utils::mimeTypeForName(document->mimeType())); } else { sendMessage = m_serverCapabilities.documentSymbolProvider().value_or(false); } if (!sendMessage) return; DocumentSymbolsRequest request( DocumentSymbolParams(TextDocumentIdentifier(DocumentUri::fromFileName(filePath)))); request.setResponseCallback( [doc = QPointer(document)] (DocumentSymbolsRequest::Response response){ if (!doc) return; const DocumentSymbolsResult result = response.result().value_or(DocumentSymbolsResult()); if (!holds_alternative>(result)) return; const auto &symbols = get>(result); QFutureInterface future; for (const SymbolInformation &symbol : symbols) future.reportResult(createHighlightingResult(symbol)); const TextEditor::FontSettings &fs = doc->fontSettings(); QHash formatMap; formatMap[static_cast(LanguageServerProtocol::SymbolKind::File )] = fs.toTextCharFormat(TextEditor::C_STRING); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Module )] = fs.toTextCharFormat(TextEditor::C_STRING); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Namespace )] = fs.toTextCharFormat(TextEditor::C_STRING); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Package )] = fs.toTextCharFormat(TextEditor::C_STRING); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Class )] = fs.toTextCharFormat(TextEditor::C_TYPE); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Method )] = fs.toTextCharFormat(TextEditor::C_FUNCTION); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Property )] = fs.toTextCharFormat(TextEditor::C_FIELD); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Field )] = fs.toTextCharFormat(TextEditor::C_FIELD); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Constructor )] = fs.toTextCharFormat(TextEditor::C_FUNCTION); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Enum )] = fs.toTextCharFormat(TextEditor::C_TYPE); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Interface )] = fs.toTextCharFormat(TextEditor::C_TYPE); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Function )] = fs.toTextCharFormat(TextEditor::C_FUNCTION); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Variable )] = fs.toTextCharFormat(TextEditor::C_LOCAL); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Constant )] = fs.toTextCharFormat(TextEditor::C_LOCAL); formatMap[static_cast(LanguageServerProtocol::SymbolKind::String )] = fs.toTextCharFormat(TextEditor::C_STRING); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Number )] = fs.toTextCharFormat(TextEditor::C_NUMBER); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Boolean )] = fs.toTextCharFormat(TextEditor::C_KEYWORD); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Array )] = fs.toTextCharFormat(TextEditor::C_STRING); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Object )] = fs.toTextCharFormat(TextEditor::C_LOCAL); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Key )] = fs.toTextCharFormat(TextEditor::C_LOCAL); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Null )] = fs.toTextCharFormat(TextEditor::C_KEYWORD); formatMap[static_cast(LanguageServerProtocol::SymbolKind::EnumMember )] = fs.toTextCharFormat(TextEditor::C_ENUMERATION); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Struct )] = fs.toTextCharFormat(TextEditor::C_TYPE); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Event )] = fs.toTextCharFormat(TextEditor::C_STRING); formatMap[static_cast(LanguageServerProtocol::SymbolKind::Operator )] = fs.toTextCharFormat(TextEditor::C_OPERATOR); formatMap[static_cast(LanguageServerProtocol::SymbolKind::TypeParameter)] = fs.toTextCharFormat(TextEditor::C_LOCAL); TextEditor::SemanticHighlighter::incrementalApplyExtraAdditionalFormats( doc->syntaxHighlighter(), future.future(), 0, future.resultCount() - 1, formatMap); }); sendContent(request); } void Client::cursorPositionChanged(TextEditor::TextEditorWidget *widget) { const auto uri = DocumentUri::fromFileName(widget->textDocument()->filePath()); if (m_dynamicCapabilities.isRegistered(DocumentHighlightsRequest::methodName).value_or(false)) { TextDocumentRegistrationOptions option( m_dynamicCapabilities.option(DocumentHighlightsRequest::methodName)); if (!option.filterApplies(widget->textDocument()->filePath())) return; } else if (!m_serverCapabilities.documentHighlightProvider().value_or(false)) { return; } auto runningRequest = m_highlightRequests.find(uri); if (runningRequest != m_highlightRequests.end()) cancelRequest(runningRequest.value()); DocumentHighlightsRequest request(TextDocumentPositionParams(uri, widget->textCursor())); request.setResponseCallback( [widget = QPointer(widget), this, uri] (DocumentHighlightsRequest::Response response) { m_highlightRequests.remove(uri); if (!widget) return; QList selections; const DocumentHighlightsResult result = response.result().value_or(DocumentHighlightsResult()); if (!holds_alternative>(result)) { widget->setExtraSelections(TextEditor::TextEditorWidget::CodeSemanticsSelection, selections); return; } const QTextCharFormat &format = widget->textDocument()->fontSettings().toTextCharFormat(TextEditor::C_OCCURRENCES); QTextDocument *document = widget->document(); for (const auto &highlight : get>(result)) { QTextEdit::ExtraSelection selection{widget->textCursor(), format}; const int &start = highlight.range().start().toPositionInDocument(document); const int &end = highlight.range().end().toPositionInDocument(document); if (start < 0 || end < 0) continue; selection.cursor.setPosition(start); selection.cursor.setPosition(end, QTextCursor::KeepAnchor); selections << selection; } widget->setExtraSelections(TextEditor::TextEditorWidget::CodeSemanticsSelection, selections); }); m_highlightRequests[uri] = request.id(); sendContent(request); } void Client::requestCodeActions(const DocumentUri &uri, const QList &diagnostics) { const Utils::FileName fileName = uri.toFileName(); TextEditor::TextDocument *doc = TextEditor::TextDocument::textDocumentForFileName(fileName); if (!doc) return; CodeActionParams codeActionParams; CodeActionParams::CodeActionContext context; context.setDiagnostics(diagnostics); codeActionParams.setContext(context); codeActionParams.setTextDocument(uri); Position start(0, 0); const QTextBlock &lastBlock = doc->document()->lastBlock(); Position end(lastBlock.blockNumber(), lastBlock.length() - 1); codeActionParams.setRange(Range(start, end)); CodeActionRequest request(codeActionParams); request.setResponseCallback( [uri, self = QPointer(this)](const CodeActionRequest::Response &response) { if (self) self->handleCodeActionResponse(response, uri); }); requestCodeActions(request); } void Client::requestCodeActions(const CodeActionRequest &request) { if (!request.isValid(nullptr)) return; const Utils::FileName fileName = request.params().value_or(CodeActionParams()).textDocument().uri().toFileName(); const QString method(CodeActionRequest::methodName); if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { if (!registered.value()) return; const TextDocumentRegistrationOptions option( m_dynamicCapabilities.option(method).toObject()); if (option.isValid(nullptr) && !option.filterApplies(fileName)) return; } else { Utils::variant provider = m_serverCapabilities.codeActionProvider().value_or(false); if (!(Utils::holds_alternative(provider) || Utils::get(provider))) return; } sendContent(request); } void Client::handleCodeActionResponse(const CodeActionRequest::Response &response, const DocumentUri &uri) { if (const Utils::optional &error = response.error()) log(*error); if (const Utils::optional &_result = response.result()) { const CodeActionResult &result = _result.value(); if (auto list = Utils::get_if>>(&result)) { for (const Utils::variant &item : *list) { if (auto action = Utils::get_if(&item)) updateCodeActionRefactoringMarker(this, *action, uri); else if (auto command = Utils::get_if(&item)) { Q_UNUSED(command); // todo } } } } } void Client::executeCommand(const Command &command) { using CommandOptions = LanguageServerProtocol::ServerCapabilities::ExecuteCommandOptions; const QString method(ExecuteCommandRequest::methodName); if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { if (!registered.value()) return; const CommandOptions option(m_dynamicCapabilities.option(method).toObject()); if (option.isValid(nullptr) && !option.commands().isEmpty() && !option.commands().contains(command.command())) return; } else if (Utils::optional option = m_serverCapabilities.executeCommandProvider()) { if (option->isValid(nullptr) && !option->commands().isEmpty() && !option->commands().contains(command.command())) return; } else { return; } const ExecuteCommandRequest request((ExecuteCommandParams(command))); sendContent(request); } void Client::projectOpened(ProjectExplorer::Project *project) { if (!sendWorkspceFolderChanges()) return; WorkspaceFoldersChangeEvent event; event.setAdded({WorkSpaceFolder(project->projectDirectory().toString(), project->displayName())}); DidChangeWorkspaceFoldersParams params; params.setEvent(event); DidChangeWorkspaceFoldersNotification change(params); sendContent(change); } void Client::projectClosed(ProjectExplorer::Project *project) { if (!sendWorkspceFolderChanges()) return; WorkspaceFoldersChangeEvent event; event.setRemoved({WorkSpaceFolder(project->projectDirectory().toString(), project->displayName())}); DidChangeWorkspaceFoldersParams params; params.setEvent(event); DidChangeWorkspaceFoldersNotification change(params); sendContent(change); } void Client::setSupportedLanguage(const LanguageFilter &filter) { m_languagFilter = filter; } bool Client::isSupportedDocument(const Core::IDocument *document) const { QTC_ASSERT(document, return false); return m_languagFilter.isSupported(document); } bool Client::isSupportedFile(const Utils::FileName &filePath, const QString &mimeType) const { return m_languagFilter.isSupported(filePath, mimeType); } -bool Client::isSupportedUri(const DocumentUri &uri) const -{ - return m_languagFilter.isSupported(uri.toFileName(), - Utils::mimeTypeForFile(uri.toFileName().fileName()).name()); -} - bool Client::needsRestart(const BaseSettings *settings) const { QTC_ASSERT(settings, return false); return m_languagFilter.mimeTypes != settings->m_languageFilter.mimeTypes || m_languagFilter.filePattern != settings->m_languageFilter.filePattern; } QList Client::diagnosticsAt(const DocumentUri &uri, const Range &range) const { QList diagnostics; for (const TextMark *mark : m_diagnostics[uri]) { const Diagnostic diagnostic = mark->diagnostic(); if (diagnostic.range().overlaps(range)) diagnostics << diagnostic; } return diagnostics; } const ServerCapabilities &Client::capabilities() const { return m_serverCapabilities; } const DynamicCapabilities &Client::dynamicCapabilities() const { return m_dynamicCapabilities; } const BaseClientInterface *Client::clientInterface() const { return m_clientInterface.data(); } DocumentSymbolCache *Client::documentSymbolCache() { return &m_documentSymbolCache; } void Client::log(const ShowMessageParams &message, Core::MessageManager::PrintToOutputPaneFlag flag) { log(message.toString(), flag); } void Client::showMessageBox(const ShowMessageRequestParams &message, const MessageId &id) { auto box = new QMessageBox(); box->setText(message.toString()); box->setAttribute(Qt::WA_DeleteOnClose); switch (message.type()) { case Error: box->setIcon(QMessageBox::Critical); break; case Warning: box->setIcon(QMessageBox::Warning); break; case Info: box->setIcon(QMessageBox::Information); break; case Log: box->setIcon(QMessageBox::NoIcon); break; } QHash itemForButton; if (const Utils::optional> actions = message.actions()) { for (const MessageActionItem &action : actions.value()) itemForButton.insert(box->addButton(action.title(), QMessageBox::InvalidRole), action); } box->setModal(true); connect(box, &QMessageBox::finished, this, [=]{ ShowMessageRequest::Response response(id); const MessageActionItem &item = itemForButton.value(box->clickedButton()); response.setResult(item.isValid(nullptr) ? LanguageClientValue(item) : LanguageClientValue()); sendContent(response); }); box->show(); } void Client::showDiagnostics(const DocumentUri &uri) { if (TextEditor::TextDocument *doc = TextEditor::TextDocument::textDocumentForFileName(uri.toFileName())) { for (TextMark *mark : m_diagnostics.value(uri)) doc->addMark(mark); } } void Client::removeDiagnostics(const DocumentUri &uri) { TextEditor::TextDocument *doc = TextEditor::TextDocument::textDocumentForFileName(uri.toFileName()); for (TextMark *mark : m_diagnostics.take(uri)) { if (doc) doc->removeMark(mark); delete mark; } } void Client::handleMethod(const QString &method, MessageId id, const IContent *content) { QStringList error; bool paramsValid = true; if (method == PublishDiagnosticsNotification::methodName) { auto params = dynamic_cast(content)->params().value_or(PublishDiagnosticsParams()); paramsValid = params.isValid(&error); if (paramsValid) handleDiagnostics(params); } else if (method == LogMessageNotification::methodName) { auto params = dynamic_cast(content)->params().value_or(LogMessageParams()); paramsValid = params.isValid(&error); if (paramsValid) log(params, Core::MessageManager::Flash); } else if (method == ShowMessageNotification::methodName) { auto params = dynamic_cast(content)->params().value_or(ShowMessageParams()); paramsValid = params.isValid(&error); if (paramsValid) log(params); } else if (method == ShowMessageRequest::methodName) { auto request = dynamic_cast(content); auto params = request->params().value_or(ShowMessageRequestParams()); paramsValid = params.isValid(&error); if (paramsValid) { showMessageBox(params, request->id()); } else { ShowMessageRequest::Response response(request->id()); ResponseError error; const QString errorMessage = QString("Could not parse ShowMessageRequest parameter of '%1': \"%2\"") .arg(request->id().toString(), QString::fromUtf8(QJsonDocument(params).toJson())); error.setMessage(errorMessage); response.setError(error); sendContent(response); } } else if (method == RegisterCapabilityRequest::methodName) { auto params = dynamic_cast(content)->params().value_or(RegistrationParams()); paramsValid = params.isValid(&error); if (paramsValid) m_dynamicCapabilities.registerCapability(params.registrations()); } else if (method == UnregisterCapabilityRequest::methodName) { auto params = dynamic_cast(content)->params().value_or(UnregistrationParams()); paramsValid = params.isValid(&error); if (paramsValid) m_dynamicCapabilities.unregisterCapability(params.unregistrations()); } else if (method == ApplyWorkspaceEditRequest::methodName) { auto params = dynamic_cast(content)->params().value_or(ApplyWorkspaceEditParams()); paramsValid = params.isValid(&error); if (paramsValid) applyWorkspaceEdit(params.edit()); } else if (method == WorkSpaceFolderRequest::methodName) { WorkSpaceFolderRequest::Response response(dynamic_cast(content)->id()); const QList projects = ProjectExplorer::SessionManager::projects(); WorkSpaceFolderResult result; if (projects.isEmpty()) { result = nullptr; } else { result = Utils::transform(projects, [](ProjectExplorer::Project *project) { return WorkSpaceFolder(project->projectDirectory().toString(), project->displayName()); }); } response.setResult(result); sendContent(response); } else if (id.isValid(&error)) { Response response(id); ResponseError error; error.setCode(ResponseError::MethodNotFound); response.setError(error); sendContent(response); } std::reverse(error.begin(), error.end()); if (!paramsValid) { log(tr("Invalid parameter in \"%1\": %2").arg(method, error.join("->")), Core::MessageManager::Flash); } delete content; } void Client::handleDiagnostics(const PublishDiagnosticsParams ¶ms) { const DocumentUri &uri = params.uri(); removeDiagnostics(uri); const QList &diagnostics = params.diagnostics(); m_diagnostics[uri] = Utils::transform(diagnostics, [fileName = uri.toFileName()](const Diagnostic &diagnostic) { return new TextMark(fileName, diagnostic); }); showDiagnostics(uri); requestCodeActions(uri, diagnostics); } bool Client::sendWorkspceFolderChanges() const { if (m_dynamicCapabilities.isRegistered( DidChangeWorkspaceFoldersNotification::methodName).value_or(false)) { return true; } if (auto workspace = m_serverCapabilities.workspace()) { if (auto folder = workspace.value().workspaceFolders()) { if (folder.value().supported().value_or(false)) { // holds either the Id for deregistration or whether it is registered auto notification = folder.value().changeNotifications().value_or(false); return holds_alternative(notification) || (holds_alternative(notification) && get(notification)); } } } return false; } #endif } // namespace LanguageClient diff --git a/addons/project/lsp/languageclient/client.h b/addons/project/lsp/languageclient/client.h index 537683549..6dee27f44 100644 --- a/addons/project/lsp/languageclient/client.h +++ b/addons/project/lsp/languageclient/client.h @@ -1,193 +1,197 @@ /**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #pragma once #include #include #include #include #include #include #include +#include "dynamiccapabilities.h" + #include #include #include #include #include +#include "kateproject.h" + class KateProjectPlugin; namespace LanguageClient { class BaseClientInterface; class Client : public QObject { Q_OBJECT public: explicit Client(KateProjectPlugin *parent, BaseClientInterface *clientInterface); // takes ownership ~Client() override; Client(const Client &) = delete; Client(Client &&) = delete; Client &operator=(const Client &) = delete; Client &operator=(Client &&) = delete; enum State { Uninitialized, InitializeRequested, Initialized, ShutdownRequested, Shutdown, Error }; void initialize(); void shutdown(); State state() const; bool reachable() const { return m_state == Initialized; } bool start(); bool reset(); void setError(const QString &message); void handleMessage(const LanguageServerProtocol::BaseMessage &message); void log(const QString &message); template void log(const LanguageServerProtocol::ResponseError &responseError) { log(responseError.toString()); } signals: void initialized(LanguageServerProtocol::ServerCapabilities capabilities); void finished(); public: void intializeCallback(const LanguageServerProtocol::InitializeRequest::Response &initResponse); void sendContent(const LanguageServerProtocol::IContent &content); void sendContent(const LanguageServerProtocol::DocumentUri &uri, const LanguageServerProtocol::IContent &content); void cancelRequest(const LanguageServerProtocol::MessageId &id); void handleResponse(const LanguageServerProtocol::MessageId &id, const QByteArray &content, QTextCodec *codec); void handleMethod(const QString &method, LanguageServerProtocol::MessageId id, const LanguageServerProtocol::IContent *content); void shutDownCallback(const LanguageServerProtocol::ShutdownRequest::Response &shutdownResponse); + bool findLinkAt(LanguageServerProtocol::GotoDefinitionRequest &request); + bool isSupportedUri(const LanguageServerProtocol::DocumentUri &uri) const; + bool openDocument(KTextEditor::Document *document); + void closeDocument(const LanguageServerProtocol::DidCloseTextDocumentParams ¶ms); #if 0 // document synchronization - bool openDocument(Core::IDocument *document); - void closeDocument(const LanguageServerProtocol::DidCloseTextDocumentParams ¶ms); bool documentOpen(const Core::IDocument *document) const; void documentContentsSaved(Core::IDocument *document); void documentWillSave(Core::IDocument *document); void documentContentsChanged(Core::IDocument *document); void registerCapabilities(const QList ®istrations); void unregisterCapabilities(const QList &unregistrations); - bool findLinkAt(LanguageServerProtocol::GotoDefinitionRequest &request); bool findUsages(LanguageServerProtocol::FindReferencesRequest &request); void requestDocumentSymbols(TextEditor::TextDocument *document); void cursorPositionChanged(TextEditor::TextEditorWidget *widget); void requestCodeActions(const LanguageServerProtocol::DocumentUri &uri, const QList &diagnostics); void requestCodeActions(const LanguageServerProtocol::CodeActionRequest &request); void handleCodeActionResponse(const LanguageServerProtocol::CodeActionRequest::Response &response, const LanguageServerProtocol::DocumentUri &uri); void executeCommand(const LanguageServerProtocol::Command &command); // workspace control void projectOpened(ProjectExplorer::Project *project); void projectClosed(ProjectExplorer::Project *project); void setSupportedLanguage(const LanguageFilter &filter); bool isSupportedDocument(const Core::IDocument *document) const; bool isSupportedFile(const Utils::FileName &filePath, const QString &mimeType) const; - bool isSupportedUri(const LanguageServerProtocol::DocumentUri &uri) const; void setName(const QString &name) { m_displayName = name; } QString name() const { return m_displayName; } Core::Id id() const { return m_id; } bool needsRestart(const BaseSettings *) const; QList diagnosticsAt( const LanguageServerProtocol::DocumentUri &uri, const LanguageServerProtocol::Range &range) const; const LanguageServerProtocol::ServerCapabilities &capabilities() const; const DynamicCapabilities &dynamicCapabilities() const; const BaseClientInterface *clientInterface() const; DocumentSymbolCache *documentSymbolCache(); private: void handleDiagnostics(const LanguageServerProtocol::PublishDiagnosticsParams ¶ms); bool sendWorkspceFolderChanges() const; void log(const LanguageServerProtocol::ShowMessageParams &message, Core::MessageManager::PrintToOutputPaneFlag flag = Core::MessageManager::NoModeSwitch); void showMessageBox(const LanguageServerProtocol::ShowMessageRequestParams &message, const LanguageServerProtocol::MessageId &id); void showDiagnostics(const LanguageServerProtocol::DocumentUri &uri); void removeDiagnostics(const LanguageServerProtocol::DocumentUri &uri); LanguageFilter m_languagFilter; QList m_openedDocument; Core::Id m_id; - DynamicCapabilities m_dynamicCapabilities; LanguageClientCompletionAssistProvider m_completionProvider; LanguageClientQuickFixProvider m_quickFixProvider; QSet m_resetAssistProvider; QHash m_highlightRequests; QMap> m_diagnostics; DocumentSymbolCache m_documentSymbolCache; #endif /** * the project plugin we belong to * allows access to signals for project management */ KateProjectPlugin * const m_projectPlugin = nullptr; + DynamicCapabilities m_dynamicCapabilities; int m_restartsLeft = 5; QString m_displayName; QHash m_responseHandlers; QScopedPointer m_clientInterface; State m_state = Uninitialized; LanguageServerProtocol::ServerCapabilities m_serverCapabilities; using ContentHandler = std::function; QHash m_contentHandler; }; } // namespace LanguageClient diff --git a/addons/project/lsp/languageclient/dynamiccapabilities.cpp b/addons/project/lsp/languageclient/dynamiccapabilities.cpp new file mode 100644 index 000000000..1b0aef037 --- /dev/null +++ b/addons/project/lsp/languageclient/dynamiccapabilities.cpp @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "dynamiccapabilities.h" + +using namespace LanguageServerProtocol; + +namespace LanguageClient { + +void DynamicCapabilities::registerCapability(const QList ®istrations) +{ + for (const Registration& registration : registrations) { + const QString &method = registration.method(); + m_capability[method].enable(registration.id(), registration.registerOptions()); + m_methodForId.insert(registration.id(), method); + } +} + +void DynamicCapabilities::unregisterCapability(const QList &unregistrations) +{ + for (const Unregistration& unregistration : unregistrations) { + QString method = unregistration.method(); + if (method.isEmpty()) + method = m_methodForId[unregistration.id()]; + m_capability[method].disable(); + m_methodForId.remove(unregistration.id()); + } +} + +Utils::optional DynamicCapabilities::isRegistered(const QString &method) const +{ + if (!m_capability.contains(method)) + return Utils::nullopt; + return m_capability[method].enabled(); +} + +void DynamicCapabilities::reset() +{ + m_capability.clear(); + m_methodForId.clear(); +} + +} // namespace LanguageClient diff --git a/addons/project/lsp/languageclient/dynamiccapabilities.h b/addons/project/lsp/languageclient/dynamiccapabilities.h new file mode 100644 index 000000000..9abee3eac --- /dev/null +++ b/addons/project/lsp/languageclient/dynamiccapabilities.h @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include + +namespace LanguageClient { + +class DynamicCapability +{ +public: + DynamicCapability() = default; + void enable(const QString &id, const QJsonValue &options) + { + QTC_CHECK(!m_enabled); + m_enabled = true; + m_id = id; + m_options = options; + } + + void disable() + { + m_enabled = true; + m_id.clear(); + m_options = QJsonValue(); + } + + bool enabled() const { return m_enabled; } + + QJsonValue options() const { return m_options; } + +private: + bool m_enabled = false; + QString m_id; + QJsonValue m_options; + +}; + +class DynamicCapabilities +{ +public: + DynamicCapabilities() = default; + + void registerCapability(const QList ®istrations); + void unregisterCapability(const QList &unregistrations); + + Utils::optional isRegistered(const QString &method) const; + QJsonValue option(const QString &method) const { return m_capability[method].options(); } + + void reset(); + +private: + QHash m_capability; + QHash m_methodForId; +}; + +} // namespace LanguageClient diff --git a/addons/project/lsp/languageclient/languageclientmanager.cpp b/addons/project/lsp/languageclient/languageclientmanager.cpp index cb85d1039..02202db01 100644 --- a/addons/project/lsp/languageclient/languageclientmanager.cpp +++ b/addons/project/lsp/languageclient/languageclientmanager.cpp @@ -1,417 +1,412 @@ /**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "languageclientmanager.h" #include "client.h" #include "languageclientinterface.h" #include "kateprojectplugin.h" #include #include #include #include #include using namespace LanguageServerProtocol; namespace LanguageClient { LanguageClientManager::LanguageClientManager(KateProjectPlugin *parent) : m_projectPlugin(parent) { JsonRpcMessageHandler::registerMessageProvider(); JsonRpcMessageHandler::registerMessageProvider(); JsonRpcMessageHandler::registerMessageProvider(); JsonRpcMessageHandler::registerMessageProvider(); JsonRpcMessageHandler::registerMessageProvider(); JsonRpcMessageHandler::registerMessageProvider(); /** * we watch for new projects * first project creation will trigger client starting */ connect(m_projectPlugin, &KateProjectPlugin::projectCreated, this, &LanguageClientManager::projectCreated); } bool LanguageClientManager::initClients() { // do this once if (m_clientsInited) return false; m_clientsInited = true; // we shall arrive here not before we have some projects around to ensure we have some initial root Q_ASSERT(!m_projectPlugin->projects().isEmpty()); /** * start some clients, later make this configurable */ startClient(new LanguageClient::Client (m_projectPlugin, new LanguageClient::StdIOClientInterface (QStringLiteral("clangd"), QString()))); // ccls needs initial rootUri, bad startClient(new LanguageClient::Client (new LanguageClient::StdIOClientInterface (QStringLiteral("ccls"), QString()))); return true; } LanguageClientManager::~LanguageClientManager() { // FIXME: atm we just delete all clients, we shall do proper shutdown sequence //qDeleteAll(m_clients); } void LanguageClientManager::startClient(Client *client) { QTC_ASSERT(client, return); if (m_shuttingDown) { clientFinished(client); return; } if (!m_clients.contains(client)) m_clients.append(client); connect(client, &Client::finished, this, [this,client](){ clientFinished(client); }); if (client->start()) client->initialize(); else clientFinished(client); } QVector LanguageClientManager::clients() { return m_clients; } void LanguageClientManager::addExclusiveRequest(const MessageId &id, Client *client) { m_exclusiveRequests[id] << client; } void LanguageClientManager::reportFinished(const MessageId &id, Client *byClient) { for (Client *client : m_exclusiveRequests[id]) { if (client != byClient) client->cancelRequest(id); } m_exclusiveRequests.remove(id); } void LanguageClientManager::deleteClient(Client *client) { QTC_ASSERT(client, return); client->disconnect(); m_clients.removeAll(client); if (m_shuttingDown) delete client; else client->deleteLater(); } void LanguageClientManager::shutdown() { if (m_shuttingDown) return; m_shuttingDown = true; for (auto interface : m_clients) { if (interface->reachable()) interface->shutdown(); else deleteClient(interface); } QTimer::singleShot(3000, this, [this](){ for (auto interface : m_clients) deleteClient(interface); emit shutdownFinished(); }); } QVector LanguageClientManager::reachableClients() { return Utils::filtered(m_clients, &Client::reachable); } static void sendToInterfaces(const IContent &content, const QVector &interfaces) { for (Client *interface : interfaces) interface->sendContent(content); } void LanguageClientManager::sendToAllReachableServers(const IContent &content) { sendToInterfaces(content, reachableClients()); } void LanguageClientManager::clientFinished(Client *client) { constexpr int restartTimeoutS = 5; const bool unexpectedFinish = client->state() != Client::Shutdown && client->state() != Client::ShutdownRequested; if (unexpectedFinish && !m_shuttingDown && client->reset()) { client->disconnect(this); client->log(tr("Unexpectedly finished. Restarting in %1 seconds.").arg(restartTimeoutS)); QTimer::singleShot(restartTimeoutS * 1000, client, [this, client](){ startClient(client); }); } else { if (unexpectedFinish && !m_shuttingDown) client->log(tr("Unexpectedly finished.")); deleteClient(client); if (m_shuttingDown && m_clients.isEmpty()) emit shutdownFinished(); } } +void LanguageClientManager::findLinkAt(KTextEditor::Document *doc, const KTextEditor::Cursor &cursor, + Utils::ProcessLinkCallback callback) +{ + const DocumentUri uri = DocumentUri::fromProtocol(doc->url().toString()); + const TextDocumentIdentifier document(uri); + const Position pos(cursor.line(), cursor.column()); + TextDocumentPositionParams params(document, pos); + GotoDefinitionRequest request(params); + request.setResponseCallback([callback](const GotoDefinitionRequest::Response &response){ + if (Utils::optional _result = response.result()) { + const GotoResult result = _result.value(); + if (Utils::holds_alternative(result)) + return; + if (auto ploc = Utils::get_if(&result)) { + callback(ploc->toLink()); + } else if (auto plloc = Utils::get_if>(&result)) { + if (!plloc->isEmpty()) + callback(plloc->value(0).toLink()); + } + } + }); + for (Client *interface : reachableClients()) { + if (interface->findLinkAt(request)) + m_exclusiveRequests[request.id()] << interface; + } +} + +void LanguageClientManager::documentOpened(KTextEditor::Document *document) +{ + for (Client *interface : reachableClients()) + interface->openDocument(document); +} + +void LanguageClientManager::documentClosed(KTextEditor::Document *document) +{ + const DidCloseTextDocumentParams params( + TextDocumentIdentifier(DocumentUri::fromProtocol(document->url().toString()))); + for (Client *interface : reachableClients()) + interface->closeDocument(params); +} + #if 0 QList LanguageClientManager::clientsSupportingDocument( const TextEditor::TextDocument *doc) { QTC_ASSERT(managerInstance, return {}); QTC_ASSERT(doc, return {};); return Utils::filtered(managerInstance->reachableClients(), [doc](Client *client) { return client->isSupportedDocument(doc); }).toList(); } void LanguageClientManager::applySettings() { QTC_ASSERT(managerInstance, return); qDeleteAll(managerInstance->m_currentSettings); managerInstance->m_currentSettings = Utils::transform(LanguageClientSettings::currentPageSettings(), [](BaseSettings *settings) { return settings->copy(); }); LanguageClientSettings::toSettings(Core::ICore::settings(), managerInstance->m_currentSettings); const QList restarts = Utils::filtered(managerInstance->m_currentSettings, [](BaseSettings *settings) { return settings->needsRestart(); }); for (BaseSettings *setting : restarts) { if (auto client = clientForSetting(setting)) { if (client->reachable()) client->shutdown(); else deleteClient(client); } if (setting->canStartClient()) startClient(setting); } } QList LanguageClientManager::currentSettings() { QTC_ASSERT(managerInstance, return {}); return managerInstance->m_currentSettings; } Client *LanguageClientManager::clientForSetting(const BaseSettings *setting) { QTC_ASSERT(managerInstance, return nullptr); return managerInstance->m_clientsForSetting.value(setting->m_id, nullptr); } void LanguageClientManager::editorOpened(Core::IEditor *editor) { using namespace TextEditor; if (auto *textEditor = qobject_cast(editor)) { if (TextEditorWidget *widget = textEditor->editorWidget()) { connect(widget, &TextEditorWidget::requestLinkAt, this, [this, filePath = editor->document()->filePath()] (const QTextCursor &cursor, Utils::ProcessLinkCallback &callback) { findLinkAt(filePath, cursor, callback); }); connect(widget, &TextEditorWidget::requestUsages, this, [this, filePath = editor->document()->filePath()] (const QTextCursor &cursor){ findUsages(filePath, cursor); }); connect(widget, &TextEditorWidget::cursorPositionChanged, this, [this, widget](){ // TODO This would better be a compressing timer QTimer::singleShot(50, this, [this, widget = QPointer(widget)]() { if (widget) { for (Client *client : this->reachableClients()) { if (client->isSupportedDocument(widget->textDocument())) client->cursorPositionChanged(widget); } } }); }); updateEditorToolBar(editor); } } } -void LanguageClientManager::documentOpened(Core::IDocument *document) -{ - for (BaseSettings *setting : LanguageClientSettings::currentPageSettings()) { - if (clientForSetting(setting) == nullptr && setting->canStartClient()) - startClient(setting); - } - for (Client *interface : reachableClients()) - interface->openDocument(document); -} - -void LanguageClientManager::documentClosed(Core::IDocument *document) -{ - const DidCloseTextDocumentParams params( - TextDocumentIdentifier(DocumentUri::fromFileName(document->filePath()))); - for (Client *interface : reachableClients()) - interface->closeDocument(params); -} - void LanguageClientManager::documentContentsSaved(Core::IDocument *document) { for (Client *interface : reachableClients()) interface->documentContentsSaved(document); } void LanguageClientManager::documentWillSave(Core::IDocument *document) { for (Client *interface : reachableClients()) interface->documentContentsSaved(document); } -void LanguageClientManager::findLinkAt(const Utils::FileName &filePath, - const QTextCursor &cursor, - Utils::ProcessLinkCallback callback) -{ - const DocumentUri uri = DocumentUri::fromFileName(filePath); - const TextDocumentIdentifier document(uri); - const Position pos(cursor); - TextDocumentPositionParams params(document, pos); - GotoDefinitionRequest request(params); - request.setResponseCallback([callback](const GotoDefinitionRequest::Response &response){ - if (Utils::optional _result = response.result()) { - const GotoResult result = _result.value(); - if (Utils::holds_alternative(result)) - return; - if (auto ploc = Utils::get_if(&result)) { - callback(ploc->toLink()); - } else if (auto plloc = Utils::get_if>(&result)) { - if (!plloc->isEmpty()) - callback(plloc->value(0).toLink()); - } - } - }); - for (Client *interface : reachableClients()) { - if (interface->findLinkAt(request)) - m_exclusiveRequests[request.id()] << interface; - } -} - QList generateSearchResultItems(const LanguageClientArray &locations) { auto convertPosition = [](const Position &pos){ return Core::Search::TextPosition(pos.line() + 1, pos.character()); }; auto convertRange = [convertPosition](const Range &range){ return Core::Search::TextRange(convertPosition(range.start()), convertPosition(range.end())); }; QList result; if (locations.isNull()) return result; QMap> rangesInDocument; for (const Location &location : locations.toList()) rangesInDocument[location.uri().toFileName().toString()] << convertRange(location.range()); for (auto it = rangesInDocument.begin(); it != rangesInDocument.end(); ++it) { const QString &fileName = it.key(); QFile file(fileName); file.open(QFile::ReadOnly); Core::SearchResultItem item; item.path = QStringList() << fileName; item.useTextEditorFont = true; QStringList lines = QString::fromLocal8Bit(file.readAll()).split(QChar::LineFeed); for (const Core::Search::TextRange &range : it.value()) { item.mainRange = range; if (file.isOpen() && range.begin.line > 0 && range.begin.line <= lines.size()) item.text = lines[range.begin.line - 1]; else item.text.clear(); result << item; } } return result; } void LanguageClientManager::findUsages(const Utils::FileName &filePath, const QTextCursor &cursor) { const DocumentUri uri = DocumentUri::fromFileName(filePath); const TextDocumentIdentifier document(uri); const Position pos(cursor); QTextCursor termCursor(cursor); termCursor.select(QTextCursor::WordUnderCursor); ReferenceParams params(TextDocumentPositionParams(document, pos)); params.setContext(ReferenceParams::ReferenceContext(true)); FindReferencesRequest request(params); auto callback = [wordUnderCursor = termCursor.selectedText()] (const QString &clientName, const FindReferencesRequest::Response &response){ if (auto result = response.result()) { Core::SearchResult *search = Core::SearchResultWindow::instance()->startNewSearch( tr("Find References with %1 for:").arg(clientName), "", wordUnderCursor); search->addResults(generateSearchResultItems(result.value()), Core::SearchResult::AddOrdered); QObject::connect(search, &Core::SearchResult::activated, [](const Core::SearchResultItem& item) { Core::EditorManager::openEditorAtSearchResult(item); }); search->finishSearch(false); search->popup(); } }; for (Client *client : reachableClients()) { request.setResponseCallback([callback, clientName = client->name()] (const FindReferencesRequest::Response &response){ callback(clientName, response); }); if (client->findUsages(request)) m_exclusiveRequests[request.id()] << client; } } void LanguageClientManager::projectAdded(ProjectExplorer::Project *project) { for (Client *interface : reachableClients()) interface->projectOpened(project); } void LanguageClientManager::projectRemoved(ProjectExplorer::Project *project) { for (Client *interface : reachableClients()) interface->projectClosed(project); } #endif void LanguageClientManager::projectCreated(KateProject *project) { // init LSP clients if not already done, that will take care of the new project, too, if successful if (initClients()) return; } } // namespace LanguageClient diff --git a/addons/project/lsp/languageclient/languageclientmanager.h b/addons/project/lsp/languageclient/languageclientmanager.h index cdafde6f2..96ecc59ea 100644 --- a/addons/project/lsp/languageclient/languageclientmanager.h +++ b/addons/project/lsp/languageclient/languageclientmanager.h @@ -1,116 +1,117 @@ /**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #pragma once #include "client.h" #include #include #include #include "kateproject.h" class KateProjectPlugin; namespace LanguageClient { class LanguageClientMark; class LanguageClientManager : public QObject { Q_OBJECT public: LanguageClientManager(KateProjectPlugin *parent); LanguageClientManager(const LanguageClientManager &other) = delete; LanguageClientManager(LanguageClientManager &&other) = delete; ~LanguageClientManager() override; void startClient(Client *client); QVector clients(); void addExclusiveRequest(const LanguageServerProtocol::MessageId &id, Client *client); void reportFinished(const LanguageServerProtocol::MessageId &id, Client *byClient); void deleteClient(Client *client); void shutdown(); + void findLinkAt(KTextEditor::Document *document, const KTextEditor::Cursor &cursor, + Utils::ProcessLinkCallback callback); + #if 0 static QList clientsSupportingDocument(const TextEditor::TextDocument *doc); static void applySettings(); static QList currentSettings(); static Client *clientForSetting(const BaseSettings *setting); #endif signals: void shutdownFinished(); -private Q_SLOTS: +public Q_SLOTS: /** * A new project got created. * @param project new created project */ void projectCreated(KateProject *project); + void documentOpened(KTextEditor::Document *document); + void documentClosed(KTextEditor::Document *document); private: bool initClients(); #if 0 void editorOpened(Core::IEditor *editor); - void documentOpened(Core::IDocument *document); - void documentClosed(Core::IDocument *document); void documentContentsSaved(Core::IDocument *document); void documentWillSave(Core::IDocument *document); - void findLinkAt(const Utils::FileName &filePath, const QTextCursor &cursor, - Utils::ProcessLinkCallback callback); void findUsages(const Utils::FileName &filePath, const QTextCursor &cursor); void projectAdded(ProjectExplorer::Project *project); void projectRemoved(ProjectExplorer::Project *project); #endif /** * the project plugin we belong to * allows access to signals for project management */ KateProjectPlugin * const m_projectPlugin = nullptr; /** * clients inited? */ bool m_clientsInited = false; QVector reachableClients(); void sendToAllReachableServers(const LanguageServerProtocol::IContent &content); void clientFinished(Client *client); bool m_shuttingDown = false; QVector m_clients; QHash> m_exclusiveRequests; }; } // namespace LanguageClient