| /* |
| * 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); |
| } |
| } |
| } |
| } |
| } |