org.fxmisc.richtext.model.ReadOnlyStyledDocument Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of richtextfx Show documentation
Show all versions of richtextfx Show documentation
Rich-text area for JavaFX
package org.fxmisc.richtext.model;
import static org.reactfx.util.Either.*;
import static org.reactfx.util.Tuples.*;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.reactfx.collection.MaterializedListModification;
import org.reactfx.util.BiIndex;
import org.reactfx.util.Either;
import org.reactfx.util.FingerTree;
import org.reactfx.util.FingerTree.NonEmptyFingerTree;
import org.reactfx.util.ToSemigroup;
import org.reactfx.util.Tuple2;
import org.reactfx.util.Tuple3;
public final class ReadOnlyStyledDocument implements StyledDocument {
private static class Summary {
private final int paragraphCount;
private final int charCount;
public Summary(int paragraphCount, int charCount) {
assert paragraphCount > 0;
assert charCount >= 0;
this.paragraphCount = paragraphCount;
this.charCount = charCount;
}
public int length() {
return charCount + paragraphCount - 1;
}
}
private static ToSemigroup, Summary> summaryProvider() {
return new ToSemigroup, Summary>() {
@Override
public Summary apply(Paragraph p) {
return new Summary(1, p.length());
}
@Override
public Summary reduce(Summary left, Summary right) {
return new Summary(
left.paragraphCount + right.paragraphCount,
left.charCount + right.charCount);
}
};
}
private static final Pattern LINE_TERMINATOR = Pattern.compile("\r\n|\r|\n");
private static final BiFunction> NAVIGATE =
(s, i) -> i <= s.length() ? left(i) : right(i - (s.length() + 1));
public static ReadOnlyStyledDocument fromString(String str, PS paragraphStyle, S style) {
Matcher m = LINE_TERMINATOR.matcher(str);
int n = 1;
while(m.find()) ++n;
List> res = new ArrayList<>(n);
int start = 0;
m.reset();
while(m.find()) {
String s = str.substring(start, m.start());
res.add(new Paragraph<>(paragraphStyle, s, style));
start = m.end();
}
String last = str.substring(start);
res.add(new Paragraph<>(paragraphStyle, last, style));
return new ReadOnlyStyledDocument<>(res);
}
public static ReadOnlyStyledDocument from(StyledDocument doc) {
if(doc instanceof ReadOnlyStyledDocument) {
return (ReadOnlyStyledDocument) doc;
} else {
return new ReadOnlyStyledDocument<>(doc.getParagraphs());
}
}
public static Codec> codec(Codec pCodec, Codec tCodec) {
return new Codec>() {
private final Codec>> codec = Codec.listCodec(paragraphCodec(pCodec, tCodec));
@Override
public String getName() {
return "application/richtextfx-styled-document<" + tCodec.getName() + ";" + pCodec.getName() + ">";
}
@Override
public void encode(DataOutputStream os, StyledDocument doc) throws IOException {
codec.encode(os, doc.getParagraphs());
}
@Override
public StyledDocument decode(DataInputStream is) throws IOException {
return new ReadOnlyStyledDocument<>(codec.decode(is));
}
};
}
private static Codec> paragraphCodec(Codec pCodec, Codec tCodec) {
return new Codec>() {
private final Codec>> segmentsCodec = Codec.listCodec(styledTextCodec(tCodec));
@Override
public String getName() {
return "paragraph<" + tCodec.getName() + ";" + pCodec.getName() + ">";
}
@Override
public void encode(DataOutputStream os, Paragraph p) throws IOException {
pCodec.encode(os, p.getParagraphStyle());
segmentsCodec.encode(os, p.getSegments());
}
@Override
public Paragraph decode(DataInputStream is) throws IOException {
PS paragraphStyle = pCodec.decode(is);
List> segments = segmentsCodec.decode(is);
return new Paragraph<>(paragraphStyle, segments);
}
};
}
private static Codec> styledTextCodec(Codec styleCodec) {
return new Codec>() {
@Override
public String getName() {
return "styledtext<" + styleCodec.getName() + ">";
}
@Override
public void encode(DataOutputStream os, StyledText t) throws IOException {
STRING_CODEC.encode(os, t.getText());
styleCodec.encode(os, t.getStyle());
}
@Override
public StyledText decode(DataInputStream is) throws IOException {
String text = STRING_CODEC.decode(is);
S style = styleCodec.decode(is);
return new StyledText<>(text, style);
}
};
}
private final NonEmptyFingerTree, Summary> tree;
private String text = null;
private List> paragraphs = null;
private ReadOnlyStyledDocument(NonEmptyFingerTree, Summary> tree) {
this.tree = tree;
}
ReadOnlyStyledDocument(List> paragraphs) {
this.tree =
FingerTree.mkTree(paragraphs, summaryProvider()).caseEmpty().unify(
emptyTree -> { throw new AssertionError("Unreachable code"); },
neTree -> neTree);
}
@Override
public int length() {
return tree.getSummary().length();
}
@Override
public String getText() {
if(text == null) {
String[] strings = getParagraphs().stream()
.map(Paragraph::getText)
.toArray(n -> new String[n]);
text = String.join("\n", strings);
}
return text;
}
public int getParagraphCount() {
return tree.getLeafCount();
}
public Paragraph getParagraph(int index) {
return tree.getLeaf(index);
}
@Override
public List> getParagraphs() {
if(paragraphs == null) {
paragraphs = tree.asList();
}
return paragraphs;
}
@Override
public Position position(int major, int minor) {
return new Pos(major, minor);
}
@Override
public Position offsetToPosition(int offset, Bias bias) {
return position(0, 0).offsetBy(offset, bias);
}
public Tuple2, ReadOnlyStyledDocument> split(int position) {
return tree.locate(NAVIGATE, position).map(this::split);
}
public Tuple2, ReadOnlyStyledDocument> split(
int row, int col) {
return tree.splitAt(row).map((l, p, r) -> {
Paragraph p1 = p.trim(col);
Paragraph p2 = p.subSequence(col);
ReadOnlyStyledDocument doc1 = new ReadOnlyStyledDocument<>(l.append(p1));
ReadOnlyStyledDocument doc2 = new ReadOnlyStyledDocument<>(r.prepend(p2));
return t(doc1, doc2);
});
}
@Override
public ReadOnlyStyledDocument concat(StyledDocument other) {
return concat0(other, Paragraph::concat);
}
private ReadOnlyStyledDocument concatR(StyledDocument other) {
return concat0(other, Paragraph::concatR);
}
private ReadOnlyStyledDocument concat0(StyledDocument other, BinaryOperator> parConcat) {
int n = tree.getLeafCount() - 1;
Paragraph p0 = tree.getLeaf(n);
Paragraph p1 = other.getParagraphs().get(0);
Paragraph p = parConcat.apply(p0, p1);
NonEmptyFingerTree, Summary> tree1 = tree.updateLeaf(n, p);
FingerTree, Summary> tree2 = (other instanceof ReadOnlyStyledDocument)
? ((ReadOnlyStyledDocument) other).tree.split(1)._2
: FingerTree.mkTree(other.getParagraphs().subList(1, other.getParagraphs().size()), summaryProvider());
return new ReadOnlyStyledDocument<>(tree1.join(tree2));
}
@Override
public StyledDocument subSequence(int start, int end) {
return split(end)._1.split(start)._2;
}
public Tuple3, RichTextChange, MaterializedListModification>> replace(
int from, int to, ReadOnlyStyledDocument replacement) {
return replace(from, to, x -> replacement);
}
Tuple3, RichTextChange, MaterializedListModification>> replace(
int from, int to, UnaryOperator> f) {
BiIndex start = tree.locate(NAVIGATE, from);
BiIndex end = tree.locate(NAVIGATE, to);
return replace(start, end, f);
}
Tuple3, RichTextChange, MaterializedListModification>> replace(
BiIndex start, BiIndex end, UnaryOperator> f) {
int pos = tree.getSummaryBetween(0, start.major).map(s -> s.length() + 1).orElse(0) + start.minor;
List> removedPars =
getParagraphs().subList(start.major, end.major + 1);
return end.map(this::split).map((l0, r) -> {
return start.map(l0::split).map((l, removed) -> {
ReadOnlyStyledDocument replacement = f.apply(removed);
ReadOnlyStyledDocument doc = l.concatR(replacement).concat(r);
RichTextChange change = new RichTextChange<>(pos, removed, replacement);
List> addedPars = doc.getParagraphs().subList(start.major, start.major + replacement.getParagraphCount());
MaterializedListModification> parChange =
MaterializedListModification.create(start.major, removedPars, addedPars);
return t(doc, change, parChange);
});
});
}
Tuple3, RichTextChange, MaterializedListModification>> replaceParagraph(
int parIdx, UnaryOperator> f) {
return replace(
new BiIndex(parIdx, 0),
new BiIndex(parIdx, tree.getLeaf(parIdx).length()),
doc -> doc.mapParagraphs(f));
}
ReadOnlyStyledDocument mapParagraphs(UnaryOperator> f) {
int n = tree.getLeafCount();
List> pars = new ArrayList<>(n);
for(int i = 0; i < n; ++i) {
pars.add(f.apply(tree.getLeaf(i)));
}
return new ReadOnlyStyledDocument<>(pars);
}
@Override
public String toString() {
return getParagraphs()
.stream()
.map(Paragraph::toString)
.reduce((p1, p2) -> p1 + "\n" + p2)
.orElse("");
}
@Override
public final boolean equals(Object other) {
if(other instanceof StyledDocument) {
StyledDocument, ?> that = (StyledDocument, ?>) other;
return Objects.equals(this.getParagraphs(), that.getParagraphs());
} else {
return false;
}
}
@Override
public final int hashCode() {
return getParagraphs().hashCode();
}
private class Pos implements Position {
private final int major;
private final int minor;
private Pos(int major, int minor) {
this.major = major;
this.minor = minor;
}
@Override
public String toString() {
return "(" + major + ", " + minor + ")";
}
@Override
public boolean sameAs(Position other) {
return getTargetObject() == other.getTargetObject()
&& major == other.getMajor()
&& minor == other.getMinor();
}
@Override
public TwoDimensional getTargetObject() {
return ReadOnlyStyledDocument.this;
}
@Override
public int getMajor() {
return major;
}
@Override
public int getMinor() {
return minor;
}
@Override
public Position clamp() {
if(major == tree.getLeafCount() - 1) {
int elemLen = tree.getLeaf(major).length();
if(minor < elemLen) {
return this;
} else {
return new Pos(major, elemLen-1);
}
} else {
return this;
}
}
@Override
public Position offsetBy(int offset, Bias bias) {
return tree.locateProgressively(s -> s.charCount + s.paragraphCount, toOffset() + offset)
.map(Pos::new);
}
@Override
public int toOffset() {
if(major == 0) {
return minor;
} else {
return tree.getSummaryBetween(0, major).get().length() + 1 + minor;
}
}
}
}