From 6373839870bf39a47cfe04bb1be5c85604006dcf Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 21:10:25 +0200 Subject: [PATCH] CRAZY GuiScriptTextArea REFACTOR (W OPUS 4.5) --- .../client/gui/util/GuiScriptTextArea.java | 544 +++--------------- 1 file changed, 94 insertions(+), 450 deletions(-) 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 47bcf1447..4ecd25847 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -6,30 +6,38 @@ import net.minecraft.client.gui.ScaledResolution; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; -import noppes.npcs.client.gui.util.script.JavaTextContainer; +import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; +/** + * Script text editor component with syntax highlighting, bracket matching, + * smooth scrolling, and IDE-like features. + * + * Helper classes used: + * - BracketMatcher: bracket matching and brace span computation + * - IndentHelper: indentation utilities and text formatting + * - CommentHandler: line comment toggling + * - CursorNavigation: cursor movement logic + */ public class GuiScriptTextArea extends GuiNpcTextField { + + // ==================== DIMENSIONS & POSITION ==================== public int id; public int x; public int y; public int width; public int height; - private int cursorCounter; - private long lastInputTime = 0L; - private ITextChangeListener listener; - public String text = null; - private JavaTextContainer container = null; + + // ==================== STATE FLAGS ==================== public boolean active = false; public boolean enabled = true; public boolean visible = true; @@ -38,27 +46,43 @@ public class GuiScriptTextArea extends GuiNpcTextField { public boolean tripleClicked = false; public boolean clickScrolling = false; private int clickCount = 0; + private long lastClicked = 0L; + + // ==================== TEXT & CONTAINER ==================== + public String text = null; + private JavaTextContainer container = null; + private boolean enableCodeHighlighting = false; + + // ==================== SELECTION STATE ==================== private int startSelection; private int endSelection; private int cursorPosition; + + // ==================== SCROLL STATE ==================== private int scrolledLine = 0; - // Smooth scrolling state: fractional line position and target position private double scrollPos = 0.0; private double targetScroll = 0.0; - // velocity used for spring-based easing private double scrollVelocity = 0.0; - // last timestamp used for scroll integration (ms) private long lastScrollTime = 0L; - // scrollbar drag offset in pixels (mouseY - thumbTop) when dragging begins private int scrollbarDragOffset = 0; - private boolean enableCodeHighlighting = false; - public List undoList = new ArrayList(); - public List redoList = new ArrayList(); - public boolean undoing = false; - private long lastClicked = 0L; - // Line number gutter width (left margin for line numbers) - dynamically calculated + + // Scroll animation constants + private static final double SCROLL_TAU = 0.1; // Time constant (~55ms) + private static final double SCROLL_SNAP = 0.01; // Snap threshold + private static final double SCROLL_MAX_DT = 0.05; // Max delta time + + // ==================== UI COMPONENTS ==================== + private int cursorCounter; + private long lastInputTime = 0L; + private ITextChangeListener listener; private static int LINE_NUMBER_GUTTER_WIDTH = 25; - // private static TrueTypeFont font = new TrueTypeFont(new Font("Arial Unicode MS", Font.PLAIN, ConfigClient.FontSize), 1); + + // ==================== UNDO/REDO ==================== + public List undoList = new ArrayList<>(); + public List redoList = new ArrayList<>(); + public boolean undoing = false; + + // ==================== CONSTRUCTOR ==================== public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { super(id, guiScreen, x, y, width, height, null); @@ -71,6 +95,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.setText(text); this.undoing = false; } + + // ==================== RENDERING ==================== public void drawTextBox(int xMouse, int yMouse) { if (!visible) @@ -408,190 +434,27 @@ public class GuiScriptTextArea extends GuiNpcTextField { } GL11.glDisable(GL11.GL_SCISSOR_TEST); } - // Find a bracket span for a given cursor position. Returns {start, end} or null. + // ==================== BRACKET MATCHING ==================== + // Uses BracketMatcher helper for bracket span detection + private int[] findBracketSpanAt(int pos) { - if (text == null || text.isEmpty()) return null; - // First try at pos (caret before the char) - if (pos >= 0 && pos < text.length()) { - char c = text.charAt(pos); - int found = 0; - if (c == '{') found = findClosingBracket(text.substring(pos), '{', '}'); - else if (c == '[') found = findClosingBracket(text.substring(pos), '[', ']'); - else if (c == '(') found = findClosingBracket(text.substring(pos), '(', ')'); - else if (c == '}') found = findOpeningBracket(text.substring(0, pos + 1), '{', '}'); - else if (c == ']') found = findOpeningBracket(text.substring(0, pos + 1), '[', ']'); - else if (c == ')') found = findOpeningBracket(text.substring(0, pos + 1), '(', ')'); - if (found != 0) return new int[]{pos, pos + found}; - } - - // Scan backwards from just before the caret, skipping spaces/tabs on the same line, - // to find a nearby bracket that should be treated as "immediately before" the caret. - if (pos > 0) { - int scan = pos - 1; - // Move left over spaces/tabs but do not cross a newline - while (scan >= 0) { - char sc = text.charAt(scan); - if (sc == ' ' || sc == '\t') {scan--;continue;} - if (sc == '\n') {scan = -1; // stop, do not cross line boundary - break; - } - - int found2 = 0; - if (sc == '{') found2 = findClosingBracket(text.substring(scan), '{', '}'); - else if (sc == '[') found2 = findClosingBracket(text.substring(scan), '[', ']'); - else if (sc == '(') found2 = findClosingBracket(text.substring(scan), '(', ')'); - else if (sc == '}') found2 = findOpeningBracket(text.substring(0, scan + 1), '{', '}'); - else if (sc == ']') found2 = findOpeningBracket(text.substring(0, scan + 1), '[', ']'); - else if (sc == ')') found2 = findOpeningBracket(text.substring(0, scan + 1), '(', ')'); - - if (found2 != 0) return new int[]{scan, scan + found2}; - // Not a bracket; stop scanning further - break; - } - } - - return null; + return BracketMatcher.findBracketSpanAt(text, pos); } + private int findClosingBracket(String str, char s, char e) { - int found = 0; - char[] chars = str.toCharArray(); - - for (int i = 0; i < chars.length; ++i) { - char c = chars[i]; - if (c == s) { - ++found; - } else if (c == e) { - --found; - if (found == 0) { - return i; - } - } - } - - return 0; + return BracketMatcher.findClosingBracket(str, s, e); } private int findOpeningBracket(String str, char s, char e) { - int found = 0; - char[] chars = str.toCharArray(); - - for (int i = chars.length - 1; i >= 0; --i) { - char c = chars[i]; - if (c == e) { - ++found; - } else if (c == s) { - --found; - if (found == 0) { - return i - chars.length + 1; - } - } - } - - return 0; + return BracketMatcher.findOpeningBracket(str, s, e); } - // Computes brace spans as {originalDepth, openLineIndex, closeLineIndex, adjustedDepth}, inclusive of brace lines private List computeBraceSpans(String fullText, List lines) { - List spans = new ArrayList<>(); - if (fullText == null || fullText.isEmpty() || lines == null || lines.isEmpty()) - return spans; - - int lineIdx = 0; - int lineEnd = lines.get(0).end; - List openStack = new ArrayList<>(); // line indices of unmatched opens - - boolean inLineComment = false; - boolean inBlockComment = false; - boolean inString = false; - boolean escape = false; - char stringDelimiter = 0; - - for (int pos = 0; pos < fullText.length(); pos++) { - while (lineIdx < lines.size() - 1 && pos >= lineEnd) { - lineIdx++; - lineEnd = lines.get(lineIdx).end; - } - - char c = fullText.charAt(pos); - char next = pos + 1 < fullText.length() ? fullText.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') { - // Treat unterminated strings as ending at EOL to avoid breaking later brace parsing - 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 == '{') { - openStack.add(lineIdx); - } else if (c == '}') { - if (!openStack.isEmpty()) { - int openLine = openStack.remove(openStack.size() - 1); - int spanDepth = openStack.size() + 1; - int closeLine = lineIdx; - spans.add(new int[]{spanDepth, openLine, closeLine}); - } - } - } - - // Normalize depths to reduce impact of unmatched opens without hiding nested guides - if (!openStack.isEmpty()) { - int baseline = openStack.size(); - List adjusted = new ArrayList<>(spans.size()); - for (int[] span : spans) { - int adjustedDepth = Math.max(1, span[0] - baseline); - adjusted.add(new int[]{span[0], span[1], span[2], adjustedDepth}); - } - spans = adjusted; - } else { - for (int idx = 0; idx < spans.size(); idx++) { - int[] span = spans.get(idx); - spans.set(idx, new int[]{span[0], span[1], span[2], span[0]}); - } - } - - return spans; + return BracketMatcher.computeBraceSpans(fullText, lines); } + // ==================== SELECTION & CURSOR POSITION ==================== + private int getSelectionPos(int xMouse, int yMouse) { xMouse -= (this.x + LINE_NUMBER_GUTTER_WIDTH + 1); yMouse -= this.y + 1; @@ -686,6 +549,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void ensureCursorVisible() { scrollToCursor(); } + + // ==================== KEYBOARD INPUT HANDLING ==================== @Override public boolean textboxKeyTyped(char c, int i) { @@ -1146,109 +1011,26 @@ public class GuiScriptTextArea extends GuiNpcTextField { return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54); } + // ==================== COMMENT TOGGLING ==================== + // Uses CommentHandler helper for comment operations + private void toggleCommentSelection() { - StringBuilder newText = new StringBuilder(text.length() + 100); - int prevEnd = 0; - - int newStart = -1; //calculates expanded startSelection - int newEnd = -1; //calculates expanded endSelection - int newIndex = 0; - - for (LineData line : container.lines) { - int safeLineStart = Math.max(0, Math.min(line.start, text.length())); - int safeLineEnd = Math.max(0, Math.min(line.end, text.length())); - - if (safeLineStart > prevEnd) { - if (newStart == -1 && startSelection >= prevEnd && startSelection <= safeLineStart) { - newStart = newIndex + (startSelection - prevEnd); - } - if (newEnd == -1 && endSelection >= prevEnd && endSelection <= safeLineStart) { - newEnd = newIndex + (endSelection - prevEnd); - } - newText.append(text, prevEnd, safeLineStart); - newIndex += safeLineStart - prevEnd; - } - - if (safeLineEnd > startSelection && safeLineStart < endSelection) { - String lineText = text.substring(safeLineStart, safeLineEnd); - CommentToggleInfo ti = toggleLine(lineText); - String newLineText = ti.text; - int nonWs = ti.nonWs; - int delta = ti.delta; - - if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { - int offsetInOldStart = startSelection - safeLineStart; - int newOffsetStart = offsetInOldStart; - if (offsetInOldStart >= nonWs) { - newOffsetStart = offsetInOldStart + delta; - } - newStart = newIndex + Math.max(0, newOffsetStart); - } - if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { - int offsetInOld = endSelection - safeLineStart; - int newOffset = offsetInOld; - if (offsetInOld >= nonWs) { - newOffset = offsetInOld + delta; - } - newEnd = newIndex + Math.max(0, newOffset); - } - - newText.append(newLineText); - newIndex += newLineText.length(); - } else { - if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { - newStart = newIndex + (startSelection - safeLineStart); - } - if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { - newEnd = newIndex + (endSelection - safeLineStart); - } - newText.append(text, safeLineStart, safeLineEnd); - newIndex += safeLineEnd - safeLineStart; - } - prevEnd = safeLineEnd; - } - - if (prevEnd < text.length()) { - if (newStart == -1 && startSelection >= prevEnd && startSelection <= text.length()) { - newStart = newIndex + (startSelection - prevEnd); - } - if (newEnd == -1 && endSelection >= prevEnd && endSelection <= text.length()) { - newEnd = newIndex + (endSelection - prevEnd); - } - newText.append(text, prevEnd, text.length()); - newIndex += text.length() - prevEnd; - } - - if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length())); - if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length())); - - setText(newText.toString()); - startSelection = newStart; - endSelection = newEnd; + CommentHandler.SelectionToggleResult result = CommentHandler.toggleCommentSelection( + text, container.lines, startSelection, endSelection); + setText(result.newText); + startSelection = result.newStartSelection; + endSelection = result.newEndSelection; cursorPosition = endSelection; } private void toggleCommentLineAtCursor() { - for (LineData line : container.lines) { - int lineStart = Math.max(0, Math.min(line.start, text.length())); - int lineEnd = Math.max(0, Math.min(line.end, text.length())); - if (cursorPosition >= lineStart && cursorPosition <= lineEnd) { - String lineText = text.substring(lineStart, lineEnd); - CommentToggleInfo ti = toggleLine(lineText); - String newLineText = ti.text; - int nonWs = ti.nonWs; - boolean hasComment = ti.delta < 0; - String before = text.substring(0, lineStart); - String after = lineEnd <= text.length() ? text.substring(lineEnd) : ""; - setText(before + newLineText + after); - int cursorDelta = hasComment ? -2 : 2; - int newCursor = cursorPosition + cursorDelta; - if (cursorPosition < lineStart + nonWs + (hasComment ? 2 : 0)) newCursor = cursorPosition; - setCursor(Math.max(lineStart, newCursor), false); - return; - } - } + CommentHandler.SingleLineToggleResult result = CommentHandler.toggleCommentAtCursor( + text, container.lines, cursorPosition); + setText(result.newText); + setCursor(result.newCursorPosition, false); } + + // ==================== KEYBOARD MODIFIERS ==================== private boolean isAltKeyDown() { return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184); @@ -1275,22 +1057,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private String getIndentCurrentLine() { - Iterator var1 = this.container.lines.iterator(); - - LineData data; - do { - if (!var1.hasNext()) { - return ""; + for (LineData data : this.container.lines) { + if (this.cursorPosition > data.start && this.cursorPosition <= data.end) { + int i; + for (i = 0; i < data.text.length() && data.text.charAt(i) == ' '; ++i) { + } + return data.text.substring(0, i); } - - data = (LineData) var1.next(); - } while (this.cursorPosition <= data.start || this.cursorPosition > data.end); - - int i; - for (i = 0; i < data.text.length() && data.text.charAt(i) == 32; ++i) { } - - return data.text.substring(0, i); + return ""; } private String getAutoIndentForEnter() { @@ -1387,133 +1162,23 @@ public class GuiScriptTextArea extends GuiNpcTextField { return prevIndent; } + // ==================== TEXT FORMATTING ==================== + private int getTabSize() { - return 4; + return IndentHelper.TAB_SIZE; } private String repeatSpace(int count) { - if (count <= 0) - return ""; - char[] arr = new char[count]; - java.util.Arrays.fill(arr, ' '); - return new String(arr); + return IndentHelper.spaces(count); } private void formatText() { - // Find which line the cursor is on and calculate position within that line (after stripping indent) - int cursorLine = -1; - int cursorColInContent = 0; // position in line after removing leading whitespace - int lineStartPos = 0; - String[] linesArr = text.split("\n", -1); - - for (int li = 0; li < linesArr.length; li++) { - String line = linesArr[li]; - int lineEndPos = lineStartPos + line.length(); - - if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) { - cursorLine = li; - int leadingSpaces = line.length() - line.replaceAll("^[ \t]+", "").length(); - cursorColInContent = Math.max(0, cursorPosition - lineStartPos - leadingSpaces); - break; - } - lineStartPos = lineEndPos + 1; // +1 for newline - } - - StringBuilder out = new StringBuilder(text.length() + 32); - - boolean inString = false; - boolean escape = false; - boolean inBlockComment = false; - int depth = 0; - int tab = getTabSize(); - - int newCursorPos = 0; - - for (int li = 0; li < linesArr.length; li++) { - String line = linesArr[li]; - String trimmedLeading = line.replaceAll("^[ \t]+", ""); - - int lineIndentDec = 0; - int opens = 0, closes = 0; - boolean startsWithClose = false; - - // First pass to determine startsWithClose and brace counts ignoring strings/comments - for (int idx = 0; idx < line.length(); idx++) { - char c = line.charAt(idx); - char next = idx + 1 < line.length() ? line.charAt(idx + 1) : 0; - - if (inString) { - if (escape) { - escape = false; - } else if (c == '\\') { - escape = true; - } else if (c == '"') { - inString = false; - } - continue; - } - - if (inBlockComment) { - if (c == '*' && next == '/') { - inBlockComment = false; - idx++; - } - continue; - } - - if (c == '/' && next == '/') { - break; // rest of line is comment - } - if (c == '/' && next == '*') { - inBlockComment = true; - idx++; - continue; - } - if (c == '"') { - inString = true; - escape = false; - continue; - } - - if (c == '{') { - opens++; - } else if (c == '}') { - closes++; - if (!startsWithClose) { - String prefix = line.substring(0, idx); - if (prefix.trim().isEmpty()) { - startsWithClose = true; - } - } - } - } - - int indentLevel = depth; - if (startsWithClose) - indentLevel = Math.max(0, indentLevel - 1); - - int targetIndent = indentLevel * tab; - - // Track cursor position in the formatted output - if (li == cursorLine) { - newCursorPos = out.length() + targetIndent + Math.min(cursorColInContent, trimmedLeading.length()); - } - - out.append(repeatSpace(targetIndent)).append(trimmedLeading); - if (li < linesArr.length - 1) - out.append('\n'); - - depth = Math.max(0, depth + opens - closes); - } - - setText(out.toString()); - - // Restore cursor position - if (cursorLine >= 0) { - newCursorPos = Math.max(0, Math.min(newCursorPos, this.text.length())); - startSelection = endSelection = cursorPosition = newCursorPos; - } + IndentHelper.FormatResult result = IndentHelper.formatText(text, cursorPosition); + setText(result.text); + startSelection = endSelection = cursorPosition = Math.max(0, Math.min(result.cursorPosition, this.text.length())); } + + // ==================== TAB HANDLING ==================== private void handleTab() { LineData currentLine = null; @@ -1632,6 +1297,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } + + // ==================== CURSOR MANAGEMENT ==================== private void setCursor(int i, boolean select) { i = Math.min(Math.max(i, 0), this.text.length()); @@ -1733,6 +1400,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public String getSelectionAfterText() { return this.text.substring(this.endSelection); } + + // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; @@ -1789,6 +1458,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void updateCursorCounter() { ++this.cursorCounter; } + + // ==================== TEXT MANAGEMENT ==================== public void setText(String text) { if (text == null) { @@ -1867,35 +1538,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.endSelection = Math.max(0, Math.min(this.endSelection, length)); this.cursorPosition = Math.max(0, Math.min(this.cursorPosition, length)); } - - private static class CommentToggleInfo { - public final String text; - public final int delta; // newText.length() - oldText.length() - public final int nonWs; - - public CommentToggleInfo(String text, int delta, int nonWs) { - this.text = text; - this.delta = delta; - this.nonWs = nonWs; - } - } - - private CommentToggleInfo toggleLine(String lineText) { - int nonWs = 0; - while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) nonWs++; - boolean hasContent = nonWs < lineText.length(); - boolean hasComment = hasContent && lineText.startsWith("//", nonWs); - String newLineText; - if (hasComment) { - int cut = Math.min(nonWs + 2, lineText.length()); - newLineText = lineText.substring(0, nonWs) + (cut <= lineText.length() ? lineText.substring(cut) : ""); - } else if (hasContent) { - newLineText = lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs); - } else { - newLineText = lineText; - } - return new CommentToggleInfo(newLineText, newLineText.length() - lineText.length(), nonWs); - } + + // ==================== INNER CLASSES ==================== class UndoData { public String text;