diff --git a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java new file mode 100644 index 000000000..675a6b097 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java @@ -0,0 +1,1124 @@ +package noppes.npcs.client.gui.util.script; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Configurable code formatter with IntelliJ-like settings. + * + * Features: + * - Operator spacing (a+b → a + b) + * - Whitespace normalization (a b → a b) + * - Long line wrapping + * - Bracket spacing configuration + * - Preserves string content and comments + */ +public class FormatHelper { + + // ==================== SETTINGS ==================== + + /** + * Formatting configuration - similar to IntelliJ Code Style settings + */ + public static class FormatSettings { + // Spaces around operators + public boolean spaceAroundAssignment = true; // a = b + public boolean spaceAroundArithmetic = true; // a + b - c * d / e + public boolean spaceAroundComparison = true; // a == b, a != b, a <= b + public boolean spaceAroundLogical = true; // a && b || c + public boolean spaceAroundBitwise = true; // a & b | c ^ d + + // Spaces within brackets + public boolean spaceWithinParens = false; // (a) vs ( a ) + public boolean spaceWithinBrackets = false; // [a] vs [ a ] + public boolean spaceWithinBraces = true; // {a} vs { a } (for initializers) + + // Spaces before/after + public boolean spaceAfterComma = true; // a, b vs a,b + public boolean spaceBeforeComma = false; // a, b vs a , b + public boolean spaceAfterSemicolon = true; // for (a; b; c) vs for (a;b;c) + public boolean spaceBeforeSemicolon = false; + public boolean spaceAfterColon = true; // case a: b vs case a:b + public boolean spaceBeforeColon = false; + + // Method calls + public boolean spaceBeforeMethodParens = false; // foo() vs foo () + public boolean spaceAfterTypeCast = true; // (int) x vs (int)x + + // Control statements + public boolean spaceBeforeIfParens = true; // if (...) vs if(...) + public boolean spaceBeforeWhileParens = true; + public boolean spaceBeforeForParens = true; + public boolean spaceBeforeSwitchParens = true; + public boolean spaceBeforeCatchParens = true; + + // Braces + public boolean spaceBeforeOpenBrace = true; // if () { vs if (){ + + // Line length + public int maxLineLength = 120; + public boolean wrapLongLines = false; // Auto-wrap lines longer than maxLineLength + public int wrapIndentSpaces = 4; // Extra indent for wrapped lines + public boolean wrapComments = true; // Also wrap long comments + + // Wrap break preferences (where to break long lines) + public boolean wrapAfterComma = true; // Prefer breaking after commas + public boolean wrapAfterOperator = true; // Prefer breaking after operators + public boolean wrapBeforeDot = true; // Break before . for chained calls + public boolean wrapMethodArguments = true; // Each arg on new line when wrapping method calls + + // Whitespace normalization + public boolean normalizeWhitespace = true; // Multiple spaces → single space + public boolean trimTrailingWhitespace = true; + + // Unary operators + public boolean spaceAroundUnaryNot = false; // !a vs ! a + public boolean spaceAroundUnaryIncDec = false; // ++i vs ++ i + + /** + * Create default settings (similar to IntelliJ defaults) + */ + public static FormatSettings defaults() { + return new FormatSettings(); + } + + /** + * Create compact settings (minimal spacing) + */ + public static FormatSettings compact() { + FormatSettings s = new FormatSettings(); + s.spaceWithinParens = false; + s.spaceWithinBrackets = false; + s.spaceWithinBraces = false; + s.wrapLongLines = false; + return s; + } + } + + private FormatSettings settings; + + public FormatHelper() { + this.settings = FormatSettings.defaults(); + } + + public FormatHelper(FormatSettings settings) { + this.settings = settings != null ? settings : FormatSettings.defaults(); + } + + public void setSettings(FormatSettings settings) { + this.settings = settings; + } + + public FormatSettings getSettings() { + return settings; + } + + // ==================== MAIN FORMAT METHOD ==================== + + /** + * Format the entire text with all configured options + */ + public String format(String text) { + if (text == null || text.isEmpty()) return text; + + // Process line by line to preserve structure + String[] lines = text.split("\n", -1); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < lines.length; i++) { + String line = formatLine(lines[i], text, getLineStartOffset(lines, i)); + + if (settings.trimTrailingWhitespace) { + line = trimTrailing(line); + } + + result.append(line); + if (i < lines.length - 1) { + result.append("\n"); + } + } + + return result.toString(); + } + + private int getLineStartOffset(String[] lines, int lineIndex) { + int offset = 0; + for (int i = 0; i < lineIndex; i++) { + offset += lines[i].length() + 1; // +1 for \n + } + return offset; + } + + /** + * Format a single line of code + */ + private String formatLine(String line, String fullText, int lineOffset) { + if (line.trim().isEmpty()) return line; + + // Preserve leading indentation + int indent = 0; + while (indent < line.length() && (line.charAt(indent) == ' ' || line.charAt(indent) == '\t')) { + indent++; + } + String indentation = line.substring(0, indent); + String content = line.substring(indent); + + if (content.isEmpty()) return line; + + // Skip formatting for lines that are entirely comments + String trimmed = content.trim(); + if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) { + return line; + } + + // Format the content + String formatted = formatContent(content, fullText, lineOffset + indent); + + return indentation + formatted; + } + + /** + * Format line content, preserving strings and comments + */ + private String formatContent(String content, String fullText, int offset) { + // Find string/comment regions to preserve + List preserveRegions = findPreserveRegions(content); + + if (preserveRegions.isEmpty()) { + // No strings/comments, format everything + return formatCodeSegment(content); + } + + // Build result by formatting non-preserved regions + StringBuilder result = new StringBuilder(); + int lastEnd = 0; + + for (int[] region : preserveRegions) { + // Format code before this preserved region + if (region[0] > lastEnd) { + String codeSegment = content.substring(lastEnd, region[0]); + result.append(formatCodeSegment(codeSegment)); + } + + // Append preserved region unchanged + result.append(content.substring(region[0], region[1])); + lastEnd = region[1]; + } + + // Format remaining code after last preserved region + if (lastEnd < content.length()) { + result.append(formatCodeSegment(content.substring(lastEnd))); + } + + return result.toString(); + } + + /** + * Find string literals and comments that should not be formatted + */ + private List findPreserveRegions(String content) { + List regions = new ArrayList<>(); + + int i = 0; + while (i < content.length()) { + char c = content.charAt(i); + + // String literal + if (c == '"') { + int end = findStringEnd(content, i, '"'); + regions.add(new int[]{i, end}); + i = end; + continue; + } + + // Char literal + if (c == '\'') { + int end = findStringEnd(content, i, '\''); + regions.add(new int[]{i, end}); + i = end; + continue; + } + + // Line comment + if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '/') { + regions.add(new int[]{i, content.length()}); + break; + } + + // Block comment (could span multiple lines but we process line-by-line) + if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '*') { + int end = content.indexOf("*/", i + 2); + if (end >= 0) { + regions.add(new int[]{i, end + 2}); + i = end + 2; + } else { + regions.add(new int[]{i, content.length()}); + break; + } + continue; + } + + i++; + } + + return regions; + } + + /** + * Find end of string/char literal + */ + private int findStringEnd(String content, int start, char quote) { + int i = start + 1; + while (i < content.length()) { + char c = content.charAt(i); + if (c == '\\' && i + 1 < content.length()) { + i += 2; // Skip escape sequence + continue; + } + if (c == quote) { + return i + 1; + } + i++; + } + return content.length(); + } + + // ==================== CODE SEGMENT FORMATTING ==================== + + /** + * Format a code segment (no strings/comments) + */ + private String formatCodeSegment(String code) { + if (code.isEmpty()) return code; + + String result = code; + + // Normalize whitespace first + if (settings.normalizeWhitespace) { + result = normalizeWhitespace(result); + } + + // Apply operator spacing + result = formatOperators(result); + + // Apply bracket spacing + result = formatBrackets(result); + + // Apply keyword spacing + result = formatKeywords(result); + + // Apply punctuation spacing + result = formatPunctuation(result); + + return result; + } + + /** + * Normalize whitespace (multiple spaces → single space) + */ + private String normalizeWhitespace(String code) { + // Replace multiple spaces with single space, but preserve indentation + return code.replaceAll(" +", " "); + } + + /** + * Format operator spacing + */ + private String formatOperators(String code) { + String result = code; + + // Assignment operators + if (settings.spaceAroundAssignment) { + // Compound assignments first (longer patterns first) + result = formatBinaryOp(result, "+=", " += "); + result = formatBinaryOp(result, "-=", " -= "); + result = formatBinaryOp(result, "*=", " *= "); + result = formatBinaryOp(result, "/=", " /= "); + result = formatBinaryOp(result, "%=", " %= "); + result = formatBinaryOp(result, "&=", " &= "); + result = formatBinaryOp(result, "|=", " |= "); + result = formatBinaryOp(result, "^=", " ^= "); + result = formatBinaryOp(result, "<<=", " <<= "); + result = formatBinaryOp(result, ">>=", " >>= "); + result = formatBinaryOp(result, ">>>=", " >>>= "); + + // Simple assignment (but not ==, !=, <=, >=) + result = formatSimpleAssignment(result); + } + + // Comparison operators + if (settings.spaceAroundComparison) { + result = formatBinaryOp(result, "==", " == "); + result = formatBinaryOp(result, "!=", " != "); + result = formatBinaryOp(result, "<=", " <= "); + result = formatBinaryOp(result, ">=", " >= "); + // Simple < > (but not generics) + result = formatComparisonOperators(result); + } + + // Logical operators + if (settings.spaceAroundLogical) { + result = formatBinaryOp(result, "&&", " && "); + result = formatBinaryOp(result, "||", " || "); + } + + // Arithmetic operators + if (settings.spaceAroundArithmetic) { + // Handle ++ and -- first to avoid conflicts + result = formatArithmeticOperators(result); + } + + // Bitwise operators (but not in generics) + if (settings.spaceAroundBitwise) { + result = formatBitwiseOperators(result); + } + + return result; + } + + /** + * Format a binary operator with proper spacing + */ + private String formatBinaryOp(String code, String op, String replacement) { + // First normalize: remove extra spaces around the operator + String pattern = "\\s*" + Pattern.quote(op) + "\\s*"; + return code.replaceAll(pattern, replacement); + } + + /** + * Format simple assignment = (not ==, !=, <=, >=, etc.) + */ + private String formatSimpleAssignment(String code) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < code.length()) { + char c = code.charAt(i); + if (c == '=') { + // Check it's not part of ==, !=, <=, >=, +=, etc. + char prev = i > 0 ? code.charAt(i - 1) : ' '; + char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; + + boolean isCompound = prev == '!' || prev == '<' || prev == '>' || + prev == '+' || prev == '-' || prev == '*' || + prev == '/' || prev == '%' || prev == '&' || + prev == '|' || prev == '^'; + boolean isEquality = next == '='; + + if (!isCompound && !isEquality) { + // Ensure space before = + if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { + result.append(' '); + } + result.append('='); + // Ensure space after = + if (next != ' ') { + result.append(' '); + } + } else { + result.append(c); + } + } else { + result.append(c); + } + i++; + } + return result.toString(); + } + + /** + * Format < and > comparison operators (avoiding generics) + */ + private String formatComparisonOperators(String code) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < code.length()) { + char c = code.charAt(i); + + if ((c == '<' || c == '>') && !isLikelyGeneric(code, i, c)) { + char prev = i > 0 ? code.charAt(i - 1) : ' '; + char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; + + // Skip if part of <<, >>, <=, >= + if ((c == '<' && next == '<') || (c == '>' && next == '>') || + next == '=' || prev == '<' || prev == '>') { + result.append(c); + } else { + // Add spacing + if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { + result.append(' '); + } + result.append(c); + if (next != ' ' && next != '=') { + result.append(' '); + } + } + } else { + result.append(c); + } + i++; + } + return result.toString(); + } + + /** + * Check if < or > is likely part of a generic type declaration + */ + private boolean isLikelyGeneric(String code, int pos, char bracket) { + if (bracket == '<') { + // Check for pattern like List<, Map<, etc. + if (pos > 0) { + int start = pos - 1; + while (start > 0 && Character.isLetterOrDigit(code.charAt(start - 1))) { + start--; + } + String before = code.substring(start, pos); + // Common generic types or capital letter start (type name) + if (!before.isEmpty() && Character.isUpperCase(before.charAt(0))) { + return true; + } + } + // Check if there's a matching > + int depth = 1; + for (int i = pos + 1; i < code.length() && depth > 0; i++) { + char c = code.charAt(i); + if (c == '<') depth++; + else if (c == '>') depth--; + else if (c == ';' || c == '{' || c == '}') break; // Not a generic + } + return depth == 0; + } else { // '>' + // Check if there's a matching < before + int depth = 1; + for (int i = pos - 1; i >= 0 && depth > 0; i--) { + char c = code.charAt(i); + if (c == '>') depth++; + else if (c == '<') depth--; + else if (c == ';' || c == '{' || c == '}') break; + } + return depth == 0; + } + } + + /** + * Format arithmetic operators (+, -, *, /, %) + */ + private String formatArithmeticOperators(String code) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < code.length()) { + char c = code.charAt(i); + char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; + char prev = i > 0 ? code.charAt(i - 1) : ' '; + + boolean isArithOp = c == '+' || c == '-' || c == '*' || c == '/' || c == '%'; + + if (isArithOp) { + // Skip ++, --, +=, -=, *=, /=, %=, and unary +/- + if ((c == '+' && next == '+') || (c == '-' && next == '-') || + (c == '+' && prev == '+') || (c == '-' && prev == '-') || + next == '=') { + result.append(c); + } else if (isUnaryContext(code, i)) { + // Unary + or - (after operator, open paren, comma, etc.) + result.append(c); + } else { + // Binary operator - add spacing + if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { + result.append(' '); + } + result.append(c); + if (next != ' ' && next != '=' && !((c == '+' && next == '+') || (c == '-' && next == '-'))) { + result.append(' '); + } + } + } else { + result.append(c); + } + i++; + } + return result.toString(); + } + + /** + * Check if position is in a unary context (operator is unary, not binary) + */ + private boolean isUnaryContext(String code, int pos) { + if (pos == 0) return true; + + // Look back for context + int i = pos - 1; + while (i >= 0 && code.charAt(i) == ' ') { + i--; + } + + if (i < 0) return true; + + char prev = code.charAt(i); + // Unary context after: ( [ { , ; = + - * / % < > & | ^ ? : + return prev == '(' || prev == '[' || prev == '{' || prev == ',' || + prev == ';' || prev == '=' || prev == '+' || prev == '-' || + prev == '*' || prev == '/' || prev == '%' || prev == '<' || + prev == '>' || prev == '&' || prev == '|' || prev == '^' || + prev == '?' || prev == ':' || prev == '!'; + } + + /** + * Format bitwise operators + */ + private String formatBitwiseOperators(String code) { + String result = code; + + // Shift operators first + result = formatBinaryOp(result, ">>>", " >>> "); + result = formatBinaryOp(result, "<<", " << "); + result = formatBinaryOp(result, ">>", " >> "); + + // Single & | ^ (not && ||) + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < result.length()) { + char c = result.charAt(i); + char next = i + 1 < result.length() ? result.charAt(i + 1) : ' '; + char prev = i > 0 ? result.charAt(i - 1) : ' '; + + if ((c == '&' && next != '&' && prev != '&' && next != '=') || + (c == '|' && next != '|' && prev != '|' && next != '=') || + (c == '^' && next != '=')) { + if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') { + sb.append(' '); + } + sb.append(c); + if (next != ' ') { + sb.append(' '); + } + } else { + sb.append(c); + } + i++; + } + + return sb.toString(); + } + + // ==================== BRACKET FORMATTING ==================== + + /** + * Format bracket spacing + */ + private String formatBrackets(String code) { + String result = code; + + // Parentheses + if (settings.spaceWithinParens) { + result = result.replaceAll("\\(\\s*", "( "); + result = result.replaceAll("\\s*\\)", " )"); + } else { + result = result.replaceAll("\\(\\s+", "("); + result = result.replaceAll("\\s+\\)", ")"); + } + + // Square brackets + if (settings.spaceWithinBrackets) { + result = result.replaceAll("\\[\\s*", "[ "); + result = result.replaceAll("\\s*\\]", " ]"); + } else { + result = result.replaceAll("\\[\\s+", "["); + result = result.replaceAll("\\s+\\]", "]"); + } + + // Curly braces (for array initializers inline) + // Note: standalone braces for blocks are handled by indentation logic + + return result; + } + + // ==================== KEYWORD FORMATTING ==================== + + /** + * Format keyword spacing (if, while, for, etc.) + */ + private String formatKeywords(String code) { + String result = code; + + if (settings.spaceBeforeIfParens) { + result = result.replaceAll("\\bif\\s*\\(", "if ("); + } else { + result = result.replaceAll("\\bif\\s+\\(", "if("); + } + + if (settings.spaceBeforeWhileParens) { + result = result.replaceAll("\\bwhile\\s*\\(", "while ("); + } else { + result = result.replaceAll("\\bwhile\\s+\\(", "while("); + } + + if (settings.spaceBeforeForParens) { + result = result.replaceAll("\\bfor\\s*\\(", "for ("); + } else { + result = result.replaceAll("\\bfor\\s+\\(", "for("); + } + + if (settings.spaceBeforeSwitchParens) { + result = result.replaceAll("\\bswitch\\s*\\(", "switch ("); + } else { + result = result.replaceAll("\\bswitch\\s+\\(", "switch("); + } + + if (settings.spaceBeforeCatchParens) { + result = result.replaceAll("\\bcatch\\s*\\(", "catch ("); + } else { + result = result.replaceAll("\\bcatch\\s+\\(", "catch("); + } + + // Space before opening brace + if (settings.spaceBeforeOpenBrace) { + result = result.replaceAll("\\)\\s*\\{", ") {"); + } else { + result = result.replaceAll("\\)\\s+\\{", "){"); + } + + return result; + } + + // ==================== PUNCTUATION FORMATTING ==================== + + /** + * Format punctuation spacing (commas, semicolons, colons) + */ + private String formatPunctuation(String code) { + String result = code; + + // Commas + if (settings.spaceAfterComma && !settings.spaceBeforeComma) { + result = result.replaceAll("\\s*,\\s*", ", "); + } else if (settings.spaceAfterComma && settings.spaceBeforeComma) { + result = result.replaceAll("\\s*,\\s*", " , "); + } else if (!settings.spaceAfterComma && settings.spaceBeforeComma) { + result = result.replaceAll("\\s*,\\s*", " ,"); + } else { + result = result.replaceAll("\\s*,\\s*", ","); + } + + // Semicolons (but not in for loops - handle more carefully) + // This is tricky because for(;;) should be handled differently + // For now, just handle trailing semicolons + if (settings.trimTrailingWhitespace) { + result = result.replaceAll("\\s+;", ";"); + } + + return result; + } + + /** + * Trim trailing whitespace from a line + */ + private String trimTrailing(String line) { + int end = line.length(); + while (end > 0 && (line.charAt(end - 1) == ' ' || line.charAt(end - 1) == '\t')) { + end--; + } + return line.substring(0, end); + } + + // ==================== STATIC UTILITIES ==================== + + /** + * Format code with default settings + */ + public static String formatCode(String code) { + return new FormatHelper().format(code); + } + + /** + * Format code with custom settings + */ + public static String formatCode(String code, FormatSettings settings) { + return new FormatHelper(settings).format(code); + } + + // ==================== LINE WRAPPING ==================== + + /** + * Wrap long lines according to settings. + * Call this after format() if wrapLongLines is enabled. + * + * @param text The text to wrap + * @param maxWidth Maximum line width (use settings.maxLineLength if 0) + * @return Text with wrapped lines + */ + public String wrapLines(String text, int maxWidth) { + if (text == null || text.isEmpty()) return text; + + int width = maxWidth > 0 ? maxWidth : settings.maxLineLength; + String[] lines = text.split("\n", -1); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + + if (getVisualLength(line) <= width) { + result.append(line); + } else { + // Determine if this is a comment line + String trimmed = line.trim(); + if (trimmed.startsWith("//")) { + result.append(wrapCommentLine(line, width)); + } else if (trimmed.startsWith("/*") || trimmed.startsWith("*")) { + result.append(wrapBlockCommentLine(line, width)); + } else { + result.append(wrapCodeLine(line, width)); + } + } + + if (i < lines.length - 1) { + result.append("\n"); + } + } + + return result.toString(); + } + + /** + * Get visual length of a line (tabs count as 4 spaces) + */ + private int getVisualLength(String line) { + int len = 0; + for (char c : line.toCharArray()) { + if (c == '\t') { + len += 4; + } else { + len++; + } + } + return len; + } + + /** + * Extract leading indentation from a line + */ + private String extractIndent(String line) { + int i = 0; + while (i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t')) { + i++; + } + return line.substring(0, i); + } + + /** + * Wrap a single-line comment (// style) + */ + private String wrapCommentLine(String line, int maxWidth) { + if (!settings.wrapComments) return line; + + String indent = extractIndent(line); + String content = line.substring(indent.length()); + + // Must start with // + if (!content.startsWith("//")) return line; + + String commentContent = content.substring(2).trim(); + String prefix = indent + "// "; + int prefixLen = getVisualLength(prefix); + int contentWidth = maxWidth - prefixLen; + + if (contentWidth <= 10) return line; // Too narrow to wrap + + StringBuilder result = new StringBuilder(); + result.append(indent).append("// "); + + String[] words = commentContent.split("\\s+"); + int lineLen = prefixLen; + + for (int i = 0; i < words.length; i++) { + String word = words[i]; + int wordLen = word.length(); + + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { + // Start new line + result.append("\n").append(prefix); + lineLen = prefixLen; + } + + if (lineLen > prefixLen) { + result.append(" "); + lineLen++; + } + + result.append(word); + lineLen += wordLen; + } + + return result.toString(); + } + + /** + * Wrap a block comment line (* style) + */ + private String wrapBlockCommentLine(String line, int maxWidth) { + if (!settings.wrapComments) return line; + + String indent = extractIndent(line); + String content = line.substring(indent.length()).trim(); + + // Determine prefix for wrapped lines + String prefix; + String textContent; + + if (content.startsWith("/**")) { + prefix = indent + " * "; + textContent = content.substring(3).trim(); + } else if (content.startsWith("/*")) { + prefix = indent + " * "; + textContent = content.substring(2).trim(); + } else if (content.startsWith("*")) { + prefix = indent + " * "; + textContent = content.substring(1).trim(); + } else { + return line; + } + + // Handle closing */ + boolean hasClose = textContent.endsWith("*/"); + if (hasClose) { + textContent = textContent.substring(0, textContent.length() - 2).trim(); + } + + int prefixLen = getVisualLength(prefix); + int contentWidth = maxWidth - prefixLen; + + if (contentWidth <= 10) return line; + + StringBuilder result = new StringBuilder(); + + // First line keeps original prefix + if (content.startsWith("/**")) { + result.append(indent).append("/** "); + } else if (content.startsWith("/*")) { + result.append(indent).append("/* "); + } else { + result.append(indent).append(" * "); + } + + String[] words = textContent.split("\\s+"); + int lineLen = prefixLen; + + for (int i = 0; i < words.length; i++) { + String word = words[i]; + int wordLen = word.length(); + + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { + result.append("\n").append(prefix); + lineLen = prefixLen; + } + + if (lineLen > prefixLen) { + result.append(" "); + lineLen++; + } + + result.append(word); + lineLen += wordLen; + } + + if (hasClose) { + result.append(" */"); + } + + return result.toString(); + } + + /** + * Wrap a code line at appropriate break points + */ + private String wrapCodeLine(String line, int maxWidth) { + String indent = extractIndent(line); + String content = line.substring(indent.length()); + + // Additional indent for wrapped lines + String wrapIndent = indent + spaces(settings.wrapIndentSpaces); + + // Find break points - positions where we can break the line + List breakPoints = findBreakPoints(content); + + if (breakPoints.isEmpty()) { + // No good break points, just return as-is + return line; + } + + StringBuilder result = new StringBuilder(); + result.append(indent); + + int currentLen = getVisualLength(indent); + int lastBreak = 0; + boolean isFirstLine = true; + + for (BreakPoint bp : breakPoints) { + int segmentEnd = bp.position; + String segment = content.substring(lastBreak, segmentEnd); + int segmentLen = getVisualLength(segment); + + // Check if adding this segment would exceed max width + if (currentLen + segmentLen > maxWidth && !isFirstLine) { + // Break here + result.append("\n").append(wrapIndent); + currentLen = getVisualLength(wrapIndent); + isFirstLine = false; + + // Trim leading space from continuation if any + segment = trimLeading(segment); + } else if (currentLen + segmentLen > maxWidth && isFirstLine && lastBreak > 0) { + // Even first segment is too long after indent, break before it + result.append("\n").append(wrapIndent); + currentLen = getVisualLength(wrapIndent); + isFirstLine = false; + } + + result.append(segment); + currentLen += getVisualLength(segment); + lastBreak = segmentEnd; + isFirstLine = false; + } + + // Append remaining content + if (lastBreak < content.length()) { + String remaining = content.substring(lastBreak); + int remLen = getVisualLength(remaining); + + if (currentLen + remLen > maxWidth && !isFirstLine) { + result.append("\n").append(wrapIndent); + remaining = trimLeading(remaining); + } + + result.append(remaining); + } + + return result.toString(); + } + + /** + * Trim leading whitespace (Java 8 compatible) + */ + private String trimLeading(String s) { + int start = 0; + while (start < s.length() && Character.isWhitespace(s.charAt(start))) { + start++; + } + return s.substring(start); + } + + /** + * A potential line break point + */ + private static class BreakPoint { + int position; // Position in string (after this char) + @SuppressWarnings("unused") + int priority; // Lower = better place to break (for future sorting) + + BreakPoint(int pos, int prio) { + this.position = pos; + this.priority = prio; + } + } + + /** + * Find good positions to break a line + */ + private List findBreakPoints(String content) { + List points = new ArrayList<>(); + + int parenDepth = 0; + int bracketDepth = 0; + int braceDepth = 0; + boolean inString = false; + char stringChar = 0; + + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + char prev = i > 0 ? content.charAt(i - 1) : ' '; + + // Track string state + if ((c == '"' || c == '\'') && prev != '\\') { + if (!inString) { + inString = true; + stringChar = c; + } else if (c == stringChar) { + inString = false; + } + } + + if (inString) continue; + + // Track bracket depth + if (c == '(') parenDepth++; + else if (c == ')') parenDepth--; + else if (c == '[') bracketDepth++; + else if (c == ']') bracketDepth--; + else if (c == '{') braceDepth++; + else if (c == '}') braceDepth--; + + // Calculate total nesting depth (prefer breaking at lower depth) + int depth = parenDepth + bracketDepth + braceDepth; + + // Find break points + if (settings.wrapAfterComma && c == ',') { + // After comma is a good break point (better at lower depth) + points.add(new BreakPoint(i + 1, 1 + depth)); + } + + if (settings.wrapAfterOperator) { + // After binary operators + if ((c == '+' || c == '-') && i > 0 && i < content.length() - 1) { + char next = content.charAt(i + 1); + if (prev != '(' && prev != '[' && prev != ',' && prev != '=' && + next != '+' && next != '-' && next != '=') { + points.add(new BreakPoint(i + 1, 3 + depth)); + } + } + if (c == '&' && i + 1 < content.length() && content.charAt(i + 1) == '&') { + points.add(new BreakPoint(i + 2, 2 + depth)); + } + if (c == '|' && i + 1 < content.length() && content.charAt(i + 1) == '|') { + points.add(new BreakPoint(i + 2, 2 + depth)); + } + } + + if (settings.wrapBeforeDot && c == '.') { + // Before dot (for method chaining) + points.add(new BreakPoint(i, 2 + depth)); + } + + // After opening brace + if (c == '{' && i + 1 < content.length()) { + points.add(new BreakPoint(i + 1, 1)); + } + } + + // Sort by position + points.sort((a, b) -> Integer.compare(a.position, b.position)); + + return points; + } + + /** + * Create a string of n spaces + */ + private String spaces(int n) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + sb.append(' '); + } + return sb.toString(); + } + + /** + * Format and wrap code in one step + */ + public String formatAndWrap(String text, int maxWidth) { + String formatted = format(text); + if (settings.wrapLongLines) { + return wrapLines(formatted, maxWidth); + } + return formatted; + } +} 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 8a7acaa2a..88bc354b8 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 @@ -145,36 +145,86 @@ public class IndentHelper { } /** - * Format entire text with proper indentation + * Format entire text with proper indentation and operator spacing */ public static FormatResult formatText(String text, int cursorPosition) { - String[] linesArr = text.split("\n", -1); - StringBuilder out = new StringBuilder(text.length() + 32); + return formatText(text, cursorPosition, 0); + } + + /** + * Format entire text with proper indentation, operator spacing, and optional line wrapping + * @param text The text to format + * @param cursorPosition Current cursor position + * @param viewportWidth Viewport width for line wrapping (0 = no wrapping) + */ + public static FormatResult formatText(String text, int cursorPosition, int viewportWidth) { + // First pass: fix indentation + String indented = formatIndentation(text, cursorPosition); + + // Second pass: apply operator spacing and whitespace normalization + FormatHelper helper = new FormatHelper(); + FormatHelper.FormatSettings settings = helper.getSettings(); + + // Enable wrapping if viewport width is provided + if (viewportWidth > 0) { + settings.wrapLongLines = true; + 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)); + } + + String formatted = helper.format(indented); + + // Apply wrapping if enabled + if (viewportWidth > 0 && settings.wrapLongLines) { + formatted = helper.wrapLines(formatted, settings.maxLineLength); + } + + // Recalculate cursor position (try to maintain relative position) + int newCursorPos = Math.min(cursorPosition, formatted.length()); + + // Try to find cursor on same line with same content context + String[] origLines = text.split("\n", -1); + String[] newLines = formatted.split("\n", -1); - // Track cursor position in formatted output - int cursorLine = -1; - int cursorColInContent = 0; int lineStartPos = 0; + int cursorLine = -1; + int cursorColInLine = 0; - // Find cursor's line and column - for (int li = 0; li < linesArr.length; li++) { - String line = linesArr[li]; - int lineEndPos = lineStartPos + line.length(); - + for (int li = 0; li < origLines.length; li++) { + int lineEndPos = lineStartPos + origLines[li].length(); if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) { cursorLine = li; - int leadingSpaces = line.length() - line.replaceAll("^[ \\t]+", "").length(); - cursorColInContent = Math.max(0, cursorPosition - lineStartPos - leadingSpaces); + cursorColInLine = cursorPosition - lineStartPos; break; } lineStartPos = lineEndPos + 1; } + if (cursorLine >= 0 && cursorLine < newLines.length) { + int newLineStart = 0; + for (int li = 0; li < cursorLine; li++) { + newLineStart += newLines[li].length() + 1; + } + // Clamp cursor to new line length + newCursorPos = newLineStart + Math.min(cursorColInLine, newLines[cursorLine].length()); + } + + return new FormatResult(formatted, newCursorPos); + } + + /** + * Format indentation only (first pass) + */ + private static String formatIndentation(String text, int cursorPosition) { + String[] linesArr = text.split("\n", -1); + StringBuilder out = new StringBuilder(text.length() + 32); + boolean inString = false; boolean escape = false; boolean inBlockComment = false; int depth = 0; - int newCursorPos = 0; for (int li = 0; li < linesArr.length; li++) { String line = linesArr[li]; @@ -218,17 +268,13 @@ public class IndentHelper { if (startsWithClose) indentLevel = Math.max(0, indentLevel - 1); int targetIndent = indentLevel * TAB_SIZE; - if (li == cursorLine) { - newCursorPos = out.length() + targetIndent + Math.min(cursorColInContent, trimmedLeading.length()); - } - out.append(spaces(targetIndent)).append(trimmedLeading); if (li < linesArr.length - 1) out.append('\n'); depth = Math.max(0, depth + opens - closes); } - return new FormatResult(out.toString(), newCursorPos); + return out.toString(); } /** 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 88e647703..8873fbc0a 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 @@ -139,22 +139,95 @@ public class JavaTextContainer extends TextContainer { } private void highlightVariableReferences(List marks) { + // Known Java keywords and types that shouldn't be flagged as undefined + java.util.Set knownIdentifiers = new java.util.HashSet<>(java.util.Arrays.asList( + // Primitive types + "boolean", "int", "float", "double", "long", "char", "byte", "short", "void", + // Keywords + "null", "true", "false", "if", "else", "switch", "case", "for", "while", "do", + "try", "catch", "finally", "return", "throw", "var", "let", "const", "function", + "continue", "break", "this", "new", "typeof", "instanceof", "class", "interface", + "extends", "implements", "import", "package", "public", "private", "protected", + "static", "final", "abstract", "synchronized", "native", "default", "enum", + "throws", "super", "assert", "volatile", "transient", "strictfp", "goto", + // Common JS/scripting keywords + "undefined", "NaN", "Infinity", "arguments", "prototype", "constructor", + // Common types (first letter uppercase pattern handles most) + "String", "Object", "Array", "Math", "System", "Integer", "Double", "Float", + "Boolean", "Long", "Byte", "Short", "Character", "List", "Map", "Set" + )); + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); Matcher m = identifier.matcher(text); while (m.find()) { String name = m.group(1); int position = m.start(1); - - // Check if this variable is local to the method it's in + + // Skip known keywords and types + if (knownIdentifiers.contains(name)) continue; + + // Skip type names (first letter uppercase) + if (Character.isUpperCase(name.charAt(0))) continue; + + // Check if inside a method MethodBlock methodBlock = findMethodBlockAtPosition(position); - if (methodBlock != null && methodBlock.localVariables.contains(name)) { - marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); - } else if (globalFields.contains(name)) { - marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + + if (methodBlock != null) { + // Inside a method - check in order: parameter, local, global + if (methodBlock.parameters.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.PARAMETER)); + } else if (methodBlock.localVariables.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); + } else if (globalFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + } else { + // Unknown variable - mark as undefined (Bug 11) + // But only if it looks like a variable reference (not a method call) + if (!isMethodCall(position) && !isTypeReference(name, position)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); + } + } + } else { + // Outside any method - check global fields + if (globalFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + } + // Don't mark as undefined outside methods - could be a type name or declaration } } } + /** + * Check if the identifier at this position is a method call (followed by parenthesis) + */ + private boolean isMethodCall(int position) { + // Skip whitespace after identifier + int i = position; + while (i < text.length() && Character.isLetterOrDigit(text.charAt(i)) || text.charAt(i) == '_') { + i++; + } + // Skip whitespace + while (i < text.length() && Character.isWhitespace(text.charAt(i))) { + i++; + } + // Check for opening paren + return i < text.length() && text.charAt(i) == '('; + } + + /** + * Check if this looks like a type reference (e.g., part of a declaration or generic) + */ + private boolean isTypeReference(String name, int position) { + // Check if preceded by 'new ', '<', or ',Type' + if (position > 4) { + String before = text.substring(Math.max(0, position - 5), position); + if (before.endsWith("new ") || before.endsWith("< ") || before.endsWith("<")) { + return true; + } + } + return false; + } + public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); @@ -366,8 +439,10 @@ public class JavaTextContainer extends TextContainer { METHOD_CALL('a', 50), NUMBER('7', 40), VARIABLE('f', 30), - GLOBAL_FIELD('b', 35), // new - LOCAL_FIELD('e', 25), // new + GLOBAL_FIELD('b', 35), // aqua - class-level fields + LOCAL_FIELD('e', 25), // yellow - local variables + PARAMETER('9', 36), // blue - method parameters (Bug 10) + UNDEFINED_VAR('4', 10), // dark red - undefined variables (Bug 11) DEFAULT('f', 0); public final char color; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java index a85b2ce35..e16fdb764 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java @@ -16,18 +16,49 @@ public class MethodBlock { // Pattern for variable declarations (same as in JavaTextContainer) public static final Pattern LOCAL_VAR_DECL = Pattern.compile( "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + + // Pattern for method parameters: Type name (with optional ... for varargs) + public static final Pattern PARAMETER_PATTERN = Pattern.compile( + "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); public int startOffset; public int endOffset; public String text; public List localVariables = new ArrayList<>(); + public List parameters = new ArrayList<>(); // Method parameters public MethodBlock(int start, int end, String text) { this.startOffset = start; this.endOffset = end; this.text = text; + extractParameters(); extractLocalVariables(); } + + // Extract method parameters from the method signature + private void extractParameters() { + parameters.clear(); + + // Find the parameter list between ( and ) + int parenStart = text.indexOf('('); + int parenEnd = text.indexOf(')'); + + if (parenStart < 0 || parenEnd <= parenStart) return; + + String paramList = text.substring(parenStart + 1, parenEnd); + + // Skip if empty + if (paramList.trim().isEmpty()) return; + + // Split by comma (but be careful of generic types like List) + Matcher m = PARAMETER_PATTERN.matcher(paramList); + while (m.find()) { + String paramName = m.group(2); + if (!parameters.contains(paramName)) { + parameters.add(paramName); + } + } + } // Extract local variables declared within this method private void extractLocalVariables() {