From ee16f1a8456514651f06abc56f77e8874c398dcb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 19:32:50 +0000 Subject: [PATCH] Wrap long lines at spaces and punctuation in the script editor Lines were broken at any character, splitting identifiers such as itemstackIsStackable over two lines. Lines now break after spaces, commas and brackets, before member accesses, and around operators, and wrapped lines are marked in the gutter. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018MRyTdV3fSV5Q5NPGep4q9 --- .../gui/component/input/TextEditing.java | 67 +++++++++++++++++++ .../gui/component/input/WidgetTextArea.java | 14 ++-- .../gui/component/input/TextEditingTest.java | 37 ++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/TextEditing.java b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/TextEditing.java index 8f7c0e221..5c67e4fdd 100644 --- a/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/TextEditing.java +++ b/src/main/java/org/cyclops/integratedscripting/client/gui/component/input/TextEditing.java @@ -1,5 +1,10 @@ package org.cyclops.integratedscripting.client.gui.component.input; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; + +import java.util.function.IntToDoubleFunction; + /** * Code editing operations on the state of a text area. * @author rubensworks @@ -305,4 +310,66 @@ public static TextHistory.State moveLines(TextHistory.State state, int direction } } + + private static final String BREAK_AFTER = ",;([{"; + private static final String OPERATORS = "+-*/%=&|<>!?:"; + + private static boolean canBreakBefore(String value, int pos) { + char prev = value.charAt(pos - 1); + char cur = value.charAt(pos); + if (cur == ' ') { + return false; + } + if (prev == ' ' || BREAK_AFTER.indexOf(prev) >= 0) { + return true; + } + // Don't split multi-character operators such as && and >= + if ((OPERATORS.indexOf(prev) >= 0) != (OPERATORS.indexOf(cur) >= 0)) { + return true; + } + // Keep member access chains readable, but don't split numbers + return cur == '.' && !Character.isDigit(prev); + } + + /** + * Wrap lines to the given width, preferably at spaces and punctuation, so identifiers are not split. + * Trailing spaces may exceed the width. + * @param value A text. + * @param maxWidth The maximum line width. + * @param charWidth The width of a character. + * @return The start positions of all wrapped lines. + */ + public static int[] wrapLines(String value, float maxWidth, IntToDoubleFunction charWidth) { + IntList starts = new IntArrayList(); + int start = 0; + while (true) { + starts.add(start); + int end = value.indexOf('\n', start); + if (end < 0) { + end = value.length(); + } + float width = 0; + int lastBreak = -1; + for (int i = start; i < end; i++) { + if (i > start && canBreakBefore(value, i)) { + lastBreak = i; + } + char c = value.charAt(i); + width += (float) charWidth.applyAsDouble(c); + if (width > maxWidth && i > start && c != ' ') { + start = lastBreak > start ? lastBreak : i; + starts.add(start); + width = 0; + lastBreak = -1; + i = start - 1; + } + } + if (end >= value.length() - 1) { + break; + } + start = end + 1; + } + return starts.toIntArray(); + } + } 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 3bc4177e0..05987749f 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 @@ -787,6 +787,10 @@ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float false, Font.DisplayMode.NORMAL ); + } else if (this.showLineNumbers) { + // Mark wrapped continuation lines + RenderHelpers.drawScaledString(guiGraphics, font, "\u21B3", this.getX() + 4, line.y - offsetY + 2, 0.5f, + Helpers.RGBToInt(160, 160, 160), false, Font.DisplayMode.NORMAL); } lastLineNumber = line.lineNumber; } @@ -1012,17 +1016,19 @@ private TextLayout getLayout(String s) { IntList lineNumbers = new IntArrayList(); MutableBoolean trailingNewline = new MutableBoolean(); MutableInt lineNumber = new MutableInt(); - this.font.getSplitter().splitLines(s, getTextWidth() /* changed */, Style.EMPTY, true, (style, startPos, endPos) -> { - String stringPart = s.substring(startPos, endPos); + StringSplitter splitter = this.font.getSplitter(); + int[] starts = TextEditing.wrapLines(s, getTextWidth(), c -> splitter.stringWidth(String.valueOf((char) c))); + for (int i = 0; i < starts.length; i++) { + String stringPart = s.substring(starts[i], i + 1 < starts.length ? starts[i + 1] : s.length()); boolean hasNewLine = stringPart.endsWith("\n"); trailingNewline.setValue(hasNewLine); - lineStarts.add(startPos); + lineStarts.add(starts[i]); contents.add(StringUtils.stripEnd(stringPart, " \n")); lineNumbers.add(lineNumber.intValue()); if (hasNewLine) { lineNumber.increment(); } - }); + } // Add an empty line after a trailing newline, so the cursor can be placed and scrolled there if (trailingNewline.isTrue()) { lineStarts.add(s.length()); diff --git a/src/test/java/org/cyclops/integratedscripting/client/gui/component/input/TextEditingTest.java b/src/test/java/org/cyclops/integratedscripting/client/gui/component/input/TextEditingTest.java index 729ba3337..8ef894782 100644 --- a/src/test/java/org/cyclops/integratedscripting/client/gui/component/input/TextEditingTest.java +++ b/src/test/java/org/cyclops/integratedscripting/client/gui/component/input/TextEditingTest.java @@ -220,4 +220,41 @@ public void testNewlineTwoSpaces() { assertThat(TextEditing.newline(s(" if (a) {", 10), TWO), equalTo(s(" if (a) {\n ", 15))); } + + private static int[] wrap(String value, int width) { + return TextEditing.wrapLines(value, width, c -> 1); + } + + @Test + public void testWrapLinesFits() { + assertThat(wrap("", 10), equalTo(new int[]{0})); + assertThat(wrap("abc\ndef", 10), equalTo(new int[]{0, 4})); + assertThat(wrap("abc\n", 10), equalTo(new int[]{0})); + assertThat(wrap("a\n\nb", 10), equalTo(new int[]{0, 2, 3})); + } + + @Test + public void testWrapLinesAtSpace() { + // Trailing spaces stay on the line + assertThat(wrap("return abc", 8), equalTo(new int[]{0, 7})); + assertThat(wrap("a bc", 3), equalTo(new int[]{0, 4})); + } + + @Test + public void testWrapLinesAtPunctuation() { + assertThat(wrap("ctx.ops.size(item)", 10), equalTo(new int[]{0, 7, 13})); + assertThat(wrap("abc,def", 5), equalTo(new int[]{0, 4})); + } + + @Test + public void testWrapLinesKeepsOperatorsAndNumbers() { + assertThat(wrap("a>=b", 2), equalTo(new int[]{0, 1, 3})); + assertThat(wrap("x=12.5", 5), equalTo(new int[]{0, 2})); + } + + @Test + public void testWrapLinesSplitsLongTokens() { + assertThat(wrap("abcdefgh", 3), equalTo(new int[]{0, 3, 6})); + } + }