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());
setDocumentSymbol(SymbolCapabilities());
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
@@ -123,7 +129,7 @@ bool TextDocumentClientCapabilities::isValid(QStringList *error) const
&& checkOptional<DynamicRegistrationCapabilities>(error, definitionKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, typeDefinitionKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, implementationKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, codeActionKey)
&& checkOptional<CodeActionCapabilities>(error, codeActionKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, codeLensKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, documentLinkKey)
&& checkOptional<DynamicRegistrationCapabilities>(error, colorProviderKey)
@@ -168,4 +174,10 @@ bool TextDocumentClientCapabilities::SignatureHelpCapabilities::isValid(QStringL
&& checkOptional<SignatureHelpCapabilities>(error, signatureInformationKey);
}
bool TextDocumentClientCapabilities::CodeActionCapabilities::isValid(QStringList *errorHierarchy) const
{
return DynamicRegistrationCapabilities::isValid(errorHierarchy)
&& checkOptional<CodeActionLiteralSupport>(errorHierarchy, codeActionLiteralSupportKey);
}
} // namespace LanguageServerProtocol

View File

@@ -355,10 +355,53 @@ public:
{ insert(implementationKey, implementation); }
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.
Utils::optional<DynamicRegistrationCapabilities> codeAction() const
{ return optionalValue<DynamicRegistrationCapabilities>(codeActionKey); }
void setCodeAction(const DynamicRegistrationCapabilities &codeAction)
Utils::optional<CodeActionCapabilities> codeAction() const
{ return optionalValue<CodeActionCapabilities>(codeActionKey); }
void setCodeAction(const CodeActionCapabilities &codeAction)
{ insert(codeActionKey, codeAction); }
void clearCodeAction() { remove(codeActionKey); }

View File

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

View File

@@ -105,6 +105,16 @@ DocumentSymbolsRequest::DocumentSymbolsRequest(const DocumentSymbolParams &param
: 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
{
return checkArray<Diagnostic>(error, diagnosticsKey);
@@ -380,4 +390,32 @@ SignatureHelpRequest::SignatureHelpRequest(const TextDocumentPositionParams &par
: 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

View File

@@ -344,12 +344,37 @@ public:
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
{
public:
using JsonObject::JsonObject;
class CodeActionContext : public JsonObject
class LANGUAGESERVERPROTOCOL_EXPORT CodeActionContext : public JsonObject
{
public:
using JsonObject::JsonObject;
@@ -358,6 +383,10 @@ public:
void setDiagnostics(const QList<Diagnostic> &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;
};
@@ -375,8 +404,45 @@ public:
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<
LanguageClientArray<Command>, std::nullptr_t, CodeActionParams>
CodeActionResult, std::nullptr_t, CodeActionParams>
{
public:
CodeActionRequest(const CodeActionParams &params = CodeActionParams());

View File

@@ -75,22 +75,20 @@ bool Diagnostic::isValid(QStringList *error) const
&& 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);
if (it != end())
if (it == end())
return Utils::nullopt;
QTC_ASSERT(it.value().type() == QJsonValue::Object, return Changes());
QJsonObject changesObject(it.value().toObject());
QMap<QString, QList<TextEdit>> changesResult;
Changes changesResult;
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);
}
void WorkspaceEdit::setChanges(const QMap<QString, QList<TextEdit> > &changes)
void WorkspaceEdit::setChanges(const Changes &changes)
{
QJsonObject changesObject;
const auto end = changes.end();
@@ -98,7 +96,7 @@ void WorkspaceEdit::setChanges(const QMap<QString, QList<TextEdit> > &changes)
QJsonArray edits;
for (const TextEdit &edit : it.value())
edits.append(QJsonValue(edit));
changesObject.insert(it.key(), edits);
changesObject.insert(it.key().toFileName().toString(), edits);
}
insert(changesKey, changesObject);
}
@@ -329,6 +327,13 @@ int Position::toPositionInDocument(QTextDocument *doc) const
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)
{
setStart(start);

View File

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

View File

@@ -98,6 +98,19 @@ void ServerCapabilities::setImplementationProvider(
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
{
return checkOptional<TextDocumentSyncOptions, int>(error, textDocumentSyncKey)

View File

@@ -120,6 +120,19 @@ enum class TextDocumentSyncKind
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
{
public:
@@ -305,10 +318,11 @@ public:
void clearWorkspaceSymbolProvider() { remove(workspaceSymbolProviderKey); }
// The server provides code actions.
Utils::optional<bool> codeActionProvider() const
{ return optionalValue<bool>(codeActionProviderKey); }
Utils::optional<Utils::variant<bool, CodeActionOptions>> codeActionProvider() const;
void setCodeActionProvider(bool codeActionProvider)
{ insert(codeActionProviderKey, codeActionProvider); }
void setCodeActionProvider(CodeActionOptions options)
{ insert(codeActionProviderKey, options); }
void clearCodeActionProvider() { remove(codeActionProviderKey); }
// The server provides code lens.

View File

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

View File

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

View File

@@ -24,7 +24,9 @@
****************************************************************************/
#include "baseclient.h"
#include "languageclientmanager.h"
#include "languageclient/languageclientutils.h"
#include <coreplugin/icore.h>
#include <coreplugin/idocument.h>
@@ -59,6 +61,7 @@ namespace LanguageClient {
static Q_LOGGING_CATEGORY(LOGLSPCLIENT, "qtc.languageclient.client", QtWarningMsg);
static Q_LOGGING_CATEGORY(LOGLSPCLIENTV, "qtc.languageclient.messages", QtWarningMsg);
static Q_LOGGING_CATEGORY(LOGLSPCLIENTPARSE, "qtc.languageclient.parse", QtWarningMsg);
BaseClient::BaseClient()
: m_id(Core::Id::fromString(QUuid::createUuid().toString()))
@@ -71,11 +74,18 @@ BaseClient::BaseClient()
BaseClient::~BaseClient()
{
using namespace TextEditor;
m_buffer.close();
// FIXME: instead of replacing the completion provider in the text document store the
// completion provider as a prioritised list in the text document
for (TextEditor::TextDocument *document : m_resetCompletionProvider)
for (TextDocument *document : m_resetCompletionProvider)
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()
@@ -91,6 +101,7 @@ void BaseClient::initialize()
params.setWorkSpaceFolders(Utils::transform(SessionManager::projects(), [](Project *pro){
return WorkSpaceFolder(pro->projectDirectory().toString(), pro->displayName());
}));
initRequest->setParams(params);
}
initRequest->setResponseCallback([this](const InitializeRequest::Response &initResponse){
intializeCallback(initResponse);
@@ -282,6 +293,7 @@ void BaseClient::documentContentsChanged(Core::IDocument *document)
}
}
auto textDocument = qobject_cast<TextEditor::TextDocument *>(document);
if (syncKind != TextDocumentSyncKind::None) {
const auto uri = DocumentUri::fromFileName(document->filePath());
VersionedTextDocumentIdentifier docId(uri);
@@ -289,8 +301,14 @@ void BaseClient::documentContentsChanged(Core::IDocument *document)
const DidChangeTextDocumentParams params(docId, QString::fromUtf8(document->contents()));
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);
}
}
void BaseClient::registerCapabilities(const QList<Registration> &registrations)
@@ -493,6 +511,85 @@ void BaseClient::cursorPositionChanged(TextEditor::TextEditorWidget *widget)
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)
{
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());
paramsValid = params.isValid(&error);
if (paramsValid)
LanguageClientManager::publishDiagnostics(m_id, params);
LanguageClientManager::publishDiagnostics(m_id, params, this);
} else if (method == LogMessageNotification::methodName) {
auto params = dynamic_cast<const LogMessageNotification *>(content)->params().value_or(LogMessageParams());
paramsValid = params.isValid(&error);
@@ -679,6 +776,11 @@ void BaseClient::handleMethod(const QString &method, MessageId id, const IConten
paramsValid = params.isValid(&error);
if (paramsValid)
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)) {
Response<JsonObject, JsonObject> response;
response.setId(id);
@@ -773,6 +875,8 @@ bool BaseClient::sendWorkspceFolderChanges() const
void BaseClient::parseData(const QByteArray &data)
{
const qint64 preWritePosition = m_buffer.pos();
qCDebug(LOGLSPCLIENTPARSE) << "parse buffer pos: " << preWritePosition;
qCDebug(LOGLSPCLIENTPARSE) << " data: " << data;
if (!m_buffer.atEnd())
m_buffer.seek(preWritePosition + m_buffer.bytesAvailable());
m_buffer.write(data);
@@ -780,6 +884,9 @@ void BaseClient::parseData(const QByteArray &data)
while (!m_buffer.atEnd()) {
QString parseError;
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())
log(parseError);
if (!m_currentMessage.isComplete())

View File

@@ -97,6 +97,12 @@ public:
void requestDocumentSymbols(TextEditor::TextDocument *document);
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
void projectOpened(ProjectExplorer::Project *project);
void projectClosed(ProjectExplorer::Project *project);

View File

@@ -24,7 +24,9 @@
****************************************************************************/
#include "languageclientcodeassist.h"
#include "baseclient.h"
#include "languageclientutils.h"
#include <languageserverprotocol/completion.h>
#include <texteditor/codeassist/assistinterface.h>
@@ -89,17 +91,6 @@ bool LanguageClientCompletionItem::implicitlyApplies() const
bool LanguageClientCompletionItem::prematurelyApplies(const QChar &/*typedCharacter*/) const
{ 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,
int /*basePosition*/) const
{

View File

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

View File

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

View File

@@ -25,14 +25,169 @@
#include "languageclientutils.h"
#include <texteditor/textdocument.h>
#include "baseclient.h"
#include <coreplugin/editormanager/documentmodel.h>
using namespace LanguageClient;
using namespace LanguageServerProtocol;
#include <texteditor/codeassist/textdocumentmanipulatorinterface.h>
#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 *>(
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
#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 {
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);
void updateCodeActionRefactoringMarker(BaseClient *client,
const LanguageServerProtocol::CodeAction &action,
const LanguageServerProtocol::DocumentUri &uri);
} // namespace LanguageClient