diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java index 1c4b4e95..e10bc7fd 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java @@ -195,43 +195,55 @@ public Paragraph concat(Paragraph p) { if(p.length() == 0) { return this; } - if(length() == 0) { return p; } + List concatSegments = concatSegmentsOf(p); + StyleSpans concatStyles = concatStylesOf(p); + return new Paragraph<>(paragraphStyle, segmentOps, concatSegments, concatStyles); + } - List updatedSegs; - SEG leftSeg = segments.get(segments.size() - 1); - SEG rightSeg = p.segments.get(0); - Optional joined = segmentOps.joinSeg(leftSeg, rightSeg); - if(joined.isPresent()) { - SEG segment = joined.get(); - updatedSegs = new ArrayList<>(segments.size() + p.segments.size() - 1); - updatedSegs.addAll(segments.subList(0, segments.size()-1)); - updatedSegs.add(segment); - updatedSegs.addAll(p.segments.subList(1, p.segments.size())); - } else { - updatedSegs = new ArrayList<>(segments.size() + p.segments.size()); - updatedSegs.addAll(segments); - updatedSegs.addAll(p.segments); - } - - StyleSpans updatedStyles; + private StyleSpans concatStylesOf(Paragraph p) { StyleSpan leftSpan = styles.getStyleSpan(styles.getSpanCount() - 1); StyleSpan rightSpan = p.styles.getStyleSpan(0); - Optional merge = segmentOps.joinStyle(leftSpan.getStyle(), rightSpan.getStyle()); - if (merge.isPresent()) { - int startOfMerge = styles.position(styles.getSpanCount() - 1, 0).toOffset(); - StyleSpans updatedLeftSpan = styles.subView(0, startOfMerge); - int endOfMerge = p.styles.position(1, 0).toOffset(); - StyleSpans updatedRightSpan = p.styles.subView(endOfMerge, p.styles.length()); - updatedStyles = updatedLeftSpan - .append(merge.get(), leftSpan.getLength() + rightSpan.getLength()) + Optional mergedStyle = segmentOps.joinStyle(leftSpan.getStyle(), rightSpan.getStyle()); + if (mergedStyle.isPresent()) { + StyleSpans updatedLeftSpan = getSubStyle(0, styles.getSpanCount() - 1); + StyleSpans updatedRightSpan = getSubStyle(1, styles.getSpanCount()); + return updatedLeftSpan + .append(mergedStyle.get(), leftSpan.getLength() + rightSpan.getLength()) .concat(updatedRightSpan); + } + return styles.concat(p.styles); + } + + private StyleSpans getSubStyle(int from, int to) { + int start = styles.position(from, 0).toOffset(); + int end = styles.position(to, 0).toOffset(); + return styles.subView(start, end); + } + + private List concatSegmentsOf(Paragraph par) { + List updatedSegments; + Optional joined = concatLastSegmentWith(par); + if(joined.isPresent()) { + SEG segment = joined.get(); + updatedSegments = new ArrayList<>(segments.size() + par.segments.size() - 1); // -2 + 1 + updatedSegments.addAll(segments.subList(0, segments.size()-1)); + updatedSegments.add(segment); + updatedSegments.addAll(par.segments.subList(1, par.segments.size())); } else { - updatedStyles = styles.concat(p.styles); + updatedSegments = new ArrayList<>(segments.size() + par.segments.size()); + updatedSegments.addAll(segments); + updatedSegments.addAll(par.segments); } - return new Paragraph<>(paragraphStyle, segmentOps, updatedSegs, updatedStyles); + return updatedSegments; + } + + private Optional concatLastSegmentWith(Paragraph par) { + SEG leftLastSegment = segments.get(segments.size() - 1); + SEG rightFirstSegment = par.segments.get(0); + return segmentOps.joinSeg(leftLastSegment, rightFirstSegment); } /** @@ -248,20 +260,13 @@ public Paragraph subSequence(int start, int end) { return trim(end).subSequence(start); } - public Paragraph trim(int length) { - if(length >= length()) { - return this; - } else { - Position pos = navigator.offsetToPosition(length, Backward); - int segIdx = pos.getMajor(); - List segs = new ArrayList<>(segIdx + 1); - segs.addAll(segments.subList(0, segIdx)); - segs.add(segmentOps.subSequence(segments.get(segIdx), 0, pos.getMinor())); - if (segs.isEmpty()) { - segs.add(segmentOps.createEmptySeg()); - } - return new Paragraph<>(paragraphStyle, segmentOps, segs, styles.subView(0, length)); + public Paragraph trim(int end) { + end = Math.max(0, end); + if(end < length()) { + List segments = trimSegmentsTo(navigator.offsetToPosition(end, Backward)); + return new Paragraph<>(paragraphStyle, segmentOps, segments, styles.subView(0, end)); } + return this; } public Paragraph subSequence(int start) { @@ -274,18 +279,27 @@ public Paragraph subSequence(int start) { // to use the left ops' default empty seg, not the right one's empty seg return new Paragraph<>(paragraphStyle, segmentOps, segmentOps.createEmptySeg(), styles.subView(start,start)); } else if(start < length()) { - Position pos = navigator.offsetToPosition(start, Forward); - int segIdx = pos.getMajor(); - List segs = new ArrayList<>(segments.size() - segIdx); - segs.add(segmentOps.subSequence(segments.get(segIdx), pos.getMinor())); - segs.addAll(segments.subList(segIdx + 1, segments.size())); - if (segs.isEmpty()) { - segs.add(segmentOps.createEmptySeg()); - } - return new Paragraph<>(paragraphStyle, segmentOps, segs, styles.subView(start, styles.length())); - } else { - throw new IndexOutOfBoundsException(start + " not in [0, " + length() + "]"); + Position position = navigator.offsetToPosition(start, Forward); + List segments = trimSegmentsFrom(position); + return new Paragraph<>(paragraphStyle, segmentOps, segments, styles.subView(start, styles.length())); } + throw new IndexOutOfBoundsException(start + " not in [0, " + length() + "]"); + } + + private List trimSegmentsTo(Position position) { + int index = position.getMajor(); + List segments = new ArrayList<>(index + 1); + segments.addAll(this.segments.subList(0, index)); + segments.add(segmentOps.subSequence(this.segments.get(index), 0, position.getMinor())); + return segments; + } + + private List trimSegmentsFrom(Position position) { + int index = position.getMajor(); + List segments = new ArrayList<>(this.segments.size() - index); + segments.add(segmentOps.subSequence(this.segments.get(index), position.getMinor())); + segments.addAll(this.segments.subList(index + 1, this.segments.size())); + return segments; } public Paragraph delete(int start, int end) { @@ -306,14 +320,13 @@ public Paragraph restyle(S style) { } public Paragraph restyle(int from, int to, S style) { - if(from >= length()) { - return this; - } else { + if(from < length()) { StyleSpans left = styles.subView(0, from); StyleSpans right = styles.subView(to, length()); StyleSpans updatedStyles = left.append(style, to - from).concat(right); return new Paragraph<>(paragraphStyle, segmentOps, segments, updatedStyles); } + return this; } public Paragraph restyle(int from, StyleSpans styleSpans) { @@ -324,14 +337,15 @@ public Paragraph restyle(int from, StyleSpans styleSpan if(length() == 0) { return new Paragraph<>(paragraphStyle, segmentOps, segments, (StyleSpans) styleSpans); } + StyleSpans updatedStyles = restyleExistingFrom(from, styleSpans); + return new Paragraph<>(paragraphStyle, segmentOps, segments, updatedStyles); + } + private StyleSpans restyleExistingFrom(int from, StyleSpans styleSpans) { + int len = styleSpans.length(); StyleSpans left = styles.subView(0, from); StyleSpans right = styles.subView(from + len, length()); - - // type issue with concat - StyleSpans castedSpans = (StyleSpans) styleSpans; - StyleSpans updatedStyles = left.concat(castedSpans).concat(right); - return new Paragraph<>(paragraphStyle, segmentOps, segments, updatedStyles); + return left.concat((StyleSpans) styleSpans).concat(right); } /** 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..ec55d060 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java @@ -8,6 +8,7 @@ import javafx.scene.control.IndexRange; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class ParagraphTest { @@ -20,9 +21,9 @@ private void checkStyle(Paragraph paragraph, int length, T[] styles 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(styles[i], style.getStyle(), "Incorrect style for " + 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); } } @@ -155,10 +156,21 @@ public void multiStyleParagraphReturnsCorrect_subSequenceOfLength() { } @Test + @DisplayName("Trim on empty text should return empty") + public void trimOnEmpty() { + Paragraph p1 = createTextParagraph(SegmentOps.styledTextOps(), ""); + assertEquals("", p1.trim(-1).getText()); + assertEquals("", p1.trim(0).getText()); + assertEquals("", p1.trim(1).getText()); + assertEquals("", p1.trim(Integer.MAX_VALUE).getText()); + } + + @Test + @DisplayName("Trim text should return the text until the provided position") public void trimParagraph() { Paragraph p1 = createTextParagraph(SegmentOps.styledTextOps(), "Alpha", "MyStyle"); // Not very consistent that MIN_VALUE is throwing an exception while other negative numbers work - assertThrows(StringIndexOutOfBoundsException.class, () -> p1.trim(Integer.MIN_VALUE).getText()); + assertEquals("", p1.trim(Integer.MIN_VALUE).getText()); assertEquals("", p1.trim(-10).getText()); assertEquals("", p1.trim(-1).getText()); assertEquals("Alpha", p1.trim(Integer.MAX_VALUE).getText()); @@ -186,7 +198,7 @@ public void deletePartOfParagraph() { assertThrows(IndexOutOfBoundsException.class, () -> paragraph.delete(4, 10).getText()); // Not consistent with -1 assertEquals("gated", paragraph.delete(0, 4).getText()); assertEquals("gated", paragraph.delete(-1, 4).getText()); - assertThrows(StringIndexOutOfBoundsException.class, () -> paragraph.delete(Integer.MIN_VALUE, 4).getText()); // Not very consistent with -1 + assertEquals("gated", paragraph.delete(Integer.MIN_VALUE, 4).getText()); // Check style too Paragraph p2 = paragraph.delete(2, 5); @@ -232,6 +244,10 @@ public void multipleStyle() { Paragraph p1 = createTextParagraph("To be or not to be", "text"); checkStyle(p1, 18, new String[] {"text"}, 0, 18); + // Restyle with same style + Paragraph p1b = p1.restyle(0, 18, "text"); + checkStyle(p1b, 18, new String[] {"text"}, 0, 18); + // P1 is immutable, its style hasn't changed, but P2 has now three styles Paragraph p2 = p1.restyle(9, 12, "keyword"); checkStyle(p1, 18, new String[] {"text"}, 0, 18); @@ -241,8 +257,18 @@ 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 + // Bug + 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);