CRAZY GuiScriptTextArea MODULATION (W OPUS 4.5)

This commit is contained in:
bigguy345 committed 2025-12-18 22:11:42 +02:00
1 parent 6373839870
commit 79edd08ff4
8 files changed
+1571 -488

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -0,0 +1,252 @@
package noppes.npcs.client.gui.util.script;
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
import java.util.ArrayList;
import java.util.List;
/**
* Handles bracket matching, brace span computation, and indent guide generation.
*/
public class BracketMatcher {
/**
* Find matching bracket at cursor position.
* @return int[2] with {startPos, endPos} or null if no bracket found
*/
public static int[] findBracketSpanAt(String text, int pos) {
if (text == null || text.isEmpty()) return null;
// Try at current position
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, skipping whitespace on same line
if (pos > 0) {
int scan = pos - 1;
while (scan >= 0) {
char sc = text.charAt(scan);
if (sc == ' ' || sc == '\t') { scan--; continue; }
if (sc == '\n') break; // Don't cross line boundary
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};
break; // Not a bracket; stop
}
}
return null;
}
/**
* Find closing bracket, returns offset from start or 0 if not found
*/
public static int findClosingBracket(String str, char open, char close) {
int depth = 0;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == open) depth++;
else if (c == close) {
depth--;
if (depth == 0) return i;
}
}
return 0;
}
/**
* Find opening bracket, returns negative offset from end or 0 if not found
*/
public static int findOpeningBracket(String str, char open, char close) {
int depth = 0;
for (int i = str.length() - 1; i >= 0; i--) {
char c = str.charAt(i);
if (c == close) depth++;
else if (c == open) {
depth--;
if (depth == 0) return i - str.length() + 1;
}
}
return 0;
}
/**
* Compute brace spans for indent guides.
* Returns list of int[4]: {originalDepth, openLineIdx, closeLineIdx, adjustedDepth}
*/
public static List<int[]> computeBraceSpans(String text, List<LineData> lines) {
List<int[]> spans = new ArrayList<>();
if (text == null || text.isEmpty() || lines == null || lines.isEmpty())
return spans;
int lineIdx = 0;
int lineEnd = lines.get(0).end;
List<Integer> openStack = new ArrayList<>();
boolean inLineComment = false;
boolean inBlockComment = false;
boolean inString = false;
boolean escape = false;
char stringDelimiter = 0;
for (int pos = 0; pos < text.length(); pos++) {
// Advance line index
while (lineIdx < lines.size() - 1 && pos >= lineEnd) {
lineIdx++;
lineEnd = lines.get(lineIdx).end;
}
char c = text.charAt(pos);
char next = pos + 1 < text.length() ? text.charAt(pos + 1) : 0;
// String handling
if (inString) {
if (escape) { escape = false; }
else if (c == '\\') { escape = true; }
else if (c == stringDelimiter) { inString = false; }
else if (c == '\n') { inString = false; } // Unterminated string
continue;
}
// Block comment handling
if (inBlockComment) {
if (c == '*' && next == '/') { inBlockComment = false; pos++; }
continue;
}
// Line comment handling
if (inLineComment) {
if (c == '\n') { inLineComment = false; }
continue;
}
// Start of comments
if (c == '/' && next == '/') { inLineComment = true; pos++; continue; }
if (c == '/' && next == '*') { inBlockComment = true; pos++; continue; }
// Start of string
if (c == '"' || c == '\'') {
inString = true;
stringDelimiter = c;
escape = false;
continue;
}
// Brace matching
if (c == '{') {
openStack.add(lineIdx);
} else if (c == '}') {
if (!openStack.isEmpty()) {
int openLine = openStack.remove(openStack.size() - 1);
int spanDepth = openStack.size() + 1;
spans.add(new int[]{spanDepth, openLine, lineIdx});
}
}
}
// Normalize depths for unmatched braces
if (!openStack.isEmpty()) {
int baseline = openStack.size();
List<int[]> 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});
}
return adjusted;
} else {
for (int i = 0; i < spans.size(); i++) {
int[] span = spans.get(i);
spans.set(i, new int[]{span[0], span[1], span[2], span[0]});
}
}
return spans;
}
/**
* Find which brace span should be highlighted based on bracket at position
* @return int[2] {openLine, closeLine} or null
*/
public static int[] findHighlightedSpan(int bracketPos, List<LineData> lines, List<int[]> spans) {
if (bracketPos < 0 || lines == null || spans == null) return null;
// Find line containing bracket
int bracketLineIdx = -1;
for (int i = 0; i < lines.size(); i++) {
LineData ld = lines.get(i);
if (bracketPos >= ld.start && bracketPos < ld.end) {
bracketLineIdx = i;
break;
}
}
if (bracketLineIdx >= 0) {
for (int[] span : spans) {
int openLine = span[1];
int closeLine = span[2];
if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) {
return new int[]{openLine, closeLine};
}
}
}
return null;
}
/**
* Check if a brace at given position has a matching close with same indent
*/
public static boolean hasMatchingCloseWithSameIndent(String text, int bracePos, List<LineData> lines, int currentIndent) {
if (text == null || lines == null || bracePos < 0) return false;
// Find line containing the brace
int openLineIdx = -1;
for (int i = 0; i < lines.size(); i++) {
LineData ld = lines.get(i);
if (bracePos >= ld.start && bracePos < ld.end) {
openLineIdx = i;
break;
}
}
if (openLineIdx < 0) return false;
List<int[]> spans = computeBraceSpans(text, lines);
for (int[] span : spans) {
if (span[1] == openLineIdx) {
int closeLineIdx = span[2];
if (closeLineIdx < lines.size()) {
String closeLine = lines.get(closeLineIdx).text;
int closeIndent = getLineIndent(closeLine);
return closeIndent == currentIndent;
}
}
}
return false;
}
/**
* Get leading whitespace count of a line
*/
private static int getLineIndent(String line) {
int indent = 0;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == ' ' || c == '\t') indent++;
else break;
}
return indent;
}
}
@@ -0,0 +1,217 @@
package noppes.npcs.client.gui.util.script;
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
import java.util.List;
/**
* Handles line comment toggling for the script editor.
*/
public class CommentHandler {
/**
* Result of comment toggle operation
*/
public static class ToggleResult {
public final String text;
public final int delta; // Change in length (negative if comment removed)
public final int nonWsIndex; // Index of first non-whitespace character
public ToggleResult(String text, int delta, int nonWsIndex) {
this.text = text;
this.delta = delta;
this.nonWsIndex = nonWsIndex;
}
public boolean wasCommented() {
return delta < 0;
}
}
/**
* Toggle line comment on a single line
*/
public static ToggleResult toggleLine(String lineText) {
// Find first non-whitespace
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 newText;
if (hasComment) {
// Remove comment
int cut = Math.min(nonWs + 2, lineText.length());
newText = lineText.substring(0, nonWs) + lineText.substring(cut);
} else if (hasContent) {
// Add comment
newText = lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs);
} else {
// Empty/whitespace line - no change
newText = lineText;
}
return new ToggleResult(newText, newText.length() - lineText.length(), nonWs);
}
/**
* Result of toggling comments on a selection
*/
public static class SelectionToggleResult {
public final String newText;
public final int newStartSelection;
public final int newEndSelection;
public SelectionToggleResult(String text, int start, int end) {
this.newText = text;
this.newStartSelection = start;
this.newEndSelection = end;
}
}
/**
* Toggle comments on all lines in a selection
*/
public static SelectionToggleResult toggleCommentSelection(
String text,
List<LineData> lines,
int startSelection,
int endSelection) {
StringBuilder newText = new StringBuilder(text.length() + 100);
int prevEnd = 0;
int newStart = -1;
int newEnd = -1;
int newIndex = 0;
for (LineData line : lines) {
int safeLineStart = Math.max(0, Math.min(line.start, text.length()));
int safeLineEnd = Math.max(0, Math.min(line.end, text.length()));
// Handle gap between lines (shouldn't happen normally)
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;
}
// Check if this line is in selection
if (safeLineEnd > startSelection && safeLineStart < endSelection) {
String lineText = text.substring(safeLineStart, safeLineEnd);
ToggleResult tr = toggleLine(lineText);
String newLineText = tr.text;
int nonWs = tr.nonWsIndex;
int delta = tr.delta;
// Update start selection position
if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) {
int offsetInOld = startSelection - safeLineStart;
int newOffset = offsetInOld;
if (offsetInOld >= nonWs) {
newOffset = offsetInOld + delta;
}
newStart = newIndex + Math.max(0, newOffset);
}
// Update end selection position
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 {
// Line not in selection - keep unchanged
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;
}
// Handle remaining text after last line
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;
}
// Fallback for positions not found
if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length()));
if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length()));
return new SelectionToggleResult(newText.toString(), newStart, newEnd);
}
/**
* Toggle comment on a single line at cursor position
*/
public static SingleLineToggleResult toggleCommentAtCursor(
String text,
List<LineData> lines,
int cursorPosition) {
for (LineData line : 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);
ToggleResult tr = toggleLine(lineText);
boolean hadComment = tr.delta < 0;
String before = text.substring(0, lineStart);
String after = lineEnd <= text.length() ? text.substring(lineEnd) : "";
String newText = before + tr.text + after;
// Calculate new cursor position
int cursorDelta = hadComment ? -2 : 2;
int newCursor = cursorPosition + cursorDelta;
if (cursorPosition < lineStart + tr.nonWsIndex + (hadComment ? 2 : 0)) {
newCursor = cursorPosition;
}
newCursor = Math.max(lineStart, newCursor);
return new SingleLineToggleResult(newText, newCursor);
}
}
// No line found at cursor - return unchanged
return new SingleLineToggleResult(text, cursorPosition);
}
/**
* Result of toggling comment on single line
*/
public static class SingleLineToggleResult {
public final String newText;
public final int newCursorPosition;
public SingleLineToggleResult(String text, int cursor) {
this.newText = text;
this.newCursorPosition = cursor;
}
}
}
@@ -0,0 +1,217 @@
package noppes.npcs.client.gui.util.script;
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Handles cursor navigation logic (up, down, left, right, word movement).
*/
public class CursorNavigation {
/**
* Move cursor up one line, maintaining column position
*/
public static int cursorUp(int cursorPosition, List<LineData> lines, String text) {
if (lines == null || lines.isEmpty()) return 0;
for (int i = 0; i < lines.size(); i++) {
LineData data = lines.get(i);
boolean isLastLine = (i == lines.size() - 1);
boolean isOnLine = isLastLine
? (cursorPosition >= data.start && cursorPosition <= data.end)
: (cursorPosition >= data.start && cursorPosition < data.end);
if (isOnLine) {
if (i == 0) {
return 0; // Already on first line
}
int column = Math.min(cursorPosition - data.start, data.text.length());
LineData target = lines.get(i - 1);
int targetColumn = Math.min(column, target.text.length());
int targetIndent = getLineIndent(target.text);
int minPos = target.start + Math.min(targetIndent, target.text.length());
int targetPos = target.start + targetColumn;
return Math.max(minPos, targetPos);
}
}
return 0;
}
/**
* Move cursor down one line, maintaining column position
*/
public static int cursorDown(int cursorPosition, List<LineData> lines, String text) {
if (lines == null || lines.isEmpty()) return text != null ? text.length() : 0;
if (text != null && cursorPosition == text.length()) return cursorPosition;
for (int i = 0; i < lines.size(); i++) {
LineData data = lines.get(i);
boolean isLastLine = (i == lines.size() - 1);
boolean isOnLine = isLastLine
? (cursorPosition >= data.start && cursorPosition <= data.end)
: (cursorPosition >= data.start && cursorPosition < data.end);
if (isOnLine) {
if (i >= lines.size() - 1) {
return cursorPosition; // Already on last line
}
int column = Math.min(cursorPosition - data.start, data.text.length());
LineData target = lines.get(i + 1);
int targetColumn = Math.min(column, target.text.length());
int targetIndent = getLineIndent(target.text);
int minPos = target.start + Math.min(targetIndent, target.text.length());
int targetPos = target.start + targetColumn;
return Math.max(minPos, targetPos);
}
}
return text != null ? text.length() : 0;
}
/**
* Move cursor left, optionally by word
*/
public static int cursorLeft(int cursorPosition, String text, Pattern wordPattern, boolean byWord) {
if (text == null || cursorPosition <= 0) return 0;
int moveAmount = 1;
if (byWord && wordPattern != null) {
Matcher m = wordPattern.matcher(text.substring(0, cursorPosition));
while (m.find()) {
if (m.start() != m.end()) {
moveAmount = cursorPosition - m.start();
}
}
}
return Math.max(cursorPosition - moveAmount, 0);
}
/**
* Move cursor right, optionally by word
*/
public static int cursorRight(int cursorPosition, String text, Pattern wordPattern, boolean byWord) {
if (text == null) return 0;
int textLen = text.length();
if (cursorPosition >= textLen) return textLen;
int moveAmount = 1;
if (byWord && wordPattern != null) {
Matcher m = wordPattern.matcher(text.substring(cursorPosition));
if (m.find() && m.start() > 0) {
moveAmount = m.start();
} else if (m.find()) {
moveAmount = m.start();
}
}
return Math.min(cursorPosition + moveAmount, textLen);
}
/**
* Get cursor position from mouse click coordinates
*/
public static int getPositionFromMouse(
int mouseX, int mouseY,
int areaX, int areaY,
int gutterWidth,
int lineHeight,
int scrolledLine,
double scrollFracOffset,
List<LineData> lines,
String text,
FontWidthProvider fontWidth) {
int xMouse = mouseX - (areaX + gutterWidth + 1);
int yMouse = mouseY - areaY - 1;
// Account for fractional scroll offset
double fracPixels = scrollFracOffset * lineHeight;
yMouse = (int) Math.round(yMouse + fracPixels);
if (lines == null || lines.isEmpty()) {
return text != null ? text.length() : 0;
}
int visibleLines = Math.min(lines.size(), scrolledLine + 50); // Reasonable limit
for (int i = 0; i < lines.size(); i++) {
LineData data = lines.get(i);
if (i >= scrolledLine && i <= visibleLines) {
int yPos = (i - scrolledLine) * lineHeight;
if (yMouse >= yPos && yMouse < yPos + lineHeight) {
// Found the line, now find character position
int lineWidth = 0;
char[] chars = data.text.toCharArray();
for (int j = 1; j <= chars.length; j++) {
int w = fontWidth.getWidth(data.text.substring(0, j));
if (xMouse < lineWidth + (w - lineWidth) / 2) {
return data.start + j - 1;
}
lineWidth = w;
}
// Click past end of line - place cursor at end of text (not on newline)
int posAfterChars = data.start + chars.length;
return Math.min(posAfterChars, text.length());
}
}
}
return text != null ? text.length() : 0;
}
/**
* Interface for font width measurement
*/
public interface FontWidthProvider {
int getWidth(String text);
}
/**
* Get leading whitespace count of a line
*/
private static int getLineIndent(String lineText) {
int indent = 0;
for (int i = 0; i < lineText.length(); i++) {
char c = lineText.charAt(i);
if (c == ' ' || c == '\t') indent++;
else break;
}
return indent;
}
/**
* Find line index containing given position
*/
public static int findLineIndex(int position, List<LineData> lines) {
if (lines == null || lines.isEmpty()) return 0;
for (int i = 0; i < lines.size(); i++) {
LineData line = lines.get(i);
boolean isLastLine = (i == lines.size() - 1);
boolean isOnLine = isLastLine
? (position >= line.start && position <= line.end)
: (position >= line.start && position < line.end);
if (isOnLine) {
return i;
}
}
return lines.size() - 1;
}
/**
* Find line data containing given position
*/
public static LineData findLineAt(int position, List<LineData> lines) {
int idx = findLineIndex(position, lines);
if (idx >= 0 && idx < lines.size()) {
return lines.get(idx);
}
return null;
}
}
@@ -0,0 +1,246 @@
package noppes.npcs.client.gui.util.script;
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
import java.util.Arrays;
import java.util.List;
/**
* Helper class for indentation-related operations.
*/
public class IndentHelper {
public static final int TAB_SIZE = 4;
/**
* Get leading whitespace count of a line
*/
public static int getLineIndent(String lineText) {
int leading = 0;
for (int i = 0; i < lineText.length(); i++) {
char c = lineText.charAt(i);
if (c == ' ' || c == '\t') leading++;
else break;
}
return leading;
}
/**
* Extract leading whitespace as string
*/
public static String getIndentString(String lineText) {
int indent = getLineIndent(lineText);
return lineText.substring(0, indent);
}
/**
* Create a string of n spaces
*/
public static String spaces(int n) {
if (n <= 0) return "";
char[] arr = new char[n];
Arrays.fill(arr, ' ');
return new String(arr);
}
/**
* Calculate expected indent for a line based on context
*/
public static int getExpectedIndent(LineData currentLine, List<LineData> allLines) {
if (currentLine == null || allLines == null) return 0;
int idx = allLines.indexOf(currentLine);
if (idx <= 0) return 0;
// Find previous non-empty line
LineData prevLine = null;
for (int i = idx - 1; i >= 0; i--) {
LineData line = allLines.get(i);
if (line.text.trim().length() > 0) {
prevLine = line;
break;
}
}
if (prevLine == null) return 0;
int prevIndent = getLineIndent(prevLine.text);
String prevTrimmed = prevLine.text.trim();
String currTrimmed = currentLine.text.trim();
// If current line starts with }, expected is one level back
if (currTrimmed.startsWith("}")) {
return Math.max(0, prevIndent - TAB_SIZE);
}
// If previous line ends with {, expected is one level forward
if (prevTrimmed.endsWith("{")) {
return prevIndent + TAB_SIZE;
}
return prevIndent;
}
/**
* Calculate auto-indent for Enter key
*/
public static String getAutoIndentForEnter(String lineText, int cursorInLine) {
int leading = 0;
while (leading < lineText.length() &&
(lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) {
leading++;
}
String baseIndent = lineText.substring(0, leading);
int relativeCursor = Math.max(0, Math.min(cursorInLine, lineText.length()));
String beforeCursor = lineText.substring(0, relativeCursor);
// Find last non-whitespace before cursor
int lastNonWs = -1;
for (int i = beforeCursor.length() - 1; i >= 0; i--) {
if (!Character.isWhitespace(beforeCursor.charAt(i))) {
lastNonWs = i;
break;
}
}
// Add extra indent if last char is {
// Only add extra indent if cursor is actually after the opening brace (not just in trailing whitespace)
// Check that the last non-whitespace is at or after the line's indent position
boolean opensBlock = lastNonWs >= 0 && lastNonWs >= leading && beforeCursor.charAt(lastNonWs) == '{';
if (opensBlock) {
return baseIndent + spaces(TAB_SIZE);
}
return baseIndent;
}
/**
* Calculate target indent for Tab key
*/
public static int calculateTabTargetIndent(int currentIndent, int cursorColumn, boolean atTextStart) {
if (atTextStart) {
// At text start, move to next tab stop
return ((currentIndent / TAB_SIZE) + 1) * TAB_SIZE;
} else {
// Inside indent, snap to nearest tab stop
int remainder = currentIndent % TAB_SIZE;
if (remainder == 0) {
return currentIndent + TAB_SIZE;
} else {
int down = currentIndent - remainder;
int up = currentIndent + (TAB_SIZE - remainder);
int distDown = remainder;
int distUp = TAB_SIZE - remainder;
if (distUp <= distDown) return up;
else return down;
}
}
}
/**
* Calculate target indent for Shift+Tab
*/
public static int calculateShiftTabTargetIndent(int currentIndent) {
return Math.max(0, ((currentIndent - 1) / TAB_SIZE) * TAB_SIZE);
}
/**
* Format entire text with proper indentation
*/
public static FormatResult formatText(String text, int cursorPosition) {
String[] linesArr = text.split("\n", -1);
StringBuilder out = new StringBuilder(text.length() + 32);
// Track cursor position in formatted output
int cursorLine = -1;
int cursorColInContent = 0;
int lineStartPos = 0;
// Find cursor's line and column
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;
}
boolean inString = false;
boolean escape = false;
boolean inBlockComment = false;
int depth = 0;
int newCursorPos = 0;
for (int li = 0; li < linesArr.length; li++) {
String line = linesArr[li];
String trimmedLeading = line.replaceAll("^[ \\t]+", "");
int opens = 0, closes = 0;
boolean startsWithClose = false;
// Parse line for braces
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;
if (c == '/' && next == '*') { inBlockComment = true; idx++; continue; }
if (c == '"') { inString = true; escape = false; continue; }
if (c == '{') {
opens++;
} else if (c == '}') {
closes++;
if (!startsWithClose && line.substring(0, idx).trim().isEmpty()) {
startsWithClose = true;
}
}
}
int indentLevel = depth;
if (startsWithClose) indentLevel = Math.max(0, indentLevel - 1);
int targetIndent = indentLevel * TAB_SIZE;
if (li == cursorLine) {
newCursorPos = out.length() + targetIndent + Math.min(cursorColInContent, trimmedLeading.length());
}
out.append(spaces(targetIndent)).append(trimmedLeading);
if (li < linesArr.length - 1) out.append('\n');
depth = Math.max(0, depth + opens - closes);
}
return new FormatResult(out.toString(), newCursorPos);
}
/**
* Result of text formatting
*/
public static class FormatResult {
public final String text;
public final int cursorPosition;
public FormatResult(String text, int cursorPosition) {
this.text = text;
this.cursorPosition = cursorPosition;
}
}
}
@@ -0,0 +1,153 @@
package noppes.npcs.client.gui.util.script;
/**
* Manages smooth scrolling state and animation for the script text area.
* Uses exponential smoothing for fluid scroll animations.
*/
public class ScrollState {
// Current fractional scroll position (in lines)
private double scrollPos = -1.0;
// Target scroll position to animate towards
private double targetScroll = 0.0;
// Current scroll velocity for momentum calculations
private double scrollVelocity = 0.0;
// Last timestamp for delta time calculation
private long lastScrollTime = 0L;
// Integer line index for compatibility with rendering
private int scrolledLine = 0;
// Scrollbar drag offset when dragging thumb
private int scrollbarDragOffset = 0;
// Whether currently dragging the scrollbar
private boolean clickScrolling = false;
// Animation parameters
private static final double TAU = 0.1; // Time constant (~55ms feels snappy)
private static final double SNAP_THRESHOLD = 0.01; // Snap to target when this close
private static final double MAX_DT = 0.05; // Max delta time for stability
/**
* Reset scroll state to initial values
*/
public void reset() {
scrollPos = 0.0;
targetScroll = 0.0;
scrollVelocity = 0.0;
scrolledLine = 0;
lastScrollTime = System.currentTimeMillis();
}
/**
* Initialize scroll position if not already initialized
*/
public void initializeIfNeeded(int currentLine) {
if (scrollPos < 0) {
scrollPos = currentLine;
lastScrollTime = System.currentTimeMillis();
}
}
/**
* Update scroll animation. Call once per frame.
* @param maxScroll Maximum valid scroll position
*/
public void update(int maxScroll) {
long nowMs = System.currentTimeMillis();
double dt = Math.min(MAX_DT, (nowMs - lastScrollTime) / 1000.0);
lastScrollTime = nowMs;
double dist = targetScroll - scrollPos;
// Snap to target if close enough
if (Math.abs(dist) < SNAP_THRESHOLD) {
scrollPos = targetScroll;
scrollVelocity = 0.0;
} else {
// Exponential smoothing
double alpha = 1.0 - Math.exp(-dt / Math.max(1e-6, TAU));
double prev = scrollPos;
scrollPos += dist * alpha;
scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6);
// Clamp overshoot
if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) {
scrollPos = targetScroll;
scrollVelocity = 0.0;
}
}
// Keep in bounds
clampToBounds(maxScroll);
// Update integer line for rendering
scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll));
}
/**
* Clamp scroll position to valid bounds
*/
public void clampToBounds(int maxScroll) {
if (scrollPos < 0) scrollPos = 0;
if (scrollPos > maxScroll) scrollPos = maxScroll;
if (targetScroll < 0) targetScroll = 0;
if (targetScroll > maxScroll) targetScroll = maxScroll;
scrolledLine = Math.max(0, Math.min(scrolledLine, maxScroll));
}
/**
* Apply mouse wheel scroll
* @param wheelDelta Positive = scroll up, negative = scroll down
* @param maxScroll Maximum valid scroll position
*/
public void applyWheelScroll(int wheelDelta, int maxScroll) {
double sign = Math.copySign(1, wheelDelta);
targetScroll -= sign * 2.0; // 2 lines per wheel tick
clampToBounds(maxScroll);
}
/**
* Set target scroll directly (for scrollbar dragging)
*/
public void setTargetScroll(double target, int maxScroll) {
targetScroll = Math.max(0, Math.min(target, maxScroll));
}
/**
* Scroll to make a specific line visible
* @param lineIdx Line index to make visible
* @param visibleLines Number of visible lines in viewport
* @param maxScroll Maximum scroll position
*/
public void scrollToLine(int lineIdx, int visibleLines, int maxScroll) {
int firstVisible = scrolledLine;
int lastFullyVisible = scrolledLine + visibleLines;
if (lineIdx < firstVisible) {
// Line is above viewport - scroll up
targetScroll = lineIdx;
} else if (lineIdx > lastFullyVisible) {
// Line is below viewport - scroll down
targetScroll = Math.min(lineIdx - visibleLines, maxScroll);
}
// If line is visible, don't change scroll
}
// Getters
public double getScrollPos() { return scrollPos; }
public double getTargetScroll() { return targetScroll; }
public int getScrolledLine() { return scrolledLine; }
public double getScrollVelocity() { return scrollVelocity; }
public boolean isClickScrolling() { return clickScrolling; }
public int getScrollbarDragOffset() { return scrollbarDragOffset; }
/**
* Get fractional offset for sub-pixel rendering
*/
public double getFractionalOffset() {
return scrollPos - scrolledLine;
}
// Setters for scrollbar interaction
public void setClickScrolling(boolean scrolling) { this.clickScrolling = scrolling; }
public void setScrollbarDragOffset(int offset) { this.scrollbarDragOffset = offset; }
public void setScrolledLine(int line) { this.scrolledLine = line; }
}
@@ -0,0 +1,192 @@
package noppes.npcs.client.gui.util.script;
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
import java.util.List;
/**
* Manages cursor position, text selection, and related operations.
*/
public class SelectionState {
private int startSelection = 0;
private int endSelection = 0;
private int cursorPosition = 0;
private long lastInputTime = 0L;
/**
* Reset selection to a single point
*/
public void reset(int position) {
startSelection = endSelection = cursorPosition = position;
}
/**
* Clamp all selection bounds to valid text range
*/
public void clamp(int textLength) {
if (textLength <= 0) {
startSelection = endSelection = cursorPosition = 0;
return;
}
startSelection = Math.max(0, Math.min(startSelection, textLength));
endSelection = Math.max(0, Math.min(endSelection, textLength));
cursorPosition = Math.max(0, Math.min(cursorPosition, textLength));
}
/**
* Set cursor position, optionally extending selection
*/
public void setCursor(int position, int textLength, boolean extendSelection) {
position = Math.max(0, Math.min(position, textLength));
if (position != cursorPosition) {
if (!extendSelection) {
endSelection = startSelection = cursorPosition = position;
} else {
int diff = cursorPosition - position;
if (cursorPosition == startSelection) {
startSelection -= diff;
} else if (cursorPosition == endSelection) {
endSelection -= diff;
}
// Ensure start <= end
if (startSelection > endSelection) {
int temp = endSelection;
endSelection = startSelection;
startSelection = temp;
}
cursorPosition = position;
}
clamp(textLength);
markActivity();
}
}
/**
* Select a range of text
*/
public void setSelection(int start, int end) {
startSelection = Math.min(start, end);
endSelection = Math.max(start, end);
cursorPosition = endSelection;
}
/**
* Select all text
*/
public void selectAll(int textLength) {
startSelection = cursorPosition = 0;
endSelection = textLength;
}
/**
* Check if there's an active selection (not just cursor)
*/
public boolean hasSelection() {
return startSelection != endSelection;
}
/**
* Get selected text
*/
public String getSelectedText(String text) {
if (!hasSelection() || text == null) return "";
int start = Math.max(0, Math.min(startSelection, text.length()));
int end = Math.max(0, Math.min(endSelection, text.length()));
return text.substring(start, end);
}
/**
* Get text before selection/cursor
*/
public String getTextBefore(String text) {
if (text == null || startSelection <= 0) return "";
return text.substring(0, Math.min(startSelection, text.length()));
}
/**
* Get text after selection/cursor
*/
public String getTextAfter(String text) {
if (text == null) return "";
int pos = Math.min(endSelection, text.length());
return text.substring(pos);
}
/**
* Find which line the cursor is on (0-indexed)
*/
public int getCursorLineIndex(List<LineData> lines, int textLength) {
if (lines == null || lines.isEmpty()) return 0;
for (int i = 0; i < lines.size(); i++) {
LineData ld = lines.get(i);
boolean isLastLine = (i == lines.size() - 1);
boolean isOnLine = isLastLine
? (cursorPosition >= ld.start && cursorPosition <= ld.end)
: (cursorPosition >= ld.start && cursorPosition < ld.end);
if (isOnLine) {
return i;
}
}
return lines.size() - 1;
}
/**
* Check if cursor is on a specific line, handling the last line specially
* (last line doesn't have trailing newline, so use <= for end boundary)
*/
public boolean isCursorOnLine(int lineIndex, LineData line, int totalLines) {
boolean isLastLine = (lineIndex == totalLines - 1);
return isLastLine
? (cursorPosition >= line.start && cursorPosition <= line.end)
: (cursorPosition >= line.start && cursorPosition < line.end);
}
/**
* Find line containing the cursor
*/
public LineData findCurrentLine(List<LineData> lines) {
if (lines == null) return null;
for (int i = 0; i < lines.size(); i++) {
LineData line = lines.get(i);
if (isCursorOnLine(i, line, lines.size())) {
return line;
}
}
return null;
}
/**
* Mark user activity (for cursor blink timing)
*/
public void markActivity() {
lastInputTime = System.currentTimeMillis();
}
/**
* Check if there was recent input (for cursor blink pause)
*/
public boolean hadRecentInput() {
return System.currentTimeMillis() - lastInputTime < 500;
}
// Getters
public int getStartSelection() { return startSelection; }
public int getEndSelection() { return endSelection; }
public int getCursorPosition() { return cursorPosition; }
public long getLastInputTime() { return lastInputTime; }
// Direct setters (use with caution, prefer higher-level methods)
public void setStartSelection(int pos) { this.startSelection = pos; }
public void setEndSelection(int pos) { this.endSelection = pos; }
public void setCursorPositionDirect(int pos) { this.cursorPosition = pos; }
/**
* After text modification, update selection to new position
*/
public void afterTextInsert(int newPosition) {
endSelection = startSelection = cursorPosition = newPosition;
markActivity();
}
}
@@ -0,0 +1,119 @@
package noppes.npcs.client.gui.util.script;
import java.util.ArrayList;
import java.util.List;
/**
* Manages undo/redo stacks for the text editor.
*/
public class UndoManager {
/**
* Stores a snapshot of text state for undo/redo
*/
public static class UndoState {
public final String text;
public final int cursorPosition;
public UndoState(String text, int cursorPosition) {
this.text = text;
this.cursorPosition = cursorPosition;
}
}
private final List<UndoState> undoStack = new ArrayList<>();
private final List<UndoState> redoStack = new ArrayList<>();
private boolean isUndoing = false;
// Optional: limit stack size to prevent memory issues
private static final int MAX_STACK_SIZE = 100;
/**
* Record current state before making a change
*/
public void recordState(String text, int cursorPosition) {
if (isUndoing) return;
undoStack.add(new UndoState(text, cursorPosition));
redoStack.clear();
// Trim stack if too large
while (undoStack.size() > MAX_STACK_SIZE) {
undoStack.remove(0);
}
}
/**
* Undo to previous state
* @param currentText Current text before undo
* @param currentCursor Current cursor position
* @return Previous state, or null if nothing to undo
*/
public UndoState undo(String currentText, int currentCursor) {
if (undoStack.isEmpty()) return null;
isUndoing = true;
redoStack.add(new UndoState(currentText, currentCursor));
UndoState state = undoStack.remove(undoStack.size() - 1);
isUndoing = false;
return state;
}
/**
* Redo to next state
* @param currentText Current text before redo
* @param currentCursor Current cursor position
* @return Next state, or null if nothing to redo
*/
public UndoState redo(String currentText, int currentCursor) {
if (redoStack.isEmpty()) return null;
isUndoing = true;
undoStack.add(new UndoState(currentText, currentCursor));
UndoState state = redoStack.remove(redoStack.size() - 1);
isUndoing = false;
return state;
}
/**
* Check if undo is available
*/
public boolean canUndo() {
return !undoStack.isEmpty();
}
/**
* Check if redo is available
*/
public boolean canRedo() {
return !redoStack.isEmpty();
}
/**
* Clear all history
*/
public void clear() {
undoStack.clear();
redoStack.clear();
}
/**
* Check if currently performing an undo/redo operation
*/
public boolean isUndoing() {
return isUndoing;
}
/**
* Set undoing flag (for external control during setText)
*/
public void setUndoing(boolean undoing) {
this.isUndoing = undoing;
}
// For backward compatibility with existing code
public List<UndoState> getUndoList() { return undoStack; }
public List<UndoState> getRedoList() { return redoStack; }
}