mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 20:07:32 +00:00
JSAutoCompleteProvier revamp:
-Type resolution now happens directly in ScriptDocument -Extends JavaAutocompleteProvider -Removed redundant code/logic
This commit is contained in:
1 parent
1221badc69
commit
bbe6d9bb77
3 files changed
+124
-209
No files matched your search
+1
-4
@@ -390,10 +390,7 @@ public class AutocompleteManager {
|
||||
// Get suggestions from appropriate provider
|
||||
AutocompleteProvider provider = document.isJavaScript() ? jsProvider : javaProvider;
|
||||
|
||||
// For JavaScript, update variable types from analyzer
|
||||
if (document.isJavaScript() && document.getJSAnalyzer() != null) {
|
||||
jsProvider.updateVariableTypes(document.getJSAnalyzer().getVariableTypes());
|
||||
}
|
||||
// No need to update variable types - JSAutocompleteProvider now gets them directly from ScriptDocument
|
||||
|
||||
List<AutocompleteItem> suggestions = provider.getSuggestions(context);
|
||||
|
||||
|
||||
+107
-191
@@ -1,22 +1,21 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.*;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Autocomplete provider for JavaScript/ECMAScript scripts.
|
||||
* Uses JSTypeRegistry for type resolution.
|
||||
* Uses ScriptDocument's unified TypeInfo/TypeResolver system for type resolution.
|
||||
*/
|
||||
public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
public class JSAutocompleteProvider extends JavaAutocompleteProvider {
|
||||
|
||||
private ScriptDocument document;
|
||||
private final JSTypeRegistry registry;
|
||||
|
||||
// Variable types inferred from the document
|
||||
private Map<String, String> variableTypes = new HashMap<>();
|
||||
|
||||
public JSAutocompleteProvider() {
|
||||
this.registry = JSTypeRegistry.getInstance();
|
||||
if (!registry.isInitialized()) {
|
||||
@@ -24,17 +23,6 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public void setDocument(ScriptDocument document) {
|
||||
this.document = document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the variable type map from document analysis.
|
||||
*/
|
||||
public void updateVariableTypes(Map<String, String> types) {
|
||||
this.variableTypes = types != null ? new HashMap<>(types) : new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProvide(Context context) {
|
||||
return document != null && document.isJavaScript();
|
||||
@@ -43,13 +31,13 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
@Override
|
||||
public List<AutocompleteItem> getSuggestions(Context context) {
|
||||
List<AutocompleteItem> items = new ArrayList<>();
|
||||
|
||||
// Resolve owner type for usage tracking
|
||||
|
||||
// Resolve owner type for usage tracking using ScriptDocument's unified type resolution
|
||||
String ownerFullName = null;
|
||||
if (context.isMemberAccess && context.receiverExpression != null) {
|
||||
String receiverType = resolveReceiverType(context.receiverExpression);
|
||||
if (receiverType != null) {
|
||||
ownerFullName = receiverType;
|
||||
TypeInfo receiverType = document.resolveExpressionType(context.receiverExpression, context.prefixStart);
|
||||
if (receiverType != null && receiverType.isResolved()) {
|
||||
ownerFullName = receiverType.getFullName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +50,7 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
}
|
||||
|
||||
// Filter and score by prefix, then apply usage boosts
|
||||
filterAndScore(items, context.prefix, context.isMemberAccess, ownerFullName);
|
||||
filterAndScore(items, context.prefix, context.isMemberAccess, false, ownerFullName);
|
||||
|
||||
// Sort by score
|
||||
Collections.sort(items);
|
||||
@@ -77,35 +65,75 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
|
||||
/**
|
||||
* Add suggestions for member access (after dot).
|
||||
* Uses ScriptDocument's unified type resolution system.
|
||||
*/
|
||||
private void addMemberSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
protected void addMemberSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
String receiverExpr = context.receiverExpression;
|
||||
if (receiverExpr == null || receiverExpr.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Infer type of receiver
|
||||
String receiverType = inferExpressionType(receiverExpr);
|
||||
if (receiverType == null) {
|
||||
|
||||
// Use ScriptDocument's resolveExpressionType - handles both Java and JS
|
||||
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, context.prefixStart);
|
||||
if (receiverType == null || !receiverType.isResolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
JSTypeInfo typeInfo = registry.getType(receiverType);
|
||||
if (typeInfo == null) {
|
||||
|
||||
// For JS types, check JSTypeRegistry
|
||||
JSTypeInfo jsTypeInfo = registry.getType(receiverType.getSimpleName());
|
||||
if (jsTypeInfo != null) {
|
||||
// Add methods
|
||||
addMethodsFromType(jsTypeInfo, items, new HashSet<>());
|
||||
// Add fields
|
||||
addFieldsFromType(jsTypeInfo, items, new HashSet<>());
|
||||
return;
|
||||
}
|
||||
|
||||
// Add methods
|
||||
addMethodsFromType(typeInfo, items, new HashSet<>());
|
||||
|
||||
// Add fields
|
||||
addFieldsFromType(typeInfo, items, new HashSet<>());
|
||||
|
||||
// For Java types with Java class, use reflection to get members
|
||||
Class<?> javaClass = receiverType.getJavaClass();
|
||||
if (javaClass != null) {
|
||||
try {
|
||||
// Add methods from Java reflection
|
||||
for (java.lang.reflect.Method method : javaClass.getMethods()) {
|
||||
String methodName = method.getName();
|
||||
StringBuilder signature = new StringBuilder();
|
||||
signature.append(methodName).append("(");
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
if (i > 0)
|
||||
signature.append(", ");
|
||||
signature.append(params[i].getSimpleName());
|
||||
}
|
||||
signature.append(")");
|
||||
|
||||
items.add(new AutocompleteItem.Builder()
|
||||
.name(methodName)
|
||||
.insertText(methodName)
|
||||
.kind(AutocompleteItem.Kind.METHOD)
|
||||
.typeLabel(method.getReturnType().getSimpleName())
|
||||
.signature(signature.toString())
|
||||
.build());
|
||||
}
|
||||
|
||||
// Add fields from Java reflection
|
||||
for (java.lang.reflect.Field field : javaClass.getFields()) {
|
||||
items.add(new AutocompleteItem.Builder()
|
||||
.name(field.getName())
|
||||
.insertText(field.getName())
|
||||
.kind(AutocompleteItem.Kind.FIELD)
|
||||
.typeLabel(field.getType().getSimpleName())
|
||||
.build());
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
// Can't access members, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively add methods from a type and its parents.
|
||||
*/
|
||||
private void addMethodsFromType(JSTypeInfo type, List<AutocompleteItem> items, Set<String> added) {
|
||||
protected void addMethodsFromType(JSTypeInfo type, List<AutocompleteItem> items, Set<String> added) {
|
||||
for (JSMethodInfo method : type.getMethods().values()) {
|
||||
String name = method.getName();
|
||||
// Skip overload markers (name$1, name$2, etc.)
|
||||
@@ -127,7 +155,7 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
/**
|
||||
* Recursively add fields from a type and its parents.
|
||||
*/
|
||||
private void addFieldsFromType(JSTypeInfo type, List<AutocompleteItem> items, Set<String> added) {
|
||||
protected void addFieldsFromType(JSTypeInfo type, List<AutocompleteItem> items, Set<String> added) {
|
||||
for (JSFieldInfo field : type.getFields().values()) {
|
||||
if (!added.contains(field.getName())) {
|
||||
added.add(field.getName());
|
||||
@@ -143,131 +171,55 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
|
||||
/**
|
||||
* Add suggestions based on current scope (not after a dot).
|
||||
* Uses ScriptDocument's unified data structures for variables and functions.
|
||||
*/
|
||||
private void addScopeSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
protected void addScopeSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
|
||||
int pos = context.cursorPosition;
|
||||
|
||||
// Find containing method
|
||||
MethodInfo containingMethod = document.findContainingMethod(pos);
|
||||
|
||||
// Add local variables
|
||||
for (Map.Entry<String, String> entry : variableTypes.entrySet()) {
|
||||
String varName = entry.getKey();
|
||||
String varType = entry.getValue();
|
||||
if (containingMethod != null) {
|
||||
Map<String, FieldInfo> locals = document.getLocalsForMethod(containingMethod);
|
||||
if (locals != null) {
|
||||
for (FieldInfo local : locals.values()) {
|
||||
if (local.isVisibleAt(pos)) {
|
||||
items.add(AutocompleteItem.fromField(local));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add method parameters
|
||||
for (FieldInfo param : containingMethod.getParameters()) {
|
||||
items.add(AutocompleteItem.fromField(param));
|
||||
}
|
||||
}
|
||||
|
||||
// Add global fields
|
||||
for (FieldInfo globalField : document.getGlobalFields().values()) {
|
||||
if (globalField.isVisibleAt(pos)) {
|
||||
items.add(AutocompleteItem.fromField(globalField));
|
||||
}
|
||||
}
|
||||
|
||||
// Add script-defined methods
|
||||
for (MethodInfo method : document.getAllMethods()) {
|
||||
items.add(AutocompleteItem.fromMethod(method));
|
||||
|
||||
items.add(new AutocompleteItem.Builder()
|
||||
.name(varName)
|
||||
.insertText(varName)
|
||||
.kind(AutocompleteItem.Kind.VARIABLE)
|
||||
.typeLabel(varType != null ? varType : "any")
|
||||
.build());
|
||||
}
|
||||
|
||||
// Add hook functions
|
||||
for (String hookName : registry.getHookNames()) {
|
||||
List<JSTypeRegistry.HookSignature> sigs = registry.getHookSignatures(hookName);
|
||||
if (!sigs.isEmpty()) {
|
||||
JSTypeRegistry.HookSignature sig = sigs.get(0);
|
||||
items.add(new AutocompleteItem.Builder()
|
||||
.name(hookName)
|
||||
.insertText(hookName)
|
||||
.kind(AutocompleteItem.Kind.METHOD)
|
||||
.typeLabel("hook")
|
||||
.signature("function " + hookName + "(e: " + sig.paramType + ")")
|
||||
.documentation(sig.doc)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
// Add global types
|
||||
for (String typeName : registry.getTypeNames()) {
|
||||
JSTypeInfo type = registry.getType(typeName);
|
||||
if (type != null) {
|
||||
items.add(new AutocompleteItem.Builder()
|
||||
.name(typeName)
|
||||
.insertText(typeName)
|
||||
.kind(AutocompleteItem.Kind.CLASS)
|
||||
.typeLabel("interface")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
// Add JavaScript keywords
|
||||
addJSKeywords(items);
|
||||
addKeywords(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer the type of an expression.
|
||||
*/
|
||||
private String inferExpressionType(String expr) {
|
||||
return resolveReceiverType(expr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the type of a receiver expression for member access.
|
||||
*/
|
||||
private String resolveReceiverType(String expr) {
|
||||
if (expr == null || expr.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
expr = expr.trim();
|
||||
|
||||
// Check if it's a known variable
|
||||
if (variableTypes.containsKey(expr)) {
|
||||
return variableTypes.get(expr);
|
||||
}
|
||||
|
||||
// Handle method chain: x.y.z() -> resolve step by step
|
||||
if (expr.contains(".")) {
|
||||
String[] parts = expr.split("\\.");
|
||||
String currentType = null;
|
||||
|
||||
// Start with the first part
|
||||
if (variableTypes.containsKey(parts[0])) {
|
||||
currentType = variableTypes.get(parts[0]);
|
||||
}
|
||||
|
||||
if (currentType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Walk through the chain
|
||||
for (int i = 1; i < parts.length; i++) {
|
||||
String member = parts[i];
|
||||
// Remove () for method calls
|
||||
if (member.endsWith("()")) {
|
||||
member = member.substring(0, member.length() - 2);
|
||||
}
|
||||
|
||||
JSTypeInfo typeInfo = registry.getType(currentType);
|
||||
if (typeInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check method
|
||||
JSMethodInfo method = typeInfo.getMethod(member);
|
||||
if (method != null) {
|
||||
currentType = method.getReturnType();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check field
|
||||
JSFieldInfo field = typeInfo.getField(member);
|
||||
if (field != null) {
|
||||
currentType = field.getType();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unknown member
|
||||
return null;
|
||||
}
|
||||
|
||||
return currentType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add JavaScript keywords.
|
||||
*/
|
||||
private void addJSKeywords(List<AutocompleteItem> items) {
|
||||
protected void addKeywords(List<AutocompleteItem> items) {
|
||||
String[] keywords = {
|
||||
"function", "var", "let", "const", "if", "else", "for", "while", "do",
|
||||
"switch", "case", "break", "continue", "return", "try", "catch", "finally",
|
||||
@@ -280,44 +232,8 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
items.add(AutocompleteItem.keyword(keyword));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items by prefix, calculate match scores, and apply usage boosts.
|
||||
*/
|
||||
private void filterAndScore(List<AutocompleteItem> items, String prefix,
|
||||
boolean isMemberAccess, String ownerFullName) {
|
||||
UsageTracker tracker = UsageTracker.getJSInstance();
|
||||
|
||||
if (prefix == null || prefix.isEmpty()) {
|
||||
for (AutocompleteItem item : items) {
|
||||
item.calculateMatchScore("");
|
||||
applyUsageBoost(item, tracker, ownerFullName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-member access (first word), require strict prefix matching
|
||||
// For member access (after dot), allow fuzzy/contains matching
|
||||
boolean requirePrefix = !isMemberAccess;
|
||||
|
||||
Iterator<AutocompleteItem> iter = items.iterator();
|
||||
while (iter.hasNext()) {
|
||||
AutocompleteItem item = iter.next();
|
||||
int score = item.calculateMatchScore(prefix, requirePrefix);
|
||||
if (score < 0) {
|
||||
iter.remove();
|
||||
} else {
|
||||
applyUsageBoost(item, tracker, ownerFullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply usage-based score boost to an item.
|
||||
*/
|
||||
private void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) {
|
||||
int usageCount = tracker.getUsageCount(item, ownerFullName);
|
||||
int boost = UsageTracker.calculateUsageBoost(usageCount);
|
||||
item.addScoreBoost(boost);
|
||||
|
||||
protected UsageTracker getUsageTracker() {
|
||||
return UsageTracker.getJSInstance();
|
||||
}
|
||||
}
|
||||
+16
-14
@@ -19,7 +19,7 @@ import java.util.*;
|
||||
*/
|
||||
public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
|
||||
private ScriptDocument document;
|
||||
protected ScriptDocument document;
|
||||
|
||||
public void setDocument(ScriptDocument document) {
|
||||
this.document = document;
|
||||
@@ -71,7 +71,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
/**
|
||||
* Add suggestions for member access (after dot).
|
||||
*/
|
||||
private void addMemberSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
protected void addMemberSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
String receiverExpr = context.receiverExpression;
|
||||
if (receiverExpr == null || receiverExpr.isEmpty()) {
|
||||
return;
|
||||
@@ -132,7 +132,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
/**
|
||||
* Add members from a script-defined type.
|
||||
*/
|
||||
private void addScriptTypeMembers(ScriptTypeInfo scriptType, List<AutocompleteItem> items, boolean isStaticContext) {
|
||||
protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List<AutocompleteItem> items, boolean isStaticContext) {
|
||||
// Add methods (getMethods returns Map<String, List<MethodInfo>>)
|
||||
for (List<MethodInfo> overloads : scriptType.getMethods().values()) {
|
||||
for (MethodInfo method : overloads) {
|
||||
@@ -181,7 +181,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
/**
|
||||
* Add suggestions based on current scope (not after a dot).
|
||||
*/
|
||||
private void addScopeSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
protected void addScopeSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
int pos = context.cursorPosition;
|
||||
|
||||
// Find containing method
|
||||
@@ -256,7 +256,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
* Add suggestions for unimported classes that match the prefix.
|
||||
* These will trigger auto-import when selected.
|
||||
*/
|
||||
private void addUnimportedClassSuggestions(String prefix, List<AutocompleteItem> items) {
|
||||
protected void addUnimportedClassSuggestions(String prefix, List<AutocompleteItem> items) {
|
||||
// Get the type resolver
|
||||
TypeResolver resolver = TypeResolver.getInstance();
|
||||
// Find classes matching this prefix (not just exact matches)
|
||||
@@ -312,7 +312,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
/**
|
||||
* Add Java keywords.
|
||||
*/
|
||||
private void addKeywords(List<AutocompleteItem> items) {
|
||||
protected void addKeywords(List<AutocompleteItem> items) {
|
||||
String[] keywords = {
|
||||
"if", "else", "for", "while", "do", "switch", "case", "break", "continue",
|
||||
"return", "try", "catch", "finally", "throw", "throws", "new", "this", "super",
|
||||
@@ -331,7 +331,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
* Find the enclosing script type at a position.
|
||||
* This is a workaround since findEnclosingScriptType is package-private.
|
||||
*/
|
||||
private ScriptTypeInfo findEnclosingType(int position) {
|
||||
protected ScriptTypeInfo findEnclosingType(int position) {
|
||||
for (ScriptTypeInfo type : document.getScriptTypesMap().values()) {
|
||||
if (type.containsPosition(position)) {
|
||||
return type;
|
||||
@@ -344,18 +344,20 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
* Check if the receiver expression represents static access (class type).
|
||||
* Similar logic to FieldChainMarker.isStaticContext().
|
||||
*/
|
||||
private boolean isStaticAccess(String receiverExpr, int position) {
|
||||
protected boolean isStaticAccess(String receiverExpr, int position) {
|
||||
// Try to resolve the receiver expression as a type
|
||||
TypeInfo typeCheck = document.resolveType(receiverExpr);
|
||||
return typeCheck != null && typeCheck.isResolved();
|
||||
}
|
||||
|
||||
protected UsageTracker getUsageTracker() {
|
||||
return UsageTracker.getJavaInstance();
|
||||
}
|
||||
/**
|
||||
* Filter items by prefix, calculate match scores, apply usage boosts, and penalize static members in instance contexts.
|
||||
*/
|
||||
private void filterAndScore(List<AutocompleteItem> items, String prefix,
|
||||
protected void filterAndScore(List<AutocompleteItem> items, String prefix,
|
||||
boolean isMemberAccess, boolean isStaticContext, String ownerFullName) {
|
||||
UsageTracker tracker = UsageTracker.getJavaInstance();
|
||||
UsageTracker tracker = getUsageTracker();
|
||||
|
||||
if (prefix == null || prefix.isEmpty()) {
|
||||
// No filtering needed, all items get a base score + usage boost
|
||||
@@ -390,7 +392,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
/**
|
||||
* Apply usage-based score boost to an item.
|
||||
*/
|
||||
private void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) {
|
||||
protected void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) {
|
||||
int usageCount = tracker.getUsageCount(item, ownerFullName);
|
||||
int boost = UsageTracker.calculateUsageBoost(usageCount);
|
||||
item.addScoreBoost(boost);
|
||||
@@ -402,7 +404,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
* accessing through an instance (e.g., Minecraft.getMinecraft().getMinecraft()).
|
||||
* However, if the item is a very strong match (exact prefix), don't penalize as much.
|
||||
*/
|
||||
private void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, boolean isStaticContext) {
|
||||
protected void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, boolean isStaticContext) {
|
||||
// Only apply penalty in member access contexts (after dot)
|
||||
if (!isMemberAccess) {
|
||||
return;
|
||||
@@ -434,7 +436,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
* Apply penalty to inherited Object methods to push them to bottom.
|
||||
* Strong matches get lighter penalty, weak matches get pushed all the way down.
|
||||
*/
|
||||
private void applyObjectMethodPenalty(AutocompleteItem item) {
|
||||
protected void applyObjectMethodPenalty(AutocompleteItem item) {
|
||||
if (!item.isInheritedObjectMethod()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user