package org.robotframework.ide.eclipse.main.plugin.tableeditor.source.colouring; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.ITextInputListener; import org.eclipse.jface.text.rules.IToken; import org.rf.ide.core.testdata.model.RobotFileOutput; import org.robotframework.ide.eclipse.main.plugin.tableeditor.source.RobotDocument; import org.robotframework.ide.eclipse.main.plugin.tableeditor.source.RobotDocument.IRobotDocumentParsingListener; import org.robotframework.ide.eclipse.main.plugin.tableeditor.source.colouring.ISyntaxColouringRule.PositionedTextToken; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Range; public class RedTokensStore implements ITextInputListener, IDocumentListener, IRobotDocumentParsingListener { private RobotDocument document; private final List<PositionedTextToken> tokens = new ArrayList<>(); public void installFor(final RobotDocument document) { if (this.document != null && this.document != document) { throw new IllegalStateException("The document got replaced!"); } if (this.document == null) { this.document = document; // it has to know first about changes in order to update tokens, so that // painting will use properly adjusted cached tokens this.document.addFirstDocumentListener(this); this.document.addParseListener(this); } } @Override public void inputDocumentAboutToBeChanged(final IDocument oldInput, final IDocument newInput) { // document will be different, so we need to install it again when needed if (this.document != null) { document.removeDocumentListener(this); document.removeParseListener(this); } document = null; } @Override public void inputDocumentChanged(final IDocument oldInput, final IDocument newInput) { // nothing to do } @Override public void documentAboutToBeChanged(final DocumentEvent event) { // nothing to do } @Override public void documentChanged(final DocumentEvent event) { if (event.getDocument() != document) { return; } synchronized (this) { if (document.hasNewestModel()) { return; } final int damageOffset = event.getOffset(); final int delta = event.getText().length() - event.getLength(); updatePositions(damageOffset, delta); } } @Override public void reparsingFinished(final RobotFileOutput parsedOutput) { synchronized (this) { // when reparsing has been finished we can remove all the tokens, so that // the scanner will take tokens directly from reparsed output instead // of a store tokens.clear(); } } List<PositionedTextToken> tokensAt(final int offset) { final Range<Integer> range = entriesAt(offset); final ArrayList<PositionedTextToken> entries = new ArrayList<>(); if (range == null) { return entries; } for (int i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) { entries.add(tokens.get(i)); } return entries; } private Range<Integer> entriesAt(final int offset) { if (tokens.isEmpty()) { return null; } int foundItemIndex = binarySearch(offset); if (foundItemIndex < 0) { foundItemIndex = -foundItemIndex - 1; if (foundItemIndex == 0 || tokens.get(foundItemIndex - 1).getOffset() + tokens.get(foundItemIndex - 1).getLength() <= offset) { return null; } // -1 additionally because it lays in previous segment foundItemIndex--; } int min = foundItemIndex; int i = foundItemIndex - 1; while (i >= 0 && tokens.get(i).getOffset() == offset) { min--; i--; } int max = foundItemIndex; i = foundItemIndex + 1; while (i < tokens.size() && tokens.get(i).getOffset() == offset) { max++; i++; } return Range.closed(min, max); } public void insert(final int offset, final int length, final IToken token) { final int foundItemIndex = binarySearch(offset); if (foundItemIndex >= 0) { if (tokens.get(foundItemIndex).getLength() == 0) { // we allow to have marker tokens of 0 length at the same position tokens.add(foundItemIndex + 1, new PositionedTextToken(token, offset, length)); } else if (length == 0) { tokens.add(foundItemIndex, new PositionedTextToken(token, offset, length)); } else { tokens.set(foundItemIndex, new PositionedTextToken(token, offset, length)); } } else { final int wouldBeIndex = -foundItemIndex - 1; tokens.add(wouldBeIndex, new PositionedTextToken(token, offset, length)); } } @VisibleForTesting void updatePositions(final int damageOffset, final int delta) { if (tokens.isEmpty() || delta == 0) { // either there is nothing to update, or the damage will be colored with old style // and changed later return; } final int foundItemIndex = binarySearch(damageOffset); final int startIndex = foundItemIndex >= 0 ? foundItemIndex : Math.max(0, -foundItemIndex - 2); final PositionedTextToken firstEntry = tokens.get(startIndex); if (delta > 0) { firstEntry.setLength(firstEntry.getLength() + delta); for (int i = startIndex + 1; i < tokens.size(); i++) { final PositionedTextToken entry = tokens.get(i); entry.setOffset(entry.getOffset() + delta); } } else { int toRemove = -delta; int length = firstEntry.getLength(); firstEntry.setLength( Math.max(damageOffset - firstEntry.getOffset(), firstEntry.getLength() - toRemove)); int removedSoFar = length - firstEntry.getLength(); toRemove -= removedSoFar; int i = startIndex + 1; while (i < tokens.size()) { final PositionedTextToken entry = tokens.get(i); entry.setOffset(entry.getOffset() - removedSoFar); if (toRemove > 0) { length = entry.getLength(); entry.setLength(Math.max(0, length - toRemove)); removedSoFar += length - entry.getLength(); toRemove -= length - entry.getLength(); } if (entry.getLength() == 0 && (!entry.getToken().isEOF() || tokens.get(i - 1).getToken().isEOF())) { tokens.remove(i); } else { i++; } } if (tokens.get(startIndex).getLength() == 0 && !tokens.get(startIndex).getToken().isEOF()) { tokens.remove(startIndex); } } } private int binarySearch(final int offset) { // works similarly to Collections#binarySearch() although does not require comparator and // works only on offsets int low = 0; int high = tokens.size() - 1; while (low <= high) { final int mid = (low + high) >>> 1; final int cmp = Integer.compare(tokens.get(mid).getOffset(), offset); if (cmp < 0) { low = mid + 1; } else if (cmp > 0) { high = mid - 1; } else { return mid; // found } } return -(low + 1); // not found } public List<PositionedTextToken> getTokens() { return Collections.unmodifiableList(tokens); } @Override public String toString() { // for debugging purposes only return tokens.toString(); } }