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})); + } + }