From c6415e265234ad7e3d97f23ef48ce7642a05f71e Mon Sep 17 00:00:00 2001 From: David Schulz Date: Tue, 29 Jan 2019 13:20:58 +0100 Subject: [PATCH] LSP: collect and execute quick fixes via shortcut Fixes: QTCREATORBUG-21802 Change-Id: I611fac1c3fc5b094816441e36492ed57706c98b8 Reviewed-by: Eike Ziller --- src/libs/languageserverprotocol/lsptypes.cpp | 25 ++- src/libs/languageserverprotocol/lsptypes.h | 2 + src/plugins/languageclient/client.cpp | 69 ++++++-- src/plugins/languageclient/client.h | 11 +- src/plugins/languageclient/languageclient.pro | 6 +- src/plugins/languageclient/languageclient.qbs | 6 +- ...cpp => languageclientcompletionassist.cpp} | 2 +- ...ist.h => languageclientcompletionassist.h} | 2 +- .../languageclient/languageclientquickfix.cpp | 166 ++++++++++++++++++ .../languageclient/languageclientquickfix.h | 45 +++++ src/plugins/texteditor/textdocument.cpp | 8 +- src/plugins/texteditor/textdocument.h | 1 + 12 files changed, 313 insertions(+), 30 deletions(-) rename src/plugins/languageclient/{languageclientcodeassist.cpp => languageclientcompletionassist.cpp} (99%) rename src/plugins/languageclient/{languageclientcodeassist.h => languageclientcompletionassist.h} (97%) create mode 100644 src/plugins/languageclient/languageclientquickfix.cpp create mode 100644 src/plugins/languageclient/languageclientquickfix.h diff --git a/src/libs/languageserverprotocol/lsptypes.cpp b/src/libs/languageserverprotocol/lsptypes.cpp index 41f5eb07bd6..2b3e2628b37 100644 --- a/src/libs/languageserverprotocol/lsptypes.cpp +++ b/src/libs/languageserverprotocol/lsptypes.cpp @@ -27,14 +27,15 @@ #include "lsputils.h" #include +#include +#include #include -#include -#include #include #include +#include +#include #include -#include namespace LanguageServerProtocol { @@ -340,6 +341,24 @@ Range::Range(const Position &start, const Position &end) setEnd(end); } +Range::Range(const QTextCursor &cursor) +{ + int line, character = 0; + Utils::Text::convertPosition(cursor.document(), cursor.selectionStart(), &line, &character); + if (line <= 0 || character <= 0) + return; + setStart(Position(line - 1, character - 1)); + Utils::Text::convertPosition(cursor.document(), cursor.selectionEnd(), &line, &character); + if (line <= 0 || character <= 0) + return; + setEnd(Position(line - 1, character - 1)); +} + +bool Range::overlaps(const Range &range) const +{ + return contains(range.start()) || contains(range.end()); +} + bool DocumentFilter::applies(const Utils::FileName &fileName, const Utils::MimeType &mimeType) const { if (Utils::optional _scheme = scheme()) { diff --git a/src/libs/languageserverprotocol/lsptypes.h b/src/libs/languageserverprotocol/lsptypes.h index 5175255a13d..9fe9a47a639 100644 --- a/src/libs/languageserverprotocol/lsptypes.h +++ b/src/libs/languageserverprotocol/lsptypes.h @@ -100,6 +100,7 @@ class LANGUAGESERVERPROTOCOL_EXPORT Range : public JsonObject public: Range() = default; Range(const Position &start, const Position &end); + explicit Range(const QTextCursor &cursor); using JsonObject::JsonObject; // The range's start position. @@ -111,6 +112,7 @@ public: void setEnd(const Position &end) { insert(endKey, end); } bool contains(const Position &pos) const { return start() <= pos && pos <= end(); } + bool overlaps(const Range &range) const; bool isValid(QStringList *error) const override { return check(error, startKey) && check(error, endKey); } diff --git a/src/plugins/languageclient/client.cpp b/src/plugins/languageclient/client.cpp index d401f452ac6..5b1022c8086 100644 --- a/src/plugins/languageclient/client.cpp +++ b/src/plugins/languageclient/client.cpp @@ -69,6 +69,7 @@ class TextMark : public TextEditor::TextMark public: TextMark(const Utils::FileName &fileName, const Diagnostic &diag) : TextEditor::TextMark(fileName, diag.range().start().line() + 1, "lspmark") + , m_diagnostic(diag) { using namespace Utils; setLineAnnotation(diag.message()); @@ -81,11 +82,17 @@ public: setIcon(isError ? Icons::CODEMODEL_ERROR.icon() : Icons::CODEMODEL_WARNING.icon()); } + + const Diagnostic &diagnostic() const { return m_diagnostic; } + +private: + const Diagnostic m_diagnostic; }; Client::Client(BaseClientInterface *clientInterface) : m_id(Core::Id::fromString(QUuid::createUuid().toString())) , m_completionProvider(this) + , m_quickFixProvider(this) , m_clientInterface(clientInterface) { m_contentHandler.insert(JsonRpcMessageHandler::jsonRpcMimeType(), @@ -101,8 +108,10 @@ Client::~Client() using namespace TextEditor; // FIXME: instead of replacing the completion provider in the text document store the // completion provider as a prioritised list in the text document - for (TextDocument *document : m_resetCompletionProvider) + for (TextDocument *document : m_resetAssistProvider) { document->setCompletionAssistProvider(nullptr); + document->setQuickFixAssistProvider(nullptr); + } for (Core::IEditor * editor : Core::DocumentModel::editorsForOpenedDocuments()) { if (auto textEditor = qobject_cast(editor)) { TextEditorWidget *widget = textEditor->editorWidget(); @@ -192,10 +201,12 @@ void Client::openDocument(Core::IDocument *document) documentContentsChanged(document); }); if (textDocument) { - m_resetCompletionProvider << textDocument; + textDocument->completionAssistProvider(); + m_resetAssistProvider << textDocument; textDocument->setCompletionAssistProvider(&m_completionProvider); + textDocument->setQuickFixAssistProvider(&m_quickFixProvider); connect(textDocument, &QObject::destroyed, this, [this, textDocument]{ - m_resetCompletionProvider.remove(textDocument); + m_resetAssistProvider.remove(textDocument); }); if (BaseTextEditor *editor = BaseTextEditor::textEditorForDocument(textDocument)) { if (QPointer widget = editor->editorWidget()) { @@ -547,21 +558,6 @@ void Client::requestCodeActions(const DocumentUri &uri, const QList if (!doc) return; - 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; - } - CodeActionParams codeActionParams; CodeActionParams::CodeActionContext context; context.setDiagnostics(diagnostics); @@ -577,6 +573,32 @@ void Client::requestCodeActions(const DocumentUri &uri, const QList 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); } @@ -682,6 +704,17 @@ bool Client::needsRestart(const BaseSettings *settings) const || 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; +} + bool Client::start() { return m_clientInterface->start(); diff --git a/src/plugins/languageclient/client.h b/src/plugins/languageclient/client.h index 3d691f2bd25..2d92424f42d 100644 --- a/src/plugins/languageclient/client.h +++ b/src/plugins/languageclient/client.h @@ -26,7 +26,8 @@ #pragma once #include "dynamiccapabilities.h" -#include "languageclientcodeassist.h" +#include "languageclientcompletionassist.h" +#include "languageclientquickfix.h" #include "languageclientsettings.h" #include @@ -104,6 +105,7 @@ public: 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); @@ -129,6 +131,10 @@ public: bool needsRestart(const BaseSettings *) const; + QList diagnosticsAt( + const LanguageServerProtocol::DocumentUri &uri, + const LanguageServerProtocol::Range &range) const; + bool start(); bool reset(); @@ -184,7 +190,8 @@ private: LanguageServerProtocol::ServerCapabilities m_serverCapabilities; DynamicCapabilities m_dynamicCapabilities; LanguageClientCompletionAssistProvider m_completionProvider; - QSet m_resetCompletionProvider; + LanguageClientQuickFixProvider m_quickFixProvider; + QSet m_resetAssistProvider; QHash m_highlightRequests; int m_restartsLeft = 5; QScopedPointer m_clientInterface; diff --git a/src/plugins/languageclient/languageclient.pro b/src/plugins/languageclient/languageclient.pro index 59c568a75b4..dbae20e1098 100644 --- a/src/plugins/languageclient/languageclient.pro +++ b/src/plugins/languageclient/languageclient.pro @@ -6,11 +6,12 @@ HEADERS += \ client.h \ dynamiccapabilities.h \ languageclient_global.h \ - languageclientcodeassist.h \ + languageclientcompletionassist.h \ languageclientinterface.h \ languageclientmanager.h \ languageclientoutline.h \ languageclientplugin.h \ + languageclientquickfix.h \ languageclientsettings.h \ languageclientutils.h @@ -18,11 +19,12 @@ HEADERS += \ SOURCES += \ client.cpp \ dynamiccapabilities.cpp \ - languageclientcodeassist.cpp \ + languageclientcompletionassist.cpp \ languageclientinterface.cpp \ languageclientmanager.cpp \ languageclientoutline.cpp \ languageclientplugin.cpp \ + languageclientquickfix.cpp \ languageclientsettings.cpp \ languageclientutils.cpp diff --git a/src/plugins/languageclient/languageclient.qbs b/src/plugins/languageclient/languageclient.qbs index 0da6a580766..0c2752ef5eb 100644 --- a/src/plugins/languageclient/languageclient.qbs +++ b/src/plugins/languageclient/languageclient.qbs @@ -20,16 +20,18 @@ QtcPlugin { "dynamiccapabilities.h", "languageclient.qrc", "languageclient_global.h", - "languageclientcodeassist.cpp", - "languageclientcodeassist.h", "languageclientinterface.cpp", "languageclientinterface.h", + "languageclientcompletionassist.cpp", + "languageclientcompletionassist.h", "languageclientmanager.cpp", "languageclientmanager.h", "languageclientoutline.cpp", "languageclientoutline.h", "languageclientplugin.cpp", "languageclientplugin.h", + "languageclientquickfix.cpp", + "languageclientquickfix.h", "languageclientsettings.cpp", "languageclientsettings.h", "languageclientutils.cpp", diff --git a/src/plugins/languageclient/languageclientcodeassist.cpp b/src/plugins/languageclient/languageclientcompletionassist.cpp similarity index 99% rename from src/plugins/languageclient/languageclientcodeassist.cpp rename to src/plugins/languageclient/languageclientcompletionassist.cpp index af0e9ce8920..1d9a10f6e4f 100644 --- a/src/plugins/languageclient/languageclientcodeassist.cpp +++ b/src/plugins/languageclient/languageclientcompletionassist.cpp @@ -23,7 +23,7 @@ ** ****************************************************************************/ -#include "languageclientcodeassist.h" +#include "languageclientcompletionassist.h" #include "client.h" #include "languageclientutils.h" diff --git a/src/plugins/languageclient/languageclientcodeassist.h b/src/plugins/languageclient/languageclientcompletionassist.h similarity index 97% rename from src/plugins/languageclient/languageclientcodeassist.h rename to src/plugins/languageclient/languageclientcompletionassist.h index 1410e67f821..f0ad95bc883 100644 --- a/src/plugins/languageclient/languageclientcodeassist.h +++ b/src/plugins/languageclient/languageclientcompletionassist.h @@ -48,7 +48,7 @@ public: private: QList m_triggerChars; int m_activationCharSequenceLength = 0; - Client *m_client; + Client *m_client = nullptr; // not owned }; } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientquickfix.cpp b/src/plugins/languageclient/languageclientquickfix.cpp new file mode 100644 index 00000000000..def01b17d81 --- /dev/null +++ b/src/plugins/languageclient/languageclientquickfix.cpp @@ -0,0 +1,166 @@ +/**************************************************************************** +** +** 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 "languageclientquickfix.h" + +#include "client.h" +#include "languageclientutils.h" + +#include +#include +#include +#include + + +using namespace LanguageServerProtocol; +using namespace TextEditor; + +namespace LanguageClient { + +class CodeActionQuickFixOperation : public QuickFixOperation +{ +public: + CodeActionQuickFixOperation(const CodeAction &action, Client *client) + : m_action(action) + , m_client(client) + { setDescription(action.title()); } + + void perform() override + { + if (Utils::optional edit = m_action.edit()) { + applyWorkspaceEdit(*edit); + } else if (Utils::optional command = m_action.command()) { + if (m_client) + m_client->executeCommand(*command); + } + } + +private: + CodeAction m_action; + QPointer m_client; +}; + +class CommandQuickFixOperation : public QuickFixOperation +{ +public: + CommandQuickFixOperation(const Command &command, Client *client) + : m_command(command) + , m_client(client) + { setDescription(command.title()); } + void perform() override + { + if (m_client) + m_client->executeCommand(m_command); + } + +private: + Command m_command; + QPointer m_client; +}; + +class LanguageClientQuickFixAssistProcessor : public IAssistProcessor +{ +public: + explicit LanguageClientQuickFixAssistProcessor(Client *client) : m_client(client) {} + bool running() override { return m_running; } + IAssistProposal *perform(const AssistInterface *interface) override; + +private: + void handleCodeActionResponse(const CodeActionRequest::Response &response); + + QSharedPointer m_assistInterface; + Client *m_client = nullptr; // not owned + bool m_running = false; +}; + +IAssistProposal *LanguageClientQuickFixAssistProcessor::perform(const AssistInterface *interface) +{ + m_assistInterface = QSharedPointer(interface); + + CodeActionParams params; + params.setContext({}); + QTextCursor cursor(interface->textDocument()); + cursor.setPosition(interface->position()); + if (cursor.atBlockEnd() || cursor.atBlockStart()) + cursor.select(QTextCursor::LineUnderCursor); + else + cursor.select(QTextCursor::WordUnderCursor); + if (!cursor.hasSelection()) + cursor.select(QTextCursor::LineUnderCursor); + Range range(cursor); + params.setRange(range); + auto uri = DocumentUri::fromFileName(Utils::FileName::fromString(interface->fileName())); + params.setTextDocument(uri); + CodeActionParams::CodeActionContext context; + context.setDiagnostics(m_client->diagnosticsAt(uri, range)); + params.setContext(context); + + CodeActionRequest request(params); + request.setResponseCallback([this](const CodeActionRequest::Response &response){ + handleCodeActionResponse(response); + }); + + m_client->requestCodeActions(request); + m_running = true; + return nullptr; +} + +void LanguageClientQuickFixAssistProcessor::handleCodeActionResponse( + const CodeActionRequest::Response &response) +{ + m_running = false; + if (const Utils::optional &error = response.error()) + m_client->log(*error); + QuickFixOperations ops; + 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)) + ops << new CodeActionQuickFixOperation(*action, m_client); + else if (auto command = Utils::get_if(&item)) + ops << new CommandQuickFixOperation(*command, m_client); + } + } + } + setAsyncProposalAvailable(GenericProposal::createProposal(m_assistInterface.data(), ops)); +} + +LanguageClientQuickFixProvider::LanguageClientQuickFixProvider(Client *client) : m_client(client) +{ + QTC_CHECK(client); +} + +IAssistProvider::RunType LanguageClientQuickFixProvider::runType() const +{ + return Asynchronous; +} + +IAssistProcessor *LanguageClientQuickFixProvider::createProcessor() const +{ + return new LanguageClientQuickFixAssistProcessor(m_client); +} + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientquickfix.h b/src/plugins/languageclient/languageclientquickfix.h new file mode 100644 index 00000000000..d9b322769fe --- /dev/null +++ b/src/plugins/languageclient/languageclientquickfix.h @@ -0,0 +1,45 @@ +/**************************************************************************** +** +** 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 Client; + +class LanguageClientQuickFixProvider : public TextEditor::IAssistProvider +{ +public: + explicit LanguageClientQuickFixProvider(Client *client); + IAssistProvider::RunType runType() const override; + TextEditor::IAssistProcessor *createProcessor() const override; + +private: + Client *m_client = nullptr; // not owned +}; + +} // namespace LanguageClient diff --git a/src/plugins/texteditor/textdocument.cpp b/src/plugins/texteditor/textdocument.cpp index 98096daee9e..60af37cadb2 100644 --- a/src/plugins/texteditor/textdocument.cpp +++ b/src/plugins/texteditor/textdocument.cpp @@ -100,6 +100,7 @@ public: QTextDocument m_document; SyntaxHighlighter *m_highlighter = nullptr; CompletionAssistProvider *m_completionAssistProvider = nullptr; + IAssistProvider *m_quickFixProvider = nullptr; QScopedPointer m_indenter; bool m_fileIsReadOnly = false; @@ -388,9 +389,14 @@ CompletionAssistProvider *TextDocument::completionAssistProvider() const return d->m_completionAssistProvider; } +void TextDocument::setQuickFixAssistProvider(IAssistProvider *provider) const +{ + d->m_quickFixProvider = provider; +} + IAssistProvider *TextDocument::quickFixAssistProvider() const { - return nullptr; + return d->m_quickFixProvider; } void TextDocument::applyFontSettings() diff --git a/src/plugins/texteditor/textdocument.h b/src/plugins/texteditor/textdocument.h index b7c771e7481..580dc0dfeaf 100644 --- a/src/plugins/texteditor/textdocument.h +++ b/src/plugins/texteditor/textdocument.h @@ -140,6 +140,7 @@ public: void setCompletionAssistProvider(CompletionAssistProvider *provider); virtual CompletionAssistProvider *completionAssistProvider() const; + void setQuickFixAssistProvider(IAssistProvider *provider) const; virtual IAssistProvider *quickFixAssistProvider() const; void setTabSettings(const TextEditor::TabSettings &tabSettings);