blob: 4394fe2ba665015676bc6416bd26b8d10931c950 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2000, 2021 IBM Corporation and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0.
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# IBM Corporation - org.eclipse.ui.console: initial API and implementation
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.nico.ui.console;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobManager;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.AbstractDocument;
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.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.progress.WorkbenchJob;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.nico.ui.console.NIConsole;
import org.eclipse.statet.nico.ui.console.NIConsoleOutputStream;
/**
* Partitioner for a NIConsole's document.
*/
public class NIConsolePartitioner implements IConsoleDocumentPartitioner, IDocumentPartitionerExtension {
/**
* Holds data until updateJob can be run and the document can be updated.
*/
final class PendingPartition {
private final NIConsoleOutputStream stream;
private final StringBuilder text;
PendingPartition(final NIConsoleOutputStream stream, final String text) {
this.stream= stream;
this.text= new StringBuilder(Math.max(4, 2 + (text.length()/1014)) * 1024);
append(text);
}
public NIConsoleOutputStream getStream() {
return this.stream;
}
public StringBuilder getText() {
return this.text;
}
private void append(final String text) {
this.text.append(text);
NIConsolePartitioner.this.pendingTextLength+= text.length();
}
}
private final NIConsole console;
private final String[] partitionIds;
private AbstractDocument document;
private boolean isConnected= false;
private final ArrayList<NIConsolePartition> partitions= new ArrayList<>();
/**
* The last partition appended to the document
*/
private NIConsolePartition lastPartition;
/**
* Blocks of data that have not yet been appended to the document.
*/
private final ArrayList<PendingPartition> pendingPartitions= new ArrayList<>();
private int pendingTextLength;
/**
* A list of PendingPartitions to be appended by the updateJob
*/
private ImList<PendingPartition> updatePartitions;
private final QueueProcessingJob queueJob= new QueueProcessingJob();
private final TrimJob trimJob= new TrimJob();
/**
* Flag to indicate that the updateJob is updating the document.
*/
private boolean updateInProgress;
/**
* offset used by updateJob
*/
private final StreamProcessor streamProcessor;
/**
* An array of legal line delimiters
*/
private int highWaterMark= -1;
private int lowWaterMark= -1;
/**
* 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 final Object overflowLock= new Object();
public NIConsolePartitioner(final NIConsole console, final List<String> ids) {
this.console= console;
this.streamProcessor= new StreamProcessor(this);
this.partitionIds= ids.toArray(new String[ids.size()]);
this.trimJob.setRule(console.getSchedulingRule());
this.queueJob.setRule(this.console.getSchedulingRule());
}
public NIConsole getConsole() {
return this.console;
}
public AbstractDocument getDocument() {
return this.document;
}
NIConsolePartition getLastPartition() {
return this.lastPartition;
}
@Override
public void connect(final IDocument doc) {
this.document= (AbstractDocument) doc;
this.document.setDocumentPartitioner(this);
this.isConnected= true;
}
public int getHighWaterMark() {
return this.highWaterMark;
}
public int getLowWaterMark() {
return this.lowWaterMark;
}
public void setWaterMarks(final int low, final int high) {
this.lowWaterMark= low;
this.highWaterMark= high;
ConsolePlugin.getStandardDisplay().asyncExec(new Runnable() {
@Override
public void run() {
checkBufferSize();
}
});
}
/**
* Notification from the console that all of its streams have been closed.
*/
public void finish() {
synchronized (this.pendingPartitions) {
this.pendingPartitions.add(null);
}
this.queueJob.schedule(); //ensure that all pending partitions are processed.
}
@Override
public void disconnect() {
synchronized (this.overflowLock) {
this.isConnected= false;
this.document= null;
this.partitions.clear();
}
}
@Override
public void documentAboutToBeChanged(final DocumentEvent event) {
}
@Override
public boolean documentChanged(final DocumentEvent event) {
return documentChanged2(event) != null;
}
@Override
public String[] getLegalContentTypes() {
return this.partitionIds;
}
@Override
public String getContentType(final int offset) {
return getPartition(offset).getType();
}
@Override
public ITypedRegion[] computePartitioning(final int offset, final int length) {
final int rangeEnd= offset + length;
int left= 0;
int right= this.partitions.size() - 1;
int mid= 0;
NIConsolePartition position= null;
if (right <= 0) {
if (right == 0) {
return new NIConsolePartition[] { this.partitions.get(0) };
}
return new NIConsolePartition[0];
}
while (left < right) {
mid= (left + right) / 2;
position= this.partitions.get(mid);
if (rangeEnd < position.getOffset()) {
if (left == mid) {
right= left;
} else {
right= mid -1;
}
}
else if (offset > (position.getOffset() + position.getLength() - 1)) {
if (right == mid) {
left= right;
} else {
left= mid +1;
}
}
else {
left= right= mid;
}
}
final List<NIConsolePartition> list= new ArrayList<>();
int index= left - 1;
if (index >= 0) {
position= this.partitions.get(index);
while (index >= 0 && (position.getOffset() + position.getLength()) > offset) {
index--;
if (index >= 0) {
position= this.partitions.get(index);
}
}
}
index++;
position= this.partitions.get(index);
while (index < this.partitions.size() && (position.getOffset() < rangeEnd)) {
list.add(position);
index++;
if (index < this.partitions.size()) {
position= this.partitions.get(index);
}
}
return list.toArray(new NIConsolePartition[list.size()]);
}
@Override
public ITypedRegion getPartition(final int offset) {
for (int i= 0; i < this.partitions.size(); i++) {
final ITypedRegion partition= this.partitions.get(i);
final int start= partition.getOffset();
final int end= start + partition.getLength();
if (offset >= start && offset < end) {
return partition;
}
}
return (this.lastPartition != null) ?
this.lastPartition : new NIConsolePartition(this.partitionIds[0], null);
}
/**
* 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 (this.document != null && this.highWaterMark > 0) {
final int length= this.document.getLength();
if (length > this.highWaterMark) {
if (this.trimJob.getState() == Job.NONE) { //if the job isn't already running
this.trimJob.setOffset(length - this.lowWaterMark);
this.trimJob.schedule();
}
}
}
}
/**
* Clears the console
*/
public void clearBuffer() {
synchronized (this.overflowLock) {
this.trimJob.setOffset(-1);
this.trimJob.schedule();
}
}
@Override
public IRegion documentChanged2(final DocumentEvent event) {
if (this.document == null) {
return null; //another thread disconnected the partitioner
}
if (this.document.getLength() == 0) { //document cleared
this.partitions.clear();
this.lastPartition= null;
this.streamProcessor.clear();
return new Region(0, 0);
}
if (this.updateInProgress) {
if (this.updatePartitions != null) {
int offset= this.streamProcessor.getTextOffsetInDoc();
NIConsolePartition partition= this.lastPartition;
for (final PendingPartition pp : this.updatePartitions) {
if (pp != null) {
final int ppLen= pp.text.length();
if (partition != null) {
if (partition.getStream() == pp.stream) {
partition.setLength(
offset + ppLen - partition.getOffset() );
offset+= ppLen;
continue;
}
if (partition.getLength() == 0) {
final int idx= this.partitions.lastIndexOf(partition);
if (idx >= 0) {
this.partitions.remove(idx);
}
}
partition= null;
}
if (ppLen > 0) {
partition= new NIConsolePartition(
pp.stream.getId(), pp.stream, offset, ppLen);
this.partitions.add(partition);
offset+= ppLen;
}
}
}
this.lastPartition= partition;
this.streamProcessor.updateApplied();
}
}
return new Region(event.fOffset, event.fText.length());
}
private void setUpdateInProgress(final boolean b) {
this.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.
*/
public void streamAppended(final NIConsoleOutputStream stream, final String s) throws IOException {
if (this.document == null) {
throw new IOException("Document is closed"); //$NON-NLS-1$
}
if (stream == null) {
throw new NullPointerException("stream"); //$NON-NLS-1$
}
synchronized(this.pendingPartitions) {
final PendingPartition last= (this.pendingPartitions.size() > 0 ?
this.pendingPartitions.get(this.pendingPartitions.size() - 1) : null );
if (last != null && last.stream == stream) {
last.append(s);
}
else {
this.pendingPartitions.add(new PendingPartition(stream, s));
if (this.pendingTextLength > 0x1ff) {
this.queueJob.schedule();
} else {
this.queueJob.schedule(50);
}
}
if (this.pendingTextLength > 0xffff) {
if (Display.getCurrent() == null){
try {
this.pendingPartitions.wait();
}
catch (final InterruptedException e) {
}
}
else {
/*
* if we are in UI thread we cannot lock it, so process
* queued output directly.
*/
processQueue();
}
}
}
}
/**
* Updates the document. Will append everything that is available before
* finishing.
*/
private class QueueProcessingJob extends WorkbenchJob {
QueueProcessingJob() {
super("IOConsole Updater"); //$NON-NLS-1$
setSystem(true);
setPriority(Job.INTERACTIVE);
}
@Override
public IStatus runInUIThread(final IProgressMonitor monitor) {
processQueue();
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() {
final boolean shouldRun= NIConsolePartitioner.this.isConnected
&& NIConsolePartitioner.this.pendingPartitions.size() > 0;
return shouldRun;
}
}
private void processQueue() {
synchronized (this.overflowLock) {
final ImList<PendingPartition> pendingCopy;
final int pendingLength;
synchronized(this.pendingPartitions) {
pendingCopy= ImCollections.toList(this.pendingPartitions);
this.pendingPartitions.clear();
pendingLength= this.pendingTextLength;
this.pendingTextLength= 0;
this.pendingPartitions.notifyAll();
}
if (pendingCopy.isEmpty()) {
return;
}
this.streamProcessor.prepareUpdate(pendingCopy, pendingLength);
if (this.isConnected) {
setUpdateInProgress(true);
this.updatePartitions= pendingCopy;
try {
this.document.replace(
this.streamProcessor.getTextOffsetInDoc(),
this.streamProcessor.getTextReplaceLengthInDoc(),
this.streamProcessor.getText() );
}
catch (final BadLocationException e) {}
finally {
this.updatePartitions= null;
setUpdateInProgress(false);
}
}
if (this.streamProcessor.wasFinished()) {
this.console.partitionerFinished();
}
this.streamProcessor.updateDone();
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(final int offset) {
this.truncateOffset= offset;
}
@Override
public IStatus runInUIThread(final IProgressMonitor monitor) {
final IJobManager jobManager= Job.getJobManager();
try {
jobManager.join(NIConsolePartitioner.this.console, monitor);
}
catch (final OperationCanceledException e1) {
return Status.CANCEL_STATUS;
}
catch (final InterruptedException e1) {
return Status.CANCEL_STATUS;
}
if (NIConsolePartitioner.this.document == null) {
return Status.OK_STATUS;
}
final int length= NIConsolePartitioner.this.document.getLength();
if (this.truncateOffset < length) {
synchronized (NIConsolePartitioner.this.overflowLock) {
try {
if (this.truncateOffset < 0) {
// clear
setUpdateInProgress(true);
NIConsolePartitioner.this.document.set(""); //$NON-NLS-1$
setUpdateInProgress(false);
NIConsolePartitioner.this.partitions.clear();
}
else {
// overflow
final int cutoffLine= NIConsolePartitioner.this.document.getLineOfOffset(this.truncateOffset);
final int cutOffset= NIConsolePartitioner.this.document.getLineOffset(cutoffLine);
// set the new length of the first partition
final NIConsolePartition partition= (NIConsolePartition) getPartition(cutOffset);
partition.setLength(partition.getOffset() + partition.getLength() - cutOffset);
setUpdateInProgress(true);
NIConsolePartitioner.this.document.replace(0, cutOffset, ""); //$NON-NLS-1$
setUpdateInProgress(false);
//remove partitions and reset Partition offsets
final int index= NIConsolePartitioner.this.partitions.indexOf(partition);
for (int i= 0; i < index; i++) {
NIConsolePartitioner.this.partitions.remove(0);
}
int offset= 0;
for (final Iterator<NIConsolePartition> i= NIConsolePartitioner.this.partitions.iterator(); i.hasNext();) {
final NIConsolePartition p= i.next();
p.setOffset(offset);
offset += p.getLength();
}
}
} catch (final BadLocationException e) {
}
}
}
return Status.OK_STATUS;
}
}
@Override
public boolean isReadOnly(final int offset) {
return true;
}
@Override
public StyleRange[] getStyleRanges(final int offset, final int length) {
if (!this.isConnected) {
return new StyleRange[0];
}
final NIConsolePartition[] computedPartitions= (NIConsolePartition[]) computePartitioning(offset, length);
final StyleRange[] styles= new StyleRange[computedPartitions.length];
for (int i= 0; i < computedPartitions.length; i++) {
final int rangeStart= Math.max(computedPartitions[i].getOffset(), offset);
final int rangeLength= computedPartitions[i].getLength();
styles[i]= new StyleRange(rangeStart, rangeLength,
computedPartitions[i].getStream().getColor(),
computedPartitions[i].getStream().getBackgroundColor(),
computedPartitions[i].getStream().getFontStyle() );
}
return styles;
}
}