2023-07-30 01:26:36 -05:00
|
|
|
package noppes.npcs.client.gui.util;
|
|
|
|
|
|
|
|
|
|
import net.minecraft.client.Minecraft;
|
|
|
|
|
import net.minecraft.client.gui.GuiScreen;
|
2025-12-18 11:51:50 +02:00
|
|
|
import net.minecraft.client.gui.ScaledResolution;
|
2023-07-30 01:26:36 -05:00
|
|
|
import net.minecraft.util.ChatAllowedCharacters;
|
|
|
|
|
import noppes.npcs.NoppesStringUtils;
|
|
|
|
|
import noppes.npcs.client.ClientProxy;
|
2025-12-18 21:10:25 +02:00
|
|
|
import noppes.npcs.client.gui.util.script.*;
|
2025-12-18 10:48:34 +02:00
|
|
|
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
|
2023-07-30 01:26:36 -05:00
|
|
|
import org.lwjgl.input.Keyboard;
|
|
|
|
|
import org.lwjgl.input.Mouse;
|
2025-12-18 11:51:50 +02:00
|
|
|
import org.lwjgl.opengl.GL11;
|
2023-07-30 01:26:36 -05:00
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.regex.Matcher;
|
2025-12-16 18:33:55 +02:00
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown;
|
|
|
|
|
|
2025-12-18 21:10:25 +02:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2023-07-30 01:26:36 -05:00
|
|
|
public class GuiScriptTextArea extends GuiNpcTextField {
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== DIMENSIONS & POSITION ====================
|
2023-07-30 01:26:36 -05:00
|
|
|
public int id;
|
|
|
|
|
public int x;
|
|
|
|
|
public int y;
|
|
|
|
|
public int width;
|
|
|
|
|
public int height;
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== STATE FLAGS ====================
|
2023-07-30 01:26:36 -05:00
|
|
|
public boolean active = false;
|
|
|
|
|
public boolean enabled = true;
|
|
|
|
|
public boolean visible = true;
|
|
|
|
|
public boolean clicked = false;
|
|
|
|
|
public boolean doubleClicked = false;
|
2025-12-18 10:00:02 +02:00
|
|
|
public boolean tripleClicked = false;
|
2023-07-30 01:26:36 -05:00
|
|
|
public boolean clickScrolling = false;
|
2025-12-18 10:00:02 +02:00
|
|
|
private int clickCount = 0;
|
2025-12-18 21:10:25 +02:00
|
|
|
private long lastClicked = 0L;
|
|
|
|
|
|
|
|
|
|
// ==================== TEXT & CONTAINER ====================
|
|
|
|
|
public String text = null;
|
|
|
|
|
private JavaTextContainer container = null;
|
|
|
|
|
private boolean enableCodeHighlighting = false;
|
|
|
|
|
|
|
|
|
|
// ==================== SELECTION STATE ====================
|
2023-07-30 01:26:36 -05:00
|
|
|
private int startSelection;
|
|
|
|
|
private int endSelection;
|
|
|
|
|
private int cursorPosition;
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== SCROLL STATE ====================
|
2023-07-30 01:26:36 -05:00
|
|
|
private int scrolledLine = 0;
|
2025-12-18 12:05:57 +02:00
|
|
|
private double scrollPos = 0.0;
|
|
|
|
|
private double targetScroll = 0.0;
|
2025-12-18 12:28:17 +02:00
|
|
|
private double scrollVelocity = 0.0;
|
|
|
|
|
private long lastScrollTime = 0L;
|
2025-12-18 13:13:53 +02:00
|
|
|
private int scrollbarDragOffset = 0;
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// 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;
|
2025-12-18 19:07:05 +02:00
|
|
|
private static int LINE_NUMBER_GUTTER_WIDTH = 25;
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== UNDO/REDO ====================
|
|
|
|
|
public List<UndoData> undoList = new ArrayList<>();
|
|
|
|
|
public List<UndoData> redoList = new ArrayList<>();
|
|
|
|
|
public boolean undoing = false;
|
|
|
|
|
|
|
|
|
|
// ==================== CONSTRUCTOR ====================
|
2023-07-30 01:26:36 -05:00
|
|
|
|
|
|
|
|
public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) {
|
|
|
|
|
super(id, guiScreen, x, y, width, height, null);
|
|
|
|
|
this.id = id;
|
|
|
|
|
this.x = x;
|
|
|
|
|
this.y = y;
|
|
|
|
|
this.width = width;
|
|
|
|
|
this.height = height;
|
|
|
|
|
this.undoing = true;
|
|
|
|
|
this.setText(text);
|
|
|
|
|
this.undoing = false;
|
|
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== RENDERING ====================
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
public void drawTextBox(int xMouse, int yMouse) {
|
|
|
|
|
if (!visible)
|
2023-07-30 01:26:36 -05:00
|
|
|
return;
|
2025-10-15 06:28:43 -04:00
|
|
|
clampSelectionBounds();
|
2025-12-18 15:57:48 +02:00
|
|
|
|
|
|
|
|
// Dynamically calculate gutter width based on line count digits
|
|
|
|
|
if (container != null && container.linesCount > 0) {
|
|
|
|
|
int maxLineNum = container.linesCount;
|
|
|
|
|
String maxLineStr = String.valueOf(maxLineNum);
|
|
|
|
|
int digitWidth = ClientProxy.Font.width(maxLineStr);
|
|
|
|
|
LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10; // 10px total padding (5px left + 5px right)
|
|
|
|
|
}
|
2025-12-18 14:05:39 +02:00
|
|
|
// Draw outer border around entire area
|
2023-07-30 01:26:36 -05:00
|
|
|
drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0);
|
2025-12-18 14:05:39 +02:00
|
|
|
// Draw line number gutter background
|
|
|
|
|
drawRect(x, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff000000);
|
|
|
|
|
// Draw text viewport background (starts after gutter)
|
|
|
|
|
drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, 0xff000000);
|
|
|
|
|
// Draw separator line between gutter and text area
|
2025-12-18 14:48:14 +02:00
|
|
|
drawRect(x + LINE_NUMBER_GUTTER_WIDTH-1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff3c3f41);
|
2025-12-18 14:05:39 +02:00
|
|
|
|
|
|
|
|
// Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter)
|
2025-12-18 11:51:50 +02:00
|
|
|
GL11.glEnable(GL11.GL_SCISSOR_TEST);
|
|
|
|
|
Minecraft mc = Minecraft.getMinecraft();
|
|
|
|
|
ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
|
|
|
|
int scaleFactor = sr.getScaleFactor();
|
2025-12-18 14:05:39 +02:00
|
|
|
int scissorX = (this.x ) * scaleFactor;
|
2025-12-18 11:51:50 +02:00
|
|
|
int scissorY = (sr.getScaledHeight() - (this.y + this.height)) * scaleFactor;
|
2025-12-18 14:05:39 +02:00
|
|
|
int scissorW = (this.width ) * scaleFactor;
|
2025-12-18 11:51:50 +02:00
|
|
|
int scissorH = this.height * scaleFactor;
|
|
|
|
|
GL11.glScissor(scissorX, scissorY, scissorW, scissorH);
|
|
|
|
|
|
2025-12-18 19:07:05 +02:00
|
|
|
container.visibleLines = height / container.lineHeight-1;
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-12-18 12:05:57 +02:00
|
|
|
int maxScroll = Math.max(0, this.container.linesCount - container.visibleLines);
|
2025-03-13 17:14:45 -04:00
|
|
|
if (listener instanceof GuiNPCInterface) {
|
2024-05-26 18:10:37 -04:00
|
|
|
int k2 = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel();
|
2025-03-13 17:14:45 -04:00
|
|
|
if (k2 != 0) {
|
2025-12-18 12:05:57 +02:00
|
|
|
// update target scroll smoothly instead of jumping
|
2025-12-18 12:28:17 +02:00
|
|
|
double sign = Math.copySign(1, k2);
|
2025-12-18 13:13:53 +02:00
|
|
|
targetScroll -= sign * 2.0; // one line per wheel tick
|
2025-12-18 12:05:57 +02:00
|
|
|
if (targetScroll < 0) targetScroll = 0;
|
|
|
|
|
if (targetScroll > maxScroll) targetScroll = maxScroll;
|
2024-05-22 19:05:41 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
if (clickScrolling) {
|
2025-12-18 13:13:53 +02:00
|
|
|
// keep dragging while mouse button is down
|
2023-07-30 01:26:36 -05:00
|
|
|
clickScrolling = Mouse.isButtonDown(0);
|
2025-12-18 13:13:53 +02:00
|
|
|
int diff = Math.max(0, container.linesCount - container.visibleLines);
|
|
|
|
|
if (diff > 0) {
|
|
|
|
|
int sbSize = Math.max((int) (1f * container.visibleLines / container.linesCount * height), 2);
|
|
|
|
|
int trackTop = y + 1;
|
|
|
|
|
int trackHeight = Math.max(1, height - 4);
|
|
|
|
|
int thumbRange = Math.max(1, trackHeight - sbSize);
|
|
|
|
|
|
|
|
|
|
// current thumb top for hit testing
|
|
|
|
|
double linesCountD = Math.max(1, (double) container.linesCount);
|
|
|
|
|
int thumbTop = (int) (y + 1f * scrollPos / linesCountD * (height - 4)) + 1;
|
|
|
|
|
|
|
|
|
|
// If user clicked on the track (outside the thumb), jump to proportional position
|
|
|
|
|
if (yMouse < thumbTop || yMouse > thumbTop + sbSize) {
|
|
|
|
|
double centerRatio = (double) (yMouse - trackTop) / (double) trackHeight;
|
|
|
|
|
centerRatio = Math.max(0.0, Math.min(1.0, centerRatio));
|
|
|
|
|
targetScroll = Math.max(0, Math.min(diff, centerRatio * diff));
|
|
|
|
|
// prepare drag offset for subsequent drags (center grab)
|
|
|
|
|
scrollbarDragOffset = sbSize / 2;
|
|
|
|
|
} else {
|
|
|
|
|
// Dragging the thumb itself: preserve initial grab offset
|
|
|
|
|
int desiredTop = yMouse - scrollbarDragOffset;
|
|
|
|
|
desiredTop = Math.max(trackTop, Math.min(trackTop + thumbRange, desiredTop));
|
2025-12-18 13:39:32 +02:00
|
|
|
double ratio = (double) (desiredTop - trackTop) / (double) thumbRange;
|
2025-12-18 13:13:53 +02:00
|
|
|
ratio = Math.max(0.0, Math.min(1.0, ratio));
|
|
|
|
|
targetScroll = Math.max(0, Math.min(diff, ratio * diff));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!clickScrolling) {
|
|
|
|
|
// reset drag offset when released
|
|
|
|
|
scrollbarDragOffset = 0;
|
|
|
|
|
}
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-12-18 12:05:57 +02:00
|
|
|
|
2025-12-18 12:28:17 +02:00
|
|
|
// Animate scrollPos towards targetScroll using exponential smoothing
|
|
|
|
|
long nowMs = System.currentTimeMillis();
|
|
|
|
|
if (scrollPos < 0) {
|
|
|
|
|
scrollPos = scrolledLine; // initialize
|
|
|
|
|
lastScrollTime = nowMs;
|
|
|
|
|
}
|
|
|
|
|
double dt = Math.min(0.05, (nowMs - lastScrollTime) / 1000.0); // clamp dt for stability
|
|
|
|
|
lastScrollTime = nowMs;
|
|
|
|
|
|
|
|
|
|
double dist = targetScroll - scrollPos;
|
|
|
|
|
// If close enough, snap to target to avoid long slow tail
|
|
|
|
|
if (Math.abs(dist) < 0.01) {
|
2025-12-18 12:05:57 +02:00
|
|
|
scrollPos = targetScroll;
|
2025-12-18 12:28:17 +02:00
|
|
|
scrollVelocity = 0.0;
|
|
|
|
|
} else {
|
|
|
|
|
// Time constant controlling speed (seconds). Smaller -> faster snap.
|
2025-12-18 13:13:53 +02:00
|
|
|
final double tau = 0.1; // ~55ms time constant feels snappy
|
2025-12-18 12:28:17 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2025-12-18 12:05:57 +02:00
|
|
|
}
|
2025-12-18 19:07:05 +02:00
|
|
|
// Ensure scrollPos stays in valid bounds
|
|
|
|
|
if (scrollPos < 0) scrollPos = 0;
|
|
|
|
|
if (scrollPos > maxScroll) scrollPos = maxScroll;
|
|
|
|
|
|
2025-12-18 12:05:57 +02:00
|
|
|
// Update integer scrolledLine for compatibility
|
|
|
|
|
scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll));
|
2025-12-18 13:13:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if (clicked) {
|
|
|
|
|
clicked = Mouse.isButtonDown(0);
|
|
|
|
|
int i = getSelectionPos(xMouse, yMouse);
|
|
|
|
|
if (i != cursorPosition) {
|
|
|
|
|
if (doubleClicked || tripleClicked) {
|
|
|
|
|
startSelection = endSelection = cursorPosition;
|
|
|
|
|
doubleClicked = false;
|
|
|
|
|
tripleClicked = false;
|
|
|
|
|
}
|
|
|
|
|
setCursor(i, true);
|
|
|
|
|
}
|
|
|
|
|
} else if (doubleClicked || tripleClicked) {
|
|
|
|
|
doubleClicked = false;
|
|
|
|
|
tripleClicked = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Calculate braces next to cursor to highlight
|
2023-07-30 01:26:36 -05:00
|
|
|
int startBracket = 0, endBracket = 0;
|
2025-12-18 00:52:50 +02:00
|
|
|
if (startSelection >= 0 && text != null && text.length() > 0 &&
|
|
|
|
|
(endSelection - startSelection == 1 || startSelection == endSelection)) {
|
|
|
|
|
int[] span = findBracketSpanAt(startSelection);
|
|
|
|
|
if (span != null) {
|
|
|
|
|
startBracket = span[0];
|
|
|
|
|
endBracket = span[1];
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 18:33:55 +02:00
|
|
|
List<JavaTextContainer.LineData> list = new ArrayList<>(container.lines);
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-12-17 20:06:47 +02:00
|
|
|
// Build brace spans: {origDepth, open line, close line, adjustedDepth}
|
2025-12-17 18:13:29 +02:00
|
|
|
List<int[]> braceSpans = computeBraceSpans(text, list);
|
|
|
|
|
|
2025-12-18 10:25:10 +02:00
|
|
|
// Determine which exact brace span (openLine/closeLine) should be highlighted based on bracket under caret
|
|
|
|
|
int highlightedOpenLine = -1;
|
|
|
|
|
int highlightedCloseLine = -1;
|
|
|
|
|
if (startBracket != endBracket && startBracket >= 0) {
|
|
|
|
|
int bracketLineIdx = -1;
|
|
|
|
|
for (int li = 0; li < list.size(); li++) {
|
|
|
|
|
LineData ld = list.get(li);
|
|
|
|
|
if (startBracket >= ld.start && startBracket < ld.end) {
|
|
|
|
|
bracketLineIdx = li;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (bracketLineIdx >= 0) {
|
|
|
|
|
for (int[] span : braceSpans) {
|
|
|
|
|
int openLine = span[1];
|
|
|
|
|
int closeLine = span[2];
|
|
|
|
|
if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) {
|
|
|
|
|
highlightedOpenLine = openLine;
|
|
|
|
|
highlightedCloseLine = closeLine;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
String wordHightLight = null;
|
2025-03-13 17:14:45 -04:00
|
|
|
if (startSelection != endSelection) {
|
2023-07-30 01:26:36 -05:00
|
|
|
Matcher m = container.regexWord.matcher(text);
|
2025-03-13 17:14:45 -04:00
|
|
|
while (m.find()) {
|
|
|
|
|
if (m.start() == startSelection && m.end() == endSelection) {
|
2023-07-30 01:26:36 -05:00
|
|
|
wordHightLight = text.substring(startSelection, endSelection);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 12:05:57 +02:00
|
|
|
// Expand render range by one line above/below so partially-visible lines are drawn
|
2025-12-18 13:13:53 +02:00
|
|
|
int renderStart = Math.max(0, scrolledLine - 1);
|
|
|
|
|
int renderEnd = Math.min(list.size() - 1, scrolledLine + container.visibleLines + 1);
|
|
|
|
|
|
|
|
|
|
// Apply fractional GL translate for sub-pixel smooth scrolling
|
|
|
|
|
double fracOffset = scrollPos - scrolledLine;
|
|
|
|
|
float fracPixels = (float) (fracOffset * container.lineHeight);
|
|
|
|
|
GL11.glPushMatrix();
|
|
|
|
|
GL11.glTranslatef(0.0f, -fracPixels, 0.0f);
|
|
|
|
|
|
2025-12-18 14:05:39 +02:00
|
|
|
// Render line numbers in the gutter (outside scissor region, so pop first)
|
|
|
|
|
GL11.glPopMatrix();
|
|
|
|
|
// GL11.glDisable(GL11.GL_SCISSOR_TEST);
|
|
|
|
|
GL11.glPushMatrix();
|
|
|
|
|
GL11.glTranslatef(0.0f, -fracPixels, 0.0f);
|
2025-12-18 19:08:32 +02:00
|
|
|
|
|
|
|
|
int stringYOffset = 2;
|
|
|
|
|
|
|
|
|
|
// Render LINE GUTTER numbers
|
2025-12-18 14:05:39 +02:00
|
|
|
for (int i = renderStart; i <= renderEnd; i++) {
|
2025-12-18 19:08:32 +02:00
|
|
|
int posY = y + (i - scrolledLine) * container.lineHeight + stringYOffset;
|
2025-12-18 14:05:39 +02:00
|
|
|
String lineNum = String.valueOf(i + 1);
|
|
|
|
|
int lineNumWidth = ClientProxy.Font.width(lineNum);
|
|
|
|
|
int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding
|
|
|
|
|
int lineNumY = posY + 1;
|
|
|
|
|
// Highlight current line number
|
|
|
|
|
int lineNumColor = 0xFF606366;
|
|
|
|
|
if (active && isEnabled()) {
|
|
|
|
|
for (int li = 0; li < list.size(); li++) {
|
|
|
|
|
LineData ld = list.get(li);
|
|
|
|
|
if (cursorPosition >= ld.start && cursorPosition < ld.end || (li == list.size() - 1 && cursorPosition == text.length())) {
|
|
|
|
|
if (li == i) {
|
2025-12-18 19:08:32 +02:00
|
|
|
lineNumColor = 0xFFb9c7d6;
|
2025-12-18 14:05:39 +02:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ClientProxy.Font.drawString(lineNum, lineNumX, lineNumY, lineNumColor);
|
|
|
|
|
}
|
|
|
|
|
GL11.glPopMatrix();
|
|
|
|
|
// Re-enable scissor for text area and push matrix again
|
|
|
|
|
GL11.glEnable(GL11.GL_SCISSOR_TEST);
|
|
|
|
|
GL11.glPushMatrix();
|
|
|
|
|
GL11.glTranslatef(0.0f, -fracPixels, 0.0f);
|
|
|
|
|
|
2025-12-18 14:44:09 +02:00
|
|
|
renderStart = Math.max(0, scrolledLine - 1);
|
2025-12-18 12:05:57 +02:00
|
|
|
for (int i = renderStart; i <= renderEnd; i++) {
|
2023-07-30 01:26:36 -05:00
|
|
|
LineData data = list.get(i);
|
|
|
|
|
String line = data.text;
|
|
|
|
|
int w = line.length();
|
2025-12-18 13:13:53 +02:00
|
|
|
// Use integer Y relative to scrolledLine; fractional offset applied via GL translate
|
|
|
|
|
int posY = y + (i - scrolledLine) * container.lineHeight;
|
2025-12-18 12:05:57 +02:00
|
|
|
if (i >= renderStart && i <= renderEnd) {
|
2025-12-18 10:27:17 +02:00
|
|
|
//Highlight braces the cursor position is on
|
|
|
|
|
if (startBracket != endBracket) {
|
|
|
|
|
if (startBracket >= data.start && startBracket < data.end) {
|
|
|
|
|
int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start));
|
|
|
|
|
int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1;
|
2025-12-18 14:05:39 +02:00
|
|
|
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00);
|
2025-12-18 10:27:17 +02:00
|
|
|
}
|
|
|
|
|
if (endBracket >= data.start && endBracket < data.end) {
|
|
|
|
|
int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start));
|
|
|
|
|
int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1;
|
2025-12-18 14:05:39 +02:00
|
|
|
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00);
|
2025-12-18 10:27:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//Highlight words
|
2025-03-13 17:14:45 -04:00
|
|
|
if (wordHightLight != null) {
|
2023-07-30 01:26:36 -05:00
|
|
|
Matcher m = container.regexWord.matcher(line);
|
2025-03-13 17:14:45 -04:00
|
|
|
while (m.find()) {
|
|
|
|
|
if (line.substring(m.start(), m.end()).equals(wordHightLight)) {
|
2023-07-30 01:26:36 -05:00
|
|
|
int s = ClientProxy.Font.width(line.substring(0, m.start()));
|
|
|
|
|
int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1;
|
2025-12-18 14:05:39 +02:00
|
|
|
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x99004c00);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 09:53:02 +02:00
|
|
|
// Highlight the current line (light gray) under any selection
|
|
|
|
|
if (active && isEnabled() && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) {
|
2025-12-18 14:05:39 +02:00
|
|
|
drawRect(x , posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0);
|
2025-12-18 09:53:02 +02:00
|
|
|
}
|
2025-12-18 10:27:17 +02:00
|
|
|
// Highlight selection
|
2025-03-13 17:14:45 -04:00
|
|
|
if (startSelection != endSelection && endSelection > data.start && startSelection <= data.end) {
|
|
|
|
|
if (startSelection < data.end) {
|
2023-07-30 01:26:36 -05:00
|
|
|
int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0)));
|
|
|
|
|
int e = ClientProxy.Font.width(line.substring(0, Math.min(endSelection - data.start, w))) + 1;
|
2025-12-18 14:05:39 +02:00
|
|
|
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 18:13:29 +02:00
|
|
|
|
|
|
|
|
// Draw indent guides once per visible block based on brace spans
|
2025-12-18 13:13:53 +02:00
|
|
|
if (i == Math.max(0, scrolledLine) && !braceSpans.isEmpty()) {
|
|
|
|
|
int visStart = Math.max(0, scrolledLine);
|
2025-12-18 13:39:14 +02:00
|
|
|
int visEnd = Math.min(list.size() - 1, visStart + container.visibleLines - 0);
|
2025-12-17 18:13:29 +02:00
|
|
|
for (int[] span : braceSpans) {
|
2025-12-17 20:06:47 +02:00
|
|
|
int originalDepth = span[0];
|
2025-12-17 18:13:29 +02:00
|
|
|
int openLine = span[1];
|
|
|
|
|
int closeLine = span[2];
|
2025-12-17 20:06:47 +02:00
|
|
|
int depth = span.length > 3 ? span[3] : originalDepth;
|
|
|
|
|
// Skip top-level (depth 1) using the original depth to avoid hiding nested guides when adjusted
|
2025-12-18 14:07:18 +02:00
|
|
|
if (originalDepth <= 1)
|
|
|
|
|
continue;
|
2025-12-17 18:13:29 +02:00
|
|
|
int startLine = openLine + 1; // start under the opening brace
|
|
|
|
|
int endLine = closeLine - 1; // stop before the closing brace
|
|
|
|
|
if (startLine > endLine)
|
|
|
|
|
continue;
|
|
|
|
|
if (endLine < visStart || startLine > visEnd)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
int drawStart = Math.max(startLine, visStart);
|
|
|
|
|
int drawEnd = Math.min(endLine, visEnd);
|
|
|
|
|
// Compute horizontal position: 4 spaces per indent level, minus a tiny left offset
|
2025-12-17 20:06:47 +02:00
|
|
|
int safeDepth = Math.max(1, depth);
|
|
|
|
|
int spaces = (safeDepth - 1) * 4;
|
2025-12-17 18:13:29 +02:00
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
for (int k = 0; k < spaces; k++)
|
|
|
|
|
sb.append(' ');
|
|
|
|
|
int px = ClientProxy.Font.width(sb.toString());
|
2025-12-18 14:05:39 +02:00
|
|
|
int gx = x + LINE_NUMBER_GUTTER_WIDTH + 4 + px - 2; // shift left ~2px for the IntelliJ feel
|
2025-12-17 18:13:29 +02:00
|
|
|
|
2025-12-18 13:13:53 +02:00
|
|
|
int topY = y + (drawStart - scrolledLine) * container.lineHeight;
|
2025-12-18 13:39:14 +02:00
|
|
|
int bottomY = y + (endLine - scrolledLine + 1) * container.lineHeight;
|
2025-12-18 10:25:10 +02:00
|
|
|
int guideColor = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine) ? 0x9933cc00 : 0x33FFFFFF;
|
|
|
|
|
drawRect(gx, topY, gx + 1, bottomY, guideColor);
|
2025-12-17 18:13:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 19:08:32 +02:00
|
|
|
int yPos = posY + stringYOffset;
|
2025-12-18 14:05:39 +02:00
|
|
|
data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0);
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-12-18 11:23:54 +02:00
|
|
|
// Draw cursor: pause blinking while user is active recently
|
|
|
|
|
boolean recentInput = System.currentTimeMillis() - this.lastInputTime < 500;
|
|
|
|
|
if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) {
|
2025-12-18 14:05:39 +02:00
|
|
|
int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width(
|
2025-12-17 14:33:17 +02:00
|
|
|
line.substring(0, Math.min(cursorPosition - data.start, line.length())));
|
2025-12-18 11:25:46 +02:00
|
|
|
drawRect(posX + 1, yPos -1, posX + 2, yPos - 1 + container.lineHeight, 0xffffffff);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 13:13:53 +02:00
|
|
|
GL11.glPopMatrix();
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
if (hasVerticalScrollbar()) {
|
2023-07-30 01:26:36 -05:00
|
|
|
Minecraft.getMinecraft().renderEngine.bindTexture(GuiCustomScroll.resource);
|
2025-12-18 13:13:53 +02:00
|
|
|
int sbSize = Math.max((int) (1f * (container.visibleLines) / container.linesCount * height), 2);
|
2023-07-30 01:26:36 -05:00
|
|
|
|
|
|
|
|
int posX = x + width - 6;
|
2025-12-18 12:05:57 +02:00
|
|
|
double linesCount = Math.max(1, (double) container.linesCount);
|
|
|
|
|
int posY = (int) (y + 1f * scrollPos / linesCount * (height - 4)) + 1;
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-12-18 13:13:53 +02:00
|
|
|
drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-12-18 11:51:50 +02:00
|
|
|
GL11.glDisable(GL11.GL_SCISSOR_TEST);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
// ==================== BRACKET MATCHING ====================
|
|
|
|
|
// Uses BracketMatcher helper for bracket span detection
|
|
|
|
|
|
2025-12-18 00:52:50 +02:00
|
|
|
private int[] findBracketSpanAt(int pos) {
|
2025-12-18 21:10:25 +02:00
|
|
|
return BracketMatcher.findBracketSpanAt(text, pos);
|
2025-12-18 00:52:50 +02:00
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
private int findClosingBracket(String str, char s, char e) {
|
2025-12-18 21:10:25 +02:00
|
|
|
return BracketMatcher.findClosingBracket(str, s, e);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int findOpeningBracket(String str, char s, char e) {
|
2025-12-18 21:10:25 +02:00
|
|
|
return BracketMatcher.findOpeningBracket(str, s, e);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 18:13:29 +02:00
|
|
|
private List<int[]> computeBraceSpans(String fullText, List<LineData> lines) {
|
2025-12-18 21:10:25 +02:00
|
|
|
return BracketMatcher.computeBraceSpans(fullText, lines);
|
2025-12-17 18:13:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-18 21:10:25 +02:00
|
|
|
// ==================== SELECTION & CURSOR POSITION ====================
|
|
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
private int getSelectionPos(int xMouse, int yMouse) {
|
2025-12-18 14:05:39 +02:00
|
|
|
xMouse -= (this.x + LINE_NUMBER_GUTTER_WIDTH + 1);
|
2023-07-30 01:26:36 -05:00
|
|
|
yMouse -= this.y + 1;
|
2025-12-18 13:13:53 +02:00
|
|
|
// Adjust yMouse to account for fractional GL translation (negative offset applied in rendering)
|
|
|
|
|
double fracOffset = scrollPos - scrolledLine;
|
|
|
|
|
double fracPixels = fracOffset * container.lineHeight;
|
|
|
|
|
yMouse = (int) Math.round(yMouse + fracPixels);
|
|
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
ArrayList list = new ArrayList(this.container.lines);
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
for (int i = 0; i < list.size(); ++i) {
|
|
|
|
|
LineData data = (LineData) list.get(i);
|
2025-12-18 19:07:05 +02:00
|
|
|
if (i >= this.scrolledLine && i <= this.scrolledLine + this.container.visibleLines) {
|
2023-07-30 01:26:36 -05:00
|
|
|
int yPos = (i - this.scrolledLine) * this.container.lineHeight;
|
2025-03-13 17:14:45 -04:00
|
|
|
if (yMouse >= yPos && yMouse < yPos + this.container.lineHeight) {
|
2023-07-30 01:26:36 -05:00
|
|
|
int lineWidth = 0;
|
|
|
|
|
char[] chars = data.text.toCharArray();
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
for (int j = 1; j <= chars.length; ++j) {
|
2023-07-30 01:26:36 -05:00
|
|
|
int w = ClientProxy.Font.width(data.text.substring(0, j));
|
2025-03-13 17:14:45 -04:00
|
|
|
if (xMouse < lineWidth + (w - lineWidth) / 2) {
|
2023-07-30 01:26:36 -05:00
|
|
|
return data.start + j - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lineWidth = w;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 15:13:38 +02:00
|
|
|
// Place cursor after the last visible character of the line.
|
|
|
|
|
// `data.end - 1` previously pointed at the newline for non-last lines
|
|
|
|
|
// which made clicks land on the newline rather than after the text.
|
|
|
|
|
// Use data.start + chars.length to return the position directly
|
|
|
|
|
// after the line's characters, clamped to the total text length.
|
|
|
|
|
int posAfterChars = data.start + chars.length;
|
|
|
|
|
return Math.min(posAfterChars, text.length());
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.container.text.length();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 19:07:05 +02:00
|
|
|
// Find which line the cursor is on (0-indexed)
|
|
|
|
|
private int getCursorLineIndex() {
|
|
|
|
|
if (container == null || container.lines == null || container.lines.isEmpty()) return 0;
|
2025-12-18 14:44:09 +02:00
|
|
|
for (int i = 0; i < container.lines.size(); i++) {
|
|
|
|
|
LineData ld = container.lines.get(i);
|
2025-12-18 19:07:05 +02:00
|
|
|
// For last line, use <= since end doesn't include +1 for newline
|
|
|
|
|
// For other lines, use < since end includes the newline position
|
|
|
|
|
boolean isLastLine = (i == container.lines.size() - 1);
|
|
|
|
|
boolean isOnLine = isLastLine
|
|
|
|
|
? (cursorPosition >= ld.start && cursorPosition <= ld.end)
|
|
|
|
|
: (cursorPosition >= ld.start && cursorPosition < ld.end);
|
|
|
|
|
if (isOnLine) {
|
|
|
|
|
return i;
|
2025-12-18 14:44:09 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 19:07:05 +02:00
|
|
|
return container.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)
|
|
|
|
|
private boolean isCursorOnLine(int lineIndex, LineData line) {
|
|
|
|
|
boolean isLastLine = (lineIndex == container.lines.size() - 1);
|
|
|
|
|
return isLastLine
|
|
|
|
|
? (cursorPosition >= line.start && cursorPosition <= line.end)
|
|
|
|
|
: (cursorPosition >= line.start && cursorPosition < line.end);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ)
|
|
|
|
|
// Only scrolls if cursor is outside the visible area
|
|
|
|
|
private void scrollToCursor() {
|
|
|
|
|
if (container == null || container.lines == null || container.lines.isEmpty()) return;
|
2025-12-18 14:44:09 +02:00
|
|
|
|
2025-12-18 19:07:05 +02:00
|
|
|
int lineIdx = getCursorLineIndex();
|
2025-12-18 14:44:09 +02:00
|
|
|
int visible = Math.max(1, container.visibleLines);
|
|
|
|
|
int maxScroll = Math.max(0, container.linesCount - visible);
|
|
|
|
|
|
2025-12-18 19:07:05 +02:00
|
|
|
int firstVisible = scrolledLine;
|
|
|
|
|
int lastFullyVisible = scrolledLine + visible;
|
2025-12-18 14:44:09 +02:00
|
|
|
|
2025-12-18 19:07:05 +02:00
|
|
|
if (lineIdx < firstVisible) {
|
|
|
|
|
// Cursor is above viewport - scroll up just enough to show it
|
|
|
|
|
targetScroll = lineIdx;
|
|
|
|
|
} else if (lineIdx > lastFullyVisible) {
|
|
|
|
|
// Cursor is below viewport - scroll down just enough to show it
|
|
|
|
|
targetScroll = Math.min(lineIdx - visible, maxScroll);
|
|
|
|
|
}
|
|
|
|
|
// If cursor is visible, don't change scroll
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Legacy method name for compatibility
|
|
|
|
|
private void ensureCursorVisible() {
|
|
|
|
|
scrollToCursor();
|
2025-12-18 14:44:09 +02:00
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== KEYBOARD INPUT HANDLING ====================
|
2025-12-18 14:44:09 +02:00
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
@Override
|
|
|
|
|
public boolean textboxKeyTyped(char c, int i) {
|
2025-03-13 17:14:45 -04:00
|
|
|
if (!active)
|
2023-07-30 01:26:36 -05:00
|
|
|
return false;
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.isKeyComboCtrlA(i)) {
|
2023-07-30 01:26:36 -05:00
|
|
|
startSelection = cursorPosition = 0;
|
|
|
|
|
endSelection = text.length();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
if (!isEnabled())
|
2023-07-30 01:26:36 -05:00
|
|
|
return false;
|
|
|
|
|
|
2025-12-17 14:33:17 +02:00
|
|
|
LineData currentLine = null;
|
|
|
|
|
for (LineData line : this.container.lines) {
|
2025-12-18 01:00:27 +02:00
|
|
|
if (startSelection >= line.start && startSelection < line.end) {
|
2025-12-17 14:33:17 +02:00
|
|
|
currentLine = line;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
String original = text;
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_LEFT) {
|
2023-07-30 01:26:36 -05:00
|
|
|
int j = 1;
|
2025-03-13 17:14:45 -04:00
|
|
|
if (isCtrlKeyDown()) {
|
2023-07-30 01:26:36 -05:00
|
|
|
Matcher m = container.regexWord.matcher(text.substring(0, cursorPosition));
|
2025-03-13 17:14:45 -04:00
|
|
|
while (m.find()) {
|
|
|
|
|
if (m.start() == m.end())
|
2023-07-30 01:26:36 -05:00
|
|
|
continue;
|
|
|
|
|
j = cursorPosition - m.start();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 14:33:17 +02:00
|
|
|
int newPos = Math.max(cursorPosition - j, 0);
|
|
|
|
|
setCursor(newPos, GuiScreen.isShiftKeyDown());
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_RIGHT) {
|
2023-07-30 01:26:36 -05:00
|
|
|
int j = 1;
|
2025-03-13 17:14:45 -04:00
|
|
|
if (isCtrlKeyDown()) {
|
2023-07-30 01:26:36 -05:00
|
|
|
Matcher m = container.regexWord.matcher(text.substring(cursorPosition));
|
2025-03-13 17:14:45 -04:00
|
|
|
if (m.find() && m.start() > 0 || m.find()) {
|
2023-07-30 01:26:36 -05:00
|
|
|
j = m.start();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 20:06:47 +02:00
|
|
|
int newPos = Math.min(cursorPosition + j, text.length());
|
2025-12-17 14:33:17 +02:00
|
|
|
setCursor(newPos, GuiScreen.isShiftKeyDown());
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_UP) {
|
2023-07-30 01:26:36 -05:00
|
|
|
setCursor(cursorUp(), GuiScreen.isShiftKeyDown());
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_DOWN) {
|
2023-07-30 01:26:36 -05:00
|
|
|
setCursor(cursorDown(), GuiScreen.isShiftKeyDown());
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_DELETE) {
|
2023-07-30 01:26:36 -05:00
|
|
|
String s = getSelectionAfterText();
|
2025-03-13 17:14:45 -04:00
|
|
|
if (!s.isEmpty() && startSelection == endSelection)
|
2023-07-30 01:26:36 -05:00
|
|
|
s = s.substring(1);
|
|
|
|
|
setText(getSelectionBeforeText() + s);
|
|
|
|
|
endSelection = cursorPosition = startSelection;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (isKeyComboCtrlBackspace(i)) {
|
2023-07-30 15:47:03 -04:00
|
|
|
String s = getSelectionBeforeText();
|
2025-03-13 17:14:45 -04:00
|
|
|
if (startSelection > 0 && startSelection == endSelection) {
|
2023-07-30 15:47:03 -04:00
|
|
|
int nearestCondition = cursorPosition;
|
|
|
|
|
int g;
|
|
|
|
|
boolean cursorInWhitespace = Character.isWhitespace(s.charAt(cursorPosition - 1));
|
2025-03-13 17:14:45 -04:00
|
|
|
if (cursorInWhitespace) {
|
2023-07-30 15:47:03 -04:00
|
|
|
// Find the nearest word if we are starting in whitespace
|
|
|
|
|
for (g = cursorPosition - 1; g >= 0; g--) {
|
|
|
|
|
char currentChar = s.charAt(g);
|
|
|
|
|
if (!Character.isWhitespace(currentChar)) {
|
|
|
|
|
nearestCondition = g;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (g == 0) {
|
2023-07-30 15:47:03 -04:00
|
|
|
nearestCondition = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
} else {
|
2023-07-30 15:47:03 -04:00
|
|
|
// Find the nearest blank space or new line
|
|
|
|
|
for (g = cursorPosition - 1; g >= 0; g--) {
|
|
|
|
|
char currentChar = s.charAt(g);
|
2025-03-13 17:14:45 -04:00
|
|
|
if (Character.isWhitespace(currentChar) || currentChar == '\n') {
|
2023-07-30 15:47:03 -04:00
|
|
|
nearestCondition = g;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (g == 0) {
|
2023-07-30 15:47:03 -04:00
|
|
|
nearestCondition = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove all text to the left up to the nearest boundary
|
|
|
|
|
s = s.substring(0, nearestCondition);
|
|
|
|
|
startSelection -= (cursorPosition - nearestCondition);
|
|
|
|
|
}
|
|
|
|
|
setText(s + getSelectionAfterText());
|
|
|
|
|
endSelection = cursorPosition = startSelection;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_BACK) {
|
2025-12-18 00:35:38 +02:00
|
|
|
// Handle selection deletion first
|
|
|
|
|
if (startSelection != endSelection) {
|
|
|
|
|
String s = getSelectionBeforeText();
|
|
|
|
|
setText(s + getSelectionAfterText());
|
|
|
|
|
endSelection = cursorPosition = startSelection;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-18 00:35:38 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nothing to delete if at start
|
|
|
|
|
if (startSelection <= 0) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find current line for indent-aware backspace
|
|
|
|
|
LineData curr = null;
|
2025-12-18 19:07:05 +02:00
|
|
|
for (int idx = 0; idx < container.lines.size(); idx++) {
|
|
|
|
|
LineData line = container.lines.get(idx);
|
|
|
|
|
boolean isLastLine = (idx == container.lines.size() - 1);
|
|
|
|
|
boolean isOnLine = isLastLine
|
|
|
|
|
? (cursorPosition >= line.start && cursorPosition <= line.end)
|
|
|
|
|
: (cursorPosition >= line.start && cursorPosition < line.end);
|
|
|
|
|
if (isOnLine) {
|
2025-12-18 00:35:38 +02:00
|
|
|
curr = line;
|
|
|
|
|
break;
|
2025-12-17 15:38:19 +02:00
|
|
|
}
|
2025-12-18 00:35:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Indent-aware backspace: if cursor is at or before the EXPECTED indent, merge with previous line
|
|
|
|
|
// If cursor is after expected indent (even in extra whitespace), do normal backspace
|
|
|
|
|
if (curr != null && curr.start > 0) {
|
|
|
|
|
int col = cursorPosition - curr.start;
|
|
|
|
|
int actualIndent = getLineIndent(curr.text);
|
|
|
|
|
int expectedIndent = getExpectedIndent(curr);
|
|
|
|
|
|
|
|
|
|
// Only trigger smart merge if cursor is at or before the expected indent position
|
|
|
|
|
if (col <= expectedIndent) {
|
|
|
|
|
boolean lineHasContent = curr.text.trim().length() > 0;
|
|
|
|
|
int newlinePos = curr.start - 1; // Position of newline before this line
|
|
|
|
|
|
|
|
|
|
if (!lineHasContent) {
|
|
|
|
|
// Empty/whitespace-only line: remove the entire line including its trailing newline
|
|
|
|
|
int removeEnd = text.indexOf('\n', curr.start);
|
2025-12-17 15:38:19 +02:00
|
|
|
if (removeEnd == -1) {
|
|
|
|
|
removeEnd = text.length();
|
|
|
|
|
} else {
|
2025-12-18 00:35:38 +02:00
|
|
|
removeEnd = removeEnd + 1; // Include the newline
|
2025-12-17 15:38:19 +02:00
|
|
|
}
|
2025-12-18 00:35:38 +02:00
|
|
|
String before = text.substring(0, curr.start);
|
|
|
|
|
String after = removeEnd <= text.length() ? text.substring(removeEnd) : "";
|
2025-12-17 10:46:08 +02:00
|
|
|
setText(before + after);
|
2025-12-18 00:35:38 +02:00
|
|
|
// Cursor goes to end of previous line (at the newline position, which is now end of prev line content)
|
|
|
|
|
int newCursor = Math.max(0, curr.start - 1);
|
2025-12-17 15:38:19 +02:00
|
|
|
endSelection = cursorPosition = startSelection = newCursor;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 10:46:08 +02:00
|
|
|
return true;
|
2025-12-18 00:35:38 +02:00
|
|
|
} else {
|
|
|
|
|
// Line has content: merge with previous line
|
|
|
|
|
// Remove newline + indent, keep content
|
|
|
|
|
int contentStart = curr.start + actualIndent;
|
|
|
|
|
|
|
|
|
|
String before = text.substring(0, newlinePos);
|
|
|
|
|
String content = contentStart <= text.length() ? text.substring(contentStart) : "";
|
|
|
|
|
|
|
|
|
|
// Determine if we need spacing between merged content
|
|
|
|
|
String spacer = "";
|
|
|
|
|
if (before.length() > 0 && content.length() > 0) {
|
|
|
|
|
char lastChar = before.charAt(before.length() - 1);
|
|
|
|
|
char firstChar = content.charAt(0);
|
|
|
|
|
// Add space unless previous ends with whitespace/open bracket or current starts with close bracket/punctuation
|
|
|
|
|
if (!Character.isWhitespace(lastChar) &&
|
|
|
|
|
lastChar != '{' && lastChar != '(' && lastChar != '[' &&
|
|
|
|
|
firstChar != '}' && firstChar != ')' && firstChar != ']' &&
|
|
|
|
|
firstChar != ';' && firstChar != ',' && firstChar != '.' &&
|
|
|
|
|
firstChar != '\n') {
|
|
|
|
|
spacer = " ";
|
2025-12-17 23:54:36 +02:00
|
|
|
}
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
2025-12-18 00:35:38 +02:00
|
|
|
|
|
|
|
|
setText(before + spacer + content);
|
|
|
|
|
int newCursor = before.length() + spacer.length();
|
|
|
|
|
endSelection = cursorPosition = startSelection = newCursor;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 20:06:47 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-12-18 00:35:38 +02:00
|
|
|
|
|
|
|
|
// Auto-pair deletion: if deleting an opening bracket/quote and next char is the matching closer
|
|
|
|
|
if (startSelection > 0 && startSelection < text.length()) {
|
|
|
|
|
char prev = text.charAt(startSelection - 1);
|
|
|
|
|
char nextc = text.charAt(startSelection);
|
|
|
|
|
if ((prev == '(' && nextc == ')') ||
|
|
|
|
|
(prev == '[' && nextc == ']') ||
|
|
|
|
|
(prev == '{' && nextc == '}') ||
|
|
|
|
|
(prev == '\'' && nextc == '\'') ||
|
|
|
|
|
(prev == '"' && nextc == '"')) {
|
|
|
|
|
String before = text.substring(0, startSelection - 1);
|
|
|
|
|
String after = startSelection + 1 < text.length() ? text.substring(startSelection + 1) : "";
|
|
|
|
|
setText(before + after);
|
|
|
|
|
startSelection -= 1;
|
|
|
|
|
endSelection = cursorPosition = startSelection;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-18 00:35:38 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normal backspace: delete one character
|
|
|
|
|
String s = getSelectionBeforeText();
|
|
|
|
|
s = s.substring(0, s.length() - 1);
|
|
|
|
|
startSelection--;
|
2023-07-30 01:26:36 -05:00
|
|
|
setText(s + getSelectionAfterText());
|
|
|
|
|
endSelection = cursorPosition = startSelection;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.isKeyComboCtrlX(i)) {
|
|
|
|
|
if (startSelection != endSelection) {
|
2023-07-30 01:26:36 -05:00
|
|
|
NoppesStringUtils.setClipboardContents(text.substring(startSelection, endSelection));
|
|
|
|
|
String s = getSelectionBeforeText();
|
|
|
|
|
setText(s + getSelectionAfterText());
|
|
|
|
|
endSelection = startSelection = cursorPosition = s.length();
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.isKeyComboCtrlC(i)) {
|
|
|
|
|
if (startSelection != endSelection) {
|
2023-07-30 01:26:36 -05:00
|
|
|
NoppesStringUtils.setClipboardContents(text.substring(startSelection, endSelection));
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.isKeyComboCtrlV(i)) {
|
2023-07-30 01:26:36 -05:00
|
|
|
addText(NoppesStringUtils.getClipboardContents());
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_Z && isCtrlKeyDown()) {
|
|
|
|
|
if (undoList.isEmpty())
|
2023-07-30 01:26:36 -05:00
|
|
|
return false;
|
|
|
|
|
undoing = true;
|
|
|
|
|
redoList.add(new UndoData(this.text, this.cursorPosition));
|
|
|
|
|
UndoData data = undoList.remove(undoList.size() - 1);
|
|
|
|
|
setText(data.text);
|
|
|
|
|
endSelection = startSelection = cursorPosition = data.cursorPosition;
|
|
|
|
|
undoing = false;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_Y && isCtrlKeyDown()) {
|
|
|
|
|
if (redoList.isEmpty())
|
2023-07-30 01:26:36 -05:00
|
|
|
return false;
|
|
|
|
|
undoing = true;
|
|
|
|
|
undoList.add(new UndoData(this.text, this.cursorPosition));
|
|
|
|
|
UndoData data = redoList.remove(redoList.size() - 1);
|
|
|
|
|
setText(data.text);
|
|
|
|
|
endSelection = startSelection = cursorPosition = data.cursorPosition;
|
|
|
|
|
undoing = false;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_TAB) {
|
2025-12-17 20:06:47 +02:00
|
|
|
boolean shift = isShiftKeyDown();
|
|
|
|
|
if (shift) {
|
|
|
|
|
handleShiftTab();
|
|
|
|
|
} else {
|
|
|
|
|
handleTab();
|
|
|
|
|
}
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
|
|
|
|
if (i == Keyboard.KEY_F && isCtrlKeyDown()) {
|
|
|
|
|
formatText();
|
|
|
|
|
return true;
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_RETURN) {
|
2025-12-17 20:06:47 +02:00
|
|
|
if (cursorPosition > 0 && cursorPosition <= text.length() && text.charAt(cursorPosition - 1) == '{') {
|
|
|
|
|
String indent = getIndentCurrentLine();
|
|
|
|
|
String childIndent = indent + " ";
|
|
|
|
|
String before = getSelectionBeforeText();
|
|
|
|
|
String after = getSelectionAfterText();
|
|
|
|
|
|
2025-12-18 02:06:14 +02:00
|
|
|
// If there is non-empty code on the same line after the brace (before the next newline),
|
|
|
|
|
// treat it as code that should be moved into the newly created inner line. In that case
|
|
|
|
|
// just insert the child indent and return — do not insert the closing brace before that code.
|
|
|
|
|
int firstNewline = after.indexOf('\n');
|
|
|
|
|
String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline);
|
|
|
|
|
if (leadingSegment.trim().length() > 0) {
|
|
|
|
|
addText("\n" + childIndent);
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-18 02:06:14 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 00:51:40 +02:00
|
|
|
// 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.
|
2025-12-17 20:06:47 +02:00
|
|
|
boolean hasMatchingCloseSameIndent = false;
|
2025-12-18 00:51:40 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
2025-12-18 00:51:40 +02:00
|
|
|
|
|
|
|
|
if (openLineIdx >= 0) {
|
|
|
|
|
List<int[]> 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
2025-12-18 00:51:40 +02:00
|
|
|
} catch (Exception ex) {
|
|
|
|
|
// Fallback: if anything goes wrong, conservatively behave as before
|
|
|
|
|
hasMatchingCloseSameIndent = false;
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-18 00:51:40 +02:00
|
|
|
|
2025-12-17 20:06:47 +02:00
|
|
|
if (hasMatchingCloseSameIndent) {
|
|
|
|
|
addText("\n" + childIndent);
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 20:06:47 +02:00
|
|
|
} else {
|
|
|
|
|
String insert = "\n" + childIndent + "\n" + indent + "}";
|
|
|
|
|
setText(before + insert + after);
|
|
|
|
|
int newCursor = before.length() + 1 + childIndent.length();
|
|
|
|
|
startSelection = endSelection = cursorPosition = newCursor;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
addText(Character.toString('\n') + getAutoIndentForEnter());
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-03-13 17:14:45 -04:00
|
|
|
if (ChatAllowedCharacters.isAllowedCharacter(c)) {
|
2025-12-17 20:06:47 +02:00
|
|
|
String before = getSelectionBeforeText();
|
|
|
|
|
String after = getSelectionAfterText();
|
|
|
|
|
|
|
|
|
|
// If typing a closing char and the next char is already that closer, skip over it
|
|
|
|
|
if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) {
|
|
|
|
|
// move cursor forward over existing closer
|
|
|
|
|
startSelection = endSelection = cursorPosition = before.length() + 1;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 20:06:47 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (c == '"') {
|
|
|
|
|
setText(before + "\"\"" + after);
|
|
|
|
|
startSelection = endSelection = cursorPosition = before.length() + 1;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 20:06:47 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == '\'') {
|
|
|
|
|
setText(before + "''" + after);
|
|
|
|
|
startSelection = endSelection = cursorPosition = before.length() + 1;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 20:06:47 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == '[') {
|
|
|
|
|
setText(before + "[]" + after);
|
|
|
|
|
startSelection = endSelection = cursorPosition = before.length() + 1;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 20:06:47 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == '(') {
|
|
|
|
|
setText(before + "()" + after);
|
|
|
|
|
startSelection = endSelection = cursorPosition = before.length() + 1;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-17 20:06:47 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
addText(Character.toString(c));
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 09:53:32 +02:00
|
|
|
if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) {
|
2025-12-17 10:17:15 +02:00
|
|
|
if (startSelection != endSelection) {
|
2025-12-17 15:18:33 +02:00
|
|
|
toggleCommentSelection();
|
2025-12-17 10:17:15 +02:00
|
|
|
} else {
|
2025-12-17 15:18:33 +02:00
|
|
|
toggleCommentLineAtCursor();
|
2025-12-17 09:53:32 +02:00
|
|
|
}
|
2025-12-17 15:18:33 +02:00
|
|
|
return true;
|
2025-12-17 09:53:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i == Keyboard.KEY_D && isCtrlKeyDown()) {
|
2025-12-17 10:17:15 +02:00
|
|
|
if (startSelection != endSelection) {
|
|
|
|
|
// Handle multi-line selection duplication
|
|
|
|
|
LineData firstLine = null, lastLine = null;
|
|
|
|
|
for (LineData line : container.lines) {
|
|
|
|
|
if (line.end > startSelection && line.start < endSelection) {
|
|
|
|
|
if (firstLine == null) firstLine = line;
|
|
|
|
|
lastLine = line;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (firstLine != null && lastLine != null) {
|
|
|
|
|
String selectedText = text.substring(firstLine.start, lastLine.end);
|
2025-12-18 01:09:06 +02:00
|
|
|
// Insert the selected block immediately after the last line without adding an extra newline
|
|
|
|
|
String insertText = selectedText;
|
2025-12-17 10:17:15 +02:00
|
|
|
// Save selection before setText
|
|
|
|
|
int savedStart = startSelection;
|
|
|
|
|
int savedEnd = endSelection;
|
2025-12-18 01:09:06 +02:00
|
|
|
int insertAt = lastLine.end;
|
|
|
|
|
setText(text.substring(0, insertAt) + insertText + text.substring(insertAt));
|
|
|
|
|
// Restore cursor and selection
|
2025-12-17 10:17:15 +02:00
|
|
|
startSelection = savedStart;
|
2025-12-18 01:09:06 +02:00
|
|
|
endSelection = this.cursorPosition = savedEnd;
|
2025-12-17 09:53:32 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-17 10:17:15 +02:00
|
|
|
} else {
|
|
|
|
|
// Single line duplication (existing logic)
|
|
|
|
|
for (LineData line : container.lines) {
|
|
|
|
|
if (cursorPosition >= line.start && cursorPosition <= line.end) {
|
|
|
|
|
int lineStart = line.start, lineEnd = line.end;
|
|
|
|
|
String lineText = text.substring(lineStart, lineEnd);
|
2025-12-18 01:09:06 +02:00
|
|
|
// Avoid inserting an extra blank line: if lineText already ends with a newline,
|
|
|
|
|
// reuse it; otherwise prepend a newline so the duplicate appears immediately after.
|
|
|
|
|
String insertText;
|
|
|
|
|
if (lineText.endsWith("\n")) {
|
|
|
|
|
insertText = lineText;
|
|
|
|
|
} else {
|
|
|
|
|
insertText = "\n" + lineText;
|
|
|
|
|
}
|
|
|
|
|
int insertionPoint = lineEnd;
|
|
|
|
|
setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint));
|
|
|
|
|
// Place cursor at end of duplicated line (just before any trailing newline)
|
|
|
|
|
int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0);
|
|
|
|
|
startSelection = endSelection = cursorPosition = Math.max(0, Math.min(newCursor, this.text.length()));
|
2025-12-17 10:17:15 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 09:53:32 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
private boolean isShiftKeyDown() {
|
2023-07-30 01:26:36 -05:00
|
|
|
return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 21:10:25 +02:00
|
|
|
// ==================== COMMENT TOGGLING ====================
|
|
|
|
|
// Uses CommentHandler helper for comment operations
|
|
|
|
|
|
2025-12-17 15:18:33 +02:00
|
|
|
private void toggleCommentSelection() {
|
2025-12-18 21:10:25 +02:00
|
|
|
CommentHandler.SelectionToggleResult result = CommentHandler.toggleCommentSelection(
|
|
|
|
|
text, container.lines, startSelection, endSelection);
|
|
|
|
|
setText(result.newText);
|
|
|
|
|
startSelection = result.newStartSelection;
|
|
|
|
|
endSelection = result.newEndSelection;
|
2025-12-17 15:18:33 +02:00
|
|
|
cursorPosition = endSelection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void toggleCommentLineAtCursor() {
|
2025-12-18 21:10:25 +02:00
|
|
|
CommentHandler.SingleLineToggleResult result = CommentHandler.toggleCommentAtCursor(
|
|
|
|
|
text, container.lines, cursorPosition);
|
|
|
|
|
setText(result.newText);
|
|
|
|
|
setCursor(result.newCursorPosition, false);
|
2025-12-17 15:18:33 +02:00
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== KEYBOARD MODIFIERS ====================
|
2025-12-17 15:18:33 +02:00
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
private boolean isAltKeyDown() {
|
2023-07-30 01:26:36 -05:00
|
|
|
return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
private boolean isKeyComboCtrlX(int keyID) {
|
2023-07-30 01:26:36 -05:00
|
|
|
return keyID == 45 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
private boolean isKeyComboCtrlBackspace(int keyID) {
|
2023-07-30 15:47:03 -04:00
|
|
|
return keyID == Keyboard.KEY_BACK && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
private boolean isKeyComboCtrlV(int keyID) {
|
2023-07-30 01:26:36 -05:00
|
|
|
return keyID == 47 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
private boolean isKeyComboCtrlC(int keyID) {
|
2023-07-30 01:26:36 -05:00
|
|
|
return keyID == 46 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
private boolean isKeyComboCtrlA(int keyID) {
|
2023-07-30 01:26:36 -05:00
|
|
|
return keyID == 30 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String getIndentCurrentLine() {
|
2025-12-18 21:10:25 +02:00
|
|
|
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);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
return "";
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 18:35:26 +02:00
|
|
|
private String getAutoIndentForEnter() {
|
|
|
|
|
LineData currentLine = null;
|
|
|
|
|
for (LineData line : this.container.lines) {
|
2025-12-18 01:00:27 +02:00
|
|
|
if (this.cursorPosition >= line.start && this.cursorPosition < line.end) {
|
2025-12-17 18:35:26 +02:00
|
|
|
currentLine = line;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentLine == null) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String lineText = currentLine.text;
|
|
|
|
|
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(this.cursorPosition - currentLine.start, lineText.length()));
|
|
|
|
|
String beforeCursor = lineText.substring(0, relativeCursor);
|
|
|
|
|
|
2025-12-17 23:54:36 +02:00
|
|
|
// Find last non-whitespace before cursor
|
2025-12-17 18:35:26 +02:00
|
|
|
int lastNonWs = -1;
|
|
|
|
|
for (int idx = beforeCursor.length() - 1; idx >= 0; idx--) {
|
|
|
|
|
if (!Character.isWhitespace(beforeCursor.charAt(idx))) {
|
|
|
|
|
lastNonWs = idx;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 23:54:36 +02:00
|
|
|
// 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) == '{';
|
2025-12-17 18:35:26 +02:00
|
|
|
if (opensBlock) {
|
|
|
|
|
return baseIndent + " ";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return baseIndent;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 20:06:47 +02:00
|
|
|
private int getLineIndent(String lineText) {
|
|
|
|
|
int leading = 0;
|
|
|
|
|
while (leading < lineText.length() && (lineText.charAt(leading) == ' ' || lineText.charAt(
|
|
|
|
|
leading) == '\t')) {
|
|
|
|
|
leading++;
|
|
|
|
|
}
|
|
|
|
|
return leading;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 23:51:28 +02:00
|
|
|
// Calculate the expected/proper indent for the current line based on surrounding context
|
|
|
|
|
private int getExpectedIndent(LineData currentLine) {
|
|
|
|
|
if (currentLine == null) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int idx = container.lines.indexOf(currentLine);
|
|
|
|
|
if (idx <= 0) {
|
|
|
|
|
return 0; // First line has no expected indent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the previous non-empty line
|
|
|
|
|
LineData prevLine = null;
|
|
|
|
|
for (int i = idx - 1; i >= 0; i--) {
|
|
|
|
|
LineData line = container.lines.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 indent is one level back
|
|
|
|
|
if (currTrimmed.startsWith("}")) {
|
|
|
|
|
return Math.max(0, prevIndent - getTabSize());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If previous line ends with {, expected indent is one level forward
|
|
|
|
|
if (prevTrimmed.endsWith("{")) {
|
|
|
|
|
return prevIndent + getTabSize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise, expected indent matches previous line
|
|
|
|
|
return prevIndent;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 21:10:25 +02:00
|
|
|
// ==================== TEXT FORMATTING ====================
|
|
|
|
|
|
2025-12-17 20:06:47 +02:00
|
|
|
private int getTabSize() {
|
2025-12-18 21:10:25 +02:00
|
|
|
return IndentHelper.TAB_SIZE;
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String repeatSpace(int count) {
|
2025-12-18 21:10:25 +02:00
|
|
|
return IndentHelper.spaces(count);
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void formatText() {
|
2025-12-18 21:10:25 +02:00
|
|
|
IndentHelper.FormatResult result = IndentHelper.formatText(text, cursorPosition);
|
|
|
|
|
setText(result.text);
|
|
|
|
|
startSelection = endSelection = cursorPosition = Math.max(0, Math.min(result.cursorPosition, this.text.length()));
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== TAB HANDLING ====================
|
2025-12-17 20:06:47 +02:00
|
|
|
|
|
|
|
|
private void handleTab() {
|
|
|
|
|
LineData currentLine = null;
|
2025-12-18 19:07:05 +02:00
|
|
|
for (int i = 0; i < this.container.lines.size(); i++) {
|
|
|
|
|
LineData line = this.container.lines.get(i);
|
|
|
|
|
boolean isLastLine = (i == this.container.lines.size() - 1);
|
|
|
|
|
boolean isOnLine = isLastLine
|
|
|
|
|
? (this.cursorPosition >= line.start && this.cursorPosition <= line.end)
|
|
|
|
|
: (this.cursorPosition >= line.start && this.cursorPosition < line.end);
|
|
|
|
|
if (isOnLine) {
|
2025-12-17 20:06:47 +02:00
|
|
|
currentLine = line;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (currentLine == null) {
|
|
|
|
|
addText(" ");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
int tab = getTabSize();
|
|
|
|
|
int indentLen = getLineIndent(currentLine.text);
|
|
|
|
|
int textStartPos = currentLine.start + indentLen;
|
|
|
|
|
|
|
|
|
|
if (this.cursorPosition <= textStartPos) {
|
|
|
|
|
// Cursor before any text: if cursor is exactly at text start, move forward to next tab stop.
|
|
|
|
|
// If cursor is inside leading whitespace (before text start), choose nearest tab stop (tie -> forward).
|
|
|
|
|
int targetIndent;
|
|
|
|
|
if (this.cursorPosition == textStartPos) {
|
|
|
|
|
targetIndent = ((indentLen / tab) + 1) * tab;
|
|
|
|
|
} else {
|
|
|
|
|
int remainder = indentLen % tab;
|
|
|
|
|
if (remainder == 0) {
|
|
|
|
|
targetIndent = indentLen + tab; // already aligned -> next level
|
|
|
|
|
} else {
|
|
|
|
|
int down = indentLen - remainder;
|
|
|
|
|
int up = indentLen + (tab - remainder);
|
|
|
|
|
int distDown = remainder;
|
|
|
|
|
int distUp = tab - remainder;
|
|
|
|
|
if (distUp < distDown) targetIndent = up;
|
|
|
|
|
else if (distUp > distDown) targetIndent = down;
|
|
|
|
|
else targetIndent = up; // tie -> forward
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (targetIndent < 0) targetIndent = 0;
|
|
|
|
|
String newIndent = repeatSpace(targetIndent);
|
|
|
|
|
String rest = currentLine.text.substring(indentLen);
|
|
|
|
|
String before = text.substring(0, currentLine.start);
|
|
|
|
|
int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length());
|
|
|
|
|
int sepEnd = Math.min(currentLine.end, text.length());
|
|
|
|
|
String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : "";
|
|
|
|
|
String after = text.substring(sepEnd);
|
|
|
|
|
setText(before + newIndent + rest + sep + after);
|
|
|
|
|
int newCursor = currentLine.start + targetIndent;
|
|
|
|
|
startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length());
|
|
|
|
|
} else {
|
|
|
|
|
// Cursor is after start of text: insert spaces at cursor to move following text to next tab stop
|
|
|
|
|
int column = this.cursorPosition - currentLine.start;
|
|
|
|
|
int targetColumn = ((column / tab) + 1) * tab;
|
|
|
|
|
int toInsert = Math.max(0, targetColumn - column);
|
|
|
|
|
if (toInsert > 0) {
|
|
|
|
|
String spaces = repeatSpace(toInsert);
|
|
|
|
|
addText(spaces);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void handleShiftTab() {
|
|
|
|
|
LineData currentLine = null;
|
2025-12-18 19:07:05 +02:00
|
|
|
for (int i = 0; i < this.container.lines.size(); i++) {
|
|
|
|
|
LineData line = this.container.lines.get(i);
|
|
|
|
|
boolean isLastLine = (i == this.container.lines.size() - 1);
|
|
|
|
|
boolean isOnLine = isLastLine
|
|
|
|
|
? (this.cursorPosition >= line.start && this.cursorPosition <= line.end)
|
|
|
|
|
: (this.cursorPosition >= line.start && this.cursorPosition < line.end);
|
|
|
|
|
if (isOnLine) {
|
2025-12-17 20:06:47 +02:00
|
|
|
currentLine = line;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (currentLine == null)
|
|
|
|
|
return;
|
|
|
|
|
int tab = getTabSize();
|
|
|
|
|
int indentLen = getLineIndent(currentLine.text);
|
|
|
|
|
int textStartPos = currentLine.start + indentLen;
|
|
|
|
|
|
|
|
|
|
if (this.cursorPosition <= textStartPos) {
|
|
|
|
|
// Cursor before any text: reduce leading indent to previous tab stop
|
|
|
|
|
int targetIndent = Math.max(0, ((indentLen - 1) / tab) * tab);
|
|
|
|
|
String newIndent = repeatSpace(targetIndent);
|
|
|
|
|
String rest = currentLine.text.substring(indentLen);
|
|
|
|
|
String before = text.substring(0, currentLine.start);
|
|
|
|
|
int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length());
|
|
|
|
|
int sepEnd = Math.min(currentLine.end, text.length());
|
|
|
|
|
String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : "";
|
|
|
|
|
String after = text.substring(sepEnd);
|
|
|
|
|
setText(before + newIndent + rest + sep + after);
|
|
|
|
|
int newCursor = currentLine.start + targetIndent;
|
|
|
|
|
startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length());
|
|
|
|
|
} else {
|
|
|
|
|
// Cursor after start of text: remove up to previous tab stop worth of spaces immediately before cursor
|
|
|
|
|
int column = this.cursorPosition - currentLine.start;
|
|
|
|
|
int mod = column % tab;
|
|
|
|
|
int toRemove = mod == 0 ? tab : mod;
|
|
|
|
|
int removed = 0;
|
|
|
|
|
int pos = this.cursorPosition - 1;
|
|
|
|
|
while (pos >= currentLine.start && removed < toRemove && text.charAt(pos) == ' ') {
|
|
|
|
|
pos--;
|
|
|
|
|
removed++;
|
|
|
|
|
}
|
|
|
|
|
if (removed > 0) {
|
|
|
|
|
int removeStart = pos + 1;
|
|
|
|
|
String before = text.substring(0, removeStart);
|
|
|
|
|
String after = text.substring(this.cursorPosition);
|
|
|
|
|
setText(before + after);
|
|
|
|
|
int newCursor = removeStart;
|
|
|
|
|
startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== CURSOR MANAGEMENT ====================
|
2025-12-17 20:06:47 +02:00
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
private void setCursor(int i, boolean select) {
|
|
|
|
|
i = Math.min(Math.max(i, 0), this.text.length());
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i != this.cursorPosition) {
|
|
|
|
|
if (!select) {
|
2023-07-30 01:26:36 -05:00
|
|
|
this.endSelection = this.startSelection = this.cursorPosition = i;
|
|
|
|
|
} else {
|
|
|
|
|
int diff = this.cursorPosition - i;
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.cursorPosition == this.startSelection) {
|
2023-07-30 01:26:36 -05:00
|
|
|
this.startSelection -= diff;
|
2025-03-13 17:14:45 -04:00
|
|
|
} else if (this.cursorPosition == this.endSelection) {
|
2023-07-30 01:26:36 -05:00
|
|
|
this.endSelection -= diff;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.startSelection > this.endSelection) {
|
2023-07-30 01:26:36 -05:00
|
|
|
int j = this.endSelection;
|
|
|
|
|
this.endSelection = this.startSelection;
|
|
|
|
|
this.startSelection = j;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.cursorPosition = i;
|
|
|
|
|
}
|
2025-10-15 06:28:43 -04:00
|
|
|
|
|
|
|
|
clampSelectionBounds();
|
2025-12-18 11:23:54 +02:00
|
|
|
// Moving the cursor is user activity — pause blinking briefly
|
|
|
|
|
this.lastInputTime = System.currentTimeMillis();
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void addText(String s) {
|
|
|
|
|
this.setText(this.getSelectionBeforeText() + s + this.getSelectionAfterText());
|
|
|
|
|
this.endSelection = this.startSelection = this.cursorPosition = this.startSelection + s.length();
|
2025-12-18 11:23:54 +02:00
|
|
|
this.lastInputTime = System.currentTimeMillis();
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int cursorUp() {
|
2025-03-13 17:14:45 -04:00
|
|
|
for (int i = 0; i < this.container.lines.size(); ++i) {
|
|
|
|
|
LineData data = (LineData) this.container.lines.get(i);
|
2025-12-18 19:07:05 +02:00
|
|
|
// For last line, use <= since end doesn't include +1 for newline
|
|
|
|
|
// For other lines, use < since end includes the newline position
|
|
|
|
|
boolean isLastLine = (i == this.container.lines.size() - 1);
|
|
|
|
|
boolean isOnLine = isLastLine
|
|
|
|
|
? (this.cursorPosition >= data.start && this.cursorPosition <= data.end)
|
|
|
|
|
: (this.cursorPosition >= data.start && this.cursorPosition < data.end);
|
|
|
|
|
if (isOnLine) {
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == 0) {
|
2023-07-30 01:26:36 -05:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 19:07:05 +02:00
|
|
|
int column = Math.min(this.cursorPosition - data.start, data.text.length());
|
2025-12-17 20:06:47 +02:00
|
|
|
LineData target = this.container.lines.get(i - 1);
|
2025-12-18 19:07:05 +02:00
|
|
|
// Calculate target position on the line above, maintaining column if possible
|
|
|
|
|
int targetColumn = Math.min(column, target.text.length());
|
2025-12-17 20:06:47 +02:00
|
|
|
int targetIndent = getLineIndent(target.text);
|
2025-12-18 19:07:05 +02:00
|
|
|
int minPos = target.start + Math.min(targetIndent, target.text.length());
|
|
|
|
|
int targetPos = target.start + targetColumn;
|
2025-12-17 20:06:47 +02:00
|
|
|
return Math.max(minPos, targetPos);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int cursorDown() {
|
2025-12-17 14:33:17 +02:00
|
|
|
if (cursorPosition == text.length())
|
|
|
|
|
return cursorPosition;
|
2025-03-13 17:14:45 -04:00
|
|
|
for (int i = 0; i < this.container.lines.size(); ++i) {
|
|
|
|
|
LineData data = (LineData) this.container.lines.get(i);
|
2025-12-18 19:07:05 +02:00
|
|
|
// For last line, use <= since end doesn't include +1 for newline
|
|
|
|
|
// For other lines, use < since end includes the newline position
|
|
|
|
|
boolean isLastLine = (i == this.container.lines.size() - 1);
|
|
|
|
|
boolean isOnLine = isLastLine
|
|
|
|
|
? (this.cursorPosition >= data.start && this.cursorPosition <= data.end)
|
|
|
|
|
: (this.cursorPosition >= data.start && this.cursorPosition < data.end);
|
|
|
|
|
if (isOnLine) {
|
|
|
|
|
// If already on last line, stay at current position
|
|
|
|
|
if (i >= this.container.lines.size() - 1) {
|
|
|
|
|
return cursorPosition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int column = Math.min(this.cursorPosition - data.start, data.text.length());
|
|
|
|
|
LineData target = this.container.lines.get(i + 1);
|
|
|
|
|
// Calculate target position on the line below, maintaining column if possible
|
|
|
|
|
int targetColumn = Math.min(column, target.text.length());
|
2025-12-17 20:06:47 +02:00
|
|
|
int targetIndent = getLineIndent(target.text);
|
2025-12-18 19:07:05 +02:00
|
|
|
int minPos = target.start + Math.min(targetIndent, target.text.length());
|
|
|
|
|
int targetPos = target.start + targetColumn;
|
2025-12-17 20:06:47 +02:00
|
|
|
return Math.max(minPos, targetPos);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.text.length();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getSelectionBeforeText() {
|
2025-03-13 17:14:45 -04:00
|
|
|
return this.startSelection == 0 ? "" : this.text.substring(0, this.startSelection);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getSelectionAfterText() {
|
|
|
|
|
return this.text.substring(this.endSelection);
|
|
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== MOUSE HANDLING ====================
|
2023-07-30 01:26:36 -05:00
|
|
|
|
|
|
|
|
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;
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.active) {
|
2023-07-30 01:26:36 -05:00
|
|
|
this.startSelection = this.endSelection = this.cursorPosition = this.getSelectionPos(xMouse, yMouse);
|
2025-12-18 11:23:54 +02:00
|
|
|
this.lastInputTime = System.currentTimeMillis();
|
2023-07-30 01:26:36 -05:00
|
|
|
this.clicked = mouseButton == 0;
|
|
|
|
|
this.doubleClicked = false;
|
2025-12-18 10:00:02 +02:00
|
|
|
this.tripleClicked = false;
|
2023-07-30 01:26:36 -05:00
|
|
|
long time = System.currentTimeMillis();
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.clicked && this.container.linesCount * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) {
|
2023-07-30 01:26:36 -05:00
|
|
|
this.clicked = false;
|
|
|
|
|
this.clickScrolling = true;
|
2025-12-18 13:39:32 +02:00
|
|
|
double linesCountD = Math.max(1, (double) this.container.linesCount);
|
|
|
|
|
int thumbTop = (int) (this.y + 1f * this.scrollPos / linesCountD * (this.height - 4)) + 1;
|
|
|
|
|
this.scrollbarDragOffset = yMouse - thumbTop;
|
2025-12-18 10:00:02 +02:00
|
|
|
} else {
|
|
|
|
|
if (time - this.lastClicked < 300L) {
|
|
|
|
|
this.clickCount++;
|
|
|
|
|
} else {
|
|
|
|
|
this.clickCount = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.clickCount == 2) {
|
|
|
|
|
// Double-click: select word under cursor (restore prior behavior)
|
|
|
|
|
this.doubleClicked = true;
|
|
|
|
|
Matcher m = this.container.regexWord.matcher(this.text);
|
|
|
|
|
while (m.find()) {
|
2025-12-18 10:48:34 +02:00
|
|
|
if (this.cursorPosition >= m.start() && this.cursorPosition <= m.end()) {
|
2025-12-18 10:00:02 +02:00
|
|
|
this.startSelection = m.start();
|
|
|
|
|
this.endSelection = m.end();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (this.clickCount >= 3) {
|
|
|
|
|
// Triple-click: select entire line
|
|
|
|
|
this.tripleClicked = true;
|
|
|
|
|
for (LineData line : container.lines) {
|
|
|
|
|
if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) {
|
|
|
|
|
this.startSelection = line.start;
|
|
|
|
|
this.endSelection = line.end;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-12-18 10:00:02 +02:00
|
|
|
this.clickCount = 0;
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.lastClicked = time;
|
|
|
|
|
activeTextfield = this;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void updateCursorCounter() {
|
|
|
|
|
++this.cursorCounter;
|
|
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== TEXT MANAGEMENT ====================
|
2023-07-30 01:26:36 -05:00
|
|
|
|
|
|
|
|
public void setText(String text) {
|
|
|
|
|
if (text == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
text = text.replace("\r", "");
|
|
|
|
|
text = text.replace("\t", " ");
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.text == null || !this.text.equals(text)) {
|
|
|
|
|
if (this.listener != null) {
|
2023-07-30 01:26:36 -05:00
|
|
|
this.listener.textUpdate(text);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
if (!this.undoing) {
|
2023-07-30 01:26:36 -05:00
|
|
|
this.undoList.add(new GuiScriptTextArea.UndoData(this.text, this.cursorPosition));
|
|
|
|
|
this.redoList.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.text = text;
|
2025-12-16 18:33:55 +02:00
|
|
|
//this.container = new TextContainer(text);
|
|
|
|
|
this.container = new JavaTextContainer(text);
|
2023-07-30 01:26:36 -05:00
|
|
|
this.container.init(this.width, this.height);
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.enableCodeHighlighting) {
|
2023-07-30 01:26:36 -05:00
|
|
|
this.container.formatCodeText();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 19:07:05 +02:00
|
|
|
// Ensure scroll state stays in bounds after text change
|
2025-12-18 12:05:57 +02:00
|
|
|
int maxScroll = Math.max(0, this.container.linesCount - this.container.visibleLines);
|
|
|
|
|
if (this.scrolledLine > maxScroll) {
|
|
|
|
|
this.scrolledLine = maxScroll;
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-12-18 19:07:05 +02:00
|
|
|
if (this.targetScroll > maxScroll) {
|
|
|
|
|
this.targetScroll = maxScroll;
|
|
|
|
|
}
|
|
|
|
|
if (this.scrollPos > maxScroll) {
|
|
|
|
|
this.scrollPos = maxScroll;
|
|
|
|
|
}
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-10-15 06:28:43 -04:00
|
|
|
clampSelectionBounds();
|
|
|
|
|
|
2025-12-18 11:23:54 +02:00
|
|
|
// Consider text changes user activity to pause caret blinking briefly
|
|
|
|
|
this.lastInputTime = System.currentTimeMillis();
|
|
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getText() {
|
|
|
|
|
return this.text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isEnabled() {
|
|
|
|
|
return this.enabled && this.visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean hasVerticalScrollbar() {
|
|
|
|
|
return this.container.visibleLines < this.container.linesCount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void enableCodeHighlighting() {
|
|
|
|
|
this.enableCodeHighlighting = true;
|
|
|
|
|
this.container.formatCodeText();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setListener(ITextChangeListener listener) {
|
|
|
|
|
this.listener = listener;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 06:28:43 -04:00
|
|
|
private void clampSelectionBounds() {
|
|
|
|
|
if (this.text == null || this.text.isEmpty()) {
|
|
|
|
|
this.startSelection = this.endSelection = this.cursorPosition = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int length = this.text.length();
|
|
|
|
|
this.startSelection = Math.max(0, Math.min(this.startSelection, length));
|
|
|
|
|
this.endSelection = Math.max(0, Math.min(this.endSelection, length));
|
|
|
|
|
this.cursorPosition = Math.max(0, Math.min(this.cursorPosition, length));
|
|
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== INNER CLASSES ====================
|
2025-12-17 15:18:33 +02:00
|
|
|
|
2023-07-30 01:26:36 -05:00
|
|
|
class UndoData {
|
|
|
|
|
public String text;
|
|
|
|
|
public int cursorPosition;
|
|
|
|
|
|
|
|
|
|
public UndoData(String text, int cursorPosition) {
|
|
|
|
|
this.text = text;
|
|
|
|
|
this.cursorPosition = cursorPosition;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|