From ac922107715e993b3055cb084d49dc4281b909ff Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 03:59:51 +0200 Subject: [PATCH] Implemented AUTO IMPORT SUGGESTION COMPLETIONS --- .../client/gui/util/GuiScriptTextArea.java | 131 ++++++++++++++++++ .../script/autocomplete/AutocompleteItem.java | 49 +++++-- .../autocomplete/AutocompleteManager.java | 14 +- .../JavaAutocompleteProvider.java | 63 +++++++++ .../script/interpreter/type/TypeResolver.java | 23 +++ 5 files changed, 271 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index de4ef92b6..f49547c2e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -499,6 +499,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollToCursor(); } + @Override + public void addImport(String importPath) { + // Add import statement and sort all imports + addAndSortImport(importPath); + } + @Override public int getCursorPosition() { return selection.getCursorPosition(); @@ -2534,6 +2540,131 @@ public class GuiScriptTextArea extends GuiNpcTextField { mouseY >= menuY && mouseY <= menuY + menuHeight; } + // ==================== AUTO-IMPORT ==================== + + /** + * Add an import statement and sort all imports. + */ + private void addAndSortImport(String importPath) { + String currentText = this.text; + int savedCursorPos = selection.getCursorPosition(); + + // Find all existing imports + java.util.regex.Pattern importPattern = java.util.regex.Pattern.compile( + "(?m)^\\s*import\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_*][A-Za-z0-9_]*)*)\\s*;\\s*$" + ); + java.util.regex.Matcher matcher = importPattern.matcher(currentText); + + java.util.List imports = new java.util.ArrayList<>(); + int firstImportStart = -1; + int lastImportEnd = -1; + + while (matcher.find()) { + String importStatement = matcher.group(0); + String importPathFound = matcher.group(1).replaceAll("\\s+", ""); + + if (firstImportStart == -1) { + firstImportStart = matcher.start(); + } + lastImportEnd = matcher.end(); + + // Skip if this is the import we're trying to add + if (!importPathFound.equals(importPath)) { + imports.add(new ImportEntry(importPathFound, importStatement.trim())); + } + } + + // Add the new import + imports.add(new ImportEntry(importPath, "import " + importPath + ";")); + + // Sort imports + java.util.Collections.sort(imports, new java.util.Comparator() { + @Override + public int compare(ImportEntry a, ImportEntry b) { + // Sort order: java.*, javax.*, then others alphabetically + boolean aIsJava = a.path.startsWith("java."); + boolean aIsJavax = a.path.startsWith("javax."); + boolean bIsJava = b.path.startsWith("java."); + boolean bIsJavax = b.path.startsWith("javax."); + + if (aIsJava && !bIsJava) return -1; + if (!aIsJava && bIsJava) return 1; + if (aIsJavax && !bIsJavax && !bIsJava) return -1; + if (!aIsJavax && bIsJavax && !aIsJava) return 1; + + return a.path.compareTo(b.path); + } + }); + + // Build the new import block + StringBuilder importBlock = new StringBuilder(); + String prevPackage = ""; + for (ImportEntry entry : imports) { + // Add blank line between different top-level packages + String topPackage = entry.path.contains(".") ? + entry.path.substring(0, entry.path.indexOf('.')) : entry.path; + if (!prevPackage.isEmpty() && !topPackage.equals(prevPackage)) { + importBlock.append("\n"); + } + importBlock.append(entry.statement).append("\n"); + prevPackage = topPackage; + } + + // Determine where to insert/replace imports + String newText; + int cursorAdjustment = 0; + + if (firstImportStart != -1) { + // Replace existing import block + String before = currentText.substring(0, firstImportStart); + String after = currentText.substring(lastImportEnd); + newText = before + importBlock.toString() + after; + + // Adjust cursor if it's after the import block + int newImportEnd = firstImportStart + importBlock.length(); + if (savedCursorPos >= lastImportEnd) { + cursorAdjustment = newImportEnd - lastImportEnd; + } + } else { + // No existing imports - add at top after package statement (if any) + java.util.regex.Pattern packagePattern = java.util.regex.Pattern.compile( + "(?m)^\\s*package\\s+[A-Za-z_][A-Za-z0-9_.]*\\s*;\\s*$" + ); + java.util.regex.Matcher pkgMatcher = packagePattern.matcher(currentText); + + int insertPos = 0; + if (pkgMatcher.find()) { + insertPos = pkgMatcher.end(); + // Add blank line after package + newText = currentText.substring(0, insertPos) + "\n" + + importBlock.toString() + "\n" + currentText.substring(insertPos); + cursorAdjustment = importBlock.length() + 2; // +2 for the newlines + } else { + // Insert at very beginning + newText = importBlock.toString() + "\n" + currentText; + cursorAdjustment = importBlock.length() + 1; + } + } + + // Apply the changes + setText(newText); + selection.reset(savedCursorPos + cursorAdjustment); + scrollToCursor(); + } + + /** + * Helper class for tracking imports during sorting. + */ + private static class ImportEntry { + String path; + String statement; + + ImportEntry(String path, String statement) { + this.path = path; + this.statement = statement; + } + } + // ==================== INNER CLASSES ==================== public static class UndoData { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 58e38aa30..96da52b9d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -45,12 +45,17 @@ public class AutocompleteItem implements Comparable { private final Object sourceData; // Original source (MethodInfo, FieldInfo, etc.) private final boolean deprecated; // Whether this item is deprecated + // Import tracking + private final boolean requiresImport; // Whether selecting this item requires adding an import + private final String importPath; // Full path for import (e.g., "net.minecraft.client.Minecraft") + // Match scoring private int matchScore = 0; // How well this matches the query private int[] matchIndices; // Indices of matched characters for highlighting private AutocompleteItem(String name, String insertText, Kind kind, String typeLabel, - String signature, String documentation, Object sourceData, boolean deprecated) { + String signature, String documentation, Object sourceData, boolean deprecated, + boolean requiresImport, String importPath) { this.name = name; this.insertText = insertText; this.kind = kind; @@ -59,6 +64,8 @@ public class AutocompleteItem implements Comparable { this.documentation = documentation; this.sourceData = sourceData; this.deprecated = deprecated; + this.requiresImport = requiresImport; + this.importPath = importPath; } // ==================== FACTORY METHODS ==================== @@ -105,7 +112,9 @@ public class AutocompleteItem implements Comparable { signature, method.getDocumentation(), method, - false // TODO: Check for @Deprecated annotation + false, // TODO: Check for @Deprecated annotation + false, // Methods don't require imports + null ); } @@ -139,7 +148,9 @@ public class AutocompleteItem implements Comparable { typeLabel + " " + field.getName(), null, field, - false + false, + false, // Fields don't require imports + null ); } @@ -167,7 +178,9 @@ public class AutocompleteItem implements Comparable { type.getFullName(), null, type, - false + false, + false, // Will be overridden for unimported types + null ); } @@ -192,7 +205,9 @@ public class AutocompleteItem implements Comparable { method.getSignature(), method.getDocumentation(), method, - false + false, + false, + null ); } @@ -208,7 +223,9 @@ public class AutocompleteItem implements Comparable { field.toString(), field.getDocumentation(), field, - false + false, + false, + null ); } @@ -224,7 +241,9 @@ public class AutocompleteItem implements Comparable { null, null, null, - false + false, + false, + null ); } @@ -339,6 +358,8 @@ public class AutocompleteItem implements Comparable { public String getDocumentation() { return documentation; } public Object getSourceData() { return sourceData; } public boolean isDeprecated() { return deprecated; } + public boolean requiresImport() { return requiresImport; } + public String getImportPath() { return importPath; } public int getMatchScore() { return matchScore; } public int[] getMatchIndices() { return matchIndices; } @@ -409,6 +430,8 @@ public class AutocompleteItem implements Comparable { private String documentation; private Object sourceData; private boolean deprecated = false; + private boolean requiresImport = false; + private String importPath = null; public Builder name(String name) { this.name = name; @@ -450,12 +473,22 @@ public class AutocompleteItem implements Comparable { return this; } + public Builder requiresImport(boolean requiresImport) { + this.requiresImport = requiresImport; + return this; + } + + public Builder importPath(String importPath) { + this.importPath = importPath; + return this; + } + public AutocompleteItem build() { if (insertText == null) { insertText = name; } return new AutocompleteItem(name, insertText, kind, typeLabel, - signature, documentation, sourceData, deprecated); + signature, documentation, sourceData, deprecated, requiresImport, importPath); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index d2c4aa016..ace3cab81 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -75,6 +75,12 @@ public class AutocompleteManager { */ void replaceTextRange(String text, int startPosition, int endPosition); + /** + * Add an import statement and sort all imports. + * @param importPath Full path of the class to import (e.g., "net.minecraft.client.Minecraft") + */ + void addImport(String importPath); + /** * Get current cursor position. */ @@ -510,9 +516,15 @@ public class AutocompleteManager { } } - // Replace from prefixStart to endPos using the new method + // IMPORTANT: Do the text replacement FIRST, then add import + // This prevents position corruption since import adds text at the TOP insertCallback.replaceTextRange(insertText, prefixStartPosition, endPos); + // If this item requires an import, add it AFTER the text replacement + if (item.requiresImport() && item.getImportPath() != null) { + insertCallback.addImport(item.getImportPath()); + } + active = false; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 23f6e6aea..4126d8526 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -224,10 +224,73 @@ public class JavaAutocompleteProvider implements AutocompleteProvider { items.add(AutocompleteItem.fromType(scriptType)); } + // Add unimported classes that match the prefix (for auto-import) + if (context.prefix != null && context.prefix.length() >= 2 && Character.isUpperCase(context.prefix.charAt(0))) { + addUnimportedClassSuggestions(context.prefix, items); + } + // Add keywords addKeywords(items); } + /** + * Add suggestions for unimported classes that match the prefix. + * These will trigger auto-import when selected. + */ + private void addUnimportedClassSuggestions(String prefix, List items) { + // Get the type resolver + TypeResolver resolver = TypeResolver.getInstance(); + + // Find classes matching this prefix (not just exact matches) + List matchingClasses = resolver.findClassesByPrefix(prefix, 15); + + // Track what's already imported to avoid duplicates + Set importedFullNames = new HashSet<>(); + for (TypeInfo imported : document.getImportedTypes()) { + importedFullNames.add(imported.getFullName()); + } + + // Also track simple names already in the list to avoid showing both + // imported and unimported versions of the same class + Set existingSimpleNames = new HashSet<>(); + for (AutocompleteItem item : items) { + if (item.getKind() == AutocompleteItem.Kind.CLASS || + item.getKind() == AutocompleteItem.Kind.ENUM) { + existingSimpleNames.add(item.getName()); + } + } + + for (String fullName : matchingClasses) { + // Skip if already imported + if (importedFullNames.contains(fullName)) { + continue; + } + + TypeInfo type = resolver.resolveFullName(fullName); + if (type != null && type.isResolved()) { + // Skip if a class with this simple name is already in the list + if (existingSimpleNames.contains(type.getSimpleName())) { + continue; + } + + // Create an item that requires import + AutocompleteItem item = new AutocompleteItem.Builder() + .name(type.getSimpleName()) + .insertText(type.getSimpleName()) + .kind(type.getKind() == TypeInfo.Kind.ENUM ? + AutocompleteItem.Kind.ENUM : AutocompleteItem.Kind.CLASS) + .typeLabel(type.getPackageName()) + .signature(type.getFullName()) + .sourceData(type) + .requiresImport(true) + .importPath(fullName) + .build(); + items.add(item); + existingSimpleNames.add(type.getSimpleName()); + } + } + } + /** * Add Java keywords. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java index b686594a1..1be0617d7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java @@ -391,6 +391,29 @@ public class TypeResolver { word.equals("native") || word.equals("strictfp"); } + // ==================== CLASS SEARCH ==================== + + /** + * Search for classes matching the given prefix. + * Uses ClassIndex for O(n) lookup where n is the number of indexed classes. + * + * @param prefix The prefix to match (case-sensitive for class names) + * @param maxResults Maximum number of results to return + * @return List of fully-qualified class names that match + */ + public List findClassesByPrefix(String prefix, int maxResults) { + return ClassIndex.getInstance().findByPrefix(prefix, maxResults); + } + + /** + * @deprecated Use findClassesByPrefix instead for prefix matching + */ + @Deprecated + public List findClassesBySimpleName(String simpleName, int maxResults) { + // Redirect to prefix matching for backwards compatibility + return findClassesByPrefix(simpleName, maxResults); + } + /** * Represents a type occurrence within generic content. */