mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 20:44:08 +00:00
imports functional state? (not really)
This commit is contained in:
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user