From 5cc7802b9c38f7aa2319798507a73c859137987d Mon Sep 17 00:00:00 2001
From: basix86 <17149962+basix86@users.noreply.github.com>
Date: Fri, 11 Nov 2022 10:43:12 +0100
Subject: [PATCH 1/5] LazyLoadDocument
---
.../ui/rsyntaxtextarea/RSyntaxDocument.java | 12 ++
.../ui/rsyntaxtextarea/RSyntaxTextArea.java | 2 +-
.../ui/rsyntaxtextarea/TextEditorPane.java | 107 ++++++----
.../java/org/fife/ui/rtextarea/RContent.java | 7 +
.../java/org/fife/ui/rtextarea/RDocument.java | 31 +--
.../org/fife/ui/rtextarea/RGapContent.java | 24 +++
.../readonly/FileOffsetLineDiscover.java | 79 ++++++++
.../readonly/NullReadOnlyContent.java | 65 +++++++
.../rtextarea/readonly/ReadOnlyContent.java | 182 ++++++++++++++++++
.../readonly/ReadOnlyContentInterface.java | 17 ++
.../rtextarea/readonly/ReadOnlyDocument.java | 32 +++
.../readonly/ReadOnlyLeafElement.java | 73 +++++++
.../readonly/ReadOnlyRootElement.java | 73 +++++++
.../readonly/FileBuilderTestUtil.java | 31 +++
.../readonly/FileOffsetLineDiscoverTest.java | 57 ++++++
.../readonly/ReadOnlyContentTest.java | 99 ++++++++++
.../ReadOnlyContentWithEmptyFileTest.java | 84 ++++++++
.../readonly/ReadOnlyDocumentLauncher.java | 127 ++++++++++++
.../readonly/ReadOnlyDocumentTest.java | 79 ++++++++
19 files changed, 1113 insertions(+), 68 deletions(-)
create mode 100644 RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/RContent.java
create mode 100644 RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/RGapContent.java
create mode 100644 RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscover.java
create mode 100644 RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/NullReadOnlyContent.java
create mode 100644 RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContent.java
create mode 100644 RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentInterface.java
create mode 100644 RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocument.java
create mode 100644 RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyLeafElement.java
create mode 100644 RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyRootElement.java
create mode 100644 RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileBuilderTestUtil.java
create mode 100644 RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscoverTest.java
create mode 100644 RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentTest.java
create mode 100644 RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentWithEmptyFileTest.java
create mode 100644 RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocumentLauncher.java
create mode 100644 RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocumentTest.java
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxDocument.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxDocument.java
index a4f86b275..80b07ceff 100755
--- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxDocument.java
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxDocument.java
@@ -22,6 +22,7 @@
import org.fife.ui.rsyntaxtextarea.modes.AbstractMarkupTokenMaker;
import org.fife.ui.rtextarea.RDocument;
+import org.fife.ui.rtextarea.RGapContent;
import org.fife.util.DynamicIntArray;
@@ -110,6 +111,11 @@ public RSyntaxDocument(String syntaxStyle) {
* @param syntaxStyle The syntax highlighting scheme to use.
*/
public RSyntaxDocument(TokenMakerFactory tmf, String syntaxStyle) {
+ this(tmf, syntaxStyle, new RGapContent());
+ }
+
+ public RSyntaxDocument(TokenMakerFactory tmf, String syntaxStyle, Content content) {
+ super(content);
putProperty(tabSizeAttribute, 5);
lastTokensOnLines = new DynamicIntArray(400);
lastTokensOnLines.add(Token.NULL); // Initial (empty) line.
@@ -531,6 +537,9 @@ private void setSharedSegment(int line) {
}
+ public TokenMakerFactory getTokenMakerFactory() {
+ return tokenMakerFactory;
+ }
/**
* Sets the syntax style being used for syntax highlighting in this
@@ -680,4 +689,7 @@ private void updateSyntaxHighlightingInformation() {
}
+ public boolean isEditable() {
+ return true;
+ }
}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxTextArea.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxTextArea.java
index bb00e15a2..2b3ac3ae4 100755
--- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxTextArea.java
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxTextArea.java
@@ -680,7 +680,7 @@ public void copyAsStyledText() {
* @return The document.
*/
@Override
- protected Document createDefaultModel() {
+ protected RSyntaxDocument createDefaultModel() {
return new RSyntaxDocument(SYNTAX_STYLE_NONE);
}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TextEditorPane.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TextEditorPane.java
index 1b7bd97f3..f8e61d320 100755
--- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TextEditorPane.java
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/TextEditorPane.java
@@ -9,21 +9,19 @@
*/
package org.fife.ui.rsyntaxtextarea;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.charset.Charset;
-import java.nio.charset.UnsupportedCharsetException;
+import org.fife.io.UnicodeReader;
+import org.fife.io.UnicodeWriter;
+import org.fife.ui.rtextarea.RTextAreaEditorKit;
+import org.fife.ui.rtextarea.readonly.ReadOnlyContent;
+import org.fife.ui.rtextarea.readonly.ReadOnlyDocument;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
+import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
-
-import org.fife.io.UnicodeReader;
-import org.fife.io.UnicodeWriter;
-import org.fife.ui.rtextarea.RTextAreaEditorKit;
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
/**
* An extension of {@link org.fife.ui.rsyntaxtextarea.RSyntaxTextArea}
@@ -456,18 +454,17 @@ public void load(FileLocation loc) throws IOException {
* @see #save()
* @see #saveAs(FileLocation)
*/
- public void load(FileLocation loc, Charset defaultEnc) throws IOException {
- load(loc, defaultEnc == null ? null : defaultEnc.name());
+ public void load(FileLocation loc, String defaultEnc) throws IOException {
+ load(loc, defaultEnc == null ? Charset.defaultCharset() : Charset.forName(defaultEnc));
}
-
/**
* Loads the specified file in this editor. This method fires a property
* change event of type {@link #FULL_PATH_PROPERTY}.
*
* @param loc The location of the file to load. This cannot be
* null.
- * @param defaultEnc The encoding to use when loading/saving the file.
+ * @param defaultCharset The encoding to use when loading/saving the file.
* This encoding will only be used if the file is not Unicode.
* If this value is null, the system default encoding
* is used.
@@ -477,41 +474,69 @@ public void load(FileLocation loc, Charset defaultEnc) throws IOException {
* @see #save()
* @see #saveAs(FileLocation)
*/
- public void load(FileLocation loc, String defaultEnc) throws IOException {
+ public void load(FileLocation loc, Charset defaultCharset) throws IOException {
+ Charset fileLocationCharset = getFileLocationCharset(loc, defaultCharset);
+ RSyntaxDocument doc = createEditableDocument(loc, fileLocationCharset);
+ loadDocument(loc, doc, defaultCharset);
+ }
- // For new local files, just go with it.
- if (loc.isLocal() && !loc.isLocalAndExists()) {
- this.charSet = defaultEnc!=null ? defaultEnc : getDefaultEncoding();
- this.loc = loc;
- setText(null);
- discardAllEdits();
- setDirty(false);
- return;
+ public void loadLocalFileInReadOnlyDocument(File file, Charset defaultCharset) throws IOException {
+ FileLocation fileLocation = FileLocation.create(file);
+ Charset fileLocationCharset = getFileLocationCharset(fileLocation, defaultCharset);
+ RSyntaxDocument doc = createLazyLoadDocument(fileLocation, fileLocationCharset);
+ loadDocument(fileLocation, doc, defaultCharset);
+ }
+
+ private Charset getFileLocationCharset(FileLocation fileLocation, Charset defaultCharset) throws IOException {
+ try( InputStream is = fileLocation.getInputStream() ){
+ UnicodeReader ur = new UnicodeReader(is, defaultCharset);
+ defaultCharset = Charset.forName(ur.getEncoding());
}
+ return defaultCharset;
+ }
- // Old local files and remote files, load 'em up. UnicodeReader will
- // check for BOMs and handle them correctly in all cases, then pass
- // rest of stream down to InputStreamReader.
- UnicodeReader ur = new UnicodeReader(loc.getInputStream(), defaultEnc);
+ private RSyntaxDocument createEditableDocument(FileLocation loc, Charset charset) throws IOException {
+ RSyntaxDocument doc = createDefaultModel();
+ // For new local files, just go with it.
+ if( !loc.isLocal() || loc.isLocalAndExists() ){
+ try( InputStreamReader r = new InputStreamReader(loc.getInputStream(), charset) ){
+ RTextAreaEditorKit kit = (RTextAreaEditorKit) getUI().getEditorKit(this);
+ try{
+ // NOTE: Resets the "line separator" property.
+ kit.read(r, doc, 0);
+ } catch( BadLocationException e ){
+ throw new IOException(e.getMessage());
+ }
+ }
+ }
+ return doc;
+ }
- // Remove listener so dirty flag doesn't get set when loading a file.
- Document doc = getDocument();
- doc.removeDocumentListener(this);
- try (BufferedReader r = new BufferedReader(ur)) {
- read(r, null);
- } finally {
- doc.addDocumentListener(this);
+ private RSyntaxDocument createLazyLoadDocument(FileLocation fileLocation, Charset charSet) throws IOException {
+ if( !fileLocation.isLocalAndExists() ){
+ throw new FileNotFoundException("ReadOnlyDucument supports only files from local file system");
}
+ return new ReadOnlyDocument(getTokenMarkerFactory(), getSyntaxEditingStyle(), new ReadOnlyContent(new File(fileLocation.getFileFullPath()), charSet));
+ }
- // No IOException thrown, so we can finally change the location.
- charSet = ur.getEncoding();
- String old = getFileFullPath();
- this.loc = loc;
+ public void loadDocument(FileLocation fileLocation, RSyntaxDocument doc, Charset charSet) {
+ this.charSet = charSet.name();
+ String oldFileFullPath = getFileFullPath();
+ setDocument(doc);
+ this.loc = fileLocation;
setDirty(false);
setCaretPosition(0);
discardAllEdits();
- firePropertyChange(FULL_PATH_PROPERTY, old, getFileFullPath());
+ firePropertyChange(FULL_PATH_PROPERTY, oldFileFullPath, getFileFullPath());
+ setReadOnly(isReadOnly() && doc.isEditable());
+ }
+ public TokenMakerFactory getTokenMarkerFactory() {
+ Document document = getDocument();
+ if( document instanceof RSyntaxDocument ){
+ return ((RSyntaxDocument) document).getTokenMakerFactory();
+ }
+ return null;
}
@@ -701,7 +726,7 @@ public void setLineSeparator(String separator) {
/**
* Sets the line separator sequence to use when this file is saved (e.g.
* "\n", "\r\n" or "\r").
- *
+ *
* Besides parameter checking, this method is preferred over
* getDocument().putProperty() because can set the editor's
* dirty flag when the line separator is changed.
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/RContent.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/RContent.java
new file mode 100644
index 000000000..d49012035
--- /dev/null
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/RContent.java
@@ -0,0 +1,7 @@
+package org.fife.ui.rtextarea;
+
+import javax.swing.text.BadLocationException;
+
+public interface RContent {
+ char charAt(int offset) throws BadLocationException;
+}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/RDocument.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/RDocument.java
index 4328935e3..bf69dd3f8 100755
--- a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/RDocument.java
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/RDocument.java
@@ -9,7 +9,6 @@
package org.fife.ui.rtextarea;
import javax.swing.text.BadLocationException;
-import javax.swing.text.GapContent;
import javax.swing.text.PlainDocument;
@@ -21,14 +20,16 @@
*/
public class RDocument extends PlainDocument {
-
/**
* Constructor.
*/
public RDocument() {
- super(new RGapContent());
+ this(new RGapContent());
}
+ public RDocument(Content content) {
+ super(content);
+ }
/**
* Returns the character in the document at the specified offset.
@@ -38,28 +39,6 @@ public RDocument() {
* @throws BadLocationException If the offset is invalid.
*/
public char charAt(int offset) throws BadLocationException {
- return ((RGapContent)getContent()).charAt(offset);
+ return ((RContent) getContent()).charAt(offset);
}
-
-
- /**
- * Document content that provides fast access to individual characters.
- */
- private static class RGapContent extends GapContent {
-
- public char charAt(int offset) throws BadLocationException {
- if (offset<0 || offset>=length()) {
- throw new BadLocationException("Invalid offset", offset);
- }
- int g0 = getGapStart();
- char[] array = (char[]) getArray();
- if (offset= length() ){
+ throw new BadLocationException("Invalid offset", offset);
+ }
+ int g0 = getGapStart();
+ char[] array = (char[]) getArray();
+ if( offset < g0 ){ // below gap
+ return array[offset];
+ }
+ return array[getGapEnd() + offset - g0]; // above gap
+ }
+
+}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscover.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscover.java
new file mode 100644
index 000000000..2ea2f6bb8
--- /dev/null
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscover.java
@@ -0,0 +1,79 @@
+package org.fife.ui.rtextarea.readonly;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+class FileOffsetLineDiscover {
+
+ private static final Logger LOGGER = Logger.getLogger(FileOffsetLineDiscover.class.getName());
+
+ private FileOffsetLineDiscover() {
+ }
+
+ public static List getOffsets(Path filePath, Charset charset) {
+
+ List lfOffsets = new ArrayList<>();
+ Set crOffsets = new HashSet<>();
+
+ List lineOffsets = new ArrayList<>();
+
+ lineOffsets.add(0);
+
+ readFile(filePath, charset, lfOffsets, crOffsets);
+
+ for( Integer newLineIndex : lfOffsets ){
+
+ //reconize \n\r LFCR
+ if( crOffsets.contains(newLineIndex + 1) ){
+ int lfcrIndex = newLineIndex + 1;
+ lineOffsets.add(lfcrIndex);
+ crOffsets.remove(lfcrIndex);
+ }
+
+ //recognize \r\n CRLF
+ else if( crOffsets.contains(newLineIndex - 1) ){
+ int crlfOffset = newLineIndex - 1;
+ lineOffsets.add(newLineIndex);
+ crOffsets.remove(crlfOffset);
+ }
+
+ //add LF
+ else lineOffsets.add(newLineIndex);
+ }
+
+ lineOffsets.addAll(crOffsets);
+ lineOffsets.sort(Integer::compare);
+ return lineOffsets;
+ }
+
+
+ private static void readFile(Path filePath, Charset fixedSizeCharset, Collection newLines, Collection carrigeRetrun) {
+ try( BufferedReader bufferedReader = Files.newBufferedReader(filePath, fixedSizeCharset); ){
+ int offset = 0;
+ char character;
+ int readValue;
+ while( true ){
+ readValue = bufferedReader.read();
+ if( readValue == -1 ) return;
+ offset++;
+ character = (char) readValue;
+ if( character == '\n' ){
+ newLines.add(offset);
+ } else if( character == '\r' ){
+ carrigeRetrun.add(offset);
+ }
+
+
+ }
+ } catch( IOException e ){
+ LOGGER.log(Level.WARNING, "Unable to read line offsets from file", e);
+ }
+ }
+}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/NullReadOnlyContent.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/NullReadOnlyContent.java
new file mode 100644
index 000000000..879f8e671
--- /dev/null
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/NullReadOnlyContent.java
@@ -0,0 +1,65 @@
+package org.fife.ui.rtextarea.readonly;
+
+import org.fife.ui.rtextarea.readonly.ReadOnlyContentInterface;
+
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Position;
+import javax.swing.text.Segment;
+import javax.swing.undo.UndoableEdit;
+
+public class NullReadOnlyContent implements ReadOnlyContentInterface {
+ @Override
+ public char charAt(int offset) {
+ return 0;
+ }
+
+ @Override
+ public int getStartOffset(int row) {
+ return 0;
+ }
+
+ @Override
+ public int getEndOffset(int index) {
+ return 0;
+ }
+
+ @Override
+ public int getElementCount() {
+ return 0;
+ }
+
+ @Override
+ public int getElementIndex(int offset) {
+ return 0;
+ }
+
+ @Override
+ public Position createPosition(int offset) {
+ return null;
+ }
+
+ @Override
+ public int length() {
+ return 0;
+ }
+
+ @Override
+ public UndoableEdit insertString(int where, String str) throws BadLocationException {
+ return null;
+ }
+
+ @Override
+ public UndoableEdit remove(int where, int nitems) throws BadLocationException {
+ return null;
+ }
+
+ @Override
+ public String getString(int where, int len) throws BadLocationException {
+ return "";
+ }
+
+ @Override
+ public void getChars(int where, int len, Segment txt) throws BadLocationException {
+
+ }
+}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContent.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContent.java
new file mode 100644
index 000000000..16294dbc9
--- /dev/null
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContent.java
@@ -0,0 +1,182 @@
+package org.fife.ui.rtextarea.readonly;
+
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Position;
+import javax.swing.text.Segment;
+import javax.swing.undo.UndoableEdit;
+import java.io.BufferedReader;
+import java.io.File;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class ReadOnlyContent implements ReadOnlyContentInterface {
+
+ private static final Logger LOGGER = Logger.getLogger(ReadOnlyContent.class.getName());
+
+ private static final int MAX_BUFFER_SIZE = 1024 * 1024; //1 MB
+ private static final int NOT_INITIALIZED_BUFFER_OFFSET = -1;
+
+ private final File file;
+ private final Charset charset;
+ private final List offsets;
+ private final int bufferSize;
+ private final int fileSize;
+ private final char[] buffer;
+
+ private int charsCount = -1;
+
+ private int startBufferOffset = NOT_INITIALIZED_BUFFER_OFFSET;
+
+
+ public ReadOnlyContent(File file, Charset charset) {
+ this.file = file;
+ this.charset = charset;
+ fileSize = (int) file.length();
+ bufferSize = Math.min(fileSize, MAX_BUFFER_SIZE);
+ buffer = new char[bufferSize];
+ offsets = FileOffsetLineDiscover.getOffsets(file.toPath(), charset);
+ }
+
+
+ @Override
+ public Position createPosition(int offset) {
+ return () -> offset;
+ }
+
+ @Override
+ public int length() {
+ if( charsCount == -1 ){
+ charsCount = getCharsCount();
+ }
+ return charsCount;
+ }
+
+ @Override
+ public UndoableEdit insertString(int where, String str) throws BadLocationException {
+ throw new BadLocationException("Unable to insert String. This content is not editable", where);
+ }
+
+ @Override
+ public UndoableEdit remove(int where, int nitems) throws BadLocationException {
+ throw new BadLocationException("Unable to remove String. This content is not editable", where);
+ }
+
+ @Override
+ public String getString(int charsOffset, int amountCharsToRead) throws BadLocationException {
+ int endReadOffset = charsOffset + amountCharsToRead;
+
+ if( endReadOffset >= length() ){
+ throw new BadLocationException("Unable to get String. The requested end offset exceed the document length: ", charsOffset + amountCharsToRead);
+ }
+
+ int endBuffedOffset = buffer.length + startBufferOffset;
+
+ if( startBufferOffset == NOT_INITIALIZED_BUFFER_OFFSET || charsOffset < startBufferOffset || endReadOffset > endBuffedOffset ){
+ reloadBuffer(charsOffset);
+ }
+ int relativeIndex = Math.min(charsOffset - startBufferOffset, bufferSize - amountCharsToRead);
+ try{
+ return new String(buffer, relativeIndex, amountCharsToRead);
+ } catch( Exception e ){
+ throw new ReadOnlyBadLocationException("Unable to read String", charsOffset, e);
+ }
+ }
+
+
+ private void reloadBuffer(int startReadOffset) throws BadLocationException {
+ if( fileSize < startReadOffset + bufferSize ){
+ startReadOffset = Math.max(0, fileSize - bufferSize);
+ }
+ this.startBufferOffset = startReadOffset;
+
+ read(startReadOffset, bufferSize);
+ }
+
+ private void read(int startReadOffset, int charsToRead) throws BadLocationException {
+ try( BufferedReader bufferedReader = Files.newBufferedReader(file.toPath(), charset); ){
+ bufferedReader.skip(startReadOffset);
+ int foundCharsCount = bufferedReader.read(buffer, 0, charsToRead);
+
+ if( charsToRead != foundCharsCount ){
+ throw new BadLocationException("Not all required chars are available", startReadOffset);
+ }
+ } catch( Exception e ){
+ throw new ReadOnlyBadLocationException("Unable to read file", startReadOffset, e);
+ }
+ }
+
+ @Override
+ public void getChars(int where, int len, Segment chars) throws BadLocationException {
+ String content = getString(where, len);
+
+ chars.array = content.toCharArray();
+ chars.offset = 0;
+ chars.count = len;
+ }
+
+ @Override
+ public char charAt(int offset) {
+ try{
+ return getString(offset, 1).charAt(0);
+ } catch( BadLocationException e ){
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public int getStartOffset(int row) {
+ return offsets.get(row);
+ }
+
+ @Override
+ public int getEndOffset(int index) {
+ if( index < offsets.size() - 1 )
+ return offsets.get(index + 1);
+ else return offsets.get(offsets.size() - 1) + 1;
+ }
+
+ @Override
+ public int getElementCount() {
+ return offsets.size();
+ }
+
+ @Override
+ public int getElementIndex(int offset) {
+ if (offsets.isEmpty()) return 0;
+
+ for( int i = 0; i < offsets.size(); i++ ){
+ if (offset < offsets.get(i+1)) {
+ return i;
+ }
+ }
+ return 0;
+
+ }
+
+ private int getCharsCount() {
+ try( BufferedReader bufferedReader = Files.newBufferedReader(file.toPath(), charset); ){
+ char[] charBuffer = new char[1024 * 1024];
+ int count = 2;
+ int readChars = 0;
+ while( readChars >= 0 ){
+ readChars = bufferedReader.read(charBuffer);
+ count += readChars;
+ }
+ return count;
+ } catch( Exception e ){
+ LOGGER.log(Level.WARNING, "Unable to read chars count", e);
+ return 0;
+ }
+ }
+
+ private static class ReadOnlyBadLocationException extends BadLocationException {
+
+ public ReadOnlyBadLocationException(String s, int offs, Exception cause) {
+ super(s, offs);
+ setStackTrace(cause.getStackTrace());
+ }
+ }
+}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentInterface.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentInterface.java
new file mode 100644
index 000000000..06f2a1a75
--- /dev/null
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentInterface.java
@@ -0,0 +1,17 @@
+package org.fife.ui.rtextarea.readonly;
+
+import org.fife.ui.rtextarea.RContent;
+
+import javax.swing.text.AbstractDocument;
+
+public interface ReadOnlyContentInterface extends AbstractDocument.Content, RContent {
+ char charAt(int offset);
+
+ int getStartOffset(int row);
+
+ int getEndOffset(int index);
+
+ int getElementCount();
+
+ int getElementIndex(int offset);
+}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocument.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocument.java
new file mode 100644
index 000000000..6b13e3b9a
--- /dev/null
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocument.java
@@ -0,0 +1,32 @@
+package org.fife.ui.rtextarea.readonly;
+
+import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
+import org.fife.ui.rsyntaxtextarea.TokenMakerFactory;
+import org.fife.ui.rsyntaxtextarea.TokenTypes;
+
+import javax.swing.text.Element;
+
+public class ReadOnlyDocument extends RSyntaxDocument {
+
+ private ReadOnlyRootElement readOnlyRootElement;
+
+ public ReadOnlyDocument(TokenMakerFactory tmf, String syntaxStyle, ReadOnlyContentInterface content) {
+ super(tmf, syntaxStyle, content);
+ readOnlyRootElement.setContent(content);
+ lastTokensOnLines.clear();
+ lastTokensOnLines.insertRange(0, content.getElementCount(), TokenTypes.NULL);
+ }
+
+ @Override
+ public Element getDefaultRootElement() {
+ if( readOnlyRootElement == null ){
+ readOnlyRootElement = new ReadOnlyRootElement(this);
+ }
+ return readOnlyRootElement;
+ }
+
+ @Override
+ public boolean isEditable() {
+ return false;
+ }
+}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyLeafElement.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyLeafElement.java
new file mode 100644
index 000000000..1170d36f0
--- /dev/null
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyLeafElement.java
@@ -0,0 +1,73 @@
+package org.fife.ui.rtextarea.readonly;
+
+import javax.swing.text.AttributeSet;
+import javax.swing.text.Document;
+import javax.swing.text.Element;
+import javax.swing.text.SimpleAttributeSet;
+
+public class ReadOnlyLeafElement implements Element {
+
+ private final AttributeSet attributeSet = new SimpleAttributeSet();
+
+ private final Document document;
+ private final Element parentElement;
+ private final ReadOnlyContentInterface content;
+ private final int index;
+
+ public ReadOnlyLeafElement(Document document, Element parentElement, ReadOnlyContentInterface content, int index) {
+ this.document = document;
+ this.parentElement = parentElement;
+ this.content = content;
+ this.index = index;
+ }
+
+ @Override
+ public Document getDocument() {
+ return document;
+ }
+
+ @Override
+ public Element getParentElement() {
+ return parentElement;
+ }
+
+ @Override
+ public String getName() {
+ return "content";
+ }
+
+ @Override
+ public AttributeSet getAttributes() {
+ return attributeSet;
+ }
+
+ @Override
+ public int getStartOffset() {
+ return content.getStartOffset(index);
+ }
+
+ @Override
+ public int getEndOffset() {
+ return content.getEndOffset(index);
+ }
+
+ @Override
+ public int getElementIndex(int offset) {
+ return 0;
+ }
+
+ @Override
+ public int getElementCount() {
+ return 0;
+ }
+
+ @Override
+ public Element getElement(int index) {
+ return null;
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return true;
+ }
+}
diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyRootElement.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyRootElement.java
new file mode 100644
index 000000000..41c72fdb8
--- /dev/null
+++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyRootElement.java
@@ -0,0 +1,73 @@
+package org.fife.ui.rtextarea.readonly;
+
+import javax.swing.text.AttributeSet;
+import javax.swing.text.Document;
+import javax.swing.text.Element;
+import javax.swing.text.SimpleAttributeSet;
+
+public class ReadOnlyRootElement implements Element {
+
+ private final AttributeSet attributeSet = new SimpleAttributeSet();
+
+ private final Document document;
+
+ private ReadOnlyContentInterface content = new NullReadOnlyContent();
+
+ public ReadOnlyRootElement(ReadOnlyDocument document) {
+ this.document = document;
+ }
+
+ @Override
+ public Document getDocument() {
+ return document;
+ }
+
+ @Override
+ public Element getParentElement() {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return "paragraph";
+ }
+
+ @Override
+ public AttributeSet getAttributes() {
+ return attributeSet;
+ }
+
+ @Override
+ public int getStartOffset() {
+ return 0;
+ }
+
+ @Override
+ public int getEndOffset() {
+ return content.length();
+ }
+
+ @Override
+ public int getElementIndex(int offset) {
+ return content.getElementIndex(offset);
+ }
+
+ @Override
+ public int getElementCount() {
+ return content.getElementCount();
+ }
+
+ @Override
+ public Element getElement(int index) {
+ return new ReadOnlyLeafElement(document, this, content, index);
+ }
+
+ @Override
+ public boolean isLeaf() {
+ return false;
+ }
+
+ public void setContent(ReadOnlyContentInterface content) {
+ this.content = content;
+ }
+}
diff --git a/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileBuilderTestUtil.java b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileBuilderTestUtil.java
new file mode 100644
index 000000000..4f3e8bfb5
--- /dev/null
+++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileBuilderTestUtil.java
@@ -0,0 +1,31 @@
+package org.fife.ui.rtextarea.readonly;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class FileBuilderTestUtil {
+
+ public static void writeTestFileWithAsciiChars(Path filePath, Charset charset, String lineSeparator, int rows) {
+ writeTestFile(filePath, charset, lineSeparator, "abcdefghi", rows);
+ }
+
+ public static void writeTestFileWithDoubleByteChar(Path filePath, Charset charset, String lineSeparator, int rows) {
+ writeTestFile(filePath, charset, lineSeparator, "Aݔabcdefg", rows);
+ }
+
+ private static void writeTestFile(Path filePath, Charset charset, String lineSeparator, String content, int rows) {
+ String text = lineSeparator;
+ try( BufferedWriter writer = Files.newBufferedWriter(filePath, charset) ){
+ for( int i = 0; i < rows; i++ ){
+ text = content + text;
+ writer.write(text);
+ }
+
+ } catch( IOException e ){
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscoverTest.java b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscoverTest.java
new file mode 100644
index 000000000..33b2e93c5
--- /dev/null
+++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscoverTest.java
@@ -0,0 +1,57 @@
+package org.fife.ui.rtextarea.readonly;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class FileOffsetLineDiscoverTest {
+
+ @Test
+ void testOffsetsWithLFCRSeparator(@TempDir Path tempDir) {
+ Path filePath = tempDir.resolve("template.txt");
+ testOffsets("\n\r", filePath);
+ }
+
+ @Test
+ void testOffsetsWithCRLFSeparator(@TempDir Path tempDir) {
+ Path filePath = tempDir.resolve("template.txt");
+ testOffsets("\r\n", filePath);
+ }
+
+ @Test
+ void testOffsetsWithCRSeparator(@TempDir Path tempDir) {
+ Path filePath = tempDir.resolve("template.txt");
+ testOffsets("\r", filePath);
+ }
+
+ @Test
+ void testOffsetsWithLFSeparator(@TempDir Path tempDir) {
+ Path filePath = tempDir.resolve("template.txt");
+ testOffsets("\n", filePath);
+ }
+
+ private void testOffsets(String separator, Path filePath) {
+ Charset charset = StandardCharsets.UTF_8;
+ FileBuilderTestUtil.writeTestFileWithAsciiChars(filePath, charset, separator, 1024);
+
+ List offsets = FileOffsetLineDiscover.getOffsets(filePath, charset);
+
+ List expectedOffsets = new ArrayList<>();
+ int separatorSize = separator.length();
+ int rows = 1025;
+ int last = 0;
+ for( int i = 0; i < rows; i++ ){
+ last = (last + separatorSize) + i * 9;
+ expectedOffsets.add(last - separatorSize);
+ }
+
+ assertEquals(expectedOffsets, offsets);
+ }
+}
diff --git a/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentTest.java b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentTest.java
new file mode 100644
index 000000000..e652bc6c3
--- /dev/null
+++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentTest.java
@@ -0,0 +1,99 @@
+package org.fife.ui.rtextarea.readonly;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import javax.swing.text.BadLocationException;
+import javax.swing.text.GapContent;
+import javax.swing.text.Segment;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ReadOnlyContentTest {
+
+ private static GapContent expectedContent;
+ private static ReadOnlyContent readOnlyContent;
+
+ @BeforeAll
+ static void beforeAll(@TempDir Path tempDir) throws Exception {
+ Path filePath = tempDir.resolve("template.txt");
+ Charset charset = StandardCharsets.UTF_16;
+ FileBuilderTestUtil.writeTestFileWithAsciiChars(filePath, charset, "\n", 128);
+ expectedContent = new GapContent();
+ String readString = Files.readString(filePath, charset);
+ expectedContent.insertString(0, readString);
+ readOnlyContent = new ReadOnlyContent(filePath.toFile(), charset);
+ }
+
+ @Test
+ void testGetLength() {
+ assertEquals(expectedContent.length(), readOnlyContent.length());
+ }
+
+ @Test
+ void testGetStringWithSingleChar() throws BadLocationException {
+ int last = expectedContent.length() - 1;
+ for( int i = 0; i < last; i++ ){
+ testGetString(i, 1);
+ }
+ }
+
+ @Test
+ void testGetStringWithTwoChars() throws BadLocationException {
+ int last = expectedContent.length() - 1;
+ for( int i = 0; i < last - 1; i++ ){
+ testGetString(i, 2);
+ }
+ }
+
+ @Test
+ void testGetStringThrowsExceptionWhenOffsetExceedLength() {
+ int length = expectedContent.length();
+ testGetStringThrowsException(length, 1);
+ testGetStringThrowsException(length-1, 2);
+ }
+
+ @Test
+ void testGetSegmentWithSingleChar() throws BadLocationException {
+ int last = expectedContent.length() - 1;
+ for( int i = 0; i < last; i++ ){
+ testGetSegment(i, 1);
+ }
+ }
+
+ @Test
+ void testGetSegmentWithTwoChars() throws BadLocationException {
+ int last = expectedContent.length() - 1;
+ for( int i = 0; i < last - 1; i++ ){
+ testGetSegment(i, 2);
+ }
+ }
+
+ private void testGetString(int where, int len) throws BadLocationException {
+ assertEquals(expectedContent.getString(where, len), readOnlyContent.getString(where, len), () -> "Error on offset where: " + where + " len: " + len);
+ }
+
+ private void testGetStringThrowsException(int where, int len) {
+ assertThrows(BadLocationException.class, ()-> expectedContent.getString(where, len));
+ assertThrows(BadLocationException.class, ()-> readOnlyContent.getString(where, len));
+ }
+
+
+ private void testGetSegment(int where, int len) throws BadLocationException {
+ Segment testSegment = new Segment();
+ Segment readOnlySegment = new Segment();
+
+ testSegment.setPartialReturn(true);
+ readOnlySegment.setPartialReturn(true);
+ expectedContent.getChars(where, len, testSegment);
+ readOnlyContent.getChars(where, len, readOnlySegment);
+
+ assertEquals(testSegment.toString(), readOnlySegment.toString(), () -> "Error on offset where:" + where);
+ }
+}
diff --git a/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentWithEmptyFileTest.java b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentWithEmptyFileTest.java
new file mode 100644
index 000000000..0866ff8de
--- /dev/null
+++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentWithEmptyFileTest.java
@@ -0,0 +1,84 @@
+package org.fife.ui.rtextarea.readonly;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import javax.swing.text.BadLocationException;
+import javax.swing.text.GapContent;
+import javax.swing.text.Segment;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ReadOnlyContentWithEmptyFileTest {
+
+ private static GapContent expectedContent;
+ private static ReadOnlyContent readOnlyContent;
+
+ @BeforeAll
+ static void beforeAll(@TempDir Path tempDir) throws Exception {
+ Path filePath = tempDir.resolve("template.txt");
+ Files.createFile(filePath);
+ Charset charset = StandardCharsets.UTF_16;
+ expectedContent = new GapContent();
+ readOnlyContent = new ReadOnlyContent(filePath.toFile(), charset);
+ }
+
+ @Test
+ void testGetLength() {
+ assertEquals(expectedContent.length(), readOnlyContent.length());
+ }
+
+ @Test
+ void testGetStringWithSingleChar() throws BadLocationException {
+ int last = expectedContent.length() - 1;
+ for( int i = 0; i < last; i++ ){
+ testGetString(i, 1);
+ }
+ }
+
+ @Test
+ void testGetStringWithTwoChars() throws BadLocationException {
+ int last = expectedContent.length() - 1;
+ for( int i = 0; i < last - 1; i++ ){
+ testGetString(i, 2);
+ }
+ }
+
+ @Test
+ void testGetSegmentWithSingleChar() throws BadLocationException {
+ int last = expectedContent.length() - 1;
+ for( int i = 0; i < last; i++ ){
+ testGetSegment(i, 1);
+ }
+ }
+
+ @Test
+ void testGetSegmentWithTwoChars() throws BadLocationException {
+ int last = expectedContent.length() - 1;
+ for( int i = 0; i < last - 1; i++ ){
+ testGetSegment(i, 2);
+ }
+ }
+
+ private void testGetString(int where, int len) throws BadLocationException {
+ assertEquals(expectedContent.getString(where, len), readOnlyContent.getString(where, len), () -> "Error on offset where: " + where + " len: " + len);
+ }
+
+
+ private void testGetSegment(int where, int len) throws BadLocationException {
+ Segment testSegment = new Segment();
+ Segment readOnlySegment = new Segment();
+
+ testSegment.setPartialReturn(true);
+ readOnlySegment.setPartialReturn(true);
+ expectedContent.getChars(where, len, testSegment);
+ readOnlyContent.getChars(where, len, readOnlySegment);
+
+ assertEquals(testSegment.toString(), readOnlySegment.toString(), () -> "Error on offset where:" + where);
+ }
+}
diff --git a/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocumentLauncher.java b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocumentLauncher.java
new file mode 100644
index 000000000..cbd563a49
--- /dev/null
+++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocumentLauncher.java
@@ -0,0 +1,127 @@
+package org.fife.ui.rtextarea.readonly;
+
+import org.fife.ui.rsyntaxtextarea.FileLocation;
+import org.fife.ui.rsyntaxtextarea.TextEditorPane;
+import org.fife.ui.rtextarea.RTextScrollPane;
+import org.fife.ui.rtextarea.SmartHighlightPainter;
+
+import javax.swing.*;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.Highlighter;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+
+import static org.fife.ui.rsyntaxtextarea.SyntaxConstants.SYNTAX_STYLE_XML;
+
+public class ReadOnlyDocumentLauncher {
+
+ public static void main(String[] args) throws IOException {
+ TestApp testApp = new TestApp();
+ testApp.show();
+ }
+
+ private static class TestApp {
+
+ public void show() throws IOException {
+ JFrame frame = buildAndDisplayGui();
+ frame.setVisible(true);
+ }
+
+ private JFrame buildAndDisplayGui() throws IOException {
+ JFrame frame = new JFrame("Test Frame");
+ frame.setMinimumSize(new Dimension(400, 400));
+ frame.setMaximumSize(new Dimension(400, 400));
+ frame.setResizable(false);
+
+ buildContent(frame);
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.pack();
+ frame.setLocationRelativeTo(null);
+ frame.setMinimumSize(new Dimension(400, 400));
+ frame.setSize(new Dimension(400, 400));
+ frame.setResizable(true);
+ return frame;
+ }
+
+ private void buildContent(JFrame frame) throws IOException {
+ File documentFile = File.createTempFile("ReadOnlyDocumentLauncher", ".txt");
+ documentFile.deleteOnExit();
+ Charset charset = StandardCharsets.UTF_8;
+ FileBuilderTestUtil.writeTestFileWithAsciiChars(documentFile.toPath(), charset, "\n", 1024);
+
+ TextEditorPane textArea = new TextEditorPane();
+
+ textArea.loadLocalFileInReadOnlyDocument(documentFile, charset);
+ textArea.setSyntaxEditingStyle(SYNTAX_STYLE_XML);
+ RTextScrollPane scrollPane = new RTextScrollPane(textArea, true);
+ frame.getContentPane().add(scrollPane);
+ JMenuBar menubar = new JMenuBar();
+
+ menubar.add(creteFileChooserAction(frame, (selectedFile) -> textArea.load(FileLocation.create(selectedFile)), "Open editable document"));
+ menubar.add(creteFileChooserAction(frame, file -> {
+
+ JProgressBar progressBar = new JProgressBar();
+ progressBar.setIndeterminate(true);
+ JDialog progressDialog = new JDialog(frame);
+ progressDialog.add(progressBar);
+ progressDialog.setSize(400,400);
+ progressDialog.setVisible(true);
+ SwingWorker