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 37ef18642..c20d6d3c0 100755 --- a/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxTextArea.java +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxTextArea.java @@ -681,7 +681,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..7f0a9b9d4 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,22 @@ */ 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 org.fife.ui.rtextarea.readonly.ReadOnlyFileStructure; +import org.fife.ui.rtextarea.readonly.ReadOnlyFileStructureParser; 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; +import java.nio.file.Path; /** * An extension of {@link org.fife.ui.rsyntaxtextarea.RSyntaxTextArea} @@ -456,18 +457,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 +477,73 @@ 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); + } - // 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 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; + } - // 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 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; + } - // No IOException thrown, so we can finally change the location. - charSet = ur.getEncoding(); - String old = getFileFullPath(); - this.loc = loc; + private RSyntaxDocument createLazyLoadDocument(FileLocation fileLocation, Charset charSet) throws IOException { + if( !fileLocation.isLocalAndExists() ){ + throw new FileNotFoundException("ReadOnlyDocument supports only files from local file system"); + } + Path filePath = Path.of(fileLocation.getFileFullPath()); + ReadOnlyFileStructureParser fileStructureParser = new ReadOnlyFileStructureParser(filePath, charSet); + ReadOnlyFileStructure readOnlyFileStructure = fileStructureParser.readStructure(); + ReadOnlyContent content = new ReadOnlyContent(filePath, charSet, readOnlyFileStructure); + return new ReadOnlyDocument(getTokenMarkerFactory(), getSyntaxEditingStyle(), content); + } + + 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 +733,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/FileCharsCounter.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/FileCharsCounter.java new file mode 100644 index 000000000..69c621e2a --- /dev/null +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/FileCharsCounter.java @@ -0,0 +1,14 @@ +package org.fife.ui.rtextarea.readonly; + +public class FileCharsCounter { + + int count = 2; + + public void onBufferRead(int readChars) { + count += readChars; + } + + public int getCount() { + return count; + } +} 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..8dcd1e946 --- /dev/null +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscover.java @@ -0,0 +1,56 @@ +package org.fife.ui.rtextarea.readonly; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +class FileOffsetLineDiscover { + + int lastOffset = 0; + + private final List lfOffsets = new ArrayList<>(); + private final Set crOffsets = new HashSet<>(); + + public List getOffsets() { + + List lineOffsets = new ArrayList<>(); + lineOffsets.add(0); + + 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; + } + + public void onBufferRead(char[] charBuffer, int readChars) { + for( int i = 0; i < readChars; i++ ){ + lastOffset++; + char character = charBuffer[i]; + if( character == '\n' ){ + lfOffsets.add(lastOffset); + } else if( character == '\r' ){ + crOffsets.add(lastOffset); + } + } + } +} 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..55ae0b207 --- /dev/null +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContent.java @@ -0,0 +1,157 @@ +package org.fife.ui.rtextarea.readonly; + +import org.fife.io.UnicodeReader; + +import javax.swing.text.BadLocationException; +import javax.swing.text.Position; +import javax.swing.text.Segment; +import javax.swing.undo.UndoableEdit; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.List; + +public class ReadOnlyContent implements ReadOnlyContentInterface { + + private static final int MAX_BUFFER_SIZE = 1024 * 1024; //1 MB + private static final int NOT_INITIALIZED_BUFFER_OFFSET = -1; + + private final Path filePath; + private final Charset charset; + private final List offsets; + private final int bufferSize; + private final int fileSize; + private final int tailOffset; + private final char[] buffer; + private final int charsCount; + + private int startBufferOffset = NOT_INITIALIZED_BUFFER_OFFSET; + + public ReadOnlyContent(Path filePath, Charset charset, ReadOnlyFileStructure fileStructure) { + this.filePath = filePath; + this.charset = charset; + fileSize = (int) fileStructure.getFileSize(); + this.tailOffset = fileStructure.getTailOffset(); + bufferSize = Math.min(fileSize, MAX_BUFFER_SIZE); + buffer = new char[bufferSize]; + offsets = fileStructure.getOffsets(); + charsCount = fileStructure.getCharsCount(); + } + + + @Override + public Position createPosition(int offset) { + return () -> offset; + } + + @Override + public int length() { + 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( UnicodeReader bufferedReader = new UnicodeReader(filePath.toFile(), charset); ){ + bufferedReader.skip(tailOffset); + bufferedReader.skip(startReadOffset); + bufferedReader.read(buffer, 0, charsToRead); + } 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) throws BadLocationException { + try{ + return getString(offset, 1).charAt(0); + } catch( BadLocationException e ){ + throw new ReadOnlyBadLocationException("Invalid offset", offset, 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() - 1; i++ ){ + if( offset < offsets.get(i + 1) ){ + return i; + } + } + return offsets.size() - 1; + } + + 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..cf4476e9e --- /dev/null +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentInterface.java @@ -0,0 +1,18 @@ +package org.fife.ui.rtextarea.readonly; + +import org.fife.ui.rtextarea.RContent; + +import javax.swing.text.AbstractDocument; +import javax.swing.text.BadLocationException; + +public interface ReadOnlyContentInterface extends AbstractDocument.Content, RContent { + char charAt(int offset) throws BadLocationException; + + 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..cbd591a02 --- /dev/null +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocument.java @@ -0,0 +1,33 @@ +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; + +//This document is useful to read a huge files, like Log Files +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/ReadOnlyFileStructure.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyFileStructure.java new file mode 100644 index 000000000..006cf93a8 --- /dev/null +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyFileStructure.java @@ -0,0 +1,34 @@ +package org.fife.ui.rtextarea.readonly; + +import java.util.List; + +public class ReadOnlyFileStructure { + + private final int charsCount; + private final List offsets; + private final long fileSize; + private final int tailOffset; + + public ReadOnlyFileStructure(int charsCount, List offsets, long fileSize, int tailOffset) { + this.charsCount = charsCount; + this.offsets = offsets; + this.fileSize = fileSize; + this.tailOffset = tailOffset; + } + + public int getCharsCount() { + return charsCount; + } + + public List getOffsets() { + return offsets; + } + + public long getFileSize() { + return fileSize; + } + + public int getTailOffset() { + return tailOffset; + } +} diff --git a/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyFileStructureParser.java b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyFileStructureParser.java new file mode 100644 index 000000000..28a9990ae --- /dev/null +++ b/RSyntaxTextArea/src/main/java/org/fife/ui/rtextarea/readonly/ReadOnlyFileStructureParser.java @@ -0,0 +1,45 @@ +package org.fife.ui.rtextarea.readonly; + +import org.fife.io.UnicodeReader; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ReadOnlyFileStructureParser { + + private static final int BUFFER_SIZE = 1024 * 1024; + private final Path filePath; + private final Charset charset; + private final int tailOffset; + + public ReadOnlyFileStructureParser(Path filePath, Charset charset) { + this(filePath, charset, 0); + } + + //tailOffset can be used to exclude the header of a file + public ReadOnlyFileStructureParser(Path filePath, Charset charset, int tailOffset) { + this.filePath = filePath; + this.charset = charset; + this.tailOffset = tailOffset; + } + + public ReadOnlyFileStructure readStructure() throws IOException { + + try( UnicodeReader bufferedReader = new UnicodeReader(filePath.toFile(), charset) ){ + bufferedReader.skip(tailOffset); + char[] charBuffer = new char[BUFFER_SIZE]; + FileCharsCounter fileCharsCounter = new FileCharsCounter(); + FileOffsetLineDiscover fileOffsetLineDiscover = new FileOffsetLineDiscover(); + + int readChars = 0; + while( readChars >= 0 ){ + readChars = bufferedReader.read(charBuffer); + fileCharsCounter.onBufferRead(readChars); + fileOffsetLineDiscover.onBufferRead(charBuffer, readChars); + } + return new ReadOnlyFileStructure(fileCharsCounter.getCount(), fileOffsetLineDiscover.getOffsets(), Files.size(filePath), tailOffset); + } + } +} 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..effdd986f --- /dev/null +++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileBuilderTestUtil.java @@ -0,0 +1,41 @@ +package org.fife.ui.rtextarea.readonly; + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +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) throws IOException { + writeTestFile(filePath, charset, lineSeparator, "abcdefghi", rows, true); + } + + public static void writeTestFileWithDoubleByteChar(Path filePath, Charset charset, String lineSeparator, int rows) throws IOException { + writeTestFile(filePath, charset, lineSeparator, "Aݔabcdefg", rows, true); + } + + public static void writeTestFile(Path filePath, Charset charset, String lineSeparator, String content, int rows, boolean includeBom) throws IOException { + + if( includeBom ){ + String text = lineSeparator; + try( BufferedWriter writer = Files.newBufferedWriter(filePath, charset) ){ + for( int i = 0; i < rows; i++ ){ + text = content + text; + writer.write(text); + } + } + } else{ + try( OutputStream out = new FileOutputStream(filePath.toFile()) ){ + String text = lineSeparator; + for( int i = 0; i < rows; i++ ){ + text = content + text; + out.write(text.getBytes(charset)); + } + } + } + } +} 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..8d18dd147 --- /dev/null +++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/FileOffsetLineDiscoverTest.java @@ -0,0 +1,70 @@ +package org.fife.ui.rtextarea.readonly; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +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) throws IOException { + Path filePath = tempDir.resolve("template.txt"); + testOffsets("\n\r", filePath); + } + + @Test + void testOffsetsWithCRLFSeparator(@TempDir Path tempDir) throws IOException { + Path filePath = tempDir.resolve("template.txt"); + testOffsets("\r\n", filePath); + } + + @Test + void testOffsetsWithCRSeparator(@TempDir Path tempDir) throws IOException { + Path filePath = tempDir.resolve("template.txt"); + testOffsets("\r", filePath); + } + + @Test + void testOffsetsWithLFSeparator(@TempDir Path tempDir) throws IOException { + Path filePath = tempDir.resolve("template.txt"); + testOffsets("\n", filePath); + } + + private void testOffsets(String separator, Path filePath) throws IOException { + Charset charset = StandardCharsets.UTF_8; + int rows = 2048; + FileBuilderTestUtil.writeTestFileWithAsciiChars(filePath, charset, separator, rows); + + FileOffsetLineDiscover fileOffsetLineDiscover = new FileOffsetLineDiscover(); + try( BufferedReader bufferedReader = Files.newBufferedReader(filePath, charset); ){ + char[] charBuffer = new char[1024]; + int readChars = 0; + while( readChars >= 0 ){ + readChars = bufferedReader.read(charBuffer); + fileOffsetLineDiscover.onBufferRead(charBuffer, readChars); + } + } + List offsets = fileOffsetLineDiscover.getOffsets(); + + + List expectedOffsets = new ArrayList<>(); + int separatorSize = separator.length(); + 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..503b3efae --- /dev/null +++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentTest.java @@ -0,0 +1,100 @@ +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); + ReadOnlyFileStructure fileStructure = new ReadOnlyFileStructureParser(filePath, charset).readStructure(); + readOnlyContent = new ReadOnlyContent(filePath, charset, fileStructure); + } + + @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..86b615c84 --- /dev/null +++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyContentWithEmptyFileTest.java @@ -0,0 +1,85 @@ +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(); + ReadOnlyFileStructure fileStructure = new ReadOnlyFileStructureParser(filePath, charset).readStructure(); + readOnlyContent = new ReadOnlyContent(filePath, charset, fileStructure); + } + + @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..b65f2ac6a --- /dev/null +++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocumentLauncher.java @@ -0,0 +1,212 @@ +package org.fife.ui.rtextarea.readonly; + +import org.fife.io.UnicodeReader; +import org.fife.ui.rsyntaxtextarea.FileLocation; +import org.fife.ui.rsyntaxtextarea.TextEditorPane; +import org.fife.ui.rtextarea.RTextScrollPane; + +import javax.swing.*; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.fife.ui.rsyntaxtextarea.SyntaxConstants.SYNTAX_STYLE_XML; + +public class ReadOnlyDocumentLauncher { + + private static final Logger LOGGER = Logger.getLogger(ReadOnlyDocumentLauncher.class.getName()); + private static TextEditorPane textArea = new TextEditorPane(); + + public static void main(String[] args) throws IOException { + TestApp testApp = new TestApp(); + testApp.show(); + } + + private static class TestApp { + + private File documentFile; + boolean createBom = true; + Charset selectedCharset = StandardCharsets.UTF_8; + + JTextField fileContentField = new JTextField("abcd"); + JTextField fileRowsField = new JTextField(String.valueOf(20)); + JTextField tailField = new JTextField(String.valueOf(0)); + + JLabel statusLabel = new JLabel(" "); + private JProgressBar progressBar = new JProgressBar(); + + public void show() { + JFrame frame = buildAndDisplayGui(); + frame.setVisible(true); + } + + private JFrame buildAndDisplayGui() { + JFrame frame = new JFrame("Test Frame"); + frame.setMinimumSize(new Dimension(640, 480)); + frame.setResizable(false); + + frame.setContentPane(createPanel()); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setResizable(true); + return frame; + } + + private JComponent createPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.add(createConfigurationPanel(), BorderLayout.EAST); + panel.add(createEditorPanel(), BorderLayout.CENTER); + return panel; + } + + private JComponent createConfigurationPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + JCheckBox bomCheckbox = new JCheckBox(new AbstractAction("Add BOM") { + @Override + public void actionPerformed(ActionEvent e) { + createBom = ((JCheckBox) e.getSource()).isSelected(); + } + }); + bomCheckbox.setSelected(createBom); + panel.add(bomCheckbox); + + panel.add(createCharsetPanel()); + + panel.add(new JLabel("File content")); + panel.add(fileContentField); + + panel.add(new JLabel("Rows")); + panel.add(fileRowsField); + + panel.add(new JLabel("Skip chars")); + panel.add(tailField); + + JButton createFileButton = new JButton(new AbstractAction("Write file") { + @Override + public void actionPerformed(ActionEvent e) { + createFile(); + } + }); + panel.add(createFileButton); + + JButton readFileButton = new JButton(new AbstractAction("Read file") { + @Override + public void actionPerformed(ActionEvent e) { + createReadDocumentWorker().execute(); + } + }); + panel.add(readFileButton); + + panel.add(statusLabel); + + panel.add(progressBar); +// progressBar.setEnabled(false); + progressBar.setIndeterminate(false); + + + return panel; + } + + private JPanel createCharsetPanel() { + ButtonGroup charsetGroup = new ButtonGroup(); + JPanel panel = new JPanel(); + BoxLayout layout = new BoxLayout(panel, BoxLayout.Y_AXIS); + panel.setLayout(layout); + + panel.add(new JLabel("Charset")); + panel.add(createCharsetRadioButton(charsetGroup, StandardCharsets.US_ASCII)); + panel.add(createCharsetRadioButton(charsetGroup, StandardCharsets.ISO_8859_1)); + panel.add(createCharsetRadioButton(charsetGroup, StandardCharsets.UTF_8)); + panel.add(createCharsetRadioButton(charsetGroup, StandardCharsets.UTF_16BE)); + panel.add(createCharsetRadioButton(charsetGroup, StandardCharsets.UTF_16LE)); + panel.add(createCharsetRadioButton(charsetGroup, StandardCharsets.UTF_16)); + panel.add(createCharsetRadioButton(charsetGroup, null)); + + return panel; + + } + + + private JRadioButton createCharsetRadioButton(ButtonGroup charsetGroup, Charset charset) { + JRadioButton radioButton = new JRadioButton(createRadioButtonAction(charset)); + radioButton.setSelected(charset == selectedCharset); + charsetGroup.add(radioButton); + return radioButton; + } + + private Action createRadioButtonAction(Charset charset) { + String name = charset == null ? "From BOM" : charset.displayName(); + return new AbstractAction(name) { + @Override + public void actionPerformed(ActionEvent event) { + if( charset == null ){ + try( UnicodeReader reader = new UnicodeReader(documentFile) ){ + selectedCharset = Charset.forName(reader.getEncoding()); + } catch( IOException e ){ + LOGGER.log(Level.WARNING, e.getMessage(), e); + } + + } else selectedCharset = charset; + } + }; + } + + private void createFile() { + try{ + documentFile = File.createTempFile("ReadOnlyDocumentLauncher", ".txt"); + documentFile.deleteOnExit(); + FileBuilderTestUtil.writeTestFile(documentFile.toPath(), selectedCharset, "\n", fileContentField.getText(), Integer.parseInt(fileRowsField.getText()), createBom); + statusLabel.setText("File saved"); + LOGGER.log(Level.INFO, "File saved: " + documentFile.getAbsolutePath()); + } catch( IOException e ){ + statusLabel.setText("Save file failed"); + LOGGER.log(Level.WARNING, e.getMessage(), e); + } + } + + private JComponent createEditorPanel() { + return new RTextScrollPane(textArea, true); + } + + + private SwingWorker createReadDocumentWorker() { + return new SwingWorker<>() { + @Override + protected ReadOnlyDocument doInBackground() throws Exception { + progressBar.setIndeterminate(true); + + Path filePath = documentFile.toPath(); + ReadOnlyFileStructureParser structureParser = new ReadOnlyFileStructureParser(filePath, selectedCharset, Integer.parseInt(tailField.getText())); + ReadOnlyFileStructure fileStructure = structureParser.readStructure(); + ReadOnlyContent content = new ReadOnlyContent(filePath, selectedCharset, fileStructure); + ReadOnlyDocument document = new ReadOnlyDocument(null, SYNTAX_STYLE_XML, content); + textArea.loadDocument(FileLocation.create(documentFile), document, selectedCharset); + + return document; + } + + @Override + protected void done() { + try{ + statusLabel.setText("File loaded"); + } catch( Exception e ){ + LOGGER.log(Level.WARNING, e.getMessage(), e); + statusLabel.setText("Load file failed"); + } + progressBar.setIndeterminate(false); + } + }; + } + + } + +} diff --git a/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocumentTest.java b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocumentTest.java new file mode 100644 index 000000000..1a90d7150 --- /dev/null +++ b/RSyntaxTextArea/src/test/java/org/fife/ui/rtextarea/readonly/ReadOnlyDocumentTest.java @@ -0,0 +1,82 @@ +package org.fife.ui.rtextarea.readonly; + +import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.swing.text.Element; +import java.io.IOException; +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 ReadOnlyDocumentTest { + + private static RSyntaxDocument expectedDocument; + private static ReadOnlyDocument readOnlyDocument; + + @BeforeAll + static void beforeAll(@TempDir Path tempDir) throws IOException { + expectedDocument = new RSyntaxDocument(SyntaxConstants.SYNTAX_STYLE_NONE); + + Charset charset; + Path filePath; + try{ + charset = StandardCharsets.UTF_8; + filePath = tempDir.resolve("template.txt"); + FileBuilderTestUtil.writeTestFileWithDoubleByteChar(filePath, charset, "\n", 128); + + String readString = Files.readString(filePath, charset); + + expectedDocument.insertString(0, readString, null); + } catch( Exception e ){ + throw new RuntimeException(e); + } + + ReadOnlyFileStructure fileStructure = new ReadOnlyFileStructureParser(filePath, charset).readStructure(); + ReadOnlyContent content = new ReadOnlyContent(filePath, charset, fileStructure); + readOnlyDocument = new ReadOnlyDocument(null, "", content); + } + + @Test + void testLengths() { + assertEquals(expectedDocument.getRootElements().length, readOnlyDocument.getRootElements().length); + assertEquals(expectedDocument.getLength(), readOnlyDocument.getLength()); + assertEquals(expectedDocument.getDefaultRootElement().getElementCount(), readOnlyDocument.getDefaultRootElement().getElementCount()); + } + + @Test + void testElementIndex() { + int length = expectedDocument.getLength(); + for( int i = 0; i < length; i++ ){ + assertEquals(expectedDocument.getDefaultRootElement().getElementIndex(i), readOnlyDocument.getDefaultRootElement().getElementIndex(i), "testElementIndex filed on index: " + i); + } + } + + @Test + void testStartOffsets() { + Element originalRootElement = expectedDocument.getDefaultRootElement(); + Element readonlyRootElement = readOnlyDocument.getDefaultRootElement(); + int elementCount = originalRootElement.getElementCount(); + + for( int i = 0; i < elementCount; i++ ){ + assertEquals(originalRootElement.getElement(i).getStartOffset(), readonlyRootElement.getElement(i).getStartOffset()); + } + } + + @Test + void testEndOffsets() { + Element originalRootElement = expectedDocument.getDefaultRootElement(); + Element readonlyRootElement = readOnlyDocument.getDefaultRootElement(); + int elementCount = originalRootElement.getElementCount(); + + for( int i = 0; i < elementCount; i++ ){ + assertEquals(originalRootElement.getElement(i).getEndOffset(), readonlyRootElement.getElement(i).getEndOffset()); + } + } +}