forked from qt-creator/qt-creator
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:
@@ -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
|
||||
|
@@ -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); }
|
||||
|
||||
|
@@ -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";
|
||||
|
@@ -105,6 +105,16 @@ DocumentSymbolsRequest::DocumentSymbolsRequest(const DocumentSymbolParams ¶m
|
||||
: 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
|
||||
|
@@ -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 ¶ms = CodeActionParams());
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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> ®istrations)
|
||||
@@ -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())
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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 ¶ms)
|
||||
const PublishDiagnosticsParams ¶ms,
|
||||
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)
|
||||
|
@@ -56,7 +56,7 @@ public:
|
||||
static void init();
|
||||
|
||||
static void publishDiagnostics(const Core::Id &id,
|
||||
const LanguageServerProtocol::PublishDiagnosticsParams ¶ms);
|
||||
const LanguageServerProtocol::PublishDiagnosticsParams ¶ms, BaseClient *publishingClient);
|
||||
|
||||
static void removeMark(LanguageClientMark *mark);
|
||||
static void removeMarks(const Utils::FileName &fileName);
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user