Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleDocumentAdapter.java770
1 files changed, 569 insertions, 201 deletions
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleDocumentAdapter.java b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleDocumentAdapter.java
index f8f0df6f5..64b4616a5 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleDocumentAdapter.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleDocumentAdapter.java
@@ -10,13 +10,13 @@
*
* Contributors:
* IBM Corporation - initial API and implementation
+ * Paul Pazderski - Bug 544970: reimplementation with correct text change event calculation
*******************************************************************************/
package org.eclipse.ui.internal.console;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.BadLocationException;
@@ -24,105 +24,122 @@ import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentAdapter;
import org.eclipse.jface.text.IDocumentListener;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
+import org.eclipse.jface.text.TextUtilities;
import org.eclipse.swt.custom.TextChangeListener;
import org.eclipse.swt.custom.TextChangedEvent;
import org.eclipse.swt.custom.TextChangingEvent;
+import org.eclipse.ui.console.ConsolePlugin;
/**
- * Adapts a Console's document to the viewer StyledText widget. Allows proper line
- * wrapping of fixed width consoles without having to add line delimiters to the StyledText.
+ * Adapts a Console's document to the viewer StyledText widget. Allows proper
+ * line wrapping of fixed width consoles without having to add line delimiters
+ * to the StyledText.
*
- * By using this adapter, the offset of any character is the same in both the widget and the
- * document.
+ * By using this adapter, the offset of any character is the same in both the
+ * widget and the document.
+ *
+ * <p>
+ * Note: tab character (\t) is not handled special and counts as one character.
+ * So wrapped content may look less fixed width if it contains tab characters.
+ * </p>
*
* @since 3.1
*/
public class ConsoleDocumentAdapter implements IDocumentAdapter, IDocumentListener {
+ /*
+ * Because of the fixed width feature this adapter knows two types of lines.
+ * Document lines are lines as known to the adapted document and are terminated
+ * by a line delimiter (except for the last line). Widget lines are lines as
+ * seen in the text viewer. If a document line is wrapped due to fixed width it
+ * starts a new widget line. Widget lines generated by line wrapping do not have
+ * additional line delimiters. If fixed width is disabled widget line and
+ * document line are the same.
+ */
- private int consoleWidth = -1;
- private List<TextChangeListener> textChangeListeners;
- private IDocument document;
-
- int[] offsets = new int[5000];
- int[] lengths = new int[5000];
- private int regionCount = 1;
- private Pattern pattern = Pattern.compile("$", Pattern.MULTILINE); //$NON-NLS-1$
-
-
- public ConsoleDocumentAdapter(int width) {
- textChangeListeners = new ArrayList<>();
- consoleWidth = width;
- }
+ /**
+ * If <code>true</code> the adapter will check if the predicted document change
+ * calculated for the TextChangingEvent matches the document after the applied
+ * change. Useful for developing/debugging.
+ */
+ private static final boolean ASSERT = false;
- /*
- * repairs lines list from the beginning of the line containing the offset of any
- * DocumentEvent, to the end of the Document.
+ /**
+ * If {@link #widgetLineOffsets} need to grow add some extra space to prevent
+ * frequent array copies.
*/
- private void repairLines(int eventOffset) {
- if (document == null) {
- return;
- }
- try {
- int docLine = document.getLineOfOffset(eventOffset);
- int docLineOffset = document.getLineOffset(docLine);
- int widgetLine = getLineAtOffset(docLineOffset);
+ private static final int GROW = 500;
- for (int i=regionCount-1; i>=widgetLine; i--) {
- regionCount--;
- }
+ /** Registered {@link TextChangeListener}s. */
+ private final List<TextChangeListener> textChangeListeners = new ArrayList<>();
- int numLinesInDoc = document.getNumberOfLines();
+ /** Fixed console width. If &lt;= 0 fixed console mode is disabled. */
+ private int fixedConsoleWidth;
- int nextOffset = document.getLineOffset(docLine);
- for (int i = docLine; i<numLinesInDoc; i++) {
- int offset = nextOffset;
- int length = document.getLineLength(i);
- nextOffset += length;
+ /** Adapted document. */
+ private IDocument document;
+ /**
+ * The strings the connected document interprets as line delimiters.
+ * <p>
+ * This is usually <code>{ "\r", "\n", "\r\n" }</code>.
+ * </p>
+ */
+ private String[] docLegalLineDelimiters;
- if (length == 0) {
- addRegion(offset, 0);
- } else {
- while (length > 0) {
- int trimmedLength = length;
- String lineDelimiter = document.getLineDelimiter(i);
- int lineDelimiterLength = 0;
- if (lineDelimiter != null) {
- lineDelimiterLength = lineDelimiter.length();
- trimmedLength -= lineDelimiterLength;
- }
-
- if (consoleWidth > 0 && consoleWidth < trimmedLength) {
- addRegion(offset, consoleWidth);
- offset += consoleWidth;
- length -= consoleWidth;
- } else {
- addRegion(offset, length);
- offset += length;
- length -= length;
- }
- }
- }
- }
- } catch (BadLocationException e) {
- }
+ /**
+ * Number of widget lines in document. If fixed width is disabled it is always
+ * equal to {@link IDocument#getNumberOfLines()}. If fixed width mode is enabled
+ * there may be more widget lines then doc lines but never less.
+ */
+ private int widgetLines;
- if (regionCount == 0) {
- addRegion(0, document.getLength());
- }
- }
+ /**
+ * Start offsets of widget lines.
+ * <p>
+ * <b>Note:</b> offsets are only valid and updated if fixed width is enabled.
+ * </p>
+ * <p>
+ * Example content: if the document contains the following content (with
+ * <code>\r\n</code> as line delimiter)
+ *
+ * <pre>
+ * 0123456789ABCD\r\n
+ * ---\r\n
+ * 0123456789
+ * </pre>
+ *
+ * a fixed width of <code>10</code> the widget will show it as
+ *
+ * <pre>
+ * 0123456789
+ * ABCD\r\n
+ * ---\r\n
+ * 0123456789
+ * </pre>
+ *
+ * the content of the {@link #widgetLineOffsets} array is:
+ * <code>{ 0, 10, 16, 21 }</code> which represent the start offsets from each of
+ * the four widget lines.
+ * </p>
+ * <p>
+ * Initialized with one element to save memory in non fixed width mode but at
+ * the same time prevent NPE.
+ * </p>
+ *
+ * @see #isFixedWidth()
+ * @see #ensureOffsetsCapacity(int)
+ */
+ private int[] widgetLineOffsets = new int[1];
- private void addRegion(int offset, int length) {
- if (regionCount == 0) {
- offsets[0] = offset;
- lengths[0] = length;
- } else {
- if (regionCount == offsets.length) {
- growRegionArray(regionCount * 2);
- }
- offsets[regionCount] = offset;
- lengths[regionCount] = length;
- }
- regionCount++;
+ /**
+ * New {@link ConsoleDocumentAdapter} with no {@link IDocument} connected yet.
+ *
+ * @param width fixed console width to enforce text wrap or &lt;= 0 to disable
+ * fixed console width
+ */
+ public ConsoleDocumentAdapter(int width) {
+ setWidth(width);
}
@Override
@@ -132,16 +149,18 @@ public class ConsoleDocumentAdapter implements IDocumentAdapter, IDocumentListen
}
document = doc;
+ docLegalLineDelimiters = null;
+ updateWidgetOffsets(0);
- if (document != null) {
- document.addDocumentListener(this);
- repairLines(0);
+ if (doc != null) {
+ doc.addDocumentListener(this);
+ docLegalLineDelimiters = doc.getLegalLineDelimiters();
}
}
@Override
public synchronized void addTextChangeListener(TextChangeListener listener) {
- Assert.isNotNull(listener);
+ Assert.isLegal(listener != null, "listener null"); //$NON-NLS-1$
if (!textChangeListeners.contains(listener)) {
textChangeListeners.add(listener);
}
@@ -149,10 +168,8 @@ public class ConsoleDocumentAdapter implements IDocumentAdapter, IDocumentListen
@Override
public synchronized void removeTextChangeListener(TextChangeListener listener) {
- if(textChangeListeners != null) {
- Assert.isNotNull(listener);
- textChangeListeners.remove(listener);
- }
+ Assert.isLegal(listener != null, "listener null"); //$NON-NLS-1$
+ textChangeListeners.remove(listener);
}
@Override
@@ -163,62 +180,134 @@ public class ConsoleDocumentAdapter implements IDocumentAdapter, IDocumentListen
@Override
public String getLine(int lineIndex) {
try {
- StringBuffer line = new StringBuffer(document.get(offsets[lineIndex], lengths[lineIndex]));
- int index = line.length() - 1;
- while(index > -1 && (line.charAt(index)=='\n' || line.charAt(index)=='\r')) {
- index--;
- }
- return new String(line.substring(0, index+1));
+ final IRegion lineRegion = getLineInformation(lineIndex);
+ return document.get(lineRegion.getOffset(), lineRegion.getLength());
} catch (BadLocationException e) {
+ log(e);
+ return ""; //$NON-NLS-1$
}
- return ""; //$NON-NLS-1$
}
- @Override
- public int getLineAtOffset(int offset) {
- if (offset == 0 || regionCount <= 1) {
- return 0;
+ /**
+ * Return information about a widget line.
+ * <p>
+ * Note: in contrast to some implementations of
+ * {@link org.eclipse.jface.text.ILineTracker#getLineInformation(int)} this
+ * implementation throws an exception if line after last line is queried.
+ * </p>
+ *
+ * @param widgetLineIndex the widget line number (first line has number 0)
+ * @return the lines start offset and length excluding line delimiter
+ * @throws BadLocationException if widget line does not exist
+ * @see IDocument#getLineInformation(int)
+ */
+ private IRegion getLineInformation(int widgetLineIndex) throws BadLocationException {
+ if (!isFixedWidth()) {
+ return document.getLineInformation(widgetLineIndex);
}
- if (offset == document.getLength()) {
- return regionCount-1;
- }
+ final int widgetLineOffset = getLineOffset(widgetLineIndex);
+ final IRegion docLine = document.getLineInformationOfOffset(widgetLineOffset);
+ // widget line length is calculated as: length of containing document line
+ int widgetLineLength = docLine.getLength();
+ // minus preceding wrapped lines
+ widgetLineLength -= (widgetLineOffset - docLine.getOffset());
+ // now widgetLinelength is length from widget line start offset until real
+ // document line end. If widgetLinelength is still greater than
+ // fixedConsoleWidth there are more wrapped content after this widget line
+ // therefore this widget line length must equal fixedConsoleWidth otherwise it
+ // is the length of requested widget line.
+ widgetLineLength = Math.min(widgetLineLength, fixedConsoleWidth);
+ return new Region(widgetLineOffset, widgetLineLength);
+ }
- int left= 0;
- int right= regionCount-1;
- int midIndex = 0;
+ @Override
+ public int getLineAtOffset(int offset) {
+ try {
+ return getLineOfOffset(offset);
+ } catch (BadLocationException e) {
+ log(e);
+ // Note: this strange return value is inherited from the previous implementation
+ // of this method which did no boundary checks on the offset and either returned
+ // the first or last line index for bad locations.
+ return offset < 0 ? 0 : widgetLines - 1;
+ }
+ }
- while (left <= right) {
- if(left == right) {
- return right;
+ /**
+ * Return the widget line index at the given character offset.
+ * <p>
+ * Like {@link #getLineAtOffset(int)} but throws exception in case of invalid
+ * offset.
+ * </p>
+ *
+ * @param offset offset in document
+ * @return the number of the widget line at offset (0 for first line)
+ * @throws BadLocationException if the offset is invalid in adapted document
+ * @see #getLineAtOffset(int)
+ * @see IDocument#getLineOfOffset(int)
+ */
+ private int getLineOfOffset(int offset) throws BadLocationException {
+ if (!isFixedWidth()) {
+ return document.getLineOfOffset(offset);
+ } else {
+ if (offset < 0 || offset > getCharCount()) {
+ throw new BadLocationException(offset + " is not a valid offset."); //$NON-NLS-1$
}
- midIndex = (left + right) / 2;
-
- if (offset < offsets[midIndex]) {
- right = midIndex;
- } else if (offset >= offsets[midIndex] + lengths[midIndex]) {
- left = midIndex + 1;
- } else {
- return midIndex;
+ int widgetLine = Arrays.binarySearch(widgetLineOffsets, 0, widgetLines, offset);
+ if (widgetLine < 0) {
+ // no exact offset match. At this point (-widgetLine) - 1 can be used as
+ // insertion point to insert the searched offset in the array. Since we do not
+ // want to insert but request the line containing the offset the desired answer
+ // is the index before the insertion index.
+ widgetLine = (-widgetLine) - 2;
}
+ return widgetLine;
}
-
- return midIndex;
}
@Override
public int getLineCount() {
- return regionCount;
+ return widgetLines;
}
@Override
public String getLineDelimiter() {
- return System.getProperty("line.separator"); //$NON-NLS-1$
+ return System.lineSeparator();
}
@Override
public int getOffsetAtLine(int lineIndex) {
- return offsets[lineIndex];
+ try {
+ return getLineOffset(lineIndex);
+ } catch (BadLocationException e) {
+ log(e);
+ return -1;
+ }
+ }
+
+ /**
+ * Return the character offset of the first character of the given line.
+ * <p>
+ * Like {@link #getOffsetAtLine(int)} but throws exception in case of invalid
+ * line numbers.
+ * </p>
+ *
+ * @param widgetLineIndex the widget line number (first line has number 0)
+ * @return offset of the first character of the line
+ * @throws BadLocationException if widget line does not exist
+ * @see #getOffsetAtLine(int)
+ * @see IDocument#getLineOffset(int)
+ */
+ private int getLineOffset(int widgetLineIndex) throws BadLocationException {
+ if (!isFixedWidth()) {
+ return document.getLineOffset(widgetLineIndex);
+ } else {
+ if (widgetLineIndex < 0 || widgetLineIndex >= widgetLines) {
+ throw new BadLocationException(widgetLineIndex + " is not a valid line index."); //$NON-NLS-1$
+ }
+ return widgetLineOffsets[widgetLineIndex];
+ }
}
@Override
@@ -226,8 +315,9 @@ public class ConsoleDocumentAdapter implements IDocumentAdapter, IDocumentListen
try {
return document.get(start, length);
} catch (BadLocationException e) {
+ log(e);
+ return null;
}
- return null;
}
@Override
@@ -235,6 +325,7 @@ public class ConsoleDocumentAdapter implements IDocumentAdapter, IDocumentListen
try {
document.replace(start, replaceLength, text);
} catch (BadLocationException e) {
+ log(e);
}
}
@@ -249,121 +340,398 @@ public class ConsoleDocumentAdapter implements IDocumentAdapter, IDocumentListen
@Override
public synchronized void documentAboutToBeChanged(DocumentEvent event) {
- if (document == null) {
- return;
+ TextChangingEvent changingEvent;
+ if (!isFixedWidth()) {
+ changingEvent = new TextChangingEvent(this);
+ changingEvent.start = event.getOffset();
+ changingEvent.newText = event.getText() == null ? "" : event.getText(); //$NON-NLS-1$
+ changingEvent.replaceCharCount = event.getLength();
+ changingEvent.newCharCount = changingEvent.newText.length();
+ try {
+ changingEvent.replaceLineCount = document.getNumberOfLines(event.getOffset(), event.getLength()) - 1;
+ } catch (BadLocationException e) {
+ log(e);
+ }
+ changingEvent.newLineCount = document.computeNumberOfLines(changingEvent.newText);
+ } else {
+ try {
+ changingEvent = generateTextChangingEvent(event);
+ } catch (BadLocationException e) {
+ log(e);
+ // Should never happen.
+ // But provide empty changing event to hopefully reduce damage.
+ changingEvent = new TextChangingEvent(this);
+ }
}
- TextChangingEvent changeEvent = new TextChangingEvent(this);
- changeEvent.start = event.fOffset;
- changeEvent.newText = (event.fText == null ? "" : event.fText); //$NON-NLS-1$
- changeEvent.replaceCharCount = event.fLength;
- changeEvent.newCharCount = (event.fText == null ? 0 : event.fText.length());
+ for (TextChangeListener listener : textChangeListeners) {
+ listener.textChanging(changingEvent);
+ }
- int first = getLineAtOffset(event.fOffset);
- int lOffset = Math.max(event.fOffset + event.fLength - 1, 0);
- int last = getLineAtOffset(lOffset);
- changeEvent.replaceLineCount = Math.max(last - first, 0);
+ if (ASSERT) {
+ updatePrediction(changingEvent);
+ }
+ }
- int newLineCount = countNewLines(event.fText);
- changeEvent.newLineCount = newLineCount >= 0 ? newLineCount : 0;
+ /**
+ * Generate the {@link TextChangingEvent} for <b>fixed width mode</b>. Due to
+ * the dynamic wrapping of lines and the lack of line delimiters it is much more
+ * complicated than without fixed width.
+ *
+ * @param event the document change event
+ * @return the text changing event for the widget
+ * @throws BadLocationException if document event is invalid
+ */
+ private TextChangingEvent generateTextChangingEvent(DocumentEvent event) throws BadLocationException {
+ String newText = event.getText() == null ? "" : event.getText(); //$NON-NLS-1$
+ int newTextLength = newText.length();
+ int eventOffset = event.getOffset();
+ int eventLength = event.getLength();
+ int replaceLineCount = 0;
+ int newLineCount = 0;
+
+ // The primary duty of this method is to calculate new and removed line which
+ // includes the changing in line wrapping.
+ // The number of new and replaced lines must include any line whose line content
+ // is changing. Normally this only includes lines directly affected by the
+ // change event. But due to our magic line wrapping, as an extreme case, a
+ // single character inserted at first offset can, from StyledText-Widgets
+ // perspective, change the content of every line.
+
+ if (newTextLength >= 0 || eventLength > 0) {
+ // In this method first and last refer to the first and last line affected by
+ // the current document event
+ final int firstDocLineIndex = document.getLineOfOffset(eventOffset);
+ final int firstDocLineOffset = document.getLineOffset(firstDocLineIndex);
+
+ if (eventOffset != firstDocLineOffset && (eventOffset - firstDocLineOffset) % fixedConsoleWidth == 0) {
+ // event start is at fixed width border
+ // XXX: the trick here is important to do the impossible
+ // The fact that wrapped lines are new lines without a newline delimiter leads
+ // to some (nearly) impossible edge cases since all involved interfaces
+ // (implicit) assume a newline has its unique offset.
+ //
+ // Consider a fixed with of 10 and a console filled with (automatically wrapped)
+ // content of:
+ // 0123456789
+ // 0
+ // If we remove the last character ('0') we must set replaceLineCount to 1 since
+ // there is one line less due to the unwrapped line. The replaceLineCount is
+ // necessary so that StyledTextRenderer updates the lines which possible follow
+ // below and have changed due to moved content.
+ // But StyledTextRenderer presumes that the line index where the event occurs is
+ // the same before and after the text change. This is not the case for our auto
+ // wrapped lines since getLineAtOffset(10) is 1 before text change and 0 after.
+ //
+ // To solve this unlucky situation we simply never send text change event
+ // occurring at the fixed width wrap border. If such an document change happens
+ // we expand the text change to include also the character before the wrap
+ // border.
+ eventOffset--;
+ eventLength++;
+ newText = document.get(eventOffset, 1) + newText;
+ newTextLength++;
+ }
- if (changeEvent.newLineCount > offsets.length-regionCount) {
- growRegionArray(changeEvent.newLineCount);
- }
+ final int eventEnd = eventOffset + eventLength;
+ final int firstWidgetLineIndex = getLineOfOffset(eventOffset);
+ final int firstWidgetLineOffset = getLineOffset(firstWidgetLineIndex);
- for (TextChangeListener listener : textChangeListeners) {
- listener.textChanging(changeEvent);
+ final int lastInsertLength;
+ int lastDocLineLengthDiff = -eventLength;
+
+ int newTextOffset = 0;
+ int[] result = TextUtilities.indexOf(docLegalLineDelimiters, newText, newTextOffset);
+ if (result[1] < 0) {
+ // single line insert
+ lastInsertLength = eventOffset - firstWidgetLineOffset + newTextLength;
+ lastDocLineLengthDiff += newTextLength;
+ } else {
+ // multiline insert
+ // processed in three parts:
+ // 1. First line: everything from start of inserted text to (including) first
+ // line delimiter
+ // 2. Middle lines: everything from first line delimiter (exclusive) to last
+ // line delimiter (inclusive) (may contain additional delimiters)
+ // 3. Last line: everything (including) last line delimiter to end of inserted
+ // text
+
+ final int firstInsertLength = result[0];
+ // newLineCount here is numbers of lines required if text is wrapped -1 because
+ // we start inserting in an existing line and +1 for the first line delimiter we
+ // had found
+ newLineCount = linesIfWrapped(eventOffset - firstWidgetLineOffset + firstInsertLength);
+
+ while (true) {
+ newTextOffset = result[0] + docLegalLineDelimiters[result[1]].length();
+ result = TextUtilities.indexOf(docLegalLineDelimiters, newText, newTextOffset);
+ if (result[1] < 0) {
+ break;
+ }
+ final int insertedLineLength = result[0] - newTextOffset;
+ // new text's middle lines are unaffected from existing content and simply count
+ // as number of lines they need with wrapping
+ newLineCount += linesIfWrapped(insertedLineLength);
+ }
+
+ final int lastTextPartLength = newTextLength - newTextOffset;
+ lastInsertLength = lastTextPartLength;
+ lastDocLineLengthDiff += lastTextPartLength;
+ lastDocLineLengthDiff -= eventOffset - firstWidgetLineOffset;
+ }
+
+ final int lastDocLineIndex = document.getLineOfOffset(eventEnd);
+ final IRegion lastDocLine = document.getLineInformation(lastDocLineIndex);
+
+ int affectedContentAfterRange = 0;
+ if (lastDocLineLengthDiff != 0) {
+ // the content after actual event range moved due to wrapping and must be
+ // reported as replaced lines to StyledTextRenderer to update lines rendering
+ affectedContentAfterRange = lastDocLine.getOffset() + lastDocLine.getLength() - eventEnd;
+ }
+
+ // newLineCount here is numbers of lines required if text is wrapped -1 because
+ // we are prepending an existing line
+ // (and no real line delimiter left to consider)
+ newLineCount += linesIfWrapped(lastInsertLength + affectedContentAfterRange) - 1;
+
+ // replaceLineCount is basically the difference between the index of the last
+ // affected line (line at event offset + replace length) and the index of the
+ // first affected line (line at event offset)
+ // There is one special case caused by the wrapping without delimiter approach.
+ // If the replace (a real replace event at this point, removing as many
+ // characters as inserting) is ending at a virtual wrap border the
+ // lastAffectedOffset would indicate the next line after wrap is affected even
+ // if this line isn't really affected.
+ final int lastAffectedOffset = eventEnd + affectedContentAfterRange;
+ int lastAffectedWidgetLineIndex = getLineOfOffset(lastAffectedOffset);
+ final int lastAffectedWidgetLineOffset = getLineOffset(lastAffectedWidgetLineIndex);
+ if (lastAffectedWidgetLineOffset == lastAffectedOffset && lastDocLine.getOffset() != lastAffectedOffset) {
+ lastAffectedWidgetLineIndex--;
+ }
+ replaceLineCount = lastAffectedWidgetLineIndex - firstWidgetLineIndex;
}
+
+ final TextChangingEvent changingEvent = new TextChangingEvent(this);
+ changingEvent.start = eventOffset;
+ changingEvent.newText = newText;
+ changingEvent.replaceCharCount = eventLength;
+ changingEvent.newCharCount = newTextLength;
+ changingEvent.replaceLineCount = replaceLineCount;
+ changingEvent.newLineCount = newLineCount;
+ return changingEvent;
}
- private void growRegionArray(int minSize) {
- int size = Math.max(offsets.length*2, minSize*2);
- int[] newOffsets = new int[size];
- System.arraycopy(offsets, 0, newOffsets, 0, regionCount);
- offsets = newOffsets;
- int[] newLengths = new int[size];
- System.arraycopy(lengths, 0, newLengths, 0, regionCount);
- lengths = newLengths;
+ /**
+ * For the given text length calculate number of wrapped lines this text
+ * requires with current {@link #fixedConsoleWidth} setting.
+ * <p>
+ * A line ending at the fixed length border will not result in an extra line,
+ * i.e. a line with length of <i>consoleWidth</i> results in one line.
+ * </p>
+ * <p>
+ * Line delimiters are ignored and count like any other character. Since line
+ * delimiters should not cause line wraps this method is intended to be called
+ * with line length excluding line delimiter.
+ * </p>
+ *
+ * @param lineLength line length to calculate
+ * @return number of lines if given length is wrapped with current
+ * {@link #fixedConsoleWidth} (always &gt;= 1)
+ */
+ private int linesIfWrapped(int lineLength) {
+ if (fixedConsoleWidth <= 0 || lineLength <= 0) {
+ return 1;
+ }
+ return ((lineLength - 1) / fixedConsoleWidth) + 1;
}
- private int countNewLines(String string) {
- int count = 0;
+ @Override
+ public synchronized void documentChanged(DocumentEvent event) {
- if (string.length() == 0) {
- return 0;
- }
+ updateWidgetOffsets(event.getOffset());
- // work around to
- // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4994840
- // see bug 84641
- int offset = string.length() - 1;
- while (offset > -1 && string.charAt(offset) == '\r') {
- offset--;
- count++;
+ TextChangedEvent changeEvent = new TextChangedEvent(this);
+ for (TextChangeListener listener : textChangeListeners) {
+ listener.textChanged(changeEvent);
}
- // if offset == -1, the line was all '\r' and there is no string to search for matches (bug 207743)
- if (offset > -1) {
- String str = string;
- if (offset < (str.length() - 1)) {
- str = str.substring(0, offset);
- }
-
- int lastIndex = 0;
- int index = 0;
- Matcher matcher = pattern.matcher(str);
+ if (ASSERT) {
+ verifyPrediction();
+ }
+ }
- while (matcher.find()) {
- index = matcher.start();
+ /**
+ * Update list of widget line offsets.
+ * <p>
+ * <b>Note:</b> the widget line offset lookup is only used for fixed width
+ * console and therefore only updated if fixed width is enabled.
+ * </p>
+ *
+ * @param fromOffset all offsets before given offset are considered valid. Only
+ * update widget offsets from this offset onwards.
+ * @see #widgetLines
+ * @see #widgetLineOffsets
+ */
+ private void updateWidgetOffsets(int fromOffset) {
+ if (document == null) {
+ widgetLines = 0;
+ return;
+ }
- if (index == 0) {
- count++;
- } else if (index != str.length()) {
- count++;
+ final int docLines = document.getNumberOfLines();
+ if (!isFixedWidth()) {
+ widgetLines = docLines;
+ } else {
+ try {
+ final int offset = Math.max(fromOffset, 0);
+ // if someone manages to set the documents text to null the document may return
+ // a negative line number
+ int docLineIndex = Math.max(document.getLineOfOffset(offset), 0);
+ final int docLineOffset = document.getLineOffset(docLineIndex);
+ int widgetLineIndex;
+ if (docLineOffset > 0) {
+ widgetLineIndex = getLineOfOffset(docLineOffset);
+ } else {
+ widgetLineIndex = 0;
+ setLookupEntry(widgetLineIndex, 0);
}
- if (consoleWidth > 0) {
- int lineLen = index - lastIndex + 1;
- if (index == 0) {
- lineLen += lengths[regionCount-1];
+ for (; docLineIndex < docLines; docLineIndex++) {
+ int lineLength = document.getLineInformation(docLineIndex).getLength();
+ for (; lineLength > fixedConsoleWidth; lineLength -= fixedConsoleWidth) {
+ widgetLineIndex++;
+ setLookupEntry(widgetLineIndex, widgetLineOffsets[widgetLineIndex - 1] + fixedConsoleWidth);
}
- count += lineLen/consoleWidth;
- }
- lastIndex = index;
+ final String docLineDelimiter = document.getLineDelimiter(docLineIndex);
+ if (docLineDelimiter != null) {
+ widgetLineIndex++;
+ setLookupEntry(widgetLineIndex,
+ widgetLineOffsets[widgetLineIndex - 1] + lineLength + docLineDelimiter.length());
+ }
+ }
+ widgetLines = widgetLineIndex + 1;
+ } catch (BadLocationException e) {
+ // should be impossible if document is not changed meanwhile
+ log(e);
}
}
- return count;
}
+ /**
+ * Set an entry in the {@link #widgetLineOffsets} lookup. Will grow the lookup
+ * array if necessary.
+ *
+ * @param widgetLineIndex the lookup entry to set (does not have to exist yet)
+ * @param value the entry value to set
+ */
+ private void setLookupEntry(int widgetLineIndex, int value) {
+ ensureOffsetsCapacity(widgetLineIndex + 1);
+ widgetLineOffsets[widgetLineIndex] = value;
+ }
- @Override
- public synchronized void documentChanged(DocumentEvent event) {
- if (document == null) {
- return;
+ /**
+ * Ensure the offset lookup array can store at least <i>size</i> entries.
+ *
+ * @param requiredSize minimum size lookup array should have after
+ * @see #widgetLineOffsets
+ */
+ private void ensureOffsetsCapacity(int requiredSize) {
+ final int oldSize = widgetLineOffsets != null ? widgetLineOffsets.length : 0;
+ if (oldSize < requiredSize) {
+ final int[] oldWidgetOffsets = widgetLineOffsets;
+ widgetLineOffsets = new int[requiredSize + GROW];
+ if (oldSize > 0) {
+ System.arraycopy(oldWidgetOffsets, 0, widgetLineOffsets, 0, oldSize);
+ }
}
+ }
- repairLines(event.fOffset);
-
- TextChangedEvent changeEvent = new TextChangedEvent(this);
+ /**
+ * Check if fixed width console feature is enabled or not.
+ *
+ * @return true if lines are wrapped at fixed width
+ * @see #getWidth()
+ */
+ public boolean isFixedWidth() {
+ return fixedConsoleWidth > 0;
+ }
- for (TextChangeListener listener : textChangeListeners) {
- listener.textChanged(changeEvent);
- }
+ /**
+ * Get current consoles fixed width.
+ *
+ * @return current fixed console width or &lt;= 0 if disabled
+ * @see #isFixedWidth()
+ */
+ public int getWidth() {
+ return fixedConsoleWidth;
}
/**
- * sets consoleWidth, repairs line information, then fires event to the viewer text widget.
- * @param width The console's width
+ * Set new fixed console width. Also signals text viewer widget to render
+ * content with new fixed width.
+ *
+ * @param width the fixed console width or &lt;= 0 to not break lines at fixed
+ * width
*/
public void setWidth(int width) {
- if (width != consoleWidth) {
- consoleWidth = width;
- repairLines(0);
+ if (width != fixedConsoleWidth) {
+ fixedConsoleWidth = width;
+ updateWidgetOffsets(0);
TextChangedEvent changeEvent = new TextChangedEvent(this);
for (TextChangeListener listener : textChangeListeners) {
listener.textSet(changeEvent);
}
}
}
+
+ /**
+ * Log an exception and include if error occurred in fixed or non fixed width
+ * mode.
+ *
+ * @param error the error to log
+ */
+ private void log(Throwable error) {
+ ConsolePlugin.log(ConsolePlugin.newErrorStatus("fixed width: " + isFixedWidth(), error)); //$NON-NLS-1$
+ }
+
+ // +---------------------+
+ // | Debug and test code |
+ // +---------------------+
+ /**
+ * The expected document length after next document change is applied. Only used
+ * if {@link #ASSERT} is <code>true</code>.
+ */
+ private int predictedCharCount;
+ /**
+ * The expected number of lines after next document change is applied. Only used
+ * if {@link #ASSERT} is <code>true</code>.
+ */
+ private int predictedLines;
+
+ /**
+ * Set the expected document length and lines from the current document state
+ * and the changes predicted by the changing event.
+ *
+ * @param event the {@link TextChangingEvent} describing the next document
+ * change
+ */
+ private void updatePrediction(TextChangingEvent event) {
+ predictedCharCount = getCharCount() + event.newCharCount - event.replaceCharCount;
+ predictedLines = getLineCount() + event.newLineCount - event.replaceLineCount;
+ }
+
+ /**
+ * Check if the last change prediction was correct. Throws an
+ * AssertionFailedException if the prediction was wrong.
+ */
+ private void verifyPrediction() {
+ final int charCount = getCharCount();
+ Assert.isTrue(predictedCharCount == charCount, String.format(java.util.Locale.ROOT,
+ "Wrong char count. Expected<%d>, Actual<%d>", predictedCharCount, charCount)); //$NON-NLS-1$
+ final int lineCount = getLineCount();
+ Assert.isTrue(predictedLines == lineCount, String.format(java.util.Locale.ROOT,
+ "Wrong line count. Expected<%d>, Actual<%d>", predictedLines, lineCount)); //$NON-NLS-1$
+ }
}

Back to the top