diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpan.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpan.java index 8ccf7069..0bf8437e 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpan.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpan.java @@ -8,7 +8,6 @@ * @param the style type */ public class StyleSpan { - private final S style; private final int length; private int startPos = 0; @@ -20,7 +19,6 @@ public StyleSpan(S style, int length) { if(length < 0) { throw new IllegalArgumentException("StyleSpan's length cannot be negative"); } - this.style = style; this.length = length; } @@ -39,10 +37,16 @@ public int getLength() { return length; } + // TODO This one is only used in the factory, can't se make the start position immutable and use "moveTo" + // instead (essentially turning this into an immutable style) void setStart( int start ) { startPos = start; } + StyleSpan moveTo(int start) { + return new StyleSpan<>(style, start, length); + } + int getStart() { return startPos; } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpans.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpans.java index 2a132788..86e15557 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpans.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpans.java @@ -136,7 +136,7 @@ default StyleSpans subView(int from, int to) { } /** - * Same as {@link java.util.List#subList(int, int)}, except that the arguments are two dimensional. + * Same as {@link java.util.List#subList(int, int)}, except that the arguments are two-dimensional. */ default StyleSpans subView(Position from, Position to) { return new SubSpans<>(this, from, to); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpansBuilder.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpansBuilder.java index 3d98137c..0760eed3 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpansBuilder.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpansBuilder.java @@ -165,10 +165,12 @@ public StyleSpans create() { private void _add(StyleSpan span) { if(spans.isEmpty()) { spans.add(span); - } else if(span.getLength() > 0) { + } + else if(span.getLength() > 0) { if(spans.size() == 1 && spans.get(0).getLength() == 0) { spans.set(0, span); - } else { + } + else { StyleSpan prev = spans.get(spans.size() - 1); if(prev.getStyle().equals(span.getStyle())) { spans.set(spans.size() - 1, new StyleSpan<>(span.getStyle(), prev.getStart(), prev.getLength() + span.getLength())); @@ -177,8 +179,6 @@ private void _add(StyleSpan span) { spans.add(span); } } - } else { - // do nothing, don't add a zero-length span } } @@ -328,7 +328,7 @@ class AppendedSpans extends StyleSpansBase { public AppendedSpans(StyleSpans original, StyleSpan appended) { this.original = original; - this.appended = appended; + this.appended = appended; // .moveTo(original.length()); } @Override diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java index f0658b53..0850ea28 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java @@ -1,5 +1,6 @@ package org.fxmisc.richtext.model; +import static org.fxmisc.richtext.model.StyleSpansChecker.checkStyle; import static org.junit.jupiter.api.Assertions.*; import java.util.Collection; @@ -11,21 +12,6 @@ import org.junit.jupiter.api.Test; public class ParagraphTest { - private void checkStyle(Paragraph paragraph, int length, T[] styles, int... ranges) { - if(ranges.length % 2 == 1 || styles.length != ranges.length / 2) { - throw new IllegalArgumentException("Ranges must come in pair [start;end] and correspond to the style count"); - } - StyleSpans styleSpans = paragraph.getStyleSpans(); - assertEquals(length, styleSpans.length()); - assertEquals(ranges.length/2, styleSpans.getSpanCount(), "Style segment count invalid"); - for (int i = 0; i < ranges.length/2 ; i++) { - StyleSpan style = styleSpans.getStyleSpan(i); - assertEquals(ranges[i*2], style.getStart(), "Start not matching for " + i); - assertEquals(ranges[i*2 + 1] - ranges[i*2], style.getLength(), "Length not matching for " + i); - assertEquals(styles[i], style.getStyle(), "Incorrect style for " + i); - } - } - private Paragraph createTextParagraph(TextOps segOps, String text) { return new Paragraph<>(null, segOps, segOps.create(text), (Void)null); } @@ -241,8 +227,17 @@ public void multipleStyle() { Paragraph p3 = p2.restyle(3, 10, "unknown"); checkStyle(p3, 18, new String[] {"text", "unknown", "keyword", "text"}, 0, 3, 3, 10, 10, 12, 12, 18); + // Restyle up to the end + checkStyle(p3.restyle(11, 17, "out"), 18, + new String[] {"text", "unknown", "keyword", "out", "text"}, + 0, 3, 3, 10, 10, 11, 11, 17, 17, 18); + + // Restyle up to the end + checkStyle(p3.restyle(11, 18, "out"), 18, + new String[] {"text", "unknown", "keyword", "out"}, + 0, 3, 3, 10, 0, 1, 0, 7); + // Restyle out of bound - // Bug: the styles are totally off checkStyle(p3.restyle(11, 19, "out"), 19, new String[] {"text", "unknown", "keyword", "out"}, 0, 3, 3, 10, 0, 1, 0, 8); diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpanTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpanTest.java new file mode 100644 index 00000000..8596fbcc --- /dev/null +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpanTest.java @@ -0,0 +1,68 @@ +package org.fxmisc.richtext.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class StyleSpanTest { + private void checkSpan(StyleSpan span, String style, int pos, int len) { + assertEquals(len, span.getLength(), "Incorrect length"); + assertEquals(pos, span.getStart(), "Incorrect position"); + assertEquals(style, span.getStyle(), "Incorrect style"); + } + + @Test + @DisplayName("Style do not equal if their length or style is different") + void equals() { + assertEquals( + new StyleSpan<>("myStyle", 2, 3), + new StyleSpan<>("myStyle", 1, 3) + ); + assertNotEquals( + new StyleSpan<>("myStyle", 2, 3), + new StyleSpan<>("myStyle", 2, 2) + ); + assertNotEquals( + new StyleSpan<>("myStyle", 2, 3), + new StyleSpan<>("other", 2, 3) + ); + } + + @Test + @DisplayName("Create a valid span") + void createValidSpan() { + StyleSpan a = new StyleSpan<>("myStyle", 10); + StyleSpan b = new StyleSpan<>("myStyle", 0, 10); + checkSpan(a, "myStyle", 0, 10); + checkSpan(b, "myStyle", 0, 10); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + @DisplayName("Move span to a different location") + void moveSpan() { + StyleSpan a = new StyleSpan<>("myStyle", 10); + StyleSpan b = new StyleSpan<>("myStyle", 2, 10); + assertEquals(a, b); // position does not matter for equality + assertEquals(a.hashCode(), b.hashCode()); + StyleSpan c = a.moveTo(2); + assertEquals(c, b); + checkSpan(a, "myStyle", 0, 10); + checkSpan(b, "myStyle", 2, 10); + checkSpan(c, "myStyle", 2, 10); + } + + @Test + @DisplayName("Cannot create a span with a negative length") + void cannotCreateWithNegativeLength() { + // This is bad practice to throw exception in constructor, but that's the way it is. Other solutions would + // have been to use a static create method, a factory, or to change length to 0 + assertThrows(IllegalArgumentException.class, () -> new StyleSpan<>("test", -1)); + // This is not consistent, as it doesn't throw + checkSpan(new StyleSpan<>("test", 0, -1), "test", 0, -1); + } +} diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpansBuilderTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpansBuilderTest.java new file mode 100644 index 00000000..5fe025b7 --- /dev/null +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpansBuilderTest.java @@ -0,0 +1,70 @@ +package org.fxmisc.richtext.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.fxmisc.richtext.model.StyleSpansChecker.checkStyle; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class StyleSpansBuilderTest { + @Test + @DisplayName("Cannot create an empty style span") + void empty() { + assertThrows(IllegalStateException.class, () -> new StyleSpansBuilder<>().create()); + } + + @Test + @DisplayName("Creating a single-style style span") + void createStyleSpan() { + StyleSpans style = new StyleSpansBuilder().add("alpha", 10).create(); + checkStyle(style, 10, new String[] {"alpha"}, 0, 10); + } + + @Test + @DisplayName("Adding only empty styles") + void onlyEmptyStyles() { + StyleSpans style = new StyleSpansBuilder() + .add("alpha", 0) + .add("beta", 0) + .add("charlie", 0) + .create(); + checkStyle(style, 0, new String[] {"alpha"}, 0, 0); + } + + @Test + @DisplayName("Adding style on only empty, overwrites it") + void nonEmptyOnEmpty() { + StyleSpans style = new StyleSpansBuilder() + .add("alpha", 0) + .add("beta", 0) + .add("charlie", 0) + .add("delta", 1) + .create(); + checkStyle(style, 1, new String[] {"delta"}, 0, 1); + } + + @Test + @DisplayName("Add a list of only one style will merge them all") + void addListOfOneStyle() { + StyleSpans style = new StyleSpansBuilder() + .addAll(List.of(new StyleSpan<>("alpha", 1), new StyleSpan<>("alpha", 2), new StyleSpan<>("alpha", 3))) + .create(); + checkStyle(style, 6, new String[] {"alpha"}, 0, 6); + } + + @Test + @DisplayName("Creating a multi-style style span") + void multiStyleSpan() { + StyleSpans style = new StyleSpansBuilder() + .add("alpha", 1) + .add("alpha", 2) + .add("beta", 3) + .add("delta", 0) + .add("charlie", 4) + .add("alpha", 5) + .create(); + checkStyle(style, 15, new String[] {"alpha", "beta", "charlie", "alpha"}, 0, 3, 3, 6, 6, 10, 10, 15); + } +} diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpansChecker.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpansChecker.java new file mode 100644 index 00000000..b91bec03 --- /dev/null +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpansChecker.java @@ -0,0 +1,33 @@ +package org.fxmisc.richtext.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StyleSpansChecker { + private final StyleSpans styleSpans; + + public StyleSpansChecker(StyleSpans styleSpans) { + this.styleSpans = styleSpans; + } + + public void check(int length, T[] styles, int... ranges) { + if(ranges.length % 2 == 1 || styles.length != ranges.length / 2) { + throw new IllegalArgumentException("Ranges must come in pair [start;end] and correspond to the style count"); + } + assertEquals(length, styleSpans.length()); + assertEquals(ranges.length/2, styleSpans.getSpanCount(), "Style segment count invalid"); + for (int i = 0; i < ranges.length/2 ; i++) { + StyleSpan style = styleSpans.getStyleSpan(i); + assertEquals(ranges[i*2], style.getStart(), "Start not matching for " + i); + assertEquals(ranges[i*2 + 1] - ranges[i*2], style.getLength(), "Length not matching for " + i); + assertEquals(styles[i], style.getStyle(), "Incorrect style for " + i); + } + } + + public static void checkStyle(Paragraph paragraph, int length, T[] styles, int... ranges) { + checkStyle(paragraph.getStyleSpans(), length, styles, ranges); + } + + public static void checkStyle(StyleSpans styleSpans, int length, T[] styles, int... ranges) { + new StyleSpansChecker(styleSpans).check(length, styles, ranges); + } +} diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpansTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpansTest.java new file mode 100644 index 00000000..657d2189 --- /dev/null +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyleSpansTest.java @@ -0,0 +1,90 @@ +package org.fxmisc.richtext.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.fxmisc.richtext.model.StyleSpansChecker.checkStyle; + +public class StyleSpansTest { + private StyleSpans create(String style, int len) { + StyleSpansBuilder builder = new StyleSpansBuilder<>(); + builder.add(style, len); + return builder.create(); + } + + @Nested + @DisplayName("Extract subview") + class SubViewTest { + @Test + @DisplayName("Extract subview from a single style span should return the same style") + void createSubViewFromSingleStyle() { + StyleSpans subStyle = create("text", 10).subView(2, 8); + checkStyle(subStyle, 6, new String[] {"text"}, 0, 6); + } + + @Test + @DisplayName("Extract subview cutting in different styles") + void createSubViewFromMultiple() { + StyleSpansBuilder builder = new StyleSpansBuilder<>(); + builder.add("alpha", 6); + builder.add("beta", 7); + builder.add("charlie", 8); + StyleSpans styleSpans = builder.create(); + checkStyle(styleSpans, 21, new String[] {"alpha", "beta", "charlie"}, 0, 6, 6, 13, 13, 21); + checkStyle(styleSpans.subView(6, 13), 7, new String[] {"beta"}, 0, 7); + // Empty + checkStyle(styleSpans.subView(7, 7), 0, new String[] {"beta"}, 0, 0); // Strange, why isn't it empty? + // Inverted indexes + checkStyle(styleSpans.subView(14, 5), 0, new String[] {"charlie"}, 0, 0); + // Out of bound + checkStyle(styleSpans.subView(-1,22), 23, new String[] {"alpha", "beta", "charlie"}, 0, 6, 6, 13, 0, 10); + // Bug + checkStyle(styleSpans.subView(6, 14), 8, new String[] {"beta", "charlie"}, 0, 7, 0, 1); + checkStyle(styleSpans.subView(5, 13), 8, new String[] {"alpha", "beta"}, 0, 1, 0, 7); + checkStyle(styleSpans.subView(5, 14), 9, new String[] {"alpha", "beta", "charlie"}, 0, 1, 6, 13, 0, 1); + } + } + + @Nested + @DisplayName("Append") + class AppendTest { + private StyleSpans base; + + @BeforeEach + void setup() { + base = create("text", 10); + checkStyle(base, 10, new String[]{"text"}, 0, 10); + } + + @Test + @DisplayName("Append empty style at the end of an existing style does not do anything") + void appendEmpty() { + checkStyle(base.append("text", 0), 10, + new String[]{"text"}, 0, 10); + + } + + @Test + @DisplayName("Append on an empty style will create a new style with the provided values") + void appendOnEmpty() { + checkStyle(create("test", 0).append("text", 2), 2, + new String[]{"text"}, 0, 2); + } + + @Test + @DisplayName("Append a different style to an existing style span should add it at the end") + void appendDifferentStyle() { + checkStyle(base.append("else", 2), 12, + new String[]{"text", "else"}, 0, 10, 0, 2); + } + + @Test + @DisplayName("Append an existing style at the end of a style should extend the existing one") + void appendSameStyle() { + checkStyle(base.append("text", 2), 12, + new String[]{"text"}, 0, 12); + } + } +}