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