Skip to content

Commit 6d49399

Browse files
JFormDesignerJugen
authored andcommitted
Paragraph list trim fix (#795)
* bug: getVisibleParagraph does not work with blank lines * trim paragraphs using reference instead of object comparison (issues #777 and #788)
1 parent fa72c42 commit 6d49399

2 files changed

Lines changed: 225 additions & 1 deletion

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package org.fxmisc.richtext.api;
2+
3+
import org.fxmisc.richtext.InlineCssTextAreaAppTest;
4+
import org.fxmisc.richtext.model.Paragraph;
5+
import org.junit.Test;
6+
import org.junit.Assert;
7+
import org.reactfx.collection.LiveList;
8+
9+
import static org.junit.Assert.assertEquals;
10+
import static org.junit.Assert.assertFalse;
11+
12+
public class VisibleParagraphTest extends InlineCssTextAreaAppTest {
13+
14+
@Test
15+
public void get_first_visible_paragraph_index_with_non_blank_lines() {
16+
String[] lines = {
17+
"abc",
18+
"def",
19+
"ghi"
20+
};
21+
interact(() -> {
22+
area.setWrapText(true);
23+
stage.setWidth(120);
24+
area.replaceText(String.join("\n", lines));
25+
});
26+
assertEquals(0, area.firstVisibleParToAllParIndex());
27+
assertEquals(2, area.lastVisibleParToAllParIndex());
28+
assertEquals(1, area.visibleParToAllParIndex(1));
29+
}
30+
@Test
31+
public void get_last_visible_paragraph_index_with_non_blank_lines() {
32+
String[] lines = {
33+
"abc",
34+
"def",
35+
"ghi"
36+
};
37+
interact(() -> {
38+
area.setWrapText(true);
39+
stage.setWidth(120);
40+
area.replaceText(String.join("\n", lines));
41+
});
42+
assertEquals(2, area.lastVisibleParToAllParIndex());
43+
}
44+
@Test
45+
public void get_specific_visible_paragraph_index_with_non_blank_lines() {
46+
String[] lines = {
47+
"abc",
48+
"def",
49+
"ghi"
50+
};
51+
interact(() -> {
52+
area.setWrapText(true);
53+
stage.setWidth(120);
54+
area.replaceText(String.join("\n", lines));
55+
});
56+
assertEquals(2, area.visibleParToAllParIndex(2));
57+
}
58+
@Test
59+
public void get_first_visible_paragraph_index_with_all_blank_lines() {
60+
String[] lines = {
61+
"",
62+
"",
63+
""
64+
};
65+
interact(() -> {
66+
area.setWrapText(true);
67+
stage.setWidth(120);
68+
area.replaceText(String.join("\n", lines));
69+
});
70+
assertEquals(0, area.firstVisibleParToAllParIndex());
71+
}
72+
@Test
73+
public void get_last_visible_paragraph_index_with_all_blank_lines() {
74+
String[] lines = {
75+
"",
76+
"",
77+
""
78+
};
79+
interact(() -> {
80+
area.setWrapText(true);
81+
stage.setWidth(120);
82+
area.replaceText(String.join("\n", lines));
83+
});
84+
assertEquals(2, area.lastVisibleParToAllParIndex());
85+
}
86+
@Test
87+
public void get_specific_visible_paragraph_index_with_all_blank_lines() {
88+
String[] lines = {
89+
"",
90+
"",
91+
""
92+
};
93+
interact(() -> {
94+
area.setWrapText(true);
95+
stage.setWidth(120);
96+
area.replaceText(String.join("\n", lines));
97+
});
98+
assertEquals(2, area.visibleParToAllParIndex(2));
99+
}
100+
101+
@Test
102+
public void get_first_visible_paragraph_index_with_some_blank_lines() {
103+
String[] lines = {
104+
"abc",
105+
"",
106+
"",
107+
"",
108+
"def",
109+
""
110+
};
111+
interact(() -> {
112+
area.setWrapText(true);
113+
stage.setWidth(120);
114+
area.replaceText(String.join("\n", lines));
115+
});
116+
assertEquals(0, area.firstVisibleParToAllParIndex());
117+
}
118+
@Test
119+
public void get_last_visible_paragraph_index_with_some_blank_lines() {
120+
String[] lines = {
121+
"abc",
122+
"",
123+
"",
124+
"",
125+
"def",
126+
""
127+
};
128+
interact(() -> {
129+
area.setWrapText(true);
130+
stage.setWidth(120);
131+
area.replaceText(String.join("\n", lines));
132+
});
133+
assertEquals(5, area.lastVisibleParToAllParIndex());
134+
}
135+
@Test
136+
public void get_specific_visible_paragraph_index_with_some_blank_lines() {
137+
String[] lines = {
138+
"abc",
139+
"",
140+
"",
141+
"",
142+
"def",
143+
""
144+
};
145+
interact(() -> {
146+
area.setWrapText(true);
147+
stage.setWidth(120);
148+
area.replaceText(String.join("\n", lines));
149+
});
150+
assertEquals(3, area.visibleParToAllParIndex(3));
151+
}
152+
}

richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.ArrayList;
66
import java.util.Collections;
77
import java.util.List;
8+
import java.util.ListIterator;
89

910
import org.reactfx.EventSource;
1011
import org.reactfx.EventStream;
@@ -19,6 +20,8 @@
1920
import org.reactfx.collection.QuasiListModification;
2021
import org.reactfx.collection.SuspendableList;
2122
import org.reactfx.collection.UnmodifiableByDefaultLiveList;
23+
import org.reactfx.util.Tuple2;
24+
import org.reactfx.util.Tuples;
2225
import org.reactfx.value.SuspendableVal;
2326
import org.reactfx.value.Val;
2427

@@ -43,7 +46,7 @@ protected Subscription observeInputs() {
4346
return parChangesList.subscribe(list -> {
4447
ListChangeAccumulator<Paragraph<PS, SEG, S>> accumulator = new ListChangeAccumulator<>();
4548
for (MaterializedListModification<Paragraph<PS, SEG, S>> mod : list) {
46-
mod = mod.trim();
49+
mod = trim(mod);
4750

4851
// add the quasiListModification itself, not as a quasiListChange, in case some overlap
4952
accumulator.add(QuasiListModification.create(mod.getFrom(), mod.getRemoved(), mod.getAddedSize()));
@@ -217,4 +220,73 @@ private void updateMulti(
217220
parChangesList.push(parChanges);
218221
});
219222
}
223+
224+
/**
225+
* Copy of org.reactfx.collection.MaterializedListModification.trim()
226+
* that uses reference comparison instead of equals().
227+
*/
228+
private MaterializedListModification<Paragraph<PS, SEG, S>> trim(MaterializedListModification<Paragraph<PS, SEG, S>> mod) {
229+
return commonPrefixSuffixLengths(mod.getRemoved(), mod.getAdded()).map((pref, suff) -> {
230+
if(pref == 0 && suff == 0) {
231+
return mod;
232+
} else {
233+
return MaterializedListModification.create(
234+
mod.getFrom() + pref,
235+
mod.getRemoved().subList(pref, mod.getRemovedSize() - suff),
236+
mod.getAdded().subList(pref, mod.getAddedSize() - suff));
237+
}
238+
});
239+
}
240+
241+
/**
242+
* Copy of org.reactfx.util.Lists.commonPrefixSuffixLengths()
243+
* that uses reference comparison instead of equals().
244+
*/
245+
private static Tuple2<Integer, Integer> commonPrefixSuffixLengths(List<?> l1, List<?> l2) {
246+
int n1 = l1.size();
247+
int n2 = l2.size();
248+
249+
if(n1 == 0 || n2 == 0) {
250+
return Tuples.t(0, 0);
251+
}
252+
253+
int pref = commonPrefixLength(l1, l2);
254+
if(pref == n1 || pref == n2) {
255+
return Tuples.t(pref, 0);
256+
}
257+
258+
int suff = commonSuffixLength(l1, l2);
259+
260+
return Tuples.t(pref, suff);
261+
}
262+
263+
/**
264+
* Copy of org.reactfx.util.Lists.commonPrefixLength()
265+
* that uses reference comparison instead of equals().
266+
*/
267+
private static int commonPrefixLength(List<?> l, List<?> m) {
268+
ListIterator<?> i = l.listIterator();
269+
ListIterator<?> j = m.listIterator();
270+
while(i.hasNext() && j.hasNext()) {
271+
if(i.next() != j.next()) {
272+
return i.nextIndex() - 1;
273+
}
274+
}
275+
return i.nextIndex();
276+
}
277+
278+
/**
279+
* Copy of org.reactfx.util.Lists.commonSuffixLength()
280+
* that uses reference comparison instead of equals().
281+
*/
282+
private static int commonSuffixLength(List<?> l, List<?> m) {
283+
ListIterator<?> i = l.listIterator(l.size());
284+
ListIterator<?> j = m.listIterator(m.size());
285+
while(i.hasPrevious() && j.hasPrevious()) {
286+
if(i.previous() != j.previous()) {
287+
return l.size() - i.nextIndex() - 1;
288+
}
289+
}
290+
return l.size() - i.nextIndex();
291+
}
220292
}

0 commit comments

Comments
 (0)