LSP: collect and execute quick fixes via shortcut

Fixes: QTCREATORBUG-21802
Change-Id: I611fac1c3fc5b094816441e36492ed57706c98b8
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
David Schulz
2019-01-29 13:20:58 +01:00
parent 0ec8a255d9
commit c6415e2652
12 changed files with 313 additions and 30 deletions

View File

@@ -27,14 +27,15 @@
#include "lsputils.h" #include "lsputils.h"
#include <utils/mimetypes/mimedatabase.h> #include <utils/mimetypes/mimedatabase.h>
#include <utils/textutils.h>
#include <QFile>
#include <QHash> #include <QHash>
#include <QTextBlock>
#include <QTextDocument>
#include <QJsonArray> #include <QJsonArray>
#include <QMap> #include <QMap>
#include <QTextBlock>
#include <QTextDocument>
#include <QVector> #include <QVector>
#include <QFile>
namespace LanguageServerProtocol { namespace LanguageServerProtocol {
@@ -340,6 +341,24 @@ Range::Range(const Position &start, const Position &end)
setEnd(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 bool DocumentFilter::applies(const Utils::FileName &fileName, const Utils::MimeType &mimeType) const
{ {
if (Utils::optional<QString> _scheme = scheme()) { if (Utils::optional<QString> _scheme = scheme()) {

View File

@@ -100,6 +100,7 @@ class LANGUAGESERVERPROTOCOL_EXPORT Range : public JsonObject
public: public:
Range() = default; Range() = default;
Range(const Position &start, const Position &end); Range(const Position &start, const Position &end);
explicit Range(const QTextCursor &cursor);
using JsonObject::JsonObject; using JsonObject::JsonObject;
// The range's start position. // The range's start position.
@@ -111,6 +112,7 @@ public:
void setEnd(const Position &end) { insert(endKey, end); } void setEnd(const Position &end) { insert(endKey, end); }
bool contains(const Position &pos) const { return start() <= pos && pos <= end(); } bool contains(const Position &pos) const { return start() <= pos && pos <= end(); }
bool overlaps(const Range &range) const;
bool isValid(QStringList *error) const override bool isValid(QStringList *error) const override
{ return check<Position>(error, startKey) && check<Position>(error, endKey); } { return check<Position>(error, startKey) && check<Position>(error, endKey); }

View File

@@ -69,6 +69,7 @@ class TextMark : public TextEditor::TextMark
public: public:
TextMark(const Utils::FileName &fileName, const Diagnostic &diag) TextMark(const Utils::FileName &fileName, const Diagnostic &diag)
: TextEditor::TextMark(fileName, diag.range().start().line() + 1, "lspmark") : TextEditor::TextMark(fileName, diag.range().start().line() + 1, "lspmark")
, m_diagnostic(diag)
{ {
using namespace Utils; using namespace Utils;
setLineAnnotation(diag.message()); setLineAnnotation(diag.message());
@@ -81,11 +82,17 @@ public:
setIcon(isError ? Icons::CODEMODEL_ERROR.icon() setIcon(isError ? Icons::CODEMODEL_ERROR.icon()
: Icons::CODEMODEL_WARNING.icon()); : Icons::CODEMODEL_WARNING.icon());
} }
const Diagnostic &diagnostic() const { return m_diagnostic; }
private:
const Diagnostic m_diagnostic;
}; };
Client::Client(BaseClientInterface *clientInterface) Client::Client(BaseClientInterface *clientInterface)
: m_id(Core::Id::fromString(QUuid::createUuid().toString())) : m_id(Core::Id::fromString(QUuid::createUuid().toString()))
, m_completionProvider(this) , m_completionProvider(this)
, m_quickFixProvider(this)
, m_clientInterface(clientInterface) , m_clientInterface(clientInterface)
{ {
m_contentHandler.insert(JsonRpcMessageHandler::jsonRpcMimeType(), m_contentHandler.insert(JsonRpcMessageHandler::jsonRpcMimeType(),
@@ -101,8 +108,10 @@ Client::~Client()
using namespace TextEditor; using namespace TextEditor;
// FIXME: instead of replacing the completion provider in the text document store the // FIXME: instead of replacing the completion provider in the text document store the
// completion provider as a prioritised list in the text document // completion provider as a prioritised list in the text document
for (TextDocument *document : m_resetCompletionProvider) for (TextDocument *document : m_resetAssistProvider) {
document->setCompletionAssistProvider(nullptr); document->setCompletionAssistProvider(nullptr);
document->setQuickFixAssistProvider(nullptr);
}
for (Core::IEditor * editor : Core::DocumentModel::editorsForOpenedDocuments()) { for (Core::IEditor * editor : Core::DocumentModel::editorsForOpenedDocuments()) {
if (auto textEditor = qobject_cast<BaseTextEditor *>(editor)) { if (auto textEditor = qobject_cast<BaseTextEditor *>(editor)) {
TextEditorWidget *widget = textEditor->editorWidget(); TextEditorWidget *widget = textEditor->editorWidget();
@@ -192,10 +201,12 @@ void Client::openDocument(Core::IDocument *document)
documentContentsChanged(document); documentContentsChanged(document);
}); });
if (textDocument) { if (textDocument) {
m_resetCompletionProvider << textDocument; textDocument->completionAssistProvider();
m_resetAssistProvider << textDocument;
textDocument->setCompletionAssistProvider(&m_completionProvider); textDocument->setCompletionAssistProvider(&m_completionProvider);
textDocument->setQuickFixAssistProvider(&m_quickFixProvider);
connect(textDocument, &QObject::destroyed, this, [this, textDocument]{ connect(textDocument, &QObject::destroyed, this, [this, textDocument]{
m_resetCompletionProvider.remove(textDocument); m_resetAssistProvider.remove(textDocument);
}); });
if (BaseTextEditor *editor = BaseTextEditor::textEditorForDocument(textDocument)) { if (BaseTextEditor *editor = BaseTextEditor::textEditorForDocument(textDocument)) {
if (QPointer<TextEditorWidget> widget = editor->editorWidget()) { if (QPointer<TextEditorWidget> widget = editor->editorWidget()) {
@@ -547,21 +558,6 @@ void Client::requestCodeActions(const DocumentUri &uri, const QList<Diagnostic>
if (!doc) if (!doc)
return; return;
const QString method(CodeActionRequest::methodName);
if (Utils::optional<bool> 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<bool, CodeActionOptions> provider
= m_serverCapabilities.codeActionProvider().value_or(false);
if (!(Utils::holds_alternative<CodeActionOptions>(provider) || Utils::get<bool>(provider)))
return;
}
CodeActionParams codeActionParams; CodeActionParams codeActionParams;
CodeActionParams::CodeActionContext context; CodeActionParams::CodeActionContext context;
context.setDiagnostics(diagnostics); context.setDiagnostics(diagnostics);
@@ -577,6 +573,32 @@ void Client::requestCodeActions(const DocumentUri &uri, const QList<Diagnostic>
if (self) if (self)
self->handleCodeActionResponse(response, uri); 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<bool> 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<bool, CodeActionOptions> provider
= m_serverCapabilities.codeActionProvider().value_or(false);
if (!(Utils::holds_alternative<CodeActionOptions>(provider) || Utils::get<bool>(provider)))
return;
}
sendContent(request); sendContent(request);
} }
@@ -682,6 +704,17 @@ bool Client::needsRestart(const BaseSettings *settings) const
|| m_languagFilter.filePattern != settings->m_languageFilter.filePattern; || m_languagFilter.filePattern != settings->m_languageFilter.filePattern;
} }
QList<Diagnostic> Client::diagnosticsAt(const DocumentUri &uri, const Range &range) const
{
QList<Diagnostic> 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() bool Client::start()
{ {
return m_clientInterface->start(); return m_clientInterface->start();

View File

@@ -26,7 +26,8 @@
#pragma once #pragma once
#include "dynamiccapabilities.h" #include "dynamiccapabilities.h"
#include "languageclientcodeassist.h" #include "languageclientcompletionassist.h"
#include "languageclientquickfix.h"
#include "languageclientsettings.h" #include "languageclientsettings.h"
#include <coreplugin/id.h> #include <coreplugin/id.h>
@@ -104,6 +105,7 @@ public:
void requestCodeActions(const LanguageServerProtocol::DocumentUri &uri, void requestCodeActions(const LanguageServerProtocol::DocumentUri &uri,
const QList<LanguageServerProtocol::Diagnostic> &diagnostics); const QList<LanguageServerProtocol::Diagnostic> &diagnostics);
void requestCodeActions(const LanguageServerProtocol::CodeActionRequest &request);
void handleCodeActionResponse(const LanguageServerProtocol::CodeActionRequest::Response &response, void handleCodeActionResponse(const LanguageServerProtocol::CodeActionRequest::Response &response,
const LanguageServerProtocol::DocumentUri &uri); const LanguageServerProtocol::DocumentUri &uri);
void executeCommand(const LanguageServerProtocol::Command &command); void executeCommand(const LanguageServerProtocol::Command &command);
@@ -129,6 +131,10 @@ public:
bool needsRestart(const BaseSettings *) const; bool needsRestart(const BaseSettings *) const;
QList<LanguageServerProtocol::Diagnostic> diagnosticsAt(
const LanguageServerProtocol::DocumentUri &uri,
const LanguageServerProtocol::Range &range) const;
bool start(); bool start();
bool reset(); bool reset();
@@ -184,7 +190,8 @@ private:
LanguageServerProtocol::ServerCapabilities m_serverCapabilities; LanguageServerProtocol::ServerCapabilities m_serverCapabilities;
DynamicCapabilities m_dynamicCapabilities; DynamicCapabilities m_dynamicCapabilities;
LanguageClientCompletionAssistProvider m_completionProvider; LanguageClientCompletionAssistProvider m_completionProvider;
QSet<TextEditor::TextDocument *> m_resetCompletionProvider; LanguageClientQuickFixProvider m_quickFixProvider;
QSet<TextEditor::TextDocument *> m_resetAssistProvider;
QHash<LanguageServerProtocol::DocumentUri, LanguageServerProtocol::MessageId> m_highlightRequests; QHash<LanguageServerProtocol::DocumentUri, LanguageServerProtocol::MessageId> m_highlightRequests;
int m_restartsLeft = 5; int m_restartsLeft = 5;
QScopedPointer<BaseClientInterface> m_clientInterface; QScopedPointer<BaseClientInterface> m_clientInterface;

View File

@@ -6,11 +6,12 @@ HEADERS += \
client.h \ client.h \
dynamiccapabilities.h \ dynamiccapabilities.h \
languageclient_global.h \ languageclient_global.h \
languageclientcodeassist.h \ languageclientcompletionassist.h \
languageclientinterface.h \ languageclientinterface.h \
languageclientmanager.h \ languageclientmanager.h \
languageclientoutline.h \ languageclientoutline.h \
languageclientplugin.h \ languageclientplugin.h \
languageclientquickfix.h \
languageclientsettings.h \ languageclientsettings.h \
languageclientutils.h languageclientutils.h
@@ -18,11 +19,12 @@ HEADERS += \
SOURCES += \ SOURCES += \
client.cpp \ client.cpp \
dynamiccapabilities.cpp \ dynamiccapabilities.cpp \
languageclientcodeassist.cpp \ languageclientcompletionassist.cpp \
languageclientinterface.cpp \ languageclientinterface.cpp \
languageclientmanager.cpp \ languageclientmanager.cpp \
languageclientoutline.cpp \ languageclientoutline.cpp \
languageclientplugin.cpp \ languageclientplugin.cpp \
languageclientquickfix.cpp \
languageclientsettings.cpp \ languageclientsettings.cpp \
languageclientutils.cpp languageclientutils.cpp

View File

@@ -20,16 +20,18 @@ QtcPlugin {
"dynamiccapabilities.h", "dynamiccapabilities.h",
"languageclient.qrc", "languageclient.qrc",
"languageclient_global.h", "languageclient_global.h",
"languageclientcodeassist.cpp",
"languageclientcodeassist.h",
"languageclientinterface.cpp", "languageclientinterface.cpp",
"languageclientinterface.h", "languageclientinterface.h",
"languageclientcompletionassist.cpp",
"languageclientcompletionassist.h",
"languageclientmanager.cpp", "languageclientmanager.cpp",
"languageclientmanager.h", "languageclientmanager.h",
"languageclientoutline.cpp", "languageclientoutline.cpp",
"languageclientoutline.h", "languageclientoutline.h",
"languageclientplugin.cpp", "languageclientplugin.cpp",
"languageclientplugin.h", "languageclientplugin.h",
"languageclientquickfix.cpp",
"languageclientquickfix.h",
"languageclientsettings.cpp", "languageclientsettings.cpp",
"languageclientsettings.h", "languageclientsettings.h",
"languageclientutils.cpp", "languageclientutils.cpp",

View File

@@ -23,7 +23,7 @@
** **
****************************************************************************/ ****************************************************************************/
#include "languageclientcodeassist.h" #include "languageclientcompletionassist.h"
#include "client.h" #include "client.h"
#include "languageclientutils.h" #include "languageclientutils.h"

View File

@@ -48,7 +48,7 @@ public:
private: private:
QList<QString> m_triggerChars; QList<QString> m_triggerChars;
int m_activationCharSequenceLength = 0; int m_activationCharSequenceLength = 0;
Client *m_client; Client *m_client = nullptr; // not owned
}; };
} // namespace LanguageClient } // namespace LanguageClient

View File

@@ -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 <texteditor/codeassist/assistinterface.h>
#include <texteditor/codeassist/genericproposal.h>
#include <texteditor/codeassist/iassistprocessor.h>
#include <texteditor/quickfix.h>
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<WorkspaceEdit> edit = m_action.edit()) {
applyWorkspaceEdit(*edit);
} else if (Utils::optional<Command> command = m_action.command()) {
if (m_client)
m_client->executeCommand(*command);
}
}
private:
CodeAction m_action;
QPointer<Client> 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<Client> 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<const AssistInterface> m_assistInterface;
Client *m_client = nullptr; // not owned
bool m_running = false;
};
IAssistProposal *LanguageClientQuickFixAssistProcessor::perform(const AssistInterface *interface)
{
m_assistInterface = QSharedPointer<const AssistInterface>(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<CodeActionRequest::Response::Error> &error = response.error())
m_client->log(*error);
QuickFixOperations ops;
if (const Utils::optional<CodeActionResult> &_result = response.result()) {
const CodeActionResult &result = _result.value();
if (auto list = Utils::get_if<QList<Utils::variant<Command, CodeAction>>>(&result)) {
for (const Utils::variant<Command, CodeAction> &item : *list) {
if (auto action = Utils::get_if<CodeAction>(&item))
ops << new CodeActionQuickFixOperation(*action, m_client);
else if (auto command = Utils::get_if<Command>(&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

View File

@@ -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 <texteditor/codeassist/iassistprovider.h>
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

View File

@@ -100,6 +100,7 @@ public:
QTextDocument m_document; QTextDocument m_document;
SyntaxHighlighter *m_highlighter = nullptr; SyntaxHighlighter *m_highlighter = nullptr;
CompletionAssistProvider *m_completionAssistProvider = nullptr; CompletionAssistProvider *m_completionAssistProvider = nullptr;
IAssistProvider *m_quickFixProvider = nullptr;
QScopedPointer<Indenter> m_indenter; QScopedPointer<Indenter> m_indenter;
bool m_fileIsReadOnly = false; bool m_fileIsReadOnly = false;
@@ -388,9 +389,14 @@ CompletionAssistProvider *TextDocument::completionAssistProvider() const
return d->m_completionAssistProvider; return d->m_completionAssistProvider;
} }
void TextDocument::setQuickFixAssistProvider(IAssistProvider *provider) const
{
d->m_quickFixProvider = provider;
}
IAssistProvider *TextDocument::quickFixAssistProvider() const IAssistProvider *TextDocument::quickFixAssistProvider() const
{ {
return nullptr; return d->m_quickFixProvider;
} }
void TextDocument::applyFontSettings() void TextDocument::applyFontSettings()

View File

@@ -140,6 +140,7 @@ public:
void setCompletionAssistProvider(CompletionAssistProvider *provider); void setCompletionAssistProvider(CompletionAssistProvider *provider);
virtual CompletionAssistProvider *completionAssistProvider() const; virtual CompletionAssistProvider *completionAssistProvider() const;
void setQuickFixAssistProvider(IAssistProvider *provider) const;
virtual IAssistProvider *quickFixAssistProvider() const; virtual IAssistProvider *quickFixAssistProvider() const;
void setTabSettings(const TextEditor::TabSettings &tabSettings); void setTabSettings(const TextEditor::TabSettings &tabSettings);