From 51993c3efc91508b216f8ec645245f041ea4fa20 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 23 Dec 2025 00:01:23 +0200 Subject: [PATCH] HUGE MASSIVE REFACTOR! BRAND NEW SYNTAX HIGHLIGHT/IMPORT RESOLUTION FRAMEWORK --- .../client/gui/util/GuiScriptTextArea.java | 12 +- .../util/script/interpreter/FieldInfo.java | 111 +++ .../util/script/interpreter/ImportData.java | 115 +++ .../util/script/interpreter/MethodInfo.java | 145 +++ .../script/interpreter/ScriptDocument.java | 885 ++++++++++++++++++ .../util/script/interpreter/ScriptLine.java | 303 ++++++ .../interpreter/ScriptTextContainer.java | 199 ++++ .../gui/util/script/interpreter/Token.java | 237 +++++ .../util/script/interpreter/TokenType.java | 103 ++ .../gui/util/script/interpreter/TypeInfo.java | 173 ++++ .../util/script/interpreter/TypeResolver.java | 389 ++++++++ .../util/script/interpreter/package-info.java | 102 ++ 12 files changed, 2771 insertions(+), 3 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/package-info.java 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 3426bcae8..bb77cfaad 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -10,6 +10,9 @@ import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +// New interpreter system imports +import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; +import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; import noppes.npcs.client.key.impl.ScriptEditorKeys; import noppes.npcs.util.ValueUtil; import org.lwjgl.input.Keyboard; @@ -54,7 +57,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== TEXT & CONTAINER ==================== public String text = null; public String highlightedWord; - private JavaTextContainer container = null; + private ScriptTextContainer container = null; private boolean enableCodeHighlighting = false; // Extra empty lines to allow padding at the bottom of the editor viewport private int bottomPaddingLines = 6; @@ -643,6 +646,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Render Viewport for (int i = renderStart; i <= renderEnd; i++) { LineData data = list.get(i); + ScriptLine scriptLine = container.getDocument().getLine(i); String line = data.text; int w = line.length(); // Use integer Y relative to scrolledLine; fractional offset applied via GL translate @@ -832,7 +836,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } int yPos = posY + stringYOffset; - data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + + // data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + scriptLine.drawString(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); // Draw cursor: pause blinking while user is active recently boolean recentInput = selection.hadRecentInput(); @@ -2122,7 +2128,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.text = text; //this.container = new TextContainer(text); if (this.container == null) - this.container = new JavaTextContainer(text); + this.container = new ScriptTextContainer(text); this.container.init(text, this.width, this.height); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java new file mode 100644 index 000000000..8d4e85e09 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java @@ -0,0 +1,111 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Metadata for a field (variable) declaration or reference. + * Tracks the field name, type, scope, and declaration location. + */ +public final class FieldInfo { + + public enum Scope { + GLOBAL, // Class-level field + LOCAL, // Local variable inside a method + PARAMETER // Method parameter + } + + private final String name; + private final Scope scope; + private final TypeInfo declaredType; // The declared type (e.g., String, List) + private final int declarationOffset; // Where this field was declared in the source + private final boolean resolved; + + // For local/parameter fields, track the containing method + private final MethodInfo containingMethod; + + private FieldInfo(String name, Scope scope, TypeInfo declaredType, + int declarationOffset, boolean resolved, MethodInfo containingMethod) { + this.name = name; + this.scope = scope; + this.declaredType = declaredType; + this.declarationOffset = declarationOffset; + this.resolved = resolved; + this.containingMethod = containingMethod; + } + + // Factory methods + public static FieldInfo globalField(String name, TypeInfo type, int declOffset) { + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null); + } + + public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method) { + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method); + } + + public static FieldInfo parameter(String name, TypeInfo type, int declOffset, MethodInfo method) { + return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method); + } + + public static FieldInfo unresolved(String name, Scope scope) { + return new FieldInfo(name, scope, null, -1, false, null); + } + + // Getters + public String getName() { return name; } + public Scope getScope() { return scope; } + public TypeInfo getDeclaredType() { return declaredType; } + public int getDeclarationOffset() { return declarationOffset; } + public boolean isResolved() { return resolved; } + public MethodInfo getContainingMethod() { return containingMethod; } + + public boolean isGlobal() { return scope == Scope.GLOBAL; } + public boolean isLocal() { return scope == Scope.LOCAL; } + public boolean isParameter() { return scope == Scope.PARAMETER; } + + /** + * Check if a reference at the given position can see this field. + * For local variables, they're only visible after their declaration. + */ + public boolean isVisibleAt(int position) { + if (scope == Scope.GLOBAL || scope == Scope.PARAMETER) { + return true; // Always visible in their scope + } + // Local variables are only visible after declaration + return position >= declarationOffset; + } + + /** + * Get the appropriate TokenType for highlighting this field. + */ + public TokenType getTokenType() { + if (!resolved) { + return TokenType.UNDEFINED_VAR; + } + switch (scope) { + case GLOBAL: + return TokenType.GLOBAL_FIELD; + case LOCAL: + return TokenType.LOCAL_FIELD; + case PARAMETER: + return TokenType.PARAMETER; + default: + return TokenType.VARIABLE; + } + } + + @Override + public String toString() { + return "FieldInfo{" + name + ", " + scope + ", type=" + declaredType + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FieldInfo fieldInfo = (FieldInfo) o; + return name.equals(fieldInfo.name) && scope == fieldInfo.scope; + } + + @Override + public int hashCode() { + return name.hashCode() * 31 + scope.ordinal(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java new file mode 100644 index 000000000..00557fa6a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java @@ -0,0 +1,115 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Represents a single import statement with all its metadata. + * Tracks the full import path, resolution status, and source positions. + */ +public final class ImportData { + + private final String fullPath; // e.g., "java.util.List" or "kamkeel.npcdbc.api.*" + private final String simpleName; // e.g., "List" or null for wildcard + private final boolean isWildcard; // true if ends with .* + private final boolean isStatic; // true if 'import static' + private final int startOffset; // start of 'import' keyword in source + private final int endOffset; // end of import statement (before or at semicolon) + private final int pathStartOffset; // start of the package.Class path + private final int pathEndOffset; // end of the package.Class path (before .*) + + private TypeInfo resolvedType; // null if wildcard or unresolved + private boolean resolved; // whether resolution was attempted and succeeded + + public ImportData(String fullPath, String simpleName, boolean isWildcard, boolean isStatic, + int startOffset, int endOffset, int pathStartOffset, int pathEndOffset) { + this.fullPath = fullPath; + this.simpleName = simpleName; + this.isWildcard = isWildcard; + this.isStatic = isStatic; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.pathStartOffset = pathStartOffset; + this.pathEndOffset = pathEndOffset; + this.resolved = false; + } + + // Getters + public String getFullPath() { return fullPath; } + public String getSimpleName() { return simpleName; } + public boolean isWildcard() { return isWildcard; } + public boolean isStatic() { return isStatic; } + public int getStartOffset() { return startOffset; } + public int getEndOffset() { return endOffset; } + public int getPathStartOffset() { return pathStartOffset; } + public int getPathEndOffset() { return pathEndOffset; } + public TypeInfo getResolvedType() { return resolvedType; } + public boolean isResolved() { return resolved; } + + /** + * Set the resolved type information. + */ + public void setResolvedType(TypeInfo typeInfo) { + this.resolvedType = typeInfo; + this.resolved = typeInfo != null && typeInfo.isResolved(); + } + + /** + * Mark this import as resolved (for wildcard imports where we confirm the package exists). + */ + public void markResolved(boolean resolved) { + this.resolved = resolved; + } + + /** + * Get the package portion of the import path. + * For "java.util.List" returns "java.util" + * For wildcards like "java.util.*" returns "java.util" + */ + public String getPackagePortion() { + if (isWildcard) { + return fullPath; + } + int lastDot = fullPath.lastIndexOf('.'); + return lastDot > 0 ? fullPath.substring(0, lastDot) : ""; + } + + /** + * Get all segments of the import path. + */ + public String[] getPathSegments() { + return fullPath.split("\\."); + } + + /** + * Check if this import could provide the given simple class name. + * For wildcard imports, always returns true (might provide any class). + * For specific imports, checks if the simple name matches. + */ + public boolean couldProvide(String className) { + if (isWildcard) { + return true; + } + return className.equals(simpleName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("import "); + if (isStatic) sb.append("static "); + sb.append(fullPath); + if (isWildcard) sb.append(".*"); + sb.append(" [").append(resolved ? "resolved" : "unresolved").append("]"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImportData that = (ImportData) o; + return fullPath.equals(that.fullPath) && isWildcard == that.isWildcard && isStatic == that.isStatic; + } + + @Override + public int hashCode() { + return fullPath.hashCode() * 31 + (isWildcard ? 1 : 0) + (isStatic ? 2 : 0); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java new file mode 100644 index 000000000..95b9b76a5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -0,0 +1,145 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Metadata for a method declaration or method call. + * Tracks method name, parameters, return type, and containing class. + */ +public final class MethodInfo { + + private final String name; + private final TypeInfo returnType; + private final TypeInfo containingType; // The class/interface that owns this method + private final List parameters; + private final int declarationOffset; // -1 for external methods (calls to library methods) + private final int bodyStart; // Start of method body (after {) + private final int bodyEnd; // End of method body (before }) + private final boolean resolved; + private final boolean isDeclaration; // true if this is a declaration, false if it's a call + + private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, + List parameters, int declarationOffset, + int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration) { + this.name = name; + this.returnType = returnType; + this.containingType = containingType; + this.parameters = parameters != null ? new ArrayList<>(parameters) : new ArrayList<>(); + this.declarationOffset = declarationOffset; + this.bodyStart = bodyStart; + this.bodyEnd = bodyEnd; + this.resolved = resolved; + this.isDeclaration = isDeclaration; + } + + // Factory methods + public static MethodInfo declaration(String name, TypeInfo returnType, List params, + int declOffset, int bodyStart, int bodyEnd) { + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true); + } + + public static MethodInfo call(String name, TypeInfo containingType, int paramCount) { + boolean resolved = containingType != null && containingType.isResolved() && + containingType.hasMethod(name); + List params = new ArrayList<>(); + for (int i = 0; i < paramCount; i++) { + params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); + } + return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false); + } + + public static MethodInfo unresolvedCall(String name, int paramCount) { + List params = new ArrayList<>(); + for (int i = 0; i < paramCount; i++) { + params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); + } + return new MethodInfo(name, null, null, params, -1, -1, -1, false, false); + } + + // Getters + public String getName() { return name; } + public TypeInfo getReturnType() { return returnType; } + public TypeInfo getContainingType() { return containingType; } + public List getParameters() { return Collections.unmodifiableList(parameters); } + public int getParameterCount() { return parameters.size(); } + public int getDeclarationOffset() { return declarationOffset; } + public int getBodyStart() { return bodyStart; } + public int getBodyEnd() { return bodyEnd; } + public boolean isResolved() { return resolved; } + public boolean isDeclaration() { return isDeclaration; } + public boolean isCall() { return !isDeclaration; } + + /** + * Check if a position is inside this method's body. + */ + public boolean containsPosition(int position) { + return position >= bodyStart && position < bodyEnd; + } + + /** + * Check if this method has a parameter with the given name. + */ + public boolean hasParameter(String paramName) { + for (FieldInfo p : parameters) { + if (p.getName().equals(paramName)) { + return true; + } + } + return false; + } + + /** + * Get parameter info by name. + */ + public FieldInfo getParameter(String paramName) { + for (FieldInfo p : parameters) { + if (p.getName().equals(paramName)) { + return p; + } + } + return null; + } + + /** + * Get the appropriate TokenType for highlighting this method. + */ + public TokenType getTokenType() { + if (isDeclaration) { + return TokenType.METHOD_DECL; + } + return resolved ? TokenType.METHOD_CALL : TokenType.DEFAULT; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MethodInfo{"); + sb.append(name).append("("); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(parameters.get(i).getName()); + } + sb.append(")"); + if (returnType != null) { + sb.append(" -> ").append(returnType.getSimpleName()); + } + sb.append(", ").append(isDeclaration ? "decl" : "call"); + sb.append(", ").append(resolved ? "resolved" : "unresolved"); + sb.append("}"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MethodInfo that = (MethodInfo) o; + return name.equals(that.name) && parameters.size() == that.parameters.size(); + } + + @Override + public int hashCode() { + return name.hashCode() * 31 + parameters.size(); + } +} 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 new file mode 100644 index 000000000..33399c954 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -0,0 +1,885 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.ClientProxy; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The main document container that manages script text, lines, and tokens. + * This is the clean reimplementation of JavaTextContainer. + * + * Architecture: + * - ScriptDocument holds the complete source text and global state + * - ScriptLine holds individual lines with their tokens + * - Token holds individual syntax elements with type-specific metadata + * - TypeResolver handles all class/type resolution + * + * Single-pass tokenization: + * 1. Parse excluded regions (comments, strings) + * 2. Parse imports and resolve types + * 3. Parse structure (methods, classes, fields) + * 4. Tokenize with all context available + */ +public class ScriptDocument { + + // ==================== PATTERNS ==================== + + private static final Pattern WORD_PATTERN = Pattern.compile("[\\p{L}\\p{N}_-]+|\\n|$"); + + // Literals + private static final Pattern STRING_PATTERN = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); + private static final Pattern COMMENT_PATTERN = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); + private static final Pattern NUMBER_PATTERN = Pattern.compile( + "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); + + // Keywords + private static final Pattern MODIFIER_PATTERN = Pattern.compile( + "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); + private static final Pattern KEYWORD_PATTERN = Pattern.compile( + "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); + + // Declarations + private static final Pattern IMPORT_PATTERN = Pattern.compile( + "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)"); + private static final Pattern CLASS_DECL_PATTERN = Pattern.compile( + "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + private static final Pattern METHOD_DECL_PATTERN = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); + private static final Pattern METHOD_CALL_PATTERN = Pattern.compile( + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); + private static final Pattern FIELD_DECL_PATTERN = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + + // Function parameters (for JS-style scripts) + private static final Pattern FUNC_PARAMS_PATTERN = Pattern.compile( + "\\bfunction\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*\\(([^)]*)\\)"); + + // ==================== STATE ==================== + + private String text = ""; + private final List lines = new ArrayList<>(); + private final List imports = new ArrayList<>(); + private final List methods = new ArrayList<>(); + private final Map globalFields = new HashMap<>(); + private final Set wildcardPackages = new HashSet<>(); + private Map importsBySimpleName = new HashMap<>(); + + // Excluded regions (strings/comments) - positions where other patterns shouldn't match + private final List excludedRanges = new ArrayList<>(); + + // Type resolver + private final TypeResolver typeResolver; + + // Layout properties + public int lineHeight = 13; + public int totalHeight; + public int visibleLines = 1; + public int linesCount; + + // ==================== CONSTRUCTOR ==================== + + public ScriptDocument(String text) { + this.typeResolver = TypeResolver.getInstance(); + setText(text); + } + + public ScriptDocument(String text, TypeResolver resolver) { + this.typeResolver = resolver != null ? resolver : TypeResolver.getInstance(); + setText(text); + } + + // ==================== TEXT MANAGEMENT ==================== + + public void setText(String text) { + this.text = text != null ? text.replaceAll("\\r?\\n|\\r", "\n") : ""; + } + + public String getText() { + return text; + } + + // ==================== INITIALIZATION ==================== + + /** + * Initialize the document with layout constraints. + * Builds lines based on width wrapping. + */ + public void init(int width, int height) { + lines.clear(); + lineHeight = ClientProxy.Font.height(); + if (lineHeight == 0) + lineHeight = 12; + + String[] sourceLines = text.split("\n", -1); + int totalChars = 0; + int lineIndex = 0; + + for (String sourceLine : sourceLines) { + StringBuilder currentLine = new StringBuilder(); + Matcher m = WORD_PATTERN.matcher(sourceLine); + int i = 0; + + while (m.find()) { + String word = sourceLine.substring(i, m.start()); + if (ClientProxy.Font.width(currentLine + word) > width - 10) { + // Wrap line + int lineStart = totalChars; + int lineEnd = totalChars + currentLine.length(); + lines.add(new ScriptLine(currentLine.toString(), lineStart, lineEnd, lineIndex++)); + totalChars = lineEnd; + currentLine = new StringBuilder(); + } + currentLine.append(word); + i = m.start(); + } + + // Add final line segment (including newline character in range) + int lineStart = totalChars; + int lineEnd = totalChars + currentLine.length() + 1; + lines.add(new ScriptLine(currentLine.toString(), lineStart, lineEnd, lineIndex++)); + totalChars = lineEnd; + } + + // Set up line navigation + for (int li = 0; li < lines.size(); li++) { + ScriptLine line = lines.get(li); + line.setParent(this); + if (li > 0) { + line.setPrev(lines.get(li - 1)); + lines.get(li - 1).setNext(line); + } + } + + linesCount = lines.size(); + totalHeight = linesCount * lineHeight; + visibleLines = Math.max(height / lineHeight - 1, 1); + } + + // ==================== TOKENIZATION ==================== + + /** + * Main tokenization entry point. + * Performs complete analysis and builds tokens for all lines. + */ + public void formatCodeText() { + // Clear previous state + imports.clear(); + methods.clear(); + globalFields.clear(); + wildcardPackages.clear(); + excludedRanges.clear(); + + // Phase 1: Find excluded regions (strings/comments) + findExcludedRanges(); + + // Phase 2: Parse imports + parseImports(); + + // Phase 3: Parse structure (methods, fields) + parseStructure(); + + // Phase 4: Build marks and assign to lines + List marks = buildMarks(); + + // Phase 5: Resolve conflicts and sort + marks = resolveConflicts(marks); + + // Phase 6: Build tokens for each line + for (ScriptLine line : lines) { + line.buildTokensFromMarks(marks, text); + } + + // Phase 7: Compute indent guides + computeIndentGuides(marks); + } + + // ==================== PHASE 1: EXCLUDED RANGES ==================== + + private void findExcludedRanges() { + // Find strings + Matcher m = STRING_PATTERN.matcher(text); + while (m.find()) { + excludedRanges.add(new int[]{m.start(), m.end()}); + } + + // Find comments + m = COMMENT_PATTERN.matcher(text); + while (m.find()) { + excludedRanges.add(new int[]{m.start(), m.end()}); + } + + // Sort and merge overlapping ranges + excludedRanges.sort(Comparator.comparingInt(a -> a[0])); + mergeOverlappingRanges(); + } + + private void mergeOverlappingRanges() { + if (excludedRanges.size() < 2) + return; + + List merged = new ArrayList<>(); + int[] current = excludedRanges.get(0); + + for (int i = 1; i < excludedRanges.size(); i++) { + int[] next = excludedRanges.get(i); + if (next[0] <= current[1]) { + current[1] = Math.max(current[1], next[1]); + } else { + merged.add(current); + current = next; + } + } + merged.add(current); + + excludedRanges.clear(); + excludedRanges.addAll(merged); + } + + private boolean isExcluded(int position) { + for (int[] range : excludedRanges) { + if (position >= range[0] && position < range[1]) { + return true; + } + } + return false; + } + + // ==================== PHASE 2: IMPORTS ==================== + + private void parseImports() { + Matcher m = IMPORT_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String fullPath = m.group(1).replaceAll("\\s+", "").trim(); + String matchText = m.group(0); + boolean isWildcard = matchText.contains("*"); + boolean isStatic = matchText.contains("static"); + + int lastDot = fullPath.lastIndexOf('.'); + String simpleName = isWildcard ? null : (lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath); + + ImportData importData = new ImportData( + fullPath, simpleName, isWildcard, isStatic, + m.start(), m.end(), m.start(1), m.end(1) + ); + imports.add(importData); + + if (isWildcard) { + wildcardPackages.add(fullPath); + } + } + + // Resolve all imports + importsBySimpleName = typeResolver.resolveImports(imports); + } + + // ==================== PHASE 3: STRUCTURE ==================== + + private void parseStructure() { + // Parse methods + parseMethodDeclarations(); + + // Parse global fields (outside methods) + parseGlobalFields(); + } + + private void parseMethodDeclarations() { + Pattern methodWithBody = Pattern.compile( + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*\\{"); + + Matcher m = methodWithBody.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String returnType = m.group(1); + String methodName = m.group(2); + String paramList = m.group(3); + + int bodyStart = text.indexOf('{', m.end() - 1); + int bodyEnd = findMatchingBrace(bodyStart); + if (bodyEnd < 0) + bodyEnd = text.length(); + + // Parse parameters + List params = parseParameters(paramList, m.start()); + + MethodInfo methodInfo = MethodInfo.declaration( + methodName, + resolveType(returnType), + params, + m.start(), + bodyStart, + bodyEnd + ); + methods.add(methodInfo); + } + + // Also parse JS-style functions + Matcher funcM = FUNC_PARAMS_PATTERN.matcher(text); + while (funcM.find()) { + if (isExcluded(funcM.start())) + continue; + // JS functions don't have explicit return types + String paramList = funcM.group(1); + List params = new ArrayList<>(); + if (paramList != null && !paramList.trim().isEmpty()) { + for (String p : paramList.split(",")) { + String pn = p.trim(); + if (!pn.isEmpty()) { + params.add(FieldInfo.parameter(pn, null, funcM.start(), null)); + } + } + } + // We don't need to store JS functions in methods list - just extract params + } + } + + private List parseParameters(String paramList, int declOffset) { + List params = new ArrayList<>(); + if (paramList == null || paramList.trim().isEmpty()) { + return params; + } + + Pattern paramPattern = Pattern.compile( + "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher m = paramPattern.matcher(paramList); + while (m.find()) { + String typeName = m.group(1); + String paramName = m.group(2); + TypeInfo typeInfo = resolveType(typeName); + params.add(FieldInfo.parameter(paramName, typeInfo, declOffset, null)); + } + return params; + } + + private void parseGlobalFields() { + Matcher m = FIELD_DECL_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String typeName = m.group(1); + String fieldName = m.group(2); + int position = m.start(2); + + // Check if inside a method - if so, it's a local, not global + boolean insideMethod = false; + for (MethodInfo method : methods) { + if (method.containsPosition(position)) { + insideMethod = true; + break; + } + } + + if (!insideMethod) { + TypeInfo typeInfo = resolveType(typeName); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position); + globalFields.put(fieldName, fieldInfo); + } + } + } + + private TypeInfo resolveType(String typeName) { + if (typeName == null || typeName.isEmpty()) + return null; + + // Strip generics for resolution + int genericStart = typeName.indexOf('<'); + String baseName = genericStart > 0 ? typeName.substring(0, genericStart) : typeName; + + // Strip array brackets + baseName = baseName.replace("[]", "").trim(); + + // Try to resolve + return typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); + } + + // ==================== PHASE 4: BUILD MARKS ==================== + + private List buildMarks() { + List marks = new ArrayList<>(); + + // Comments and strings first (highest priority) + addPatternMarks(marks, COMMENT_PATTERN, TokenType.COMMENT); + addPatternMarks(marks, STRING_PATTERN, TokenType.STRING); + + // Import statements + markImports(marks); + + // Class/interface/enum declarations + markClassDeclarations(marks); + + // Keywords and modifiers + addPatternMarks(marks, KEYWORD_PATTERN, TokenType.KEYWORD); + addPatternMarks(marks, MODIFIER_PATTERN, TokenType.MODIFIER); + + // Type declarations and usages + markTypeDeclarations(marks); + addPatternMarks(marks, NEW_TYPE_PATTERN, TokenType.NEW_TYPE, 1); + + // Methods + markMethodDeclarations(marks); + markMethodCalls(marks); + + // Numbers + addPatternMarks(marks, NUMBER_PATTERN, TokenType.NUMBER); + + // Variables and fields + markVariables(marks); + + // Imported class usages + markImportedClassUsages(marks); + + return marks; + } + + private void addPatternMarks(List marks, Pattern pattern, TokenType type) { + addPatternMarks(marks, pattern, type, 0); + } + + private void addPatternMarks(List marks, Pattern pattern, TokenType type, int group) { + Matcher m = pattern.matcher(text); + while (m.find()) { + marks.add(new ScriptLine.Mark(m.start(group), m.end(group), type)); + } + } + + private void markImports(List marks) { + for (ImportData imp : imports) { + // Mark 'import' keyword + marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getStartOffset() + 6, TokenType.IMPORT_KEYWORD)); + + // Mark the path + int pathStart = imp.getPathStartOffset(); + int pathEnd = imp.getPathEndOffset(); + + if (imp.isResolved()) { + // Resolved import - mark as type declaration + if (imp.getResolvedType() != null) { + marks.add(new ScriptLine.Mark(pathStart, pathEnd, imp.getResolvedType().getTokenType())); + } else { + marks.add(new ScriptLine.Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + } + } else { + // Unresolved import + marks.add(new ScriptLine.Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR)); + } + } + } + + private void markClassDeclarations(List marks) { + Matcher m = CLASS_DECL_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + // Mark the keyword (class/interface/enum) + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.CLASS_KEYWORD)); + + // Mark the class name + String kind = m.group(1); + TokenType nameType; + if ("interface".equals(kind)) { + nameType = TokenType.INTERFACE_DECL; + } else if ("enum".equals(kind)) { + nameType = TokenType.ENUM_DECL; + } else { + nameType = TokenType.CLASS_DECL; + } + marks.add(new ScriptLine.Mark(m.start(2), m.end(2), nameType)); + } + } + + private void markTypeDeclarations(List marks) { + // Pattern for type followed by variable name + Pattern typeDecl = Pattern.compile( + "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + + "([A-Z][a-zA-Z0-9_]*)\\s*(?:<[^>]+>)?\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); + + Matcher m = typeDecl.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String typeName = m.group(1); + TypeInfo info = resolveType(typeName); + TokenType tokenType = (info != null && info.isResolved()) ? info.getTokenType() : TokenType.UNDEFINED_VAR; + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), tokenType)); + } + } + + private void markMethodDeclarations(List marks) { + Matcher m = METHOD_DECL_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + // Return type + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); + // Method name + marks.add(new ScriptLine.Mark(m.start(2), m.end(2), TokenType.METHOD_DECL)); + } + } + + private void markMethodCalls(List marks) { + Matcher m = METHOD_CALL_PATTERN.matcher(text); + while (m.find()) { + int start = m.start(1); + int end = m.end(1); + + if (isExcluded(start)) + continue; + + // Check if this is preceded by a dot (method call on an object) + boolean hasReceiver = false; + String receiverName = null; + int scanPos = start - 1; + while (scanPos >= 0 && Character.isWhitespace(text.charAt(scanPos))) + scanPos--; + + if (scanPos >= 0 && text.charAt(scanPos) == '.') { + hasReceiver = true; + // Find receiver identifier + int recvEnd = scanPos - 1; + while (recvEnd >= 0 && Character.isWhitespace(text.charAt(recvEnd))) + recvEnd--; + int recvStart = recvEnd; + while (recvStart >= 0 && Character.isJavaIdentifierPart(text.charAt(recvStart))) + recvStart--; + recvStart++; + if (recvStart <= recvEnd && recvStart >= 0) { + receiverName = text.substring(recvStart, recvEnd + 1); + } + } + + // Determine if we should highlight this as a method call + boolean shouldHighlight = true; + + if (hasReceiver && receiverName != null && !receiverName.isEmpty()) { + // Check if receiver is an unresolved class reference + if (Character.isUpperCase(receiverName.charAt(0))) { + TypeInfo recvType = resolveType(receiverName); + if (recvType == null || !recvType.isResolved()) { + // Don't highlight method calls on unresolved types + shouldHighlight = false; + } + } + } + + if (shouldHighlight) { + marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); + } + } + } + + private void markVariables(List marks) { + Set knownKeywords = new HashSet<>(Arrays.asList( + "boolean", "int", "float", "double", "long", "char", "byte", "short", "void", + "null", "true", "false", "if", "else", "switch", "case", "for", "while", "do", + "try", "catch", "finally", "return", "throw", "var", "let", "const", "function", + "continue", "break", "this", "new", "typeof", "instanceof", "class", "interface", + "extends", "implements", "import", "package", "public", "private", "protected", + "static", "final", "abstract", "synchronized", "native", "default", "enum", + "throws", "super", "assert", "volatile", "transient" + )); + + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); + Matcher m = identifier.matcher(text); + + while (m.find()) { + String name = m.group(1); + int position = m.start(1); + + if (isExcluded(position)) + continue; + if (knownKeywords.contains(name)) + continue; + + // Skip if preceded by dot (field access) + if (isPrecededByDot(position)) + continue; + + // Skip import/package statements + if (isInImportOrPackage(position)) + continue; + + // Skip uppercase (type references handled elsewhere) + if (Character.isUpperCase(name.charAt(0))) + continue; + + // Find containing method + MethodInfo containingMethod = findMethodAtPosition(position); + + if (containingMethod != null) { + // Check parameters + if (containingMethod.hasParameter(name)) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER)); + continue; + } + } + + // Check global fields + if (globalFields.containsKey(name)) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + continue; + } + + // Check if it's a method call (followed by paren) + if (isFollowedByParen(m.end(1))) + continue; + + // Unknown identifier - could mark as undefined, but be conservative + // Only mark as undefined if it looks like a variable usage + } + } + + private void markImportedClassUsages(List marks) { + // Find uppercase identifiers followed by dot (static method calls, field access) + Pattern classUsage = Pattern.compile("\\b([A-Z][a-zA-Z0-9_]*)\\s*\\."); + Matcher m = classUsage.matcher(text); + + while (m.find()) { + String className = m.group(1); + int start = m.start(1); + int end = m.end(1); + + if (isExcluded(start)) + continue; + + // Try to resolve the class + TypeInfo info = resolveType(className); + if (info != null && info.isResolved()) { + marks.add(new ScriptLine.Mark(start, end, info.getTokenType())); + } else if (!importsBySimpleName.containsKey(className) && + !TypeResolver.JAVA_LANG_CLASSES.contains(className) && + wildcardPackages.isEmpty()) { + marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + } + } + } + + // ==================== PHASE 5: CONFLICT RESOLUTION ==================== + + private List resolveConflicts(List marks) { + // Sort by start position, then by descending priority + marks.sort((a, b) -> { + if (a.start != b.start) + return Integer.compare(a.start, b.start); + return Integer.compare(b.type.getPriority(), a.type.getPriority()); + }); + + List result = new ArrayList<>(); + for (ScriptLine.Mark m : marks) { + boolean skip = false; + + // Remove lower-priority overlapping marks + for (int i = result.size() - 1; i >= 0; i--) { + ScriptLine.Mark r = result.get(i); + if (m.start < r.end && m.end > r.start) { + if (r.type.getPriority() < m.type.getPriority()) { + result.remove(i); + } else { + skip = true; + break; + } + } + } + + if (!skip) { + result.add(m); + } + } + + result.sort(Comparator.comparingInt(m -> m.start)); + return result; + } + + // ==================== PHASE 7: INDENT GUIDES ==================== + + private void computeIndentGuides(List marks) { + for (ScriptLine line : lines) { + line.clearIndentGuides(); + } + + // Build ignored ranges from STRING and COMMENT marks + Set ignored = new HashSet<>(); + for (ScriptLine.Mark m : marks) { + if (m.type == TokenType.STRING || m.type == TokenType.COMMENT) { + ignored.add(new int[]{m.start, m.end}); + } + } + + class OpenBrace { + int lineIdx, col; + + OpenBrace(int l, int c) { + lineIdx = l; + col = c; + } + } + Deque stack = new ArrayDeque<>(); + final int tabSize = 4; + + for (int li = 0; li < lines.size(); li++) { + ScriptLine line = lines.get(li); + String s = line.getText(); + + for (int i = 0; i < s.length(); i++) { + int absPos = line.getGlobalStart() + i; + + // Check if in ignored range + boolean isIgnored = false; + for (int[] range : ignored) { + if (absPos >= range[0] && absPos < range[1]) { + isIgnored = true; + break; + } + } + if (isIgnored) + continue; + + char c = s.charAt(i); + if (c == '{') { + int leading = 0; + for (int k = 0; k < i; k++) { + char ch = s.charAt(k); + leading += (ch == '\t') ? tabSize : 1; + } + stack.push(new OpenBrace(li, leading)); + } else if (c == '}') { + if (!stack.isEmpty()) { + OpenBrace open = stack.pop(); + if (open.lineIdx == li) + continue; + + int from = Math.max(0, open.lineIdx + 1); + int to = Math.min(lines.size() - 1, li); + for (int l = from; l <= to; l++) { + lines.get(l).addIndentGuide(open.col); + } + } + } + } + } + } + + // ==================== UTILITY METHODS ==================== + + private boolean isPrecededByDot(int position) { + if (position <= 0) + return false; + int i = position - 1; + while (i >= 0 && Character.isWhitespace(text.charAt(i))) + i--; + return i >= 0 && text.charAt(i) == '.'; + } + + private boolean isFollowedByParen(int position) { + int i = position; + while (i < text.length() && Character.isWhitespace(text.charAt(i))) + i++; + return i < text.length() && text.charAt(i) == '('; + } + + private boolean isInImportOrPackage(int position) { + if (position < 0 || position >= text.length()) + return false; + + int lineStart = text.lastIndexOf('\n', position); + lineStart = lineStart < 0 ? 0 : lineStart + 1; + + int i = lineStart; + while (i < text.length() && Character.isWhitespace(text.charAt(i))) + i++; + + if (text.startsWith("import", i) || text.startsWith("package", i)) { + return true; + } + return false; + } + + private MethodInfo findMethodAtPosition(int position) { + for (MethodInfo method : methods) { + if (method.containsPosition(position)) { + return method; + } + } + return null; + } + + private int findMatchingBrace(int openBraceIndex) { + if (openBraceIndex < 0 || openBraceIndex >= text.length()) + return -1; + + int depth = 0; + for (int i = openBraceIndex; i < text.length(); i++) { + if (isExcluded(i)) + continue; + + char c = text.charAt(i); + if (c == '{') + depth++; + else if (c == '}') { + depth--; + if (depth == 0) + return i; + } + } + return -1; + } + + // ==================== ACCESSORS ==================== + + public List getLines() { + return Collections.unmodifiableList(lines); + } + + public List getImports() { + return Collections.unmodifiableList(imports); + } + + public List getMethods() { + return Collections.unmodifiableList(methods); + } + + public Map getGlobalFields() { + return Collections.unmodifiableMap(globalFields); + } + + public TypeResolver getTypeResolver() { + return typeResolver; + } + + public ScriptLine getLine(int index) { + for (ScriptLine line : lines) { + if (line.getLineIndex() == index) { + return line; + } + } + return null; + } + + public ScriptLine getLineAt(int globalPosition) { + for (ScriptLine line : lines) { + if (line.containsPosition(globalPosition)) { + return line; + } + } + return null; + } + + public int getLineIndexAt(int globalPosition) { + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).containsPosition(globalPosition)) { + return i; + } + } + return -1; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java new file mode 100644 index 000000000..019c4d7aa --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -0,0 +1,303 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.ClientProxy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a single line of source code with its tokens. + * Handles token management, navigation, and rendering. + * + * A ScriptLine is responsible for: + * - Storing tokens that fall within this line's character range + * - Managing token navigation (prev/next within line) + * - Rendering the line with proper syntax highlighting + * - Computing indent guides for this line + */ +public class ScriptLine { + + private static final char COLOR_CHAR = '\u00A7'; + + private final String text; // The actual text of this line + private final int globalStart; // Start offset in the full document + private final int globalEnd; // End offset in the full document (exclusive) + private final int lineIndex; // 0-based line number + + private final List tokens = new ArrayList<>(); + private final List indentGuides = new ArrayList<>(); // Column positions for indent guides + + // Navigation + private ScriptLine prev; + private ScriptLine next; + private ScriptDocument parent; + + public ScriptLine(String text, int globalStart, int globalEnd, int lineIndex) { + this.text = text; + this.globalStart = globalStart; + this.globalEnd = globalEnd; + this.lineIndex = lineIndex; + } + + // ==================== GETTERS ==================== + + public String getText() { return text; } + public int getGlobalStart() { return globalStart; } + public int getGlobalEnd() { return globalEnd; } + public int getLineIndex() { return lineIndex; } + public int getLength() { return text.length(); } + public List getTokens() { return Collections.unmodifiableList(tokens); } + public List getIndentGuides() { return Collections.unmodifiableList(indentGuides); } + public ScriptDocument getParent() { return parent; } + + void setParent(ScriptDocument parent) { this.parent = parent; } + void setPrev(ScriptLine prev) { this.prev = prev; } + void setNext(ScriptLine next) { this.next = next; } + + // ==================== NAVIGATION ==================== + + public ScriptLine prev() { return prev; } + public ScriptLine next() { return next; } + + public Token getFirstToken() { + return tokens.isEmpty() ? null : tokens.get(0); + } + + public Token getLastToken() { + return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); + } + + public Token getTokenAt(int globalPosition) { + for (Token t : tokens) { + if (globalPosition >= t.getGlobalStart() && globalPosition < t.getGlobalEnd()) { + return t; + } + } + return null; + } + + // ==================== TOKEN MANAGEMENT ==================== + + /** + * Clear all tokens from this line. + */ + public void clearTokens() { + tokens.clear(); + } + + /** + * Add a token to this line and set up navigation links. + */ + public void addToken(Token token) { + if (!tokens.isEmpty()) { + Token last = tokens.get(tokens.size() - 1); + last.setNext(token); + token.setPrev(last); + } + token.setParentLine(this); + tokens.add(token); + } + + /** + * Build tokens from a list of marks (highlight regions). + * Fills gaps between marks with DEFAULT tokens. + * + * @param marks List of highlight marks that overlap this line + * @param fullText The complete document text + */ + public void buildTokensFromMarks(List marks, String fullText) { + clearTokens(); + int cursor = globalStart; + + for (Mark mark : marks) { + // Skip marks that don't overlap this line + if (mark.end <= globalStart || mark.start >= globalEnd) { + continue; + } + + // Clamp mark to line boundaries + int tokenStart = Math.max(mark.start, globalStart); + int tokenEnd = Math.min(mark.end, globalEnd); + + // Validate bounds + tokenStart = Math.max(0, Math.min(tokenStart, fullText.length())); + tokenEnd = Math.max(0, Math.min(tokenEnd, fullText.length())); + + // Add default token for any gap before this mark + if (cursor < tokenStart) { + int gapEnd = Math.min(tokenStart, fullText.length()); + String gapText = fullText.substring(cursor, gapEnd); + addToken(Token.defaultToken(gapText, cursor, gapEnd)); + } + + // Add the marked token + if (tokenStart < tokenEnd) { + String tokenText = fullText.substring(tokenStart, tokenEnd); + Token token = new Token(tokenText, tokenStart, tokenEnd, mark.type); + addToken(token); + } + + cursor = tokenEnd; + } + + // Add trailing default token if needed + if (cursor < globalEnd) { + int end = Math.min(globalEnd, fullText.length()); + if (cursor < end) { + String trailingText = fullText.substring(cursor, end); + addToken(Token.defaultToken(trailingText, cursor, end)); + } + } + } + + // ==================== INDENT GUIDES ==================== + + /** + * Clear indent guides for this line. + */ + public void clearIndentGuides() { + indentGuides.clear(); + } + + /** + * Add an indent guide at the specified column. + */ + public void addIndentGuide(int column) { + if (!indentGuides.contains(column)) { + indentGuides.add(column); + } + } + + // ==================== RENDERING ==================== + + /** + * Draw this line with syntax highlighting using Minecraft color codes. + * Compatible with the existing rendering system. + */ + public void drawString(int x, int y, int defaultColor) { + StringBuilder builder = new StringBuilder(); + int lastIndex = 0; + + for (Token t : tokens) { + int tokenStart = t.getGlobalStart() - globalStart; // relative position in line + + // Append any text before this token (spaces, punctuation, etc.) + if (tokenStart > lastIndex && tokenStart <= text.length()) { + builder.append(text, lastIndex, tokenStart); + } + + // Append the colored token + builder.append(COLOR_CHAR) + .append(t.getColorCode()) + .append(t.getText()) + .append(COLOR_CHAR) + .append('f'); // Reset to white + + lastIndex = tokenStart + t.getText().length(); + } + + // Append any remaining text after the last token + if (lastIndex < text.length()) { + builder.append(text.substring(lastIndex)); + } + + ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor); + } + + /** + * Draw this line with hex colors instead of Minecraft color codes. + * More flexible but requires custom font rendering. + * + * @param x X position + * @param y Y position + * @param renderer A functional interface for drawing colored text segments + */ + public void drawStringHex(int x, int y, HexColorRenderer renderer) { + int currentX = x; + int lastIndex = 0; + + for (Token t : tokens) { + int tokenStart = t.getGlobalStart() - globalStart; + + // Draw any text before this token in default color + if (tokenStart > lastIndex && tokenStart <= text.length()) { + String gap = text.substring(lastIndex, tokenStart); + currentX = renderer.draw(gap, currentX, y, 0xFFFFFF); + } + + // Draw the colored token + currentX = renderer.draw(t.getText(), currentX, y, t.getHexColor()); + + lastIndex = tokenStart + t.getText().length(); + } + + // Draw any remaining text in default color + if (lastIndex < text.length()) { + renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF); + } + } + + /** + * Functional interface for rendering text with hex colors. + */ + @FunctionalInterface + public interface HexColorRenderer { + /** + * Draw text at the specified position with the given color. + * @return The X position after drawing (for continuation) + */ + int draw(String text, int x, int y, int hexColor); + } + + // ==================== UTILITIES ==================== + + /** + * Check if a global position falls within this line. + */ + public boolean containsPosition(int globalPosition) { + return globalPosition >= globalStart && globalPosition < globalEnd; + } + + /** + * Convert a global position to a column (local offset) within this line. + */ + public int toColumn(int globalPosition) { + return Math.max(0, Math.min(globalPosition - globalStart, text.length())); + } + + /** + * Convert a column (local offset) to a global position. + */ + public int toGlobal(int column) { + return globalStart + Math.max(0, Math.min(column, text.length())); + } + + @Override + public String toString() { + return "ScriptLine{" + lineIndex + ": '" + text + "' [" + globalStart + "-" + globalEnd + "], " + tokens.size() + " tokens}"; + } + + // ==================== MARK CLASS (for token building) ==================== + + /** + * A simple mark representing a highlighted region. + * Used during token building phase. + */ + public static class Mark { + public final int start; + public final int end; + public final TokenType type; + + public Mark(int start, int end, TokenType type) { + this.start = start; + this.end = end; + this.type = type; + } + + @Override + public String toString() { + return "Mark{" + type + " [" + start + "-" + end + "]}"; + } + } +} 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 new file mode 100644 index 000000000..e3808a35f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java @@ -0,0 +1,199 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.script.JavaTextContainer; + +import java.util.ArrayList; +import java.util.List; + +/** + * New interpreter-based text container that replaces JavaTextContainer. + * + * This class provides the same interface as JavaTextContainer but uses the + * new modular interpreter system internally. It produces compatible LineData + * objects for seamless integration with GuiScriptTextArea. + * + * Key differences from JavaTextContainer: + * - Clean single-pass tokenization with 7 distinct phases + * - Proper token navigation via prev()/next() methods + * - Type-specific metadata on tokens (TypeInfo, FieldInfo, MethodInfo) + * - Better separation of concerns (TypeResolver, ScriptDocument, ScriptLine) + * - Hex color support in addition to Minecraft color codes + */ +public class ScriptTextContainer extends JavaTextContainer { + + /** + * Feature flag to enable/disable the new interpreter system. + * When false, falls back to original JavaTextContainer behavior. + */ + public static boolean USE_NEW_INTERPRETER = true; + + private ScriptDocument document; + + public ScriptTextContainer(String text) { + super(text); + if (USE_NEW_INTERPRETER) { + document = new ScriptDocument(text); + } + } + + @Override + public void init(int width, int height) { + if (!USE_NEW_INTERPRETER) { + super.init(width, height); + return; + } + + lineHeight = ClientProxy.Font.height(); + if (lineHeight == 0) lineHeight = 12; + + // Initialize the document + document.setText(text); + document.init(width, height); + + // Convert ScriptLines to LineData for compatibility + rebuildLineData(); + + linesCount = lines.size(); + totalHeight = linesCount * lineHeight; + visibleLines = Math.max(height / lineHeight - 1, 1); + } + + /** + * Initialize with explicit text (called when text changes). + */ + @Override + public void init(String text, int width, int height) { + this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); + + if (!USE_NEW_INTERPRETER) { + super.init(text, width, height); + return; + } + + document = new ScriptDocument(this.text); + init(width, height); + } + + /** + * Convert ScriptDocument lines to the legacy LineData format. + */ + private void rebuildLineData() { + lines.clear(); + for (ScriptLine scriptLine : document.getLines()) { + LineData ld = new LineData( + scriptLine.getText(), + scriptLine.getGlobalStart(), + scriptLine.getGlobalEnd() + ); + + // Copy indent guides + ld.indentCols.addAll(scriptLine.getIndentGuides()); + + // Convert tokens - use fully qualified name to avoid conflict with inherited Token + for (noppes.npcs.client.gui.util.script.interpreter.Token interpreterToken : scriptLine.getTokens()) { + ld.tokens.add(new JavaTextContainer.Token( + interpreterToken.getText(), + toLegacyTokenType(interpreterToken.getType()), + interpreterToken.getGlobalStart(), + interpreterToken.getGlobalEnd() + )); + } + + lines.add(ld); + } + } + + /** + * Main formatting entry point - matches JavaTextContainer signature. + */ + @Override + public void formatCodeText() { + if (!USE_NEW_INTERPRETER) { + super.formatCodeText(); + return; + } + + document.formatCodeText(); + rebuildLineData(); + } + + /** + * Convert new interpreter TokenType to legacy JavaTextContainer.TokenType format. + */ + private JavaTextContainer.TokenType toLegacyTokenType(TokenType type) { + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.COMMENT) return JavaTextContainer.TokenType.COMMENT; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.STRING) return JavaTextContainer.TokenType.STRING; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.CLASS_KEYWORD) return JavaTextContainer.TokenType.CLASS_KEYWORD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.IMPORT_KEYWORD) return JavaTextContainer.TokenType.IMPORT_KEYWORD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.KEYWORD) return JavaTextContainer.TokenType.KEYWORD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.MODIFIER) return JavaTextContainer.TokenType.MODIFIER; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.NEW_TYPE) return JavaTextContainer.TokenType.NEW_TYPE; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.INTERFACE_DECL) return JavaTextContainer.TokenType.INTERFACE_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.ENUM_DECL) return JavaTextContainer.TokenType.ENUM_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.CLASS_DECL) return JavaTextContainer.TokenType.CLASS_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.IMPORTED_CLASS) return JavaTextContainer.TokenType.IMPORTED_CLASS; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.TYPE_DECL) return JavaTextContainer.TokenType.TYPE_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.METHOD_DECL) return JavaTextContainer.TokenType.METHOD_DECARE; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.METHOD_CALL) return JavaTextContainer.TokenType.METHOD_CALL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.NUMBER) return JavaTextContainer.TokenType.NUMBER; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.GLOBAL_FIELD) return JavaTextContainer.TokenType.GLOBAL_FIELD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.LOCAL_FIELD) return JavaTextContainer.TokenType.LOCAL_FIELD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.PARAMETER) return JavaTextContainer.TokenType.PARAMETER; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.UNDEFINED_VAR) return JavaTextContainer.TokenType.UNDEFINED_VAR; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.VARIABLE) return JavaTextContainer.TokenType.VARIABLE; + return JavaTextContainer.TokenType.DEFAULT; + } + + // ==================== ACCESSORS ==================== + + /** + * Get the underlying ScriptDocument for advanced operations. + */ + public ScriptDocument getDocument() { + return document; + } + + /** + * Get method blocks for compatibility with existing code. + * Creates MethodBlock-like objects from the new MethodInfo. + */ + public List getMethodBlocks() { + if (document == null) { + return new ArrayList<>(); + } + List blocks = new ArrayList<>(); + for (MethodInfo method : document.getMethods()) { + blocks.add(new MethodBlockCompat(method)); + } + return blocks; + } + + /** + * Compatibility wrapper for MethodBlock. + */ + public static class MethodBlockCompat { + private final MethodInfo methodInfo; + public List parameters = new ArrayList<>(); + public List localVariables = new ArrayList<>(); + + public MethodBlockCompat(MethodInfo methodInfo) { + this.methodInfo = methodInfo; + for (FieldInfo param : methodInfo.getParameters()) { + parameters.add(param.getName()); + } + } + + public int getStartOffset() { return methodInfo.getDeclarationOffset(); } + public int getEndOffset() { return methodInfo.getBodyEnd(); } + + public boolean containsPosition(int position) { + return methodInfo.containsPosition(position); + } + + public boolean isLocalDeclaredAtPosition(String varName, int position) { + // Simplified - would need local variable tracking for full support + return localVariables.contains(varName); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java new file mode 100644 index 000000000..cbd8cfc4f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -0,0 +1,237 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Represents a single token in the source code with its type and metadata. + * Tokens are the atomic units of the syntax highlighting system. + * + * Each token knows: + * - Its text content + * - Its position in the global source + * - Its type (for coloring) + * - Type-specific metadata (resolved class info, declaration info, etc.) + */ +public class Token { + + private final String text; + private final int globalStart; // start offset in the full document + private final int globalEnd; // end offset in the full document + private TokenType type; + + // Optional metadata based on token type + private TypeInfo typeInfo; // For class references, type declarations + private FieldInfo fieldInfo; // For field references + private MethodInfo methodInfo; // For method calls/declarations + private ImportData importData; // For import statements + + // Navigation - set by ScriptLine + private Token prev; + private Token next; + private ScriptLine parentLine; + + public Token(String text, int globalStart, int globalEnd, TokenType type) { + this.text = text; + this.globalStart = globalStart; + this.globalEnd = globalEnd; + this.type = type; + } + + // ==================== FACTORY METHODS ==================== + + public static Token defaultToken(String text, int start, int end) { + return new Token(text, start, end, TokenType.DEFAULT); + } + + public static Token keyword(String text, int start, int end) { + return new Token(text, start, end, TokenType.KEYWORD); + } + + public static Token modifier(String text, int start, int end) { + return new Token(text, start, end, TokenType.MODIFIER); + } + + public static Token comment(String text, int start, int end) { + return new Token(text, start, end, TokenType.COMMENT); + } + + public static Token string(String text, int start, int end) { + return new Token(text, start, end, TokenType.STRING); + } + + public static Token number(String text, int start, int end) { + return new Token(text, start, end, TokenType.NUMBER); + } + + public static Token typeReference(String text, int start, int end, TypeInfo info) { + Token t = new Token(text, start, end, info != null ? info.getTokenType() : TokenType.TYPE_DECL); + t.typeInfo = info; + return t; + } + + public static Token methodDecl(String text, int start, int end, MethodInfo info) { + Token t = new Token(text, start, end, TokenType.METHOD_DECL); + t.methodInfo = info; + return t; + } + + public static Token methodCall(String text, int start, int end, MethodInfo info, boolean resolved) { + Token t = new Token(text, start, end, resolved ? TokenType.METHOD_CALL : TokenType.DEFAULT); + t.methodInfo = info; + return t; + } + + public static Token globalField(String text, int start, int end, FieldInfo info) { + Token t = new Token(text, start, end, TokenType.GLOBAL_FIELD); + t.fieldInfo = info; + return t; + } + + public static Token localField(String text, int start, int end, FieldInfo info) { + Token t = new Token(text, start, end, TokenType.LOCAL_FIELD); + t.fieldInfo = info; + return t; + } + + public static Token parameter(String text, int start, int end, FieldInfo info) { + Token t = new Token(text, start, end, TokenType.PARAMETER); + t.fieldInfo = info; + return t; + } + + public static Token undefined(String text, int start, int end) { + return new Token(text, start, end, TokenType.UNDEFINED_VAR); + } + + // ==================== GETTERS ==================== + + public String getText() { return text; } + public int getGlobalStart() { return globalStart; } + public int getGlobalEnd() { return globalEnd; } + public int getLength() { return globalEnd - globalStart; } + public TokenType getType() { return type; } + public TypeInfo getTypeInfo() { return typeInfo; } + public FieldInfo getFieldInfo() { return fieldInfo; } + public MethodInfo getMethodInfo() { return methodInfo; } + public ImportData getImportData() { return importData; } + public ScriptLine getParentLine() { return parentLine; } + + // ==================== SETTERS ==================== + + public void setType(TokenType type) { this.type = type; } + public void setTypeInfo(TypeInfo info) { this.typeInfo = info; } + public void setFieldInfo(FieldInfo info) { this.fieldInfo = info; } + public void setMethodInfo(MethodInfo info) { this.methodInfo = info; } + public void setImportData(ImportData data) { this.importData = data; } + + void setParentLine(ScriptLine line) { this.parentLine = line; } + void setPrev(Token prev) { this.prev = prev; } + void setNext(Token next) { this.next = next; } + + // ==================== NAVIGATION ==================== + + /** + * Get the previous token (may be on a previous line). + */ + public Token prev() { + if (prev != null) return prev; + if (parentLine == null) return null; + + ScriptLine prevLine = parentLine.prev(); + while (prevLine != null) { + Token last = prevLine.getLastToken(); + if (last != null) return last; + prevLine = prevLine.prev(); + } + return null; + } + + /** + * Get the next token (may be on a following line). + */ + public Token next() { + if (next != null) return next; + if (parentLine == null) return null; + + ScriptLine nextLine = parentLine.next(); + while (nextLine != null) { + Token first = nextLine.getFirstToken(); + if (first != null) return first; + nextLine = nextLine.next(); + } + return null; + } + + /** + * Get the previous token on the same line only. + */ + public Token prevOnLine() { + return prev; + } + + /** + * Get the next token on the same line only. + */ + public Token nextOnLine() { + return next; + } + + // ==================== TYPE CHECKS ==================== + + public boolean isKeyword() { + return type == TokenType.KEYWORD || type == TokenType.MODIFIER || + type == TokenType.CLASS_KEYWORD || type == TokenType.IMPORT_KEYWORD; + } + + public boolean isTypeReference() { + return type == TokenType.TYPE_DECL || type == TokenType.IMPORTED_CLASS || + type == TokenType.CLASS_DECL || type == TokenType.INTERFACE_DECL || + type == TokenType.ENUM_DECL || type == TokenType.NEW_TYPE; + } + + public boolean isResolved() { + if (typeInfo != null) return typeInfo.isResolved(); + if (fieldInfo != null) return fieldInfo.isResolved(); + if (methodInfo != null) return methodInfo.isResolved(); + return type != TokenType.UNDEFINED_VAR; + } + + public boolean isIdentifier() { + if (text.isEmpty()) return false; + char first = text.charAt(0); + return Character.isJavaIdentifierStart(first); + } + + public boolean startsWithUpperCase() { + return !text.isEmpty() && Character.isUpperCase(text.charAt(0)); + } + + public boolean isMethodCall() { + return type == TokenType.METHOD_CALL || + (type == TokenType.DEFAULT && methodInfo != null); + } + + public boolean isField() { + return type == TokenType.GLOBAL_FIELD || type == TokenType.LOCAL_FIELD || + type == TokenType.PARAMETER; + } + + // ==================== RENDERING ==================== + + /** + * Get the hex color for this token. + */ + public int getHexColor() { + return type.getHexColor(); + } + + /** + * Get the Minecraft color code character. + */ + public char getColorCode() { + return type.toColorCode(); + } + + @Override + public String toString() { + return "Token{'" + text + "', " + type + ", [" + globalStart + "-" + globalEnd + "]}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java new file mode 100644 index 000000000..2f91597d2 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java @@ -0,0 +1,103 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Defines all token types for syntax highlighting with hex colors and priorities. + * Priority determines which token type wins when marks overlap. + * Higher priority = wins conflicts. + */ +public enum TokenType { + // Comments and strings have highest priority - they override everything inside them + COMMENT(0x777777, 130), + STRING(0xCC8855, 120), + + // Keywords and modifiers + CLASS_KEYWORD(0xFF5555, 115), // 'class', 'interface', 'enum' keywords + IMPORT_KEYWORD(0xFFAA00, 110), // 'import' keyword + KEYWORD(0xFF5555, 100), // control flow: if, else, for, while, etc. + MODIFIER(0xFFAA00, 90), // public, private, static, final, etc. + + // Type declarations and references + NEW_TYPE(0xFF55FF, 80), // type after 'new' keyword + INTERFACE_DECL(0x55FFFF, 85), // interface names (aqua) + ENUM_DECL(0xFF55FF, 85), // enum names (magenta) + CLASS_DECL(0x00AAAA, 85), // class names in declarations + IMPORTED_CLASS(0x00AAAA, 75), // imported class usages + TYPE_DECL(0x00AAAA, 70), // package paths, type references + + // Methods + METHOD_DECL(0x00AA00, 60), // method declarations (green) + METHOD_CALL(0x55FF55, 50), // method calls (bright green) + + // Variables and fields + UNDEFINED_VAR(0xAA0000, 105), // unresolved variables (dark red) - high priority + PARAMETER(0x5555FF, 36), // method parameters (blue) + GLOBAL_FIELD(0x55FFFF, 35), // class-level fields (aqua) + LOCAL_FIELD(0xFFFF55, 25), // local variables (yellow) + + // Literals + NUMBER(0x777777, 40), // numeric literals + + // Default + VARIABLE(0xFFFFFF, 30), // generic variables + DEFAULT(0xFFFFFF, 0); // default text color (white) + + private final int hexColor; + private final int priority; + + TokenType(int hexColor, int priority) { + this.hexColor = hexColor; + this.priority = priority; + } + + public int getHexColor() { + return hexColor; + } + + public int getPriority() { + return priority; + } + + /** + * Convert this token type to a Minecraft color code character. + * Used for backward compatibility with the existing rendering system. + */ + public char toColorCode() { + switch (this) { + case COMMENT: + case NUMBER: + return '7'; // gray + case STRING: + return '5'; // purple + case CLASS_KEYWORD: + case KEYWORD: + return 'c'; // red + case IMPORT_KEYWORD: + case MODIFIER: + return '6'; // gold + case NEW_TYPE: + case ENUM_DECL: + return 'd'; // magenta + case INTERFACE_DECL: + case GLOBAL_FIELD: + return 'b'; // aqua + case CLASS_DECL: + case IMPORTED_CLASS: + case TYPE_DECL: + return '3'; // dark aqua + case METHOD_DECL: + return '2'; // dark green + case METHOD_CALL: + return 'a'; // green + case LOCAL_FIELD: + return 'e'; // yellow + case PARAMETER: + return '9'; // blue + case UNDEFINED_VAR: + return '4'; // dark red + case VARIABLE: + case DEFAULT: + default: + return 'f'; // white + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java new file mode 100644 index 000000000..544bdadf3 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java @@ -0,0 +1,173 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Represents resolved type information for a class/interface/enum. + * Immutable data class holding all type metadata. + */ +public final class TypeInfo { + + public enum Kind { + CLASS, + INTERFACE, + ENUM, + UNKNOWN + } + + private final String simpleName; // e.g., "List", "ColorType" + private final String fullName; // e.g., "java.util.List", "kamkeel...IOverlay$ColorType" + private final String packageName; // e.g., "java.util", "kamkeel.npcdbc.api.client.overlay" + private final Kind kind; // CLASS, INTERFACE, ENUM + private final Class javaClass; // The actual resolved Java class (null if unresolved) + private final boolean resolved; // Whether this type was successfully resolved + private final TypeInfo enclosingType; // For inner classes, the outer type (null if top-level) + + private TypeInfo(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType) { + this.simpleName = simpleName; + this.fullName = fullName; + this.packageName = packageName; + this.kind = kind; + this.javaClass = javaClass; + this.resolved = resolved; + this.enclosingType = enclosingType; + } + + // Factory methods + public static TypeInfo resolved(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass) { + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, null); + } + + public static TypeInfo resolvedInner(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass, TypeInfo enclosing) { + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, enclosing); + } + + public static TypeInfo unresolved(String simpleName, String fullPath) { + int lastDot = fullPath.lastIndexOf('.'); + String pkg = lastDot > 0 ? fullPath.substring(0, lastDot) : ""; + return new TypeInfo(simpleName, fullPath, pkg, Kind.UNKNOWN, null, false, null); + } + + public static TypeInfo fromClass(Class clazz) { + if (clazz == null) return null; + + Kind kind; + if (clazz.isInterface()) { + kind = Kind.INTERFACE; + } else if (clazz.isEnum()) { + kind = Kind.ENUM; + } else { + kind = Kind.CLASS; + } + + String fullName = clazz.getName(); + String simpleName = clazz.getSimpleName(); + Package pkg = clazz.getPackage(); + String packageName = pkg != null ? pkg.getName() : ""; + + TypeInfo enclosing = null; + if (clazz.getEnclosingClass() != null) { + enclosing = fromClass(clazz.getEnclosingClass()); + } + + return new TypeInfo(simpleName, fullName, packageName, kind, clazz, true, enclosing); + } + + // Getters + public String getSimpleName() { return simpleName; } + public String getFullName() { return fullName; } + public String getPackageName() { return packageName; } + public Kind getKind() { return kind; } + public Class getJavaClass() { return javaClass; } + public boolean isResolved() { return resolved; } + public TypeInfo getEnclosingType() { return enclosingType; } + public boolean isInnerClass() { return enclosingType != null; } + + /** + * Get the appropriate TokenType for highlighting this type. + */ + public TokenType getTokenType() { + if (!resolved) { + return TokenType.UNDEFINED_VAR; + } + switch (kind) { + case INTERFACE: + return TokenType.INTERFACE_DECL; + case ENUM: + return TokenType.ENUM_DECL; + case CLASS: + default: + return TokenType.IMPORTED_CLASS; + } + } + + /** + * Check if this type has a method with the given name. + */ + public boolean hasMethod(String methodName) { + if (javaClass == null) return false; + try { + for (java.lang.reflect.Method m : javaClass.getMethods()) { + if (m.getName().equals(methodName)) { + return true; + } + } + } catch (Exception e) { + // Security or linkage error + } + return false; + } + + /** + * Check if this type has a method with the given name and parameter count. + */ + public boolean hasMethod(String methodName, int paramCount) { + if (javaClass == null) return false; + try { + for (java.lang.reflect.Method m : javaClass.getMethods()) { + if (m.getName().equals(methodName) && m.getParameterCount() == paramCount) { + return true; + } + } + } catch (Exception e) { + // Security or linkage error + } + return false; + } + + /** + * Check if this type has a field with the given name. + */ + public boolean hasField(String fieldName) { + if (javaClass == null) return false; + try { + for (java.lang.reflect.Field f : javaClass.getFields()) { + if (f.getName().equals(fieldName)) { + return true; + } + } + } catch (Exception e) { + // Security or linkage error + } + return false; + } + + @Override + public String toString() { + return "TypeInfo{" + fullName + ", " + kind + ", resolved=" + resolved + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TypeInfo typeInfo = (TypeInfo) o; + return fullName.equals(typeInfo.fullName); + } + + @Override + public int hashCode() { + return fullName.hashCode(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java new file mode 100644 index 000000000..0c8794c85 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java @@ -0,0 +1,389 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import java.util.*; + +/** + * Resolves class/interface/enum types from import paths and simple names. + * Maintains caches for performance and handles inner class resolution. + * + * This is a clean reimplementation of ClassPathFinder with better OOP structure. + */ +public class TypeResolver { + + // Cache: fully-qualified class name -> TypeInfo + private final Map typeCache = new HashMap<>(); + + // Cache: validated package paths + private final Set validPackages = new HashSet<>(); + + // Auto-imported java.lang classes + public static final Set JAVA_LANG_CLASSES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", + "Character", "Boolean", "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", + "Error", "Throwable", "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", + "CharSequence", "Cloneable", "Process", "ProcessBuilder", "Runtime", "SecurityManager", + "ClassLoader", "Package", "ArithmeticException", "ArrayIndexOutOfBoundsException", + "ClassCastException", "IllegalArgumentException", "IllegalStateException", + "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", + "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" + ))); + + // Singleton instance for global caching (optional - can also use per-document instances) + private static TypeResolver instance; + + public static TypeResolver getInstance() { + if (instance == null) { + instance = new TypeResolver(); + } + return instance; + } + + public TypeResolver() { + // Pre-register common packages + validPackages.add("java"); + validPackages.add("java.lang"); + validPackages.add("java.util"); + validPackages.add("java.io"); + } + + // ==================== CACHE MANAGEMENT ==================== + + /** + * Clear all caches. Call when imports change significantly. + */ + public void clearCache() { + typeCache.clear(); + validPackages.clear(); + // Re-add common packages + validPackages.add("java"); + validPackages.add("java.lang"); + validPackages.add("java.util"); + validPackages.add("java.io"); + } + + // ==================== TYPE RESOLUTION ==================== + + /** + * Resolve a fully-qualified class name. + * Handles inner classes with both dot and dollar notation. + */ + public TypeInfo resolveFullName(String fullName) { + if (fullName == null || fullName.isEmpty()) { + return null; + } + + // Normalize: remove whitespace around dots + String normalized = fullName.replaceAll("\\s*\\.\\s*", ".").trim(); + + // Check cache first + if (typeCache.containsKey(normalized)) { + return typeCache.get(normalized); + } + + // Try direct resolution + TypeInfo result = tryResolveClass(normalized); + if (result != null) { + return result; + } + + // Try converting dots to $ for inner classes + // Work backwards from the end, trying each segment as an inner class + String[] segments = normalized.split("\\."); + for (int i = segments.length - 1; i > 0; i--) { + StringBuilder candidate = new StringBuilder(); + // Package portion + for (int j = 0; j < i; j++) { + if (j > 0) candidate.append('.'); + candidate.append(segments[j]); + } + // Class portion with $ + for (int j = i; j < segments.length; j++) { + candidate.append(j == i ? '.' : '$'); + candidate.append(segments[j]); + } + + result = tryResolveClass(candidate.toString()); + if (result != null) { + // Cache the original lookup + typeCache.put(normalized, result); + return result; + } + } + + // Not found - cache the miss + typeCache.put(normalized, null); + return null; + } + + /** + * Resolve a simple class name using provided import context. + */ + public TypeInfo resolveSimpleName(String simpleName, + Map imports, + Set wildcardPackages) { + if (simpleName == null || simpleName.isEmpty()) { + return null; + } + + // 1. Check explicit imports + ImportData importData = imports.get(simpleName); + if (importData != null && importData.getResolvedType() != null) { + return importData.getResolvedType(); + } + if (importData != null) { + TypeInfo resolved = resolveFullName(importData.getFullPath()); + if (resolved != null) { + importData.setResolvedType(resolved); + return resolved; + } + } + + // 2. Check java.lang + if (JAVA_LANG_CLASSES.contains(simpleName)) { + TypeInfo langType = resolveFullName("java.lang." + simpleName); + if (langType != null) { + return langType; + } + } + + // 3. Check wildcard packages + if (wildcardPackages != null) { + for (String pkg : wildcardPackages) { + // Try as package.ClassName + TypeInfo fromPkg = resolveFullName(pkg + "." + simpleName); + if (fromPkg != null) { + return fromPkg; + } + + // Try as OuterClass$InnerClass (for class-level wildcards like IOverlay.*) + TypeInfo fromInner = resolveFullName(pkg + "$" + simpleName); + if (fromInner != null) { + return fromInner; + } + } + } + + return null; + } + + /** + * Try to load a class and create TypeInfo for it. + */ + private TypeInfo tryResolveClass(String className) { + if (className == null || className.isEmpty()) { + return null; + } + + // Check cache + if (typeCache.containsKey(className)) { + return typeCache.get(className); + } + + try { + Class clazz = Class.forName(className); + TypeInfo info = TypeInfo.fromClass(clazz); + typeCache.put(className, info); + + // Register the package as valid + registerPackage(info.getPackageName()); + + return info; + } catch (ClassNotFoundException e) { + // Cache the miss + typeCache.put(className, null); + return null; + } catch (LinkageError e) { + // NoClassDefFoundError is a subclass of LinkageError + typeCache.put(className, null); + return null; + } + } + + // ==================== PACKAGE VALIDATION ==================== + + /** + * Check if a package path is valid. + */ + public boolean isValidPackage(String packagePath) { + if (packagePath == null || packagePath.isEmpty()) { + return false; + } + + if (validPackages.contains(packagePath)) { + return true; + } + + // Try to find a class in this package to validate it + String[] testClasses = getTestClassesForPackage(packagePath); + for (String testClass : testClasses) { + try { + Class.forName(testClass); + registerPackage(packagePath); + return true; + } catch (ClassNotFoundException | LinkageError ignored) { + } + } + + return false; + } + + /** + * Register a package path as valid (and all parent packages). + */ + private void registerPackage(String packagePath) { + if (packagePath == null || packagePath.isEmpty()) { + return; + } + validPackages.add(packagePath); + + // Also register parent packages + int lastDot; + String current = packagePath; + while ((lastDot = current.lastIndexOf('.')) > 0) { + current = current.substring(0, lastDot); + validPackages.add(current); + } + } + + private String[] getTestClassesForPackage(String packagePath) { + switch (packagePath) { + case "java": + return new String[]{"java.lang.Object"}; + case "java.util": + return new String[]{"java.util.List", "java.util.Map"}; + case "java.io": + return new String[]{"java.io.File", "java.io.InputStream"}; + case "java.net": + return new String[]{"java.net.URL", "java.net.Socket"}; + case "java.lang": + return new String[]{"java.lang.Object", "java.lang.String"}; + default: + return new String[]{}; + } + } + + // ==================== IMPORT RESOLUTION ==================== + + /** + * Resolve all imports and return a map of simple name -> ImportData. + */ + public Map resolveImports(List imports) { + Map resolved = new HashMap<>(); + + for (ImportData imp : imports) { + if (imp.isWildcard()) { + // Wildcard imports: validate the package exists + imp.markResolved(isValidPackage(imp.getFullPath()) || + resolveFullName(imp.getFullPath()) != null); + } else { + // Specific import: resolve the class + TypeInfo typeInfo = resolveFullName(imp.getFullPath()); + imp.setResolvedType(typeInfo); + if (imp.getSimpleName() != null) { + resolved.put(imp.getSimpleName(), imp); + } + } + } + + return resolved; + } + + // ==================== GENERIC TYPE PARSING ==================== + + /** + * Parse type names from generic content like "Map>". + * Returns TypeInfo for each type found. + */ + public List parseGenericTypes(String content, + Map imports, + Set wildcardPackages) { + List results = new ArrayList<>(); + parseGenericTypesRecursive(content, 0, imports, wildcardPackages, results); + return results; + } + + private void parseGenericTypesRecursive(String content, int baseOffset, + Map imports, + Set wildcardPackages, + List results) { + if (content == null || content.isEmpty()) return; + + int i = 0; + while (i < content.length()) { + char c = content.charAt(i); + + // Skip non-identifier characters + if (!Character.isJavaIdentifierStart(c)) { + i++; + continue; + } + + // Found start of identifier + int start = i; + while (i < content.length() && Character.isJavaIdentifierPart(content.charAt(i))) { + i++; + } + String typeName = content.substring(start, i); + + // Only process uppercase-starting identifiers as types + if (Character.isUpperCase(typeName.charAt(0))) { + TypeInfo info = resolveSimpleName(typeName, imports, wildcardPackages); + results.add(new GenericTypeOccurrence( + baseOffset + start, + baseOffset + i, + typeName, + info + )); + } + + // Skip whitespace + while (i < content.length() && Character.isWhitespace(content.charAt(i))) { + i++; + } + + // Check for nested generic + if (i < content.length() && content.charAt(i) == '<') { + int nestedStart = i + 1; + int depth = 1; + i++; + + while (i < content.length() && depth > 0) { + if (content.charAt(i) == '<') depth++; + else if (content.charAt(i) == '>') depth--; + i++; + } + + // Recursively parse nested content + if (nestedStart < i - 1) { + String nestedContent = content.substring(nestedStart, i - 1); + parseGenericTypesRecursive(nestedContent, baseOffset + nestedStart, + imports, wildcardPackages, results); + } + } + } + } + + /** + * Represents a type occurrence within generic content. + */ + public static class GenericTypeOccurrence { + public final int startOffset; + public final int endOffset; + public final String typeName; + public final TypeInfo typeInfo; + + public GenericTypeOccurrence(int startOffset, int endOffset, String typeName, TypeInfo typeInfo) { + this.startOffset = startOffset; + this.endOffset = endOffset; + this.typeName = typeName; + this.typeInfo = typeInfo; + } + + public TokenType getTokenType() { + if (typeInfo == null || !typeInfo.isResolved()) { + return TokenType.UNDEFINED_VAR; + } + return typeInfo.getTokenType(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/package-info.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/package-info.java new file mode 100644 index 000000000..0fe302521 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/package-info.java @@ -0,0 +1,102 @@ +/** + *

Interpreter Package - Java Syntax Highlighting System

+ * + *

This package provides a clean, modular rewrite of the JavaTextContainer + * syntax highlighting system. It replaces the original implementation with + * well-structured, atomic classes that work together like LEGO pieces.

+ * + *

Architecture Overview

+ * + *
+ * ┌─────────────────────────────────────────────────────────────────┐
+ * │                    ScriptTextContainer                          │
+ * │  (Compatibility adapter extending JavaTextContainer)            │
+ * │  - USE_NEW_INTERPRETER flag to toggle between systems           │
+ * │  - Produces JavaTextContainer.LineData/Token for GUI compat     │
+ * └───────────────────────┬─────────────────────────────────────────┘
+ *                         │
+ *                         ▼
+ * ┌─────────────────────────────────────────────────────────────────┐
+ * │                     ScriptDocument                              │
+ * │  (Main document container - replaces JavaTextContainer core)    │
+ * │                                                                 │
+ * │  7-Phase Tokenization Pipeline:                                 │
+ * │  1. findExcludedRanges() - strings/comments                     │
+ * │  2. parseImports() - import statements                          │
+ * │  3. parseStructure() - methods, fields                          │
+ * │  4. buildMarks() - all highlighting marks                       │
+ * │  5. resolveConflicts() - priority-based resolution              │
+ * │  6. buildTokens() - convert marks to tokens per line            │
+ * │  7. computeIndentGuides() - visual indent guides                │
+ * └───────────────────────┬─────────────────────────────────────────┘
+ *                         │
+ *                         ▼
+ * ┌─────────────────────────────────────────────────────────────────┐
+ * │                      ScriptLine                                 │
+ * │  (Line container with token management and rendering)           │
+ * │  - Token list with navigation                                   │
+ * │  - Indent guides                                                │
+ * │  - drawString() with color codes                                │
+ * │  - drawStringHex() for direct hex color rendering               │
+ * └───────────────────────┬─────────────────────────────────────────┘
+ *                         │
+ *                         ▼
+ * ┌─────────────────────────────────────────────────────────────────┐
+ * │                        Token                                    │
+ * │  (Atomic token unit with type-specific metadata)                │
+ * │  - Text, start/end offsets                                      │
+ * │  - TokenType (with hex color and priority)                      │
+ * │  - prev()/next() navigation                                     │
+ * │  - Type-specific metadata: TypeInfo, FieldInfo, MethodInfo      │
+ * └─────────────────────────────────────────────────────────────────┘
+ * 
+ * + *

Supporting Classes

+ * + *
    + *
  • TokenType - Enum with hex colors and priorities for all token types
  • + *
  • TypeInfo - Immutable type metadata (class/interface/enum)
  • + *
  • TypeResolver - Class resolution with caching (replaces ClassPathFinder)
  • + *
  • ImportData - Import statement tracking with resolution status
  • + *
  • FieldInfo - Field/variable metadata with scope (global/local/parameter)
  • + *
  • MethodInfo - Method declaration/call metadata with parameters
  • + *
+ * + *

Key Improvements Over Original

+ * + *
    + *
  1. Single-pass tokenization - 7 distinct phases vs multiple loops
  2. + *
  3. Token navigation - prev()/next() methods for easy traversal
  4. + *
  5. Type-specific metadata - Rich information attached to tokens
  6. + *
  7. Hex color support - Direct RGB colors in addition to MC color codes
  8. + *
  9. Clean separation - Each class has one responsibility
  10. + *
  11. Immutable data - TypeInfo, FieldInfo, MethodInfo are immutable
  12. + *
  13. Feature toggle - USE_NEW_INTERPRETER flag for safe rollback
  14. + *
+ * + *

Usage

+ * + *
{@code
+ * // The new system is automatically used via ScriptTextContainer
+ * // To disable and use original JavaTextContainer:
+ * ScriptTextContainer.USE_NEW_INTERPRETER = false;
+ * 
+ * // To access the underlying ScriptDocument:
+ * ScriptTextContainer container = (ScriptTextContainer) textArea.getContainer();
+ * ScriptDocument doc = container.getDocument();
+ * 
+ * // Token navigation example:
+ * for (ScriptLine line : doc.getLines()) {
+ *     for (Token token : line.getTokens()) {
+ *         Token next = token.next();
+ *         Token prev = token.prev();
+ *     }
+ * }
+ * }
+ * + * @since 2.0 + * @see ScriptTextContainer + * @see ScriptDocument + * @see Token + */ +package noppes.npcs.client.gui.util.script.interpreter;