imports functional state? (not really)

This commit is contained in:
bigguy345 committed 2025-12-21 23:08:44 +02:00
1 parent 1b0ce874bb
commit 5714d89c41
2 files changed
+777 -356

No files matched your search

@@ -0,0 +1,561 @@
package noppes.npcs.client.gui.util.script;
import java.util.*;
/**
* Helper class for resolving Java classpath imports and determining class types.
* Handles:
* - Class resolution via Class.forName
* - Inner class notation (Outer.Inner -> Outer$Inner)
* - Package validation
* - Type detection (interface/enum/class)
* - Result caching for performance
*/
public class ClassPathFinder {
// Cache: fully-qualified class name -> ClassInfo (null if not found)
private final Map<String, ClassInfo> classCache = new HashMap<>();
// Cache: package prefix -> whether it's a valid package (has classes under it)
private final Set<String> validPackages = new HashSet<>();
// Common java.lang classes (auto-imported in Java)
public static final Set<String> JAVA_LANG_CLASSES = 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"
));
/**
* Information about a resolved class
*/
public static class ClassInfo {
public final String resolvedName; // Full class name with $ for inner classes
public final ClassType type; // INTERFACE, ENUM, or CLASS
public final int packageEndIndex; // Index in original path where package ends (for "java.util.List" -> 9)
public ClassInfo(String resolvedName, ClassType type, int packageEndIndex) {
this.resolvedName = resolvedName;
this.type = type;
this.packageEndIndex = packageEndIndex;
}
}
public enum ClassType {
INTERFACE,
ENUM,
CLASS
}
/**
* Result of resolving an import path
*/
public static class ResolveResult {
public final boolean found;
public final ClassInfo classInfo;
public final String packagePortion; // The package part (e.g., "java.util")
public final String classPortion; // The class part (e.g., "List" or "IOverlay.ColorType")
public final List<ClassSegment> classSegments; // Each class/inner-class segment with its type
public final int invalidStartOffset; // Character offset where invalid portion starts (-1 if all valid or all invalid)
private ResolveResult(boolean found, ClassInfo classInfo, String packagePortion,
String classPortion, List<ClassSegment> classSegments, int invalidStartOffset) {
this.found = found;
this.classInfo = classInfo;
this.packagePortion = packagePortion;
this.classPortion = classPortion;
this.classSegments = classSegments != null ? classSegments : Collections.emptyList();
this.invalidStartOffset = invalidStartOffset;
}
public static ResolveResult notFound(String packagePortion, String classPortion, int invalidStartOffset) {
return new ResolveResult(false, null, packagePortion, classPortion, null, invalidStartOffset);
}
public static ResolveResult found(ClassInfo info, String packagePortion,
String classPortion, List<ClassSegment> segments) {
return new ResolveResult(true, info, packagePortion, classPortion, segments, -1);
}
}
/**
* Represents a segment of the class portion (e.g., "IOverlay" or "ColorType")
*/
public static class ClassSegment {
public final String name;
public final ClassType type;
public ClassSegment(String name, ClassType type) {
this.name = name;
this.type = type;
}
}
/**
* Clear all caches (call when imports change)
*/
public void clearCache() {
classCache.clear();
validPackages.clear();
}
/**
* Resolve an import path to determine if it's a valid class and get its type.
* Handles inner classes like "kamkeel.npcdbc.api.client.overlay.IOverlay.ColorType"
*
* @param importPath The import path (e.g., "java.util.List" or "kamkeel...IOverlay.ColorType")
* @return ResolveResult with resolution details
*/
public ResolveResult resolve(String importPath) {
if (importPath == null || importPath.isEmpty()) {
return ResolveResult.notFound("", "", -1);
}
// Handle trailing dot (user is still typing a package path like "java.util.")
boolean trailingDot = importPath.endsWith(".");
String pathToResolve = trailingDot ? importPath.substring(0, importPath.length() - 1) : importPath;
if (pathToResolve.isEmpty()) {
return ResolveResult.notFound("", "", -1);
}
String[] segments = pathToResolve.split("\\.");
int n = segments.length;
// Find the first uppercase segment (likely start of class name)
int firstUpperIndex = findFirstUppercaseSegment(segments);
// If trailing dot, check if the entire path is a valid package (either cached or all-lowercase)
if (trailingDot) {
// If entire path is lowercase, treat as valid package being typed
if (firstUpperIndex == n) {
// Check if we have it cached OR if it could be a valid package
if (isValidPackage(pathToResolve)) {
return ResolveResult.notFound(pathToResolve, "", -1);
}
// Not cached, but all lowercase - use heuristic: assume valid package
return ResolveResult.notFound(pathToResolve, "", -1);
}
}
// Try different splits: package vs class boundary
// Start from firstUpperIndex and try treating each uppercase segment as potential class start
for (int classStartIdx = firstUpperIndex; classStartIdx < n; classStartIdx++) {
// Try different inner class depths (deepest first)
for (int classEndIdx = n; classEndIdx > classStartIdx; classEndIdx--) {
String candidate = buildClassName(segments, classStartIdx, classEndIdx);
ClassInfo info = tryResolveClass(candidate);
if (info != null) {
// Found a valid class!
String packagePortion = buildPackagePortion(segments, classStartIdx);
String validClassPortion = buildClassPortion(segments, classStartIdx, classEndIdx);
// Register this package as valid for future lookups
if (!packagePortion.isEmpty()) {
registerValidPackage(packagePortion);
}
// Check if there are MORE segments after what we resolved (invalid inner class)
if (classEndIdx < n) {
// e.g., IOverlay.ColorType resolved but there's more like ".idk"
// Mark the extra parts as invalid
List<ClassSegment> classSegments = buildClassSegments(segments, classStartIdx, classEndIdx);
String fullClassPortion = buildClassPortion(segments, classStartIdx, n);
// Calculate where the invalid portion starts (after valid class + dot)
int validEndOffset = packagePortion.length() + (packagePortion.isEmpty() ? 0 : 1) + validClassPortion.length() + 1;
return new ResolveResult(false, info, packagePortion,
fullClassPortion, classSegments, validEndOffset);
}
// Full resolution successful
List<ClassSegment> classSegments = buildClassSegments(segments, classStartIdx, n);
return ResolveResult.found(
new ClassInfo(info.resolvedName, info.type, packagePortion.length()),
packagePortion, validClassPortion, classSegments
);
}
}
}
// Class resolution failed completely
// Strategy: Use heuristic - everything before first uppercase = package, rest = class
// This ensures consistent behavior regardless of cache state
if (firstUpperIndex > 0 && firstUpperIndex < n) {
// There's a package portion (lowercase) and class portion (uppercase start)
String packagePortion = buildPackagePortion(segments, firstUpperIndex);
String classPortion = buildClassPortion(segments, firstUpperIndex, n);
// Check if we can detect a typo in the package by comparing with cached packages
int typoIndex = findPackageTypoIndex(segments, firstUpperIndex);
if (typoIndex >= 0 && typoIndex < firstUpperIndex) {
// Found a divergence from a known valid package
String validPrefix = buildPackagePortion(segments, typoIndex);
int invalidOffset = validPrefix.isEmpty() ? 0 : validPrefix.length() + 1;
return ResolveResult.notFound(validPrefix, buildClassPortion(segments, typoIndex, n), invalidOffset);
}
// No typo detected - assume package is valid, only class portion is invalid
int invalidOffset = packagePortion.length() + 1; // After the package dot
return ResolveResult.notFound(packagePortion, classPortion, invalidOffset);
} else if (firstUpperIndex == 0) {
// Entire path starts with uppercase (e.g., "String" or "MyClass.Inner")
// Treat as class with no package
return ResolveResult.notFound("", pathToResolve, 0);
} else {
// All lowercase, no uppercase found - entire path is package-like
// If trailing dot was handled above, this is just a package without class
return ResolveResult.notFound(pathToResolve, "", -1);
}
}
/**
* Find index where the typed package diverges from a known cached package.
* Returns -1 if no typo detected, or the segment index where divergence starts.
*/
private int findPackageTypoIndex(String[] segments, int classStartIdx) {
// Build the typed package path segment by segment and compare with cache
StringBuilder sb = new StringBuilder();
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
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
}
}
}
}
}
return -1; // No typo detected
}
/**
* Check if a package path is valid (has been seen in successful resolutions or can be validated)
*/
public boolean isValidPackage(String packagePath) {
if (packagePath == null || packagePath.isEmpty()) {
return false;
}
// Check cache first
if (validPackages.contains(packagePath)) {
return true;
}
// Try to find any class under this package to validate it
// Common test classes for well-known packages
String[] testClasses = getTestClassesForPackage(packagePath);
for (String testClass : testClasses) {
try {
Class.forName(testClass);
registerValidPackage(packagePath);
return true;
} catch (ClassNotFoundException | LinkageError ignored) {
}
}
return false;
}
/**
* Get the ClassInfo for a simple class name, using provided imports map
*/
public ClassInfo resolveSimpleName(String simpleName, Map<String, String> importedClasses) {
if (simpleName == null || simpleName.isEmpty()) {
return null;
}
// Check imported classes first
String fullName = importedClasses.get(simpleName);
if (fullName != null) {
return tryResolveClass(fullName);
}
// Check java.lang classes
if (JAVA_LANG_CLASSES.contains(simpleName)) {
return tryResolveClass("java.lang." + simpleName);
}
return null;
}
/**
* Try to resolve a fully-qualified class name (with $ for inner classes)
*/
private ClassInfo tryResolveClass(String className) {
if (className == null || className.isEmpty()) {
return null;
}
// Check cache
if (classCache.containsKey(className)) {
return classCache.get(className);
}
ClassInfo result = null;
try {
Class<?> clazz = Class.forName(className);
result = createClassInfo(className, clazz);
classCache.put(className, result);
} catch (ClassNotFoundException | NoClassDefFoundError e) {
// Not found with given name, will be cached as null
} catch (LinkageError e) {
// Linkage error - treat as not found
}
return result;
}
private ClassInfo createClassInfo(String resolvedName, Class<?> clazz) {
ClassType type;
if (clazz.isInterface()) {
type = ClassType.INTERFACE;
} else if (clazz.isEnum()) {
type = ClassType.ENUM;
} else {
type = ClassType.CLASS;
}
// Calculate package end index
int pkgEnd = resolvedName.lastIndexOf('.');
if (pkgEnd < 0)
pkgEnd = 0;
return new ClassInfo(resolvedName, type, pkgEnd);
}
/**
* Build a class name candidate from segments, using $ for inner classes
*/
private String buildClassName(String[] segments, int classStartIdx, int classEndIdx) {
StringBuilder sb = new StringBuilder();
// Package portion (dot-separated)
for (int i = 0; i < classStartIdx; i++) {
if (i > 0)
sb.append('.');
sb.append(segments[i]);
}
// Class portion ($ for inner classes after the first class)
for (int i = classStartIdx; i < classEndIdx; i++) {
if (i == classStartIdx) {
if (sb.length() > 0)
sb.append('.');
} else {
sb.append('$');
}
sb.append(segments[i]);
}
return sb.toString();
}
private String buildPackagePortion(String[] segments, int classStartIdx) {
if (classStartIdx <= 0)
return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < classStartIdx; i++) {
if (i > 0)
sb.append('.');
sb.append(segments[i]);
}
return sb.toString();
}
private String buildClassPortion(String[] segments, int classStartIdx, int endIdx) {
StringBuilder sb = new StringBuilder();
for (int i = classStartIdx; i < endIdx; i++) {
if (i > classStartIdx)
sb.append('.');
sb.append(segments[i]);
}
return sb.toString();
}
private List<ClassSegment> buildClassSegments(String[] segments, int classStartIdx, int endIdx) {
List<ClassSegment> result = new ArrayList<>();
StringBuilder fullName = new StringBuilder();
// Build package portion
for (int i = 0; i < classStartIdx; i++) {
if (i > 0)
fullName.append('.');
fullName.append(segments[i]);
}
// Add each class segment
for (int i = classStartIdx; i < endIdx; i++) {
if (i == classStartIdx) {
if (fullName.length() > 0)
fullName.append('.');
} else {
fullName.append('$');
}
fullName.append(segments[i]);
// Try to resolve this partial class name
ClassInfo info = tryResolveClass(fullName.toString());
ClassType type = (info != null) ? info.type : ClassType.CLASS;
result.add(new ClassSegment(segments[i], type));
}
return result;
}
private int findFirstUppercaseSegment(String[] segments) {
for (int i = 0; i < segments.length; i++) {
if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) {
return i;
}
}
return segments.length - 1; // Default to last segment
}
private void registerValidPackage(String packagePath) {
validPackages.add(packagePath);
// Also register all 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) {
// Return common classes for well-known packages
switch (packagePath) {
case "java":
return new String[]{"java.lang.Object"};
case "java.util":
return new String[]{"java.util.List", "java.util.Map", "java.util.Set"};
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:
// For unknown packages, try common class suffixes
return new String[]{
packagePath + ".package-info", // Won't work but harmless
};
}
}
// ==================== GENERIC TYPE PARSING ====================
/**
* Parse generic type content and return type names with their positions.
* Handles arbitrary nesting depth like "Map<String, List<Map<String, String>>>"
*
* @param genericContent The content inside <...> (without the outer angle brackets)
* @param importedClasses Map of simple name -> full class name for resolution
* @return List of TypeOccurrence with position and type info
*/
public List<TypeOccurrence> parseGenericTypes(String genericContent, Map<String, String> importedClasses) {
List<TypeOccurrence> results = new ArrayList<>();
parseGenericTypesRecursive(genericContent, 0, importedClasses, results);
return results;
}
/**
* Represents a type occurrence within generic content
*/
public static class TypeOccurrence {
public final int startOffset; // Relative to the start of genericContent
public final int endOffset;
public final String typeName;
public final ClassType type;
public TypeOccurrence(int startOffset, int endOffset, String typeName, ClassType type) {
this.startOffset = startOffset;
this.endOffset = endOffset;
this.typeName = typeName;
this.type = type;
}
}
private void parseGenericTypesRecursive(String content, int baseOffset,
Map<String, String> importedClasses,
List<TypeOccurrence> results) {
if (content == null || content.isEmpty())
return;
int i = 0;
while (i < content.length()) {
char c = content.charAt(i);
// Skip whitespace and punctuation
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 if it looks like a type name (starts with uppercase)
if (Character.isUpperCase(typeName.charAt(0))) {
// Resolve the type
ClassInfo info = resolveSimpleName(typeName, importedClasses);
ClassType classType = (info != null) ? info.type : ClassType.CLASS;
results.add(new TypeOccurrence(baseOffset + start, baseOffset + i, typeName, classType));
}
// 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++;
// Find matching >
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, importedClasses, results);
}
}
}
}
}
@@ -16,15 +16,8 @@ public class JavaTextContainer extends TextContainer {
"\\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");
public static final Pattern CLASS_DECL = Pattern.compile("\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)");
public static final Pattern TYPE_DECL = Pattern.compile(
"(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + // optional modifiers
"([A-Za-z_][a-zA-Z0-9_]*)" + // Group 1 → main type
"\\s*(<([^<>]*(?:<[^<>]*>)*)>)?" + // Group 2 → <…>, Group 3 → inner type (handles one level of nesting)
"\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name
);
public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)");
public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)");;
public static final Pattern METHOD_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type
"([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name
@@ -44,9 +37,12 @@ public class JavaTextContainer extends TextContainer {
public static final Pattern NUMBER = 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");
// Import statement: import [static] package.path.ClassName [.*];
// Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing)
public static final Pattern IMPORT = Pattern.compile(
"\\bimport\\s+(?:static\\s+)?([a-zA-Z_][a-zA-Z0-9_.]*?)(?:\\s*\\.\\s*\\*)?\\s*;");
"(?m)\\bimport\\s+(?:static\\s+)?([a-zA-Z_][a-zA-Z0-9_.]*?)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)");
// ClassPathFinder for resolving imports and determining class types
private final ClassPathFinder classPathFinder = new ClassPathFinder();
public List<LineData> lines = new ArrayList<>();
public List<MethodBlock> methodBlocks = new ArrayList<>();
@@ -97,28 +93,11 @@ public class JavaTextContainer extends TextContainer {
private List<String> localFields = new ArrayList<>();
private java.util.Map<String, String> importedClasses = new java.util.HashMap<>(); // simpleName -> fullName
private java.util.Set<String> importedPackages = new java.util.HashSet<>(); // wildcard imports
private java.util.Map<String, TokenType> classTypeCache = new java.util.HashMap<>(); // fullClassName -> TokenType (cached reflection results)
private java.util.Set<String> resolvedPackages = new java.util.HashSet<>();
private java.util.Set<String> knownPackages = new java.util.HashSet<>();
// Common java.lang classes (auto-imported)
private static final java.util.Set<String> JAVA_LANG_CLASSES = new java.util.HashSet<>(java.util.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"
));
private void collectImports() {
importedClasses.clear();
importedPackages.clear();
// Clear cached import resolutions when re-parsing imports (keeps editor responsive during typing)
importResolveCache.clear();
knownPackages.clear();
classPathFinder.clearCache();
List<int[]> excluded = MethodBlock.getExcludedRanges(text);
Matcher m = IMPORT.matcher(text);
@@ -126,229 +105,53 @@ public class JavaTextContainer extends TextContainer {
if (isInExcludedRange(m.start(), excluded))
continue;
String fullPath = m.group(1).trim();
String matchText = m.group(0);
// Check if wildcard import (import a.b.*;)
if (text.substring(m.start(), m.end()).contains("*")) {
if (matchText.contains("*")) {
importedPackages.add(fullPath);
} else {
// Specific class import: extract simple name
// Resolve the import to get the proper class name
ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath);
// Extract simple name (last segment)
int lastDot = fullPath.lastIndexOf('.');
String simpleName = lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath;
// Remember the package prefix as a known package (even before resolution)
if (lastDot > 0) {
String pkg = fullPath.substring(0, lastDot);
knownPackages.add(pkg);
}
// Store with $ notation for inner classes (e.g., IOverlay.ColorType -> IOverlay$ColorType)
String storagePath = fullPath.replaceAll("\\.(\\p{Upper})", "\\$$1");
importedClasses.put(simpleName, storagePath);
}
}
}
// Cache for resolved import attempts: original fullPath -> resolved class name (with $ for inner classes) or null if unresolved
private java.util.Map<String, String> importResolveCache = new java.util.HashMap<>();
private static class ResolvedImport {
String resolvedClass; // full class name with $ notation if found
int classStartIndex; // index in segments where class starts
TokenType tokenType;
boolean found;
}
/**
* Try to resolve an import path to an actual class using Class.forName by attempting
* different class-start splits and inner-class ($) combinations. Caches results.
*/
private ResolvedImport resolveImportPath(String fullPath) {
ResolvedImport ri = new ResolvedImport();
if (fullPath == null || fullPath.isEmpty()) {
ri.found = false; return ri;
}
if (importResolveCache.containsKey(fullPath)) {
String cached = importResolveCache.get(fullPath);
if (cached == null) { ri.found = false; return ri; }
ri.resolvedClass = cached;
ri.found = true;
ri.tokenType = getClassTokenType(cached, TokenType.IMPORTED_CLASS);
// derive classStartIndex by comparing segments
String[] segs = fullPath.split("\\.");
String[] rcsegs = cached.replace('$', '.').split("\\.");
int si = 0; while (si < segs.length && si < rcsegs.length && segs[si].equals(rcsegs[si])) si++;
ri.classStartIndex = si;
return ri;
}
String[] segments = fullPath.split("\\.");
int n = segments.length;
// Try candidate class start positions. Start from left-most uppercase-looking segment for efficiency.
int firstUpper = 0;
for (int i = 0; i < n; i++) {
if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) { firstUpper = i; break; }
}
outer:
for (int k = firstUpper; k < n; k++) {
// number of inner parts available
int innerAvailable = n - (k + 1);
// Try the longest inner suffix first (deepest inner class)
for (int innerCount = innerAvailable; innerCount >= 0; innerCount--) {
// build top-level class and inner suffix
StringBuilder classBuilder = new StringBuilder();
// package portion
if (k > 0) {
for (int i = 0; i < k; i++) {
if (i > 0) classBuilder.append('.');
classBuilder.append(segments[i]);
}
classBuilder.append('.');
}
// top class
classBuilder.append(segments[k]);
// inner parts
if (innerCount > 0) {
classBuilder.append('$');
for (int j = 0; j < innerCount; j++) {
if (j > 0) classBuilder.append('$');
classBuilder.append(segments[k + 1 + j]);
}
}
String candidate = classBuilder.toString();
TokenType tt = getClassTokenType(candidate, TokenType.IMPORTED_CLASS);
if (tt != TokenType.UNDEFINED_VAR) {
ri.found = true;
ri.resolvedClass = candidate;
ri.classStartIndex = k;
ri.tokenType = tt;
importResolveCache.put(fullPath, candidate);
break outer;
}
// also try without package separator if k==0 (top-level without package)
if (k == 0 && innerCount == 0) {
// candidate is segments[0] only, already tried
}
}
}
if (!ri.found) {
importResolveCache.put(fullPath, null);
ri.found = false;
// approximate classStartIndex as first uppercase or last segment
int classStart = 0;
for (int i = 0; i < segments.length; i++) {
if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) { classStart = i; break; }
}
ri.classStartIndex = classStart;
}
return ri;
}
/**
* Determine the token type for a class using reflection.
* Returns INTERFACE_DECL for interfaces, ENUM_DECL for enums, UNDEFINED for unfound classes, or the defaultType for classes.
*/
private TokenType getClassTokenType(String fullClassName, TokenType defaultType) {
if (fullClassName == null || fullClassName.isEmpty()) return defaultType;
// Check cache first
// if (classTypeCache.containsKey(fullClassName)) {
// return classTypeCache.get(fullClassName);
//}
TokenType result = defaultType;
try {
Class<?> clazz = Class.forName(fullClassName);
if (clazz.isInterface()) {
result = TokenType.INTERFACE_DECL;
} else if (clazz.isEnum()) {
result = TokenType.ENUM_DECL;
} else {
result = defaultType; // regular class
}
} catch (ClassNotFoundException | NoClassDefFoundError e) {
// Try with $ notation for inner classes (e.g., IOverlay$ColorType)
String innerClassNotation = fullClassName.replaceAll("\\.(\\p{Upper})", "\\$$1");
if (!innerClassNotation.equals(fullClassName)) {
try {
Class<?> clazz = Class.forName(innerClassNotation);
if (clazz.isInterface()) {
result = TokenType.INTERFACE_DECL;
} else if (clazz.isEnum()) {
result = TokenType.ENUM_DECL;
if (!simpleName.isEmpty()) {
if (result.found && result.classInfo != null) {
// Store with resolved name (has $ for inner classes)
importedClasses.put(simpleName, result.classInfo.resolvedName);
} else {
result = defaultType; // regular class
// Store original path for unresolved imports
importedClasses.put(simpleName, fullPath);
}
// Cache with original notation
classTypeCache.put(fullClassName, result);
return result;
} catch (Exception e2) {
// Still not found
}
}
// Class not found - mark as undefined
result = TokenType.UNDEFINED_VAR;
} catch (LinkageError e) {
// Linkage errors (other than NoClassDefFoundError) - treat as undefined
result = TokenType.UNDEFINED_VAR;
}
// Cache the result
classTypeCache.put(fullClassName, result);
return result;
}
/**
* Parse and highlight generic type parameters recursively (e.g., "List<String>" or "Map<String, List<List<Integer>>>")
* Handles arbitrarily nested generics by tracking bracket depth.
* Parse and highlight generic type parameters using ClassPathFinder.
* Handles arbitrarily nested generics like "Map<String, List<Map<String, String>>>".
*/
private void highlightGenericTypes(String genericContent, int contentStart, List<Mark> marks, List<int[]> excluded) {
if (genericContent == null || genericContent.isEmpty()) return;
int i = 0;
while (i < genericContent.length()) {
char c = genericContent.charAt(i);
List<ClassPathFinder.TypeOccurrence> occurrences = classPathFinder.parseGenericTypes(genericContent, importedClasses);
for (ClassPathFinder.TypeOccurrence occ : occurrences) {
int absStart = contentStart + occ.startOffset;
int absEnd = contentStart + occ.endOffset;
// Check if we're starting a type name (uppercase letter)
if (Character.isJavaIdentifierStart(c) && Character.isUpperCase(c)) {
int start = i;
while (i < genericContent.length() && Character.isJavaIdentifierPart(genericContent.charAt(i))) {
i++;
if (!isInExcludedRange(absStart, excluded)) {
TokenType tokenType;
switch (occ.type) {
case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break;
case ENUM: tokenType = TokenType.ENUM_DECL; break;
default: tokenType = TokenType.IMPORTED_CLASS; break;
}
String typeName = genericContent.substring(start, i);
int absStart = contentStart + start;
int absEnd = contentStart + i;
if (!isInExcludedRange(absStart, excluded)) {
// Resolve full class name
String fullClassName = importedClasses.get(typeName);
if (fullClassName == null && JAVA_LANG_CLASSES.contains(typeName)) {
fullClassName = "java.lang." + typeName;
}
TokenType tokenType = getClassTokenType(fullClassName, TokenType.IMPORTED_CLASS);
marks.add(new Mark(absStart, absEnd, tokenType));
}
// Check for nested generics after this type
while (i < genericContent.length() && Character.isWhitespace(genericContent.charAt(i))) i++;
if (i < genericContent.length() && genericContent.charAt(i) == '<') {
// Find matching >
int depth = 1;
int nestedStart = i + 1;
i++;
while (i < genericContent.length() && depth > 0) {
if (genericContent.charAt(i) == '<') depth++;
else if (genericContent.charAt(i) == '>') depth--;
i++;
}
// Recursively process nested content
String nestedContent = genericContent.substring(nestedStart, i - 1);
highlightGenericTypes(nestedContent, contentStart + nestedStart, marks, excluded);
// Continue after the closing > to find more types
}
// Continue to next character (handles commas, spaces, etc.)
} else {
i++;
marks.add(new Mark(absStart, absEnd, tokenType));
}
}
}
@@ -604,7 +407,6 @@ public class JavaTextContainer extends TextContainer {
private void collectImportStatements(List<Mark> marks) {
List<int[]> excluded = MethodBlock.getExcludedRanges(text);
// Pattern to match: import [static] package.path.ClassName [.*];
Matcher m = IMPORT.matcher(text);
while (m.find()) {
if (isInExcludedRange(m.start(), excluded))
@@ -612,108 +414,95 @@ public class JavaTextContainer extends TextContainer {
String fullPath = m.group(1).trim();
int pathStart = m.start(1);
int pathEnd = m.end(1);
String matchText = m.group(0);
// Highlight the 'import' keyword itself
int importKwStart = m.start();
int importKwEnd = Math.min(importKwStart + 6, text.length()); // "import"
int importKwEnd = Math.min(importKwStart + 6, text.length());
marks.add(new Mark(importKwStart, importKwEnd, TokenType.IMPORT_KEYWORD));
// Resolve import path to an actual class if possible
ResolvedImport ri = resolveImportPath(fullPath);
String[] segments = fullPath.split("\\.");
// Use ClassPathFinder to resolve the import
ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath);
// Compute package portion length
int pkgSegments = 0;
if (ri.found && ri.resolvedClass != null) {
pkgSegments = Math.max(0, ri.classStartIndex);
// register resolved package prefix
if (pkgSegments > 0) {
StringBuilder pb = new StringBuilder();
for (int i = 0; i < pkgSegments; i++) {
if (i > 0) pb.append('.');
pb.append(segments[i]);
}
resolvedPackages.add(pb.toString());
String packagePortion = result.packagePortion;
String classPortion = result.classPortion;
boolean hasPackage = packagePortion != null && !packagePortion.isEmpty();
boolean hasClass = classPortion != null && !classPortion.isEmpty();
if (result.found && result.invalidStartOffset < 0) {
// 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));
}
} else {
// Try to find the longest previously-resolved package prefix, or known package prefix
for (int p = segments.length - 1; p >= 1; p--) {
StringBuilder test = new StringBuilder();
for (int i = 0; i < p; i++) {
if (i > 0) test.append('.');
test.append(segments[i]);
}
String testStr = test.toString();
if (resolvedPackages.contains(testStr)) {
pkgSegments = p;
break;
}
if (knownPackages.contains(testStr)) {
pkgSegments = p;
// don't break; prefer a resolved package if found later in loop
}
}
// fallback to ri.classStartIndex if nothing known
if (pkgSegments == 0) pkgSegments = Math.max(0, ri.classStartIndex);
}
StringBuilder pkgBuilder = new StringBuilder();
for (int i = 0; i < pkgSegments; i++) {
if (i > 0) pkgBuilder.append('.');
pkgBuilder.append(segments[i]);
}
int pkgLen = pkgBuilder.length();
if (pkgLen > 0) {
marks.add(new Mark(pathStart, pathStart + pkgLen + 1, TokenType.TYPE_DECL)); // package + dot
}
// Highlight class and inner-class segments
int classStart = pathStart + (pkgLen > 0 ? pkgLen + 1 : 0);
if (ri.found && ri.resolvedClass != null) {
// Use resolved class (with $). Break into segments for progressive checks.
for (int i = ri.classStartIndex; i < segments.length; i++) {
int segStart = classStart;
int segEnd = classStart + segments[i].length();
// Build candidate full name up to this segment using $ for inner separators
StringBuilder testBuilder = new StringBuilder();
if (ri.classStartIndex > 0) {
for (int p = 0; p < ri.classStartIndex; p++) {
if (p > 0) testBuilder.append('.');
testBuilder.append(segments[p]);
if (hasClass && !result.classSegments.isEmpty()) {
int classStart = pathStart + (hasPackage ? packagePortion.length() + 1 : 0);
for (ClassPathFinder.ClassSegment seg : result.classSegments) {
int segEnd = classStart + seg.name.length();
TokenType segType;
switch (seg.type) {
case INTERFACE: segType = TokenType.INTERFACE_DECL; break;
case ENUM: segType = TokenType.ENUM_DECL; break;
default: segType = TokenType.IMPORTED_CLASS; break;
}
testBuilder.append('.');
marks.add(new Mark(classStart, segEnd, segType));
classStart = segEnd + 1;
}
testBuilder.append(segments[ri.classStartIndex]);
for (int q = ri.classStartIndex + 1; q <= i; q++) {
testBuilder.append('$').append(segments[q]);
}
} else if (result.classSegments != null && !result.classSegments.isEmpty()) {
// 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
marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL));
}
int classStart = pathStart + (hasPackage ? packagePortion.length() + 1 : 0);
for (ClassPathFinder.ClassSegment seg : result.classSegments) {
int segEnd = classStart + seg.name.length();
TokenType segType;
switch (seg.type) {
case INTERFACE: segType = TokenType.INTERFACE_DECL; break;
case ENUM: segType = TokenType.ENUM_DECL; break;
default: segType = TokenType.IMPORTED_CLASS; break;
}
String testFull = testBuilder.toString();
TokenType segType = getClassTokenType(testFull, TokenType.IMPORTED_CLASS);
marks.add(new Mark(segStart, segEnd, segType));
// Update importedClasses map for simple name resolution
if (i == segments.length - 1) {
importedClasses.put(segments[i], ri.resolvedClass);
marks.add(new Mark(classStart, segEnd, segType));
classStart = segEnd + 1;
}
// Mark remaining invalid portion in red
if (result.invalidStartOffset >= 0) {
int invalidStart = pathStart + result.invalidStartOffset;
if (invalidStart < pathEnd) {
marks.add(new Mark(invalidStart, pathEnd, TokenType.UNDEFINED_VAR));
}
classStart = segEnd + 1; // +1 for dot
}
} else {
// Unresolved import: mark the entire tail (after known package prefix) as undefined
int undefStart = classStart;
int undefEnd = pathEnd;
if (undefStart < undefEnd) {
marks.add(new Mark(undefStart, undefEnd, TokenType.UNDEFINED_VAR));
}
// Remove any previously-added simple-name mapping to avoid treating it as imported
String simpleName = segments[segments.length - 1];
if (importedClasses.containsKey(simpleName)) {
importedClasses.remove(simpleName);
// Not resolved at all - use invalidStartOffset to determine valid/invalid split
if (result.invalidStartOffset >= 0 && hasPackage) {
// Valid package prefix, invalid class
int pkgEnd = pathStart + packagePortion.length();
marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL));
int invalidStart = pathStart + result.invalidStartOffset;
if (invalidStart < pathEnd) {
marks.add(new Mark(invalidStart, pathEnd, TokenType.UNDEFINED_VAR));
}
} else if (fullPath.endsWith(".") && hasPackage) {
// Trailing dot on package - all blue
marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL));
} else if (result.invalidStartOffset < 0 && hasPackage && !hasClass) {
// All-lowercase path treated as valid package
marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL));
} else {
// Entire path is invalid
marks.add(new Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR));
}
}
// Highlight .* if present (find '*' between match bounds)
String matchText = text.substring(m.start(), m.end());
// Highlight .* if present
int starIndex = matchText.indexOf('*');
if (starIndex != -1) {
int absStar = m.start() + starIndex;
@@ -735,13 +524,9 @@ public class JavaTextContainer extends TextContainer {
continue;
// Check if this is an imported class, wildcard match, or java.lang class
boolean isImported = importedClasses.containsKey(className) || JAVA_LANG_CLASSES.contains(className);
if (!isImported) {
// Check wildcard imports (heuristic: if any imported package exists, mark it)
for (String pkg : importedPackages) {
isImported = true;
break;
}
boolean isImported = importedClasses.containsKey(className) || ClassPathFinder.JAVA_LANG_CLASSES.contains(className);
if (!isImported && !importedPackages.isEmpty()) {
isImported = true; // Wildcard import present
}
// Also skip if it's a local/global field or parameter (avoid false positives)
@@ -753,12 +538,15 @@ public class JavaTextContainer extends TextContainer {
}
if (isImported && !isShadowed) {
// Determine token type dynamically using reflection
String fullClassName = importedClasses.get(className);
if (fullClassName == null && JAVA_LANG_CLASSES.contains(className)) {
fullClassName = "java.lang." + className;
// Resolve the class to determine its type
ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(className, importedClasses);
TokenType tokenType = TokenType.IMPORTED_CLASS;
if (info != null) {
switch (info.type) {
case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break;
case ENUM: tokenType = TokenType.ENUM_DECL; break;
}
}
TokenType tokenType = getClassTokenType(fullClassName, TokenType.IMPORTED_CLASS);
marks.add(new Mark(start, end, tokenType));
}
}
@@ -869,46 +657,118 @@ public class JavaTextContainer extends TextContainer {
}
private void collectTypeDeclarations(List<Mark> marks) {
// Run TYPE_DECL on each line to avoid matches bleeding across newlines
List<int[]> excluded = MethodBlock.getExcludedRanges(text);
// Pattern to find type declarations: Type<...> variableName or Type variableName
// We'll manually parse the generic portion to handle arbitrary nesting
Pattern typeStart = Pattern.compile(
"(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" +
"([A-Z][a-zA-Z0-9_]*)\\s*" // Group 1 → type name (must start with uppercase)
);
for (LineData ld : lines) {
int lineStart = ld.start;
int lineEnd = ld.end;
if (lineStart >= lineEnd) continue;
String s = ld.text;
Matcher m = TYPE_DECL.matcher(s);
while (m.find()) {
int g1s = lineStart + m.start(1);
int g1e = lineStart + m.end(1);
// Skip if any part of the match overlaps an excluded range (string/comment)
Matcher m = typeStart.matcher(s);
int searchFrom = 0;
while (m.find(searchFrom)) {
int typeNameStart = lineStart + m.start(1);
int typeNameEnd = lineStart + m.end(1);
// Skip if in excluded range
boolean skip = false;
for (int[] r : excluded) {
if (g1s < r[1] && g1e > r[0]) { skip = true; break; }
if (typeNameStart < r[1] && typeNameEnd > r[0]) { skip = true; break; }
}
if (skip) continue;
// Check if this type is a known interface or enum using reflection
if (skip) {
searchFrom = m.end();
continue;
}
String typeName = m.group(1);
String fullClassName = importedClasses.get(typeName);
if (fullClassName == null && JAVA_LANG_CLASSES.contains(typeName)) {
fullClassName = "java.lang." + typeName;
int posAfterType = m.end(1);
// Skip whitespace after type name
while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) {
posAfterType++;
}
TokenType tokenType = getClassTokenType(fullClassName, TokenType.TYPE_DECL);
marks.add(new Mark(g1s, g1e, tokenType));
if (m.group(2) != null) {
int start = lineStart + m.start(2);
int end = lineStart + m.end(2);
// Color < and > as default
marks.add(new Mark(start, start + 1, TokenType.DEFAULT)); // <
marks.add(new Mark(end - 1, end, TokenType.DEFAULT)); // >
// Parse and highlight all types in generic content (handles nested generics)
if (m.group(3) != null) {
String genericContent = m.group(3);
int contentStart = lineStart + m.start(3);
// Check for generic parameters
String genericContent = null;
int genericStart = -1;
int genericEnd = -1;
if (posAfterType < s.length() && s.charAt(posAfterType) == '<') {
genericStart = posAfterType;
int depth = 1;
int i = posAfterType + 1;
while (i < s.length() && depth > 0) {
char c = s.charAt(i);
if (c == '<') depth++;
else if (c == '>') depth--;
i++;
}
if (depth == 0) {
genericEnd = i;
genericContent = s.substring(genericStart + 1, genericEnd - 1);
posAfterType = genericEnd;
}
}
// Skip whitespace after generics
while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) {
posAfterType++;
}
// Check if this looks like a type declaration:
// 1. Has generic content (e.g., List<String>) - always a type
// 2. Followed by a variable name (lowercase start or underscore)
// 3. At end of line with generic content (line might be wrapped)
boolean hasGeneric = genericContent != null && !genericContent.isEmpty();
boolean atEndOfLine = posAfterType >= s.length();
boolean followedByVarName = false;
if (!atEndOfLine) {
char nextChar = s.charAt(posAfterType);
followedByVarName = Character.isLetter(nextChar) || nextChar == '_';
}
// Accept as type if: has generics OR followed by variable name
// This handles both `List<String> myList` and wrapped lines like `List<String>`
if (hasGeneric || followedByVarName) {
// This looks like a type declaration
// Resolve the main type
ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(typeName, importedClasses);
TokenType tokenType = TokenType.TYPE_DECL;
if (info != null) {
switch (info.type) {
case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break;
case ENUM: tokenType = TokenType.ENUM_DECL; break;
}
}
marks.add(new Mark(typeNameStart, typeNameEnd, tokenType));
// Handle generic content
if (hasGeneric && genericStart >= 0) {
int absGenericStart = lineStart + genericStart;
int absGenericEnd = lineStart + genericEnd;
// Mark < and > as default
marks.add(new Mark(absGenericStart, absGenericStart + 1, TokenType.DEFAULT));
marks.add(new Mark(absGenericEnd - 1, absGenericEnd, TokenType.DEFAULT));
// Parse and highlight all types in generic content
int contentStart = lineStart + genericStart + 1;
highlightGenericTypes(genericContent, contentStart, marks, excluded);
}
}
searchFrom = m.end();
}
}
}