/* * Copyright (c) 2012 Sam Harwell, Tunnel Vision Laboratories LLC * All rights reserved. * * The source code of this document is proprietary work, and is not licensed for * distribution. For information about licensing, contact Sam Harwell at: * sam@tunnelvisionlabs.com */ package org.antlr.netbeans.editor.text.impl; import java.util.ArrayList; import java.util.List; import org.antlr.netbeans.editor.text.DocumentChange; import org.antlr.netbeans.editor.text.NormalizedDocumentChangeCollection; import org.antlr.v4.runtime.misc.IntegerList; import org.netbeans.api.annotations.common.NonNull; import org.openide.util.Parameters; /** * * @author Sam Harwell */ public class LineTextCache { private static int MaximumBlockLength = 64; private final int _length; private final int _lineCount; private final ArrayList<ArrayList<String>> _lineData = new ArrayList<>(); private final IntegerList _blockOffsets = new IntegerList(); private final IntegerList _blockLineOffsets = new IntegerList(); private final ArrayList<IntegerList> _lineOffsets = new ArrayList<>(); public LineTextCache(@NonNull String data) { Parameters.notNull("data", data); _length = data.length(); ArrayList<String> currentBlock = new ArrayList<>(); IntegerList currentOffsets = new IntegerList(); @SuppressWarnings("LocalVariableHidesMemberVariable") int lineCount = 0; int blockStart = 0; int blockLineStart = 0; int lineStart = 0; for (int i = 0; i <= data.length(); i++) { if (i == data.length() || data.charAt(i) == '\n') { int lineEnd = Math.min(i + 1, data.length()); currentBlock.add(data.substring(lineStart, lineEnd)); currentOffsets.add(lineStart - blockStart); lineCount++; lineStart = i + 1; if (i == data.length() || currentBlock.size() == MaximumBlockLength) { _lineData.add(currentBlock); _lineOffsets.add(currentOffsets); _blockOffsets.add(blockStart); _blockLineOffsets.add(blockLineStart); currentBlock = new ArrayList<>(); currentOffsets = new IntegerList(); blockStart = lineStart; blockLineStart = lineCount; } } } this._lineCount = lineCount; trimToSize(); } private LineTextCache(int length, int lineCount) { this._length = length; this._lineCount = lineCount; } public int getLength() { return _length; } public int getLineCount() { return _lineCount; } public @NonNull ArrayList<ArrayList<String>> getLineData() { return _lineData; } public @NonNull IntegerList getBlockOffsets() { return _blockOffsets; } public @NonNull IntegerList getBlockLineOffsets() { return _blockLineOffsets; } public @NonNull ArrayList<IntegerList> getLineOffsets() { return _lineOffsets; } public int getBlockFromLineNumber(int lineNumber) { int block = _blockLineOffsets.binarySearch(lineNumber); if (block < 0) { block = -(block + 1) - 1; } return block; } public int getBlockFromPosition(int position) { int block = _blockOffsets.binarySearch(position); if (block < 0) { block = -(block + 1) - 1; } return block; } public int getBlockLineFromPosition(int block, int position) { int blockLine = _lineOffsets.get(block).binarySearch(position - _blockOffsets.get(block)); if (blockLine < 0) { blockLine = -(blockLine + 1) - 1; } return blockLine; } public int getLineNumberFromPosition(int position) { int block = getBlockFromPosition(position); int blockLine = getBlockLineFromPosition(block, position); return blockLine + _blockLineOffsets.get(block); } public @NonNull LineTextCache applyChanges(@NonNull NormalizedDocumentChangeCollection changes) { int delta = 0; int lineDelta = 0; for (DocumentChange change : changes) { delta += change.getDelta(); lineDelta += change.getLineCountDelta(); } LineTextCache next = new LineTextCache(this._length + delta, this._lineCount + lineDelta); int oldBlock = 0; int oldLine = 0; int oldColumn = 0; int oldBlockLine = 0; // index of oldLine within oldBlock int newPosition = 0; ArrayList<String> modifiedBlock = null; for (int i = 0; i <= changes.size(); i++) { DocumentChange currentChange = i < changes.size() ? changes.get(i) : null; /* * move forward to this change */ // 1. finish off the current line if necessary boolean previousLineEnded = oldColumn == 0; if (previousLineEnded && modifiedBlock != null && !modifiedBlock.isEmpty()) { String lastLine = modifiedBlock.get(modifiedBlock.size() - 1); previousLineEnded = lastLine.charAt(lastLine.length() - 1) == '\n'; } if (!previousLineEnded && (currentChange == null || lineEndsBeforeChange(oldBlock, oldLine, currentChange))) { if (modifiedBlock == null) { modifiedBlock = new ArrayList<>(); } int index = modifiedBlock.size() - 1; String text = index >= 0 ? modifiedBlock.get(index) : null; String oldLineText = getLineText(oldBlock, oldLine); String appendText = oldLineText.substring(oldColumn); if (text == null || text.charAt(text.length() - 1) == '\n') { modifiedBlock.add(appendText); } else { modifiedBlock.set(index, text + appendText); } newPosition += appendText.length(); oldLine++; oldColumn = 0; oldBlockLine++; if (oldBlockLine == _lineData.get(oldBlock).size()) { oldBlock++; oldBlockLine = 0; } } // 2. finish off the current block if necessary if (modifiedBlock != null && (currentChange == null || blockEndsBeforeChange(oldBlock, currentChange))) { assert oldColumn == 0 : "Should be at the beginning of a line."; if (oldBlock == _lineData.size()) { // special handling for updates at the end of the last block if (!modifiedBlock.isEmpty()) { next._lineData.add(modifiedBlock); } modifiedBlock = null; } else { List<String> remainingLines = _lineData.get(oldBlock).subList(oldBlockLine, _lineData.get(oldBlock).size()); if (modifiedBlock.size() + _lineData.get(oldBlock).size() - oldBlockLine < MaximumBlockLength) { modifiedBlock.addAll(remainingLines); for (String text : remainingLines) { newPosition += text.length(); } if (!modifiedBlock.isEmpty()) { next._lineData.add(modifiedBlock); } } else { if (!modifiedBlock.isEmpty()) { next._lineData.add(modifiedBlock); } if (oldBlockLine > 0) { next._lineData.add(new ArrayList<>(remainingLines)); } else { next._lineData.add(_lineData.get(oldBlock)); } for (String text : remainingLines) { newPosition += text.length(); } } modifiedBlock = null; oldLine = _blockLineOffsets.get(oldBlock) + _lineData.get(oldBlock).size(); oldBlock++; oldBlockLine = 0; } } // 3. move any whole blocks we can while (oldBlock < _lineData.size() && (currentChange == null || blockEndsBeforeChange(oldBlock, currentChange))) { if (modifiedBlock != null) { if (!modifiedBlock.isEmpty()) { next._lineData.add(modifiedBlock); } modifiedBlock = null; } newPosition += getBlockEnd(oldBlock) - _blockOffsets.get(oldBlock); next._lineData.add(_lineData.get(oldBlock)); oldLine = _blockLineOffsets.get(oldBlock) + _lineData.get(oldBlock).size(); oldBlock++; oldBlockLine = 0; } // 4. now move any whole lines we can while (oldLine < getLineCount() && (currentChange == null || lineEndsBeforeChange(oldBlock, oldLine, currentChange))) { if (modifiedBlock != null && modifiedBlock.size() == MaximumBlockLength) { next._lineData.add(modifiedBlock); modifiedBlock = null; } if (modifiedBlock == null) { modifiedBlock = new ArrayList<>(); } modifiedBlock.add(_lineData.get(oldBlock).get(oldBlockLine)); newPosition += modifiedBlock.get(modifiedBlock.size() - 1).length(); oldLine++; oldBlockLine++; assert oldBlockLine < _lineData.get(oldBlock).size() : "Should have replaced the rest of the block in stop 2 or 3."; } if (currentChange != null) { // 5. move a partial line if necessary int oldPosition = getPosition(oldBlock, oldLine, oldColumn); if (oldPosition < currentChange.getOldOffset()) { if (modifiedBlock == null) { modifiedBlock = new ArrayList<>(); } int index = modifiedBlock.size() - 1; String text = index >= 0 ? modifiedBlock.get(index) : null; String oldLineText = getLineText(oldBlock, oldLine); String appendText = oldLineText.substring(oldColumn, oldColumn + currentChange.getOldOffset() - oldPosition); if (text == null || text.charAt(text.length() - 1) == '\n') { modifiedBlock.add(appendText); } else { modifiedBlock.set(index, text + appendText); } oldColumn += currentChange.getOldOffset() - oldPosition; newPosition += appendText.length(); assert !modifiedBlock.get(modifiedBlock.size() - 1).endsWith("\n") : "Should have replaced the end of the line in step 1 or 4."; } /* * apply the change */ assert newPosition == currentChange.getNewOffset() : "Moves are not supported... should be synchronized at this point."; int lineStart = 0; String newText = currentChange.getNewText(); for (int j = 0; j <= newText.length(); j++) { if (j == newText.length() || newText.charAt(j) == '\n') { if (modifiedBlock == null) { modifiedBlock = new ArrayList<>(); } int index = modifiedBlock.size() - 1; String text = index >= 0 ? modifiedBlock.get(index) : null; int lineEnd = Math.min(j + 1, newText.length()); if (lineStart < lineEnd) { String appendText = newText.substring(lineStart, lineEnd); if (text == null || text.charAt(text.length() - 1) == '\n') { modifiedBlock.add(appendText); } else { modifiedBlock.set(index, text + appendText); } lineStart = lineEnd; if (j == newText.length() - 1) { break; } } } } // update the current position oldPosition = getPosition(oldBlock, oldLine, oldColumn) + currentChange.getOldLength(); oldBlock = getBlockFromPosition(oldPosition); oldLine = getLineNumberFromPosition(oldPosition); oldColumn = oldPosition - getLineStart(oldBlock, oldLine); oldBlockLine = oldLine - _blockLineOffsets.get(oldBlock); newPosition += newText.length(); } } if (modifiedBlock != null) { if (!modifiedBlock.isEmpty()) { next._lineData.add(modifiedBlock); } modifiedBlock = null; } // now set all the indexes in the new cache int newBlockOffset = 0; // offset of the start of the current block int newLine = 0; // absolute line number for (int newBlock = 0; newBlock < next._lineData.size(); newBlock++) { ArrayList<String> block = next._lineData.get(newBlock); next._blockOffsets.add(newBlockOffset); next._blockLineOffsets.add(newLine); next._lineOffsets.add(new IntegerList(MaximumBlockLength)); int blockLineOffset = 0; for (int newBlockLine = 0; newBlockLine < block.size(); newBlockLine++) { next._lineOffsets.get(newBlock).add(blockLineOffset); blockLineOffset += block.get(newBlockLine).length(); newLine++; // all lines before last end with \n assert (newBlock == next._lineData.size() - 1 && newBlockLine == block.size() - 1) || block.get(newBlockLine).endsWith("\n"); // last line does not end with \n assert newBlock < next._lineData.size() - 1 || newBlockLine < block.size() - 1 || !block.get(newBlockLine).endsWith("\n"); } assert next._lineOffsets.get(newBlock).size() == block.size(); newBlockOffset += blockLineOffset; } trimToSize(); assert next._blockOffsets.size() == next._lineData.size(); assert next._blockLineOffsets.size() == next._lineData.size(); assert next._lineOffsets.size() == next._lineData.size(); assert newBlockOffset == next._length; assert newLine == next._lineCount; // assert next.getLineCount() == next.blockOffsets.get(next.lineData.size() - 1) + next.lineData.get(next.lineData.size() - 1).size() : "Line counts should match"; // assert next.getLength() == next.getBlockEnd(next.lineData.size() - 1) : "Lengths should match"; return next; } public boolean lineEndsBeforeChange(int block, int line, DocumentChange change) { if (getLineEnd(block, line) < change.getOldOffset()) { return true; } return getLineEnd(block, line) == change.getOldOffset() && line < getLineCount() - 1; } public boolean blockEndsBeforeChange(int block, DocumentChange change) { if (getBlockEnd(block) < change.getOldOffset()) { return true; } return getBlockEnd(block) == change.getOldOffset() && block < _lineData.size() - 1; } public int getBlockEnd(int block) { if (block == _lineData.size() - 1) { return getLength(); } return _blockOffsets.get(block + 1); } public int getPosition(int block, int line, int column) { int blockLine = line - _blockLineOffsets.get(block); return _blockOffsets.get(block) + _lineOffsets.get(block).get(blockLine) + column; } public int getLineStart(int block, int line) { int blockLine = line - _blockLineOffsets.get(block); return _blockOffsets.get(block) + _lineOffsets.get(block).get(blockLine); } public int getLineEnd(int block, int line) { int blockLine = line - _blockLineOffsets.get(block); if (blockLine == _lineData.get(block).size() - 1) { return getBlockEnd(block); } return _blockOffsets.get(block) + _lineOffsets.get(block).get(blockLine + 1); } public @NonNull String getLineText(int block, int line) { int blockLine = line - _blockLineOffsets.get(block); return _lineData.get(block).get(blockLine); } private void trimToSize() { _lineData.trimToSize(); _blockOffsets.trimToSize(); _blockLineOffsets.trimToSize(); _lineOffsets.trimToSize(); for (ArrayList<?> list : _lineData) { list.trimToSize(); } for (IntegerList list : _lineOffsets) { list.trimToSize(); } } }