Skip to content

Commit

Permalink
Make plain text edits via converting to StyledDocument first.
Browse files Browse the repository at this point in the history
Fixes #183.
  • Loading branch information
TomasMikula committed Sep 29, 2015
1 parent 94100e2 commit 8d4d1c7
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 114 deletions.
153 changes: 48 additions & 105 deletions richtextfx/src/main/java/org/fxmisc/richtext/EditableStyledDocument.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
Expand All @@ -37,8 +33,6 @@
final class EditableStyledDocument<S>
extends StyledDocumentBase<S, ObservableList<Paragraph<S>>> {

private static final Pattern LINE_TERMINATOR = Pattern.compile("\r\n|\r|\n");

/* ********************************************************************** *
* *
* Observables *
Expand Down Expand Up @@ -99,10 +93,6 @@ public ReadOnlyStyledDocument<S> snapshot() {
// 2. push to textRemovalEnd,
// 3. push to insertedDocument;
// b)
// 1. push to textChangePosition,
// 2. push to textRemovalEnd,
// 3. push to insertionLength;
// c)
// 1. push to styleChangePosition
// 2. push to styleChangeEnd
// 3. push to styleChangeDone.
Expand All @@ -116,7 +106,6 @@ public ReadOnlyStyledDocument<S> snapshot() {
private final EventSource<String> insertedText = new EventSource<>();

private final EventSource<StyledDocument<S>> insertedDocument = new EventSource<>();
private final EventSource<Integer> insertionLength = new EventSource<>();
private final EventSource<Void> styleChangeDone = new EventSource<>();

private final EventStream<PlainTextChange> plainTextChanges;
Expand All @@ -130,9 +119,7 @@ public ReadOnlyStyledDocument<S> snapshot() {
EventStream<Integer> changePosition = EventStreams.merge(textChangePosition, styleChangePosition);
EventStream<Integer> removalEnd = EventStreams.merge(textRemovalEnd, styleChangeEnd);
EventStream<StyledDocument<S>> removedDocument = EventStreams.zip(changePosition, removalEnd).map(t2 -> t2.map((a, b) -> subSequence(a, b)));
EventStream<Integer> insertionEnd = EventStreams.merge(
changePosition.emitBothOnEach(insertionLength).map(t2 -> t2.map((start, len) -> start + len)),
styleChangeEnd.emitOn(styleChangeDone));
EventStream<Integer> insertionEnd = styleChangeEnd.emitOn(styleChangeDone);
EventStream<StyledDocument<S>> insertedDocument = EventStreams.merge(
this.insertedDocument,
changePosition.emitBothOnEach(insertionEnd).map(t2 -> t2.map((a, b) -> subSequence(a, b))));
Expand Down Expand Up @@ -187,28 +174,51 @@ public ReadOnlyStyledDocument<S> snapshot() {
* *
* ********************************************************************** */

public void replaceText(int start, int end, String replacement) {
ensureValidRange(start, end);
replace(start, end, replacement,
(repl, pos) -> stringToParagraphs(repl, getStyleForInsertionAt(pos)),
repl -> {
insertedText.push(repl);
insertionLength.push(repl.length());
});
public void replaceText(int start, int end, String text) {
StyledDocument<S> doc = ReadOnlyStyledDocument.fromString(
text, getStyleForInsertionAt(start));
replace(start, end, doc);
}

public void replace(int start, int end, StyledDocument<S> replacement) {
ensureValidRange(start, end);
replace(start, end, replacement,
(repl, pos) -> repl.getParagraphs(),
repl -> {
insertedText.push(repl.toString());
StyledDocument<S> doc =
repl instanceof ReadOnlyStyledDocument
? repl
: new ReadOnlyStyledDocument<>(repl.getParagraphs(), COPY);
insertedDocument.push(doc);
});

textChangePosition.push(start);
textRemovalEnd.push(end);

Position start2D = navigator.offsetToPosition(start, Forward);
Position end2D = start2D.offsetBy(end - start, Forward);
int firstParIdx = start2D.getMajor();
int firstParFrom = start2D.getMinor();
int lastParIdx = end2D.getMajor();
int lastParTo = end2D.getMinor();

// Get the leftovers after cutting out the deletion
Paragraph<S> firstPar = paragraphs.get(firstParIdx).trim(firstParFrom);
Paragraph<S> lastPar = paragraphs.get(lastParIdx).subSequence(lastParTo);

List<Paragraph<S>> replacementPars = replacement.getParagraphs();

List<Paragraph<S>> newPars = join(firstPar, replacementPars, lastPar);
setAll(firstParIdx, lastParIdx + 1, newPars);

// update length, invalidate text
int replacementLength =
replacementPars.stream().mapToInt(Paragraph::length).sum() +
replacementPars.size() - 1;
int newLength = length.getValue() - (end - start) + replacementLength;
length.suspendWhile(() -> { // don't publish length change until text is invalidated
length.setValue(newLength);
text.invalidate();
});

// complete the change events
insertedText.push(replacement.toString());
StyledDocument<S> doc =
replacement instanceof ReadOnlyStyledDocument
? replacement
: new ReadOnlyStyledDocument<>(replacement.getParagraphs(), COPY);
insertedDocument.push(doc);
}

public void setStyle(int from, int to, S style) {
Expand Down Expand Up @@ -351,30 +361,10 @@ public void setStyleSpans(int paragraph, int from, StyleSpans<? extends S> style

/* ********************************************************************** *
* *
* Private methods *
* Private and package private methods *
* *
* ********************************************************************** */

private static <S> List<Paragraph<S>> stringToParagraphs(String str, S style) {
Matcher m = LINE_TERMINATOR.matcher(str);

int n = 1;
while(m.find()) ++n;
List<Paragraph<S>> res = new ArrayList<>(n);

int start = 0;
m.reset();
while(m.find()) {
String s = str.substring(start, m.start());
res.add(new Paragraph<S>(s, style));
start = m.end();
}
String last = str.substring(start);
res.add(new Paragraph<>(last, style));

return res;
}

private void ensureValidRange(int start, int end) {
ensureValidRange(start, end, length());
}
Expand Down Expand Up @@ -432,57 +422,6 @@ private Guard beginStyleChange(int start, int end) {
return () -> styleChangeDone.push(null);
}

/**
* Generic implementation for text replacement.
* @param start
* @param end
* @param replacement
* @param replacementToParagraphs function to convert the replacement
* to paragraphs. In addition to replacement itself, it is also given
* the position at which the replacement is going to be inserted. This
* position can be used to determine the style of the resulting paragraphs.
* @param publishReplacement completes the change events. It has to push
* exactly one value {@link #insertedText} and exactly one value to exactly
* one of {@link #insertedDocument}, {@link #insertionLength}.
*/
private <D extends CharSequence> void replace(
int start, int end, D replacement,
BiFunction<D, Position, List<Paragraph<S>>> replacementToParagraphs,
Consumer<D> publishReplacement) {

textChangePosition.push(start);
textRemovalEnd.push(end);

Position start2D = navigator.offsetToPosition(start, Forward);
Position end2D = start2D.offsetBy(end - start, Forward);
int firstParIdx = start2D.getMajor();
int firstParFrom = start2D.getMinor();
int lastParIdx = end2D.getMajor();
int lastParTo = end2D.getMinor();

// Get the leftovers after cutting out the deletion
Paragraph<S> firstPar = paragraphs.get(firstParIdx).trim(firstParFrom);
Paragraph<S> lastPar = paragraphs.get(lastParIdx).subSequence(lastParTo);

List<Paragraph<S>> replacementPars = replacementToParagraphs.apply(replacement, start2D);

List<Paragraph<S>> newPars = join(firstPar, replacementPars, lastPar);
setAll(firstParIdx, lastParIdx + 1, newPars);

// update length, invalidate text
int replacementLength =
replacementPars.stream().mapToInt(Paragraph::length).sum() +
replacementPars.size() - 1;
int newLength = length.getValue() - (end - start) + replacementLength;
length.suspendWhile(() -> { // don't publish length change until text is invalidated
length.setValue(newLength);
text.invalidate();
});

// complete the change events
publishReplacement.accept(replacement);
}

private List<Paragraph<S>> join(Paragraph<S> first, List<Paragraph<S>> middle, Paragraph<S> last) {
int m = middle.size();
if(m == 0) {
Expand All @@ -509,7 +448,11 @@ private void setAll(int startIdx, int endIdx, Collection<Paragraph<S>> pars) {
}
}

private S getStyleForInsertionAt(Position insertionPos) {
S getStyleForInsertionAt(int pos) {
return getStyleForInsertionAt(navigator.offsetToPosition(pos, Forward));
}

S getStyleForInsertionAt(Position insertionPos) {
if(useInitialStyleForInsertion.get()) {
return initialStyle;
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
package org.fxmisc.richtext;

import static org.fxmisc.richtext.ReadOnlyStyledDocument.ParagraphsPolicy.*;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ReadOnlyStyledDocument<S> extends StyledDocumentBase<S, List<Paragraph<S>>> {

private static final Pattern LINE_TERMINATOR = Pattern.compile("\r\n|\r|\n");

public static <S> ReadOnlyStyledDocument<S> fromString(String str, S style) {
Matcher m = LINE_TERMINATOR.matcher(str);

int n = 1;
while(m.find()) ++n;
List<Paragraph<S>> res = new ArrayList<>(n);

int start = 0;
m.reset();
while(m.find()) {
String s = str.substring(start, m.start());
res.add(new Paragraph<S>(s, style));
start = m.end();
}
String last = str.substring(start);
res.add(new Paragraph<>(last, style));

return new ReadOnlyStyledDocument<>(res, ADOPT);
}

static enum ParagraphsPolicy {
ADOPT,
COPY,
Expand Down Expand Up @@ -84,6 +110,7 @@ public StyledText<S> decode(DataInputStream is) throws IOException {
};
}


private int length = -1;

private String text = null;
Expand Down
12 changes: 3 additions & 9 deletions richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java
Original file line number Diff line number Diff line change
Expand Up @@ -713,15 +713,9 @@ public void clearStyle(int paragraph, int from, int to) {

@Override
public void replaceText(int start, int end, String text) {
try(Guard g = omniSuspendable.suspend()) {
start = clamp(0, start, getLength());
end = clamp(0, end, getLength());

content.replaceText(start, end, text);

int newCaretPos = start + text.length();
selectRange(newCaretPos, newCaretPos);
}
StyledDocument<S> doc = ReadOnlyStyledDocument.fromString(
text, content.getStyleForInsertionAt(start));
replace(start, end, doc);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.fxmisc.richtext;

import static org.junit.Assert.*;
import javafx.embed.swing.JFXPanel;

import org.junit.Test;

public class StyledTextAreaTest {

@Test
public void testUndoWithWinNewlines() {
new JFXPanel(); // initialize JavaFX

String text1 = "abc\r\ndef";
String text2 = "A\r\nB\r\nC";
StyleClassedTextArea area = new StyleClassedTextArea();

area.replaceText(text1);
area.getUndoManager().forgetHistory();
area.insertText(0, text2);
assertEquals("A\nB\nCabc\ndef", area.getText());

area.undo();
assertEquals("abc\ndef", area.getText());
}

}

0 comments on commit 8d4d1c7

Please sign in to comment.