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-19 16:04:07 +02:00
|
|
|
import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer;
|
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;
|
2025-12-19 14:09:55 +02:00
|
|
|
import noppes.npcs.client.key.impl.ScriptEditorKeys;
|
2025-12-22 03:39:22 +02:00
|
|
|
import noppes.npcs.util.ValueUtil;
|
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;
|
2025-12-19 18:25:32 +02:00
|
|
|
import java.util.function.Supplier;
|
2023-07-30 01:26:36 -05:00
|
|
|
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:
|
2025-12-18 22:11:42 +02:00
|
|
|
* - ScrollState: smooth scroll animation and state management
|
|
|
|
|
* - SelectionState: cursor position and text selection management
|
2025-12-18 21:10:25 +02:00
|
|
|
* - 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 x;
|
|
|
|
|
public int y;
|
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;
|
|
|
|
|
private int clickCount = 0;
|
2025-12-18 21:10:25 +02:00
|
|
|
private long lastClicked = 0L;
|
|
|
|
|
|
|
|
|
|
// ==================== TEXT & CONTAINER ====================
|
|
|
|
|
public String text = null;
|
2025-12-20 02:02:43 +02:00
|
|
|
public String highlightedWord;
|
2025-12-18 21:10:25 +02:00
|
|
|
private JavaTextContainer container = null;
|
|
|
|
|
private boolean enableCodeHighlighting = false;
|
2025-12-20 20:59:39 +02:00
|
|
|
// Extra empty lines to allow padding at the bottom of the editor viewport
|
|
|
|
|
private int bottomPaddingLines = 6;
|
|
|
|
|
|
2025-12-22 13:28:25 +02:00
|
|
|
// Search bar layout tracking to allow idempotent/resilient resizing
|
|
|
|
|
private int searchBaseY = 0;
|
|
|
|
|
private int searchBaseHeight = 0;
|
|
|
|
|
private int searchAppliedOffset = 0;
|
|
|
|
|
private boolean searchBaseInitialized = false;
|
|
|
|
|
|
2025-12-20 20:59:39 +02:00
|
|
|
private int getPaddedLineCount() {
|
|
|
|
|
if (container == null) return 0;
|
|
|
|
|
// Only add bottom padding when the content is already scrollable. This avoids
|
|
|
|
|
// introducing a scrollbar when there's nothing to scroll for.
|
|
|
|
|
if (container.linesCount > container.visibleLines - bottomPaddingLines) {
|
|
|
|
|
return Math.max(0, container.linesCount + bottomPaddingLines);
|
|
|
|
|
} else {
|
|
|
|
|
return container.linesCount;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 22:11:42 +02:00
|
|
|
|
|
|
|
|
// ==================== HELPER CLASS INSTANCES ====================
|
|
|
|
|
private final ScrollState scroll = new ScrollState();
|
|
|
|
|
private final SelectionState selection = new SelectionState();
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== UI COMPONENTS ====================
|
|
|
|
|
private int cursorCounter;
|
|
|
|
|
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;
|
2025-12-19 14:09:55 +02:00
|
|
|
|
|
|
|
|
// ==================== KEYS ====================
|
|
|
|
|
public static final ScriptEditorKeys KEYS = new ScriptEditorKeys();
|
2025-12-19 16:04:07 +02:00
|
|
|
public OverlayKeyPresetViewer KEYS_OVERLAY = new OverlayKeyPresetViewer(KEYS);
|
|
|
|
|
|
2025-12-19 19:48:08 +02:00
|
|
|
// ==================== SEARCH/REPLACE ====================
|
2025-12-22 02:39:48 +02:00
|
|
|
private static final SearchReplaceBar searchBar = new SearchReplaceBar();
|
2025-12-19 22:38:26 +02:00
|
|
|
|
|
|
|
|
// ==================== GO TO LINE ====================
|
2025-12-22 02:39:48 +02:00
|
|
|
private final GoToLineDialog goToLineDialog = new GoToLineDialog();
|
2025-12-19 14:09:55 +02:00
|
|
|
|
2025-12-21 00:49:34 +02:00
|
|
|
// ==================== RENAME REFACTOR ====================
|
|
|
|
|
private final RenameRefactorHandler renameHandler = new RenameRefactorHandler();
|
|
|
|
|
|
2025-12-18 21:10:25 +02:00
|
|
|
// ==================== 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);
|
2025-12-22 02:39:48 +02:00
|
|
|
init(x, y, width, height, text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void init(int x, int y, int width, int height, String text) {
|
|
|
|
|
this.x = xPosition = x;
|
|
|
|
|
this.y = yPosition = y;
|
|
|
|
|
this.width = width;
|
|
|
|
|
this.height = height;
|
|
|
|
|
this.undoing = true;
|
|
|
|
|
this.setText(text);
|
|
|
|
|
this.undoing = false;
|
|
|
|
|
setCallbacks();
|
2025-12-22 13:28:25 +02:00
|
|
|
// Reset search-base tracking whenever the editor is (re)initialized
|
|
|
|
|
this.searchBaseY = 0;
|
|
|
|
|
this.searchBaseHeight = 0;
|
|
|
|
|
this.searchAppliedOffset = 0;
|
|
|
|
|
this.searchBaseInitialized = false;
|
2025-12-22 13:43:58 +02:00
|
|
|
|
|
|
|
|
KEYS_OVERLAY.openOnClick = true;
|
2025-12-22 02:39:48 +02:00
|
|
|
initGui();
|
|
|
|
|
initializeKeyBindings();
|
|
|
|
|
}
|
|
|
|
|
public void initGui() {
|
|
|
|
|
int endX = x + width, endY = y + height;
|
|
|
|
|
int xOffset = hasVerticalScrollbar() ? -8 : -2;
|
2025-12-22 14:37:08 +02:00
|
|
|
KEYS_OVERLAY.scale = 0.75f;
|
|
|
|
|
KEYS_OVERLAY.borderCol1 = KEYS_OVERLAY.borderCol2 = 0xFF3c3c3c;
|
|
|
|
|
KEYS_OVERLAY.initGui(x + (width / 4), y + 20, endX - (width / 4) + 5, endY - 28);
|
2025-12-22 02:39:48 +02:00
|
|
|
|
|
|
|
|
KEYS_OVERLAY.viewButton.scale = 0.45f;
|
|
|
|
|
KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26);
|
|
|
|
|
|
|
|
|
|
// Initialize search bar (preserves state across initGui calls)
|
|
|
|
|
searchBar.initGui(x, y, width);
|
|
|
|
|
if (searchBar.isVisible()) { // If open
|
2025-12-22 13:28:25 +02:00
|
|
|
// Shift viewport down again using the bar's current height
|
|
|
|
|
searchBar.callback.resizeEditor(true, searchBar.getTotalHeight());
|
2025-12-22 02:39:48 +02:00
|
|
|
if (!active) // Focus search if opening another script tab & bar is open
|
|
|
|
|
searchBar.focus(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize Go To Line dialog
|
|
|
|
|
goToLineDialog.initGui(x, y, width);
|
|
|
|
|
}
|
2025-12-20 00:31:30 +02:00
|
|
|
|
2025-12-22 02:39:48 +02:00
|
|
|
public void setCallbacks() {
|
2025-12-20 00:31:30 +02:00
|
|
|
searchBar.setCallback(new SearchReplaceBar.SearchCallback() {
|
|
|
|
|
@Override
|
|
|
|
|
public String getText() {
|
2025-12-20 02:00:58 +02:00
|
|
|
return GuiScriptTextArea.this.text;
|
2025-12-20 00:31:30 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-20 02:02:43 +02:00
|
|
|
public String getHighlightedWord() {
|
|
|
|
|
return GuiScriptTextArea.this.highlightedWord;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-20 20:30:04 +02:00
|
|
|
@Override
|
|
|
|
|
public int getSelectionStart() {
|
|
|
|
|
return GuiScriptTextArea.this.selection.getStartSelection();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getSelectionEnd() {
|
|
|
|
|
return GuiScriptTextArea.this.selection.getEndSelection();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-20 00:31:30 +02:00
|
|
|
@Override
|
|
|
|
|
public void setText(String newText) {
|
|
|
|
|
GuiScriptTextArea.this.setText(newText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void scrollToPosition(int position) {
|
|
|
|
|
// Find line containing position and scroll to it
|
2025-12-22 02:39:48 +02:00
|
|
|
if (container == null || container.lines == null)
|
|
|
|
|
return;
|
2025-12-20 00:31:30 +02:00
|
|
|
|
|
|
|
|
// Calculate offset to account for search bar height
|
|
|
|
|
int searchBarOffset = searchBar.getTotalHeight();
|
2025-12-20 19:51:19 +02:00
|
|
|
int effectiveHeight = GuiScriptTextArea.this.height - searchBarOffset;
|
2025-12-20 00:31:30 +02:00
|
|
|
int visibleLines = effectiveHeight / container.lineHeight;
|
|
|
|
|
// Calculate how many lines the search bar covers
|
2025-12-22 02:39:48 +02:00
|
|
|
int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil(
|
|
|
|
|
(double) searchBarOffset / container.lineHeight) : 0;
|
2025-12-20 00:31:30 +02:00
|
|
|
|
|
|
|
|
for (int i = 0; i < container.lines.size(); i++) {
|
|
|
|
|
LineData ld = container.lines.get(i);
|
|
|
|
|
if (position >= ld.start && position < ld.end) {
|
|
|
|
|
int visible = Math.max(1, visibleLines);
|
2025-12-20 20:59:39 +02:00
|
|
|
int effectiveVisible = Math.max(1, visible - bottomPaddingLines);
|
|
|
|
|
int maxScroll = Math.max(0, getPaddedLineCount() - visible);
|
2025-12-20 00:31:30 +02:00
|
|
|
int targetLine = i;
|
|
|
|
|
|
|
|
|
|
// If search bar is visible and would hide this line, scroll down so it's visible
|
|
|
|
|
// The target line should appear below the search bar, not under it
|
|
|
|
|
int currentScroll = scroll.getScrolledLine();
|
|
|
|
|
int firstVisibleLine = currentScroll + linesHiddenBySRB;
|
|
|
|
|
|
|
|
|
|
if (searchBarOffset > 0 && targetLine < firstVisibleLine) {
|
|
|
|
|
// Force scroll so target line appears just below the search bar
|
|
|
|
|
scroll.setTargetScroll(Math.max(0, targetLine - linesHiddenBySRB), maxScroll);
|
|
|
|
|
} else {
|
2025-12-20 20:59:39 +02:00
|
|
|
scroll.scrollToLine(targetLine, effectiveVisible, maxScroll);
|
2025-12-20 00:31:30 +02:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setSelection(int start, int end) {
|
|
|
|
|
selection.setSelection(start, end);
|
|
|
|
|
selection.setCursorPositionDirect(end);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getGutterWidth() {
|
|
|
|
|
return LINE_NUMBER_GUTTER_WIDTH;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void unfocusMainEditor() {
|
|
|
|
|
// Save position but unfocus
|
|
|
|
|
active = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void focusMainEditor() {
|
|
|
|
|
active = true;
|
2025-12-20 03:11:17 +02:00
|
|
|
searchBar.resetSelection();
|
2025-12-20 00:31:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onMatchesUpdated() {
|
|
|
|
|
// Called when matches change - could be used for UI updates
|
|
|
|
|
}
|
2025-12-20 19:50:28 +02:00
|
|
|
|
2025-12-22 13:28:25 +02:00
|
|
|
// Robust resize: use base editor bounds and apply the requested offset
|
|
|
|
|
public void resizeEditor(boolean open, int barHeight) {
|
|
|
|
|
int desiredOffset = Math.max(0, barHeight);
|
|
|
|
|
// Initialize base values if not set
|
|
|
|
|
if (!searchBaseInitialized) {
|
|
|
|
|
searchBaseY = GuiScriptTextArea.this.y;
|
|
|
|
|
searchBaseHeight = GuiScriptTextArea.this.height;
|
|
|
|
|
searchAppliedOffset = 0;
|
|
|
|
|
searchBaseInitialized = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int targetOffset = open ? desiredOffset : 0;
|
|
|
|
|
if (targetOffset == searchAppliedOffset)
|
|
|
|
|
return; // already in desired state
|
|
|
|
|
|
|
|
|
|
// Compute new bounds from base values (idempotent)
|
|
|
|
|
int newY = searchBaseY + targetOffset;
|
|
|
|
|
int newHeight = Math.max(12, searchBaseHeight - targetOffset);
|
|
|
|
|
|
|
|
|
|
GuiScriptTextArea.this.y = newY;
|
|
|
|
|
GuiScriptTextArea.this.height = newHeight;
|
|
|
|
|
searchAppliedOffset = targetOffset;
|
|
|
|
|
|
|
|
|
|
if (container != null)
|
|
|
|
|
container.visibleLines = Math.max(GuiScriptTextArea.this.height / container.lineHeight - 1, 1);
|
2025-12-20 19:50:28 +02:00
|
|
|
}
|
2025-12-20 00:31:30 +02:00
|
|
|
});
|
2025-12-19 22:38:26 +02:00
|
|
|
// Initialize Go To Line dialog with callback
|
2025-12-22 02:39:48 +02:00
|
|
|
goToLineDialog.setCallback(new GoToLineDialog.GoToLineCallback() {
|
2025-12-19 22:38:26 +02:00
|
|
|
@Override
|
|
|
|
|
public int getLineCount() {
|
|
|
|
|
return container != null ? container.linesCount : 0;
|
|
|
|
|
}
|
2025-12-22 02:39:48 +02:00
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
@Override
|
|
|
|
|
public int getColumnCount(int lineIndex) {
|
|
|
|
|
if (container == null || container.lines == null || lineIndex < 0 || lineIndex >= container.lines.size()) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
LineData ld = container.lines.get(lineIndex);
|
|
|
|
|
return ld.end - ld.start;
|
|
|
|
|
}
|
2025-12-22 02:39:48 +02:00
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
@Override
|
|
|
|
|
public void goToLineColumn(int line, int column) {
|
2025-12-22 02:39:48 +02:00
|
|
|
if (container == null || container.lines == null)
|
|
|
|
|
return;
|
|
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
// Convert 1-indexed line to 0-indexed
|
|
|
|
|
int lineIdx = line - 1;
|
2025-12-22 02:39:48 +02:00
|
|
|
if (lineIdx < 0 || lineIdx >= container.lines.size())
|
|
|
|
|
return;
|
|
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
LineData ld = container.lines.get(lineIdx);
|
|
|
|
|
int lineLength = ld.end - ld.start;
|
2025-12-22 02:39:48 +02:00
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
// Convert 1-indexed column to 0-indexed, clamp to line length
|
2025-12-20 14:49:46 +02:00
|
|
|
// lineLength - 1 because the line's end is the start of the next line
|
|
|
|
|
int col = Math.max(0, Math.min(column - 1, lineLength - 1));
|
2025-12-19 22:38:26 +02:00
|
|
|
int position = ld.start + col;
|
2025-12-22 02:39:48 +02:00
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
// Set cursor position
|
|
|
|
|
selection.reset(position);
|
2025-12-22 02:39:48 +02:00
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
// Scroll to make the line visible
|
2025-12-20 19:51:19 +02:00
|
|
|
int visible = GuiScriptTextArea.this.height / (container != null ? container.lineHeight : 12);
|
2025-12-20 20:59:39 +02:00
|
|
|
int effectiveVisible = Math.max(1, visible - bottomPaddingLines);
|
|
|
|
|
int maxScroll = Math.max(0, getPaddedLineCount() - visible);
|
|
|
|
|
scroll.scrollToLine(lineIdx, effectiveVisible, maxScroll);
|
2025-12-19 22:38:26 +02:00
|
|
|
}
|
2025-12-19 22:47:51 +02:00
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void unfocusMainEditor() {
|
|
|
|
|
// Save position but unfocus
|
|
|
|
|
active = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void focusMainEditor() {
|
|
|
|
|
active = true;
|
|
|
|
|
selection.markActivity();
|
|
|
|
|
}
|
2025-12-22 02:39:48 +02:00
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
@Override
|
|
|
|
|
public void onDialogClose() {
|
|
|
|
|
active = true;
|
|
|
|
|
selection.markActivity();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-21 00:49:34 +02:00
|
|
|
|
|
|
|
|
// Initialize Rename Refactor handler with callback
|
|
|
|
|
renameHandler.setCallback(new RenameRefactorHandler.RenameCallback() {
|
|
|
|
|
@Override
|
|
|
|
|
public String getText() {
|
|
|
|
|
return GuiScriptTextArea.this.text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setText(String newText) {
|
|
|
|
|
GuiScriptTextArea.this.setText(newText);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public List<LineData> getLines() {
|
|
|
|
|
return container != null ? container.lines : new ArrayList<>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getCursorPosition() {
|
|
|
|
|
return selection.getCursorPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SelectionState getSelectionState() {
|
|
|
|
|
return selection;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setCursorPosition(int pos) {
|
|
|
|
|
selection.reset(pos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void unfocusMainEditor() {
|
|
|
|
|
active = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void focusMainEditor() {
|
|
|
|
|
active = true;
|
|
|
|
|
selection.markActivity();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getGutterWidth() {
|
|
|
|
|
return LINE_NUMBER_GUTTER_WIDTH;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getLineHeight() {
|
|
|
|
|
return container != null ? container.lineHeight : 12;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getScrolledLine() {
|
|
|
|
|
return scroll.getScrolledLine();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public double getFractionalOffset() {
|
|
|
|
|
return scroll.getFractionalOffset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void scrollToPosition(int pos) {
|
|
|
|
|
if (container == null || container.lines == null)
|
|
|
|
|
return;
|
|
|
|
|
for (int i = 0; i < container.lines.size(); i++) {
|
|
|
|
|
LineData ld = container.lines.get(i);
|
|
|
|
|
if (pos >= ld.start && pos < ld.end) {
|
|
|
|
|
int visible = Math.max(1, container.visibleLines);
|
|
|
|
|
int effectiveVisible = Math.max(1, visible - bottomPaddingLines);
|
|
|
|
|
int maxScroll = Math.max(0, getPaddedLineCount() - visible);
|
|
|
|
|
scroll.scrollToLine(i, effectiveVisible, maxScroll);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public JavaTextContainer getContainer() {
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setTextWithoutUndo(String newText) {
|
|
|
|
|
// Set text without creating an undo entry (for live rename preview)
|
|
|
|
|
boolean wasUndoing = undoing;
|
|
|
|
|
undoing = true;
|
|
|
|
|
setText(newText);
|
|
|
|
|
undoing = wasUndoing;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2025-12-21 12:54:52 +02:00
|
|
|
public void pushUndoState(String textState, int cursor) {
|
2025-12-21 00:49:34 +02:00
|
|
|
// Push a specific text state to the undo list
|
|
|
|
|
if (!undoing) {
|
2025-12-21 12:54:52 +02:00
|
|
|
undoList.add(new UndoData(textState, cursor));
|
2025-12-21 00:49:34 +02:00
|
|
|
redoList.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getViewportWidth() {
|
|
|
|
|
return width - LINE_NUMBER_GUTTER_WIDTH - 8; // Account for gutter and scrollbar
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
// ==================== RENDERING ====================
|
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-19 22:38:43 +01:00
|
|
|
|
|
|
|
|
int searchHeight = searchBar.getTotalHeight();
|
2025-12-20 02:02:43 +02:00
|
|
|
|
2025-12-19 22:38:43 +01:00
|
|
|
|
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);
|
2025-12-18 22:37:40 +02:00
|
|
|
scissorViewport();
|
2025-12-19 22:39:36 +01:00
|
|
|
|
|
|
|
|
container.visibleLines = (height / container.lineHeight);
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-12-20 20:59:39 +02:00
|
|
|
int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines);
|
2025-12-18 22:11:42 +02:00
|
|
|
|
|
|
|
|
// Handle mouse wheel scroll
|
2025-12-19 16:04:07 +02:00
|
|
|
int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel();
|
2025-03-13 17:14:45 -04:00
|
|
|
if (listener instanceof GuiNPCInterface) {
|
2025-12-19 16:04:07 +02:00
|
|
|
((GuiNPCInterface) listener).mouseScroll = wheelDelta;
|
2025-12-22 14:37:08 +02:00
|
|
|
boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay;
|
|
|
|
|
if (wheelDelta != 0 && canScroll)
|
2025-12-18 22:11:42 +02:00
|
|
|
scroll.applyWheelScroll(wheelDelta, maxScroll);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-12-18 12:05:57 +02:00
|
|
|
|
2025-12-18 22:37:40 +02:00
|
|
|
// Handle scrollbar dragging (delegated to ScrollState)
|
|
|
|
|
if (scroll.isClickScrolling())
|
2025-12-20 20:59:39 +02:00
|
|
|
scroll.handleClickScrolling(yMouse, x, y, height, container.visibleLines, getPaddedLineCount(), maxScroll);
|
2025-12-18 22:37:40 +02:00
|
|
|
|
2025-12-18 22:11:42 +02:00
|
|
|
// Update scroll animation
|
|
|
|
|
scroll.initializeIfNeeded(scroll.getScrolledLine());
|
|
|
|
|
scroll.update(maxScroll);
|
2025-12-18 13:13:53 +02:00
|
|
|
|
2025-12-18 22:11:42 +02:00
|
|
|
// Handle click-dragging for selection
|
2025-12-18 13:13:53 +02:00
|
|
|
if (clicked) {
|
|
|
|
|
clicked = Mouse.isButtonDown(0);
|
|
|
|
|
int i = getSelectionPos(xMouse, yMouse);
|
2025-12-18 22:11:42 +02:00
|
|
|
if (i != selection.getCursorPosition()) {
|
2025-12-18 13:13:53 +02:00
|
|
|
if (doubleClicked || tripleClicked) {
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(selection.getCursorPosition());
|
2025-12-18 13:13:53 +02:00
|
|
|
doubleClicked = false;
|
|
|
|
|
tripleClicked = false;
|
|
|
|
|
}
|
|
|
|
|
setCursor(i, true);
|
|
|
|
|
}
|
|
|
|
|
} else if (doubleClicked || tripleClicked) {
|
|
|
|
|
doubleClicked = false;
|
|
|
|
|
tripleClicked = false;
|
|
|
|
|
}
|
2025-12-20 02:00:13 +02:00
|
|
|
// y += searchHeight;
|
|
|
|
|
// height -= searchHeight;
|
2025-12-18 22:11:42 +02:00
|
|
|
// Calculate braces next to cursor to highlight
|
2023-07-30 01:26:36 -05:00
|
|
|
int startBracket = 0, endBracket = 0;
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getStartSelection() >= 0 && text != null && text.length() > 0 &&
|
|
|
|
|
(selection.getEndSelection() - selection.getStartSelection() == 1 || !selection.hasSelection())) {
|
|
|
|
|
int[] span = BracketMatcher.findBracketSpanAt(text,selection.getStartSelection());
|
2025-12-18 00:52:50 +02:00
|
|
|
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);
|
2025-12-20 14:18:35 +02:00
|
|
|
|
2025-12-17 20:06:47 +02:00
|
|
|
// Build brace spans: {origDepth, open line, close line, adjustedDepth}
|
2025-12-18 22:11:42 +02:00
|
|
|
List<int[]> braceSpans = BracketMatcher.computeBraceSpans(text, list);
|
2025-12-20 14:18:35 +02:00
|
|
|
// Always highlight unmatched braces (positions in text)
|
|
|
|
|
List<Integer> unmatchedBraces = BracketMatcher.findUnmatchedBracePositions(text);
|
|
|
|
|
|
2025-12-20 18:56:58 +02:00
|
|
|
// Determine which exact brace span (openLine/closeLine) to highlight indent guides for
|
2025-12-18 10:25:10 +02:00
|
|
|
int highlightedOpenLine = -1;
|
|
|
|
|
int highlightedCloseLine = -1;
|
|
|
|
|
if (startBracket != endBracket && startBracket >= 0) {
|
|
|
|
|
int bracketLineIdx = -1;
|
2025-12-20 18:56:58 +02:00
|
|
|
// Only consider curly braces for highlighting the indent guides
|
|
|
|
|
boolean isCurlyBracket = false;
|
2025-12-22 01:10:24 +02:00
|
|
|
char bc = text.charAt(startBracket);
|
2025-12-20 18:56:58 +02:00
|
|
|
if (text != null && startBracket >= 0 && startBracket < text.length()) {
|
|
|
|
|
if (bc == '{' || bc == '}') isCurlyBracket = true;
|
|
|
|
|
}
|
2025-12-18 10:25:10 +02:00
|
|
|
for (int li = 0; li < list.size(); li++) {
|
|
|
|
|
LineData ld = list.get(li);
|
|
|
|
|
if (startBracket >= ld.start && startBracket < ld.end) {
|
|
|
|
|
bracketLineIdx = li;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-20 18:56:58 +02:00
|
|
|
if (bracketLineIdx >= 0 && isCurlyBracket) {
|
2025-12-22 01:10:24 +02:00
|
|
|
// Prefer a span that directly matches the bracket character's line:
|
|
|
|
|
// - if the bracket is an opening '{', prefer spans where openLine == bracketLineIdx
|
|
|
|
|
// - if the bracket is a closing '}', prefer spans where closeLine == bracketLineIdx
|
|
|
|
|
// If no exact match is found, fall back to the smallest enclosing span (innermost).
|
|
|
|
|
int bestSize = Integer.MAX_VALUE;
|
|
|
|
|
boolean foundExact = false;
|
|
|
|
|
char bracketChar = bc; // from earlier
|
2025-12-18 10:25:10 +02:00
|
|
|
for (int[] span : braceSpans) {
|
|
|
|
|
int openLine = span[1];
|
|
|
|
|
int closeLine = span[2];
|
|
|
|
|
if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) {
|
2025-12-22 01:10:24 +02:00
|
|
|
int size = closeLine - openLine;
|
|
|
|
|
boolean exactMatch = (bracketChar == '{' && openLine == bracketLineIdx) || (bracketChar == '}' && closeLine == bracketLineIdx);
|
|
|
|
|
if (exactMatch) {
|
|
|
|
|
// Prefer exact matches immediately (still choose smallest exact span)
|
|
|
|
|
if (!foundExact || size < bestSize) {
|
|
|
|
|
foundExact = true;
|
|
|
|
|
bestSize = size;
|
|
|
|
|
highlightedOpenLine = openLine;
|
|
|
|
|
highlightedCloseLine = closeLine;
|
|
|
|
|
}
|
|
|
|
|
} else if (!foundExact) {
|
|
|
|
|
// keep the smallest enclosing span as a fallback
|
|
|
|
|
if (size < bestSize) {
|
|
|
|
|
bestSize = size;
|
|
|
|
|
highlightedOpenLine = openLine;
|
|
|
|
|
highlightedCloseLine = closeLine;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 10:25:10 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-20 02:02:43 +02:00
|
|
|
highlightedWord = null;
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.hasSelection()) {
|
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()) {
|
2025-12-18 22:11:42 +02:00
|
|
|
if (m.start() == selection.getStartSelection() && m.end() == selection.getEndSelection()) {
|
2025-12-20 02:02:43 +02:00
|
|
|
highlightedWord = text.substring(selection.getStartSelection(), selection.getEndSelection());
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 13:13:53 +02:00
|
|
|
|
|
|
|
|
// Apply fractional GL translate for sub-pixel smooth scrolling
|
2025-12-18 22:11:42 +02:00
|
|
|
double fracOffset = scroll.getFractionalOffset();
|
2025-12-18 13:13:53 +02:00
|
|
|
float fracPixels = (float) (fracOffset * container.lineHeight);
|
|
|
|
|
GL11.glPushMatrix();
|
|
|
|
|
GL11.glTranslatef(0.0f, -fracPixels, 0.0f);
|
2025-12-18 19:08:32 +02:00
|
|
|
|
2025-12-19 02:26:05 +02:00
|
|
|
|
|
|
|
|
// Expand render range by one line above/below so partially-visible lines are drawn
|
|
|
|
|
int renderStart = Math.max(0, scroll.getScrolledLine() - 1);
|
|
|
|
|
// Compute the last line index to render, including the last partially-visible line if any.
|
|
|
|
|
// Adds the fractional scroll offset (fracOffset * lineHeight) to ensure the bottom line is drawn
|
|
|
|
|
// when only part of it is visible in the viewport.
|
|
|
|
|
int renderEnd = (int) Math.min(list.size() - 1,
|
|
|
|
|
scroll.getScrolledLine() + container.visibleLines + fracPixels + 1);
|
|
|
|
|
|
|
|
|
|
// Strings start drawing vertically this much into the line.
|
|
|
|
|
int stringYOffset = 2;
|
|
|
|
|
|
2025-12-18 19:08:32 +02:00
|
|
|
// Render LINE GUTTER numbers
|
2025-12-18 14:05:39 +02:00
|
|
|
for (int i = renderStart; i <= renderEnd; i++) {
|
2025-12-18 22:11:42 +02:00
|
|
|
int posY = y + (i - scroll.getScrolledLine()) * 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);
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getCursorPosition() >= ld.start && selection.getCursorPosition() < ld.end || (li == list.size() - 1 && selection.getCursorPosition() == text.length())) {
|
2025-12-18 14:05:39 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:37:40 +02:00
|
|
|
// Render Viewport
|
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
|
2025-12-18 22:11:42 +02:00
|
|
|
int posY = y + (i - scroll.getScrolledLine()) * 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
|
|
|
}
|
|
|
|
|
}
|
2025-12-20 14:18:35 +02:00
|
|
|
|
|
|
|
|
// Highlight unmatched braces in this line (always red)
|
|
|
|
|
if (unmatchedBraces != null && !unmatchedBraces.isEmpty()) {
|
|
|
|
|
for (int ubPos : unmatchedBraces) {
|
|
|
|
|
if (ubPos >= data.start && ubPos < data.end) {
|
|
|
|
|
int rel = ubPos - data.start;
|
|
|
|
|
int s = ClientProxy.Font.width(line.substring(0, rel));
|
|
|
|
|
int e = ClientProxy.Font.width(line.substring(0, rel + 1)) + 1;
|
|
|
|
|
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0xffcc0000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 10:27:17 +02:00
|
|
|
//Highlight words
|
2025-12-20 02:02:43 +02:00
|
|
|
if (highlightedWord != 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()) {
|
2025-12-20 02:02:43 +02:00
|
|
|
if (line.substring(m.start(), m.end()).equals(highlightedWord)) {
|
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-19 19:48:08 +02:00
|
|
|
|
|
|
|
|
// Highlight search matches
|
2025-12-20 01:04:31 +02:00
|
|
|
if (searchBar.isVisible()) {
|
|
|
|
|
List<int[]> searchMatches = searchBar.getMatches();
|
|
|
|
|
int currentMatchIdx = searchBar.getCurrentMatchIndex();
|
|
|
|
|
for (int mi = 0; mi < searchMatches.size(); mi++) {
|
|
|
|
|
int[] match = searchMatches.get(mi);
|
|
|
|
|
// Check if match overlaps with this line
|
|
|
|
|
if (match[1] > data.start && match[0] < data.end) {
|
|
|
|
|
int matchStart = Math.max(match[0] - data.start, 0);
|
|
|
|
|
int matchEnd = Math.min(match[1] - data.start, line.length());
|
|
|
|
|
if (matchStart < matchEnd) {
|
|
|
|
|
int s = ClientProxy.Font.width(line.substring(0, matchStart));
|
|
|
|
|
int e = ClientProxy.Font.width(line.substring(0, matchEnd)) + 1;
|
|
|
|
|
boolean isExcluded = searchBar.isMatchExcluded(mi);
|
|
|
|
|
// Current match gets brighter highlight, others get dimmer
|
|
|
|
|
int highlightColor = (mi == currentMatchIdx) ? 0xBB4488ff : 0x662266aa;
|
|
|
|
|
if (isExcluded) {
|
|
|
|
|
highlightColor = 0x33666666; // Dimmer for excluded matches
|
|
|
|
|
}
|
|
|
|
|
int highlightX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + s;
|
|
|
|
|
int highlightEndX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + e;
|
|
|
|
|
drawRect(highlightX, posY, highlightEndX, posY + container.lineHeight, highlightColor);
|
|
|
|
|
|
|
|
|
|
// Draw strikethrough line for excluded matches
|
|
|
|
|
if (isExcluded) {
|
|
|
|
|
int strikeY = posY + container.lineHeight / 2;
|
|
|
|
|
drawRect(highlightX, strikeY, highlightEndX, strikeY + 1, 0xFFaa4444);
|
|
|
|
|
}
|
2025-12-19 21:42:00 +02:00
|
|
|
}
|
2025-12-19 19:48:08 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-21 00:49:34 +02:00
|
|
|
|
|
|
|
|
// Highlight rename refactor occurrences
|
|
|
|
|
if (renameHandler.isActive()) {
|
|
|
|
|
List<int[]> renameOccurrences = renameHandler.getOccurrences();
|
|
|
|
|
for (int[] occ : renameOccurrences) {
|
|
|
|
|
// Check if occurrence overlaps with this line
|
2025-12-21 12:11:46 +02:00
|
|
|
if (occ[1] >= data.start && occ[0] <= data.end) {
|
2025-12-21 00:49:34 +02:00
|
|
|
int occStart = Math.max(occ[0] - data.start, 0);
|
|
|
|
|
int occEnd = Math.min(occ[1] - data.start, line.length());
|
2025-12-21 12:11:46 +02:00
|
|
|
|
|
|
|
|
// Handle empty word case - draw 1 pixel wide box
|
|
|
|
|
boolean isEmpty = (occ[0] == occ[1]);
|
|
|
|
|
|
|
|
|
|
if (occStart <= occEnd) { // Changed from < to <= to handle empty case
|
2025-12-21 00:49:34 +02:00
|
|
|
int s = ClientProxy.Font.width(line.substring(0, occStart));
|
2025-12-21 12:11:46 +02:00
|
|
|
int e = isEmpty ? s + 2 : ClientProxy.Font.width(
|
|
|
|
|
line.substring(0, occEnd)) + 1; // 2px wide for empty
|
2025-12-21 12:18:28 +02:00
|
|
|
int occX = x + LINE_NUMBER_GUTTER_WIDTH + s;
|
|
|
|
|
int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 2 + e;
|
2025-12-21 00:49:34 +02:00
|
|
|
boolean isPrimary = renameHandler.isPrimaryOccurrence(occ[0]);
|
|
|
|
|
|
|
|
|
|
// Draw background highlight
|
|
|
|
|
int bgColor = isPrimary ? 0x55335577 : 0x33224466;
|
|
|
|
|
drawRect(occX, posY, occEndX, posY + container.lineHeight, bgColor);
|
|
|
|
|
|
|
|
|
|
// Draw white border for primary occurrence (IntelliJ-like)
|
|
|
|
|
if (isPrimary) {
|
|
|
|
|
int borderColor = 0xDDFFFFFF;
|
2025-12-21 12:18:28 +02:00
|
|
|
//RED BG
|
|
|
|
|
drawRect(occX, posY, occEndX, posY + container.lineHeight, 0x33ff0000);
|
|
|
|
|
|
2025-12-21 00:49:34 +02:00
|
|
|
// Top border
|
|
|
|
|
drawRect(occX, posY, occEndX, posY + 1, borderColor);
|
|
|
|
|
// Bottom border
|
|
|
|
|
drawRect(occX, posY + container.lineHeight - 1, occEndX,
|
|
|
|
|
posY + container.lineHeight, borderColor);
|
|
|
|
|
// Left border
|
|
|
|
|
drawRect(occX, posY, occX + 1, posY + container.lineHeight, borderColor);
|
|
|
|
|
// Right border
|
|
|
|
|
drawRect(occEndX - 1, posY, occEndX, posY + container.lineHeight, borderColor);
|
|
|
|
|
|
|
|
|
|
// Draw cursor inside the primary occurrence
|
|
|
|
|
if (renameHandler.shouldShowCursor()) {
|
|
|
|
|
int cursorInWord = renameHandler.getCursorInWord();
|
|
|
|
|
String currentWord = renameHandler.getCurrentWord();
|
|
|
|
|
if (currentWord != null && cursorInWord >= 0 && cursorInWord <= currentWord.length()) {
|
|
|
|
|
String beforeCursor = currentWord.substring(0,
|
|
|
|
|
Math.min(cursorInWord, currentWord.length()));
|
|
|
|
|
int cursorX = occX + ClientProxy.Font.width(beforeCursor);
|
2025-12-21 12:11:46 +02:00
|
|
|
// drawRect(cursorX, posY + 1, cursorX + 1, posY + container.lineHeight - 1,
|
|
|
|
|
// 0xFFFFFFFF);
|
2025-12-21 00:49:34 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-19 19:48:08 +02:00
|
|
|
|
2025-12-18 09:53:02 +02:00
|
|
|
// Highlight the current line (light gray) under any selection
|
2025-12-18 22:11:42 +02:00
|
|
|
if (active && isEnabled() && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == 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-12-18 22:11:42 +02:00
|
|
|
if (selection.hasSelection() && selection.getEndSelection() > data.start && selection.getStartSelection() <= data.end) {
|
|
|
|
|
if (selection.getStartSelection() < data.end) {
|
|
|
|
|
int s = ClientProxy.Font.width(
|
|
|
|
|
line.substring(0, Math.max(selection.getStartSelection() - data.start, 0)));
|
|
|
|
|
int e = ClientProxy.Font.width(
|
|
|
|
|
line.substring(0, Math.min(selection.getEndSelection() - 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 22:11:42 +02:00
|
|
|
if (i == Math.max(0, scroll.getScrolledLine()) && !braceSpans.isEmpty()) {
|
|
|
|
|
int visStart = Math.max(0, scroll.getScrolledLine());
|
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-20 18:59:17 +02:00
|
|
|
boolean highlighted = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine);
|
|
|
|
|
int guideColor = highlighted ? 0x9933cc00 : 0x33FFFFFF;
|
|
|
|
|
|
2025-12-18 22:11:42 +02:00
|
|
|
int topY = y + (drawStart - scroll.getScrolledLine()) * container.lineHeight;
|
|
|
|
|
int bottomY = y + (endLine - scroll.getScrolledLine() + 1) * container.lineHeight;
|
2025-12-20 18:59:17 +02:00
|
|
|
if(highlighted)
|
|
|
|
|
bottomY-=2;
|
|
|
|
|
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
|
2025-12-18 22:11:42 +02:00
|
|
|
boolean recentInput = selection.hadRecentInput();
|
|
|
|
|
if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) {
|
2025-12-18 14:05:39 +02:00
|
|
|
int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width(
|
2025-12-18 22:11:42 +02:00
|
|
|
line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length())));
|
2025-12-19 18:15:37 +02:00
|
|
|
drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, 0xffffffff);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 13:13:53 +02:00
|
|
|
GL11.glPopMatrix();
|
2025-12-18 22:37:40 +02:00
|
|
|
GL11.glDisable(GL11.GL_SCISSOR_TEST);
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-12-18 22:37:40 +02: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-20 20:59:39 +02:00
|
|
|
int effLines = Math.max(1, getPaddedLineCount());
|
|
|
|
|
int sbSize = Math.max((int) (1f * (container.visibleLines) / effLines * height), 2);
|
2023-07-30 01:26:36 -05:00
|
|
|
|
|
|
|
|
int posX = x + width - 6;
|
2025-12-20 20:59:39 +02:00
|
|
|
double linesCount = (double) effLines;
|
2025-12-18 22:11:42 +02:00
|
|
|
int posY = (int) (y + 1f * scroll.getScrollPos() / 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-19 16:04:07 +02:00
|
|
|
|
2025-12-19 19:48:08 +02:00
|
|
|
// Draw search/replace bar (overlays viewport)
|
|
|
|
|
searchBar.draw(xMouse, yMouse);
|
|
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
// Draw go to line dialog (overlays everything)
|
|
|
|
|
goToLineDialog.draw(xMouse, yMouse);
|
2025-12-19 16:04:07 +02:00
|
|
|
KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
2025-12-17 18:13:29 +02:00
|
|
|
|
2025-12-18 22:37:40 +02:00
|
|
|
private void scissorViewport() {
|
|
|
|
|
Minecraft mc = Minecraft.getMinecraft();
|
|
|
|
|
ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
|
|
|
|
int scaleFactor = sr.getScaleFactor();
|
|
|
|
|
int scissorX = (this.x) * scaleFactor;
|
|
|
|
|
int scissorY = (sr.getScaledHeight() - (this.y + this.height)) * scaleFactor;
|
|
|
|
|
int scissorW = (this.width) * scaleFactor;
|
|
|
|
|
int scissorH = this.height * scaleFactor;
|
|
|
|
|
GL11.glScissor(scissorX, scissorY, scissorW, scissorH);
|
|
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
// ==================== SELECTION & CURSOR POSITION ====================
|
|
|
|
|
|
2025-12-18 22:37:40 +02:00
|
|
|
// Get cursor position from mouse coordinates
|
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-19 02:09:18 +02:00
|
|
|
// Adjust yMouse to account for fractional GL translation (negative offset applied in rendering).
|
|
|
|
|
// Use a double here (no integer rounding) so clicks that land on partially
|
|
|
|
|
// visible lines (fractional positions) correctly hit that line.
|
2025-12-18 22:11:42 +02:00
|
|
|
double fracPixels = scroll.getFractionalOffset() * container.lineHeight;
|
2025-12-19 02:09:18 +02:00
|
|
|
double yMouseD = yMouse + fracPixels;
|
2025-12-18 13:13:53 +02:00
|
|
|
|
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-19 02:09:18 +02:00
|
|
|
//+1 to account for the fractional line
|
|
|
|
|
if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines +1) {
|
|
|
|
|
double yPos = (i - scroll.getScrolledLine()) * this.container.lineHeight;
|
|
|
|
|
if (yMouseD >= yPos && yMouseD < 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() {
|
2025-12-18 22:11:42 +02:00
|
|
|
return selection.getCursorLineIndex(container.lines, text != null ? text.length() : 0);
|
2025-12-18 19:07:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
2025-12-20 20:59:39 +02:00
|
|
|
int effectiveVisible = Math.max(1, visible - bottomPaddingLines);
|
|
|
|
|
int maxScroll = Math.max(0, getPaddedLineCount() - visible);
|
2025-12-18 22:11:42 +02:00
|
|
|
|
2025-12-20 20:59:39 +02:00
|
|
|
scroll.scrollToLine(lineIdx, effectiveVisible, maxScroll);
|
2025-12-18 14:44:09 +02:00
|
|
|
}
|
2025-12-19 14:09:55 +02:00
|
|
|
|
|
|
|
|
// ==================== KEY BINDINGS INITIALIZATION ====================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize key bindings for editor shortcuts using ScriptEditorKeys.
|
|
|
|
|
* Centralized active/enabled checking ensures shortcuts only fire when appropriate.
|
|
|
|
|
*/
|
|
|
|
|
private void initializeKeyBindings() {
|
|
|
|
|
// Helper: execute action only if text area is active and enabled
|
2025-12-22 13:51:57 +02:00
|
|
|
Supplier<Boolean> isActive = () -> active && isEnabled() && !KEYS_OVERLAY.isVisible();
|
|
|
|
|
Supplier<Boolean> openBoxes = () -> !KEYS_OVERLAY.isVisible();
|
2025-12-19 14:09:55 +02:00
|
|
|
|
2025-12-19 18:38:11 +02:00
|
|
|
// CUT: Copy selection to clipboard and delete it. If no selection, cut the current sentence.
|
2025-12-19 14:09:55 +02:00
|
|
|
KEYS.CUT.setTask(e -> {
|
2025-12-19 18:25:32 +02:00
|
|
|
if (!e.isPress() || !isActive.get())
|
2025-12-19 14:09:55 +02:00
|
|
|
return;
|
2025-12-19 18:25:32 +02:00
|
|
|
|
|
|
|
|
if (selection.hasSelection()) {
|
|
|
|
|
NoppesStringUtils.setClipboardContents(selection.getSelectedText(text));
|
|
|
|
|
String s = getSelectionBeforeText();
|
|
|
|
|
setText(s + getSelectionAfterText());
|
|
|
|
|
selection.reset(s.length());
|
|
|
|
|
scrollToCursor();
|
2025-12-19 18:38:11 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No selection: cut the current sentence (heuristic based on .!? or newline)
|
|
|
|
|
if (text == null || text.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
int cursor = selection.getCursorPosition();
|
|
|
|
|
int start = cursor;
|
|
|
|
|
int pos = cursor - 1;
|
|
|
|
|
while (pos >= 0) {
|
|
|
|
|
char ch = text.charAt(pos);
|
|
|
|
|
if (ch == '.' || ch == '!' || ch == '?' || ch == '\n') {
|
|
|
|
|
start = pos + 1;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
pos--;
|
|
|
|
|
}
|
|
|
|
|
while (start < cursor && Character.isWhitespace(text.charAt(start))) start++;
|
|
|
|
|
|
|
|
|
|
int end = cursor;
|
|
|
|
|
pos = cursor;
|
|
|
|
|
while (pos < text.length()) {
|
|
|
|
|
char ch = text.charAt(pos);
|
|
|
|
|
if (ch == '.' || ch == '!' || ch == '?' || ch == '\n') {
|
|
|
|
|
end = pos + 1;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
pos++;
|
|
|
|
|
}
|
|
|
|
|
if (end == cursor) end = text.length();
|
|
|
|
|
while (end > start && Character.isWhitespace(text.charAt(end - 1))) end--;
|
|
|
|
|
|
|
|
|
|
if (start >= end) {
|
|
|
|
|
// fallback: cut whole current line
|
|
|
|
|
int ls = text.lastIndexOf('\n', Math.max(0, cursor - 1));
|
|
|
|
|
start = ls == -1 ? 0 : ls + 1;
|
|
|
|
|
int le = text.indexOf('\n', cursor);
|
|
|
|
|
end = le == -1 ? text.length() : le;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (start < end) {
|
|
|
|
|
String cut = text.substring(start, end);
|
|
|
|
|
NoppesStringUtils.setClipboardContents(cut);
|
|
|
|
|
setText(text.substring(0, start) + text.substring(end));
|
|
|
|
|
selection.reset(start);
|
|
|
|
|
scrollToCursor();
|
2025-12-19 18:25:32 +02:00
|
|
|
}
|
2025-12-19 14:09:55 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// COPY: Copy selection to clipboard
|
|
|
|
|
KEYS.COPY.setTask(e -> {
|
2025-12-19 18:25:32 +02:00
|
|
|
if (!e.isPress() || !isActive.get())
|
2025-12-19 14:09:55 +02:00
|
|
|
return;
|
2025-12-19 18:25:32 +02:00
|
|
|
|
|
|
|
|
if (selection.hasSelection())
|
|
|
|
|
NoppesStringUtils.setClipboardContents(selection.getSelectedText(text));
|
2025-12-19 14:09:55 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// PASTE: Insert clipboard contents at caret
|
|
|
|
|
KEYS.PASTE.setTask(e -> {
|
2025-12-19 18:25:32 +02:00
|
|
|
if (!e.isPress() || !isActive.get())
|
2025-12-19 14:09:55 +02:00
|
|
|
return;
|
2025-12-19 18:25:32 +02:00
|
|
|
|
|
|
|
|
addText(NoppesStringUtils.getClipboardContents());
|
|
|
|
|
scrollToCursor();
|
2025-12-19 14:09:55 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// UNDO: Restore last edit from undo list
|
2025-12-20 12:19:32 +02:00
|
|
|
// Works in search bar.
|
2025-12-19 14:09:55 +02:00
|
|
|
KEYS.UNDO.setTask(e -> {
|
2025-12-22 02:22:27 +02:00
|
|
|
if ((!e.isPress() && !e.isHold()) || !openBoxes.get())
|
2025-12-19 14:09:55 +02:00
|
|
|
return;
|
2025-12-19 18:25:32 +02:00
|
|
|
|
2025-12-20 12:19:32 +02:00
|
|
|
if (searchBar.hasFocus()) {
|
|
|
|
|
searchBar.undo();
|
|
|
|
|
} else {
|
|
|
|
|
if (undoList.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
undoing = true;
|
|
|
|
|
redoList.add(new UndoData(this.text, selection.getCursorPosition()));
|
|
|
|
|
UndoData data = undoList.remove(undoList.size() - 1);
|
|
|
|
|
setText(data.text);
|
|
|
|
|
selection.reset(data.cursorPosition);
|
|
|
|
|
undoing = false;
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
searchBar.updateMatches();
|
2025-12-22 02:22:27 +02:00
|
|
|
if (!active)
|
|
|
|
|
active = true;
|
2025-12-20 12:19:32 +02:00
|
|
|
}
|
2025-12-19 14:09:55 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// REDO: Restore last undone edit from redo list
|
2025-12-20 12:19:32 +02:00
|
|
|
// Works in search bar.
|
2025-12-19 14:09:55 +02:00
|
|
|
KEYS.REDO.setTask(e -> {
|
2025-12-22 02:22:27 +02:00
|
|
|
if ((!e.isPress() && !e.isHold()) || !openBoxes.get())
|
2025-12-19 14:09:55 +02:00
|
|
|
return;
|
2025-12-19 18:25:32 +02:00
|
|
|
|
2025-12-20 12:19:32 +02:00
|
|
|
if (searchBar.hasFocus()) {
|
|
|
|
|
searchBar.redo();
|
|
|
|
|
} else {
|
|
|
|
|
if (redoList.isEmpty())
|
|
|
|
|
return;
|
|
|
|
|
undoing = true;
|
|
|
|
|
undoList.add(new UndoData(this.text, selection.getCursorPosition()));
|
|
|
|
|
UndoData data = redoList.remove(redoList.size() - 1);
|
|
|
|
|
setText(data.text);
|
|
|
|
|
selection.reset(data.cursorPosition);
|
|
|
|
|
undoing = false;
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
searchBar.updateMatches();
|
2025-12-22 02:22:27 +02:00
|
|
|
if (!active)
|
|
|
|
|
active = true;
|
2025-12-20 12:19:32 +02:00
|
|
|
}
|
2025-12-19 14:09:55 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// FORMAT: Format/indent code
|
|
|
|
|
KEYS.FORMAT.setTask(e -> {
|
2025-12-19 18:25:32 +02:00
|
|
|
if (!e.isPress() || !isActive.get())
|
2025-12-19 14:09:55 +02:00
|
|
|
return;
|
2025-12-19 18:25:32 +02:00
|
|
|
formatText();
|
2025-12-19 14:09:55 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// TOGGLE_COMMENT: Toggle comment for selection or current line
|
|
|
|
|
KEYS.TOGGLE_COMMENT.setTask(e -> {
|
2025-12-19 18:25:32 +02:00
|
|
|
if (!e.isPress() || !isActive.get())
|
2025-12-19 14:09:55 +02:00
|
|
|
return;
|
2025-12-19 18:25:32 +02:00
|
|
|
|
|
|
|
|
if (selection.hasSelection())
|
|
|
|
|
toggleCommentSelection();
|
|
|
|
|
else
|
|
|
|
|
toggleCommentLineAtCursor();
|
2025-12-19 14:09:55 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// DUPLICATE: Duplicate selection or current line
|
|
|
|
|
KEYS.DUPLICATE.setTask(e -> {
|
2025-12-19 18:25:32 +02:00
|
|
|
if (!e.isPress() || !isActive.get())
|
2025-12-19 14:09:55 +02:00
|
|
|
return;
|
2025-12-19 18:25:32 +02:00
|
|
|
|
|
|
|
|
if (selection.hasSelection()) {
|
|
|
|
|
// Multi-line selection duplication
|
|
|
|
|
LineData firstLine = null, lastLine = null;
|
|
|
|
|
for (LineData line : container.lines) {
|
|
|
|
|
if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) {
|
|
|
|
|
if (firstLine == null)
|
|
|
|
|
firstLine = line;
|
|
|
|
|
lastLine = line;
|
2025-12-19 14:09:55 +02:00
|
|
|
}
|
2025-12-19 18:25:32 +02:00
|
|
|
}
|
|
|
|
|
if (firstLine != null && lastLine != null) {
|
|
|
|
|
String selectedText = text.substring(firstLine.start, lastLine.end);
|
|
|
|
|
int savedStart = selection.getStartSelection();
|
|
|
|
|
int savedEnd = selection.getEndSelection();
|
|
|
|
|
int insertAt = lastLine.end;
|
|
|
|
|
setText(text.substring(0, insertAt) + selectedText + text.substring(insertAt));
|
|
|
|
|
selection.setStartSelection(savedStart);
|
|
|
|
|
selection.setEndSelection(savedEnd);
|
|
|
|
|
selection.setCursorPositionDirect(savedEnd);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Duplicate current line
|
|
|
|
|
for (LineData line : container.lines) {
|
|
|
|
|
if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) {
|
2025-12-22 03:34:21 +02:00
|
|
|
int safeStart = Math.max(0, Math.min(line.start, text.length()));
|
|
|
|
|
int safeEnd = Math.max(safeStart, Math.min(line.end, text.length()));
|
|
|
|
|
String lineText = text.substring(safeStart, safeEnd);
|
2025-12-19 18:25:32 +02:00
|
|
|
String insertText = lineText.endsWith("\n") ? lineText : "\n" + lineText;
|
2025-12-22 03:34:21 +02:00
|
|
|
int insertionPoint = Math.min(line.end, text.length());
|
2025-12-19 18:25:32 +02:00
|
|
|
setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint));
|
|
|
|
|
int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0);
|
|
|
|
|
selection.reset(Math.max(0, Math.min(newCursor, this.text.length())));
|
|
|
|
|
break;
|
2025-12-19 14:09:55 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-19 18:25:32 +02:00
|
|
|
}
|
2025-12-19 14:09:55 +02:00
|
|
|
});
|
2025-12-20 19:50:28 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if can open just for SearchReplaceBar and GoToLine
|
|
|
|
|
|
2025-12-19 19:48:08 +02:00
|
|
|
// SEARCH: Open search bar (Ctrl+R)
|
2025-12-20 03:11:46 +02:00
|
|
|
// Works in search bar.
|
2025-12-19 19:48:08 +02:00
|
|
|
KEYS.SEARCH.setTask(e -> {
|
2025-12-20 19:50:28 +02:00
|
|
|
if (!e.isPress() || !openBoxes.get())
|
2025-12-19 19:48:08 +02:00
|
|
|
return;
|
2025-12-20 03:11:46 +02:00
|
|
|
|
|
|
|
|
unfocusAll();
|
2025-12-20 02:03:02 +02:00
|
|
|
searchBar.openSearch();
|
2025-12-19 19:48:08 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R)
|
2025-12-20 03:11:46 +02:00
|
|
|
// Works in search bar.
|
2025-12-19 19:48:08 +02:00
|
|
|
KEYS.SEARCH_REPLACE.setTask(e -> {
|
2025-12-20 19:50:28 +02:00
|
|
|
if (!e.isPress() || !openBoxes.get())
|
2025-12-19 19:48:08 +02:00
|
|
|
return;
|
2025-12-20 03:11:46 +02:00
|
|
|
|
|
|
|
|
unfocusAll();
|
2025-12-20 02:29:42 +02:00
|
|
|
searchBar.openSearchReplace();
|
2025-12-19 19:48:08 +02:00
|
|
|
});
|
2025-12-19 22:38:26 +02:00
|
|
|
|
|
|
|
|
// GO_TO_LINE: Open go to line dialog (Ctrl+G)
|
2025-12-20 03:11:46 +02:00
|
|
|
// Works in search bar.
|
2025-12-19 22:38:26 +02:00
|
|
|
KEYS.GO_TO_LINE.setTask(e -> {
|
2025-12-20 19:50:28 +02:00
|
|
|
if (!e.isPress() || !openBoxes.get())
|
2025-12-19 22:38:26 +02:00
|
|
|
return;
|
2025-12-20 03:11:46 +02:00
|
|
|
|
|
|
|
|
unfocusAll();
|
2025-12-19 22:38:26 +02:00
|
|
|
goToLineDialog.toggle();
|
|
|
|
|
});
|
2025-12-21 00:49:34 +02:00
|
|
|
|
|
|
|
|
// RENAME: Start rename refactoring (Shift+F6)
|
|
|
|
|
KEYS.RENAME.setTask(e -> {
|
|
|
|
|
if (!e.isPress() || !openBoxes.get())
|
|
|
|
|
return;
|
|
|
|
|
|
2025-12-21 12:11:46 +02:00
|
|
|
|
2025-12-21 00:49:34 +02:00
|
|
|
if (!renameHandler.isActive()) {
|
2025-12-21 12:11:46 +02:00
|
|
|
unfocusAll();
|
|
|
|
|
active = true;
|
2025-12-21 00:49:34 +02:00
|
|
|
renameHandler.startRename();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-19 14:09:55 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-20 03:11:46 +02:00
|
|
|
public void unfocusAll() {
|
|
|
|
|
if (searchBar.hasFocus()) searchBar.unfocus();
|
|
|
|
|
if (goToLineDialog.hasFocus()) goToLineDialog.unfocus();
|
2025-12-21 00:49:34 +02:00
|
|
|
if (renameHandler.isActive())
|
|
|
|
|
renameHandler.cancel();
|
2025-12-20 03:11:46 +02:00
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
// ==================== KEYBOARD INPUT HANDLING ====================
|
2025-12-18 14:44:09 +02:00
|
|
|
|
2025-12-18 22:52:32 +02:00
|
|
|
/**
|
|
|
|
|
* Handles keyboard input for the text area, delegating to specialized handlers
|
|
|
|
|
* for different types of input: navigation, deletion, shortcuts, and character input.
|
|
|
|
|
*/
|
2023-07-30 01:26:36 -05:00
|
|
|
@Override
|
|
|
|
|
public boolean textboxKeyTyped(char c, int i) {
|
2025-12-22 13:43:58 +02:00
|
|
|
if (KEYS_OVERLAY.keyTyped(c, i))
|
|
|
|
|
return true;
|
2025-12-21 00:49:34 +02:00
|
|
|
|
|
|
|
|
// Handle rename refactor input first if active
|
|
|
|
|
if (renameHandler.isActive() && renameHandler.keyTyped(c, i))
|
|
|
|
|
return true;
|
2025-12-19 16:04:07 +02:00
|
|
|
|
2025-12-19 22:38:26 +02:00
|
|
|
// Handle Go To Line dialog input first if it has focus
|
2025-12-20 11:59:34 +02:00
|
|
|
if (goToLineDialog.isVisible() &&goToLineDialog.keyTyped(c, i))
|
2025-12-19 22:38:26 +02:00
|
|
|
return true;
|
|
|
|
|
|
2025-12-19 19:48:08 +02:00
|
|
|
// Handle search bar input first if it has focus
|
2025-12-20 11:59:34 +02:00
|
|
|
if (searchBar.isVisible() && searchBar.keyTyped(c, i))
|
|
|
|
|
return true;
|
2025-12-22 13:51:57 +02:00
|
|
|
|
|
|
|
|
if (!active)
|
|
|
|
|
return false;
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.isKeyComboCtrlA(i)) {
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.selectAll(text.length());
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 14:09:55 +02:00
|
|
|
if (!isEnabled()) return false;
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-12-18 22:52:32 +02:00
|
|
|
if (handleNavigationKeys(i)) return true;
|
2025-12-19 14:09:55 +02:00
|
|
|
if (handleInsertionKeys(i)) return true;
|
2025-12-18 22:52:32 +02:00
|
|
|
if (handleDeletionKeys(i)) return true;
|
|
|
|
|
if (handleCharacterInput(c)) return true;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles cursor navigation keys (arrows) with support for word-jumping (Ctrl)
|
|
|
|
|
* and selection (Shift). Updates cursor position and scrolls viewport if needed.
|
|
|
|
|
*/
|
|
|
|
|
private boolean handleNavigationKeys(int i) {
|
|
|
|
|
// LEFT ARROW: move cursor left; with Ctrl -> jump by word
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_LEFT) {
|
2025-12-18 22:52:32 +02:00
|
|
|
int j = 1; // default: move one character
|
2025-03-13 17:14:45 -04:00
|
|
|
if (isCtrlKeyDown()) {
|
2025-12-18 22:52:32 +02:00
|
|
|
// When Ctrl is down, compute distance to previous word boundary.
|
|
|
|
|
// We match words in the text slice before the cursor and pick
|
|
|
|
|
// the last match start as the new boundary.
|
2025-12-18 22:11:42 +02:00
|
|
|
Matcher m = container.regexWord.matcher(text.substring(0, selection.getCursorPosition()));
|
2025-03-13 17:14:45 -04:00
|
|
|
while (m.find()) {
|
|
|
|
|
if (m.start() == m.end())
|
2025-12-18 22:52:32 +02:00
|
|
|
continue; // skip empty matches
|
|
|
|
|
// j becomes the number of chars to move left to reach word start
|
2025-12-18 22:11:42 +02:00
|
|
|
j = selection.getCursorPosition() - m.start();
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 22:11:42 +02:00
|
|
|
int newPos = Math.max(selection.getCursorPosition() - j, 0);
|
2025-12-18 22:52:32 +02:00
|
|
|
// If Shift is held, extend selection; otherwise place caret.
|
2025-12-17 14:33:17 +02:00
|
|
|
setCursor(newPos, GuiScreen.isShiftKeyDown());
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// RIGHT ARROW: move cursor right; with Ctrl -> jump to next word start
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_RIGHT) {
|
2025-12-18 22:52:32 +02:00
|
|
|
int j = 1; // default: move one character
|
2025-03-13 17:14:45 -04:00
|
|
|
if (isCtrlKeyDown()) {
|
2025-12-19 18:38:11 +02:00
|
|
|
String after = text.substring(selection.getCursorPosition());
|
|
|
|
|
Matcher m = container.regexWord.matcher(after);
|
2025-12-18 22:52:32 +02:00
|
|
|
if (m.find()) {
|
2025-12-19 18:38:11 +02:00
|
|
|
if (m.start() == 0) {
|
|
|
|
|
// If the first match starts at 0 (cursor at word start),
|
|
|
|
|
// try to find the next match so we advance past the current word.
|
|
|
|
|
if (m.find())
|
|
|
|
|
j = m.start();
|
|
|
|
|
else
|
|
|
|
|
j = Math.max(1, after.length());
|
|
|
|
|
} else {
|
|
|
|
|
j = m.start();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No word match found after cursor -> jump to end
|
|
|
|
|
j = Math.max(1, after.length());
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-18 22:11:42 +02:00
|
|
|
int newPos = Math.min(selection.getCursorPosition() + 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-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// UP/DOWN: logical cursor movement across lines while preserving
|
|
|
|
|
// column where possible. After moving, ensure the caret remains visible
|
|
|
|
|
// by adjusting the scroll if necessary.
|
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-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
return false; // not a navigation key
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 14:09:55 +02:00
|
|
|
/**
|
|
|
|
|
* Handles insertion-related keys such as Tab indentation and Enter behavior.
|
|
|
|
|
*/
|
|
|
|
|
private boolean handleInsertionKeys(int i) {
|
|
|
|
|
// TAB: indent or unindent depending on Shift
|
|
|
|
|
if (i == Keyboard.KEY_TAB) {
|
|
|
|
|
boolean shift = isShiftKeyDown();
|
|
|
|
|
if (shift) {
|
|
|
|
|
handleShiftTab();
|
|
|
|
|
} else {
|
|
|
|
|
handleTab();
|
|
|
|
|
}
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RETURN/ENTER: special handling when preceding char is an opening brace '{'
|
|
|
|
|
if (i == Keyboard.KEY_RETURN) {
|
|
|
|
|
int cursorPos = selection.getCursorPosition();
|
|
|
|
|
int prevNonWs = cursorPos - 1;
|
|
|
|
|
while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace(
|
|
|
|
|
text.charAt(prevNonWs))) {
|
|
|
|
|
prevNonWs--;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (prevNonWs >= 0 && cursorPos <= (text != null ? text.length() : 0) && text.charAt(prevNonWs) == '{') {
|
|
|
|
|
String indent = "";
|
|
|
|
|
for (LineData ld : this.container.lines) {
|
|
|
|
|
if (prevNonWs >= ld.start && prevNonWs < ld.end) {
|
|
|
|
|
indent = ld.text.substring(0, IndentHelper.getLineIndent(ld.text));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (indent == null)
|
|
|
|
|
indent = "";
|
|
|
|
|
String childIndent = indent + " ";
|
|
|
|
|
String before = getSelectionBeforeText();
|
|
|
|
|
String after = getSelectionAfterText();
|
|
|
|
|
|
|
|
|
|
int firstNewline = after.indexOf('\n');
|
|
|
|
|
String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline);
|
|
|
|
|
if (leadingSegment.trim().length() > 0) {
|
|
|
|
|
addText("\n" + childIndent);
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boolean hasMatchingCloseSameIndent = false;
|
|
|
|
|
try {
|
|
|
|
|
int openLineIdx = -1;
|
|
|
|
|
int bracePos = prevNonWs;
|
|
|
|
|
for (int li = 0; li < this.container.lines.size(); li++) {
|
|
|
|
|
LineData ld = this.container.lines.get(li);
|
|
|
|
|
if (bracePos >= ld.start && bracePos < ld.end) {
|
|
|
|
|
openLineIdx = li;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (openLineIdx >= 0) {
|
|
|
|
|
List<int[]> spans = BracketMatcher.computeBraceSpans(text, this.container.lines);
|
|
|
|
|
for (int[] span : spans) {
|
|
|
|
|
int spanOpen = span[1];
|
|
|
|
|
int spanClose = span[2];
|
|
|
|
|
if (spanOpen == openLineIdx) {
|
|
|
|
|
int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text);
|
|
|
|
|
if (closeIndent == indent.length()) {
|
|
|
|
|
hasMatchingCloseSameIndent = true;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ex) {
|
|
|
|
|
hasMatchingCloseSameIndent = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasMatchingCloseSameIndent) {
|
|
|
|
|
addText("\n" + childIndent);
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
} else {
|
|
|
|
|
String insert = "\n" + childIndent + "\n" + indent + "}";
|
|
|
|
|
setText(before + insert + after);
|
|
|
|
|
int newCursor = before.length() + 1 + childIndent.length();
|
|
|
|
|
selection.reset(newCursor);
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
addText(Character.toString('\n') + getAutoIndentForEnter());
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:52:32 +02:00
|
|
|
/**
|
|
|
|
|
* Handles deletion keys: Delete, Backspace, and Ctrl+Backspace.
|
|
|
|
|
* Includes smart backspace behavior for indentation-aware line merging,
|
|
|
|
|
* auto-pair deletion for brackets/quotes, and word-level deletion.
|
|
|
|
|
*/
|
|
|
|
|
private boolean handleDeletionKeys(int i) {
|
|
|
|
|
// DELETE key: remove the character under the cursor if no selection,
|
|
|
|
|
// otherwise remove the selected region's tail.
|
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-12-18 22:11:42 +02:00
|
|
|
if (!s.isEmpty() && !selection.hasSelection())
|
2025-12-18 22:52:32 +02:00
|
|
|
// remove single character after caret when nothing is selected
|
2023-07-30 01:26:36 -05:00
|
|
|
s = s.substring(1);
|
|
|
|
|
setText(getSelectionBeforeText() + s);
|
2025-12-18 22:52:32 +02:00
|
|
|
// Keep caret at same start selection
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(selection.getStartSelection());
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// CTRL+BACKSPACE: delete to previous word or whitespace boundary.
|
2025-03-13 17:14:45 -04:00
|
|
|
if (isKeyComboCtrlBackspace(i)) {
|
2023-07-30 15:47:03 -04:00
|
|
|
String s = getSelectionBeforeText();
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getStartSelection() > 0 && !selection.hasSelection()) {
|
|
|
|
|
int nearestCondition = selection.getCursorPosition();
|
2023-07-30 15:47:03 -04:00
|
|
|
int g;
|
2025-12-18 22:52:32 +02:00
|
|
|
// If the char left of caret is whitespace, find the first non-space to the left;
|
|
|
|
|
// otherwise find first whitespace/newline to the left (word boundary).
|
2025-12-18 22:11:42 +02:00
|
|
|
boolean cursorInWhitespace = Character.isWhitespace(s.charAt(selection.getCursorPosition() - 1));
|
2025-03-13 17:14:45 -04:00
|
|
|
if (cursorInWhitespace) {
|
2025-12-18 22:52:32 +02:00
|
|
|
// Scan left until non-whitespace (start of previous word)
|
2025-12-18 22:11:42 +02:00
|
|
|
for (g = selection.getCursorPosition() - 1; g >= 0; g--) {
|
2023-07-30 15:47:03 -04:00
|
|
|
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 {
|
2025-12-18 22:52:32 +02:00
|
|
|
// Scan left until whitespace/newline is found (word boundary)
|
2025-12-18 22:11:42 +02:00
|
|
|
for (g = selection.getCursorPosition() - 1; g >= 0; g--) {
|
2023-07-30 15:47:03 -04:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:52:32 +02:00
|
|
|
// Trim the prefix up to the discovered boundary
|
2023-07-30 15:47:03 -04:00
|
|
|
s = s.substring(0, nearestCondition);
|
2025-12-18 22:52:32 +02:00
|
|
|
// Adjust selection start to match removed characters
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.setStartSelection(
|
|
|
|
|
selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition));
|
2023-07-30 15:47:03 -04:00
|
|
|
}
|
|
|
|
|
setText(s + getSelectionAfterText());
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(selection.getStartSelection());
|
2023-07-30 15:47:03 -04:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// BACKSPACE: complex handling with a few cases:
|
|
|
|
|
// 1) If a selection exists, delete it
|
|
|
|
|
// 2) If at start, nothing to do
|
|
|
|
|
// 3) Smart indent-aware merge with previous line when caret is at/near expected indent
|
|
|
|
|
// 4) Auto-pair deletion (remove both opening and closing chars when deleting an opener)
|
|
|
|
|
// 5) Fallback: delete a single char to the left
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_BACK) {
|
2025-12-18 22:52:32 +02:00
|
|
|
// 1) selection deletion
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.hasSelection()) {
|
2025-12-18 00:35:38 +02:00
|
|
|
String s = getSelectionBeforeText();
|
|
|
|
|
setText(s + getSelectionAfterText());
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(selection.getStartSelection());
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-18 00:35:38 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// 2) nothing to delete
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getStartSelection() <= 0) {
|
2025-12-18 00:35:38 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
2025-12-19 01:21:00 +02:00
|
|
|
// If the current line is whitespace-only, delete the whole line
|
|
|
|
|
// (including the trailing newline if present). This makes Backspace
|
|
|
|
|
// intuitive on blank/indented lines outside any recognized scope.
|
|
|
|
|
LineData currCheck = selection.findCurrentLine(container.lines);
|
|
|
|
|
if (currCheck != null && currCheck.text.trim().length() == 0) {
|
2025-12-22 03:39:22 +02:00
|
|
|
int removeEnd = text.indexOf('\n', currCheck.start - 1);
|
2025-12-19 01:21:00 +02:00
|
|
|
if (removeEnd == -1) {
|
|
|
|
|
removeEnd = text.length();
|
|
|
|
|
} else {
|
|
|
|
|
removeEnd = removeEnd + 1; // include the newline
|
|
|
|
|
}
|
2025-12-22 03:39:22 +02:00
|
|
|
String before = text.substring(0, ValueUtil.clamp(currCheck.start - 1, 0, text.length()));
|
2025-12-19 01:21:00 +02:00
|
|
|
String after = removeEnd <= text.length() ? text.substring(removeEnd) : "";
|
|
|
|
|
setText(before + after);
|
|
|
|
|
int newCursor = Math.max(0, currCheck.start - 1);
|
|
|
|
|
selection.reset(newCursor);
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:52:32 +02:00
|
|
|
// 3) indent-aware merge: find current line and compute expected indent
|
2025-12-18 22:11:42 +02:00
|
|
|
LineData curr = selection.findCurrentLine(container.lines);
|
2025-12-18 00:35:38 +02:00
|
|
|
if (curr != null && curr.start > 0) {
|
2025-12-18 22:11:42 +02:00
|
|
|
int col = selection.getCursorPosition() - curr.start;
|
|
|
|
|
int actualIndent = IndentHelper.getLineIndent(curr.text);
|
|
|
|
|
int expectedIndent = IndentHelper.getExpectedIndent(curr, container.lines);
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// Trigger smart merge only when caret is at or before the expected indent.
|
2025-12-18 00:35:38 +02:00
|
|
|
if (col <= expectedIndent) {
|
|
|
|
|
boolean lineHasContent = curr.text.trim().length() > 0;
|
2025-12-18 22:52:32 +02:00
|
|
|
int newlinePos = curr.start - 1; // index of newline before this line
|
|
|
|
|
|
2025-12-18 00:35:38 +02:00
|
|
|
if (!lineHasContent) {
|
2025-12-18 22:52:32 +02:00
|
|
|
// Empty or whitespace-only line: remove it including its trailing newline
|
2025-12-18 00:35:38 +02:00
|
|
|
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 22:52:32 +02:00
|
|
|
removeEnd = removeEnd + 1; // include the newline in removal
|
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 22:52:32 +02:00
|
|
|
// Place caret at end of previous line
|
2025-12-18 00:35:38 +02:00
|
|
|
int newCursor = Math.max(0, curr.start - 1);
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(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 {
|
2025-12-18 22:52:32 +02:00
|
|
|
// Merge current line content with the previous line preserving spacing
|
2025-12-18 00:35:38 +02:00
|
|
|
int contentStart = curr.start + actualIndent;
|
2025-12-22 03:11:45 +02:00
|
|
|
String before = newlinePos >= 0 ? text.substring(0, newlinePos) : "";
|
2025-12-18 00:35:38 +02:00
|
|
|
String content = contentStart <= text.length() ? text.substring(contentStart) : "";
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// Decide whether a space is needed between concatenated fragments.
|
2025-12-18 00:35:38 +02:00
|
|
|
String spacer = "";
|
|
|
|
|
if (before.length() > 0 && content.length() > 0) {
|
|
|
|
|
char lastChar = before.charAt(before.length() - 1);
|
|
|
|
|
char firstChar = content.charAt(0);
|
2025-12-18 22:52:32 +02:00
|
|
|
// Avoid adding space when punctuation/brackets are adjacent
|
2025-12-18 00:35:38 +02:00
|
|
|
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 22:52:32 +02:00
|
|
|
|
2025-12-18 00:35:38 +02:00
|
|
|
setText(before + spacer + content);
|
|
|
|
|
int newCursor = before.length() + spacer.length();
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(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 22:52:32 +02:00
|
|
|
|
|
|
|
|
// 4) Auto-pair deletion: when deleting an opener and a matching closer follows,
|
|
|
|
|
// remove both so the pair is cleaned up in one backspace.
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getStartSelection() > 0 && selection.getStartSelection() < text.length()) {
|
|
|
|
|
char prev = text.charAt(selection.getStartSelection() - 1);
|
|
|
|
|
char nextc = text.charAt(selection.getStartSelection());
|
2025-12-18 00:35:38 +02:00
|
|
|
if ((prev == '(' && nextc == ')') ||
|
|
|
|
|
(prev == '[' && nextc == ']') ||
|
|
|
|
|
(prev == '{' && nextc == '}') ||
|
|
|
|
|
(prev == '\'' && nextc == '\'') ||
|
|
|
|
|
(prev == '"' && nextc == '"')) {
|
2025-12-18 22:11:42 +02:00
|
|
|
String before = text.substring(0, selection.getStartSelection() - 1);
|
|
|
|
|
String after = selection.getStartSelection() + 1 < text.length() ? text.substring(
|
|
|
|
|
selection.getStartSelection() + 1) : "";
|
2025-12-18 00:35:38 +02:00
|
|
|
setText(before + after);
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.setStartSelection(selection.getStartSelection() - 1);
|
|
|
|
|
selection.reset(selection.getStartSelection());
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2025-12-18 00:35:38 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:52:32 +02:00
|
|
|
// 5) Normal single-character backspace
|
2025-12-18 00:35:38 +02:00
|
|
|
String s = getSelectionBeforeText();
|
|
|
|
|
s = s.substring(0, s.length() - 1);
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.setStartSelection(selection.getStartSelection() - 1);
|
2023-07-30 01:26:36 -05:00
|
|
|
setText(s + getSelectionAfterText());
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(selection.getStartSelection());
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles keyboard shortcuts: clipboard operations (cut/copy/paste),
|
|
|
|
|
* undo/redo, tab indentation, code formatting, enter with brace handling,
|
|
|
|
|
* comment toggling, and line duplication.
|
|
|
|
|
*/
|
|
|
|
|
private boolean handleShortcutKeys(int i) {
|
|
|
|
|
// CTRL+X: Cut
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.isKeyComboCtrlX(i)) {
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.hasSelection()) {
|
2025-12-18 22:52:32 +02:00
|
|
|
// Copy selected text into clipboard, then remove the selection
|
2025-12-18 22:11:42 +02:00
|
|
|
NoppesStringUtils.setClipboardContents(selection.getSelectedText(text));
|
2023-07-30 01:26:36 -05:00
|
|
|
String s = getSelectionBeforeText();
|
|
|
|
|
setText(s + getSelectionAfterText());
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(s.length());
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// CTRL+C: Copy
|
2025-03-13 17:14:45 -04:00
|
|
|
if (this.isKeyComboCtrlC(i)) {
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.hasSelection()) {
|
|
|
|
|
NoppesStringUtils.setClipboardContents(selection.getSelectedText(text));
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// CTRL+V: Paste (insert clipboard contents at caret)
|
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-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// UNDO (Ctrl+Z): restore last entry from undoList and push current state to redoList
|
2025-03-13 17:14:45 -04:00
|
|
|
if (i == Keyboard.KEY_Z && isCtrlKeyDown()) {
|
|
|
|
|
if (undoList.isEmpty())
|
2025-12-18 22:52:32 +02:00
|
|
|
return false; // nothing to undo
|
2023-07-30 01:26:36 -05:00
|
|
|
undoing = true;
|
2025-12-18 22:11:42 +02:00
|
|
|
redoList.add(new UndoData(this.text, selection.getCursorPosition()));
|
2023-07-30 01:26:36 -05:00
|
|
|
UndoData data = undoList.remove(undoList.size() - 1);
|
|
|
|
|
setText(data.text);
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(data.cursorPosition);
|
2023-07-30 01:26:36 -05:00
|
|
|
undoing = false;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// REDO (Ctrl+Y): opposite of undo
|
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;
|
2025-12-18 22:11:42 +02:00
|
|
|
undoList.add(new UndoData(this.text, selection.getCursorPosition()));
|
2023-07-30 01:26:36 -05:00
|
|
|
UndoData data = redoList.remove(redoList.size() - 1);
|
|
|
|
|
setText(data.text);
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(data.cursorPosition);
|
2023-07-30 01:26:36 -05:00
|
|
|
undoing = false;
|
2025-12-18 19:07:05 +02:00
|
|
|
scrollToCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// CTRL+F: format the text according to IndentHelper rules
|
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-12-18 22:52:32 +02:00
|
|
|
|
|
|
|
|
// CTRL+/ : toggle comment for selection or current line
|
2025-12-17 09:53:32 +02:00
|
|
|
if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) {
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.hasSelection()) {
|
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
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:52:32 +02:00
|
|
|
// CTRL+D : duplicate selection or current line
|
2025-12-17 09:53:32 +02:00
|
|
|
if (i == Keyboard.KEY_D && isCtrlKeyDown()) {
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.hasSelection()) {
|
2025-12-18 22:52:32 +02:00
|
|
|
// Multi-line selection duplication: find first and last covered lines,
|
|
|
|
|
// then insert the whole block after the last line without adding extra newline.
|
2025-12-17 10:17:15 +02:00
|
|
|
LineData firstLine = null, lastLine = null;
|
|
|
|
|
for (LineData line : container.lines) {
|
2025-12-18 22:11:42 +02:00
|
|
|
if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) {
|
2025-12-17 10:17:15 +02:00
|
|
|
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
|
|
|
String insertText = selectedText;
|
2025-12-18 22:11:42 +02:00
|
|
|
int savedStart = selection.getStartSelection();
|
|
|
|
|
int savedEnd = selection.getEndSelection();
|
2025-12-18 01:09:06 +02:00
|
|
|
int insertAt = lastLine.end;
|
|
|
|
|
setText(text.substring(0, insertAt) + insertText + text.substring(insertAt));
|
2025-12-18 22:52:32 +02:00
|
|
|
// Restore prior selection / cursor positions
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.setStartSelection(savedStart);
|
|
|
|
|
selection.setEndSelection(savedEnd);
|
|
|
|
|
selection.setCursorPositionDirect(savedEnd);
|
2025-12-17 09:53:32 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-17 10:17:15 +02:00
|
|
|
} else {
|
2025-12-18 22:52:32 +02:00
|
|
|
// Duplicate current line when nothing is selected
|
2025-12-17 10:17:15 +02:00
|
|
|
for (LineData line : container.lines) {
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) {
|
2025-12-17 10:17:15 +02:00
|
|
|
int lineStart = line.start, lineEnd = line.end;
|
|
|
|
|
String lineText = text.substring(lineStart, lineEnd);
|
2025-12-18 22:52:32 +02:00
|
|
|
// If the line already ends with a newline, reuse it; otherwise
|
|
|
|
|
// prefix a newline so duplicate appears after current line.
|
2025-12-18 01:09:06 +02:00
|
|
|
String insertText;
|
|
|
|
|
if (lineText.endsWith("\n")) {
|
|
|
|
|
insertText = lineText;
|
|
|
|
|
} else {
|
|
|
|
|
insertText = "\n" + lineText;
|
|
|
|
|
}
|
|
|
|
|
int insertionPoint = lineEnd;
|
|
|
|
|
setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint));
|
|
|
|
|
int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0);
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(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
|
|
|
}
|
2025-12-18 22:52:32 +02:00
|
|
|
return true;
|
2025-12-17 09:53:32 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-18 22:52:32 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handles printable character input with auto-pairing for quotes and brackets,
|
|
|
|
|
* and smart skipping over existing closing characters.
|
|
|
|
|
*/
|
|
|
|
|
private boolean handleCharacterInput(char c) {
|
|
|
|
|
if (ChatAllowedCharacters.isAllowedCharacter(c)) {
|
|
|
|
|
String before = getSelectionBeforeText();
|
|
|
|
|
String after = getSelectionAfterText();
|
|
|
|
|
|
|
|
|
|
// If the user types a closing character and that same closer is
|
|
|
|
|
// already immediately after the caret, move caret past it instead
|
|
|
|
|
// of inserting another closer. This prevents duplicate closers
|
|
|
|
|
// when the editor auto-inserts pairs.
|
|
|
|
|
if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) {
|
|
|
|
|
// Move caret forward by one (skip over existing closer)
|
|
|
|
|
selection.reset(before.length() + 1);
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Auto-pair insertion: when opening a quote/brace/bracket is typed,
|
|
|
|
|
// insert a matching closer and place caret between the pair.
|
|
|
|
|
if (c == '"') {
|
|
|
|
|
setText(before + "\"\"" + after);
|
|
|
|
|
selection.reset(before.length() + 1);
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == '\'') {
|
|
|
|
|
setText(before + "''" + after);
|
|
|
|
|
selection.reset(before.length() + 1);
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == '[') {
|
|
|
|
|
setText(before + "[]" + after);
|
|
|
|
|
selection.reset(before.length() + 1);
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (c == '(') {
|
|
|
|
|
setText(before + "()" + after);
|
|
|
|
|
selection.reset(before.length() + 1);
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default insertion for printable characters: insert at caret (replacing selection)
|
|
|
|
|
addText(Character.toString(c));
|
|
|
|
|
scrollToCursor();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
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(
|
2025-12-18 22:11:42 +02:00
|
|
|
text, container.lines, selection.getStartSelection(), selection.getEndSelection());
|
2025-12-18 21:10:25 +02:00
|
|
|
setText(result.newText);
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.setStartSelection(result.newStartSelection);
|
|
|
|
|
selection.setEndSelection(result.newEndSelection);
|
2025-12-17 15:18:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void toggleCommentLineAtCursor() {
|
2025-12-18 21:10:25 +02:00
|
|
|
CommentHandler.SingleLineToggleResult result = CommentHandler.toggleCommentAtCursor(
|
2025-12-18 22:11:42 +02:00
|
|
|
text, container.lines, selection.getCursorPosition());
|
2025-12-18 21:10:25 +02:00
|
|
|
setText(result.newText);
|
|
|
|
|
setCursor(result.newCursorPosition, false);
|
2025-12-17 15:18:33 +02:00
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
2025-12-20 11:53:26 +02:00
|
|
|
public boolean closeOnEsc(){
|
2025-12-22 13:51:57 +02:00
|
|
|
return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive();
|
2025-12-20 11:53:26 +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) {
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getCursorPosition() > data.start && selection.getCursorPosition() <= data.end) {
|
2025-12-18 21:10:25 +02:00
|
|
|
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() {
|
2025-12-18 22:11:42 +02:00
|
|
|
LineData currentLine = selection.findCurrentLine(container.lines);
|
2025-12-17 18:35:26 +02:00
|
|
|
|
|
|
|
|
if (currentLine == null) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
2025-12-18 22:11:42 +02:00
|
|
|
return IndentHelper.getAutoIndentForEnter(currentLine.text, selection.getCursorPosition() - currentLine.start);
|
2025-12-17 23:51:28 +02:00
|
|
|
}
|
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-21 00:49:34 +02:00
|
|
|
// Calculate viewport width for line wrapping (account for gutter and scrollbar)
|
|
|
|
|
int viewportWidth = this.width - LINE_NUMBER_GUTTER_WIDTH - 10;
|
|
|
|
|
IndentHelper.FormatResult result = IndentHelper.formatText(text, selection.getCursorPosition(), viewportWidth);
|
2025-12-18 21:10:25 +02:00
|
|
|
setText(result.text);
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(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() {
|
2025-12-18 22:11:42 +02:00
|
|
|
LineData currentLine = selection.findCurrentLine(container.lines);
|
2025-12-17 20:06:47 +02:00
|
|
|
if (currentLine == null) {
|
|
|
|
|
addText(" ");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
int tab = getTabSize();
|
2025-12-18 22:11:42 +02:00
|
|
|
int indentLen = IndentHelper.getLineIndent(currentLine.text);
|
2025-12-17 20:06:47 +02:00
|
|
|
int textStartPos = currentLine.start + indentLen;
|
|
|
|
|
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getCursorPosition() <= textStartPos) {
|
2025-12-17 20:06:47 +02:00
|
|
|
// 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;
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getCursorPosition() == textStartPos) {
|
2025-12-17 20:06:47 +02:00
|
|
|
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;
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(Math.min(newCursor, this.text.length()));
|
2025-12-17 20:06:47 +02:00
|
|
|
} else {
|
|
|
|
|
// Cursor is after start of text: insert spaces at cursor to move following text to next tab stop
|
2025-12-18 22:11:42 +02:00
|
|
|
int column = selection.getCursorPosition() - currentLine.start;
|
2025-12-17 20:06:47 +02:00
|
|
|
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() {
|
2025-12-18 22:11:42 +02:00
|
|
|
LineData currentLine = selection.findCurrentLine(container.lines);
|
2025-12-17 20:06:47 +02:00
|
|
|
if (currentLine == null)
|
|
|
|
|
return;
|
|
|
|
|
int tab = getTabSize();
|
2025-12-18 22:11:42 +02:00
|
|
|
int indentLen = IndentHelper.getLineIndent(currentLine.text);
|
2025-12-17 20:06:47 +02:00
|
|
|
int textStartPos = currentLine.start + indentLen;
|
|
|
|
|
|
2025-12-18 22:11:42 +02:00
|
|
|
if (selection.getCursorPosition() <= textStartPos) {
|
2025-12-17 20:06:47 +02:00
|
|
|
// 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;
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(Math.min(newCursor, this.text.length()));
|
2025-12-17 20:06:47 +02:00
|
|
|
} else {
|
|
|
|
|
// Cursor after start of text: remove up to previous tab stop worth of spaces immediately before cursor
|
2025-12-18 22:11:42 +02:00
|
|
|
int column = selection.getCursorPosition() - currentLine.start;
|
2025-12-17 20:06:47 +02:00
|
|
|
int mod = column % tab;
|
|
|
|
|
int toRemove = mod == 0 ? tab : mod;
|
|
|
|
|
int removed = 0;
|
2025-12-18 22:11:42 +02:00
|
|
|
int pos = selection.getCursorPosition() - 1;
|
2025-12-17 20:06:47 +02:00
|
|
|
while (pos >= currentLine.start && removed < toRemove && text.charAt(pos) == ' ') {
|
|
|
|
|
pos--;
|
|
|
|
|
removed++;
|
|
|
|
|
}
|
|
|
|
|
if (removed > 0) {
|
|
|
|
|
int removeStart = pos + 1;
|
|
|
|
|
String before = text.substring(0, removeStart);
|
2025-12-18 22:11:42 +02:00
|
|
|
String after = text.substring(selection.getCursorPosition());
|
2025-12-17 20:06:47 +02:00
|
|
|
setText(before + after);
|
|
|
|
|
int newCursor = removeStart;
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(Math.min(newCursor, this.text.length()));
|
2025-12-17 20:06:47 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
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) {
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.setCursor(i, text != null ? text.length() : 0, select);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void addText(String s) {
|
2025-12-18 22:11:42 +02:00
|
|
|
int insertPos = selection.getStartSelection();
|
2023-07-30 01:26:36 -05:00
|
|
|
this.setText(this.getSelectionBeforeText() + s + this.getSelectionAfterText());
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.afterTextInsert(insertPos + s.length());
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int cursorUp() {
|
2025-12-18 22:11:42 +02:00
|
|
|
return CursorNavigation.cursorUp(selection.getCursorPosition(), container.lines, text);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int cursorDown() {
|
2025-12-18 22:11:42 +02:00
|
|
|
return CursorNavigation.cursorDown(selection.getCursorPosition(), container.lines, text);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getSelectionBeforeText() {
|
2025-12-18 22:11:42 +02:00
|
|
|
return selection.getTextBefore(text);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getSelectionAfterText() {
|
2025-12-18 22:11:42 +02:00
|
|
|
return selection.getTextAfter(text);
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
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) {
|
2025-12-19 22:38:26 +02:00
|
|
|
// Check go to line dialog clicks first
|
|
|
|
|
if (goToLineDialog.isVisible() && goToLineDialog.mouseClicked(xMouse, yMouse, mouseButton)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 19:48:08 +02:00
|
|
|
// Check search bar clicks first
|
|
|
|
|
if (searchBar.isVisible() && searchBar.mouseClicked(xMouse, yMouse, mouseButton)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 21:42:00 +02:00
|
|
|
// If search bar is visible but click was outside it, unfocus the search bar
|
|
|
|
|
if (searchBar.isVisible()) {
|
|
|
|
|
searchBar.unfocus();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 13:43:58 +02:00
|
|
|
// Let the overlay consume clicks (it returns true when it handled the event)
|
|
|
|
|
if (KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton))
|
2025-12-19 18:12:37 +02:00
|
|
|
return;
|
|
|
|
|
|
2025-12-18 23:16:08 +02:00
|
|
|
// Determine whether click occurred inside the text area bounds
|
2023-07-30 01:26:36 -05:00
|
|
|
this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height;
|
2025-12-19 18:12:37 +02:00
|
|
|
if (this.active) {
|
2025-12-21 00:49:34 +02:00
|
|
|
// Compute logical click position in text
|
2025-12-18 22:11:42 +02:00
|
|
|
int clickPos = this.getSelectionPos(xMouse, yMouse);
|
2025-12-21 00:49:34 +02:00
|
|
|
|
|
|
|
|
// Check if rename refactoring is active and click is in the rename box
|
|
|
|
|
if (renameHandler.isActive() && renameHandler.handleClick(clickPos)) {
|
|
|
|
|
// Click was handled by rename handler - don't reset selection or do other click handling
|
|
|
|
|
this.clicked = false;
|
|
|
|
|
activeTextfield = this;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normal click handling - reset selection/caret
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.reset(clickPos);
|
|
|
|
|
selection.markActivity();
|
2025-12-18 23:16:08 +02:00
|
|
|
|
|
|
|
|
// Prepare click state (left button starts most interactions)
|
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-12-18 23:16:08 +02:00
|
|
|
|
|
|
|
|
// Prefer delegating scrollbar-start logic to ScrollState. If the click
|
|
|
|
|
// is on the scrollbar area and the scrollbar can be dragged, we start
|
|
|
|
|
// click-scrolling mode and cancel the normal text click-drag behavior.
|
2025-12-20 20:59:39 +02:00
|
|
|
if (this.clicked && getPaddedLineCount() * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) {
|
2025-12-18 23:16:08 +02:00
|
|
|
// We consumed the mouse-down as a scrollbar drag start
|
2023-07-30 01:26:36 -05:00
|
|
|
this.clicked = false;
|
2025-12-20 20:59:39 +02:00
|
|
|
scroll.startScrollbarDrag(yMouse,this.y,this.height, getPaddedLineCount());
|
2025-12-18 10:00:02 +02:00
|
|
|
} else {
|
2025-12-18 23:16:08 +02:00
|
|
|
// Handle double/triple click selection counting
|
2025-12-18 10:00:02 +02:00
|
|
|
if (time - this.lastClicked < 300L) {
|
|
|
|
|
this.clickCount++;
|
|
|
|
|
} else {
|
|
|
|
|
this.clickCount = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.clickCount == 2) {
|
2025-12-18 23:16:08 +02:00
|
|
|
// Double-click: select the word under the caret using the container's word regex
|
2025-12-18 10:00:02 +02:00
|
|
|
this.doubleClicked = true;
|
2025-12-18 23:16:08 +02:00
|
|
|
selection.selectWordAtCursor(this.text, this.container.regexWord);
|
2025-12-19 01:08:36 +02:00
|
|
|
// Prevent subsequent mouse-drag handling in the render loop from
|
|
|
|
|
// treating this double-click as a normal click-drag which would
|
|
|
|
|
// immediately reset and extend the selection. Clearing `clicked`
|
|
|
|
|
// keeps the double-click selection stable.
|
|
|
|
|
this.clicked = false;
|
2025-12-18 10:00:02 +02:00
|
|
|
} else if (this.clickCount >= 3) {
|
2025-12-18 23:16:08 +02:00
|
|
|
// Triple-click: select the entire logical line that contains the caret
|
2025-12-18 10:00:02 +02:00
|
|
|
this.tripleClicked = true;
|
2025-12-18 23:16:08 +02:00
|
|
|
selection.selectLineAtCursor(container.lines);
|
2025-12-19 01:08:36 +02:00
|
|
|
// Same as double-click: clear `clicked` to avoid accidental drag-extension
|
|
|
|
|
this.clicked = false;
|
2025-12-18 10:00:02 +02:00
|
|
|
this.clickCount = 0;
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.lastClicked = time;
|
|
|
|
|
activeTextfield = this;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 14:09:55 +02:00
|
|
|
// Called from GuiScreen.updateScreen()
|
2023-07-30 01:26:36 -05:00
|
|
|
public void updateCursorCounter() {
|
2025-12-19 22:38:26 +02:00
|
|
|
// Only process KeyPresets if search bar and go-to-line dialog don't have focus
|
|
|
|
|
// This prevents COPY, PASTE, UNDO, etc. from firing when typing in dialogs
|
2025-12-20 03:11:46 +02:00
|
|
|
KEYS.tick();
|
|
|
|
|
|
2025-12-19 19:48:08 +02:00
|
|
|
searchBar.updateCursor();
|
2025-12-19 21:42:00 +02:00
|
|
|
goToLineDialog.updateCursor();
|
2025-12-21 00:49:34 +02:00
|
|
|
renameHandler.updateCursor();
|
2023-07-30 01:26:36 -05:00
|
|
|
++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-12-19 01:56:03 +02:00
|
|
|
// preserve trailing newlines here — JavaTextContainer.init will
|
|
|
|
|
// ignore an extra empty final split when rendering lines.
|
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) {
|
2025-12-18 22:11:42 +02:00
|
|
|
this.undoList.add(new GuiScriptTextArea.UndoData(this.text, selection.getCursorPosition()));
|
2023-07-30 01:26:36 -05:00
|
|
|
this.redoList.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.text = text;
|
2025-12-16 18:33:55 +02:00
|
|
|
//this.container = new TextContainer(text);
|
2025-12-20 18:42:48 +02:00
|
|
|
if (this.container == null)
|
|
|
|
|
this.container = new JavaTextContainer(text);
|
|
|
|
|
|
|
|
|
|
this.container.init(text, this.width, this.height);
|
|
|
|
|
|
|
|
|
|
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-20 20:59:39 +02:00
|
|
|
int maxScroll = Math.max(0, getPaddedLineCount() - this.container.visibleLines);
|
2025-12-18 22:11:42 +02:00
|
|
|
scroll.clampToBounds(maxScroll);
|
2023-07-30 01:26:36 -05:00
|
|
|
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.clamp(this.text.length());
|
2025-10-15 06:28:43 -04:00
|
|
|
|
2025-12-18 11:23:54 +02:00
|
|
|
// Consider text changes user activity to pause caret blinking briefly
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.markActivity();
|
2025-12-20 02:00:58 +02:00
|
|
|
searchBar.updateMatches();
|
2025-12-18 11:23:54 +02:00
|
|
|
|
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() {
|
2025-12-20 20:59:39 +02:00
|
|
|
return this.container != null && this.container.visibleLines < getPaddedLineCount();
|
2023-07-30 01:26:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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() {
|
2025-12-18 22:11:42 +02:00
|
|
|
selection.clamp(text != null ? text.length() : 0);
|
2025-10-15 06:28:43 -04:00
|
|
|
}
|
2025-12-18 21:10:25 +02:00
|
|
|
|
|
|
|
|
// ==================== INNER CLASSES ====================
|
2025-12-17 15:18:33 +02:00
|
|
|
|
2025-12-20 12:19:32 +02:00
|
|
|
public static class UndoData {
|
2023-07-30 01:26:36 -05:00
|
|
|
public String text;
|
|
|
|
|
public int cursorPosition;
|
|
|
|
|
|
|
|
|
|
public UndoData(String text, int cursorPosition) {
|
|
|
|
|
this.text = text;
|
|
|
|
|
this.cursorPosition = cursorPosition;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|