mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-14 01:24:09 +00:00
Autocomplete suggestions UsageTracker implementation
This commit is contained in:
1 parent
a496061455
commit
7b7667f2c5
5 files changed
+387
-11
No files matched your search
@@ -373,9 +373,17 @@ public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
return calculateMatchScore(query, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a boost to the match score (e.g., from usage tracking).
|
||||
*/
|
||||
public void addScoreBoost(int boost) {
|
||||
this.matchScore += boost;
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public String getName() { return name; }
|
||||
public String getSearchName() { return searchName; }
|
||||
public String getInsertText() { return insertText; }
|
||||
public Kind getKind() { return kind; }
|
||||
public String getTypeLabel() { return typeLabel; }
|
||||
|
||||
+38
@@ -3,6 +3,7 @@ package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import org.lwjgl.input.Mouse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -53,6 +54,12 @@ public class AutocompleteManager {
|
||||
/** Whether this was an explicit trigger (Ctrl+Space) */
|
||||
private boolean explicitTrigger = false;
|
||||
|
||||
/** The full class name of the current receiver type (for member access tracking) */
|
||||
private String currentReceiverFullName = null;
|
||||
|
||||
/** Whether current context is member access */
|
||||
private boolean currentIsMemberAccess = false;
|
||||
|
||||
/** Callback for text insertion */
|
||||
private InsertCallback insertCallback;
|
||||
|
||||
@@ -360,6 +367,18 @@ public class AutocompleteManager {
|
||||
int prefixStart, boolean isMemberAccess, String receiverExpr) {
|
||||
if (insertCallback == null || document == null) return;
|
||||
|
||||
// Track context for usage recording when item is selected
|
||||
currentIsMemberAccess = isMemberAccess;
|
||||
currentReceiverFullName = null;
|
||||
|
||||
// Resolve receiver type if member access
|
||||
if (isMemberAccess && receiverExpr != null) {
|
||||
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, prefixStart);
|
||||
if (receiverType != null && receiverType.isResolved()) {
|
||||
currentReceiverFullName = receiverType.getFullName();
|
||||
}
|
||||
}
|
||||
|
||||
// Build context
|
||||
int lineNumber = getLineNumber(text, cursorPosition);
|
||||
String currentLine = getCurrentLine(text, cursorPosition);
|
||||
@@ -491,6 +510,9 @@ public class AutocompleteManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record usage for learning
|
||||
recordUsage(item);
|
||||
|
||||
String insertText = item.getInsertText();
|
||||
|
||||
// Smart tab completion: replace till next separator
|
||||
@@ -528,6 +550,22 @@ public class AutocompleteManager {
|
||||
active = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that the user selected an autocomplete item for usage tracking.
|
||||
*/
|
||||
private void recordUsage(AutocompleteItem item) {
|
||||
if (document == null) return;
|
||||
|
||||
UsageTracker tracker = document.isJavaScript() ?
|
||||
UsageTracker.getJSInstance() : UsageTracker.getJavaInstance();
|
||||
|
||||
// For member access, use the resolved receiver type
|
||||
// For standalone items (types, keywords, variables), owner is null
|
||||
String owner = currentIsMemberAccess ? currentReceiverFullName : null;
|
||||
|
||||
tracker.recordUsage(item, owner);
|
||||
}
|
||||
|
||||
// ==================== DISMISS ====================
|
||||
|
||||
/**
|
||||
|
||||
+40
-5
@@ -44,6 +44,15 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
public List<AutocompleteItem> getSuggestions(Context context) {
|
||||
List<AutocompleteItem> items = new ArrayList<>();
|
||||
|
||||
// Resolve owner type for usage tracking
|
||||
String ownerFullName = null;
|
||||
if (context.isMemberAccess && context.receiverExpression != null) {
|
||||
String receiverType = resolveReceiverType(context.receiverExpression);
|
||||
if (receiverType != null) {
|
||||
ownerFullName = receiverType;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.isMemberAccess) {
|
||||
// Member access: resolve type of receiver and get its members
|
||||
addMemberSuggestions(context, items);
|
||||
@@ -52,8 +61,8 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
addScopeSuggestions(context, items);
|
||||
}
|
||||
|
||||
// Filter and score by prefix
|
||||
filterAndScore(items, context.prefix);
|
||||
// Filter and score by prefix, then apply usage boosts
|
||||
filterAndScore(items, context.prefix, context.isMemberAccess, ownerFullName);
|
||||
|
||||
// Sort by score
|
||||
Collections.sort(items);
|
||||
@@ -186,6 +195,13 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
* 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;
|
||||
}
|
||||
@@ -266,23 +282,42 @@ public class JSAutocompleteProvider implements AutocompleteProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items by prefix and calculate match scores.
|
||||
* Filter items by prefix, calculate match scores, and apply usage boosts.
|
||||
*/
|
||||
private void filterAndScore(List<AutocompleteItem> items, String prefix) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
+31
-6
@@ -37,6 +37,16 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Resolve owner type for usage tracking
|
||||
String ownerFullName = null;
|
||||
if (context.isMemberAccess && context.receiverExpression != null) {
|
||||
TypeInfo receiverType = document.resolveExpressionType(
|
||||
context.receiverExpression, context.prefixStart);
|
||||
if (receiverType != null && receiverType.isResolved()) {
|
||||
ownerFullName = receiverType.getFullName();
|
||||
}
|
||||
}
|
||||
|
||||
if (context.isMemberAccess) {
|
||||
// Member access: resolve type of receiver and get its members
|
||||
addMemberSuggestions(context, items);
|
||||
@@ -45,8 +55,8 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
addScopeSuggestions(context, items);
|
||||
}
|
||||
|
||||
// Filter and score by prefix
|
||||
filterAndScore(items, context.prefix, context.isMemberAccess);
|
||||
// Filter and score by prefix, then apply usage boosts
|
||||
filterAndScore(items, context.prefix, context.isMemberAccess, ownerFullName);
|
||||
|
||||
// Sort by score
|
||||
Collections.sort(items);
|
||||
@@ -332,13 +342,17 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items by prefix and calculate match scores.
|
||||
* Filter items by prefix, calculate match scores, and apply usage boosts.
|
||||
*/
|
||||
private void filterAndScore(List<AutocompleteItem> items, String prefix, boolean isMemberAccess) {
|
||||
private void filterAndScore(List<AutocompleteItem> items, String prefix,
|
||||
boolean isMemberAccess, String ownerFullName) {
|
||||
UsageTracker tracker = UsageTracker.getJavaInstance();
|
||||
|
||||
if (prefix == null || prefix.isEmpty()) {
|
||||
// No filtering needed, all items get a base score
|
||||
// No filtering needed, all items get a base score + usage boost
|
||||
for (AutocompleteItem item : items) {
|
||||
item.calculateMatchScore("", false);
|
||||
applyUsageBoost(item, tracker, ownerFullName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -347,17 +361,28 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
// For member access (after dot), allow fuzzy/contains matching
|
||||
boolean requirePrefix = !isMemberAccess;
|
||||
|
||||
// Filter and score
|
||||
// Filter, score, and apply usage boosts
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for AutocompleteItem to handle cases without source data.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Tracks user autocomplete selections to improve suggestion ranking.
|
||||
* Usage counts are persisted to JSON and used to boost frequently selected items.
|
||||
*
|
||||
* Key format: "ownerFullName|memberName|KIND"
|
||||
* - ownerFullName: Full class name for members, empty for standalone items
|
||||
* - memberName: The item name (method, field, type, etc.)
|
||||
* - KIND: The item kind (METHOD, FIELD, CLASS, ENUM, etc.)
|
||||
*
|
||||
* Examples:
|
||||
* - "net.minecraft.client.Minecraft|getMinecraft|METHOD"
|
||||
* - "net.minecraft.entity.player.EntityPlayer|inventory|FIELD"
|
||||
* - "|EntityPlayer|CLASS" (standalone type suggestion)
|
||||
* - "|if|KEYWORD"
|
||||
*/
|
||||
public class UsageTracker {
|
||||
|
||||
private static final String JAVA_FILE = "java_usages.json";
|
||||
private static final String JS_FILE = "js_usages.json";
|
||||
private static final long SAVE_INTERVAL_MS = 60_000; // Auto-save every 60 seconds
|
||||
private static final int USAGE_SCORE_MULTIPLIER = 50; // Score boost per usage
|
||||
private static final int MAX_USAGE_BOOST = 5000; // Cap the boost to prevent runaway scores
|
||||
|
||||
private static UsageTracker javaInstance;
|
||||
private static UsageTracker jsInstance;
|
||||
private static boolean initialized = false;
|
||||
|
||||
private final Map<String, Integer> usageCounts = new ConcurrentHashMap<>();
|
||||
private final File file;
|
||||
private final AtomicBoolean dirty = new AtomicBoolean(false);
|
||||
private final AtomicBoolean loaded = new AtomicBoolean(false);
|
||||
|
||||
private static ScheduledExecutorService scheduler;
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
// ==================== SINGLETON ACCESS ====================
|
||||
|
||||
public static synchronized UsageTracker getJavaInstance() {
|
||||
ensureInitialized();
|
||||
if (javaInstance == null) {
|
||||
javaInstance = new UsageTracker(JAVA_FILE);
|
||||
javaInstance.load();
|
||||
}
|
||||
return javaInstance;
|
||||
}
|
||||
|
||||
public static synchronized UsageTracker getJSInstance() {
|
||||
ensureInitialized();
|
||||
if (jsInstance == null) {
|
||||
jsInstance = new UsageTracker(JS_FILE);
|
||||
jsInstance.load();
|
||||
}
|
||||
return jsInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the tracker system is initialized with auto-save and shutdown hook.
|
||||
*/
|
||||
private static void ensureInitialized() {
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
initialize();
|
||||
|
||||
// Add shutdown hook to save on JVM exit
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
shutdown();
|
||||
}, "UsageTracker-Shutdown"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the auto-save scheduler. Call once on client startup.
|
||||
*/
|
||||
public static void initialize() {
|
||||
if (scheduler == null || scheduler.isShutdown()) {
|
||||
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "UsageTracker-AutoSave");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
scheduler.scheduleAtFixedRate(() -> {
|
||||
saveAllIfDirty();
|
||||
}, SAVE_INTERVAL_MS, SAVE_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown and save all data. Call on client shutdown.
|
||||
*/
|
||||
public static void shutdown() {
|
||||
saveAllIfDirty();
|
||||
if (scheduler != null && !scheduler.isShutdown()) {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static void saveAllIfDirty() {
|
||||
if (javaInstance != null) {
|
||||
javaInstance.saveIfDirty();
|
||||
}
|
||||
if (jsInstance != null) {
|
||||
jsInstance.saveIfDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== INSTANCE METHODS ====================
|
||||
|
||||
private UsageTracker(String filename) {
|
||||
File dir = getDir();
|
||||
this.file = new File(dir, filename);
|
||||
}
|
||||
|
||||
private File getDir() {
|
||||
File dir = new File(CustomNpcs.Dir, "tracked_usages");
|
||||
if (!dir.exists()) {
|
||||
dir.mkdir();
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that the user selected an autocomplete item.
|
||||
*
|
||||
* @param owner The owner context (full class name for members, null/empty for standalone)
|
||||
* @param name The item name
|
||||
* @param kind The item kind
|
||||
*/
|
||||
public void recordUsage(String owner, String name, AutocompleteItem.Kind kind) {
|
||||
String key = buildKey(owner, name, kind);
|
||||
usageCounts.merge(key, 1, Integer::sum);
|
||||
dirty.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record usage directly from an AutocompleteItem.
|
||||
*
|
||||
* @param item The selected autocomplete item
|
||||
* @param ownerFullName The full class name of the owner (for member access), or null
|
||||
*/
|
||||
public void recordUsage(AutocompleteItem item, String ownerFullName) {
|
||||
String name = item.getSearchName() != null ? item.getSearchName() : item.getName();
|
||||
recordUsage(ownerFullName, name, item.getKind());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the usage count for an item.
|
||||
*
|
||||
* @param owner The owner context
|
||||
* @param name The item name
|
||||
* @param kind The item kind
|
||||
* @return The number of times this item was selected
|
||||
*/
|
||||
public int getUsageCount(String owner, String name, AutocompleteItem.Kind kind) {
|
||||
String key = buildKey(owner, name, kind);
|
||||
return usageCounts.getOrDefault(key, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage count directly from an AutocompleteItem.
|
||||
*/
|
||||
public int getUsageCount(AutocompleteItem item, String ownerFullName) {
|
||||
String name = item.getSearchName() != null ? item.getSearchName() : item.getName();
|
||||
return getUsageCount(ownerFullName, name, item.getKind());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the score boost based on usage count.
|
||||
* Uses a logarithmic scale to prevent very frequent items from dominating completely.
|
||||
*
|
||||
* @param usageCount The number of times the item was selected
|
||||
* @return The score boost to add
|
||||
*/
|
||||
public static int calculateUsageBoost(int usageCount) {
|
||||
if (usageCount <= 0) return 0;
|
||||
|
||||
// Logarithmic scaling: log2(count + 1) * multiplier
|
||||
// This gives diminishing returns for very high counts
|
||||
int boost = (int) (Math.log(usageCount + 1) / Math.log(2) * USAGE_SCORE_MULTIPLIER);
|
||||
return Math.min(boost, MAX_USAGE_BOOST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a unique key for an item.
|
||||
*/
|
||||
public static String buildKey(String owner, String name, AutocompleteItem.Kind kind) {
|
||||
String ownerPart = owner != null ? owner : "";
|
||||
String kindPart = kind != null ? kind.name() : "UNKNOWN";
|
||||
return ownerPart + "|" + name + "|" + kindPart;
|
||||
}
|
||||
|
||||
// ==================== PERSISTENCE ====================
|
||||
|
||||
/**
|
||||
* Load usage data from file.
|
||||
*/
|
||||
public void load() {
|
||||
if (loaded.get()) return;
|
||||
|
||||
if (!file.exists()) {
|
||||
loaded.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
Type type = new TypeToken<Map<String, Integer>>(){}.getType();
|
||||
Map<String, Integer> data = GSON.fromJson(reader, type);
|
||||
if (data != null) {
|
||||
usageCounts.putAll(data);
|
||||
}
|
||||
loaded.set(true);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[UsageTracker] Failed to load " + file.getName() + ": " + e.getMessage());
|
||||
loaded.set(true); // Mark as loaded even on failure to prevent retry loops
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save usage data to file if there are pending changes.
|
||||
*/
|
||||
public void saveIfDirty() {
|
||||
if (!dirty.compareAndSet(true, false)) {
|
||||
return; // Not dirty, nothing to save
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force save usage data to file.
|
||||
*/
|
||||
public void save() {
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
|
||||
GSON.toJson(usageCounts, writer);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[UsageTracker] Failed to save " + file.getName() + ": " + e.getMessage());
|
||||
dirty.set(true); // Mark dirty again so we retry later
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all usage data (for testing/reset purposes).
|
||||
*/
|
||||
public void clear() {
|
||||
usageCounts.clear();
|
||||
dirty.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of tracked items.
|
||||
*/
|
||||
public int size() {
|
||||
return usageCounts.size();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user