From 91640c3b7bac26084416b10272d987cfa62235f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 17:41:36 +0000 Subject: [PATCH 01/12] Don't reset the editor scroll position on every edit Editing reset the first visible row to the top, while the scrollbar stayed in place. Only reset it when a new script is loaded, and clamp the scroll position when the text shrinks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/component/input/WidgetTextArea.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index 5359a1b0f..9f9deb125 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -155,6 +155,7 @@ public void setMarkupProvider(@Nullable IMarkupProvider markupProvider) { } public void setValue(String value) { + this.firstRow = 0; this.setValuePassive(value); textFieldHelper.setCursorToStart(); @@ -165,7 +166,6 @@ public void setValue(String value) { } public void setValuePassive(String value) { - this.firstRow = 0; this.value = value; this.clearDisplayCache(); @@ -552,26 +552,40 @@ private DisplayCache rebuildDisplayCache() { int cursorPos = this.textFieldHelper.getCursorPos(); int selectionPos = this.textFieldHelper.getSelectionPos(); IntList linePositionsOld = new IntArrayList(); - List linesAll = Lists.newArrayList(); - MutableInt mutableint = new MutableInt(); + List> linesRaw = Lists.newArrayList(); + IntList lineNumbers = new IntArrayList(); + List linesHasCursor = Lists.newArrayList(); MutableBoolean mutableboolean = new MutableBoolean(); StringSplitter stringsplitter = this.font.getSplitter(); MutableInt lineNumber = new MutableInt(); stringsplitter.splitLines(s, this.getWidth() - getLinesXOffset() /* changed */, Style.EMPTY, true, (style, startPos, endPos) -> { - int k3 = mutableint.getAndIncrement(); String stringPart = s.substring(startPos, endPos); boolean hasNewLine = stringPart.endsWith("\n"); mutableboolean.setValue(hasNewLine); - String s3 = StringUtils.stripEnd(stringPart, " \n"); - int l3 = (k3 - this.firstRow) * 9; // Offset firstRow! - Pos2i bookeditscreen$pos2i1 = this.convertLocalToScreen(new Pos2i(0, l3)); linePositionsOld.add(startPos); - linesAll.add(new LineInfo(this.markupLine(style, s3), bookeditscreen$pos2i1.x, bookeditscreen$pos2i1.y, lineNumber.getValue(), cursorPos >= startPos && cursorPos < (hasNewLine ? endPos : s.indexOf("\n", endPos)))); + linesRaw.add(Pair.of(StringUtils.stripEnd(stringPart, " \n"), style)); + lineNumbers.add(lineNumber.intValue()); + linesHasCursor.add(cursorPos >= startPos && cursorPos < (hasNewLine ? endPos : s.indexOf("\n", endPos))); if (hasNewLine) { lineNumber.increment(); } }); + // Keep scroll position within bounds, as the text may have shrunk + if (this.scrollBar != null) { + int maxFirstRow = Math.max(0, linesRaw.size() - this.scrollBar.getVisibleRows()); + if (this.firstRow > maxFirstRow) { + this.firstRow = maxFirstRow; + this.scrollBar.setFirstRow(this.firstRow, false); + } + } + + List linesAll = Lists.newArrayList(); + for (int k3 = 0; k3 < linesRaw.size(); k3++) { + Pos2i bookeditscreen$pos2i1 = this.convertLocalToScreen(new Pos2i(0, (k3 - this.firstRow) * 9)); // Offset firstRow! + linesAll.add(new LineInfo(this.markupLine(linesRaw.get(k3).getRight(), linesRaw.get(k3).getLeft()), bookeditscreen$pos2i1.x, bookeditscreen$pos2i1.y, lineNumbers.getInt(k3), linesHasCursor.get(k3))); + } + // --- Changed --- // Slice lines based on scroll position int linesTotal = linesAll.size(); From 776430ed79c23b5644bf1bf19f56b92902204738 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 17:42:07 +0000 Subject: [PATCH 02/12] Keep the cursor visible in the editor Only up and down arrow keys scrolled the editor, so typing, pressing enter, home/end and pasting could move the cursor out of view. Scroll to the cursor after every handled key or typed character, and allow the cursor to be placed on the empty line after a trailing newline. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/component/input/WidgetTextArea.java | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index 9f9deb125..fdeaac5fd 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -312,6 +312,7 @@ public boolean keyPressed(int typedChar, int keyCode, int modifiers) { if (flag) { this.clearDisplayCache(); this.setFocused(true); + this.ensureCursorVisible(); return true; } else { this.setFocused(false); @@ -328,6 +329,7 @@ public boolean charTyped(char typedChar, int keyCode) { this.textFieldHelper.insertText(Character.toString(typedChar)); this.clearDisplayCache(); this.setFocused(true); + this.ensureCursorVisible(); return true; } else { this.setFocused(false); @@ -404,16 +406,19 @@ private void changeLine(int p_98098_) { DisplayCache displayCache = this.getDisplayCache(); int j = displayCache.changeLine(cursorPos, p_98098_); this.textFieldHelper.setCursorPos(j, Screen.hasShiftDown()); + } - // Modify scroll position when cursor goes out of screen + /** + * Scroll the viewport so that the cursor is visible. + */ + public void ensureCursorVisible() { if (this.scrollBar != null) { - int cursorLine = findLineFromPos(Arrays.stream(displayCache.lineStarts).toArray(), cursorPos); - if (cursorLine < this.firstRow + 2) { - // Cursor is above viewport - this.setFirstRow(cursorLine - 2, true); - } else if (cursorLine > this.firstRow + this.scrollBar.getVisibleRows() - 3) { - // Cursor is below viewport - this.setFirstRow(cursorLine - (this.scrollBar.getVisibleRows() - 3), true); + int cursorLine = findLineFromPos(this.getDisplayCache().lineStarts, this.textFieldHelper.getCursorPos()); + int visibleRows = this.scrollBar.getVisibleRows(); + if (cursorLine < this.firstRow) { + this.setFirstRow(cursorLine, true); + } else if (cursorLine >= this.firstRow + visibleRows) { + this.setFirstRow(cursorLine - visibleRows + 1, true); } } } @@ -570,6 +575,13 @@ private DisplayCache rebuildDisplayCache() { lineNumber.increment(); } }); + // Add an empty line after a trailing newline, so the cursor can be placed and scrolled there + if (mutableboolean.isTrue()) { + linePositionsOld.add(s.length()); + linesRaw.add(Pair.of("", Style.EMPTY)); + lineNumbers.add(lineNumber.intValue()); + linesHasCursor.add(cursorPos == s.length()); + } // Keep scroll position within bounds, as the text may have shrunk if (this.scrollBar != null) { @@ -597,14 +609,9 @@ private DisplayCache rebuildDisplayCache() { int[] lineStartsArr = lineStarts.toIntArray(); boolean flag = cursorPos == s.length(); - Pos2i cursor; - if (flag && mutableboolean.isTrue()) { - cursor = new Pos2i(0, lines.size() * 9); - } else { - int cursorLine = findLineFromPos(lineStartsArr, cursorPos); - int cursorX = this.font.width(s.substring(lineStartsArr[cursorLine], cursorPos)); - cursor = new Pos2i(cursorX, (cursorLine - this.firstRow) * 9); // We have to offset the firstRow here! - } + int cursorLine = findLineFromPos(lineStartsArr, cursorPos); + int cursorX = this.font.width(s.substring(lineStartsArr[cursorLine], cursorPos)); + Pos2i cursor = new Pos2i(cursorX, (cursorLine - this.firstRow) * 9); // We have to offset the firstRow here! // Hide cursor if out of view if (cursor.y < 0 || cursor.y > getHeight()) { cursor = null; From 36b7e7a1b6c740d948fffdbaf8f1fea27cac1d5d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 17:42:18 +0000 Subject: [PATCH 03/12] Don't lose editor focus on unhandled keys Pressing a key the editor doesn't handle, such as tab or a function key, unfocused it, so a following 'e' closed the terminal. Consume all keys except escape while focused. Typing only worked before because of this unfocusing: once the widget stays focused, EditBox.charTyped consumes every character into its own unused value. Handle characters without delegating to EditBox. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .gitignore | 4 ++++ .../client/gui/component/input/WidgetTextArea.java | 13 +++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index ed5313ce9..e6ff7e5fa 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ changelog.txt # Ignore Minecraft runtime logs /logs + +# ClientDevBridge session state (golden images are intentionally kept) +.clientdevbridge/* +!.clientdevbridge/golden/ diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index fdeaac5fd..2865d8f49 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -39,6 +39,7 @@ import org.cyclops.cyclopscore.client.gui.component.input.IInputListener; import org.cyclops.cyclopscore.helper.Helpers; import org.cyclops.cyclopscore.helper.RenderHelpers; +import org.lwjgl.glfw.GLFW; import javax.annotation.Nullable; import java.util.Arrays; @@ -314,25 +315,25 @@ public boolean keyPressed(int typedChar, int keyCode, int modifiers) { this.setFocused(true); this.ensureCursorVisible(); return true; - } else { + } else if (typedChar == GLFW.GLFW_KEY_ESCAPE) { this.setFocused(false); return false; + } else { + // Consume all other keys, so they don't trigger screen actions (such as closing the screen) while typing + return true; } } @Override public boolean charTyped(char typedChar, int keyCode) { - if (super.charTyped(typedChar, keyCode)) { - this.setFocused(true); - return true; - } else if (StringUtil.isAllowedChatCharacter(typedChar)) { + // Don't delegate to EditBox, as that would consume the character into its own (unused) value + if (StringUtil.isAllowedChatCharacter(typedChar)) { this.textFieldHelper.insertText(Character.toString(typedChar)); this.clearDisplayCache(); this.setFocused(true); this.ensureCursorVisible(); return true; } else { - this.setFocused(false); return false; } } From 89d189ea34c4f9ef440726bfcff6be54b24709cb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 17:42:45 +0000 Subject: [PATCH 04/12] Keep trailing newlines when syncing scripts String.split dropped trailing empty strings, so blank lines at the end of a script were lost when sending it between client and server. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- ...TerminalScriptingModifiedScriptPacket.java | 2 +- ...inalScriptingModifiedScriptPacketTest.java | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingModifiedScriptPacketTest.java diff --git a/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingModifiedScriptPacket.java b/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingModifiedScriptPacket.java index 631133abf..ae34d68f2 100644 --- a/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingModifiedScriptPacket.java +++ b/src/main/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingModifiedScriptPacket.java @@ -42,7 +42,7 @@ public TerminalScriptingModifiedScriptPacket(int disk, Path path, String script) super(ID); this.disk = disk; this.path = path.toString(); - this.script = Arrays.stream(script.split("\n")).toList(); + this.script = Arrays.stream(script.split("\n", -1)).toList(); } public String getScript() { diff --git a/src/test/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingModifiedScriptPacketTest.java b/src/test/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingModifiedScriptPacketTest.java new file mode 100644 index 000000000..0f78a812e --- /dev/null +++ b/src/test/java/org/cyclops/integratedscripting/network/packet/TerminalScriptingModifiedScriptPacketTest.java @@ -0,0 +1,41 @@ +package org.cyclops.integratedscripting.network.packet; + +import org.junit.Test; + +import java.nio.file.Path; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +public class TerminalScriptingModifiedScriptPacketTest { + + private static String roundTrip(String script) { + return new TerminalScriptingModifiedScriptPacket(0, Path.of("script0.js"), script).getScript(); + } + + @Test + public void testSingleLine() { + assertThat(roundTrip("const a = 1;"), equalTo("const a = 1;")); + } + + @Test + public void testEmpty() { + assertThat(roundTrip(""), equalTo("")); + } + + @Test + public void testTrailingNewlines() { + assertThat(roundTrip("const a = 1;\n\n"), equalTo("const a = 1;\n\n")); + } + + @Test + public void testOnlyNewlines() { + assertThat(roundTrip("\n\n\n"), equalTo("\n\n\n")); + } + + @Test + public void testLeadingAndInnerNewlines() { + assertThat(roundTrip("\na\n\nb"), equalTo("\na\n\nb")); + } + +} From bb7247de6072d214319f9cf18e52dcad474dbbab Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 17:42:51 +0000 Subject: [PATCH 05/12] Start editor line numbers at 1 Script error messages count lines from 1, so a 0-based gutter made them off by one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../client/gui/component/input/WidgetTextArea.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index 2865d8f49..b5cd2399a 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -462,7 +462,7 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float RenderHelpers.drawScaledString( guiGraphics, font, - String.valueOf(line.lineNumber), + String.valueOf(line.lineNumber + 1), // Line numbers start at 1, like in error messages this.getX(), line.y - offsetY + 2, 0.5f, From 136e9f160a5ed181a18184a215763eddf118fe23 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 17:43:00 +0000 Subject: [PATCH 06/12] Don't wrap editor text below the scrollbar The wrap width didn't account for the scrollbar, so long lines were drawn underneath it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../client/gui/component/input/WidgetTextArea.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index b5cd2399a..905326095 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -66,6 +66,7 @@ public class WidgetTextArea extends EditBox implements GuiEventListener { public static final int ROW_HEIGHT = 9; + public static final int SCROLLBAR_WIDTH = 14; private final TextFieldHelperExtended textFieldHelper; private final Font font; @@ -97,7 +98,7 @@ public WidgetTextArea(Font font, int x, int y, int width, int height, Component this.showLineNumbers = showLineNumbers; this.textFieldHelper = new TextFieldHelperExtended(this::getValue, this::setValuePassive, this::getClipboard, this::setClipboard, s -> true, this::setSelected, this::onCursorPosChanged); if (scrollBar) { - this.scrollBar = new WidgetScrollBar(x + width - 14, y, height, + this.scrollBar = new WidgetScrollBar(x + width - SCROLLBAR_WIDTH, y, height, Component.translatable("gui.cyclopscore.scrollbar"), firstRow -> setFirstRow(firstRow, false), height / ROW_HEIGHT) { @Override public int getTotalRows() { @@ -526,6 +527,11 @@ private void renderHighlight(Rect2i[] p_98139_) { RenderSystem.setShaderColor(1, 1, 1, 1); } + private int getTextWidth() { + // Don't render text below the scrollbar + return this.getWidth() - getLinesXOffset() - (this.scrollBar != null ? SCROLLBAR_WIDTH : 0); + } + private int getLinesXOffset() { return this.showLineNumbers ? 14 : 0; } @@ -564,7 +570,7 @@ private DisplayCache rebuildDisplayCache() { MutableBoolean mutableboolean = new MutableBoolean(); StringSplitter stringsplitter = this.font.getSplitter(); MutableInt lineNumber = new MutableInt(); - stringsplitter.splitLines(s, this.getWidth() - getLinesXOffset() /* changed */, Style.EMPTY, true, (style, startPos, endPos) -> { + stringsplitter.splitLines(s, getTextWidth() /* changed */, Style.EMPTY, true, (style, startPos, endPos) -> { String stringPart = s.substring(startPos, endPos); boolean hasNewLine = stringPart.endsWith("\n"); mutableboolean.setValue(hasNewLine); From 6e9c2e05f64c398cfa0ccc492a16a05e24e80c6c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 17:43:12 +0000 Subject: [PATCH 07/12] Make page up and page down move through the editor Both keys were consumed without doing anything. Move the cursor and the viewport by one page instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/component/input/WidgetTextArea.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index 905326095..2f590de1f 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -378,10 +378,10 @@ private boolean textFieldKeyPressed(int p_98153_, int p_98154_, int p_98155_) { this.keyUp(); return true; case 266: -// this.backButton.onPress(); + this.keyPage(-1); return true; case 267: -// this.forwardButton.onPress(); + this.keyPage(1); return true; case 268: this.keyHome(); @@ -410,6 +410,12 @@ private void changeLine(int p_98098_) { this.textFieldHelper.setCursorPos(j, Screen.hasShiftDown()); } + private void keyPage(int direction) { + int rows = this.scrollBar != null ? this.scrollBar.getVisibleRows() : this.getHeight() / ROW_HEIGHT; + this.changeLine(direction * rows); + this.setFirstRow(this.firstRow + direction * rows, true); + } + /** * Scroll the viewport so that the cursor is visible. */ @@ -730,17 +736,10 @@ public int getIndexAtPosition(Font p_98214_, Pos2i p_98215_) { public int changeLine(int p_98211_, int p_98212_) { int i = WidgetTextArea.findLineFromPos(this.lineStarts, p_98211_); - int j = i + p_98212_; - int k; - if (0 <= j && j < this.lineStarts.length) { - int l = p_98211_ - this.lineStarts[i]; - int i1 = this.linesAll[j].contents.length(); - k = this.lineStarts[j] + Math.min(l, i1); - } else { - k = p_98211_; - } - - return k; + int j = Math.max(0, Math.min(this.lineStarts.length - 1, i + p_98212_)); + int l = p_98211_ - this.lineStarts[i]; + int i1 = this.linesAll[j].contents.length(); + return this.lineStarts[j] + Math.min(l, i1); } public int findLineStart(int p_98209_) { From 0fb748891da31d60a4848684f8b51d07bda4595f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 20:46:07 +0000 Subject: [PATCH 08/12] Keep the open script when the scripting terminal is resized Resizing the window, or changing the GUI scale, recreated the editor without the open script. The next key press then replaced the whole script with what was typed into the empty editor. Restore the open script and cursor position when the screen is initialized again, and close the removal dialog if it was open, as its widgets are removed as well. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../container/ContainerScreenTerminalScripting.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java index c13eeca3d..21f0c76d8 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/container/ContainerScreenTerminalScripting.java @@ -96,6 +96,12 @@ public ContainerScreenTerminalScripting(ContainerTerminalScripting container, In @Override public void init() { + // This is called again when the screen is resized, which recreates all widgets + if (textArea != null) { + this.saveCursorPos(); + } + pendingScriptRemovalDialog = null; + super.init(); fieldDisk = new WidgetArrowedListField<>(Minecraft.getInstance().font, leftPos + 30, @@ -132,6 +138,13 @@ public int getTotalRows() { buttonCreateFile = new ButtonText(this.leftPos + 19, this.topPos + 222, 56, 10, Component.translatable("gui.integratedscripting.create_file"), Component.literal("+"), (button) -> getMenu().createNewFile(), true); addRenderableWidget(buttonCreateFile); + + // Restore the open script in the new editor + if (getMenu().getActiveScriptPath() != null) { + this.onActiveScriptSelected(); + this.restoreCursorPos(); + textArea.ensureCursorVisible(); + } } @Override From 968d44c4eae3523f04827c6da46cd65afb9248e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 13:06:59 +0000 Subject: [PATCH 09/12] Use the macOS keys for moving by word and line in the script editor On macOS, Cmd+Left/Right jumped by word, while native text fields use Option+Left/Right for that, and Cmd+Left/Right to go to the start or end of the line. The editor now follows these conventions on macOS, including Option+Backspace to delete a word and Cmd+Up/Down to go to the start or end of the script. Other platforms keep using Ctrl. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/component/input/EditorKeys.java | 28 +++++++++++++ .../gui/component/input/WidgetTextArea.java | 39 +++++++++++++----- .../gui/component/input/EditorKeysTest.java | 40 +++++++++++++++++++ 3 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/cyclops/integratedscripting/client/gui/component/input/EditorKeys.java create mode 100644 src/test/java/org/cyclops/integratedscripting/client/gui/component/input/EditorKeysTest.java diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/EditorKeys.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/EditorKeys.java new file mode 100644 index 000000000..c45b40a6b --- /dev/null +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/EditorKeys.java @@ -0,0 +1,28 @@ +package org.cyclops.integratedscripting.client.gui.component.input; + +import org.lwjgl.glfw.GLFW; + +/** + * Platform conventions for moving the cursor in text editors. + * @author rubensworks + */ +public final class EditorKeys { + + private EditorKeys() { + } + + /** + * @return If the cursor should move or delete by word: Option on macOS, Ctrl elsewhere. + */ + public static boolean isWordModifier(int modifiers, boolean osx) { + return (modifiers & (osx ? GLFW.GLFW_MOD_ALT : GLFW.GLFW_MOD_CONTROL)) != 0; + } + + /** + * @return If arrow keys should jump to the line or document start or end, which is Cmd on macOS. + */ + public static boolean isLineModifier(int modifiers, boolean osx) { + return osx && (modifiers & GLFW.GLFW_MOD_SUPER) != 0; + } + +} diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index 2f590de1f..03130fb15 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -353,7 +353,8 @@ private boolean textFieldKeyPressed(int p_98153_, int p_98154_, int p_98155_) { this.textFieldHelper.cut(); return true; } else { - TextFieldHelper.CursorStep cursorStep = Screen.hasControlDown() ? TextFieldHelper.CursorStep.WORD : TextFieldHelper.CursorStep.CHARACTER; + TextFieldHelper.CursorStep cursorStep = EditorKeys.isWordModifier(p_98155_, Minecraft.ON_OSX) ? TextFieldHelper.CursorStep.WORD : TextFieldHelper.CursorStep.CHARACTER; + boolean lineModifier = EditorKeys.isLineModifier(p_98155_, Minecraft.ON_OSX); switch (p_98153_) { case 257: case 335: @@ -366,16 +367,32 @@ private boolean textFieldKeyPressed(int p_98153_, int p_98154_, int p_98155_) { this.textFieldHelper.removeFromCursor(1, cursorStep); return true; case 262: - this.textFieldHelper.moveBy(1, Screen.hasShiftDown(), cursorStep); + if (lineModifier) { + this.keyEnd(false); + } else { + this.textFieldHelper.moveBy(1, Screen.hasShiftDown(), cursorStep); + } return true; case 263: - this.textFieldHelper.moveBy(-1, Screen.hasShiftDown(), cursorStep); + if (lineModifier) { + this.keyHome(false); + } else { + this.textFieldHelper.moveBy(-1, Screen.hasShiftDown(), cursorStep); + } return true; case 264: - this.keyDown(); + if (lineModifier) { + this.keyEnd(true); + } else { + this.keyDown(); + } return true; case 265: - this.keyUp(); + if (lineModifier) { + this.keyHome(true); + } else { + this.keyUp(); + } return true; case 266: this.keyPage(-1); @@ -384,10 +401,10 @@ private boolean textFieldKeyPressed(int p_98153_, int p_98154_, int p_98155_) { this.keyPage(1); return true; case 268: - this.keyHome(); + this.keyHome(Screen.hasControlDown()); return true; case 269: - this.keyEnd(); + this.keyEnd(Screen.hasControlDown()); return true; default: return false; @@ -431,8 +448,8 @@ public void ensureCursorVisible() { } } - private void keyHome() { - if (Screen.hasControlDown()) { + private void keyHome(boolean document) { + if (document) { this.textFieldHelper.setCursorToStart(Screen.hasShiftDown()); } else { int i = this.textFieldHelper.getCursorPos(); @@ -442,8 +459,8 @@ private void keyHome() { } - private void keyEnd() { - if (Screen.hasControlDown()) { + private void keyEnd(boolean document) { + if (document) { this.textFieldHelper.setCursorToEnd(Screen.hasShiftDown()); } else { DisplayCache displayCache = this.getDisplayCache(); diff --git a/src/test/java/org/cyclops/integratedscripting/client/gui/component/input/EditorKeysTest.java b/src/test/java/org/cyclops/integratedscripting/client/gui/component/input/EditorKeysTest.java new file mode 100644 index 000000000..8b10b7590 --- /dev/null +++ b/src/test/java/org/cyclops/integratedscripting/client/gui/component/input/EditorKeysTest.java @@ -0,0 +1,40 @@ +package org.cyclops.integratedscripting.client.gui.component.input; + +import org.junit.Test; +import org.lwjgl.glfw.GLFW; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author rubensworks + */ +public class EditorKeysTest { + + @Test + public void testWordModifierMac() { + assertTrue(EditorKeys.isWordModifier(GLFW.GLFW_MOD_ALT, true)); + assertTrue(EditorKeys.isWordModifier(GLFW.GLFW_MOD_ALT | GLFW.GLFW_MOD_SHIFT, true)); + assertFalse(EditorKeys.isWordModifier(GLFW.GLFW_MOD_SUPER, true)); + assertFalse(EditorKeys.isWordModifier(GLFW.GLFW_MOD_CONTROL, true)); + assertFalse(EditorKeys.isWordModifier(0, true)); + } + + @Test + public void testWordModifierOthers() { + assertTrue(EditorKeys.isWordModifier(GLFW.GLFW_MOD_CONTROL, false)); + assertTrue(EditorKeys.isWordModifier(GLFW.GLFW_MOD_CONTROL | GLFW.GLFW_MOD_SHIFT, false)); + assertFalse(EditorKeys.isWordModifier(GLFW.GLFW_MOD_ALT, false)); + assertFalse(EditorKeys.isWordModifier(0, false)); + } + + @Test + public void testLineModifier() { + assertTrue(EditorKeys.isLineModifier(GLFW.GLFW_MOD_SUPER, true)); + assertTrue(EditorKeys.isLineModifier(GLFW.GLFW_MOD_SUPER | GLFW.GLFW_MOD_SHIFT, true)); + assertFalse(EditorKeys.isLineModifier(GLFW.GLFW_MOD_ALT, true)); + assertFalse(EditorKeys.isLineModifier(GLFW.GLFW_MOD_SUPER, false)); + assertFalse(EditorKeys.isLineModifier(GLFW.GLFW_MOD_CONTROL, false)); + } + +} From 076603e4efb896cd74c8149936158a790ec224d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 13:08:48 +0000 Subject: [PATCH 10/12] Don't draw the selection over the bottom border of the script editor When a selection continued below the last visible line, the highlight of the hidden next line was drawn over the editor's bottom border. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../client/gui/component/input/WidgetTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index 03130fb15..c9f589445 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -671,11 +671,11 @@ private DisplayCache rebuildDisplayCache() { selection.add(this.createPartialLineSelection(s, stringsplitter, lineStartsArr[lineEnd], i1, lineEnd * 9, lineStartsArr[lineEnd])); } } - // Hide out-of-view selections and offset them + // Hide out-of-view selections and offset them, including the partially visible row below the last line List selectionNew = Lists.newArrayList(); for (Rect2i rect2i : selection) { Rect2i rect2iNew = new Rect2i(rect2i.getX(), rect2i.getY() - this.firstRow * ROW_HEIGHT, rect2i.getWidth(), rect2i.getHeight()); - if (rect2iNew.getY() - this.getY() >= 0 && rect2iNew.getY() - this.getY() < getHeight()) { + if (rect2iNew.getY() - this.getY() >= 0 && rect2iNew.getY() + rect2iNew.getHeight() - this.getY() <= getHeight()) { selectionNew.add(rect2iNew); } } From c913defce64f407c3cbd85d1e1834429b25d1c09 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 13:10:38 +0000 Subject: [PATCH 11/12] Don't block dragging items over slots in the scripting terminal The script editor claimed every mouse drag, even outside of it, so spreading a held stack over inventory slots by dragging put the whole stack in one slot. The editor now only claims drags it handles. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../client/gui/component/input/WidgetTextArea.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index c9f589445..f6deda584 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -304,8 +304,10 @@ public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, doubl this.textFieldHelper.setCursorPos(i, true); this.clearDisplayCache(); this.setFocused(true); + return true; } - return true; + // Let other drags through, such as spreading items over inventory slots + return false; } @Override From 41456421b21cc8b8c12c58704954c36f5e9f6a54 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 13:21:02 +0000 Subject: [PATCH 12/12] Don't put the cursor on the hidden row below the script editor's last line The editor has room for part of a row below its last visible line. Clicking or dragging there put the cursor on that hidden line, and the cursor was drawn over the editor's bottom border. Such positions now go to the last visible line instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/component/input/WidgetTextArea.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java index f6deda584..6cf30e0d7 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/WidgetTextArea.java @@ -246,7 +246,7 @@ public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) { this.setFocused(true); long i = Util.getMillis(); DisplayCache displayCache = this.getDisplayCache(); - int j = displayCache.getIndexAtPosition(this.font, this.convertScreenToLocal(new Pos2i((int)mouseX, (int)mouseY))); + int j = displayCache.getIndexAtPosition(this.font, this.convertScreenToLocal(new Pos2i((int)mouseX, this.clampToVisibleRows(mouseY)))); if (j >= 0) { if (j == this.lastIndex && i - this.lastClickTime < 250L) { if (!this.textFieldHelper.isSelecting()) { @@ -300,7 +300,7 @@ public boolean mouseDragged(double mouseX, double mouseY, int mouseButton, doubl if (mouseButton == 0 && mouseX >= this.getX() && mouseX < this.getX() + this.width && mouseY >= this.getY() && mouseY < this.getY() + this.height) { DisplayCache bookeditscreen$displaycache = this.getDisplayCache(); - int i = bookeditscreen$displaycache.getIndexAtPosition(this.font, this.convertScreenToLocal(new Pos2i((int)mouseX, (int)mouseY))); + int i = bookeditscreen$displaycache.getIndexAtPosition(this.font, this.convertScreenToLocal(new Pos2i((int)mouseX, this.clampToVisibleRows(mouseY)))); this.textFieldHelper.setCursorPos(i, true); this.clearDisplayCache(); this.setFocused(true); @@ -429,6 +429,14 @@ private void changeLine(int p_98098_) { this.textFieldHelper.setCursorPos(j, Screen.hasShiftDown()); } + /** + * @return The y position, moved up to the last fully visible row if it is below it. + */ + private int clampToVisibleRows(double mouseY) { + int rows = this.scrollBar != null ? this.scrollBar.getVisibleRows() : this.getHeight() / ROW_HEIGHT; + return (int) Math.min(mouseY, this.getY() + rows * ROW_HEIGHT - 1); + } + private void keyPage(int direction) { int rows = this.scrollBar != null ? this.scrollBar.getVisibleRows() : this.getHeight() / ROW_HEIGHT; this.changeLine(direction * rows); @@ -644,8 +652,8 @@ private DisplayCache rebuildDisplayCache() { int cursorLine = findLineFromPos(lineStartsArr, cursorPos); int cursorX = this.font.width(s.substring(lineStartsArr[cursorLine], cursorPos)); Pos2i cursor = new Pos2i(cursorX, (cursorLine - this.firstRow) * 9); // We have to offset the firstRow here! - // Hide cursor if out of view - if (cursor.y < 0 || cursor.y > getHeight()) { + // Hide cursor if out of view, including the partially visible row below the last line + if (cursor.y < 0 || cursor.y + ROW_HEIGHT > getHeight()) { cursor = null; }