diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index 326df3b79..4ceb33a90 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -62,24 +62,27 @@ public class ClassPathFinder { public final String classPortion; // The class part (e.g., "List" or "IOverlay.ColorType") public final List 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) + public final int packageSegmentCount; // Number of dot-separated identifier tokens that belong to the package portion private ResolveResult(boolean found, ClassInfo classInfo, String packagePortion, - String classPortion, List classSegments, int invalidStartOffset) { + String classPortion, List classSegments, int invalidStartOffset, + int packageSegmentCount) { this.found = found; this.classInfo = classInfo; this.packagePortion = packagePortion; this.classPortion = classPortion; this.classSegments = classSegments != null ? classSegments : Collections.emptyList(); this.invalidStartOffset = invalidStartOffset; + this.packageSegmentCount = packageSegmentCount; } - public static ResolveResult notFound(String packagePortion, String classPortion, int invalidStartOffset) { - return new ResolveResult(false, null, packagePortion, classPortion, null, invalidStartOffset); + public static ResolveResult notFound(String packagePortion, String classPortion, int invalidStartOffset, int packageSegmentCount) { + return new ResolveResult(false, null, packagePortion, classPortion, null, invalidStartOffset, packageSegmentCount); } public static ResolveResult found(ClassInfo info, String packagePortion, - String classPortion, List segments) { - return new ResolveResult(true, info, packagePortion, classPortion, segments, -1); + String classPortion, List segments, int packageSegmentCount) { + return new ResolveResult(true, info, packagePortion, classPortion, segments, -1, packageSegmentCount); } } @@ -113,15 +116,20 @@ public class ClassPathFinder { */ public ResolveResult resolve(String importPath) { if (importPath == null || importPath.isEmpty()) { - return ResolveResult.notFound("", "", -1); + return ResolveResult.notFound("", "", -1, 0); } + // Normalize whitespace/newlines around dots so wrapped imports like + // "kamkeel.npcdbc.api.client.overlay.IOverlay.\n TextureFunction" become + // "kamkeel.npcdbc.api.client.overlay.IOverlay.TextureFunction" before resolving. + String normalizedPath = importPath.replaceAll("\\s*\\.\\s*", ".").trim(); + // 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; + boolean trailingDot = normalizedPath.endsWith("."); + String pathToResolve = trailingDot ? normalizedPath.substring(0, normalizedPath.length() - 1) : normalizedPath; if (pathToResolve.isEmpty()) { - return ResolveResult.notFound("", "", -1); + return ResolveResult.notFound("", "", -1, 0); } String[] segments = pathToResolve.split("\\."); @@ -136,18 +144,18 @@ public class ClassPathFinder { int longestValid = findLongestValidPackagePrefixIndex(segments); if (longestValid == n) { // Entire path is a valid package (or cached as valid) - return ResolveResult.notFound(pathToResolve, "", -1); + return ResolveResult.notFound(pathToResolve, "", -1, n); } if (longestValid > 0) { String validPrefix = buildPackagePortion(segments, longestValid); String invalidRemainder = buildClassPortion(segments, longestValid, n); int invalidOffset = validPrefix.length() + 1; // after the dot - return ResolveResult.notFound(validPrefix, invalidRemainder, invalidOffset); + return ResolveResult.notFound(validPrefix, invalidRemainder, invalidOffset, longestValid); } // Nothing validated - return ResolveResult.notFound("", pathToResolve, 0); + return ResolveResult.notFound("", pathToResolve, 0, 0); } // Trailing dot is handled above for lowercase-only paths; for mixed-case paths @@ -180,15 +188,15 @@ public class ClassPathFinder { // 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); + fullClassPortion, classSegments, validEndOffset, classStartIdx); } // Full resolution successful List classSegments = buildClassSegments(segments, classStartIdx, n); - return ResolveResult.found( + return ResolveResult.found( new ClassInfo(info.resolvedName, info.type, packagePortion.length()), - packagePortion, validClassPortion, classSegments - ); + packagePortion, validClassPortion, classSegments, classStartIdx + ); } } } @@ -208,22 +216,22 @@ public class ClassPathFinder { // 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); + return ResolveResult.notFound(validPrefix, buildClassPortion(segments, typoIndex, n), invalidOffset, typoIndex); } // 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); + return ResolveResult.notFound(packagePortion, classPortion, invalidOffset, firstUpperIndex); } 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); + return ResolveResult.notFound("", pathToResolve, 0, 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); + return ResolveResult.notFound(pathToResolve, "", -1, n); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java index fd6da00f0..a978dc724 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java @@ -171,7 +171,7 @@ public class IndentHelper { settings.wrapComments = true; // Use 75% of viewport width as max line length for some margin int charWidth = 6; // Approximate character width - settings.maxLineLength = Math.max(60, (int)(viewportWidth * 0.75 / charWidth)); + settings.maxLineLength = Math.max(60, (int)(viewportWidth * 0.8f / charWidth)); } String formatted = helper.format(indented); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 8c7489b2d..d31743a5b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -37,9 +37,11 @@ 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 [.*]; (semicolon optional for live typing) - public static final Pattern IMPORT = Pattern.compile( - "(?m)\\bimport\\s+(?:static\\s+)?([a-zA-Z_][a-zA-Z0-9_.]*?)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)"); + // Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing) + // Allows optional whitespace (including newlines) around the dots so wrapped imports match. + // Match imports while allowing optional whitespace/newlines around dots. + public static final Pattern IMPORT = Pattern.compile( + "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)"); // ClassPathFinder for resolving imports and determining class types private final ClassPathFinder classPathFinder = new ClassPathFinder(); @@ -51,6 +53,26 @@ public class JavaTextContainer extends TextContainer { super(text); } + // Map an offset into the normalized path (tokens joined with '.') back to + // an absolute index in the original substring `orig` (absolute positions start at pathStart). + // The normalization removes whitespace/newlines around dots; this function + // advances a normalized counter when it encounters identifier chars or dots + // and returns the corresponding original index (absolute). + private int mapNormalizedOffsetToOriginal(String orig, int normalizedOffset, int pathStart) { + if (orig == null || orig.isEmpty()) return pathStart; + int norm = 0; + for (int i = 0; i < orig.length(); i++) { + char c = orig.charAt(i); + if (Character.isLetterOrDigit(c) || c == '_' || c == '.') { + norm++; + } + if (norm >= normalizedOffset) { + return pathStart + i; + } + } + return pathStart + orig.length(); + } + public void init(String text,int width, int height) { this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); lines.clear(); @@ -519,75 +541,94 @@ public class JavaTextContainer extends TextContainer { // Use ClassPathFinder to resolve the import ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath); - String packagePortion = result.packagePortion; - String classPortion = result.classPortion; - boolean hasPackage = packagePortion != null && !packagePortion.isEmpty(); - boolean hasClass = classPortion != null && !classPortion.isEmpty(); + // Tokenize the original matched substring so we can map normalized segments + String orig = text.substring(pathStart, pathEnd); + java.util.List tokens = new java.util.ArrayList<>(); + java.util.List tokenStarts = new java.util.ArrayList<>(); + java.util.List tokenEnds = new java.util.ArrayList<>(); + Matcher idm = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*").matcher(orig); + while (idm.find()) { + tokens.add(idm.group()); + tokenStarts.add(idm.start()); + tokenEnds.add(idm.end()); + } + int pkgSegments = result.packageSegmentCount; + boolean hasPackage = pkgSegments > 0; + + // Highlight package tokens (if any) if (result.found && result.invalidStartOffset < 0) { - // Fully resolved - highlight package (blue) and class segments (typed) - if (hasPackage) { - int pkgEnd = pathStart + packagePortion.length(); + if (hasPackage && pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); } - - 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(); + + // Highlight class segments + if (result.classSegments != null && !result.classSegments.isEmpty()) { + int classTokenStart = hasPackage ? pkgSegments : 0; + for (int si = 0; si < result.classSegments.size(); si++) { + int tokIdx = classTokenStart + si; + if (tokIdx >= 0 && tokIdx < tokenStarts.size()) { + int s = pathStart + tokenStarts.get(tokIdx); + int e = pathStart + tokenEnds.get(tokIdx); + ClassPathFinder.ClassSegment seg = result.classSegments.get(si); + 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; + } + marks.add(new Mark(s, e, segType)); + } + } + } + } else if (result.classSegments != null && !result.classSegments.isEmpty()) { + // Partial resolution - mark package and valid class segments + if (hasPackage && pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + } + + int classTokenStart = hasPackage ? pkgSegments : 0; + for (int si = 0; si < result.classSegments.size(); si++) { + int tokIdx = classTokenStart + si; + if (tokIdx >= 0 && tokIdx < tokenStarts.size()) { + int s = pathStart + tokenStarts.get(tokIdx); + int e = pathStart + tokenEnds.get(tokIdx); + ClassPathFinder.ClassSegment seg = result.classSegments.get(si); 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; } - marks.add(new Mark(classStart, segEnd, segType)); - classStart = segEnd + 1; + marks.add(new Mark(s, e, segType)); } } - } 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(); - 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; - } - marks.add(new Mark(classStart, segEnd, segType)); - classStart = segEnd + 1; - } - - // Mark remaining invalid portion in red + + // Mark remaining invalid portion in red (map normalized offset to original) if (result.invalidStartOffset >= 0) { - int invalidStart = pathStart + result.invalidStartOffset; - if (invalidStart < pathEnd) { - marks.add(new Mark(invalidStart, pathEnd, TokenType.UNDEFINED_VAR)); + int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); + if (invalidAbs < pathEnd) { + marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); } } } else { // 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)); + if (pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); } - } else if (fullPath.endsWith(".") && hasPackage && result.invalidStartOffset < 0 && !hasClass) { + + int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); + if (invalidAbs < pathEnd) { + marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); + } + } else if (fullPath.endsWith(".") && hasPackage && result.invalidStartOffset < 0) { // Trailing dot on a fully-valid package - all blue marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); - } else if (result.invalidStartOffset < 0 && hasPackage && !hasClass) { + } else if (result.invalidStartOffset < 0 && hasPackage) { // All-lowercase path treated as valid package marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); } else {