diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index dc08df7ea..288734d1b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -331,10 +331,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } List list = new ArrayList<>(container.lines); - + // Build brace spans: {origDepth, open line, close line, adjustedDepth} List braceSpans = BracketMatcher.computeBraceSpans(text, list); - + // Always highlight unmatched braces (positions in text) + List unmatchedBraces = BracketMatcher.findUnmatchedBracePositions(text); + // Determine which exact brace span (openLine/closeLine) should be highlighted based on bracket under caret int highlightedOpenLine = -1; int highlightedCloseLine = -1; @@ -432,6 +434,18 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); } } + + // Highlight unmatched braces in this line (always red) + if (unmatchedBraces != null && !unmatchedBraces.isEmpty()) { + for (int ubPos : unmatchedBraces) { + if (ubPos >= data.start && ubPos < data.end) { + int rel = ubPos - data.start; + int s = ClientProxy.Font.width(line.substring(0, rel)); + int e = ClientProxy.Font.width(line.substring(0, rel + 1)) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0xffcc0000); + } + } + } //Highlight words if (highlightedWord != null) { Matcher m = container.regexWord.matcher(line); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java index a4c0e7a30..96fe2a7ba 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java @@ -52,6 +52,84 @@ public class BracketMatcher { return null; } + /** + * Find all unmatched brace positions (both opens and closes) in the text. + * Returns a list of character indices into the text for braces that have no matching pair. + */ + public static List findUnmatchedBracePositions(String text) { + List unmatched = new ArrayList<>(); + if (text == null || text.isEmpty()) return unmatched; + + List stack = new ArrayList<>(); // {char, pos} + + boolean inLineComment = false; + boolean inBlockComment = false; + boolean inString = false; + boolean escape = false; + char stringDelimiter = 0; + + for (int pos = 0; pos < text.length(); pos++) { + char c = text.charAt(pos); + char next = pos + 1 < text.length() ? text.charAt(pos + 1) : 0; + + if (inString) { + if (escape) { escape = false; } + else if (c == '\\') { escape = true; } + else if (c == stringDelimiter) { inString = false; } + else if (c == '\n') { inString = false; } + continue; + } + + if (inBlockComment) { + if (c == '*' && next == '/') { inBlockComment = false; pos++; } + continue; + } + + if (inLineComment) { + if (c == '\n') { inLineComment = false; } + continue; + } + + if (c == '/' && next == '/') { inLineComment = true; pos++; continue; } + if (c == '/' && next == '*') { inBlockComment = true; pos++; continue; } + + if (c == '"' || c == '\'') { + inString = true; + stringDelimiter = c; + escape = false; + continue; + } + + if (c == '{' || c == '[' || c == '(') { + stack.add(new int[]{c, pos}); + } else if (c == '}' || c == ']' || c == ')') { + if (!stack.isEmpty()) { + int[] top = stack.get(stack.size() - 1); + char topc = (char) top[0]; + char expectOpen = (c == '}') ? '{' : (c == ']') ? '[' : '('; + if (topc == expectOpen) { + stack.remove(stack.size() - 1); + continue; + } else { + // mismatched: mark this close as unmatched + unmatched.add(pos); + continue; + } + } else { + // closing with no opens + unmatched.add(pos); + } + } + } + + // any remaining opens on stack are unmatched + for (int[] open : stack) { + unmatched.add(open[1]); + } + + return unmatched; + } + /** * Find closing bracket, returns offset from start or 0 if not found */