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.Collections;
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.Lists;
import org.reactfx.util.ToSemigroup;
import org.reactfx.util.Tuple2;
import org.reactfx.util.Tuple3;
import org.reactfx.util.Tuples;
/**
* An immutable implementation of {@link StyledDocument} that does not allow editing. For a {@link StyledDocument}
* that can be edited, see {@link EditableStyledDocument}. To create one, use its static factory
* "from"-prefixed methods or {@link ReadOnlyStyledDocumentBuilder}.
*
* @param The type of the paragraph style.
* @param The type of the segments in the paragraph (e.g. {@link String}).
* @param The type of the style of individual segments.
*/
public final class ReadOnlyStyledDocument implements StyledDocument {
/**
* Private class used for calculating {@link TwoDimensional.Position}s within this document.
*/
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 method for quickly calculating the length of a portion (subdocument) of this document.
*/
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));
/**
* Creates a {@link ReadOnlyStyledDocument} from the given string.
*
* @param str the text to use to create the segments
* @param paragraphStyle the paragraph style to use for each paragraph in the returned document
* @param style the style to use for each segment in the document
* @param segmentOps the operations object that can create a segment froma given text
* @param The type of the paragraph style.
* @param The type of the segments in the paragraph (e.g. {@link String}).
* @param The type of the style of individual segments.
*/
public static ReadOnlyStyledDocument fromString(String str, PS paragraphStyle, S style, TextOps segmentOps) {
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, segmentOps, segmentOps.create(s), style));
start = m.end();
}
String last = str.substring(start);
res.add(new Paragraph<>(paragraphStyle, segmentOps, segmentOps.create(last), style));
return new ReadOnlyStyledDocument<>(res);
}
/**
* Creates a {@link ReadOnlyStyledDocument} from the given segment.
*
* @param segment the only segment in the only paragraph in the document
* @param paragraphStyle the paragraph style to use for each paragraph in the returned document
* @param style the style to use for each segment in the document
* @param segmentOps the operations object that can create a segment froma given text
* @param The type of the paragraph style.
* @param The type of the segments in the paragraph (e.g. {@link String}).
* @param The type of the style of individual segments.
*/
public static ReadOnlyStyledDocument fromSegment(SEG segment, PS paragraphStyle, S style, SegmentOps segmentOps) {
Paragraph content = new Paragraph(paragraphStyle, segmentOps, segment, style);
List> res = Collections.singletonList(content);
return new ReadOnlyStyledDocument<>(res);
}
/**
* Creates a {@link ReadOnlyStyledDocument} from the given {@link StyledDocument}.
*
* @param The type of the paragraph style.
* @param The type of the segments in the paragraph (e.g. {@link String}).
* @param The type of the style of individual segments.
*/
public static ReadOnlyStyledDocument from(StyledDocument doc) {
if(doc instanceof ReadOnlyStyledDocument) {
return (ReadOnlyStyledDocument) doc;
} else {
return new ReadOnlyStyledDocument<>(doc.getParagraphs());
}
}
/**
* Defines a codec for serializing a {@link ReadOnlyStyledDocument}.
*
* @param pCodec the codec for serializing a {@link Paragraph}
* @param segCodec the codec for serializing a {@link StyledSegment}
* @param segmentOps the operations object for operating on segments
*
* @param The type of the paragraph style.
* @param The type of the segments in the paragraph (e.g. {@link String}).
* @param The type of the style of individual segments.
*/
public static Codec> codec(Codec pCodec, Codec> segCodec,
SegmentOps segmentOps) {
return new Codec>() {
private final Codec>> codec = Codec.listCodec(
paragraphCodec(pCodec, segCodec, segmentOps)
);
@Override
public String getName() {
return "application/richtextfx-styled-document<" + pCodec.getName() + ";" + segCodec.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> segCodec,
SegmentOps segmentOps) {
return new Codec>() {
private final Codec>> segmentsCodec = Codec.listCodec(segCodec);
@Override
public String getName() {
return "paragraph<" + pCodec.getName() + ";" + segCodec.getName() + ">";
}
@Override
public void encode(DataOutputStream os, Paragraph p) throws IOException {
pCodec.encode(os, p.getParagraphStyle());
segmentsCodec.encode(os, p.getStyledSegments());
}
@Override
public Paragraph decode(DataInputStream is) throws IOException {
PS paragraphStyle = pCodec.decode(is);
List> segments = segmentsCodec.decode(is);
return new Paragraph<>(paragraphStyle, segmentOps, segments);
}
};
}
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);
}
/**
* Splits this document into two at the given position and returns both halves.
*/
public Tuple2, ReadOnlyStyledDocument> split(int position) {
return tree.locate(NAVIGATE, position).map(this::split);
}
/**
* Splits this document into two at the given paragraph's column position and returns both halves.
*/
public Tuple2, ReadOnlyStyledDocument> split(
int paragraphIndex, int columnPosition) {
return tree.splitAt(paragraphIndex).map((l, p, r) -> {
Paragraph p1 = p.trim(columnPosition);
Paragraph p2 = p.subSequence(columnPosition);
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;
}
/**
* Replaces multiple portions of this document in an efficient manner and returns
*
* -
* the updated version of this document that includes all of the replacements,
*
* -
* the List of {@link RichTextChange} that represent all the changes from this document
* to the returned one, and
*
* -
* the List of modifications used to update an area's list of paragraphs for each change.
*
*
*/
public Tuple3<
ReadOnlyStyledDocument,
List>,
List>>> replaceMulti(List> replacements) {
ReadOnlyStyledDocument updatedDoc = this;
List> richChangeList = new ArrayList<>(replacements.size());
List>> parChangeList = new ArrayList<>(replacements.size());
for (Replacement r : replacements) {
Tuple3<
ReadOnlyStyledDocument,
RichTextChange,
MaterializedListModification>
> postReplacement = updatedDoc.replace(r);
updatedDoc = postReplacement.get1();
richChangeList.add(postReplacement.get2());
parChangeList.add(postReplacement.get3());
}
return Tuples.t(updatedDoc, richChangeList, parChangeList);
}
/**
* Convenience method for calling {@link #replace(int, int, ReadOnlyStyledDocument)} with a {@link Replacement}
* argument.
*/
public Tuple3, RichTextChange, MaterializedListModification>> replace(
Replacement replacement) {
return replace(replacement.getStart(), replacement.getEnd(), replacement.getDocument());
}
/**
* Replaces the given portion {@code "from..to"} with the given replacement and returns
*
* -
* the updated version of this document that includes the replacement,
*
* -
* the {@link RichTextChange} that represents the change from this document to the returned one, and
*
* -
* the modification used to update an area's list of paragraphs.
*
*
*/
public Tuple3, RichTextChange, MaterializedListModification>> replace(
int from, int to, ReadOnlyStyledDocument replacement) {
return replace(from, to, x -> replacement);
}
/**
* Replaces the given portion {@code "from..to"} in the document by getting that portion of this document,
* passing it into the mapping function, and using the result as the replacement. Returns
*
* -
* the updated version of this document that includes the replacement,
*
* -
* the {@link RichTextChange} that represents the change from this document to the returned one, and
*
* -
* the modification used to update an area's list of paragraphs.
*
*
*/
public Tuple3, RichTextChange, MaterializedListModification>> replace(
int from, int to, UnaryOperator> mapper) {
ensureValidRange(from, to);
BiIndex start = tree.locate(NAVIGATE, from);
BiIndex end = tree.locate(NAVIGATE, to);
return replace(start, end, mapper);
}
public Tuple3, RichTextChange, MaterializedListModification>> replace(
int paragraphIndex, int fromCol, int toCol, UnaryOperator> f) {
ensureValidParagraphRange(paragraphIndex, fromCol, toCol);
return replace(new BiIndex(paragraphIndex, fromCol), new BiIndex(paragraphIndex, toCol), f);
}
// Note: there must be a "ensureValid_()" call preceding the call of this method
private 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);
});
});
}
/**
* Maps the paragraph at the given index by calling {@link #replace(int, int, UnaryOperator)}. Returns
*
* -
* the updated version of this document that includes the replacement,
*
* -
* the {@link RichTextChange} that represents the change from this document to the returned one, and
*
* -
* the modification used to update an area's list of paragraphs.
*
*
*/
public Tuple3, RichTextChange, MaterializedListModification>> replaceParagraph(
int parIdx, UnaryOperator> mapper) {
ensureValidParagraphIndex(parIdx);
return replace(
new BiIndex(parIdx, 0),
new BiIndex(parIdx, tree.getLeaf(parIdx).length()),
doc -> doc.mapParagraphs(mapper));
}
/**
* Maps all of this document's paragraphs using the given mapper and returns them in a new
* {@link ReadOnlyStyledDocument}.
*/
public ReadOnlyStyledDocument mapParagraphs(UnaryOperator> mapper) {
int n = tree.getLeafCount();
List> pars = new ArrayList<>(n);
for(int i = 0; i < n; ++i) {
pars.add(mapper.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 amount, Bias bias) {
return tree.locateProgressively(s -> s.charCount + s.paragraphCount, toOffset() + amount)
.map(Pos::new);
}
@Override
public int toOffset() {
if(major == 0) {
return minor;
} else {
return tree.getSummaryBetween(0, major).get().length() + 1 + minor;
}
}
}
private void ensureValidParagraphIndex(int parIdx) {
Lists.checkIndex(parIdx, getParagraphCount());
}
private void ensureValidRange(int start, int end) {
Lists.checkRange(start, end, length());
}
private void ensureValidParagraphRange(int par, int start, int end) {
ensureValidParagraphIndex(par);
Lists.checkRange(start, end, fullLength(par));
}
private int fullLength(int par) {
int n = getParagraphCount();
return getParagraph(par).length() + (par == n-1 ? 0 : 1);
}
}