blob: 5d8bb7a4bc77ad31255050dba74a0c881066f49a [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2015, 2020 Stephan Wahlbrink 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, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.nico.ui.console;
import org.eclipse.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.swt.widgets.Display;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.ecommons.ui.util.UIAccess;
import org.eclipse.statet.internal.nico.ui.console.NIConsolePartitioner.PendingPartition;
/**
* Console stream processor handles control chars BEL, BS, LF, VT, FF and CR.
*/
final class StreamProcessor {
private static final char BEL= 0x07;
private static final char BS= 0x08;
private static final char LF= 0x0A;
private static final char VT= 0x0B;
private static final char FF= 0x0C;
private static final char CR= 0x0D;
private static final int CHAR_BUFFER_SIZE= 0x2000;
private static final byte S_CREATED= 0b0_0000_0001;
private static final byte S_APPLIED= 0b0_0000_0010;
private static final byte S_DONE= 0b0_0000_0100;
private final NIConsolePartitioner partitioner;
private byte state;
private String docLF;
private String docVT;
private boolean finish;
private int docLength;
private final StringBuilder text= new StringBuilder(CHAR_BUFFER_SIZE);
private int textOffsetInDoc;
private int lineStartInText;
private int lastPartitionInsertGap;
private final char[] charBuffer= new char[CHAR_BUFFER_SIZE];
public StreamProcessor(final NIConsolePartitioner partitioner) {
this.partitioner= partitioner;
}
public void prepareUpdate(final ImList<PendingPartition> pendingPartitions,
final int pendingLength) {
this.text.setLength(0);
if ((this.state & S_APPLIED) == 0) { // not applied
this.lastPartitionInsertGap= 0;
}
clear();
final AbstractDocument document= this.partitioner.getDocument();
if (document != null) { // connected
this.docLength= document.getLength();
boolean mayCombineLast= true;
this.textOffsetInDoc= this.docLength;
this.lineStartInText= 0;
this.text.ensureCapacity(pendingLength);
for (final PendingPartition pp : pendingPartitions) {
if (pp != null) {
processPartition(pp, mayCombineLast);
mayCombineLast= false;
}
else {
this.finish= true;
}
}
this.state|= S_CREATED;
}
else {
for (final PendingPartition pp : pendingPartitions) {
if (pp == null) {
this.finish= true;
break;
}
}
}
}
public void updateApplied() {
this.state|= S_APPLIED;
}
public void updateDone() {
this.state|= S_DONE;
if ((this.finish) ?
(this.text.capacity() > 2 * CHAR_BUFFER_SIZE) :
(this.text.capacity() > 0x100000 && this.text.capacity() / 2 > this.text.length()) ) {
if (this.text.length() < CHAR_BUFFER_SIZE) {
this.text.append(this.charBuffer, 0, CHAR_BUFFER_SIZE - this.text.length());
}
else {
this.text.setLength(CHAR_BUFFER_SIZE);
}
this.text.trimToSize();
}
}
public void clear() {
this.state= 0;
}
/**
* Returns the text to insert into the document.
*
* @return the text
*/
public String getText() {
return this.text.toString();
}
/**
* Returns the offset for {@link #getText()}.
*
* @return offset in the document.
*/
public int getTextOffsetInDoc() {
return this.textOffsetInDoc;
}
/**
* Returns the length of text to be replaced by {@link #getText()}.
*
* @return the length in the document.
*/
public int getTextReplaceLengthInDoc() {
return this.docLength - this.textOffsetInDoc;
}
public boolean wasFinished() {
return this.finish;
}
private void processPartition(final PendingPartition pp, boolean mayCombineLast) {
final StringBuilder pText= pp.getText();
final StringBuilder text= this.text;
final int pOffset= text.length();
int insertIdx= pOffset;
int readIdx= 0, doneIdx= 0;
int pLineStart= pOffset;
if (mayCombineLast && pLineStart == 0 && this.lastPartitionInsertGap > 0) {
mayCombineLast= false;
prependLastDocLine(pp, text);
insertIdx= Math.max(text.length() - this.lastPartitionInsertGap, pLineStart);
}
while (readIdx < pText.length()) {
final char c= pText.charAt(readIdx);
switch (c) {
case BEL:
insertIdx+= copy(pText, doneIdx, readIdx, text, insertIdx);
bell();
readIdx++;
doneIdx= readIdx;
continue;
case BS:
insertIdx+= copy(pText, doneIdx, readIdx, text, insertIdx);
if (mayCombineLast && pLineStart == 0) {
mayCombineLast= false;
insertIdx+= prependLastDocLine(pp, text);
}
if (insertIdx > pLineStart) {
insertIdx--;
}
readIdx++;
doneIdx= readIdx;
continue;
case LF:
if (insertIdx < text.length()) {
copy(pText, doneIdx, readIdx, text, insertIdx);
insertIdx= text.length();
doneIdx= readIdx;
}
readIdx++;
pLineStart= insertIdx + readIdx - doneIdx;
continue;
case VT:
copy(pText, doneIdx, readIdx, text, insertIdx);
if (this.docVT == null) {
initDocTemplates();
}
text.append(this.docVT);
insertIdx= text.length();
pLineStart= insertIdx;
readIdx++;
doneIdx= readIdx;
continue;
case FF:
insertIdx+= copy(pText, doneIdx, readIdx, text, insertIdx);
if (this.docLF == null) {
initDocTemplates();
}
{ int count;
if (pLineStart == pOffset) {
count= insertIdx - this.lineStartInText;
if (this.lineStartInText == 0) {
count+= getLastDocLineLength();
}
}
else {
count= insertIdx - pLineStart;
}
text.append(this.docLF);
insertIdx= text.length();
pLineStart= insertIdx;
insertIdx+= append(' ', count, text);
}
readIdx++;
doneIdx= readIdx;
continue;
case CR:
if (readIdx + 1 < pText.length() && pText.charAt(readIdx + 1) == LF) {
if (insertIdx < text.length()) {
copy(pText, doneIdx, readIdx, text, insertIdx);
insertIdx= text.length();
doneIdx= readIdx;
}
readIdx++;
continue;
}
copy(pText, doneIdx, readIdx, text, insertIdx);
if (mayCombineLast && pLineStart == 0) {
mayCombineLast= false;
prependLastDocLine(pp, text);
}
insertIdx= pLineStart;
readIdx++;
doneIdx= readIdx;
continue;
default:
readIdx++;
continue;
}
}
this.lineStartInText= pLineStart;
if (doneIdx == 0 && text.length() == pOffset) { // nothing special found
text.append(pText);
this.lastPartitionInsertGap= 0;
}
else {
insertIdx+= copy(pText, doneIdx, readIdx, text, insertIdx);
// copy back to partition
pText.setLength(0);
copy(text, pOffset, text.length(), pText, 0);
this.lastPartitionInsertGap= text.length() - insertIdx;
}
}
/**
* @return the length of text copied (= srcEnd - srcStart)
*/
private int copy(final StringBuilder src, int srcStart, final int srcEnd,
final StringBuilder dest, final int destIdx) {
final int length= srcEnd - srcStart;
if (length == 0) {
return 0;
}
if (destIdx == dest.length()) {
if (length == 1) {
dest.append(src.charAt(srcStart));
}
else if (length <= 16) {
dest.append(src, srcStart, srcEnd);
}
else {
for (int n; (n= Math.min(srcEnd - srcStart, CHAR_BUFFER_SIZE)) != 0; srcStart+= n) {
src.getChars(srcStart, srcStart + n, this.charBuffer, 0);
dest.append(this.charBuffer, 0, n);
}
}
}
else {
if (length == 1) {
dest.setCharAt(destIdx, src.charAt(srcStart));
}
else if (destIdx + length < dest.length()) {
dest.replace(destIdx, destIdx + length, src.substring(srcStart, srcEnd));
}
else {
dest.setLength(destIdx);
for (int n; (n= Math.min(srcEnd - srcStart, CHAR_BUFFER_SIZE)) != 0; srcStart+= n) {
src.getChars(srcStart, srcStart + n, this.charBuffer, 0);
dest.append(this.charBuffer, 0, n);
}
}
}
return length;
}
/**
* @return the length of text appended (= count)
*/
private int append(final char c, final int count, final StringBuilder dest) {
for (int i= 0; i < count; i++) {
dest.append(c);
}
return count;
}
private void bell() {
final Display display= UIAccess.getDisplay();
display.asyncExec(new Runnable() {
@Override
public void run() {
display.beep();
}
});
}
/**
* Prepends the last line (or tail of last line) of the document, if it matches the specified
* partition.
*
* @return the length of text prepended
*/
private int prependLastDocLine(final PendingPartition pp, final StringBuilder dest) {
final NIConsolePartition lastPartition= this.partitioner.getLastPartition();
if (lastPartition != null && lastPartition.getStream() == pp.getStream()
&& lastPartition.getOffset() + lastPartition.getLength() == this.docLength ) {
try {
final AbstractDocument document= this.partitioner.getDocument();
final int start= Math.max(lastPartition.getOffset(),
document.getLineOffset(document.getNumberOfLines() - 1) );
final int length= this.docLength - start;
if (length > 0) {
dest.insert(0, document.get(start, length));
this.textOffsetInDoc= start;
return length;
}
}
catch (final BadLocationException e) {}
}
return 0;
}
/**
* Returns the length of the last line of the document independent of its partitions,
* except the text already reused ({@link #prependLastDocLine(PendingPartition, StringBuilder)}).
*
* @return the length of the last line.
*/
private int getLastDocLineLength() {
try {
final AbstractDocument document= this.partitioner.getDocument();
final int start= document.getLineOffset(document.getNumberOfLines() - 1);
final int length= this.textOffsetInDoc - start;
if (length > 0) {
return length;
}
}
catch (final BadLocationException e) {}
return 0;
}
private void initDocTemplates() {
this.docLF= this.partitioner.getConsole().getProcess().getWorkspaceData().getLineSeparator();
final StringBuilder sb= new StringBuilder();
sb.append(this.docLF);
sb.append(this.docLF);
sb.append(this.docLF);
sb.append(this.docLF);
this.docVT= sb.toString();
}
}