From 254655cb1de0d4b696d641395fbcecca50f6e63e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 13 Mar 2026 02:46:16 +0200 Subject: [PATCH] Improved <> formatting and auto insertion --- .../client/gui/util/GuiScriptTextArea.java | 108 ++++++++++++++++++ .../client/gui/util/script/FormatHelper.java | 37 +++++- 2 files changed, 144 insertions(+), 1 deletion(-) 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 74a2432b6..a37f0b4b6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2571,6 +2571,34 @@ public class GuiScriptTextArea extends GuiNpcTextField { String after = getSelectionAfterText(); int cursorPos = selection.getCursorPosition(); + // Diamond/generic angle bracket handling + if (c == '<') { + if (!container.getDocument().isExcludedInclusive(cursorPos) && isGenericContext(before)) { + if (hasUnmatchedCloserAhead(after, '<', '>')) { + // There's already an unmatched '>' ahead — just insert '<' + setText(before + "<" + after, true); + selection.reset(before.length() + 1); + } else { + setText(before + "<>" + after, true); + selection.reset(before.length() + 1); + } + scrollToCursor(); + autocompleteManager.onCharTyped(c, text, cursorPos); + return true; + } + } + + if (c == '>') { + if (after.length() > 0 && after.charAt(0) == '>') { + if (shouldSkipAngleBracketClose(before)) { + selection.reset(before.length() + 1); + scrollToCursor(); + autocompleteManager.onCharTyped(c, text, cursorPos); + return true; + } + } + } + if ((c == ')' || c == ']' || c == '}') && after.length() > 0 && after.charAt(0) == c) { selection.reset(before.length() + 1); scrollToCursor(); @@ -2689,6 +2717,86 @@ public class GuiScriptTextArea extends GuiNpcTextField { return (backslashes % 2) == 1; } + private boolean isGenericContext(String before) { + if (before == null || before.isEmpty()) return false; + + int i = before.length() - 1; + while (i >= 0 && (before.charAt(i) == ' ' || before.charAt(i) == '\t')) { + i--; + } + if (i < 0) return false; + + char last = before.charAt(i); + + if (last == '>' || last == ')' || last == ']') return true; + if (last == ',') return true; + + if (Character.isJavaIdentifierPart(last)) { + int end = i + 1; + while (i >= 0 && Character.isJavaIdentifierPart(before.charAt(i))) { + i--; + } + String word = before.substring(i + 1, end); + + if (isComparisonKeyword(word)) return false; + if (isNumericLiteral(word)) return false; + + return true; + } + + return false; + } + + private boolean isComparisonKeyword(String word) { + return "if".equals(word) || "while".equals(word) || "for".equals(word) + || "return".equals(word) || "else".equals(word) || "case".equals(word) + || "assert".equals(word) || "throw".equals(word) || "true".equals(word) + || "false".equals(word) || "null".equals(word); + } + + private boolean isNumericLiteral(String word) { + if (word.isEmpty()) return false; + char first = word.charAt(0); + return first >= '0' && first <= '9'; + } + + private boolean shouldSkipAngleBracketClose(String before) { + if (before == null || before.isEmpty()) return false; + int depth = 0; + boolean inLineComment = false; + boolean inBlockComment = false; + boolean inString = false; + boolean escape = false; + char stringDelimiter = 0; + + for (int i = 0; i < before.length(); i++) { + char c = before.charAt(i); + char next = i + 1 < before.length() ? before.charAt(i + 1) : 0; + + if (inString) { + if (escape) { escape = false; } + else if (c == '\\') { escape = true; } + else if (c == stringDelimiter || c == '\n') { inString = false; } + continue; + } + if (inBlockComment) { + if (c == '*' && next == '/') { inBlockComment = false; i++; } + continue; + } + if (inLineComment) { + if (c == '\n') { inLineComment = false; } + continue; + } + if (c == '/' && next == '/') { inLineComment = true; i++; continue; } + if (c == '/' && next == '*') { inBlockComment = true; i++; continue; } + if (c == '"' || c == '\'') { inString = true; stringDelimiter = c; escape = false; continue; } + + if (c == '<') depth++; + else if (c == '>') depth--; + } + return depth > 0; + } + private boolean hasUnmatchedCloserAhead(String after, char opener, char closer) { int balance = 0; int len = after.length(); 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 index 989656ddf..5877c5a13 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java @@ -1181,13 +1181,15 @@ public class FormatHelper { return settings.spaceBeforeSemicolon ? " " : ""; } - // ---- COLON spacing (ternary : always gets space, label : uses settings) ---- + // ---- COLON spacing (ternary : always gets space, enhanced-for : always gets space, label : uses settings) ---- if (prev.type == TokenType.COLON) { if (isTernaryColon(allTokens, currIdx - 1)) return " "; + if (isEnhancedForLoopColon(allTokens, currIdx - 1)) return " "; return settings.spaceAfterColon ? " " : ""; } if (curr.type == TokenType.COLON) { if (isTernaryColon(allTokens, currIdx)) return " "; + if (isEnhancedForLoopColon(allTokens, currIdx)) return " "; return settings.spaceBeforeColon ? " " : ""; } @@ -1398,6 +1400,39 @@ public class FormatHelper { return false; } + /** + * Check if a colon at the given index is the separator in an enhanced for-loop + * ({@code for (Type var : collection)}) by scanning backwards from the colon + * for an opening paren preceded by the {@code for} keyword. + */ + private boolean isEnhancedForLoopColon(List tokens, int colonIdx) { + int depth = 0; + for (int i = colonIdx - 1; i >= 0; i--) { + Token t = tokens.get(i); + if (t.type == TokenType.PAREN_CLOSE) depth++; + else if (t.type == TokenType.PAREN_OPEN) { + if (depth > 0) { + depth--; + } else { + // Found the opening paren at our depth — check if preceded by 'for' + for (int j = i - 1; j >= 0; j--) { + if (tokens.get(j).type != TokenType.WHITESPACE) { + return tokens.get(j).type == TokenType.KEYWORD + && tokens.get(j).value.equals("for"); + } + } + return false; + } + } + // Stop at statement boundaries + if (t.type == TokenType.SEMICOLON || t.type == TokenType.BRACE_OPEN || + t.type == TokenType.BRACE_CLOSE) { + return false; + } + } + return false; + } + private boolean isAssignmentOperator(String op) { switch (op) { case "=": case "+=": case "-=": case "*=": case "/=": case "%=":