blob: ced6e8c1c95d764f710f10002bf3bfeaff29686a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2019 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Paul Pazderski - Contributions for:
* Bug 547064: use binary search for getPartition
* Bug 548356: fixed user input handling
* Bug 550618: getStyleRanges produced invalid overlapping styles
* Bug 550621: Implementation of IConsoleDocumentPartitionerExtension
*******************************************************************************/
package org.eclipse.ui.internal.console;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitionerExtension;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.TypedRegion;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsoleDocumentPartitioner;
import org.eclipse.ui.console.IConsoleDocumentPartitionerExtension;
import org.eclipse.ui.console.IOConsole;
import org.eclipse.ui.console.IOConsoleInputStream;
import org.eclipse.ui.console.IOConsoleOutputStream;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.progress.WorkbenchJob;
/**
* Partitions an IOConsole's document
*
* @since 3.1
*/
public class IOConsolePartitioner
implements IConsoleDocumentPartitioner, IConsoleDocumentPartitionerExtension, IDocumentPartitionerExtension {
/**
* If true validate partitioning after changes and do other additional
* assertions. Useful for developing/debugging.
*/
private static final boolean ASSERT = false;
/**
* Comparator to sort or search {@link IRegion}s by {@link IRegion#getOffset()}.
*/
private static final Comparator<IRegion> CMP_REGION_BY_OFFSET = Comparator.comparing(IRegion::getOffset);
private PendingPartition consoleClosedPartition;
/** The connected {@link IDocument} this partitioner manages. */
private IDocument document;
/**
* List of all partitions. Must always be sorted ascending by
* {@link IRegion#getOffset()} and not contain <code>null</code> or 0-length
* elements. (see also {@link #checkPartitions()})
*/
private ArrayList<IOConsolePartition> partitions;
/** Blocks of data that have not yet been appended to the document. */
private ArrayList<PendingPartition> pendingPartitions;
/**
* A list of PendingPartitions to be appended by the updateJob
*/
private ArrayList<PendingPartition> updatePartitions;
/**
* Job that appends pending partitions to the document.
*/
private QueueProcessingJob queueJob;
/** Job that trims console content if it exceeds {@link #highWaterMark}. */
private TrimJob trimJob = new TrimJob();
/** The input stream attached to this document. */
private IOConsoleInputStream inputStream;
/**
* Flag to indicate that the updateJob is updating the document.
*/
private boolean updateInProgress;
/**
* A list of partitions containing input from the console, that have not been
* appended to the input stream yet. No guarantees on element order.
*/
private ArrayList<IOConsolePartition> inputPartitions;
/**
* offset used by updateJob
*/
private int firstOffset;
/** An array of legal line delimiters. */
private String[] lld;
private int highWaterMark = -1;
private int lowWaterMark = -1;
private boolean connected = false;
/** The partitioned {@link IOConsole}. */
private IOConsole console;
/**
* Lock for appending to and removing from the document - used
* to synchronize addition of new text/partitions in the update
* job and handling buffer overflow/clearing of the console.
*/
private Object overflowLock = new Object();
private int fBuffer;
public IOConsolePartitioner(IOConsoleInputStream inputStream, IOConsole console) {
this.inputStream = inputStream;
this.console = console;
trimJob.setRule(console.getSchedulingRule());
}
/**
* Get partitioned document or <code>null</code> if none connected.
*
* @return partitioned document
*/
public IDocument getDocument() {
return document;
}
@Override
public void connect(IDocument doc) {
document = doc;
document.setDocumentPartitioner(this);
lld = document.getLegalLineDelimiters();
partitions = new ArrayList<>();
pendingPartitions = new ArrayList<>();
inputPartitions = new ArrayList<>();
queueJob = new QueueProcessingJob();
queueJob.setSystem(true);
queueJob.setPriority(Job.INTERACTIVE);
queueJob.setRule(console.getSchedulingRule());
connected = true;
}
public int getHighWaterMark() {
return highWaterMark;
}
public int getLowWaterMark() {
return lowWaterMark;
}
public void setWaterMarks(int low, int high) {
lowWaterMark = low;
highWaterMark = high;
ConsolePlugin.getStandardDisplay().asyncExec(this::checkBufferSize);
}
/**
* Notification from the console that all of its streams have been closed.
*/
public void streamsClosed() {
consoleClosedPartition = new PendingPartition(null, null);
synchronized (pendingPartitions) {
pendingPartitions.add(consoleClosedPartition);
}
queueJob.schedule(); //ensure that all pending partitions are processed.
}
@Override
public void disconnect() {
synchronized (overflowLock) {
document = null;
partitions.clear();
connected = false;
try {
inputStream.close();
} catch (IOException e) {
}
}
}
@Override
public void documentAboutToBeChanged(DocumentEvent event) {
}
@Override
public boolean documentChanged(DocumentEvent event) {
return documentChanged2(event) != null;
}
@Override
public String[] getLegalContentTypes() {
return new String[] { IOConsolePartition.OUTPUT_PARTITION_TYPE, IOConsolePartition.INPUT_PARTITION_TYPE };
}
@Override
public String getContentType(int offset) {
return getPartition(offset).getType();
}
@Override
public ITypedRegion[] computePartitioning(int offset, int length) {
return computeIOPartitioning(offset, length);
}
/**
* Same as {@link #computePartitioning(int, int)} but with more specific return
* type.
*
* @param offset the offset of the range of interest
* @param length the length of the range of interest
* @return the partitioning of the requested range (never <code>null</code>)
*/
private IOConsolePartition[] computeIOPartitioning(int offset, int length) {
return computePartitioning(offset, length, true, true);
}
/**
* Get partitioning for a given range with possibility to filter partitions by
* their read-only property.
*
* @param offset the offset of the range of interest
* @param length the length of the range of interest
* @param includeWritable if false writable partitions are skipped
* @param includeReadOnly if false read-only partitions are skipped
* @return the partitioning of the requested range (never <code>null</code>)
*/
private IOConsolePartition[] computePartitioning(int offset, int length, boolean includeWritable,
boolean includeReadOnly) {
final List<IOConsolePartition> result = new ArrayList<>();
synchronized (partitions) {
int index = findPartitionCandidate(offset);
if (index < 0) { // requested range starts before any known partition offset
index = 0; // so we start collecting at first known partition
}
final int end = offset + length;
for (; index < partitions.size(); index++) {
final IOConsolePartition partition = partitions.get(index);
if (partition.getOffset() >= end) {
break;
}
if ((includeWritable && !partition.isReadOnly()) || (includeReadOnly && partition.isReadOnly())) {
result.add(partition);
}
}
}
return result.toArray(new IOConsolePartition[0]);
}
@Override
public ITypedRegion getPartition(int offset) {
final ITypedRegion partition = getIOPartition(offset);
return partition != null ? partition : new TypedRegion(offset, 0, IOConsolePartition.INPUT_PARTITION_TYPE);
}
/**
* Like {@link #getPartition(int)} but returns <code>null</code> for
* unpartitioned or invalid offsets.
*
* @param offset the offset for which to determine the partition
* @return the partition containing this offset or <code>null</code> if offset
* is not partitioned
*/
private IOConsolePartition getIOPartition(int offset) {
synchronized (partitions) {
final int index = findPartitionCandidate(offset);
if (index >= 0) {
final IOConsolePartition partition = partitions.get(index);
if (partition.getOffset() + partition.getLength() > offset) {
return partition;
}
}
return null;
}
}
/**
* Search {@link #partitions} for the partition which is most likely containing
* the requested offset.
* <p>
* This (index + 1) can be used to insert a new partition with this offset. The
* resulting {@link #partitions} list is guaranteed to still be sorted. (as long
* as you do proper synchronization and consider concurrency problems)
* </p>
*
* @param offset the offset for which to determine the partition candidate
* @return index of partition element with partition offset closest to requested
* offset or <code>-1</code> if requested offset is lower than offset of
* any known partition
*/
private int findPartitionCandidate(int offset) {
final Region target = new Region(offset, 0);
final int index = Collections.binarySearch(partitions, target, CMP_REGION_BY_OFFSET);
if (index >= 0) {
// found partition whose offset equals the requested offset
return index;
}
// no exact offset match. Adjust index to point at partition which is closest to
// requested offset but whose offset is still lower than requested offset.
// Results in -1 if all known offsets are greater.
return (-index) - 2;
}
/**
* Enforces the buffer size.
* When the number of lines in the document exceeds the high water mark, the
* beginning of the document is trimmed until the number of lines equals the
* low water mark.
*/
private void checkBufferSize() {
if (document != null && highWaterMark > 0) {
int length = document.getLength();
if (length > highWaterMark) {
if (trimJob.getState() == Job.NONE) { //if the job isn't already running
trimJob.setOffset(length - lowWaterMark);
trimJob.schedule();
}
}
}
}
/**
* Clears the console
*/
public void clearBuffer() {
synchronized (overflowLock) {
trimJob.setOffset(-1);
trimJob.schedule();
}
}
@Override
public IRegion documentChanged2(DocumentEvent event) {
if (document == null) {
return null; //another thread disconnected the partitioner
}
if (document.getLength() == 0) { // document cleared
synchronized (partitions) {
partitions.clear();
inputPartitions.clear();
}
return new Region(0, 0);
}
if (updateInProgress) {
synchronized(partitions) {
if (updatePartitions != null) {
IOConsolePartition lastPartition = getPartitionByIndex(partitions.size() - 1);
for (PendingPartition pp : updatePartitions) {
if (pp == consoleClosedPartition) {
continue;
}
int ppLen = pp.text.length();
if (lastPartition != null && lastPartition.getOutputStream() == pp.stream) {
int len = lastPartition.getLength();
lastPartition.setLength(len + ppLen);
} else {
IOConsolePartition partition = new IOConsolePartition(firstOffset, pp.stream);
partition.setLength(ppLen);
lastPartition = partition;
partitions.add(partition);
}
firstOffset += ppLen;
}
}
}
} else {
synchronized (partitions) {
return applyUserInput(event);
}
}
return new Region(event.fOffset, event.fText.length());
}
/**
* Update partitioning due to document change. All document change events not
* triggered by this partitioner are considered user input and therefore
* partitioned as input.
* <p>
* This method does not care if the document event removed or replaced parts of
* read-only partitions. It assumes manipulating read-only partitions is valid
* or is blocked before this method is used.
* </p>
*
* @param event the event describing the document change
* @return the region of the document in which the partition type changed or
* <code>null</code>
*/
private IRegion applyUserInput(DocumentEvent event) {
final int eventTextLength = event.getText() != null ? event.getText().length() : 0;
final int offset = event.getOffset();
final int amountDeleted = event.getLength();
if (amountDeleted == 0 && eventTextLength == 0) {
// event did not changed document
return null;
}
final int eventPartitionIndex = findPartitionCandidate(offset);
int lastPartitionWithValidOffset = eventPartitionIndex;
if (amountDeleted > 0 && eventPartitionIndex >= 0) {
// adjust length of all partitions affected by replace/remove event
int toDelete = amountDeleted;
for (int i = eventPartitionIndex; i < partitions.size() && toDelete > 0; i++) {
final IOConsolePartition partition = partitions.get(i);
final int removeLength = Math.min(partition.getLength(), toDelete);
partition.setLength(partition.getLength() - removeLength);
toDelete -= removeLength;
}
if (ASSERT) {
Assert.isTrue(toDelete == 0, "Tried to delete outside partitioned range."); //$NON-NLS-1$
}
lastPartitionWithValidOffset--; // update one more since first affected partition may be empty now
}
if (eventTextLength > 0) {
// find best partition for event text
int inputPartitionIndex = eventPartitionIndex;
IOConsolePartition inputPartition = getPartitionByIndex(inputPartitionIndex);
if (inputPartition != null && inputPartition.isReadOnly() && offset == inputPartition.getOffset()) {
// if we could not reuse partition at event offset we may append the partition
// right before our event offset (e.g. if input is at end of document)
inputPartitionIndex--;
lastPartitionWithValidOffset--;
inputPartition = getPartitionByIndex(inputPartitionIndex);
}
// process event text in parts split on line delimiters
int textOffset = 0;
while (textOffset < eventTextLength) {
final int[] result = TextUtilities.indexOf(lld, event.getText(), textOffset);
final boolean foundNewline = result[1] >= 0;
final int newTextOffset = foundNewline ? result[0] + lld[result[1]].length() : eventTextLength;
final int inputLength = newTextOffset - textOffset;
if (inputPartition == null || inputPartition.isReadOnly()) {
final int inputOffset = offset + textOffset;
if (inputPartition != null
&& inputOffset < inputPartition.getOffset() + inputPartition.getLength()) {
// input is inside an existing read-only partition
splitPartition(inputOffset);
}
inputPartition = new IOConsolePartition(inputOffset, inputStream);
inputPartitionIndex++;
partitions.add(inputPartitionIndex, inputPartition);
inputPartitions.add(inputPartition);
lastPartitionWithValidOffset++; // new input partitions get build with correct offsets
}
inputPartition.setLength(inputPartition.getLength() + inputLength);
if (foundNewline) {
inputPartitions.sort(CMP_REGION_BY_OFFSET);
final StringBuilder inputLine = new StringBuilder();
for (IOConsolePartition p : inputPartitions) {
try {
final String fragment = document.get(p.getOffset(), p.getLength());
inputLine.append(fragment);
} catch (BadLocationException e) {
log(e);
}
p.setReadOnly();
}
inputPartitions.clear();
if (ASSERT) {
Assert.isTrue(inputLine.length() > 0);
}
inputStream.appendData(inputLine.toString());
}
Assert.isTrue(newTextOffset > textOffset); // can prevent infinity loop
textOffset = newTextOffset;
}
}
// repair partition offsets
int newOffset = 0;
if (lastPartitionWithValidOffset >= 0) {
// reduce number of partition to update by skipping still valid entries
final IOConsolePartition partition = partitions.get(lastPartitionWithValidOffset);
newOffset = partition.getOffset() + partition.getLength();
}
final Iterator<IOConsolePartition> it = partitions.listIterator(lastPartitionWithValidOffset + 1);
while (it.hasNext()) {
final IOConsolePartition partition = it.next();
if (partition.getLength() <= 0) {
if (ASSERT) {
Assert.isTrue(partition.getLength() == 0);
}
it.remove();
if (isInputPartition(partition)) {
final boolean removed = inputPartitions.remove(partition);
if (ASSERT) {
Assert.isTrue(removed);
}
}
} else {
partition.setOffset(newOffset);
newOffset += partition.getLength();
}
}
if (ASSERT) {
checkPartitions();
}
return new Region(0, document.getLength());
}
/**
* Split an existing partition at offset. The offset must not be the first or
* last offset of the existing partition because this leads to empty partitions
* not bearable by this partitioner.
* <p>
* New partition is added to {@link #partitions} (always) and
* {@link #inputPartitions} (if applicable).
* </p>
*
* @param offset the offset where the existing partition will end after split
* and a new partition will start
* @return the newly created partition (i.e. the right side of the split)
*/
private IOConsolePartition splitPartition(int offset) {
final int partitionIndex = findPartitionCandidate(offset);
final IOConsolePartition existingPartition = partitions.get(partitionIndex);
final IOConsolePartition newPartition;
if (isInputPartition(existingPartition)) {
newPartition = new IOConsolePartition(offset, existingPartition.getInputStream());
if (existingPartition.isReadOnly()) {
newPartition.setReadOnly();
}
if (inputPartitions.contains(existingPartition)) {
inputPartitions.add(newPartition);
}
} else {
newPartition = new IOConsolePartition(offset, existingPartition.getOutputStream());
}
newPartition.setLength((existingPartition.getOffset() + existingPartition.getLength()) - offset);
existingPartition.setLength(offset - existingPartition.getOffset());
partitions.add(partitionIndex + 1, newPartition);
return newPartition;
}
private void setUpdateInProgress(boolean b) {
updateInProgress = b;
}
/**
* A stream has been appended, add to pendingPartions list and schedule
* updateJob. updateJob is scheduled with a slight delay, this allows the
* console to run the job less frequently and update the document with a greater
* amount of data each time the job is run
*
* @param stream The stream that was written to.
* @param s The string that should be appended to the document.
* @throws IOException if partitioner is not connected to a document
*/
public void streamAppended(IOConsoleOutputStream stream, String s) throws IOException {
if (document == null) {
throw new IOException("Document is closed"); //$NON-NLS-1$
}
synchronized(pendingPartitions) {
PendingPartition last = pendingPartitions.size() > 0 ? pendingPartitions.get(pendingPartitions.size()-1) : null;
if (last != null && last.stream == stream) {
last.append(s);
} else {
pendingPartitions.add(new PendingPartition(stream, s));
if (fBuffer > 1000) {
queueJob.schedule();
} else {
queueJob.schedule(50);
}
}
if (fBuffer > 160000) {
if(Display.getCurrent() == null){
try {
pendingPartitions.wait();
} catch (InterruptedException e) {
}
} else {
/*
* if we are in UI thread we cannot lock it, so process
* queued output.
*/
processQueue();
}
}
}
}
/**
* Holds data until updateJob can be run and the document can be updated.
*/
private class PendingPartition {
StringBuilder text = new StringBuilder(8192);
IOConsoleOutputStream stream;
PendingPartition(IOConsoleOutputStream stream, String text) {
this.stream = stream;
if (text != null) {
append(text);
}
}
void append(String moreText) {
text.append(moreText);
fBuffer += moreText.length();
}
}
/**
* Updates the document. Will append everything that is available before
* finishing.
*/
private class QueueProcessingJob extends UIJob {
QueueProcessingJob() {
super("IOConsole Updater"); //$NON-NLS-1$
}
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
processQueue();
if (ASSERT) {
checkPartitions();
}
return Status.OK_STATUS;
}
/*
* Job will process as much as it can each time it's run, but it gets
* scheduled everytime a PendingPartition is added to the list, meaning
* that this job could get scheduled unnecessarily in cases of heavy output.
* Note however, that schedule() will only reschedule a running/scheduled Job
* once even if it's called many times.
*/
@Override
public boolean shouldRun() {
boolean shouldRun = connected && pendingPartitions != null && pendingPartitions.size() > 0;
return shouldRun;
}
}
void processQueue() {
synchronized (overflowLock) {
ArrayList<PendingPartition> pendingCopy = new ArrayList<>();
StringBuilder buffer = null;
boolean consoleClosed = false;
synchronized(pendingPartitions) {
pendingCopy.addAll(pendingPartitions);
pendingPartitions.clear();
fBuffer = 0;
pendingPartitions.notifyAll();
}
// determine buffer size
int size = 0;
for (PendingPartition pp : pendingCopy) {
if (pp != consoleClosedPartition) {
size+= pp.text.length();
}
}
buffer = new StringBuilder(size);
for (PendingPartition pp : pendingCopy) {
if (pp != consoleClosedPartition) {
buffer.append(pp.text);
} else {
consoleClosed = true;
}
}
if (connected) {
setUpdateInProgress(true);
updatePartitions = pendingCopy;
firstOffset = document.getLength();
try {
if (buffer != null) {
document.replace(firstOffset, 0, buffer.toString());
}
} catch (BadLocationException e) {
}
updatePartitions = null;
setUpdateInProgress(false);
}
if (consoleClosed) {
console.partitionerFinished();
}
checkBufferSize();
}
}
/**
* Job to trim the console document, runs in the UI thread.
*/
private class TrimJob extends WorkbenchJob {
/**
* trims output up to the line containing the given offset,
* or all output if -1.
*/
private int truncateOffset;
/**
* Creates a new job to trim the buffer.
*/
TrimJob() {
super("Trim Job"); //$NON-NLS-1$
setSystem(true);
}
/**
* Sets the trim offset.
*
* @param offset trims output up to the line containing the given offset
*/
public void setOffset(int offset) {
truncateOffset = offset;
}
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
if (document == null) {
return Status.OK_STATUS;
}
int length = document.getLength();
if (truncateOffset < length) {
synchronized (overflowLock) {
try {
if (truncateOffset < 0) {
// clear
setUpdateInProgress(true);
document.set(""); //$NON-NLS-1$
setUpdateInProgress(false);
} else {
// overflow
int cutoffLine = document.getLineOfOffset(truncateOffset);
int cutOffset = document.getLineOffset(cutoffLine);
// set the new length of the first partition
IOConsolePartition partition = (IOConsolePartition) getPartition(cutOffset);
partition.setLength(partition.getOffset() + partition.getLength() - cutOffset);
setUpdateInProgress(true);
document.replace(0, cutOffset, ""); //$NON-NLS-1$
setUpdateInProgress(false);
//remove partitions and reset Partition offsets
int index = partitions.indexOf(partition);
for (int i = 0; i < index; i++) {
partitions.remove(0);
}
int offset = 0;
for (IOConsolePartition p : partitions) {
p.setOffset(offset);
offset += p.getLength();
}
}
if (ASSERT) {
checkPartitions();
}
} catch (BadLocationException e) {
}
}
}
return Status.OK_STATUS;
}
}
@Override
public boolean isReadOnly(int offset) {
final IOConsolePartition partition = getIOPartition(offset);
return partition != null ? partition.isReadOnly() : true;
}
@Override
public StyleRange[] getStyleRanges(int offset, int length) {
if (!connected) {
return new StyleRange[0];
}
final IOConsolePartition[] computedPartitions = computeIOPartitioning(offset, length);
final StyleRange[] styles = new StyleRange[computedPartitions.length];
for (int i = 0; i < computedPartitions.length; i++) {
int rangeStart = computedPartitions[i].getOffset();
int rangeLength = computedPartitions[i].getLength();
// snap partitions to requested range
final int underflow = offset - rangeStart;
if (underflow > 0) {
rangeStart += underflow;
rangeLength -= underflow;
}
final int overflow = (rangeStart + rangeLength) - (offset + length);
if (overflow > 0) {
rangeLength -= overflow;
}
styles[i] = computedPartitions[i].getStyleRange(rangeStart, rangeLength);
}
return styles;
}
@Override
public ITypedRegion[] computeReadOnlyPartitions() {
if (document == null) {
return new IOConsolePartition[0];
}
return computeReadOnlyPartitions(0, document.getLength());
}
@Override
public ITypedRegion[] computeReadOnlyPartitions(int offset, int length) {
return computePartitioning(offset, length, false, true);
}
@Override
public ITypedRegion[] computeWritablePartitions() {
if (document == null) {
return new IOConsolePartition[0];
}
return computeWritablePartitions(0, document.getLength());
}
@Override
public ITypedRegion[] computeWritablePartitions(int offset, int length) {
return computePartitioning(offset, length, true, false);
}
@Override
public boolean isReadOnly(int offset, int length) {
final ITypedRegion[] readOnlyRegions = computeReadOnlyPartitions(offset, length);
int o = offset;
int end = offset + length;
for (ITypedRegion readOnlyRegion : readOnlyRegions) {
if (o < readOnlyRegion.getOffset()) {
return false;
}
o += readOnlyRegion.getLength();
if (o >= end) {
return true;
}
}
return false;
}
@Override
public boolean containsReadOnly(int offset, int length) {
return computeReadOnlyPartitions(offset, length).length > 0;
}
@Override
public int getPreviousOffsetByState(int offset, boolean searchWritable) {
if (partitions != null) {
int partitionIndex = findPartitionCandidate(offset);
for (; partitionIndex >= 0; partitionIndex--) {
final IOConsolePartition partition = partitions.get(partitionIndex);
if (partition.isReadOnly() != searchWritable) {
return Math.min(partition.getOffset() + partition.getLength() - 1, offset);
}
}
}
return -1;
}
@Override
public int getNextOffsetByState(int offset, boolean searchWritable) {
if (partitions != null) {
int partitionIndex = findPartitionCandidate(offset);
if (partitionIndex >= 0) {
for (; partitionIndex < partitions.size(); partitionIndex++) {
final IOConsolePartition partition = partitions.get(partitionIndex);
if (partition.isReadOnly() != searchWritable) {
return Math.max(partition.getOffset(), offset);
}
}
}
}
return document != null ? document.getLength() : 0;
}
/**
* Get a partition by its index. Safe from out of bounds exceptions.
*
* @param index index of requested partition
* @return the requested partition or <code>null</code> if index is invalid
*/
private IOConsolePartition getPartitionByIndex(int index) {
return (index >= 0 && index < partitions.size()) ? partitions.get(index) : null;
}
/**
* Check if given partition is from type input partition.
*
* @param partition partition to check (not <code>null</code>)
* @return true if partition is an input partition
*/
private static boolean isInputPartition(IOConsolePartition partition) {
return IOConsolePartition.INPUT_PARTITION_TYPE.equals(partition.getType());
}
private static void log(Throwable t) {
ConsolePlugin.log(t);
}
/**
* For debug purpose. Check if whole document is partitioned, partitions are
* ordered by offset, every partition has length greater 0 and all writable
* input partitions are listed in {@link #inputPartitions}.
*/
private void checkPartitions() {
if (!connected) {
return;
}
final List<IOConsolePartition> knownInputPartitions = new ArrayList<>(inputPartitions);
int offset = 0;
for (IOConsolePartition partition : partitions) {
Assert.isTrue(offset == partition.getOffset());
Assert.isTrue(partition.getLength() > 0);
offset += partition.getLength();
if (isInputPartition(partition) && !partition.isReadOnly()) {
Assert.isTrue(knownInputPartitions.remove(partition));
}
}
Assert.isTrue(offset == document.getLength());
Assert.isTrue(knownInputPartitions.isEmpty());
}
}