forked from qt-creator/qt-creator
Clangd: Fix autocompletion for old style SIGNAL and SLOT
Fixes: QTCREATORBUG-20737 Change-Id: If6d3c6ea5924537386eca81d90d4bb1e8f1a1466 Reviewed-by: <github-actions-qt-creator@cristianadam.eu> Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
This commit is contained in:
@@ -751,9 +751,8 @@ ClangdClient::ClangdCompletionAssistProcessor::generateCompletionItems(
|
|||||||
return itemGenerator(items);
|
return itemGenerator(items);
|
||||||
const QString content = doc->toPlainText();
|
const QString content = doc->toPlainText();
|
||||||
const bool requiresSignal = CppEditor::CppModelManager::instance()
|
const bool requiresSignal = CppEditor::CppModelManager::instance()
|
||||||
->positionRequiresSignal(filePath().toString(),
|
->getSignalSlotType(filePath().toString(), content.toUtf8(), pos)
|
||||||
content.toUtf8(),
|
== CppEditor::SignalSlotType::NewStyleSignal;
|
||||||
pos);
|
|
||||||
if (requiresSignal)
|
if (requiresSignal)
|
||||||
return itemGenerator(Utils::filtered(items, criterion));
|
return itemGenerator(Utils::filtered(items, criterion));
|
||||||
return itemGenerator(items);
|
return itemGenerator(items);
|
||||||
@@ -2237,6 +2236,10 @@ IAssistProcessor *ClangdClient::ClangdCompletionAssistProvider::createProcessor(
|
|||||||
contextAnalyzer.positionEndOfExpression(),
|
contextAnalyzer.positionEndOfExpression(),
|
||||||
contextAnalyzer.completionOperator(),
|
contextAnalyzer.completionOperator(),
|
||||||
CustomAssistMode::Preprocessor);
|
CustomAssistMode::Preprocessor);
|
||||||
|
case ClangCompletionContextAnalyzer::CompleteSignal:
|
||||||
|
case ClangCompletionContextAnalyzer::CompleteSlot:
|
||||||
|
if (!interface->isBaseObject())
|
||||||
|
return CppEditor::getCppCompletionAssistProcessor();
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ public:
|
|||||||
{ getCppSpecifics(); return m_headerPaths; }
|
{ getCppSpecifics(); return m_headerPaths; }
|
||||||
CPlusPlus::LanguageFeatures languageFeatures() const
|
CPlusPlus::LanguageFeatures languageFeatures() const
|
||||||
{ getCppSpecifics(); return m_languageFeatures; }
|
{ getCppSpecifics(); return m_languageFeatures; }
|
||||||
|
bool isBaseObject() const override { return false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void getCppSpecifics() const;
|
void getCppSpecifics() const;
|
||||||
|
|||||||
@@ -1163,22 +1163,43 @@ void CppEditorWidget::updateSemanticInfo(const SemanticInfo &semanticInfo,
|
|||||||
updateFunctionDeclDefLink();
|
updateFunctionDeclDefLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CppEditorWidget::isOldStyleSignalOrSlot() const
|
||||||
|
{
|
||||||
|
QTextCursor tc(textCursor());
|
||||||
|
const QString content = textDocument()->plainText();
|
||||||
|
|
||||||
|
return CppEditor::CppModelManager::instance()
|
||||||
|
->getSignalSlotType(textDocument()->filePath().toString(),
|
||||||
|
content.toUtf8(),
|
||||||
|
tc.position())
|
||||||
|
== CppEditor::SignalSlotType::OldStyleSignal;
|
||||||
|
}
|
||||||
|
|
||||||
AssistInterface *CppEditorWidget::createAssistInterface(AssistKind kind, AssistReason reason) const
|
AssistInterface *CppEditorWidget::createAssistInterface(AssistKind kind, AssistReason reason) const
|
||||||
{
|
{
|
||||||
if (kind == Completion || kind == FunctionHint) {
|
if (kind == Completion || kind == FunctionHint) {
|
||||||
CppCompletionAssistProvider * const cap = kind == Completion
|
CppCompletionAssistProvider * const cap = kind == Completion
|
||||||
? qobject_cast<CppCompletionAssistProvider *>(cppEditorDocument()->completionAssistProvider())
|
? qobject_cast<CppCompletionAssistProvider *>(cppEditorDocument()->completionAssistProvider())
|
||||||
: qobject_cast<CppCompletionAssistProvider *>(cppEditorDocument()->functionHintAssistProvider());
|
: qobject_cast<CppCompletionAssistProvider *>(cppEditorDocument()->functionHintAssistProvider());
|
||||||
if (cap) {
|
|
||||||
|
auto getFeatures = [this]() {
|
||||||
LanguageFeatures features = LanguageFeatures::defaultFeatures();
|
LanguageFeatures features = LanguageFeatures::defaultFeatures();
|
||||||
if (Document::Ptr doc = d->m_lastSemanticInfo.doc)
|
if (Document::Ptr doc = d->m_lastSemanticInfo.doc)
|
||||||
features = doc->languageFeatures();
|
features = doc->languageFeatures();
|
||||||
features.objCEnabled |= cppEditorDocument()->isObjCEnabled();
|
features.objCEnabled |= cppEditorDocument()->isObjCEnabled();
|
||||||
|
return features;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cap)
|
||||||
return cap->createAssistInterface(textDocument()->filePath(),
|
return cap->createAssistInterface(textDocument()->filePath(),
|
||||||
this,
|
this,
|
||||||
features,
|
getFeatures(),
|
||||||
reason);
|
reason);
|
||||||
} else {
|
else {
|
||||||
|
if (isOldStyleSignalOrSlot())
|
||||||
|
return CppModelManager::instance()
|
||||||
|
->completionAssistProvider()
|
||||||
|
->createAssistInterface(textDocument()->filePath(), this, getFeatures(), reason);
|
||||||
return TextEditorWidget::createAssistInterface(kind, reason);
|
return TextEditorWidget::createAssistInterface(kind, reason);
|
||||||
}
|
}
|
||||||
} else if (kind == QuickFix) {
|
} else if (kind == QuickFix) {
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ private:
|
|||||||
void finalizeInitializationAfterDuplication(TextEditorWidget *other) override;
|
void finalizeInitializationAfterDuplication(TextEditorWidget *other) override;
|
||||||
|
|
||||||
unsigned documentRevision() const;
|
unsigned documentRevision() const;
|
||||||
|
bool isOldStyleSignalOrSlot() const;
|
||||||
|
|
||||||
QMenu *createRefactorMenu(QWidget *parent) const;
|
QMenu *createRefactorMenu(QWidget *parent) const;
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
#include <coreplugin/progressmanager/progressmanager.h>
|
#include <coreplugin/progressmanager/progressmanager.h>
|
||||||
#include <coreplugin/vcsmanager.h>
|
#include <coreplugin/vcsmanager.h>
|
||||||
#include <cplusplus/ASTPath.h>
|
#include <cplusplus/ASTPath.h>
|
||||||
|
#include <cplusplus/ExpressionUnderCursor.h>
|
||||||
#include <cplusplus/TypeOfExpression.h>
|
#include <cplusplus/TypeOfExpression.h>
|
||||||
#include <extensionsystem/pluginmanager.h>
|
#include <extensionsystem/pluginmanager.h>
|
||||||
|
|
||||||
@@ -338,11 +339,29 @@ void CppModelManager::switchHeaderSource(bool inNextSplit, Backend backend)
|
|||||||
inNextSplit);
|
inNextSplit);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CppModelManager::positionRequiresSignal(const QString &filePath, const QByteArray &content,
|
int argumentPositionOf(const AST *last, const CallAST *callAst)
|
||||||
|
{
|
||||||
|
if (!callAst || !callAst->expression_list)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int num = 0;
|
||||||
|
for (ExpressionListAST *it = callAst->expression_list; it; it = it->next) {
|
||||||
|
++num;
|
||||||
|
const ExpressionAST *const arg = it->value;
|
||||||
|
if (arg->firstToken() <= last->firstToken()
|
||||||
|
&& arg->lastToken() >= last->lastToken()) {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalSlotType CppModelManager::getSignalSlotType(const QString &filePath,
|
||||||
|
const QByteArray &content,
|
||||||
int position) const
|
int position) const
|
||||||
{
|
{
|
||||||
if (content.isEmpty())
|
if (content.isEmpty())
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
|
|
||||||
// Insert a dummy prefix if we don't have a real one. Otherwise the AST path will not contain
|
// Insert a dummy prefix if we don't have a real one. Otherwise the AST path will not contain
|
||||||
// anything after the CallAST.
|
// anything after the CallAST.
|
||||||
@@ -360,26 +379,17 @@ bool CppModelManager::positionRequiresSignal(const QString &filePath, const QByt
|
|||||||
|
|
||||||
// Are we at the second argument of a function call?
|
// Are we at the second argument of a function call?
|
||||||
const QList<AST *> path = ASTPath(document)(cursor);
|
const QList<AST *> path = ASTPath(document)(cursor);
|
||||||
if (path.isEmpty() || !path.last()->asSimpleName())
|
if (path.isEmpty())
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
const CallAST *callAst = nullptr;
|
const CallAST *callAst = nullptr;
|
||||||
for (auto it = path.crbegin(); it != path.crend(); ++it) {
|
for (auto it = path.crbegin(); it != path.crend(); ++it) {
|
||||||
if ((callAst = (*it)->asCall()))
|
if ((callAst = (*it)->asCall()))
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!callAst)
|
|
||||||
return false;
|
|
||||||
if (!callAst->expression_list || !callAst->expression_list->next)
|
|
||||||
return false;
|
|
||||||
const ExpressionAST * const secondArg = callAst->expression_list->next->value;
|
|
||||||
if (secondArg->firstToken() > path.last()->firstToken()
|
|
||||||
|| secondArg->lastToken() < path.last()->lastToken()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is the function called "connect" or "disconnect"?
|
// Is the function called "connect" or "disconnect"?
|
||||||
if (!callAst->base_expression)
|
if (!callAst || !callAst->base_expression)
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
Scope *scope = document->globalNamespace();
|
Scope *scope = document->globalNamespace();
|
||||||
for (auto it = path.crbegin(); it != path.crend(); ++it) {
|
for (auto it = path.crbegin(); it != path.crend(); ++it) {
|
||||||
if (const CompoundStatementAST * const stmtAst = (*it)->asCompoundStatement()) {
|
if (const CompoundStatementAST * const stmtAst = (*it)->asCompoundStatement()) {
|
||||||
@@ -398,7 +408,7 @@ bool CppModelManager::positionRequiresSignal(const QString &filePath, const QByt
|
|||||||
exprType.init(document, snapshot);
|
exprType.init(document, snapshot);
|
||||||
const QList<LookupItem> typeMatches = exprType(ast->base_expression, document, scope);
|
const QList<LookupItem> typeMatches = exprType(ast->base_expression, document, scope);
|
||||||
if (typeMatches.isEmpty())
|
if (typeMatches.isEmpty())
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
const std::function<const NamedType *(const FullySpecifiedType &)> getNamedType
|
const std::function<const NamedType *(const FullySpecifiedType &)> getNamedType
|
||||||
= [&getNamedType](const FullySpecifiedType &type ) -> const NamedType * {
|
= [&getNamedType](const FullySpecifiedType &type ) -> const NamedType * {
|
||||||
Type * const t = type.type();
|
Type * const t = type.type();
|
||||||
@@ -414,22 +424,22 @@ bool CppModelManager::positionRequiresSignal(const QString &filePath, const QByt
|
|||||||
if (!namedType && typeMatches.first().declaration())
|
if (!namedType && typeMatches.first().declaration())
|
||||||
namedType = getNamedType(typeMatches.first().declaration()->type());
|
namedType = getNamedType(typeMatches.first().declaration()->type());
|
||||||
if (!namedType)
|
if (!namedType)
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
const ClassOrNamespace * const result = context.lookupType(namedType->name(), scope);
|
const ClassOrNamespace * const result = context.lookupType(namedType->name(), scope);
|
||||||
if (!result)
|
if (!result)
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
scope = result->rootClass();
|
scope = result->rootClass();
|
||||||
if (!scope)
|
if (!scope)
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
}
|
}
|
||||||
if (!nameAst || !nameAst->name)
|
if (!nameAst || !nameAst->name)
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
const Identifier * const id = nameAst->name->identifier();
|
const Identifier * const id = nameAst->name->identifier();
|
||||||
if (!id)
|
if (!id)
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
const QString funcName = QString::fromUtf8(id->chars(), id->size());
|
const QString funcName = QString::fromUtf8(id->chars(), id->size());
|
||||||
if (funcName != "connect" && funcName != "disconnect")
|
if (funcName != "connect" && funcName != "disconnect")
|
||||||
return false;
|
return SignalSlotType::None;
|
||||||
|
|
||||||
// Is the function a member function of QObject?
|
// Is the function a member function of QObject?
|
||||||
const QList<LookupItem> matches = context.lookup(nameAst->name, scope);
|
const QList<LookupItem> matches = context.lookup(nameAst->name, scope);
|
||||||
@@ -440,11 +450,29 @@ bool CppModelManager::positionRequiresSignal(const QString &filePath, const QByt
|
|||||||
if (!klass || !klass->name())
|
if (!klass || !klass->name())
|
||||||
continue;
|
continue;
|
||||||
const Identifier * const classId = klass->name()->identifier();
|
const Identifier * const classId = klass->name()->identifier();
|
||||||
if (classId && QString::fromUtf8(classId->chars(), classId->size()) == "QObject")
|
if (classId && QString::fromUtf8(classId->chars(), classId->size()) == "QObject") {
|
||||||
return true;
|
QString expression;
|
||||||
|
LanguageFeatures features = LanguageFeatures::defaultFeatures();
|
||||||
|
CPlusPlus::ExpressionUnderCursor expressionUnderCursor(features);
|
||||||
|
for (int i = cursor.position(); i > 0; --i)
|
||||||
|
if (textDocument.characterAt(i) == '(') {
|
||||||
|
cursor.setPosition(i);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
expression = expressionUnderCursor(cursor);
|
||||||
|
|
||||||
|
const int argumentPosition = argumentPositionOf(path.last(), callAst);
|
||||||
|
if ((expression.endsWith(QLatin1String("SIGNAL"))
|
||||||
|
&& (argumentPosition == 2 || argumentPosition == 4))
|
||||||
|
|| (expression.endsWith(QLatin1String("SLOT")) && argumentPosition == 4))
|
||||||
|
return SignalSlotType::OldStyleSignal;
|
||||||
|
|
||||||
|
if (argumentPosition == 2)
|
||||||
|
return SignalSlotType::NewStyleSignal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SignalSlotType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
FollowSymbolUnderCursor &CppModelManager::builtinFollowSymbol()
|
FollowSymbolUnderCursor &CppModelManager::builtinFollowSymbol()
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ namespace Core {
|
|||||||
class IDocument;
|
class IDocument;
|
||||||
class IEditor;
|
class IEditor;
|
||||||
}
|
}
|
||||||
namespace CPlusPlus { class LookupContext; }
|
namespace CPlusPlus {
|
||||||
|
class AST;
|
||||||
|
class CallAST;
|
||||||
|
class LookupContext;
|
||||||
|
} // namespace CPlusPlus
|
||||||
namespace ProjectExplorer { class Project; }
|
namespace ProjectExplorer { class Project; }
|
||||||
namespace TextEditor {
|
namespace TextEditor {
|
||||||
class BaseHoverHandler;
|
class BaseHoverHandler;
|
||||||
@@ -75,6 +79,12 @@ class CppModelManagerPrivate;
|
|||||||
|
|
||||||
namespace Tests { class ModelManagerTestHelper; }
|
namespace Tests { class ModelManagerTestHelper; }
|
||||||
|
|
||||||
|
enum class SignalSlotType {
|
||||||
|
OldStyleSignal,
|
||||||
|
NewStyleSignal,
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
class CPPEDITOR_EXPORT CppModelManager final : public CPlusPlus::CppModelManagerBase
|
class CPPEDITOR_EXPORT CppModelManager final : public CPlusPlus::CppModelManagerBase
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -154,7 +164,8 @@ public:
|
|||||||
|
|
||||||
QList<int> references(CPlusPlus::Symbol *symbol, const CPlusPlus::LookupContext &context);
|
QList<int> references(CPlusPlus::Symbol *symbol, const CPlusPlus::LookupContext &context);
|
||||||
|
|
||||||
bool positionRequiresSignal(const QString &filePath, const QByteArray &content,
|
SignalSlotType getSignalSlotType(const QString &filePath,
|
||||||
|
const QByteArray &content,
|
||||||
int position) const;
|
int position) const;
|
||||||
|
|
||||||
void renameUsages(CPlusPlus::Symbol *symbol, const CPlusPlus::LookupContext &context,
|
void renameUsages(CPlusPlus::Symbol *symbol, const CPlusPlus::LookupContext &context,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public:
|
|||||||
|
|
||||||
bool isCursorOn(unsigned tokenIndex) const;
|
bool isCursorOn(unsigned tokenIndex) const;
|
||||||
bool isCursorOn(const CPlusPlus::AST *ast) const;
|
bool isCursorOn(const CPlusPlus::AST *ast) const;
|
||||||
|
bool isBaseObject() const override { return false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
CppEditorWidget *m_editor;
|
CppEditorWidget *m_editor;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "clangdiagnosticconfigsmodel.h"
|
#include "clangdiagnosticconfigsmodel.h"
|
||||||
#include "cppautocompleter.h"
|
#include "cppautocompleter.h"
|
||||||
#include "cppcodemodelsettings.h"
|
#include "cppcodemodelsettings.h"
|
||||||
|
#include "cppcompletionassist.h"
|
||||||
#include "cppeditorconstants.h"
|
#include "cppeditorconstants.h"
|
||||||
#include "cppeditorplugin.h"
|
#include "cppeditorplugin.h"
|
||||||
#include "cpphighlighter.h"
|
#include "cpphighlighter.h"
|
||||||
@@ -336,6 +337,11 @@ TextEditor::QuickFixOperations quickFixOperations(const TextEditor::AssistInterf
|
|||||||
return Internal::quickFixOperations(interface);
|
return Internal::quickFixOperations(interface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CppCompletionAssistProcessor *getCppCompletionAssistProcessor()
|
||||||
|
{
|
||||||
|
return new Internal::InternalCppCompletionAssistProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
CppCodeModelSettings *codeModelSettings()
|
CppCodeModelSettings *codeModelSettings()
|
||||||
{
|
{
|
||||||
return Internal::CppEditorPlugin::instance()->codeModelSettings();
|
return Internal::CppEditorPlugin::instance()->codeModelSettings();
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ namespace TextEditor { class AssistInterface; }
|
|||||||
namespace CppEditor {
|
namespace CppEditor {
|
||||||
class CppRefactoringFile;
|
class CppRefactoringFile;
|
||||||
class ProjectInfo;
|
class ProjectInfo;
|
||||||
|
class CppCompletionAssistProcessor;
|
||||||
|
|
||||||
void CPPEDITOR_EXPORT moveCursorToEndOfIdentifier(QTextCursor *tc);
|
void CPPEDITOR_EXPORT moveCursorToEndOfIdentifier(QTextCursor *tc);
|
||||||
void CPPEDITOR_EXPORT moveCursorToStartOfIdentifier(QTextCursor *tc);
|
void CPPEDITOR_EXPORT moveCursorToStartOfIdentifier(QTextCursor *tc);
|
||||||
@@ -80,6 +81,8 @@ bool CPPEDITOR_EXPORT isInCommentOrString(const TextEditor::AssistInterface *int
|
|||||||
TextEditor::QuickFixOperations CPPEDITOR_EXPORT
|
TextEditor::QuickFixOperations CPPEDITOR_EXPORT
|
||||||
quickFixOperations(const TextEditor::AssistInterface *interface);
|
quickFixOperations(const TextEditor::AssistInterface *interface);
|
||||||
|
|
||||||
|
CppCompletionAssistProcessor CPPEDITOR_EXPORT *getCppCompletionAssistProcessor();
|
||||||
|
|
||||||
enum class CacheUsage { ReadWrite, ReadOnly };
|
enum class CacheUsage { ReadWrite, ReadOnly };
|
||||||
|
|
||||||
QString CPPEDITOR_EXPORT correspondingHeaderOrSource(const QString &fileName, bool *wasHeader = nullptr,
|
QString CPPEDITOR_EXPORT correspondingHeaderOrSource(const QString &fileName, bool *wasHeader = nullptr,
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ public:
|
|||||||
|
|
||||||
const QString &mimeType() const { return m_mimeType; }
|
const QString &mimeType() const { return m_mimeType; }
|
||||||
const Document::Ptr &glslDocument() const { return m_glslDoc; }
|
const Document::Ptr &glslDocument() const { return m_glslDoc; }
|
||||||
|
bool isBaseObject() const override { return false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_mimeType;
|
QString m_mimeType;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public:
|
|||||||
|
|
||||||
const QmlJSTools::SemanticInfo &semanticInfo() const;
|
const QmlJSTools::SemanticInfo &semanticInfo() const;
|
||||||
QmlJSTools::QmlJSRefactoringFilePtr currentFile() const;
|
QmlJSTools::QmlJSRefactoringFilePtr currentFile() const;
|
||||||
|
bool isBaseObject() const override { return false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QmlJSTools::SemanticInfo m_semanticInfo;
|
QmlJSTools::SemanticInfo m_semanticInfo;
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public:
|
|||||||
virtual void prepareForAsyncUse();
|
virtual void prepareForAsyncUse();
|
||||||
virtual void recreateTextDocument();
|
virtual void recreateTextDocument();
|
||||||
virtual AssistReason reason() const;
|
virtual AssistReason reason() const;
|
||||||
|
virtual bool isBaseObject() const { return true; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QTextDocument *m_textDocument;
|
QTextDocument *m_textDocument;
|
||||||
|
|||||||
Reference in New Issue
Block a user