diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 6ca5a4281..391afa5f7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -95,12 +95,20 @@ public class JavaTextContainer extends TextContainer { // Extract method blocks first methodBlocks = MethodBlock.collectMethodBlocks(text); + + // Get excluded ranges (strings and comments) for the entire text + List excludedRanges = MethodBlock.getExcludedRanges(text); - // Global fields (excluding those inside methods) + // Global fields (excluding those inside methods, strings, and comments) Matcher mGlobal = GLOBAL_FIELD_DECL.matcher(text); while (mGlobal.find()) { String varName = mGlobal.group(2); int varPosition = mGlobal.start(2); + + // Skip if inside a string or comment + if (isInExcludedRange(varPosition, excludedRanges)) { + continue; + } // Check if this variable is inside a method boolean isInsideMethod = false; @@ -157,6 +165,35 @@ public class JavaTextContainer extends TextContainer { "Boolean", "Long", "Byte", "Short", "Character", "List", "Map", "Set" )); + // First pass: handle field accesses (this.field and obj.field patterns) + Pattern thisFieldPattern = Pattern.compile("\\bthis\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher thisFieldMatcher = thisFieldPattern.matcher(text); + while (thisFieldMatcher.find()) { + String fieldName = thisFieldMatcher.group(1); + int fieldPos = thisFieldMatcher.start(1); + // Highlight as GLOBAL_FIELD if it exists, UNDEFINED_VAR otherwise + if (globalFields.contains(fieldName)) { + marks.add(new Mark(fieldPos, thisFieldMatcher.end(1), TokenType.GLOBAL_FIELD)); + } else { + marks.add(new Mark(fieldPos, thisFieldMatcher.end(1), TokenType.UNDEFINED_VAR)); + } + } + + // Handle identifier.field patterns (e.g., obj.field, global.field) + // The field part should be highlighted as GLOBAL_FIELD (light blue) + Pattern objFieldPattern = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher objFieldMatcher = objFieldPattern.matcher(text); + while (objFieldMatcher.find()) { + String fieldName = objFieldMatcher.group(2); + int fieldPos = objFieldMatcher.start(2); + // Skip if this is a "this.field" pattern (already handled above) + String objName = objFieldMatcher.group(1); + if (!objName.equals("this")) { + // Highlight the field part as GLOBAL_FIELD (represents another object's field) + marks.add(new Mark(fieldPos, objFieldMatcher.end(2), TokenType.GLOBAL_FIELD)); + } + } + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); Matcher m = identifier.matcher(text); while (m.find()) { @@ -177,10 +214,11 @@ public class JavaTextContainer extends TextContainer { MethodBlock methodBlock = findMethodBlockAtPosition(position); if (methodBlock != null) { - // Inside a method - check in order: parameter, local, global + // Inside a method - check in order: parameter, local (position-aware), global if (methodBlock.parameters.contains(name)) { marks.add(new Mark(m.start(1), m.end(1), TokenType.PARAMETER)); - } else if (methodBlock.localVariables.contains(name)) { + } else if (methodBlock.isLocalDeclaredAtPosition(name, position)) { + // Local variable is declared at or before this position marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); } else if (globalFields.contains(name)) { marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); @@ -249,6 +287,18 @@ public class JavaTextContainer extends TextContainer { return false; } + /** + * Check if position is in an excluded range (string or comment) + */ + private boolean isInExcludedRange(int pos, List ranges) { + for (int[] range : ranges) { + if (pos >= range[0] && pos < range[1]) { + return true; + } + } + return false; + } + public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java index e16fdb764..ef9567779 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java @@ -1,7 +1,9 @@ package noppes.npcs.client.gui.util.script; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,6 +28,7 @@ public class MethodBlock { public String text; public List localVariables = new ArrayList<>(); public List parameters = new ArrayList<>(); // Method parameters + public Map localVarPositions = new HashMap<>(); // Track declaration positions public MethodBlock(int start, int end, String text) { this.startOffset = start; @@ -63,14 +66,54 @@ public class MethodBlock { // Extract local variables declared within this method private void extractLocalVariables() { localVariables.clear(); + localVarPositions.clear(); + + // Get excluded ranges (strings and comments) for this method's text + List excludedRanges = getExcludedRanges(text); + Matcher m = LOCAL_VAR_DECL.matcher(text); while (m.find()) { String varName = m.group(2); - if (!localVariables.contains(varName)) { + int declPosition = m.start(); + + // Skip if inside a string or comment + if (isInExcludedRange(declPosition, excludedRanges)) { + continue; + } + + // Check if this is a "this.field" assignment - if so, skip it + // Look backwards from the declaration position to see if preceded by "this." + boolean isThisFieldAssignment = false; + if (declPosition >= 5) { + int checkStart = Math.max(0, declPosition - 10); + String before = text.substring(checkStart, declPosition); + if (before.matches(".*\\bthis\\s*\\.\\s*$")) { + isThisFieldAssignment = true; + } + } + + if (!isThisFieldAssignment && !localVariables.contains(varName)) { localVariables.add(varName); + // Store the absolute position in the full text (startOffset + relative position) + localVarPositions.put(varName, startOffset + declPosition); } } } + + // Check if a reference position comes before the local variable declaration + // This handles cases like: global.field on line 7, then var global = "x" on line 9 + // At line 7, the local 'global' doesn't exist yet, so it should refer to the global 'global' + public boolean isLocalDeclaredAtPosition(String varName, int absolutePosition) { + if (!localVariables.contains(varName)) { + return false; + } + Integer declPos = localVarPositions.get(varName); + if (declPos == null) { + return false; + } + // The local variable is only "in scope" at or after its declaration position + return absolutePosition >= declPos; + } // Check if a position (in the full text) falls within this method public boolean containsPosition(int position) {