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 7474657e2..b0bed6ea9 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 @@ -17,7 +17,7 @@ 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)\\b"); public static final Pattern TYPE_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_]*)" + // Group 1 → main type - "\\s*(<([^>]+)>)?" + // Group 2 → <…>, Group 3 → inner type + "\\s*(<([^>]+)>)?" + // Group c2 → <…>, Group 3 → inner type "\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name ); @@ -47,12 +47,102 @@ public class JavaTextContainer extends TextContainer { public int lineHeight; public int totalHeight; public int visibleLines = 1; + // Cached brace spans: list of {level, startLine, endLine} + private List braceSpansCache = null; public JavaTextContainer(String text) { super(text); this.text = text.replaceAll("\\r?\\n|\\r", "\n"); } + public synchronized List getBraceSpans() { + if (braceSpansCache != null) + return braceSpansCache; + List spans = new ArrayList<>(); + if (text == null || text.isEmpty() || lines == null || lines.isEmpty()) { + braceSpansCache = spans; + return spans; + } + + // Heuristics limits to avoid runaway spans when braces are unbalanced. + final int MAX_SPAN_LINES = 200; // cap span length to this many lines + final int MAX_STACK_DEPTH = 512; // avoid excessive stack growth + + List excluded = MethodBlock.getExcludedRanges(text); + int exIdx = 0; + java.util.ArrayList openStack = new java.util.ArrayList<>(); + int len = text.length(); + for (int p = 0; p < len; p++) { + while (exIdx < excluded.size() && p >= excluded.get(exIdx)[1]) + exIdx++; + if (exIdx < excluded.size()) { + int[] r = excluded.get(exIdx); + if (p >= r[0] && p < r[1]) { + p = r[1] - 1; + continue; + } + } + char c = text.charAt(p); + if (c == '{') { + if (openStack.size() < MAX_STACK_DEPTH) { + // Look ahead to see if this open has a plausible closing brace within limits. + int openLine = getLineIndex(p); + int lookaheadChars = Math.max(1000, MAX_SPAN_LINES * 120); + int searchEnd = Math.min(len, p + lookaheadChars); + boolean foundClose = false; + int exIdx2 = exIdx; // local index for scanning ahead + for (int q = p + 1; q < searchEnd; q++) { + while (exIdx2 < excluded.size() && q >= excluded.get(exIdx2)[1]) exIdx2++; + if (exIdx2 < excluded.size()) { + int[] r2 = excluded.get(exIdx2); + if (q >= r2[0] && q < r2[1]) { q = r2[1] - 1; continue; } + } + if (text.charAt(q) == '}') { + int closeLineCandidate = getLineIndex(q); + if (closeLineCandidate - openLine <= MAX_SPAN_LINES) { + foundClose = true; + break; + } + } + } + if (foundClose) openStack.add(p); + } + } else if (c == '}') { + if (!openStack.isEmpty()) { + int openPos = openStack.remove(openStack.size() - 1); + int level = openStack.size() + 1; + int openLine = getLineIndex(openPos); + int closeLine = getLineIndex(p); + // Cap spans to avoid drawing across the whole document when syntax is broken + if (closeLine - openLine > MAX_SPAN_LINES) { + closeLine = Math.min(openLine + MAX_SPAN_LINES, lines.size() - 1); + } + // Only add spans where closeLine > openLine (multi-line blocks) + if (closeLine > openLine) { + spans.add(new int[]{level, openLine, closeLine}); + } + } + } + } + + braceSpansCache = spans; + return spans; + } + + private int getLineIndex(int pos) { + if (lines == null || lines.isEmpty()) + return 0; + int length = text == null ? 0 : text.length(); + for (int i = 0; i < lines.size(); i++) { + LineData d = lines.get(i); + int s = Math.max(0, Math.min(d.start, length)); + int e = Math.max(0, Math.min(d.end, length)); + if (pos >= s && pos <= e) + return i; + } + return Math.max(0, lines.size() - 1); + } + public void init(int width, int height) { lineHeight = ClientProxy.Font.height(); if (lineHeight == 0) @@ -81,6 +171,9 @@ public class JavaTextContainer extends TextContainer { linesCount = lines.size(); totalHeight = linesCount * lineHeight; visibleLines = Math.max(height / lineHeight, 1); + + // Invalidate caches that depend on line layout + invalidateCaches(); } private List globalFields = new ArrayList<>(); @@ -153,6 +246,9 @@ public class JavaTextContainer extends TextContainer { } } + public synchronized void invalidateCaches() { + this.braceSpansCache = null; + } public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); @@ -175,6 +271,14 @@ public class JavaTextContainer extends TextContainer { marks = resolveConflicts(marks); + // Compute indent guides based on matched braces, ignoring strings/comments + computeIndentGuides(marks); + + // Invalidate cached brace spans; will be recomputed on demand + invalidateCaches(); + + + // Step 2: Clear existing tokens for (LineData line : lines) { line.tokens.clear(); @@ -285,6 +389,7 @@ public class JavaTextContainer extends TextContainer { public String text; public int start, end; public List tokens = new ArrayList<>(); + public List indentCols = new ArrayList<>(); public LineData(String text, int startIndex, int end) { this.text = text; @@ -372,4 +477,79 @@ public class JavaTextContainer extends TextContainer { this.priority = priority; } } + + // Compute indent guide columns per line based on brace matching. + private void computeIndentGuides(List marks) { + // Clear existing guides + for (LineData ld : lines) { + ld.indentCols.clear(); + } + + // Build ignored ranges from STRING and COMMENT tokens + List ignored = new ArrayList<>(); + for (Mark m : marks) { + if (m.type == TokenType.STRING || m.type == TokenType.COMMENT) { + ignored.add(new int[]{m.start, m.end}); + } + } + + java.util.function.Predicate isIgnored = (pos) -> { + for (int[] r : ignored) { + if (pos >= r[0] && pos < r[1]) + return true; + } + return false; + }; + + class OpenBrace { + int line, col; + + OpenBrace(int l, int c) { + line = l; + col = c; + } + } + java.util.Deque stack = new java.util.ArrayDeque<>(); + + final int tabSize = 4; + + for (int li = 0; li < lines.size(); li++) { + LineData ld = lines.get(li); + String s = ld.text; + for (int i = 0; i < s.length(); i++) { + int absPos = ld.start + i; + if (isIgnored.test(absPos)) + continue; + char c = s.charAt(i); + if (c == '{') { + // compute leading indentation (expanded) up to this char + int leading = 0; + for (int k = 0; k < i; k++) { + char ch = s.charAt(k); + if (ch == '\t') + leading += tabSize; + else + leading += 1; + } + stack.push(new OpenBrace(li, leading)); + } else if (c == '}') { + if (!stack.isEmpty()) { + OpenBrace open = stack.pop(); + int startLine = open.line; + int col = open.col; + if (startLine == li) + continue; // same-line block, skip + // add guide column to lines inside the block + int from = Math.max(0, startLine + 1); + int to = Math.min(lines.size() - 1, li); + for (int l = from; l <= to; l++) { + List list = lines.get(l).indentCols; + if (!list.contains(col)) + list.add(col); + } + } + } + } + } + } } 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 6be400d3f..a85b2ce35 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 @@ -78,27 +78,45 @@ public class MethodBlock { return methods; } - // Find ranges of text to skip (strings and comments) - private static List findExcludedRanges(String text) { + // Find ranges of text to skip (strings and comments). Returns merged, sorted ranges. + public static List getExcludedRanges(String text) { List ranges = new ArrayList<>(); - // Find all strings using the pattern from JavaTextContainer + // Find all strings and char literals (simple heuristic) Pattern stringPattern = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); Matcher stringMatcher = stringPattern.matcher(text); while (stringMatcher.find()) { ranges.add(new int[] { stringMatcher.start(), stringMatcher.end() }); } - // Find all comments - Pattern commentPattern = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); + // Find all comments (block and line comments) + Pattern commentPattern = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*"); Matcher commentMatcher = commentPattern.matcher(text); while (commentMatcher.find()) { ranges.add(new int[] { commentMatcher.start(), commentMatcher.end() }); } + if (ranges.isEmpty()) + return ranges; + // Sort ranges by start position ranges.sort((a, b) -> Integer.compare(a[0], b[0])); - return ranges; + + // Merge overlapping/adjacent ranges for faster scanning + List merged = new ArrayList<>(); + int[] current = ranges.get(0); + for (int i = 1; i < ranges.size(); i++) { + int[] next = ranges.get(i); + if (next[0] <= current[1]) { + // overlap or adjacent + current[1] = Math.max(current[1], next[1]); + } else { + merged.add(current); + current = next; + } + } + merged.add(current); + return merged; } // Check if a position is within an excluded range @@ -114,7 +132,7 @@ public class MethodBlock { // Utility to find the matching closing brace, ignoring braces in strings and // comments public static int findMatchingBrace(String text, int openBraceIndex) { - List excludedRanges = findExcludedRanges(text); + List excludedRanges = getExcludedRanges(text); int depth = 0; for (int i = openBraceIndex; i < text.length(); i++) {