diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index 18f4252a5..eafa2aca9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -224,27 +224,55 @@ public class ClassPathFinder { private int findPackageTypoIndex(String[] segments, int classStartIdx) { // Build the typed package path segment by segment and compare with cache StringBuilder sb = new StringBuilder(); + int lastValidIndex = -1; + for (int i = 0; i < classStartIdx; i++) { if (i > 0) sb.append("."); sb.append(segments[i]); String currentPath = sb.toString(); // Check if this exact path is in cache - if (!validPackages.contains(currentPath)) { - // Not in cache - check if a DIFFERENT path at this level IS in cache - // This would indicate a typo (e.g., "guui" vs "gui") - if (i > 0) { - String parentPath = buildPackagePortion(segments, i); - // Look for any cached package that starts with parent but differs at segment i + if (validPackages.contains(currentPath)) { + lastValidIndex = i + 1; // This segment is valid + } else if (i > 0) { + // Not in cache - check if parent is valid but this segment is wrong + String parentPath = buildPackagePortion(segments, i); + if (validPackages.contains(parentPath)) { + // Parent is valid, but current segment isn't in any cached path + // Check if there ARE cached paths extending from parent + boolean hasAlternative = false; for (String cached : validPackages) { - if (cached.startsWith(parentPath + ".") && !cached.startsWith(currentPath)) { - // Found a cached package with same parent but different segment - return i; // This segment is likely a typo + if (cached.startsWith(parentPath + ".") && cached.length() > parentPath.length() + 1) { + hasAlternative = true; + break; } } + if (hasAlternative) { + // There are valid continuations from parent, but this segment doesn't match any + return i; // This segment is likely a typo + } } } } + + // If we found some valid prefix but not all, return where it stopped being valid + if (lastValidIndex > 0 && lastValidIndex < classStartIdx) { + // Check if segment at lastValidIndex diverges from any known path + String validPrefix = buildPackagePortion(segments, lastValidIndex); + for (String cached : validPackages) { + if (cached.startsWith(validPrefix + ".")) { + // There's a known continuation - check if typed segment matches + String nextTyped = segments[lastValidIndex]; + String cachedRemainder = cached.substring(validPrefix.length() + 1); + String nextCached = cachedRemainder.contains(".") ? + cachedRemainder.substring(0, cachedRemainder.indexOf('.')) : cachedRemainder; + if (!nextTyped.equals(nextCached)) { + return lastValidIndex; // Divergence point + } + } + } + } + return -1; // No typo detected } 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 95d446086..7d7a93a77 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 @@ -93,10 +93,12 @@ public class JavaTextContainer extends TextContainer { private List localFields = new ArrayList<>(); private java.util.Map importedClasses = new java.util.HashMap<>(); // simpleName -> fullName private java.util.Set importedPackages = new java.util.HashSet<>(); // wildcard imports + private java.util.Set unresolvedClasses = new java.util.HashSet<>(); // classes that failed to resolve private void collectImports() { importedClasses.clear(); importedPackages.clear(); + unresolvedClasses.clear(); classPathFinder.clearCache(); List excluded = MethodBlock.getExcludedRanges(text); @@ -125,6 +127,8 @@ public class JavaTextContainer extends TextContainer { } else { // Store original path for unresolved imports importedClasses.put(simpleName, fullPath); + // Track this as an unresolved class + unresolvedClasses.add(simpleName); } } } @@ -277,6 +281,10 @@ public class JavaTextContainer extends TextContainer { // Skip field access (identifier preceded by a dot) - these should be gray/default // e.g., in "container.lines", the "lines" part should not be marked if (isFieldAccess(position)) continue; + + // Never treat parts of import/package statements as undefined variables + // (otherwise the first package segment like "noppes" gets marked red and overrides import highlighting) + if (isInImportOrPackageStatement(position)) continue; // Check if inside a method MethodBlock methodBlock = findMethodBlockAtPosition(position); @@ -301,11 +309,37 @@ public class JavaTextContainer extends TextContainer { // Outside any method - check global fields if (globalFields.contains(name)) { marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + } else if (!isMethodCall(position) && !isTypeReference(name, position)) { + // Mark as undefined if it's not a method call or type reference + marks.add(new Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); } - // Don't mark as undefined outside methods - could be a type name or declaration } } } + + private boolean isInImportOrPackageStatement(int position) { + if (position < 0 || position >= text.length()) return false; + + int lineStart = text.lastIndexOf('\n', position); + lineStart = (lineStart < 0) ? 0 : lineStart + 1; + int lineEnd = text.indexOf('\n', position); + lineEnd = (lineEnd < 0) ? text.length() : lineEnd; + + // Quick scan of the line start for "import" or "package" + int i = lineStart; + while (i < lineEnd && Character.isWhitespace(text.charAt(i))) i++; + + if (i + 6 <= lineEnd && text.startsWith("import", i)) { + // Ensure word boundary + int after = i + 6; + return after == lineEnd || Character.isWhitespace(text.charAt(after)); + } + if (i + 7 <= lineEnd && text.startsWith("package", i)) { + int after = i + 7; + return after == lineEnd || Character.isWhitespace(text.charAt(after)); + } + return false; + } /** * Check if the identifier at this position is a field access (preceded by a dot) @@ -433,7 +467,6 @@ public class JavaTextContainer extends TextContainer { // Fully resolved - highlight package (blue) and class segments (typed) if (hasPackage) { int pkgEnd = pathStart + packagePortion.length(); - if (hasClass) pkgEnd++; // include dot marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); } @@ -455,7 +488,7 @@ public class JavaTextContainer extends TextContainer { // Partial resolution - some class segments are valid, rest is invalid // (e.g., IOverlay.ColorType resolved but ".idk" is invalid) if (hasPackage) { - int pkgEnd = pathStart + packagePortion.length() + 1; // include dot + int pkgEnd = pathStart + packagePortion.length(); marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); } @@ -523,6 +556,12 @@ public class JavaTextContainer extends TextContainer { if (isInExcludedRange(start, excluded)) continue; + // Check if this class was imported but failed to resolve + if (unresolvedClasses.contains(className)) { + marks.add(new Mark(start, end, TokenType.UNDEFINED_VAR)); + continue; + } + // Check if this is an imported class, wildcard match, or java.lang class boolean isImported = importedClasses.containsKey(className) || ClassPathFinder.JAVA_LANG_CLASSES.contains(className); if (!isImported && !importedPackages.isEmpty()) {