diff --git a/richtextfx/src/integrationTest/java/org/fxmisc/richtext/api/VisibleParagraphTest.java b/richtextfx/src/integrationTest/java/org/fxmisc/richtext/api/VisibleParagraphTest.java new file mode 100644 index 000000000..f5855bb9d --- /dev/null +++ b/richtextfx/src/integrationTest/java/org/fxmisc/richtext/api/VisibleParagraphTest.java @@ -0,0 +1,152 @@ +package org.fxmisc.richtext.api; + +import org.fxmisc.richtext.InlineCssTextAreaAppTest; +import org.fxmisc.richtext.model.Paragraph; +import org.junit.Test; +import org.junit.Assert; +import org.reactfx.collection.LiveList; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class VisibleParagraphTest extends InlineCssTextAreaAppTest { + + @Test + public void get_first_visible_paragraph_index_with_non_blank_lines() { + String[] lines = { + "abc", + "def", + "ghi" + }; + interact(() -> { + area.setWrapText(true); + stage.setWidth(120); + area.replaceText(String.join("\n", lines)); + }); + assertEquals(0, area.firstVisibleParToAllParIndex()); + assertEquals(2, area.lastVisibleParToAllParIndex()); + assertEquals(1, area.visibleParToAllParIndex(1)); + } + @Test + public void get_last_visible_paragraph_index_with_non_blank_lines() { + String[] lines = { + "abc", + "def", + "ghi" + }; + interact(() -> { + area.setWrapText(true); + stage.setWidth(120); + area.replaceText(String.join("\n", lines)); + }); + assertEquals(2, area.lastVisibleParToAllParIndex()); + } + @Test + public void get_specific_visible_paragraph_index_with_non_blank_lines() { + String[] lines = { + "abc", + "def", + "ghi" + }; + interact(() -> { + area.setWrapText(true); + stage.setWidth(120); + area.replaceText(String.join("\n", lines)); + }); + assertEquals(2, area.visibleParToAllParIndex(2)); + } + @Test + public void get_first_visible_paragraph_index_with_all_blank_lines() { + String[] lines = { + "", + "", + "" + }; + interact(() -> { + area.setWrapText(true); + stage.setWidth(120); + area.replaceText(String.join("\n", lines)); + }); + assertEquals(0, area.firstVisibleParToAllParIndex()); + } + @Test + public void get_last_visible_paragraph_index_with_all_blank_lines() { + String[] lines = { + "", + "", + "" + }; + interact(() -> { + area.setWrapText(true); + stage.setWidth(120); + area.replaceText(String.join("\n", lines)); + }); + assertEquals(2, area.lastVisibleParToAllParIndex()); + } + @Test + public void get_specific_visible_paragraph_index_with_all_blank_lines() { + String[] lines = { + "", + "", + "" + }; + interact(() -> { + area.setWrapText(true); + stage.setWidth(120); + area.replaceText(String.join("\n", lines)); + }); + assertEquals(2, area.visibleParToAllParIndex(2)); + } + + @Test + public void get_first_visible_paragraph_index_with_some_blank_lines() { + String[] lines = { + "abc", + "", + "", + "", + "def", + "" + }; + interact(() -> { + area.setWrapText(true); + stage.setWidth(120); + area.replaceText(String.join("\n", lines)); + }); + assertEquals(0, area.firstVisibleParToAllParIndex()); + } + @Test + public void get_last_visible_paragraph_index_with_some_blank_lines() { + String[] lines = { + "abc", + "", + "", + "", + "def", + "" + }; + interact(() -> { + area.setWrapText(true); + stage.setWidth(120); + area.replaceText(String.join("\n", lines)); + }); + assertEquals(5, area.lastVisibleParToAllParIndex()); + } + @Test + public void get_specific_visible_paragraph_index_with_some_blank_lines() { + String[] lines = { + "abc", + "", + "", + "", + "def", + "" + }; + interact(() -> { + area.setWrapText(true); + stage.setWidth(120); + area.replaceText(String.join("\n", lines)); + }); + assertEquals(3, area.visibleParToAllParIndex(3)); + } +} \ No newline at end of file diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java index e8cb35dc9..53a2ec4be 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.ListIterator; import org.reactfx.EventSource; import org.reactfx.EventStream; @@ -19,6 +20,8 @@ import org.reactfx.collection.QuasiListModification; import org.reactfx.collection.SuspendableList; import org.reactfx.collection.UnmodifiableByDefaultLiveList; +import org.reactfx.util.Tuple2; +import org.reactfx.util.Tuples; import org.reactfx.value.SuspendableVal; import org.reactfx.value.Val; @@ -43,7 +46,7 @@ protected Subscription observeInputs() { return parChangesList.subscribe(list -> { ListChangeAccumulator> accumulator = new ListChangeAccumulator<>(); for (MaterializedListModification> mod : list) { - mod = mod.trim(); + mod = trim(mod); // add the quasiListModification itself, not as a quasiListChange, in case some overlap accumulator.add(QuasiListModification.create(mod.getFrom(), mod.getRemoved(), mod.getAddedSize())); @@ -217,4 +220,73 @@ private void updateMulti( parChangesList.push(parChanges); }); } + + /** + * Copy of org.reactfx.collection.MaterializedListModification.trim() + * that uses reference comparison instead of equals(). + */ + private MaterializedListModification> trim(MaterializedListModification> mod) { + return commonPrefixSuffixLengths(mod.getRemoved(), mod.getAdded()).map((pref, suff) -> { + if(pref == 0 && suff == 0) { + return mod; + } else { + return MaterializedListModification.create( + mod.getFrom() + pref, + mod.getRemoved().subList(pref, mod.getRemovedSize() - suff), + mod.getAdded().subList(pref, mod.getAddedSize() - suff)); + } + }); + } + + /** + * Copy of org.reactfx.util.Lists.commonPrefixSuffixLengths() + * that uses reference comparison instead of equals(). + */ + private static Tuple2 commonPrefixSuffixLengths(List l1, List l2) { + int n1 = l1.size(); + int n2 = l2.size(); + + if(n1 == 0 || n2 == 0) { + return Tuples.t(0, 0); + } + + int pref = commonPrefixLength(l1, l2); + if(pref == n1 || pref == n2) { + return Tuples.t(pref, 0); + } + + int suff = commonSuffixLength(l1, l2); + + return Tuples.t(pref, suff); + } + + /** + * Copy of org.reactfx.util.Lists.commonPrefixLength() + * that uses reference comparison instead of equals(). + */ + private static int commonPrefixLength(List l, List m) { + ListIterator i = l.listIterator(); + ListIterator j = m.listIterator(); + while(i.hasNext() && j.hasNext()) { + if(i.next() != j.next()) { + return i.nextIndex() - 1; + } + } + return i.nextIndex(); + } + + /** + * Copy of org.reactfx.util.Lists.commonSuffixLength() + * that uses reference comparison instead of equals(). + */ + private static int commonSuffixLength(List l, List m) { + ListIterator i = l.listIterator(l.size()); + ListIterator j = m.listIterator(m.size()); + while(i.hasPrevious() && j.hasPrevious()) { + if(i.previous() != j.previous()) { + return l.size() - i.nextIndex() - 1; + } + } + return l.size() - i.nextIndex(); + } }