blob: 44b2d4c2fbdcbcae522da0e0b520cd3ff3e19f6e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.core.output2;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.util.Pair;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.core.output2.options.OutputOptions;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.Mutex;
import org.openide.windows.IOColors;
import org.openide.windows.OutputListener;
/**
* Abstract Lines implementation with handling for getLine wrap calculations, etc.
*/
abstract class AbstractLines implements Lines, Runnable, ActionListener {
private static final Logger LOG =
Logger.getLogger(AbstractLines.class.getName());
private OutputLimits outputLimits = OutputLimits.getDefault();
/** A collections-like lineStartList that maps file positions to getLine numbers */
IntList lineStartList;
IntListSimple lineCharLengthListWithTabs;
/** Maps output listeners to the lines they are associated with */
IntMap lineWithListenerToInfo;
/** line index to LineInfo */
IntMap linesToInfos;
/** longest line length (in chars)*/
private int longestLineLen = 0;
private int knownCharsPerLine = -1;
/** cache of logical (wrapped) lines count, used to transform logical (wrapped)
* line index to physical (real) line index */
private SparseIntList knownLogicalLineCounts = null;
/** Offset positions of tabs */
private IntList tabCharOffsets = new IntList(128);
/** Sums of length of all preceding tabs (length of extra spaces) */
private IntListSimple tabLengthSums = new IntListSimple(128);
private final IntListSimple foldOffsets = new IntListSimple(16);
private final IntListSimple visibleList = new IntListSimple(128);
private final IntListSimple visibleToRealLine = new IntListSimple(128);
private final IntListSimple realToVisibleLine = new IntListSimple(128);
private int hiddenLines = 0;
private int currentFoldStart = -1;
/** last storage size (after dispose), in bytes */
private int lastStorageSize = -1;
AbstractLines() {
if (Controller.LOG) Controller.log ("Creating a new AbstractLines");
init();
}
protected abstract Storage getStorage();
protected abstract boolean isDisposed();
protected abstract void handleException (Exception e);
public char[] getText(int start, int end, char[] chars) {
if (chars == null) {
chars = new char[end - start];
}
if (end < start || start < 0) {
throw new IllegalArgumentException ("Illogical text range from " + start + " to " + end);
}
if (end - start > chars.length) {
throw new IllegalArgumentException("Array size is too small");
}
synchronized(readLock()) {
if (isDisposed()) {
// dispose is performed asynchronously, data may be required by
// events fired before (during) dispose(), return just zeros
// (output will be cleared soon anyway)
for (int i = 0; i < end - start; i++) {
chars[i] = 0;
}
return chars;
}
int fileStart = toByteIndex(start);
int byteCount = toByteIndex(end - start);
BufferResource<ByteBuffer> br = null;
try {
br = getStorage().getReadBuffer(fileStart, byteCount);
CharBuffer chb = br.getBuffer().asCharBuffer();
//#68386 satisfy the request as much as possible, but if there's not enough remaining
// content, not much we can do..
int len = Math.min(end - start, chb.remaining());
chb.get(chars, 0, len);
return chars;
} catch (Exception e) {
handleException (e);
return new char[0];
} finally {
if (br != null) {
br.releaseBuffer();
}
}
}
}
private BufferResource<CharBuffer> getCharBuffer(int start, int len) {
if (len < 0 || start < 0) {
throw new IllegalArgumentException ("Illogical text range from " + start + " to " + (start + len));
}
synchronized(readLock()) {
if (isDisposed()) {
return null;
}
int fileStart = AbstractLines.toByteIndex(start);
int byteCount = AbstractLines.toByteIndex(len);
int available = getStorage().size();
if (available < fileStart + byteCount) {
throw new ArrayIndexOutOfBoundsException ("Bytes from " +
fileStart + " to " + (fileStart + byteCount) + " requested, " +
"but storage is only " + available + " bytes long");
}
BufferResource<ByteBuffer> readBuffer = null;
try {
readBuffer = getStorage().getReadBuffer(fileStart, byteCount);
return new CharBufferResource(readBuffer);
} catch (Exception e) {
if (readBuffer != null) {
readBuffer.releaseBuffer();
}
return null;
}
}
}
@Override
public String getText(int start, int end) {
BufferResource<CharBuffer> br = getCharBuffer(start, end - start);
try {
CharBuffer cb = br == null ? null : br.getBuffer();
String s = cb != null ? cb.toString() : new String(new char[end - start]);
return s;
} finally {
if (br != null) {
br.releaseBuffer();
}
}
}
void onDispose(int lastStorageSize) {
this.lastStorageSize = lastStorageSize;
}
int getByteSize() {
synchronized (readLock()) {
if (lastStorageSize >= 0) {
return lastStorageSize;
}
Storage storage = getStorage();
return storage == null ? 0 : storage.size();
}
}
private ChangeListener listener = null;
public void addChangeListener(ChangeListener cl) {
this.listener = cl;
synchronized(readLock()) {
if (getLineCount() > 0) {
//May be a new tab for an old output, hide and reshow, etc.
fire();
}
}
}
public void removeChangeListener (ChangeListener cl) {
if (listener == cl) {
listener = null;
}
}
private javax.swing.Timer timer = null;
private final AtomicBoolean newEvent = new AtomicBoolean(false);
public void actionPerformed(ActionEvent e) {
newEvent.set(false);
fire();
synchronized (newEvent) {
if (!newEvent.get()) {
timer.stop();
}
}
}
void delayedFire() {
newEvent.set(true);
if (listener == null) {
return;
}
if (timer == null) {
timer = new javax.swing.Timer(200, this);
}
synchronized (newEvent) {
if (newEvent.get() && !timer.isRunning()) {
timer.start();
}
}
}
public void fire() {
if (Controller.LOG) Controller.log (this + ": Writer firing " + getStorage().size() + " bytes written");
if (listener != null) {
Mutex.EVENT.readAccess(this);
}
}
public void run() {
if (listener != null) {
listener.stateChanged(new ChangeEvent(this));
}
}
public boolean hasListeners() {
return firstListenerLine() != -1;
}
public OutputListener getListener(int pos, int[] range) {
int line = getLineAt(pos);
int lineStart = getLineStart(line);
pos -= lineStart;
LineInfo info = (LineInfo) lineWithListenerToInfo.get(line);
if (info == null) {
return null;
}
int start = 0;
for (LineInfo.Segment seg : info.getLineSegments()) {
if (pos < seg.getEnd()) {
if (seg.getListener() != null) {
if (range != null) {
range[0] = lineStart + start;
range[1] = lineStart + seg.getEnd();
}
return seg.getListener();
} else {
return null;
}
}
start = seg.getEnd();
}
return null;
}
public boolean isListener(int start, int end) {
int[] range = new int[2];
OutputListener l = getListener(start, range);
return l == null ? false : (range[0] == start && range[1] == end);
}
private void init() {
knownLogicalLineCounts = null;
lineStartList = new IntList(128);
lineStartList.add(0);
lineCharLengthListWithTabs = new IntListSimple(100);
linesToInfos = new IntMap();
lineWithListenerToInfo = new IntMap();
longestLineLen = 0;
listener = null;
dirty = false;
curDefColors = getDefColors().clone();
}
private boolean dirty;
public boolean checkDirty(boolean clear) {
if (isDisposed()) {
return false;
}
boolean wasDirty = dirty;
if (clear) {
dirty = false;
}
return wasDirty;
}
public int[] getLinesWithListeners() {
return lineWithListenerToInfo.getKeys();
}
public int getCharCount() {
return AbstractLines.toCharIndex(getByteSize());
}
/**
* Get a single getLine as a string.
*/
public String getLine (int idx) throws IOException {
int lineStart = getCharLineStart(idx);
int lineEnd = toCharIndex(idx < lineStartList.size() - 1 ? lineStartList.get(idx + 1) : getByteSize());
return getText(lineStart, lineEnd);
}
/**
* Get line length (in characters), not counting input characters on the
* last line.
*
* This method returns the same result as {@code getLine(idx).length()}, but
* is much faster and cheaper, because the text doesn't have to be
* constructed (see bug bug 250084).
*
* Method {@link #length(int)} is very similar, but returns different value
* for the last line (it counts also input characters).
*
* @param idx Line index.
*
* @see #getLine(int)
*/
private int getLineLength(int idx) {
int lineStart = getCharLineStart(idx);
int lineEnd = toCharIndex(idx < lineStartList.size() - 1 ? lineStartList.get(idx + 1) : getByteSize());
return Math.max(0, lineEnd - lineStart);
}
/**
* Get a length of single line in bytes.
*/
private int getByteLineLength(int idx) {
if (idx == lineStartList.size()-1) {
return Math.max(0, toByteIndex(lastLineLength));
}
int lineStart = getByteLineStart(idx);
int lineEnd = idx < lineStartList.size() - 1 ? lineStartList.get(idx + 1) - 2 * OutWriter.LINE_SEPARATOR.length() : getByteSize();
return lineEnd - lineStart;
}
public boolean isLineStart (int chpos) {
int bpos = toByteIndex(chpos);
return lineStartList.contains (bpos) || bpos == 0 || (bpos == getByteSize() && lastLineFinished);
}
/**
* Returns the length of the specified lin characters.
*
* @param idx A getLine number
* @return The number of characters
*/
public int length (int idx) {
return toCharIndex(getByteLineLength(idx));
}
/**
* Returns the length of the specified lin characters.
*
* @param idx A getLine number
* @return The number of characters
*/
public int lengthWithTabs (int idx) {
if (idx == lineCharLengthListWithTabs.size()) {
return Math.max(0, lastCharLengthWithTabs);
}
return lineCharLengthListWithTabs.get(idx);
}
/**
* Get the <strong>character</strong> index of a getLine as a position in
* the output file.
*/
public int getLineStart (int line) {
return getCharLineStart(line);
}
private int getByteLineStart(int line) {
if (line == lineStartList.size() && lastLineFinished) {
return getByteSize();
}
return lineStartList.get(line);
}
private int getCharLineStart(int line) {
return toCharIndex(getByteLineStart(line));
}
/** Get the getLine number of a <strong>character</strong> index in the
* file (as distinct from a byte position)
*/
public int getLineAt (int position) {
int bytePos = toByteIndex (position);
if (bytePos >= getByteSize()) {
return getLineCount() - 1;
}
return lineStartList.findNearest(bytePos);
}
public int getLineCount() {
synchronized (readLock()) {
return lineStartList.size();
}
}
public Collection<OutputListener> getListenersForLine(int line) {
LineInfo info = (LineInfo) lineWithListenerToInfo.get(line);
if (info == null) {
return Collections.emptyList();
}
return info.getListeners();
}
public int firstListenerLine () {
if (isDisposed()) {
return -1;
}
return lineWithListenerToInfo.isEmpty() ? -1 : lineWithListenerToInfo.first();
}
public OutputListener nearestListener(int pos, boolean backward, int[] range) {
int posLine = getLineAt(pos);
int line = lineWithListenerToInfo.nearest(posLine, backward);
if (line < 0) {
return null;
}
LineInfo info = (LineInfo) lineWithListenerToInfo.get(line);
int lineStart = getLineStart(line);
OutputListener l = null;
int[] lpos = new int[2];
if (posLine == line) {
if (backward) {
info.getFirstListener(lpos);
if (lpos[0] + lineStart > pos) {
line = lineWithListenerToInfo.nearest(line - 1, backward);
info = (LineInfo) lineWithListenerToInfo.get(line);
lineStart = getLineStart(line);
l = info.getLastListener(lpos);
}
} else {
info.getLastListener(lpos);
if (lpos[1] + lineStart <= pos) {
line = lineWithListenerToInfo.nearest(line + 1, backward);
info = (LineInfo) lineWithListenerToInfo.get(line);
lineStart = getLineStart(line);
l = info.getFirstListener(lpos);
}
}
} else {
pos = lineStart;
l = backward ? info.getLastListener(lpos) : info.getFirstListener(lpos);
}
if (l == null) {
l = backward ? info.getListenerBefore(pos - lineStart, lpos) : info.getListenerAfter(pos - lineStart, lpos);
}
if (l != null) {
range[0] = lpos[0] + lineStart;
range[1] = lpos[1] + lineStart;
}
return l;
}
public int getLongestLineLength() {
return longestLineLen;
}
/** recalculate logical (wrapped) line index to physical (real) line index
* <p> [in]
* info[0] - "global" logical (wrapped) line index
* <p> [out]
* info[0] - physical (real) line index;
* info[1] - index of wrapped line on current realLineIdx;
* info[2] - total number of wrapped lines of found physical line
*/
public void toPhysicalLineIndex(final int[] info, int charsPerLine) {
int logicalLineIdx = info[0];
if (logicalLineIdx <= 0) {
//First getLine never has lines above it
info[0] = 0;
info[1] = 0;
info[2] = lengthToLineCount(lengthWithTabs(0), charsPerLine);
return;
}
if (charsPerLine >= longestLineLen || (getLineCount() < 1)) {
//The doc is empty, or there are no lines long enough to wrap anyway
info[0] = visibleToRealLine(logicalLineIdx);
info[1] = 0;
info[2] = 1;
return;
}
// find physical (real) line index which corresponds to logical line idx
int physLineIdx = Math.min(findPhysicalLine(logicalLineIdx, charsPerLine), getLineCount() - 1);
// compute how many logical lines is above our physical line
int linesAbove = getLogicalLineCountAbove(physLineIdx, charsPerLine);
int len = lengthWithTabs(physLineIdx);
int wrapCount = lengthToLineCount(len, charsPerLine);
info[0] = physLineIdx;
info[1] = logicalLineIdx - linesAbove;
info[2] = wrapCount;
}
private int findPhysicalLine(int logicalLineIdx, int charsPerLine) {
if (logicalLineIdx == 0) {
return 0;
}
if (charsPerLine != knownCharsPerLine || knownLogicalLineCounts == null) {
calcLogicalLineCount(charsPerLine);
}
return knownLogicalLineCounts.getNextKey(logicalLineIdx);
}
/**
* Get the number of logical lines if character wrapped at the specified width.
*/
public int getLogicalLineCountAbove(int line, int charsPerLine) {
if (line == 0) {
return 0;
}
if (charsPerLine >= longestLineLen) {
return realToVisibleLine(line);
}
if (charsPerLine != knownCharsPerLine || knownLogicalLineCounts == null) {
calcLogicalLineCount(charsPerLine);
}
return knownLogicalLineCounts.get(line - 1);
}
/**
* Get the number of logical lines above a given physical getLine if character
* wrapped at the specified
*/
public int getLogicalLineCountIfWrappedAt (int charsPerLine) {
if (charsPerLine >= longestLineLen) {
return getVisibleLineCount();
}
int lineCount = getLineCount();
if (charsPerLine == 0 || lineCount == 0) {
return 0;
}
synchronized (readLock()) {
if (charsPerLine != knownCharsPerLine || knownLogicalLineCounts == null) {
calcLogicalLineCount(charsPerLine);
}
return knownLogicalLineCounts.get(lineCount-1);
}
}
/**
* Get number of physical characters for the given logical (expanded TABs) length.
* @param offset
* @param logicalLength
* @param tabShiftPtr
* @return number of physical characters
*/
public int getNumPhysicalChars(int offset, int logicalLength, int[] tabShiftPtr) {
synchronized (readLock()) {
int tabIndex1 = tabCharOffsets.findNearest(offset);
int tabSum1;
if (tabIndex1 >= 0) {
if (tabCharOffsets.get(tabIndex1) < offset) {
tabSum1 = tabLengthSums.get(tabIndex1);
} else if (tabIndex1 > 0) {
tabSum1 = tabLengthSums.get(tabIndex1 - 1);
} else {
tabSum1 = 0;
}
} else {
tabSum1 = 0;
}
int tabIndex2 = tabCharOffsets.findNearest(offset + logicalLength);
if (tabIndex2 < 0) {
return logicalLength;
}
int tabSum2 = (tabIndex2 >= 0) ? tabLengthSums.get(tabIndex2) : 0;
if (tabSum1 == tabSum2) {
return logicalLength;
}
int realLength = logicalLength + tabSum1 - tabSum2; // rough guess, might be too small
if (realLength < 0) realLength = 0;
int i = offset + realLength;
tabIndex2 = tabCharOffsets.findNearest(i);
if (tabIndex2 < 0) {
tabIndex2 = - 1;
tabSum2 = 0;
} else {
tabSum2 = tabLengthSums.get(tabIndex2);
}
int n = tabCharOffsets.size();
for (tabIndex2++; tabIndex2 < n; tabIndex2++) {
if (realLength + tabSum2 - tabSum1 < logicalLength) {
int nextTabOffset = tabCharOffsets.get(tabIndex2);
tabSum2 = tabLengthSums.get(tabIndex2);
if (nextTabOffset - offset + tabSum2 - tabSum1 > logicalLength) {
tabSum2 = (tabIndex2 > 0) ? tabLengthSums.get(tabIndex2 - 1) : 0;
realLength = logicalLength + tabSum1 - tabSum2;
if (realLength > (nextTabOffset - offset)) {
realLength = nextTabOffset - offset;
}
break;
} else {
realLength = nextTabOffset - offset;
}
} else {
if (tabShiftPtr != null) {
tabShiftPtr[0] = realLength + tabSum2 - tabSum1 - logicalLength;
}
break;
}
}
if (realLength < 0) realLength = 0;
return realLength;
}
}
public int getNumLogicalChars(int offset, int physicalLength) {
synchronized (readLock()) {
int tabIndex1 = tabCharOffsets.findNearest(offset);
int tabSum1;
if (tabIndex1 >= 0 && tabCharOffsets.get(tabIndex1) < offset) {
tabSum1 = tabLengthSums.get(tabIndex1);
} else if (tabIndex1 > 0) {
tabSum1 = tabLengthSums.get(tabIndex1 - 1);
} else {
tabSum1 = 0;
}
int tabIndex2 = tabCharOffsets.findNearest(offset + physicalLength - 1);
if (tabIndex2 < 0) {
return physicalLength;
}
int tabSum2 = tabLengthSums.get(tabIndex2);
return physicalLength + tabSum2 - tabSum1;
}
}
private void registerLineWithListener(int line, LineInfo info, boolean important) {
lineWithListenerToInfo.put(line, info);
if (important) {
importantLines.add(line);
}
}
private IntList importantLines = new IntList(16);
public int firstImportantListenerLine() {
return importantLines.size() == 0 ? -1 : importantLines.get(0);
}
public boolean isImportantLine(int line) {
return importantLines.contains(line);
}
/**
* We use SparseIntList to create a cache which only actually holds
* the counts for lines that *are* wrapped, and interpolates the rest, so
* we don't need to create an int[] as big as the number of lines we have.
* This presumes that most lines don't wrap.
*/
private void calcLogicalLineCount(int width) {
synchronized (readLock()) {
int lineCount = getLineCount();
knownLogicalLineCounts = new SparseIntList(30);
int val = 0;
for (int i = 0; i < lineCount; i++) {
if (!isVisible(i)) {
knownLogicalLineCounts.add(i, val);
continue;
}
int len = lengthWithTabs(i);
if (len > width) {
val += lengthToLineCount(len, width);
knownLogicalLineCounts.add(i, val);
} else {
val++;
}
}
knownCharsPerLine = width;
}
}
static int lengthToLineCount(int len, int charsPerLine) {
return len > charsPerLine ? (charsPerLine == 0 ? len : (len + charsPerLine - 1) / charsPerLine) : 1;
}
void markDirty() {
dirty = true;
}
boolean isLastLineFinished() {
return lastLineFinished;
}
private boolean lastLineFinished = true;
private int lastLineLength = -1;
private int lastCharLengthWithTabs = -1;
// lineLength with tabs
private void updateLastLine(int lineIdx, int lineLength) {
synchronized (readLock()) {
longestLineLen = Math.max(longestLineLen, lineLength);
if (knownLogicalLineCounts == null) {
return;
}
if (currentFoldStart >= 0 && (visibleList.get(currentFoldStart) == 0
|| !isVisible(currentFoldStart))) {
if (knownLogicalLineCounts.lastIndex() != lineIdx) {
knownLogicalLineCounts.add(lineIdx,
knownLogicalLineCounts.get(lineIdx - 1));
}
return;
};
// nummber of logical lines above for knownLogicalLineCounts
int aboveLineCount;
boolean alreadyAdded = knownLogicalLineCounts.lastIndex() == lineIdx;
if (alreadyAdded) {
assert lastLineFinished == false;
if (lineLength <= knownCharsPerLine) {
knownLogicalLineCounts.removeLast();
} else {
aboveLineCount = knownLogicalLineCounts.lastAdded()
- lengthToLineCount(lastCharLengthWithTabs, knownCharsPerLine)
+ lengthToLineCount(lineLength, knownCharsPerLine);
knownLogicalLineCounts.updateLast(lineIdx, aboveLineCount);
}
} else {
if (lineLength <= knownCharsPerLine) {
return;
}
if (knownLogicalLineCounts.lastIndex() != -1) {
//If the cache already has some entries, calculate the
//values from the last entry - this is less expensive
//than looking it up
aboveLineCount = (lineIdx - (knownLogicalLineCounts.lastIndex() + 1)) + knownLogicalLineCounts.lastAdded();
} else {
//Otherwise, it's just the number of lines above this
//one - it's the first entry
aboveLineCount = Math.max(0, lineIdx-1);
}
//Add in the number of times this getLine will wrap
aboveLineCount += lengthToLineCount(lineLength, knownCharsPerLine);
knownLogicalLineCounts.add(lineIdx, aboveLineCount);
}
}
}
public void lineUpdated(int lineStart, int lineLength, int charLengthWithTabs, boolean isFinished) {
synchronized (readLock()) {
int charLineLength = toCharIndex(lineLength);
if (isFinished) {
charLineLength -= 1;
}
int lineIndex = lineStartList.size() - 1;
updateLastLine(lineIndex, charLengthWithTabs);
if (isFinished) {
lineStartList.add(lineStart + lineLength);
updateFolds(lineIndex);
lineCharLengthListWithTabs.add(charLengthWithTabs);
}
lastLineFinished = isFinished;
lastLineLength = isFinished ? -1 : charLineLength;
lastCharLengthWithTabs = isFinished ? -1 : charLengthWithTabs;
}
markDirty();
}
/**
* Update data structures with info about folds. Called after a new line is
* finished.
*/
private void updateFolds(int lineIndex) {
if (currentFoldStart >= visibleList.size()) {
LOG.log(Level.FINE, "currentFoldStart = {0}, visibleList" //NOI18N
+ ".size() = {1}: Forgetting current fold.", //NOI18N
new Object[]{currentFoldStart, visibleList.size()});
currentFoldStart = -1; // #229544, caused e.g. by invalid FoldHandle
}
if (currentFoldStart == -1) {
foldOffsets.add(0);
} else {
foldOffsets.add(lineIndex - currentFoldStart);
}
if (currentFoldStart != -1 && (visibleList.get(currentFoldStart) == 0
|| !isVisible(currentFoldStart))) {
hiddenLines++;
realToVisibleLine.add(-1);
} else {
visibleToRealLine.add(lineIndex);
realToVisibleLine.add(lineIndex - hiddenLines);
}
visibleList.add(1);
}
void setCurrentFoldStart(int foldStart) {
synchronized (readLock()) {
if (foldStart > -1 && foldStart + 1 < foldOffsets.size()
&& foldOffsets.get(foldStart + 1) != 1) {
LOG.log(Level.FINE, "Ignoring currentFoldStart at " //NOI18N
+ "{0}, because foldOffset at {1} is {2}", //NOI18N
new Object[]{foldStart, foldStart + 1, foldOffsets.get(
foldStart + 1)});
this.currentFoldStart = -1;
} else if (foldStart >= foldOffsets.size()) {
LOG.log(Level.FINE, "Ignoring currentFoldStart at " //NOI18N
+ "{0}, foldOffsets.size() is only {1}", //NOI18N
new Object[]{foldStart, foldOffsets.size()});
this.currentFoldStart = -1;
} else {
this.currentFoldStart = foldStart;
}
}
}
IntListSimple getFoldOffsets() {
return this.foldOffsets;
}
/** Convert an index from chars to byte count (*2). Simple math, but it
* makes the intent clearer when encountered in code */
static int toByteIndex (int charIndex) {
return charIndex << 1;
}
/** Convert an index from bytes to chars (/2). Simple math, but it
* makes the intent clearer when encountered in code */
static int toCharIndex (int byteIndex) {
assert byteIndex % 2 == 0 : "bad index: " + byteIndex; //NOI18N
return byteIndex >> 1;
}
public void saveAs(String path) throws IOException {
Storage storage = getStorage();
if (storage == null) {
throw new IOException ("Data has already been disposed"); //NOI18N
}
FileOutputStream fos = new FileOutputStream(path);
try {
String encoding = System.getProperty ("file.encoding"); //NOI18N
if (encoding == null) {
encoding = "UTF-8"; //NOI18N
}
Charset charset = Charset.forName (encoding); //NOI18N
CharsetEncoder encoder = charset.newEncoder ();
String ls = System.getProperty("line.separator");
FileChannel ch = fos.getChannel();
ByteBuffer lsbb = encoder.encode(CharBuffer.wrap(ls));
for (int i = 0; i < getLineCount(); i++) {
int lineStart = getCharLineStart(i);
int lineLength = length(i);
BufferResource<CharBuffer> br = getCharBuffer(lineStart,
lineLength);
try {
CharBuffer cb = br.getBuffer();
ByteBuffer bb = encoder.encode(cb);
ch.write(bb);
if (i != getLineCount() - 1) {
lsbb.rewind();
ch.write(lsbb);
}
} finally {
if (br != null) {
br.releaseBuffer();
}
}
}
ch.close();
} finally {
fos.close();
FileUtil.refreshFor(new java.io.File(path));
}
}
/** initial default colors */
private static Color[] DEF_COLORS = null;
/** current default colors */
Color[] curDefColors;
static Color[] getDefColors() {
if (DEF_COLORS != null) {
return DEF_COLORS;
}
return DEF_COLORS = new Color[]{
OutputOptions.getDefault().getColorStandard(),
OutputOptions.getDefault().getColorError(),
OutputOptions.getDefault().getColorLink(),
OutputOptions.getDefault().getColorLinkImportant(),
OutputOptions.getDefault().getColorInput(),
};
}
public void setDefColor(IOColors.OutputType type, Color color) {
curDefColors[type.ordinal()] = color;
}
public Color getDefColor(IOColors.OutputType type) {
return curDefColors[type.ordinal()];
}
public LineInfo getLineInfo(int line) {
synchronized (readLock()) {
LineInfo info = (LineInfo) linesToInfos.get(line);
if (info != null) {
int lineLength = length(line);
if (lineLength > info.getEnd()) {
// This is an input
info.addSegment(lineLength, OutputKind.IN, null, null, null, false);
}
return info;
} else {
// The last line can contain input
if (line == getLineCount() - 1) {
LineInfo li = new LineInfo(this);
li.addSegment(getLineLength(line), OutputKind.OUT, null, null, null, false);
li.addSegment(length(line), OutputKind.IN, null, null, null, false);
return li;
} else {
return new LineInfo(this, length(line));
}
}
}
}
public LineInfo getExistingLineInfo(int line) {
return (LineInfo) linesToInfos.get(line);
}
private static final int MAX_FIND_SIZE = 16*1024;
private Pattern pattern;
private boolean regExpChanged(String pattern, boolean matchCase) {
return this.pattern != null && (!this.pattern.toString().equals(pattern) || (this.pattern.flags() == Pattern.CASE_INSENSITIVE) == matchCase);
}
public int[] find(int start, String pattern, boolean regExp, boolean matchCase) {
Storage storage = getStorage();
if (storage == null) {
return null;
}
if (regExp && regExpChanged(pattern, matchCase)) {
this.pattern = null;
}
if (!regExp && !matchCase) {
pattern = pattern.toLowerCase();
}
while (true) {
BufferResource<ByteBuffer> br = null;
int size = getCharCount() - start;
if (size > MAX_FIND_SIZE) {
int l = getLineAt(start + MAX_FIND_SIZE);
size = getLineStart(l) + length(l) - start;
} else if (size <= 0) {
break;
}
CharBuffer buff = null;
try {
try {
br = storage.getReadBuffer(toByteIndex(start), toByteIndex(size));
buff = br.getBuffer().asCharBuffer();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
if (buff == null) {
break;
}
if (regExp) {
if (this.pattern == null) {
this.pattern = matchCase
? Pattern.compile(pattern)
: Pattern.compile(pattern,
Pattern.CASE_INSENSITIVE);
}
Matcher matcher = this.pattern.matcher(buff);
if (matcher.find()) {
return new int[]{start + matcher.start(), start + matcher.end()};
}
} else {
int idx = matchCase ? buff.toString().indexOf(pattern)
: buff.toString().toLowerCase().indexOf(pattern);
if (idx != -1) {
return new int[]{start + idx, start + idx + pattern.length()};
}
}
start += buff.length();
} finally {
if (br != null) {
br.releaseBuffer();
}
}
}
return null;
}
public int[] rfind(int start, String pattern, boolean regExp, boolean matchCase) {
Storage storage = getStorage();
if (storage == null) {
return null;
}
if (regExp && regExpChanged(pattern, matchCase)) {
this.pattern = null;
}
if (!regExp && !matchCase) {
pattern = pattern.toLowerCase();
}
while (true) {
int end = start;
start = end - MAX_FIND_SIZE;
if (start < 0) {
start = 0;
} else {
int l = getLineAt(start);
start = getLineStart(l);
}
if (start == end) {
break;
}
BufferResource<ByteBuffer> br = null;
try {
CharBuffer buff = null;
try {
br = storage.getReadBuffer(toByteIndex(start), toByteIndex(end - start));
buff = br.getBuffer().asCharBuffer();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
if (buff == null) {
break;
}
if (regExp) {
if (this.pattern == null) {
this.pattern = matchCase
? Pattern.compile(pattern)
: Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
}
Matcher matcher = this.pattern.matcher(buff);
int mStart = -1;
int mEnd = -1;
while (matcher.find()) {
mStart = matcher.start();
mEnd = matcher.end();
}
if (mStart != -1) {
return new int[]{start + mStart, start + mEnd};
}
} else {
int idx = matchCase ? buff.toString().lastIndexOf(pattern)
: buff.toString().toLowerCase().lastIndexOf(pattern);
if (idx != -1) {
return new int[]{start + idx, start + idx + pattern.length()};
}
}
} finally {
if (br != null) {
br.releaseBuffer();
}
}
}
return null;
}
@Override
public String toString() {
return lineStartList.toString();
}
private int addSegment(CharSequence s, int offset, int lineIdx, int pos, OutputListener l, boolean important, OutputKind outKind, Color c, Color b) {
int len = length(lineIdx);
if (len > 0) {
LineInfo info = (LineInfo) linesToInfos.get(lineIdx);
if (info == null) {
info = new LineInfo(this);
linesToInfos.put(lineIdx, info);
}
int curEnd = info.getEnd();
if (pos > 0 && pos != curEnd) {
info.addSegment(pos, OutputKind.OUT, null, null, null, false);
curEnd = pos;
}
if (l != null) {
int endPos = Math.min(curEnd + s.length() - offset, len);
int strlen = Math.min(s.length(), offset + len);
if (s.charAt(strlen - 1) == '\n') {
strlen--;
}
if (s.charAt(strlen - 1) == '\r') {
strlen--;
}
int leadingCnt = 0;
while (leadingCnt + offset < strlen && Character.isWhitespace(s.charAt(offset + leadingCnt))) {
leadingCnt++;
}
int trailingCnt = 0;
if (leadingCnt != strlen) {
while (trailingCnt < strlen && Character.isWhitespace(s.charAt(strlen - trailingCnt - 1))) {
trailingCnt++;
}
}
if (leadingCnt > 0) {
if (info.segments.size() > 0) {
// do not underline leading spaces only in the first segment
info.addSegment(curEnd + leadingCnt, outKind, l, c, b, important);
} else {
info.addSegment(curEnd + leadingCnt, OutputKind.OUT, null, null, null, false);
}
}
info.addSegment(endPos - trailingCnt, outKind, l, c, b, important);
if (trailingCnt > 0) {
// have to underline all trailing spaces (we cannot know if there are more segments)
// TODO: do not underline trailing spaces of the last segment
info.addSegment(endPos, outKind, l, c, b, important);
}
registerLineWithListener(lineIdx, info, important);
} else {
info.addSegment(len, outKind, l, c, b, important);
if (important) {
importantLines.add(lineIdx);
}
}
}
return len;
}
void updateLinesInfo(CharSequence s, int startLine, int startPos, OutputListener l, boolean important, OutputKind outKind, Color c, Color b) {
int offset = 0;
/* If it's necessary to translate tabs to spaces, use this.
* But it seems that it works fine without the translation. Translation breaks character indexes.
CharSequence noTabsStr = s;
if (l != null) {
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '\t') {
StringBuilder str = new StringBuilder(s.length() + 100);
int start = 0;
for (int k = i; k < s.length(); k++) {
if (s.charAt(k) == '\t') {
str.append(s, start, k);
int tabLength;
synchronized (readLock()) {
int tabIndex = tabCharOffsets.findNearest(startPos + k);
tabLength = getTabLength(tabIndex);
}
while (tabLength-- > 0) {
str.append(' ');
}
start = k + 1;
}
}
str.append(s, start, s.length());
noTabsStr = str;
break;
}
}
}
*/
synchronized (readLock()) {
int startLinePos = startPos - getLineStart(startLine);
for (int i = startLine; i < getLineCount(); i++) {
offset += addSegment(s, offset, i, startLinePos, l, important, outKind, c, b) + 1;
startLinePos = 0;
}
}
}
void addLineInfo(int idx, LineInfo info, boolean important) {
synchronized (readLock()) {
linesToInfos.put(idx, info);
if (!info.getListeners().isEmpty()) {
registerLineWithListener(idx, info, important);
}
}
}
private int getTabLength(int i) {
if (i == 0) {
return tabLengthSums.get(0);
} else {
return tabLengthSums.get(i) - tabLengthSums.get(i-1) + 1;
}
}
int checkLimits() {
synchronized (readLock()) {
int lines = getLineCount();
int chars = getCharCount();
int infos = linesToInfos.size();
if (lines >= outputLimits.getMaxLines()
|| chars >= outputLimits.getMaxChars()
|| infos >= 524288) { // #239445
LOG.log(Level.INFO, "Removing old lines: lines: {0}," //NOI18N
+ " chars: {1}, infos: {2}", new Object[]{ //NOI18N
lines, chars, infos});
return removeOldLines();
} else {
return 0;
}
}
}
/**
* Tell the storage that oldest bytes can be forgotten, and update all data
* structures.
*/
private int removeOldLines() {
int newFirstLine = Math.min(outputLimits.getRemoveLines(),
lineStartList.size() / 2);
int firstByteOffset = lineStartList.get(newFirstLine);
lineStartList.compact(newFirstLine, firstByteOffset);
lineCharLengthListWithTabs.compact(newFirstLine, 0);
lineWithListenerToInfo.decrementKeys(newFirstLine);
linesToInfos.decrementKeys(newFirstLine);
int firstCharOffset = toCharIndex(firstByteOffset);
int firstTabIndex = tabCharOffsets.findNearest(firstCharOffset);
tabCharOffsets.compact(Math.max(0, firstTabIndex), firstCharOffset);
if (firstTabIndex > 0) {
tabLengthSums.compact(firstTabIndex,
tabLengthSums.get(firstTabIndex - 1));
}
int firstImportantLine = importantLines.findNearest(newFirstLine);
importantLines.compact(Math.max(0, firstImportantLine), newFirstLine);
knownLogicalLineCounts = null;
foldOffsets.compact(newFirstLine, 0);
int foIndex = 0;
while (foIndex < foldOffsets.size() && foldOffsets.get(foIndex) != 0) {
foldOffsets.set(foIndex, 0);
foIndex++;
}
visibleList.compact(newFirstLine, 0);
visibleList.set(0, 1);
realToVisibleLine.compact(newFirstLine, 0);
currentFoldStart = Math.max(-1, currentFoldStart - newFirstLine);
recomputeRealToVisibleLine();
updateVisibleToRealLines(0);
getStorage().shiftStart(firstByteOffset);
fire();
return firstByteOffset;
}
private void recomputeRealToVisibleLine() {
int currentVisibleLine = 0;
int currentHiddenLineCount = 0;
int hiddenFoldStart = -1;
for (int i = 0; i < realToVisibleLine.size(); i++) {
if (hiddenFoldStart == -1 && visibleList.get(i) == 0) {
hiddenFoldStart = i;
realToVisibleLine.set(i, currentVisibleLine++);
} else if (hiddenFoldStart > -1 &&foldOffsets.get(i) > 0
&& foldOffsets.get(i) <= i - hiddenFoldStart) {
realToVisibleLine.set(i, -1);
currentHiddenLineCount++;
} else {
realToVisibleLine.set(i, currentVisibleLine++);
hiddenFoldStart = -1;
}
}
hiddenLines = currentHiddenLineCount;
}
/**
* Redefine output limits. Can be called from test cases.
*/
void setOutputLimits(OutputLimits outputLimits) {
this.outputLimits = outputLimits;
}
void addTabAt(int i, int tabLength) {
tabLength--; // substract the tab character as such, to have the extra length
synchronized (readLock()) {
LOG.log(Level.FINEST, "addTabAt: i = {0}", i); // #201450 //NOI18N
tabCharOffsets.add(i);
int n = tabLengthSums.size();
if (n > 0) {
tabLength += tabLengthSums.get(n - 1);
}
tabLengthSums.add(tabLength);
}
}
/**
* Remove last tab.
*
* @return Size of the last tab (characters).
*/
int removeLastTab() {
synchronized (readLock()) {
LOG.log(Level.FINEST, "removeLastTabAt");
int size = tabLengthSums.size();
if (size == 0) {
return 0;
}
tabCharOffsets.shorten(tabCharOffsets.size() - 1);
int tabLen;
if (size > 1) {
tabLen = tabLengthSums.get(size - 1) - tabLengthSums.get(size - 2);
} else {
tabLen = tabLengthSums.get(0);
}
tabLengthSums.shorten(size - 1);
return tabLen;
}
}
private boolean isFoldStartValid(int foldStartIndex) {
return foldStartIndex >= 0 && foldStartIndex < foldOffsets.size();
}
/**
* Character buffer resource for a Byte buffer resource. At most one
* CharBufferResource can exists for a ByteBufferResource.
*/
private class CharBufferResource implements BufferResource<CharBuffer> {
private BufferResource<ByteBuffer> parentResource;
private CharBuffer cb;
public CharBufferResource(BufferResource<ByteBuffer> parentResource) {
this.parentResource = parentResource;
this.cb = parentResource.getBuffer().asCharBuffer();
}
@Override
public CharBuffer getBuffer() {
return cb;
}
@Override
public void releaseBuffer() {
cb = null;
parentResource.releaseBuffer();
}
}
@Override
public void showFold(int foldStartIndex) {
synchronized (readLock()) {
if (isFoldStartValid(foldStartIndex)) {
setFoldExpanded(foldStartIndex, true);
}
}
}
@Override
public void hideFold(int foldStartIndex) {
synchronized (readLock()) {
if (isFoldStartValid(foldStartIndex)) {
setFoldExpanded(foldStartIndex, false);
}
}
}
/**
* @param foldStartIndex Real index of the first line of the fold.
*/
private void setFoldExpanded(int foldStartIndex, boolean expanded) {
synchronized (readLock()) {
if (visibleList.get(foldStartIndex) == (expanded ? 1 : 0)) {
return;
}
visibleList.set(foldStartIndex, expanded ? 1 : 0);
if (!isVisible(foldStartIndex)) {
return; // No need to recompute any mapping.
}
int len = foldLength(foldStartIndex);
if (len > 0) {
int changed = updateRealToVisibleIndexesInFold(foldStartIndex,
len, expanded);
for (int i = foldStartIndex + len + 1;
i < realToVisibleLine.size(); i++) {
int currentValue = realToVisibleLine.get(i);
if (currentValue != -1) {
realToVisibleLine.set(i,
currentValue + (expanded ? changed : -changed));
}
}
hiddenLines += expanded ? -changed : changed;
updateVisibleToRealLines(foldStartIndex);
foldVisibilityUpdated();
}
}
}
@Override
public void hideAllFolds() {
synchronized (readLock()) {
int currentVisibleLine = 0;
int currentHiddenLineCount = 0;
for (int i = 0; i < foldOffsets.size(); i++) {
boolean isFoldStart = (i + 1 < foldOffsets.size()
&& foldOffsets.get(i + 1) == 1);
if (isFoldStart) {
visibleList.set(i, 0);
}
if (foldOffsets.get(i) > 0) {
realToVisibleLine.set(i, -1);
currentHiddenLineCount++;
} else {
realToVisibleLine.set(i, currentVisibleLine);
currentVisibleLine++;
}
}
hiddenLines = currentHiddenLineCount;
updateVisibleToRealLines(0);
foldVisibilityUpdated();
}
}
@Override
public void showAllFolds() {
synchronized (readLock()) {
for (int i = 0; i < foldOffsets.size(); i++) {
boolean isFoldStart = (i + 1 < foldOffsets.size()
&& foldOffsets.get(i + 1) == 1);
if (isFoldStart) {
visibleList.set(i, 1);
}
realToVisibleLine.set(i, i);
}
hiddenLines = 0;
updateVisibleToRealLines(0);
foldVisibilityUpdated();
}
}
@Override
public void showFoldTree(int foldStartIndex) {
synchronized (readLock()) {
if (isFoldStartValid(foldStartIndex)) {
setFoldTreeExpanded(foldStartIndex, true);
}
}
}
@Override
public void showFoldAndParentFolds(int foldStartIndex) {
synchronized (readLock()) {
int foldOffset = getFoldOffsets().get(foldStartIndex);
if (foldOffset > 0) {
int parentFoldStart = foldStartIndex - foldOffset;
if (parentFoldStart >= 0) {
showFoldAndParentFolds(parentFoldStart);
}
}
showFold(foldStartIndex);
}
}
@Override
public void showFoldsForLine(int realLineIndex) {
synchronized (readLock()) {
int foldStart = getFoldStart(realLineIndex);
showFoldAndParentFolds(foldStart);
}
}
@Override
public void hideFoldTree(int foldStartIndex) {
synchronized (readLock()) {
if (isFoldStartValid(foldStartIndex)) {
setFoldTreeExpanded(foldStartIndex, false);
}
}
}
private void setFoldTreeExpanded(int foldStartIndex, boolean expanded) {
int visibleValue = expanded ? 1 : 0;
synchronized (readLock()) {
assert isVisible(foldStartIndex);
visibleList.set(foldStartIndex, visibleValue);
int visibleFoldStartIndex = realToVisibleLine.get(foldStartIndex);
int delta = 0; // difference in count of visible lines
int lastUpdatedIndex = Integer.MAX_VALUE - 1;
for (int i = foldStartIndex + 1; i < foldOffsets.size(); i++) {
int offset = i - foldStartIndex;
if (foldOffsets.get(i) == 0 || foldOffsets.get(i) > offset) {
break;
}
if (i + 1 < foldOffsets.size() && foldOffsets.get(i + 1) == 1) {
visibleList.set(i, visibleValue);
}
int visibleIndex = realToVisibleLine.get(i);
if ((expanded && visibleIndex < 0)
|| (!expanded && visibleIndex >= 0)) {
delta += expanded ? 1 : -1;
}
realToVisibleLine.set(i,
expanded ? visibleFoldStartIndex + offset : -1);
lastUpdatedIndex = i;
}
for (int i = lastUpdatedIndex + 1; i < realToVisibleLine.size(); i++) {
realToVisibleLine.set(i, realToVisibleLine.get(i) + delta);
}
hiddenLines -= delta;
updateVisibleToRealLines(foldStartIndex);
foldVisibilityUpdated();
}
}
private void foldVisibilityUpdated() {
if (knownCharsPerLine > 0) {
calcLogicalLineCount(knownCharsPerLine);
}
markDirty();
delayedFire();
}
/**
* Update realToVisibleLine indexes in a fold. Handle nested folds, keep
* their expanded/collapsed state.
*
* @param foldStart Real index of the first line of the fold.
* @param len Total length of the fold, including nested folds
* @param expanded True to expand the fold, false to collapse the fold.
*
* @return Number of newly hidden of shown lines.
*/
private int updateRealToVisibleIndexesInFold(
int foldStart, int len, boolean expanded) {
int changed = 0;
int nestedHidden = -1; // start index of currently processed hidden fold
for (int i = 0; i < len; i++) {
int lineIndex = foldStart + i + 1;
int foldOffset = foldOffsets.get(lineIndex);
if (i > 0 && foldOffset == 1 && visibleList.get(lineIndex - 1) == 0
&& nestedHidden == -1) {
// a nested hidden fold encountered
nestedHidden = lineIndex - 1;
} else if (foldOffset <= i + 1 && (nestedHidden == -1
|| foldOffset > lineIndex - nestedHidden)) {
assert !expanded || realToVisibleLine.get(lineIndex) == -1;
assert expanded || realToVisibleLine.get(lineIndex) != -1;
nestedHidden = -1;
changed++;
realToVisibleLine.set(lineIndex, expanded
? realToVisibleLine.get(foldStart) + changed
: -1); // invisible: -1
} else if (foldOffset <= len + 1 && nestedHidden != -1) {
assert foldOffset <= lineIndex - nestedHidden;
} else {
assert false : "Only nested fold expected"; //NOI18N
}
}
return changed;
}
private void updateVisibleToRealLines(int fromRealIndex) {
for (int i = fromRealIndex; i < getLineCount() - 1; i++) {
int visibleLine = realToVisibleLine.get(i);
if (visibleLine == -1) {
continue;
}
if (visibleLine >= visibleToRealLine.size()) {
visibleToRealLine.add(i);
} else {
visibleToRealLine.set(visibleLine, i);
}
}
if (visibleToRealLine.size() > realToVisibleLine.size() - hiddenLines) {
visibleToRealLine.shorten(realToVisibleLine.size() - hiddenLines);
}
}
@Override
public int visibleToRealLine(int visibleLineIndex) {
synchronized (readLock()) {
if (visibleLineIndex >= visibleToRealLine.size()) {
return visibleLineIndex + hiddenLines;
} else if (visibleLineIndex < 0) {
return visibleLineIndex;
}
return visibleToRealLine.get(visibleLineIndex);
}
}
@Override
public int realToVisibleLine(int realLineIndex) {
synchronized (readLock()) {
if (realLineIndex >= realToVisibleLine.size()) {
return realLineIndex - hiddenLines;
}
return realToVisibleLine.get(realLineIndex);
}
}
/**
* @param lineIndex Real line index.
*/
@Override
public boolean isVisible(int lineIndex) {
synchronized (readLock()) {
if (lineIndex >= foldOffsets.size()) {
return true;
}
int fo = foldOffsets.get(lineIndex);
if (fo == 0) {
return true;
}
int parentFold = lineIndex - foldOffsets.get(lineIndex);
while (parentFold >= 0) {
if (visibleList.get(parentFold) == 0) {
return false;
} else {
fo = foldOffsets.get(parentFold);
if (fo == 0) {
break;
} else {
parentFold = parentFold - fo;
}
}
}
return true;
}
}
/**
* @param foldStart Real fold start index.
*/
int foldLength(int foldStart) {
int i = foldStart + 1;
while (i < foldOffsets.size()
&& foldOffsets.get(i) > 0
&& foldOffsets.get(i) <= i - foldStart) {
i++;
}
return i - foldStart - 1;
}
@Override
public int getVisibleLineCount() {
synchronized (readLock()) {
return getLineCount() - hiddenLines;
}
}
@Override
public int getFoldStart(int realLineIndex) {
synchronized (readLock()) {
if (realLineIndex + 1 < foldOffsets.size()
&& foldOffsets.get(realLineIndex + 1) == 1) {
return realLineIndex;
} else if (realLineIndex < 0
|| realLineIndex >= foldOffsets.size()) {
return Math.max(0, realLineIndex);
} else {
return realLineIndex - foldOffsets.get(realLineIndex);
}
}
}
@Override
public int getParentFoldStart(int realLineIndex) {
synchronized (readLock()) {
if (realLineIndex < 0 || realLineIndex >= foldOffsets.size()) {
return -1;
} else {
int offset = foldOffsets.get(realLineIndex);
if (offset > 0) {
int result = realLineIndex - offset;
return result >= 0 ? result : -1;
} else {
return -1;
}
}
}
}
/**
* @param length Value 1 for last character, -1 for the whole line.
*/
@Override
public Pair<Integer, Integer> removeCharsFromLastLine(int length) {
synchronized (readLock()) {
if (lastLineFinished) {
return Pair.of(0, 0);
} else {
String lastLine;
try {
lastLine = getLine(getLineCount() - 1);
} catch (IOException e) {
LOG.log(Level.INFO, null, e);
return Pair.of(0, 0);
}
if (lastLine.isEmpty()) {
return Pair.of(0, 0);
} else {
int removed;
if (length < 0) {
removed = lastLine.length();
} else {
removed = Math.max(length, lastLine.length());
}
int tabs = 0;
for (int i = 0; i < removed; i++) {
if (lastLine.charAt(lastLine.length() - 1 - i) == '\t') {
tabs += removeLastTab();
}
}
lastLineLength -= removed;
return Pair.of(removed, tabs);
}
}
}
}
}