LSP: add Command and CodeAction support to the language client

Change-Id: I9e86c17b87c6b6aef36bd0ca293d9db40c554aad
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
David Schulz
2019-01-25 09:48:44 +01:00
parent 54437cafc6
commit aa2ad04e04
19 changed files with 546 additions and 47 deletions

View File

@@ -106,6 +106,12 @@ TextDocumentClientCapabilities::TextDocumentClientCapabilities()
setSynchronization(SynchronizationCapabilities()); setSynchronization(SynchronizationCapabilities());
setDocumentSymbol(SymbolCapabilities()); setDocumentSymbol(SymbolCapabilities());
setCompletion(CompletionCapabilities()); setCompletion(CompletionCapabilities());
CodeActionCapabilities cac;
CodeActionCapabilities::CodeActionLiteralSupport literalSupport;
literalSupport.setCodeActionKind(
CodeActionCapabilities::CodeActionLiteralSupport::CodeActionKind(QList<QString>{"*"}));
cac.setCodeActionLiteralSupport(literalSupport);
setCodeAction(cac);
} }
bool TextDocumentClientCapabilities::isValid(QStringList *error) const bool TextDocumentClientCapabilities::isValid(QStringList *error) const
@@ -123,7 +129,7 @@ bool TextDocumentClientCapabilities::isValid(QStringList *error) const
&& checkOptional<DynamicRegistrationCapabilities>(error, definitionKey) && checkOptional<DynamicRegistrationCapabilities>(error, definitionKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, typeDefinitionKey) && checkOptional<DynamicRegistrationCapabilities>(error, typeDefinitionKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, implementationKey) && checkOptional<DynamicRegistrationCapabilities>(error, implementationKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, codeActionKey) && checkOptional<CodeActionCapabilities>(error, codeActionKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, codeLensKey) && checkOptional<DynamicRegistrationCapabilities>(error, codeLensKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, documentLinkKey) && checkOptional<DynamicRegistrationCapabilities>(error, documentLinkKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, colorProviderKey) && checkOptional<DynamicRegistrationCapabilities>(error, colorProviderKey)
@@ -168,4 +174,10 @@ bool TextDocumentClientCapabilities::SignatureHelpCapabilities::isValid(QStringL
&& checkOptional<SignatureHelpCapabilities>(error, signatureInformationKey); && checkOptional<SignatureHelpCapabilities>(error, signatureInformationKey);
} }
bool TextDocumentClientCapabilities::CodeActionCapabilities::isValid(QStringList *errorHierarchy) const
{
return DynamicRegistrationCapabilities::isValid(errorHierarchy)
&& checkOptional<CodeActionLiteralSupport>(errorHierarchy, codeActionLiteralSupportKey);
}
} // namespace LanguageServerProtocol } // namespace LanguageServerProtocol

View File

@@ -355,10 +355,53 @@ public:
{ insert(implementationKey, implementation); } { insert(implementationKey, implementation); }
void clearImplementation() { remove(implementationKey); } void clearImplementation() { remove(implementationKey); }
class CodeActionCapabilities : public DynamicRegistrationCapabilities
{
public:
using DynamicRegistrationCapabilities::DynamicRegistrationCapabilities;
class CodeActionLiteralSupport : public JsonObject
{
public:
using JsonObject::JsonObject;
class CodeActionKind : public JsonObject
{
public:
using JsonObject::JsonObject;
CodeActionKind() : CodeActionKind(QList<QString>()) {}
explicit CodeActionKind(const QList<QString> &kinds) { setValueSet(kinds); }
QList<QString> valueSet() const { return array<QString>(valueSetKey); }
void setValueSet(const QList<QString> &valueSet)
{ insertArray(valueSetKey, valueSet); }
bool isValid(QStringList *errorHierarchy) const override
{ return checkArray<QString>(errorHierarchy, valueSetKey); }
};
CodeActionKind codeActionKind() const
{ return typedValue<CodeActionKind>(codeActionKindKey); }
void setCodeActionKind(const CodeActionKind &codeActionKind)
{ insert(codeActionKindKey, codeActionKind); }
bool isValid(QStringList *errorHierarchy) const override
{ return check<CodeActionKind>(errorHierarchy, codeActionKindKey); }
};
Utils::optional<CodeActionLiteralSupport> codeActionLiteralSupport() const
{ return optionalValue<CodeActionLiteralSupport>(codeActionLiteralSupportKey); }
void setCodeActionLiteralSupport(const CodeActionLiteralSupport &codeActionLiteralSupport)
{ insert(codeActionLiteralSupportKey, codeActionLiteralSupport); }
void clearCodeActionLiteralSupport() { remove(codeActionLiteralSupportKey); }
bool isValid(QStringList *errorHierarchy) const override;
};
// Whether code action supports dynamic registration. // Whether code action supports dynamic registration.
Utils::optional<DynamicRegistrationCapabilities> codeAction() const Utils::optional<CodeActionCapabilities> codeAction() const
{ return optionalValue<DynamicRegistrationCapabilities>(codeActionKey); } { return optionalValue<CodeActionCapabilities>(codeActionKey); }
void setCodeAction(const DynamicRegistrationCapabilities &codeAction) void setCodeAction(const CodeActionCapabilities &codeAction)
{ insert(codeActionKey, codeAction); } { insert(codeActionKey, codeAction); }
void clearCodeAction() { remove(codeActionKey); } void clearCodeAction() { remove(codeActionKey); }

View File

@@ -45,6 +45,9 @@ constexpr char changesKey[] = "changes";
constexpr char characterKey[] = "character"; constexpr char characterKey[] = "character";
constexpr char childrenKey[] = "children"; constexpr char childrenKey[] = "children";
constexpr char codeActionKey[] = "codeAction"; constexpr char codeActionKey[] = "codeAction";
constexpr char codeActionKindKey[] = "codeActionKind";
constexpr char codeActionKindsKey[] = "codeActionKinds";
constexpr char codeActionLiteralSupportKey[] = "codeActionLiteralSupport";
constexpr char codeActionProviderKey[] = "codeActionProvider"; constexpr char codeActionProviderKey[] = "codeActionProvider";
constexpr char codeKey[] = "code"; constexpr char codeKey[] = "code";
constexpr char codeLensKey[] = "codeLens"; constexpr char codeLensKey[] = "codeLens";
@@ -134,6 +137,7 @@ constexpr char nameKey[] = "name";
constexpr char newNameKey[] = "newName"; constexpr char newNameKey[] = "newName";
constexpr char newTextKey[] = "newText"; constexpr char newTextKey[] = "newText";
constexpr char onTypeFormattingKey[] = "onTypeFormatting"; constexpr char onTypeFormattingKey[] = "onTypeFormatting";
constexpr char onlyKey[] = "only";
constexpr char openCloseKey[] = "openClose"; constexpr char openCloseKey[] = "openClose";
constexpr char optionsKey[] = "options"; constexpr char optionsKey[] = "options";
constexpr char parametersKey[] = "params"; constexpr char parametersKey[] = "params";

View File

@@ -105,6 +105,16 @@ DocumentSymbolsRequest::DocumentSymbolsRequest(const DocumentSymbolParams &param
: Request(methodName, params) : Request(methodName, params)
{ } { }
Utils::optional<QList<CodeActionKind> > CodeActionParams::CodeActionContext::only() const
{
return optionalArray<CodeActionKind>(onlyKey);
}
void CodeActionParams::CodeActionContext::setOnly(const QList<CodeActionKind> &only)
{
insertArray(onlyKey, only);
}
bool CodeActionParams::CodeActionContext::isValid(QStringList *error) const bool CodeActionParams::CodeActionContext::isValid(QStringList *error) const
{ {
return checkArray<Diagnostic>(error, diagnosticsKey); return checkArray<Diagnostic>(error, diagnosticsKey);
@@ -380,4 +390,32 @@ SignatureHelpRequest::SignatureHelpRequest(const TextDocumentPositionParams &par
: Request(methodName, params) : Request(methodName, params)
{ } { }
CodeActionResult::CodeActionResult(const QJsonValue &val)
{
using ResultArray = QList<Utils::variant<Command, CodeAction>>;
if (val.isArray()) {
QJsonArray array = val.toArray();
ResultArray result;
for (const QJsonValue &val : array) {
Command command(val);
if (command.isValid(nullptr))
result << command;
else
result << CodeAction(val);
}
emplace<ResultArray>(result);
return;
}
emplace<nullptr_t>(nullptr);
}
bool CodeAction::isValid(QStringList *error) const
{
return check<QString>(error, titleKey)
&& checkOptional<CodeActionKind>(error, codeActionKindKey)
&& checkOptionalArray<Diagnostic>(error, diagnosticsKey)
&& checkOptional<WorkspaceEdit>(error, editKey)
&& checkOptional<Command>(error, commandKey);
}
} // namespace LanguageServerProtocol } // namespace LanguageServerProtocol

View File

@@ -344,12 +344,37 @@ public:
constexpr static const char methodName[] = "textDocument/documentSymbol"; constexpr static const char methodName[] = "textDocument/documentSymbol";
}; };
/**
* The kind of a code action.
*
* Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`.
*
* The set of kinds is open and client needs to announce the kinds it supports to the server during
* initialization.
*/
using CodeActionKind = QString;
/**
* A set of predefined code action kinds
*/
namespace CodeActionKinds {
constexpr char QuickFix[] = "quickfix";
constexpr char Refactor[] = "refactor";
constexpr char RefactorExtract[] = "refactor.extract";
constexpr char RefactorInline[] = "refactor.inline";
constexpr char RefactorRewrite[] = "refactor.rewrite";
constexpr char Source[] = "source";
constexpr char SourceOrganizeImports[] = "source.organizeImports";
}
class LANGUAGESERVERPROTOCOL_EXPORT CodeActionParams : public JsonObject class LANGUAGESERVERPROTOCOL_EXPORT CodeActionParams : public JsonObject
{ {
public: public:
using JsonObject::JsonObject; using JsonObject::JsonObject;
class CodeActionContext : public JsonObject class LANGUAGESERVERPROTOCOL_EXPORT CodeActionContext : public JsonObject
{ {
public: public:
using JsonObject::JsonObject; using JsonObject::JsonObject;
@@ -358,6 +383,10 @@ public:
void setDiagnostics(const QList<Diagnostic> &diagnostics) void setDiagnostics(const QList<Diagnostic> &diagnostics)
{ insertArray(diagnosticsKey, diagnostics); } { insertArray(diagnosticsKey, diagnostics); }
Utils::optional<QList<CodeActionKind>> only() const;
void setOnly(const QList<CodeActionKind> &only);
void clearOnly() { remove(onlyKey); }
bool isValid(QStringList *error) const override; bool isValid(QStringList *error) const override;
}; };
@@ -375,8 +404,45 @@ public:
bool isValid(QStringList *error) const override; bool isValid(QStringList *error) const override;
}; };
class LANGUAGESERVERPROTOCOL_EXPORT CodeAction : public JsonObject
{
public:
using JsonObject::JsonObject;
QString title() const { return typedValue<QString>(titleKey); }
void setTitle(QString title) { insert(titleKey, title); }
Utils::optional<CodeActionKind> kind() const { return optionalValue<CodeActionKind>(kindKey); }
void setKind(const CodeActionKind &kind) { insert(kindKey, kind); }
void clearKind() { remove(kindKey); }
Utils::optional<QList<Diagnostic>> diagnostics() const
{ return optionalArray<Diagnostic>(diagnosticsKey); }
void setDiagnostics(const QList<Diagnostic> &diagnostics)
{ insertArray(diagnosticsKey, diagnostics); }
void clearDiagnostics() { remove(diagnosticsKey); }
Utils::optional<WorkspaceEdit> edit() const { return optionalValue<WorkspaceEdit>(editKey); }
void setEdit(const WorkspaceEdit &edit) { insert(editKey, edit); }
void clearEdit() { remove(editKey); }
Utils::optional<Command> command() const { return optionalValue<Command>(commandKey); }
void setCommand(const Command &command) { insert(commandKey, command); }
void clearCommand() { remove(commandKey); }
bool isValid(QStringList *) const override;
};
class LANGUAGESERVERPROTOCOL_EXPORT CodeActionResult
: public Utils::variant<QList<Utils::variant<Command, CodeAction>>, nullptr_t>
{
public:
using variant::variant;
explicit CodeActionResult(const QJsonValue &val);
};
class LANGUAGESERVERPROTOCOL_EXPORT CodeActionRequest : public Request< class LANGUAGESERVERPROTOCOL_EXPORT CodeActionRequest : public Request<
LanguageClientArray<Command>, std::nullptr_t, CodeActionParams> CodeActionResult, std::nullptr_t, CodeActionParams>
{ {
public: public:
CodeActionRequest(const CodeActionParams &params = CodeActionParams()); CodeActionRequest(const CodeActionParams &params = CodeActionParams());

View File

@@ -75,22 +75,20 @@ bool Diagnostic::isValid(QStringList *error) const
&& check<QString>(error, messageKey); && check<QString>(error, messageKey);
} }
Utils::optional<QMap<QString, QList<TextEdit>>> WorkspaceEdit::changes() const Utils::optional<WorkspaceEdit::Changes> WorkspaceEdit::changes() const
{ {
using Changes = Utils::optional<QMap<QString, QList<TextEdit>>>;
Changes changes;
auto it = find(changesKey); auto it = find(changesKey);
if (it != end()) if (it == end())
return Utils::nullopt; return Utils::nullopt;
QTC_ASSERT(it.value().type() == QJsonValue::Object, return Changes()); QTC_ASSERT(it.value().type() == QJsonValue::Object, return Changes());
QJsonObject changesObject(it.value().toObject()); QJsonObject changesObject(it.value().toObject());
QMap<QString, QList<TextEdit>> changesResult; Changes changesResult;
for (const QString &key : changesObject.keys()) for (const QString &key : changesObject.keys())
changesResult[key] = LanguageClientArray<TextEdit>(changesObject.value(key)).toList(); changesResult[DocumentUri::fromProtocol(key)] = LanguageClientArray<TextEdit>(changesObject.value(key)).toList();
return Utils::make_optional(changesResult); return Utils::make_optional(changesResult);
} }
void WorkspaceEdit::setChanges(const QMap<QString, QList<TextEdit> > &changes) void WorkspaceEdit::setChanges(const Changes &changes)
{ {
QJsonObject changesObject; QJsonObject changesObject;
const auto end = changes.end(); const auto end = changes.end();
@@ -98,7 +96,7 @@ void WorkspaceEdit::setChanges(const QMap<QString, QList<TextEdit> > &changes)
QJsonArray edits; QJsonArray edits;
for (const TextEdit &edit : it.value()) for (const TextEdit &edit : it.value())
edits.append(QJsonValue(edit)); edits.append(QJsonValue(edit));
changesObject.insert(it.key(), edits); changesObject.insert(it.key().toFileName().toString(), edits);
} }
insert(changesKey, changesObject); insert(changesKey, changesObject);
} }
@@ -329,6 +327,13 @@ int Position::toPositionInDocument(QTextDocument *doc) const
return block.position() + character(); return block.position() + character();
} }
QTextCursor Position::toTextCursor(QTextDocument *doc) const
{
QTextCursor cursor(doc);
cursor.setPosition(toPositionInDocument(doc));
return cursor;
}
Range::Range(const Position &start, const Position &end) Range::Range(const Position &start, const Position &end)
{ {
setStart(start); setStart(start);

View File

@@ -86,6 +86,7 @@ public:
{ return check<int>(error, lineKey) && check<int>(error, characterKey); } { return check<int>(error, lineKey) && check<int>(error, characterKey); }
int toPositionInDocument(QTextDocument *doc) const; int toPositionInDocument(QTextDocument *doc) const;
QTextCursor toTextCursor(QTextDocument *doc) const;
}; };
static bool operator<=(const Position &first, const Position &second) static bool operator<=(const Position &first, const Position &second)
@@ -187,14 +188,17 @@ public:
// Title of the command, like `save`. // Title of the command, like `save`.
QString title() const { return typedValue<QString>(titleKey); } QString title() const { return typedValue<QString>(titleKey); }
void setTitle(const QString &title) { insert(titleKey, title); } void setTitle(const QString &title) { insert(titleKey, title); }
void clearTitle() { remove(titleKey); }
// The identifier of the actual command handler. // The identifier of the actual command handler.
QString command() const { return typedValue<QString>(commandKey); } QString command() const { return typedValue<QString>(commandKey); }
void setCommand(const QString &command) { insert(commandKey, command); } void setCommand(const QString &command) { insert(commandKey, command); }
void clearCommand() { remove(commandKey); }
// Arguments that the command handler should be invoked with. // Arguments that the command handler should be invoked with.
Utils::optional<QJsonArray> arguments() const { return typedValue<QJsonArray>(argumentsKey); } Utils::optional<QJsonArray> arguments() const { return typedValue<QJsonArray>(argumentsKey); }
void setArguments(const QJsonObject &arguments) { insert(argumentsKey, arguments); } void setArguments(const QJsonArray &arguments) { insert(argumentsKey, arguments); }
void clearArguments() { remove(argumentsKey); }
bool isValid(QStringList *error) const override bool isValid(QStringList *error) const override
{ return check<QString>(error, titleKey) { return check<QString>(error, titleKey)
@@ -276,8 +280,9 @@ public:
using JsonObject::JsonObject; using JsonObject::JsonObject;
// Holds changes to existing resources. // Holds changes to existing resources.
Utils::optional<QMap<QString, QList<TextEdit>>> changes() const; using Changes = QMap<DocumentUri, QList<TextEdit>>;
void setChanges(const QMap<QString, QList<TextEdit>> &changes); Utils::optional<Changes> changes() const;
void setChanges(const Changes &changes);
/* /*
* An array of `TextDocumentEdit`s to express changes to n different text documents * An array of `TextDocumentEdit`s to express changes to n different text documents

View File

@@ -150,4 +150,13 @@ QJsonArray enumArrayToJsonArray(const QList<T> &values)
return array; return array;
} }
template <typename T>
QList<T> jsonArrayToList(const QJsonArray &array)
{
QList<T> list;
for (const QJsonValue &val : array)
list << fromJsonValue<T>(val);
return list;
}
} // namespace LanguageClient } // namespace LanguageClient

View File

@@ -98,6 +98,19 @@ void ServerCapabilities::setImplementationProvider(
insert(implementationProviderKey, Utils::get<RegistrationOptions>(implementationProvider)); insert(implementationProviderKey, Utils::get<RegistrationOptions>(implementationProvider));
} }
Utils::optional<Utils::variant<bool, CodeActionOptions>> ServerCapabilities::codeActionProvider() const
{
QJsonValue provider = value(codeActionProviderKey);
if (provider.isBool())
return Utils::make_optional(Utils::variant<bool, CodeActionOptions>(provider.toBool()));
if (provider.isObject()) {
CodeActionOptions options(provider);
if (options.isValid(nullptr))
return Utils::make_optional(Utils::variant<bool, CodeActionOptions>(options));
}
return Utils::nullopt;
}
bool ServerCapabilities::isValid(QStringList *error) const bool ServerCapabilities::isValid(QStringList *error) const
{ {
return checkOptional<TextDocumentSyncOptions, int>(error, textDocumentSyncKey) return checkOptional<TextDocumentSyncOptions, int>(error, textDocumentSyncKey)

View File

@@ -120,6 +120,19 @@ enum class TextDocumentSyncKind
Incremental = 2 Incremental = 2
}; };
class LANGUAGESERVERPROTOCOL_EXPORT CodeActionOptions : public JsonObject
{
public:
using JsonObject::JsonObject;
QList<QString> codeActionKinds() const { return array<QString>(codeActionKindsKey); }
void setCodeActionKinds(const QList<QString> &codeActionKinds)
{ insertArray(codeActionKindsKey, codeActionKinds); }
bool isValid(QStringList *error) const override
{ return checkArray<QString>(error, codeActionKindsKey); }
};
class LANGUAGESERVERPROTOCOL_EXPORT ServerCapabilities : public JsonObject class LANGUAGESERVERPROTOCOL_EXPORT ServerCapabilities : public JsonObject
{ {
public: public:
@@ -305,10 +318,11 @@ public:
void clearWorkspaceSymbolProvider() { remove(workspaceSymbolProviderKey); } void clearWorkspaceSymbolProvider() { remove(workspaceSymbolProviderKey); }
// The server provides code actions. // The server provides code actions.
Utils::optional<bool> codeActionProvider() const Utils::optional<Utils::variant<bool, CodeActionOptions>> codeActionProvider() const;
{ return optionalValue<bool>(codeActionProviderKey); }
void setCodeActionProvider(bool codeActionProvider) void setCodeActionProvider(bool codeActionProvider)
{ insert(codeActionProviderKey, codeActionProvider); } { insert(codeActionProviderKey, codeActionProvider); }
void setCodeActionProvider(CodeActionOptions options)
{ insert(codeActionProviderKey, options); }
void clearCodeActionProvider() { remove(codeActionProviderKey); } void clearCodeActionProvider() { remove(codeActionProviderKey); }
// The server provides code lens. // The server provides code lens.

View File

@@ -91,4 +91,11 @@ DidChangeWatchedFilesNotification::DidChangeWatchedFilesNotification(
: Notification(methodName, params) : Notification(methodName, params)
{ } { }
ExecuteCommandParams::ExecuteCommandParams(const Command &command)
{
setCommand(command.command());
if (command.arguments().has_value())
setArguments(command.arguments().value());
}
} // namespace LanguageServerProtocol } // namespace LanguageServerProtocol

View File

@@ -199,15 +199,16 @@ public:
class LANGUAGESERVERPROTOCOL_EXPORT ExecuteCommandParams : public JsonObject class LANGUAGESERVERPROTOCOL_EXPORT ExecuteCommandParams : public JsonObject
{ {
public: public:
using JsonObject::JsonObject; explicit ExecuteCommandParams(const Command &command);
explicit ExecuteCommandParams(const QJsonValue &value) : JsonObject(value) {}
ExecuteCommandParams() : JsonObject() {}
QString command() const { return typedValue<QString>(commandKey); } QString command() const { return typedValue<QString>(commandKey); }
void setCommand(const QString &command) { insert(commandKey, command); } void setCommand(const QString &command) { insert(commandKey, command); }
void clearCommand() { remove(commandKey); }
Utils::optional<QList<QJsonValue>> arguments() const Utils::optional<QJsonArray> arguments() const { return typedValue<QJsonArray>(argumentsKey); }
{ return optionalArray<QJsonValue>(argumentsKey); } void setArguments(const QJsonArray &arguments) { insert(argumentsKey, arguments); }
void setArguments(const QList<QJsonValue> &arguments)
{ insertArray(argumentsKey, arguments); }
void clearArguments() { remove(argumentsKey); } void clearArguments() { remove(argumentsKey); }
bool isValid(QStringList *error) const override bool isValid(QStringList *error) const override

View File

@@ -24,7 +24,9 @@
****************************************************************************/ ****************************************************************************/
#include "baseclient.h" #include "baseclient.h"
#include "languageclientmanager.h" #include "languageclientmanager.h"
#include "languageclient/languageclientutils.h"
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <coreplugin/idocument.h> #include <coreplugin/idocument.h>
@@ -59,6 +61,7 @@ namespace LanguageClient {
static Q_LOGGING_CATEGORY(LOGLSPCLIENT, "qtc.languageclient.client", QtWarningMsg); static Q_LOGGING_CATEGORY(LOGLSPCLIENT, "qtc.languageclient.client", QtWarningMsg);
static Q_LOGGING_CATEGORY(LOGLSPCLIENTV, "qtc.languageclient.messages", QtWarningMsg); static Q_LOGGING_CATEGORY(LOGLSPCLIENTV, "qtc.languageclient.messages", QtWarningMsg);
static Q_LOGGING_CATEGORY(LOGLSPCLIENTPARSE, "qtc.languageclient.parse", QtWarningMsg);
BaseClient::BaseClient() BaseClient::BaseClient()
: m_id(Core::Id::fromString(QUuid::createUuid().toString())) : m_id(Core::Id::fromString(QUuid::createUuid().toString()))
@@ -71,11 +74,18 @@ BaseClient::BaseClient()
BaseClient::~BaseClient() BaseClient::~BaseClient()
{ {
using namespace TextEditor;
m_buffer.close(); m_buffer.close();
// 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 (TextEditor::TextDocument *document : m_resetCompletionProvider) for (TextDocument *document : m_resetCompletionProvider)
document->setCompletionAssistProvider(nullptr); document->setCompletionAssistProvider(nullptr);
for (Core::IEditor * editor : Core::DocumentModel::editorsForOpenedDocuments()) {
if (auto textEditor = qobject_cast<BaseTextEditor *>(editor)) {
TextEditorWidget *widget = textEditor->editorWidget();
widget->setRefactorMarkers(RefactorMarker::filterOutType(widget->refactorMarkers(), id()));
}
}
} }
void BaseClient::initialize() void BaseClient::initialize()
@@ -91,6 +101,7 @@ void BaseClient::initialize()
params.setWorkSpaceFolders(Utils::transform(SessionManager::projects(), [](Project *pro){ params.setWorkSpaceFolders(Utils::transform(SessionManager::projects(), [](Project *pro){
return WorkSpaceFolder(pro->projectDirectory().toString(), pro->displayName()); return WorkSpaceFolder(pro->projectDirectory().toString(), pro->displayName());
})); }));
initRequest->setParams(params);
} }
initRequest->setResponseCallback([this](const InitializeRequest::Response &initResponse){ initRequest->setResponseCallback([this](const InitializeRequest::Response &initResponse){
intializeCallback(initResponse); intializeCallback(initResponse);
@@ -282,6 +293,7 @@ void BaseClient::documentContentsChanged(Core::IDocument *document)
} }
} }
auto textDocument = qobject_cast<TextEditor::TextDocument *>(document); auto textDocument = qobject_cast<TextEditor::TextDocument *>(document);
if (syncKind != TextDocumentSyncKind::None) { if (syncKind != TextDocumentSyncKind::None) {
const auto uri = DocumentUri::fromFileName(document->filePath()); const auto uri = DocumentUri::fromFileName(document->filePath());
VersionedTextDocumentIdentifier docId(uri); VersionedTextDocumentIdentifier docId(uri);
@@ -289,9 +301,15 @@ void BaseClient::documentContentsChanged(Core::IDocument *document)
const DidChangeTextDocumentParams params(docId, QString::fromUtf8(document->contents())); const DidChangeTextDocumentParams params(docId, QString::fromUtf8(document->contents()));
sendContent(DidChangeTextDocumentNotification(params)); sendContent(DidChangeTextDocumentNotification(params));
} }
if (textDocument)
if (textDocument) {
using namespace TextEditor;
if (BaseTextEditor *editor = BaseTextEditor::textEditorForDocument(textDocument))
if (TextEditorWidget *widget = editor->editorWidget())
widget->setRefactorMarkers(RefactorMarker::filterOutType(widget->refactorMarkers(), id()));
requestDocumentSymbols(textDocument); requestDocumentSymbols(textDocument);
} }
}
void BaseClient::registerCapabilities(const QList<Registration> &registrations) void BaseClient::registerCapabilities(const QList<Registration> &registrations)
{ {
@@ -493,6 +511,85 @@ void BaseClient::cursorPositionChanged(TextEditor::TextEditorWidget *widget)
sendContent(request); sendContent(request);
} }
void BaseClient::requestCodeActions(const DocumentUri &uri, const QList<Diagnostic> &diagnostics)
{
const Utils::FileName fileName = uri.toFileName();
TextEditor::TextDocument *doc = textDocumentForFileName(fileName);
if (!doc)
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::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<BaseClient>(this)](const CodeActionRequest::Response &response) {
if (self)
self->handleCodeActionResponse(response, uri);
});
sendContent(request);
}
void BaseClient::handleCodeActionResponse(const CodeActionRequest::Response &response,
const DocumentUri &uri)
{
if (const Utils::optional<CodeActionRequest::Response::Error> &error = response.error())
log(*error);
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))
updateCodeActionRefactoringMarker(this, *action, uri);
else if (auto command = Utils::get_if<Command>(&item))
; // todo
}
}
}
}
void BaseClient::executeCommand(const Command &command)
{
using CommandOptions = LanguageServerProtocol::ServerCapabilities::ExecuteCommandOptions;
const QString method(ExecuteCommandRequest::methodName);
if (Utils::optional<bool> 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<CommandOptions> 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 BaseClient::projectOpened(ProjectExplorer::Project *project) void BaseClient::projectOpened(ProjectExplorer::Project *project)
{ {
if (!sendWorkspceFolderChanges()) if (!sendWorkspceFolderChanges())
@@ -640,7 +737,7 @@ void BaseClient::handleMethod(const QString &method, MessageId id, const IConten
auto params = dynamic_cast<const PublishDiagnosticsNotification *>(content)->params().value_or(PublishDiagnosticsParams()); auto params = dynamic_cast<const PublishDiagnosticsNotification *>(content)->params().value_or(PublishDiagnosticsParams());
paramsValid = params.isValid(&error); paramsValid = params.isValid(&error);
if (paramsValid) if (paramsValid)
LanguageClientManager::publishDiagnostics(m_id, params); LanguageClientManager::publishDiagnostics(m_id, params, this);
} else if (method == LogMessageNotification::methodName) { } else if (method == LogMessageNotification::methodName) {
auto params = dynamic_cast<const LogMessageNotification *>(content)->params().value_or(LogMessageParams()); auto params = dynamic_cast<const LogMessageNotification *>(content)->params().value_or(LogMessageParams());
paramsValid = params.isValid(&error); paramsValid = params.isValid(&error);
@@ -679,6 +776,11 @@ void BaseClient::handleMethod(const QString &method, MessageId id, const IConten
paramsValid = params.isValid(&error); paramsValid = params.isValid(&error);
if (paramsValid) if (paramsValid)
m_dynamicCapabilities.unregisterCapability(params.unregistrations()); m_dynamicCapabilities.unregisterCapability(params.unregistrations());
} else if (method == ApplyWorkspaceEditRequest::methodName) {
auto params = dynamic_cast<const ApplyWorkspaceEditRequest *>(content)->params().value_or(ApplyWorkspaceEditParams());
paramsValid = params.isValid(&error);
if (paramsValid)
applyWorkspaceEdit(params.edit());
} else if (id.isValid(&error)) { } else if (id.isValid(&error)) {
Response<JsonObject, JsonObject> response; Response<JsonObject, JsonObject> response;
response.setId(id); response.setId(id);
@@ -773,6 +875,8 @@ bool BaseClient::sendWorkspceFolderChanges() const
void BaseClient::parseData(const QByteArray &data) void BaseClient::parseData(const QByteArray &data)
{ {
const qint64 preWritePosition = m_buffer.pos(); const qint64 preWritePosition = m_buffer.pos();
qCDebug(LOGLSPCLIENTPARSE) << "parse buffer pos: " << preWritePosition;
qCDebug(LOGLSPCLIENTPARSE) << " data: " << data;
if (!m_buffer.atEnd()) if (!m_buffer.atEnd())
m_buffer.seek(preWritePosition + m_buffer.bytesAvailable()); m_buffer.seek(preWritePosition + m_buffer.bytesAvailable());
m_buffer.write(data); m_buffer.write(data);
@@ -780,6 +884,9 @@ void BaseClient::parseData(const QByteArray &data)
while (!m_buffer.atEnd()) { while (!m_buffer.atEnd()) {
QString parseError; QString parseError;
BaseMessage::parse(&m_buffer, parseError, m_currentMessage); BaseMessage::parse(&m_buffer, parseError, m_currentMessage);
qCDebug(LOGLSPCLIENTPARSE) << " complete: " << m_currentMessage.isComplete();
qCDebug(LOGLSPCLIENTPARSE) << " length: " << m_currentMessage.contentLength;
qCDebug(LOGLSPCLIENTPARSE) << " content: " << m_currentMessage.content;
if (!parseError.isEmpty()) if (!parseError.isEmpty())
log(parseError); log(parseError);
if (!m_currentMessage.isComplete()) if (!m_currentMessage.isComplete())

View File

@@ -97,6 +97,12 @@ public:
void requestDocumentSymbols(TextEditor::TextDocument *document); void requestDocumentSymbols(TextEditor::TextDocument *document);
void cursorPositionChanged(TextEditor::TextEditorWidget *widget); void cursorPositionChanged(TextEditor::TextEditorWidget *widget);
void requestCodeActions(const LanguageServerProtocol::DocumentUri &uri,
const QList<LanguageServerProtocol::Diagnostic> &diagnostics);
void handleCodeActionResponse(const LanguageServerProtocol::CodeActionRequest::Response &response,
const LanguageServerProtocol::DocumentUri &uri);
void executeCommand(const LanguageServerProtocol::Command &command);
// workspace control // workspace control
void projectOpened(ProjectExplorer::Project *project); void projectOpened(ProjectExplorer::Project *project);
void projectClosed(ProjectExplorer::Project *project); void projectClosed(ProjectExplorer::Project *project);

View File

@@ -24,7 +24,9 @@
****************************************************************************/ ****************************************************************************/
#include "languageclientcodeassist.h" #include "languageclientcodeassist.h"
#include "baseclient.h" #include "baseclient.h"
#include "languageclientutils.h"
#include <languageserverprotocol/completion.h> #include <languageserverprotocol/completion.h>
#include <texteditor/codeassist/assistinterface.h> #include <texteditor/codeassist/assistinterface.h>
@@ -89,17 +91,6 @@ bool LanguageClientCompletionItem::implicitlyApplies() const
bool LanguageClientCompletionItem::prematurelyApplies(const QChar &/*typedCharacter*/) const bool LanguageClientCompletionItem::prematurelyApplies(const QChar &/*typedCharacter*/) const
{ return false; } { return false; }
static void applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator,
const TextEdit &edit)
{
using namespace Utils::Text;
const Range range = edit.range();
const QTextDocument *doc = manipulator.textCursorAt(manipulator.currentPosition()).document();
const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1);
const int end = positionInText(doc, range.end().line() + 1, range.end().character() + 1);
manipulator.replace(start, end - start, edit.newText());
}
void LanguageClientCompletionItem::apply(TextEditor::TextDocumentManipulatorInterface &manipulator, void LanguageClientCompletionItem::apply(TextEditor::TextDocumentManipulatorInterface &manipulator,
int /*basePosition*/) const int /*basePosition*/) const
{ {

View File

@@ -40,6 +40,7 @@
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include <QTextBlock>
#include <QTimer> #include <QTimer>
using namespace LanguageServerProtocol; using namespace LanguageServerProtocol;
@@ -75,6 +76,7 @@ public:
LanguageClientManager::LanguageClientManager() LanguageClientManager::LanguageClientManager()
{ {
JsonRpcMessageHandler::registerMessageProvider<PublishDiagnosticsNotification>(); JsonRpcMessageHandler::registerMessageProvider<PublishDiagnosticsNotification>();
JsonRpcMessageHandler::registerMessageProvider<ApplyWorkspaceEditRequest>();
JsonRpcMessageHandler::registerMessageProvider<LogMessageNotification>(); JsonRpcMessageHandler::registerMessageProvider<LogMessageNotification>();
JsonRpcMessageHandler::registerMessageProvider<ShowMessageRequest>(); JsonRpcMessageHandler::registerMessageProvider<ShowMessageRequest>();
JsonRpcMessageHandler::registerMessageProvider<ShowMessageNotification>(); JsonRpcMessageHandler::registerMessageProvider<ShowMessageNotification>();
@@ -106,7 +108,8 @@ void LanguageClientManager::init()
} }
void LanguageClientManager::publishDiagnostics(const Core::Id &id, void LanguageClientManager::publishDiagnostics(const Core::Id &id,
const PublishDiagnosticsParams &params) const PublishDiagnosticsParams &params,
BaseClient *publishingClient)
{ {
const Utils::FileName fileName = params.uri().toFileName(); const Utils::FileName fileName = params.uri().toFileName();
TextEditor::TextDocument *doc = textDocumentForFileName(fileName); TextEditor::TextDocument *doc = textDocumentForFileName(fileName);
@@ -115,11 +118,14 @@ void LanguageClientManager::publishDiagnostics(const Core::Id &id,
removeMarks(fileName, id); removeMarks(fileName, id);
managerInstance->m_marks[fileName][id].reserve(params.diagnostics().size()); managerInstance->m_marks[fileName][id].reserve(params.diagnostics().size());
for (const Diagnostic& diagnostic : params.diagnostics()) { QList<Diagnostic> diagnostics = params.diagnostics();
for (const Diagnostic& diagnostic : diagnostics) {
auto mark = new LanguageClientMark(fileName, diagnostic); auto mark = new LanguageClientMark(fileName, diagnostic);
managerInstance->m_marks[fileName][id].append(mark); managerInstance->m_marks[fileName][id].append(mark);
doc->addMark(mark); doc->addMark(mark);
} }
publishingClient->requestCodeActions(params.uri(), diagnostics);
} }
void LanguageClientManager::removeMark(LanguageClientMark *mark) void LanguageClientManager::removeMark(LanguageClientMark *mark)

View File

@@ -56,7 +56,7 @@ public:
static void init(); static void init();
static void publishDiagnostics(const Core::Id &id, static void publishDiagnostics(const Core::Id &id,
const LanguageServerProtocol::PublishDiagnosticsParams &params); const LanguageServerProtocol::PublishDiagnosticsParams &params, BaseClient *publishingClient);
static void removeMark(LanguageClientMark *mark); static void removeMark(LanguageClientMark *mark);
static void removeMarks(const Utils::FileName &fileName); static void removeMarks(const Utils::FileName &fileName);

View File

@@ -25,14 +25,169 @@
#include "languageclientutils.h" #include "languageclientutils.h"
#include <texteditor/textdocument.h> #include "baseclient.h"
#include <coreplugin/editormanager/documentmodel.h> #include <coreplugin/editormanager/documentmodel.h>
using namespace LanguageClient; #include <texteditor/codeassist/textdocumentmanipulatorinterface.h>
using namespace LanguageServerProtocol; #include <texteditor/refactoringchanges.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
TextEditor::TextDocument *LanguageClient::textDocumentForFileName(const Utils::FileName &fileName) #include <QFile>
#include <QTextDocument>
using namespace LanguageServerProtocol;
using namespace Utils;
namespace LanguageClient {
QTextCursor rangeToTextCursor(const Range &range, QTextDocument *doc)
{
QTextCursor cursor(doc);
cursor.setPosition(range.end().toPositionInDocument(doc));
cursor.setPosition(range.start().toPositionInDocument(doc), QTextCursor::KeepAnchor);
return cursor;
}
ChangeSet::Range convertRange(const QTextDocument *doc, const Range &range)
{
return ChangeSet::Range(
Text::positionInText(doc, range.start().line() + 1, range.start().character() + 1),
Text::positionInText(doc, range.end().line() + 1, range.end().character()) + 1);
}
ChangeSet editsToChangeSet(const QList<TextEdit> &edits, const QTextDocument *doc)
{
ChangeSet changeSet;
for (const TextEdit &edit : edits)
changeSet.replace(convertRange(doc, edit.range()), edit.newText());
return changeSet;
}
bool applyTextDocumentEdit(const TextDocumentEdit &edit)
{
const QList<TextEdit> &edits = edit.edits();
if (edits.isEmpty())
return true;
const DocumentUri &uri = edit.id().uri();
if (TextEditor::TextDocument* doc = textDocumentForFileName(uri.toFileName())) {
LanguageClientValue<int> version = edit.id().version();
if (!version.isNull() && version.value(0) < doc->document()->revision())
return false;
}
return applyTextEdits(uri, edits);
}
bool applyTextEdits(const DocumentUri &uri, const QList<TextEdit> &edits)
{
if (edits.isEmpty())
return true;
TextEditor::RefactoringChanges changes;
TextEditor::RefactoringFilePtr file;
file = changes.file(uri.toFileName().toString());
file->setChangeSet(editsToChangeSet(edits, file->document()));
return file->apply();
}
void applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator, const TextEdit &edit)
{
using namespace Utils::Text;
const Range range = edit.range();
const QTextDocument *doc = manipulator.textCursorAt(manipulator.currentPosition()).document();
const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1);
const int end = positionInText(doc, range.end().line() + 1, range.end().character() + 1);
manipulator.replace(start, end - start, edit.newText());
}
bool applyWorkspaceEdit(const WorkspaceEdit &edit)
{
bool result = true;
const QList<TextDocumentEdit> &documentChanges
= edit.documentChanges().value_or(QList<TextDocumentEdit>());
if (!documentChanges.isEmpty()) {
for (const TextDocumentEdit &documentChange : documentChanges)
result |= applyTextDocumentEdit(documentChange);
} else {
const WorkspaceEdit::Changes &changes = edit.changes().value_or(WorkspaceEdit::Changes());
for (const DocumentUri &file : changes.keys())
result |= applyTextEdits(file, changes.value(file));
return result;
}
return result;
}
TextEditor::TextDocument *textDocumentForFileName(const FileName &fileName)
{ {
return qobject_cast<TextEditor::TextDocument *>( return qobject_cast<TextEditor::TextDocument *>(
Core::DocumentModel::documentForFilePath(fileName.toString())); Core::DocumentModel::documentForFilePath(fileName.toString()));
} }
QTextCursor endOfLineCursor(const QTextCursor &cursor)
{
QTextCursor ret = cursor;
ret.movePosition(QTextCursor::EndOfLine);
return ret;
}
void updateCodeActionRefactoringMarker(BaseClient *client,
const CodeAction &action,
const DocumentUri &uri)
{
using namespace TextEditor;
TextDocument* doc = textDocumentForFileName(uri.toFileName());
if (!doc)
return;
BaseTextEditor *editor = BaseTextEditor::textEditorForDocument(doc);
if (!editor)
return;
TextEditorWidget *editorWidget = editor->editorWidget();
const QList<Diagnostic> &diagnostics = action.diagnostics().value_or(QList<Diagnostic>());
RefactorMarkers markers;
RefactorMarker marker;
marker.type = client->id();
if (action.isValid(nullptr))
marker.tooltip = action.title();
if (action.edit().has_value()) {
WorkspaceEdit edit = action.edit().value();
marker.callback = [edit](const TextEditorWidget *) {
applyWorkspaceEdit(edit);
};
if (diagnostics.isEmpty()) {
QList<TextEdit> edits;
if (optional<QList<TextDocumentEdit>> documentChanges = edit.documentChanges()) {
QList<TextDocumentEdit> changesForUri = Utils::filtered(
documentChanges.value(), [uri](const TextDocumentEdit &edit) {
return edit.id().uri() == uri;
});
for (const TextDocumentEdit &edit : changesForUri)
edits << edit.edits();
} else if (optional<WorkspaceEdit::Changes> localChanges = edit.changes()) {
edits = localChanges.value()[uri];
}
for (const TextEdit &edit : edits) {
marker.cursor = endOfLineCursor(edit.range().start().toTextCursor(doc->document()));
markers << marker;
}
}
} else if (action.command().has_value()) {
const Command command = action.command().value();
marker.callback = [command, client = QPointer<BaseClient>(client)](const TextEditorWidget *) {
if (client)
client->executeCommand(command);
};
} else {
return;
}
for (const Diagnostic &diagnostic : diagnostics) {
marker.cursor = endOfLineCursor(diagnostic.range().start().toTextCursor(doc->document()));
markers << marker;
}
editorWidget->setRefactorMarkers(markers + editorWidget->refactorMarkers());
}
} // namespace LanguageClient

View File

@@ -26,11 +26,28 @@
#pragma once #pragma once
#include <languageserverprotocol/workspace.h> #include <languageserverprotocol/workspace.h>
#include <languageserverprotocol/languagefeatures.h>
namespace TextEditor { class TextDocument; } #include <texteditor/refactoroverlay.h>
namespace TextEditor {
class TextDocument;
class TextDocumentManipulatorInterface;
} // namespace TextEditor
namespace LanguageClient { namespace LanguageClient {
class BaseClient;
bool applyWorkspaceEdit(const LanguageServerProtocol::WorkspaceEdit &edit);
bool applyTextDocumentEdit(const LanguageServerProtocol::TextDocumentEdit &edit);
bool applyTextEdits(const LanguageServerProtocol::DocumentUri &uri,
const QList<LanguageServerProtocol::TextEdit> &edits);
void applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator,
const LanguageServerProtocol::TextEdit &edit);
TextEditor::TextDocument *textDocumentForFileName(const Utils::FileName &fileName); TextEditor::TextDocument *textDocumentForFileName(const Utils::FileName &fileName);
void updateCodeActionRefactoringMarker(BaseClient *client,
const LanguageServerProtocol::CodeAction &action,
const LanguageServerProtocol::DocumentUri &uri);
} // namespace LanguageClient } // namespace LanguageClient