From 32e82fc9c94f52c7883a9d4d6c58606b6edb303f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 17:37:42 +0200 Subject: [PATCH 1/4] temp indents --- .../client/gui/util/GuiScriptTextArea.java | 51 +++++ .../gui/util/script/JavaTextContainer.java | 182 +++++++++++++++++- .../client/gui/util/script/MethodBlock.java | 32 ++- 3 files changed, 257 insertions(+), 8 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 eb722ff67..314f3ced6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -7,6 +7,7 @@ import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.JavaTextContainer; +import noppes.npcs.client.gui.util.script.MethodBlock; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; @@ -120,6 +121,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { List list = new ArrayList<>(container.lines); + // Get cached brace spans from container + List braceSpans = container.getBraceSpans(); + String wordHightLight = null; if (startSelection != endSelection) { Matcher m = container.regexWord.matcher(text); @@ -168,6 +172,40 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } int yPos = y + (i - scrolledLine) * container.lineHeight + 1; + int visStart = scrolledLine; + int visEnd = Math.min(list.size() - 1, scrolledLine + container.visibleLines - 1); + if (!braceSpans.isEmpty() && i == visStart) { + // compute maximum level in visible spans + int maxLevel = 0; + for (int[] s : braceSpans) { + if (s[2] < visStart || s[1] > visEnd) continue; + maxLevel = Math.max(maxLevel, s[0]); + } + for (int lvl = 1; lvl <= maxLevel; lvl++) { + int gx = x + 6 + (lvl - 1) * 8; + int spanStart = -1; + for (int li = visStart; li <= visEnd + 1; li++) { + boolean has = false; + if (li <= visEnd && li >= 0) { + for (int[] s : braceSpans) { + if (s[0] != lvl) continue; + if (li >= s[1] && li <= s[2]) { has = true; break; } + } + } + if (has) { + if (spanStart == -1) spanStart = li; + } else { + if (spanStart != -1) { + int topY = y + (spanStart - scrolledLine) * container.lineHeight + 3; + int bottomY = y + (li - 1 - scrolledLine) * container.lineHeight + container.lineHeight - 2; + drawRect(gx, topY, gx + 2, bottomY, 0x33FFFFFF); + spanStart = -1; + } + } + } + } + } + data.drawString(x + 1, yPos, 0xFFe0e0e0); if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { @@ -263,6 +301,19 @@ public class GuiScriptTextArea extends GuiNpcTextField { return this.container.text.length(); } + private int getLineIndex(List lines, 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); + } + + @Override public boolean textboxKeyTyped(char c, int i) { if (!active) 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++) { From caab5da3bc48ab6f6fed470b0f84bf1a679ae743 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 20:20:31 +0200 Subject: [PATCH 2/4] Having cursor after braces also highlights them --- .../client/gui/util/GuiScriptTextArea.java | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 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 db70bb46c..e97e24c31 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -95,26 +95,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrolledLine = Math.min(Math.max((int) (1f * diff * (yMouse - y) / height), 0), diff); } int startBracket = 0, endBracket = 0; - if (startSelection >= 0 && startSelection < text.length() && + if (startSelection >= 0 && text != null && text.length() > 0 && (endSelection - startSelection == 1 || startSelection == endSelection)) { - char c = text.charAt(startSelection); - int found = 0; - if (c == '{') { - found = findClosingBracket(text.substring(startSelection), '{', '}'); - } else if (c == '[') { - found = findClosingBracket(text.substring(startSelection), '[', ']'); - } else if (c == '(') { - found = findClosingBracket(text.substring(startSelection), '(', ')'); - } else if (c == '}') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '{', '}'); - } else if (c == ']') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '[', ']'); - } else if (c == ')') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '(', ')'); - } - if (found != 0) { - startBracket = startSelection; - endBracket = startSelection + found; + int[] span = findBracketSpanAt(startSelection); + if (span != null) { + startBracket = span[0]; + endBracket = span[1]; } } @@ -368,7 +354,41 @@ public class GuiScriptTextArea extends GuiNpcTextField { return spans; } + // Find a bracket span for a given cursor position. Returns {start, end} or null. + 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}; + } + + // Then try immediately before pos (caret right after a char) + if (pos > 0 && pos - 1 < text.length()) { + int probe = pos - 1; + char c2 = text.charAt(probe); + int found2 = 0; + if (c2 == '{') found2 = findClosingBracket(text.substring(probe), '{', '}'); + else if (c2 == '[') found2 = findClosingBracket(text.substring(probe), '[', ']'); + else if (c2 == '(') found2 = findClosingBracket(text.substring(probe), '(', ')'); + else if (c2 == '}') found2 = findOpeningBracket(text.substring(0, probe + 1), '{', '}'); + else if (c2 == ']') found2 = findOpeningBracket(text.substring(0, probe + 1), '[', ']'); + else if (c2 == ')') found2 = findOpeningBracket(text.substring(0, probe + 1), '(', ')'); + if (found2 != 0) return new int[]{probe, probe + found2}; + } + + return null; + } private int getSelectionPos(int xMouse, int yMouse) { + + xMouse -= this.x + 1; yMouse -= this.y + 1; ArrayList list = new ArrayList(this.container.lines); @@ -441,6 +461,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } + if (i == Keyboard.KEY_RIGHT) { int j = 1; if (isCtrlKeyDown()) { From cddf2c2d60ffac336269fbe57f51f49b55a1639d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 20:59:54 +0200 Subject: [PATCH 3/4] Improved {} scaffold creation logic + tab indent bug fixes --- .../client/gui/util/GuiScriptTextArea.java | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 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 e97e24c31..a88171f94 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -440,7 +440,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData currentLine = null; for (LineData line : this.container.lines) { - if (startSelection >= line.start && startSelection <= line.end) { + if (startSelection >= line.start && startSelection < line.end) { currentLine = line; break; } @@ -577,7 +577,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (startSelection > 0 && startSelection == endSelection) { LineData curr = null; for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { + if (cursorPosition >= line.start && cursorPosition < line.end) { curr = line; break; } @@ -687,29 +687,40 @@ public class GuiScriptTextArea extends GuiNpcTextField { String before = getSelectionBeforeText(); String after = getSelectionAfterText(); - // Find the next non-whitespace character and its indent to decide if this brace is already closed at the same scope. - int nextNonWsIndex = -1; - for (int idx = 0; idx < after.length(); idx++) { - char ch = after.charAt(idx); - if (!Character.isWhitespace(ch)) { - nextNonWsIndex = idx; - break; - } - } - + // Determine whether this opening brace already has a matching closing + // brace at the same scope (and indent). Prefer using the brace-span + // computation which skips strings/comments and handles nesting. boolean hasMatchingCloseSameIndent = false; - if (nextNonWsIndex >= 0 && after.charAt(nextNonWsIndex) == '}') { - // Compute indent of that closing brace - int lineStart = after.lastIndexOf('\n', nextNonWsIndex); - int indentLen = 0; - for (int k = lineStart + 1; k < after.length(); k++) { - char ch = after.charAt(k); - if (ch == ' ') indentLen++; else if (ch == '\t') indentLen += 4; else break; - if (k == nextNonWsIndex) break; + try { + // Find the line index containing the opening brace (cursorPosition-1) + int openLineIdx = -1; + int bracePos = cursorPosition - 1; + for (int li = 0; li < this.container.lines.size(); li++) { + LineData ld = this.container.lines.get(li); + if (bracePos >= ld.start && bracePos < ld.end) { + openLineIdx = li; + break; + } } - if (indentLen == indent.length()) { - hasMatchingCloseSameIndent = true; + + if (openLineIdx >= 0) { + List spans = computeBraceSpans(text, this.container.lines); + for (int[] span : spans) { + int spanOpen = span[1]; + int spanClose = span[2]; + if (spanOpen == openLineIdx) { + // Found a matching close; check its indent equals current indent + int closeIndent = getLineIndent(this.container.lines.get(spanClose).text); + if (closeIndent == indent.length()) { + hasMatchingCloseSameIndent = true; + } + break; + } + } } + } catch (Exception ex) { + // Fallback: if anything goes wrong, conservatively behave as before + hasMatchingCloseSameIndent = false; } if (hasMatchingCloseSameIndent) { @@ -795,7 +806,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } else { // Single line duplication (existing logic) for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { + if (cursorPosition >= line.start && cursorPosition < line.end) { int lineStart = line.start, lineEnd = line.end; String lineText = text.substring(lineStart, lineEnd); String insertText = "\n" + lineText; @@ -972,7 +983,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private String getAutoIndentForEnter() { LineData currentLine = null; for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { currentLine = line; break; } @@ -1116,7 +1127,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleTab() { LineData currentLine = null; for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { currentLine = line; break; } @@ -1175,7 +1186,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleShiftTab() { LineData currentLine = null; for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { currentLine = line; break; } From a9a2ce368c4b4ba32a6550901a20a62d3440db54 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 21:59:35 +0200 Subject: [PATCH 4/4] brain hurt probably temp gonna reroll this back --- .../client/gui/util/GuiScriptTextArea.java | 137 ++++++++++-------- 1 file changed, 79 insertions(+), 58 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 a88171f94..a37444c0a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -532,49 +532,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_BACK) { if (startSelection == endSelection && startSelection > 0) { - // If the character before the cursor is a newline, remove that single newline. - // This handles backspacing into empty lines (joining the previous and current lines) - // without relying on LineData start/end calculations. - if (startSelection - 1 >= 0 && startSelection - 1 < text.length() && text.charAt(startSelection - 1) == '\n') { - String before = text.substring(0, startSelection - 1); - String after = startSelection < text.length() ? text.substring(startSelection) : ""; - setText(before + after); - endSelection = cursorPosition = startSelection = startSelection - 1; - return true; - - } - // Check if we should remove an empty/whitespace-only line - if (currentLine != null) { - // Check if cursor is at line start or after only whitespace - String beforeCursor = text.substring(currentLine.start, startSelection); - boolean atLineStartOrWhitespace = beforeCursor.trim().isEmpty(); - - // Check if line contains only whitespace - boolean lineOnlyWhitespace = currentLine.text.trim().isEmpty(); - - if (atLineStartOrWhitespace && lineOnlyWhitespace && currentLine.start > 0) { - // Remove the empty/whitespace-only line by deleting the substring - // from the start of this line up to and including the next newline (if any). - int removeStart = currentLine.start; - int removeEnd = text.indexOf('\n', removeStart); - if (removeEnd == -1) { - removeEnd = text.length(); - } else { - removeEnd = removeEnd + 1; // include the newline - } - String before = text.substring(0, removeStart); - String after = removeEnd > text.length() ? "" : text.substring(removeEnd); - setText(before + after); - // Place cursor at end of previous line (just before where the removed newline was) - int newCursor = Math.max(0, removeStart - 1); - endSelection = cursorPosition = startSelection = newCursor; - return true; - } - } - } - - String s = getSelectionBeforeText(); - if (startSelection > 0 && startSelection == endSelection) { LineData curr = null; for (LineData line : container.lines) { if (cursorPosition >= line.start && cursorPosition < line.end) { @@ -585,24 +542,56 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (curr != null) { int col = cursorPosition - curr.start; int indent = getLineIndent(curr.text); - if (col <= indent && startSelection - 1 >= 0 && text.charAt(startSelection - 1) == '\n') { - // At or before indent: jump to previous line indent by removing newline - LineData prev = null; - int idx = container.lines.indexOf(curr); - if (idx > 0) - prev = container.lines.get(idx - 1); - String before = text.substring(0, startSelection - 1); - String after = startSelection < text.length() ? text.substring(startSelection) : ""; - setText(before + after); - int newCursor = startSelection - 1; - if (prev != null) { - int prevIndent = getLineIndent(prev.text); - newCursor = prev.start + Math.min(prevIndent, Math.max(0, prev.text.length())); + int expectedIndent = computeExpectedIndent(curr); + // If caret is at or before the indent, merge with previous line and place + // caret at that line's indent. + if (col <= expectedIndent) { + int prevNewlineIdx = text.lastIndexOf('\n', curr.start - 1); + if (prevNewlineIdx >= 0) { + // Determine previous line bounds/text + int prevLineStart = text.lastIndexOf('\n', Math.max(0, prevNewlineIdx - 1)); + if (prevLineStart == -1) + prevLineStart = 0; + else + prevLineStart += 1; + + String prevLineText = text.substring(prevLineStart, prevNewlineIdx); + int prevIndent = getLineIndent(prevLineText); + + // Current line text without leading whitespace + int currLineEnd = text.indexOf('\n', curr.start); + if (currLineEnd == -1) + currLineEnd = text.length(); + String currLineText = text.substring(curr.start, currLineEnd); + String currTrimmed = currLineText.replaceFirst("^[ \\t]+", ""); + + // Build merged text: drop the newline and the current line's leading indent. + String before = text.substring(0, prevNewlineIdx); + boolean needSpace = !prevLineText.isEmpty() && !currTrimmed.isEmpty() && !Character.isWhitespace( + prevLineText.charAt(prevLineText.length() - 1)); + String after = text.substring(currLineEnd); + setText(before + (needSpace ? " " : "") + currTrimmed + after); + + int newCursor = prevLineStart + Math.min(prevIndent, Math.max(0, prevLineText.length())); + endSelection = cursorPosition = startSelection = prevLineStart + prevLineText.length();// Math.max(0, newCursor); + return true; } - endSelection = cursorPosition = startSelection = Math.max(0, newCursor); - return true; + } else { //BREAKS SINGLE CHARACTER BACK +// // Delete backwards until we reach the expected indent for this line +// int targetPos = Math.max(0, curr.start + expectedIndent); +// if (targetPos < cursorPosition) { +// String before = text.substring(0, targetPos); +// String after = text.substring(cursorPosition); +// setText(before + after); +// startSelection = endSelection = cursorPosition = Math.min(targetPos, text.length()); +// } +// return true; } } + } + + String s = getSelectionBeforeText(); + if (startSelection > 0 && startSelection == endSelection) { // If deleting an opening bracket/quote and the immediate next char is the matching closer, // remove both so the auto-inserted closer doesn't remain orphaned. if (startSelection > 0 && startSelection < text.length()) { @@ -1028,6 +1017,38 @@ public class GuiScriptTextArea extends GuiNpcTextField { return leading; } + // Estimate the logical indent for a line based on surrounding lines and braces, + // rather than its current raw whitespace (which may be over-indented). + private int computeExpectedIndent(LineData line) { + int tab = getTabSize(); + int idx = container.lines.indexOf(line); + if (idx < 0) + return getLineIndent(line.text); + + // Find previous non-empty line + int prevIdx = idx - 1; + while (prevIdx >= 0 && container.lines.get(prevIdx).text.trim().isEmpty()) { + prevIdx--; + } + + int baseIndent = 0; + if (prevIdx >= 0) { + LineData prev = container.lines.get(prevIdx); + String prevTrim = prev.text.trim(); + baseIndent = getLineIndent(prev.text); + if (!prevTrim.isEmpty() && prevTrim.endsWith("{")) { + baseIndent += tab; + } + } + + String currTrim = line.text.trim(); + if (!currTrim.isEmpty() && currTrim.startsWith("}")) { + baseIndent = Math.max(0, baseIndent - tab); + } + + return Math.max(0, baseIndent); + } + private int getTabSize() { return 4; }