diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 56a8c13d8..1f791cff2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -27,6 +27,7 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.janino.JaninoScript; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; import org.lwjgl.opengl.GL11; @@ -38,6 +39,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { protected int activeTab = 0; @@ -221,13 +223,28 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // Set the scripting language for proper syntax highlighting // Use the container's language if available, otherwise fall back to handler's language String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); - activeArea.setLanguage(language); + activeArea.setLanguage(language); // Set the script context for context-aware hook autocomplete activeArea.setScriptContext(getScriptContext()); + activeArea.enableCodeHighlighting(); - // Set the script context for context-aware hook autocomplete - activeArea.setScriptContext(getScriptContext()); + // For JaninoScripts, add implicit imports (default imports + hook signature types) + // These allow the syntax highlighter to resolve types without explicit import statements + if (container instanceof JaninoScript) { + JaninoScript janinoScript = (JaninoScript) container; + + // Add default imports (e.g., noppes.npcs.api.*, noppes.npcs.api.entity.*, etc.) + activeArea.addImplicitImports(janinoScript.getDefaultImports()); + + // Add hook types from signatures (parameters + return types) + // e.g., INpcEvent.InitEvent, Color, String, IOverlayContext, etc. + Set hookTypes = janinoScript.getHookTypes(); + activeArea.addImplicitImports(hookTypes.toArray(new String[0])); + + // Format to update which implicit imports are actually used + activeArea.formatCodeText(); + } // Setup fullscreen key binding GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { @@ -236,7 +253,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } }); - activeArea.enableCodeHighlighting(); this.addTextField(activeArea); // ==================== FULLSCREEN BUTTON ==================== 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 da73bcc2c..c1b7b0918 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2652,6 +2652,18 @@ public class GuiScriptTextArea extends GuiNpcTextField { return this.container != null ? this.container.getScriptContext() : ScriptContext.GLOBAL; } + /** + * Add implicit imports that should be resolved without explicit import statements. + * Used for JaninoScript default imports and hook parameter types. + * + * @param patterns Array of import patterns to add (wildcard packages like "noppes.npcs.api.*" or FQ class names) + */ + public void addImplicitImports(String... patterns) { + if (this.container != null) { + this.container.addImplicitImports(patterns); + } + } + public void setListener(ITextChangeListener listener) { this.listener = listener; } @@ -2797,7 +2809,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { selection.reset(savedCursorPos + cursorAdjustment); scrollToCursor(); } - + + public void formatCodeText() { + if (this.enableCodeHighlighting && this.container != null) { + this.container.formatCodeText(); + } + } + /** * Helper class for tracking imports during sorting. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java index d0be41eee..a74d473fa 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -130,6 +130,10 @@ public class ScriptDocument { // Determines which hooks and event types are available private ScriptContext scriptContext = ScriptContext.GLOBAL; + // Implicit imports from JaninoScript default imports and hook parameter types + // These are types that should be resolved even without explicit import statements + private final Set implicitImports = new HashSet<>(); + // Layout properties public int lineHeight = 13; public int totalHeight; @@ -194,6 +198,27 @@ public class ScriptDocument { return scriptContext; } + /** + * Add implicit imports that should be resolved without explicit import statements. + * Used for JaninoScript default imports and hook parameter types. + * + * Patterns can be: + * - Wildcard: "noppes.npcs.api.*" (imports all classes from package) + * - Specific class: "noppes.npcs.api.event.INpcEvent$InitEvent" (nested class) + * - Regular class: "noppes.npcs.api.entity.IEntity" + * + * @param patterns Array of import patterns to add (packages with .* or fully qualified class names) + */ + public void addImplicitImports(String... patterns) { + if (patterns != null) { + for (String pattern : patterns) { + if (pattern != null && !pattern.isEmpty()) { + this.implicitImports.add(pattern); + } + } + } + } + // ==================== TEXT MANAGEMENT ==================== public void setText(String text) { @@ -296,6 +321,9 @@ public class ScriptDocument { line.buildTokensFromMarks(marks, text, this); } + // Phase 6.5: Now that tokens are built, remove implicit imports that aren't actually used + removeUnusedImplicitImports(); + // Phase 7: Compute indent guides computeIndentGuides(marks); } @@ -451,10 +479,123 @@ public class ScriptDocument { } } - // Resolve all imports + // Add ALL implicit imports initially so tokens can be built with proper type resolution + addAllImplicitImportsToList(); + + // Resolve all imports (including all implicit ones) importsBySimpleName = typeResolver.resolveImports(imports); } + /** + * Add ALL implicit imports to the imports list initially. + * This allows tokens to be built with proper type resolution. + * After tokenization, we'll remove the unused ones. + */ + private void addAllImplicitImportsToList() { + for (String pattern : implicitImports) { + if (pattern == null || pattern.isEmpty()) continue; + + boolean isWildcard = pattern.endsWith(".*"); + String fullPath; + String simpleName; + + if (isWildcard) { + fullPath = pattern.substring(0, pattern.length() - 2); // Remove ".*" + simpleName = null; + wildcardPackages.add(fullPath); + } else { + fullPath = pattern.replace('$', '.'); + int lastDot = fullPath.lastIndexOf('.'); + simpleName = lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath; + } + + ImportData importData = new ImportData( + fullPath, simpleName, isWildcard, false, + -1, -1, -1, -1 + ); + + if (!imports.contains(importData)) { + imports.add(importData); + } + } + } + + /** + * Remove implicit imports that aren't actually used in the text. + * This keeps autocomplete clean by not showing types from hooks that aren't present. + * Called after tokenization is complete. + */ + private void removeUnusedImplicitImports() { + // Collect imports to remove (can't remove during iteration) + List toRemove = new ArrayList<>(); + + for (ImportData imp : imports) { + // Only check implicit imports (those with offset -1) + if (imp.getStartOffset() != -1) continue; + + // Skip wildcard imports - keep them all + if (imp.isWildcard()) continue; + + // Check if this specific class import is actually used + String simpleName = imp.getSimpleName(); + String fullPath = imp.getFullPath(); + + if (!isTypeReferenced(simpleName, fullPath)) { + toRemove.add(imp); + } + } + + // Remove unused implicit imports + imports.removeAll(toRemove); + + // Re-resolve imports after removing unused ones + importsBySimpleName = typeResolver.resolveImports(imports); + } + + /** + * Check if a type is referenced anywhere in the script tokens. + * Looks for the simple name or any part of the fully qualified name. + */ + private boolean isTypeReferenced(String simpleName, String fullPath) { + if (simpleName == null || simpleName.isEmpty()) return false; + + // Check all lines and their tokens + for (ScriptLine line : lines) { + for (Token token : line.getTokens()) { + String tokenText = token.getText(); + + // Check if token matches the simple name + if (tokenText.equals(simpleName)) { + return true; + } + + // Check if token contains nested class reference (e.g., "InitEvent" in "INpcEvent.InitEvent") + if (fullPath.contains("$") && tokenText.equals(simpleName)) { + return true; + } + + // Check for partial matches in qualified references + if (fullPath.contains(".") && tokenText.contains(".")) { + // e.g., "INpcEvent.InitEvent" matches "noppes.npcs.api.event.INpcEvent$InitEvent" + String[] pathParts = fullPath.replace('$', '.').split("\\."); + String[] tokenParts = tokenText.split("\\."); + + // Check if the last parts match (e.g., "InitEvent") + if (pathParts.length > 0 && tokenParts.length > 0) { + String lastPathPart = pathParts[pathParts.length - 1]; + String lastTokenPart = tokenParts[tokenParts.length - 1]; + if (lastTokenPart.equals(lastPathPart)) { + return true; + } + } + } + } + } + + return false; + } + + // ==================== PHASE 3: STRUCTURE ==================== /** @@ -1934,6 +2075,11 @@ public class ScriptDocument { private void markImports(List marks) { for (ImportData imp : imports) { + // Skip implicit imports (from JaninoScript defaults, not in source text) + // These have offset -1 and don't need to be visually marked + if (imp.getStartOffset() < 0) + continue; + // Mark 'import' keyword marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getStartOffset() + 6, TokenType.IMPORT_KEYWORD, imp)); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java index 4836c0834..4b2e89e26 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java @@ -94,6 +94,18 @@ public class ScriptTextContainer extends JavaTextContainer { return scriptContext; } + /** + * Add implicit imports that should be resolved without explicit import statements. + * Used for JaninoScript default imports and hook parameter types. + * + * @param patterns Array of import patterns to add (wildcard packages like "noppes.npcs.api.*" or FQ class names) + */ + public void addImplicitImports(String... patterns) { + if (document != null) { + document.addImplicitImports(patterns); + } + } + @Override public void init(int width, int height) { if (!USE_NEW_INTERPRETER) { @@ -127,9 +139,12 @@ public class ScriptTextContainer extends JavaTextContainer { super.init(text, width, height); return; } + + if (document == null) { + document = new ScriptDocument(this.text, this.language); + document.setScriptContext(this.scriptContext); + } - document = new ScriptDocument(this.text, this.language); - document.setScriptContext(this.scriptContext); init(width, height); } diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 22fb9ee95..d1066c329 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -466,6 +466,58 @@ public abstract class JaninoScript implements IScriptUnit { return new ArrayList<>(hookList); } + /** + * Get the default imports configured for this script type. + * These are packages/classes that are automatically available without explicit import statements. + * + * @return Array of default import patterns (e.g., "noppes.npcs.api.*") + */ + public String[] getDefaultImports() { + return defaultImports; + } + + /** + * Get all types used in hook method signatures (parameters and return types). + * This includes event types like INpcEvent.InitEvent, INpcEvent.DamagedEvent, etc., + * as well as return types like Color, String, etc. + * Useful for syntax highlighting to know what types are implicitly available. + * + * @return Set of fully qualified class names for all hook parameter and return types + */ + public Set getHookTypes() { + Set types = new HashSet<>(); + for (Method method : type.getDeclaredMethods()) { + // Add parameter types + for (Class paramType : method.getParameterTypes()) + addTypeAndEnclosingTypes(types, paramType); + + // Add return types (in case any hook has a non-void return) + Class returnType = method.getReturnType(); + if (returnType != void.class) + addTypeAndEnclosingTypes(types, returnType); + } + return types; + } + + /** + * Add a type and all its enclosing types to the set. + * For nested classes like INpcEvent.InitEvent, this adds both INpcEvent and INpcEvent$InitEvent. + */ + private void addTypeAndEnclosingTypes(Set types, Class clazz) { + if (clazz == null || clazz.isPrimitive()) + return; + + // Add the type itself + types.add(clazz.getName()); + + // Add enclosing/declaring class if it's a nested type + Class enclosing = clazz.getDeclaringClass(); + while (enclosing != null) { + types.add(enclosing.getName()); + enclosing = enclosing.getDeclaringClass(); + } + } + @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) {