make document's LineSet immutable

This commit is contained in:
peter
2015-07-06 13:18:50 +02:00
parent ba275cc6c5
commit 503038da45
4 changed files with 162 additions and 341 deletions

View File

@@ -277,8 +277,7 @@ public class TabPostFormatProcessorTest {
allowing(myDocument).getTextLength(); will(returnValue(text.length()));
}});
final LineSet lines = new LineSet();
lines.documentCreated(myDocument);
final LineSet lines = LineSet.createLineSet(myDocument.getCharsSequence());
myMockery.checking(new Expectations() {{
allowing(myDocument).getLineNumber(with(any(int.class))); will(new CustomAction("getLineNumber()") {
@Override

View File

@@ -149,8 +149,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
synchronized (myLineSetLock) {
lineSet = myLineSet;
if (lineSet == null) {
lineSet = new LineSet();
lineSet.documentCreated(this);
lineSet = LineSet.createLineSet(myText);
myLineSet = lineSet;
}
}
@@ -205,9 +204,9 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
markers.add(marker);
}
}
LineSet lineSet = getLineSet();
lineLoop:
for (int line = 0; line < lineSet.getLineCount(); line++) {
for (int line = 0; line < getLineCount(); line++) {
LineSet lineSet = getLineSet();
if (inChangedLinesOnly && !lineSet.isModified(line)) continue;
int whiteSpaceStart = -1;
final int lineEnd = lineSet.getLineEnd(line) - lineSet.getSeparatorLength(line);
@@ -636,7 +635,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
@Override
public void clearLineModificationFlags() {
getLineSet().clearModificationFlags();
myLineSet = getLineSet().clearModificationFlags();
}
void clearLineModificationFlagsExcept(@NotNull int[] caretLines) {
@@ -647,10 +646,11 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
modifiedLines.add(line);
}
}
clearLineModificationFlags();
lineSet = lineSet.clearModificationFlags();
for (int i = 0; i < modifiedLines.size(); i++) {
lineSet.setModified(modifiedLines.get(i));
lineSet = lineSet.setModified(modifiedLines.get(i));
}
myLineSet = lineSet;
}
private void updateText(@NotNull ImmutableText newText,
@@ -673,8 +673,9 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
}
}
myTextString = null;
ImmutableText prevText = myText;
myText = newText;
changedUpdate(event, newModificationStamp);
changedUpdate(event, newModificationStamp, prevText);
}
finally {
if (!enableRecursiveModifications) {
@@ -724,11 +725,11 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
}
}
private void changedUpdate(@NotNull DocumentEvent event, long newModificationStamp) {
private void changedUpdate(@NotNull DocumentEvent event, long newModificationStamp, ImmutableText prevText) {
try {
if (LOG.isDebugEnabled()) LOG.debug(event.toString());
getLineSet().changedUpdate(event);
myLineSet = getLineSet().update(prevText, event.getOffset(), event.getOffset() + event.getOldLength(), event.getNewFragment(), event.isWholeTextReplaced());
if (myTabTrackingRequestors > 0) {
updateMightContainTabs(event.getNewFragment());
}
@@ -885,7 +886,6 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx {
@Override
public final int getLineCount() {
if (getTextLength() == 0) return 0;
int lineCount = getLineSet().getLineCount();
assert lineCount >= 0;
return lineCount;

View File

@@ -15,31 +15,134 @@
*/
package com.intellij.openapi.editor.impl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.LineIterator;
import com.intellij.openapi.editor.ex.util.SegmentArrayWithData;
import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.util.text.MergingCharSequence;
import gnu.trove.TIntArrayList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import java.util.Arrays;
/**
* Data structure specialized for working with document text lines, i.e. stores information about line mapping to document
* offsets and provides convenient ways to work with that information like retrieving target line by document offset etc.
* <p/>
* Not thread-safe.
* Immutable.
*/
public class LineSet{
private SegmentArrayWithData mySegments = new SegmentArrayWithData();
private static final int MODIFIED_MASK = 0x4;
private static final int SEPARATOR_MASK = 0x3;
private final int[] myStarts;
private final int[] myFlags;
private final int myLength;
private LineSet(int[] starts, int[] flags, int length) {
myStarts = starts;
myFlags = flags;
myLength = length;
}
public static LineSet createLineSet(CharSequence text) {
return createLineSet(text, false);
}
private static LineSet createLineSet(CharSequence text, boolean markModified) {
TIntArrayList starts = new TIntArrayList();
TIntArrayList flags = new TIntArrayList();
LineTokenizer lineTokenizer = new LineTokenizer(text);
while (!lineTokenizer.atEnd()) {
starts.add(lineTokenizer.getOffset());
flags.add(lineTokenizer.getLineSeparatorLength() | (markModified ? MODIFIED_MASK : 0));
lineTokenizer.advance();
}
return new LineSet(starts.toNativeArray(), flags.toNativeArray(), text.length());
}
LineSet update(CharSequence prevText, int _start, int _end, CharSequence replacement, boolean wholeTextReplaced) {
if (myLength == 0) {
return createLineSet(replacement, !wholeTextReplaced);
}
int startOffset = _start;
if (replacement.length() > 0 && replacement.charAt(0) == '\n' && startOffset > 0 && prevText.charAt(startOffset - 1) == '\r') {
startOffset--;
}
int startLine = findLineIndex(startOffset);
startOffset = getLineStart(startLine);
int endOffset = _end;
if (replacement.length() > 0 && replacement.charAt(replacement.length() - 1) == '\r' && endOffset < prevText.length() && prevText.charAt(endOffset) == '\n') {
endOffset++;
}
int endLine = findLineIndex(endOffset);
endOffset = getLineEnd(endLine);
if (!isLastEmptyLine(endLine)) endLine++;
replacement = new MergingCharSequence(
new MergingCharSequence(prevText.subSequence(startOffset, _start), replacement),
prevText.subSequence(_end, endOffset));
LineSet patch = createLineSet(replacement, true);
LineSet applied = applyPatch(startOffset, endOffset, startLine, endLine, patch);
if (doTest) {
final MergingCharSequence newText = new MergingCharSequence(
new MergingCharSequence(prevText.subSequence(0, startOffset), replacement),
prevText.subSequence(endOffset, prevText.length()));
applied.checkEquals(createLineSet(newText));
}
return wholeTextReplaced ? applied.clearModificationFlags() : applied;
}
private void checkEquals(LineSet fresh) {
if (getLineCount() != fresh.getLineCount()) {
throw new AssertionError();
}
for (int i = 0; i < getLineCount(); i++) {
boolean start = getLineStart(i) != fresh.getLineStart(i);
boolean end = getLineEnd(i) != fresh.getLineEnd(i);
boolean sep = getSeparatorLength(i) != fresh.getSeparatorLength(i);
if (start || end || sep) {
throw new AssertionError();
}
}
}
@NotNull
private LineSet applyPatch(int startOffset, int endOffset, int startLine, int endLine, LineSet patch) {
int lineShift = patch.myStarts.length - (endLine - startLine);
int lengthShift = patch.myLength - (endOffset - startOffset);
int newLineCount = myStarts.length + lineShift;
int[] starts = new int[newLineCount];
int[] flags = new int[newLineCount];
for (int i = 0; i < startLine; i++) {
starts[i] = myStarts[i];
flags[i] = myFlags[i];
}
for (int i = 0; i < patch.myStarts.length; i++) {
starts[startLine + i] = patch.myStarts[i] + startOffset;
flags[startLine + i] = patch.myFlags[i];
}
for (int i = endLine; i < myStarts.length; i++) {
starts[lineShift + i] = myStarts[i] + lengthShift;
flags[lineShift + i] = myFlags[i];
}
return new LineSet(starts, flags, myLength + lengthShift);
}
public int findLineIndex(int offset) {
int lineIndex = mySegments.findSegmentIndex(offset);
assert lineIndex >= 0;
return lineIndex;
if (offset < 0 || offset > myLength) {
throw new IndexOutOfBoundsException("Wrong offset: " + offset + ". Should be in range: [0, " + myLength + "]");
}
if (myLength == 0) return 0;
if (offset == myLength) return getLineCount() - 1;
int bsResult = Arrays.binarySearch(myStarts, offset);
return bsResult >= 0 ? bsResult : -bsResult - 2;
}
public LineIterator createIterator() {
@@ -47,340 +150,60 @@ public class LineSet{
}
public final int getLineStart(int index) {
int lineStart = mySegments.getSegmentStart(index);
assert lineStart >= 0;
return lineStart;
checkLineIndex(index);
return isLastEmptyLine(index) ? myLength : myStarts[index];
}
private boolean isLastEmptyLine(int index) {
return index == myFlags.length && index > 0 && (myFlags[index - 1] & SEPARATOR_MASK) > 0;
}
public final int getLineEnd(int index) {
return mySegments.getSegmentEnd(index);
checkLineIndex(index);
return index >= myStarts.length - 1 ? myLength : myStarts[index + 1];
}
private void checkLineIndex(int index) {
if (index < 0 || index >= getLineCount()) {
throw new IndexOutOfBoundsException("Wrong line: " + index + ". Available lines count: " + getLineCount());
}
}
final boolean isModified(int index) {
return (mySegments.getSegmentData(index) & MODIFIED_MASK) != 0;
checkLineIndex(index);
return !isLastEmptyLine(index) && (myFlags[index] & MODIFIED_MASK) != 0;
}
final void setModified(int index) {
setSegmentModified(mySegments, index);
final LineSet setModified(int index) {
if (isLastEmptyLine(index) || isModified(index)) return this;
int[] flags = myFlags.clone();
flags[index] |= MODIFIED_MASK;
return new LineSet(myStarts, flags, myLength);
}
LineSet clearModificationFlags() {
int[] flags = myFlags.clone();
for (int i = 0; i < flags.length; i++) {
flags[i] &= ~MODIFIED_MASK;
}
return new LineSet(myStarts, flags, myLength);
}
final int getSeparatorLength(int index) {
return mySegments.getSegmentData(index) & SEPARATOR_MASK;
checkLineIndex(index);
return index < myFlags.length ? myFlags[index] & SEPARATOR_MASK : 0;
}
final int getLineCount() {
return mySegments.getSegmentCount();
}
public void documentCreated(@NotNull Document document) {
initSegments(document.getCharsSequence(), false);
}
public void changedUpdate(DocumentEvent e1) {
DocumentEventImpl e = (DocumentEventImpl) e1;
if (e.isOnlyOneLineChanged() && mySegments.getSegmentCount() > 0) {
processOneLineChange(e);
} else {
if (mySegments.getSegmentCount() == 0 || e.getStartOldIndex() >= mySegments.getSegmentCount() ||
e.getStartOldIndex() < 0) {
initSegments(e.getDocument().getCharsSequence(), true);
return;
}
final int optimizedLineShift = e.getOptimizedLineShift();
if (optimizedLineShift != -1) {
processOptimizedMultilineInsert(e, optimizedLineShift);
} else {
final int optimizedOldLineShift = e.getOptimizedOldLineShift();
if (optimizedOldLineShift != -1) {
processOptimizedMultilineDelete(e, optimizedOldLineShift);
} else {
processMultilineChange(e);
}
}
}
if (e.isWholeTextReplaced()) {
clearModificationFlags();
}
return myStarts.length + (isLastEmptyLine(myStarts.length) ? 1 : 0);
}
@TestOnly
public static void setTestingMode(boolean testMode) {
assert ApplicationManager.getApplication().isUnitTestMode();
doTest = testMode;
}
private static boolean doTest = false;
private void processOptimizedMultilineDelete(final DocumentEventImpl e, final int optimizedLineShift) {
final int insertionPoint = e.getOffset();
final int changedLineIndex = e.getStartOldIndex();
final int lengthDiff = e.getOldLength();
SegmentArrayWithData workingCopySegmentsForTesting = null;
SegmentArrayWithData segments; //
if (doTest) {
segments = new SegmentArrayWithData();
workingCopySegmentsForTesting = new SegmentArrayWithData();
fillSegments(segments, workingCopySegmentsForTesting);
} else {
segments = mySegments;
}
final int oldSegmentStart = segments.getSegmentStart(changedLineIndex);
final int lastChangedEnd = segments.getSegmentEnd(changedLineIndex + optimizedLineShift);
final short lastChangedData = segments.getSegmentData(changedLineIndex + optimizedLineShift);
final int newSegmentEnd = oldSegmentStart + (insertionPoint - oldSegmentStart) + (lastChangedEnd - insertionPoint - lengthDiff);
segments.remove(changedLineIndex, changedLineIndex + optimizedLineShift);
if (newSegmentEnd != 0) {
segments.setElementAt(
changedLineIndex,
oldSegmentStart, newSegmentEnd,
lastChangedData | MODIFIED_MASK
);
} else {
segments.remove(changedLineIndex, changedLineIndex + 1);
}
// update data after lineIndex, shifting with optimizedLineShift
final int segmentCount = segments.getSegmentCount();
for(int i = changedLineIndex + 1; i < segmentCount; ++i) {
segments.setElementAt(i, segments.getSegmentStart(i) - lengthDiff,
segments.getSegmentEnd(i) - lengthDiff,
segments.getSegmentData(i)
);
}
if (doTest) {
final SegmentArrayWithData data = mySegments;
mySegments = segments;
addEmptyLineAtEnd();
doCheckResults(workingCopySegmentsForTesting, e, data, segments);
} else {
addEmptyLineAtEnd();
}
}
private void processOptimizedMultilineInsert(final DocumentEventImpl e, final int optimizedLineShift) {
final int insertionPoint = e.getOffset();
final int changedLineIndex = e.getStartOldIndex();
final int lengthDiff = e.getNewLength();
final LineTokenizer tokenizer = new LineTokenizer(e.getNewFragment());
SegmentArrayWithData workingCopySegmentsForTesting = null;
SegmentArrayWithData segments; //
if (doTest) {
segments = new SegmentArrayWithData();
workingCopySegmentsForTesting = new SegmentArrayWithData();
fillSegments(segments, workingCopySegmentsForTesting);
} else {
segments = mySegments;
}
int i;
// update data after lineIndex, shifting with optimizedLineShift
for(i = segments.getSegmentCount() - 1; i > changedLineIndex; --i) {
segments.setElementAt(i + optimizedLineShift, segments.getSegmentStart(i) + lengthDiff,
segments.getSegmentEnd(i) + lengthDiff,
segments.getSegmentData(i)
);
}
final int oldSegmentEnd = segments.getSegmentEnd(changedLineIndex);
final int oldSegmentStart = segments.getSegmentStart(changedLineIndex);
final short oldSegmentData = segments.getSegmentData(changedLineIndex);
final int newChangedLineEnd = insertionPoint + tokenizer.getLineSeparatorLength() + tokenizer.getOffset() + tokenizer.getLength();
segments.setElementAt(
changedLineIndex,
oldSegmentStart, newChangedLineEnd,
tokenizer.getLineSeparatorLength() | MODIFIED_MASK
);
tokenizer.advance();
i = 1;
int lastFragmentLength = 0;
while(!tokenizer.atEnd()) {
lastFragmentLength = tokenizer.getLineSeparatorLength() != 0 ? 0:tokenizer.getLength();
segments.setElementAt(
changedLineIndex + i,
insertionPoint + tokenizer.getOffset(),
insertionPoint + tokenizer.getOffset() + tokenizer.getLength() + tokenizer.getLineSeparatorLength(),
tokenizer.getLineSeparatorLength() | MODIFIED_MASK
);
i++;
tokenizer.advance();
}
segments.setElementAt(
changedLineIndex + optimizedLineShift, insertionPoint + lengthDiff - lastFragmentLength,
oldSegmentEnd + lengthDiff,
oldSegmentData | MODIFIED_MASK
);
if (doTest) {
final SegmentArrayWithData data = mySegments;
mySegments = segments;
addEmptyLineAtEnd();
doCheckResults(workingCopySegmentsForTesting, e, data, segments);
} else {
addEmptyLineAtEnd();
}
}
private void doCheckResults(final SegmentArrayWithData workingCopySegmentsForTesting, final DocumentEventImpl e,
final SegmentArrayWithData data,
final SegmentArrayWithData segments) {
mySegments = workingCopySegmentsForTesting;
processMultilineChange(e);
mySegments = data;
assert workingCopySegmentsForTesting.getSegmentCount() == segments.getSegmentCount();
for(int i =0; i < segments.getSegmentCount();++i) {
assert workingCopySegmentsForTesting.getSegmentStart(i) == segments.getSegmentStart(i);
assert workingCopySegmentsForTesting.getSegmentEnd(i) == segments.getSegmentEnd(i);
assert workingCopySegmentsForTesting.getSegmentData(i) == segments.getSegmentData(i);
}
processMultilineChange(e);
}
private void fillSegments(final SegmentArrayWithData segments, final SegmentArrayWithData workingCopySegmentsForTesting) {
for(int i = mySegments.getSegmentCount() - 1; i >=0; --i) {
segments.setElementAt(
i,
mySegments.getSegmentStart(i),
mySegments.getSegmentEnd(i),
mySegments.getSegmentData(i)
);
workingCopySegmentsForTesting.setElementAt(
i,
mySegments.getSegmentStart(i),
mySegments.getSegmentEnd(i),
mySegments.getSegmentData(i)
);
}
}
private void processMultilineChange(DocumentEventImpl e) {
int offset = e.getOffset();
CharSequence newString = e.getNewFragment();
CharSequence chars = e.getDocument().getCharsSequence();
int oldStartLine = e.getStartOldIndex();
int offset1 = getLineStart(oldStartLine);
if (offset1 != offset) {
CharSequence prefix = chars.subSequence(offset1, offset);
newString = new MergingCharSequence(prefix, newString);
}
int oldEndLine = findLineIndex(e.getOffset() + e.getOldLength());
if (oldEndLine < 0) {
oldEndLine = getLineCount() - 1;
}
int offset2 = getLineEnd(oldEndLine);
if (offset2 != offset + e.getOldLength()) {
final int start = offset + e.getNewLength();
final int length = offset2 - offset - e.getOldLength();
CharSequence postfix = chars.subSequence(start, start + length);
newString = new MergingCharSequence(newString, postfix);
}
updateSegments(newString, oldStartLine, oldEndLine, offset1, e);
// We add empty line at the end, if the last line ends by line separator.
addEmptyLineAtEnd();
}
private void updateSegments(CharSequence newText, int oldStartLine, int oldEndLine, int offset1,
DocumentEventImpl e) {
int count = 0;
LineTokenizer lineTokenizer = new LineTokenizer(newText);
for (int index = oldStartLine; index <= oldEndLine; index++) {
if (!lineTokenizer.atEnd()) {
setSegmentAt(mySegments, index, lineTokenizer, offset1, true);
lineTokenizer.advance();
} else {
mySegments.remove(index, oldEndLine + 1);
break;
}
count++;
}
if (!lineTokenizer.atEnd()) {
SegmentArrayWithData insertSegments = new SegmentArrayWithData();
int i = 0;
while (!lineTokenizer.atEnd()) {
setSegmentAt(insertSegments, i, lineTokenizer, offset1, true);
lineTokenizer.advance();
count++;
i++;
}
mySegments.insert(insertSegments, oldEndLine + 1);
}
int shift = e.getNewLength() - e.getOldLength();
mySegments.shiftSegments(oldStartLine + count, shift);
}
private void processOneLineChange(DocumentEventImpl e) {
// Check, if the change on the end of text
if (e.getOffset() >= mySegments.getSegmentEnd(mySegments.getSegmentCount() - 1)) {
mySegments.changeSegmentLength(mySegments.getSegmentCount() - 1, e.getNewLength() - e.getOldLength());
setSegmentModified(mySegments, mySegments.getSegmentCount() - 1);
} else {
mySegments.changeSegmentLength(e.getStartOldIndex(), e.getNewLength() - e.getOldLength());
setSegmentModified(mySegments, e.getStartOldIndex());
}
}
public void clearModificationFlags() {
for (int i = 0; i < mySegments.getSegmentCount(); i++) {
mySegments.setSegmentData(i, mySegments.getSegmentData(i) & ~MODIFIED_MASK);
}
}
private static void setSegmentAt(SegmentArrayWithData segmentArrayWithData, int index, LineTokenizer lineTokenizer, int offsetShift, boolean isModified) {
int offset = lineTokenizer.getOffset() + offsetShift;
int length = lineTokenizer.getLength();
int separatorLength = lineTokenizer.getLineSeparatorLength();
int separatorAndModifiedFlag = separatorLength;
if(isModified) {
separatorAndModifiedFlag |= MODIFIED_MASK;
}
segmentArrayWithData.setElementAt(index, offset, offset + length + separatorLength, separatorAndModifiedFlag);
}
private static void setSegmentModified(SegmentArrayWithData segments, int i) {
segments.setSegmentData(i, segments.getSegmentData(i)|MODIFIED_MASK);
}
private void initSegments(CharSequence text, boolean toSetModified) {
mySegments.removeAll();
LineTokenizer lineTokenizer = new LineTokenizer(text);
int i = 0;
while(!lineTokenizer.atEnd()) {
setSegmentAt(mySegments, i, lineTokenizer, 0, toSetModified);
i++;
lineTokenizer.advance();
}
// We add empty line at the end, if the last line ends by line separator.
addEmptyLineAtEnd();
}
// Add empty line at the end, if the last line ends by line separator.
private void addEmptyLineAtEnd() {
int segmentCount = mySegments.getSegmentCount();
if(segmentCount > 0 && getSeparatorLength(segmentCount-1) > 0) {
mySegments.setElementAt(segmentCount, mySegments.getSegmentEnd(segmentCount-1), mySegments.getSegmentEnd(segmentCount-1), 0);
setSegmentModified(mySegments, segmentCount);
}
}
}

View File

@@ -316,10 +316,9 @@ public abstract class EditorTextFieldCellRenderer implements TableCellRenderer,
private static class MyDocument extends UserDataHolderBase implements DocumentEx {
RangeMarkerTree<RangeMarkerEx> myRangeMarkers = new RangeMarkerTree<RangeMarkerEx>(this) {};
LineSet myLineSet = new LineSet();
char[] myChars = ArrayUtil.EMPTY_CHAR_ARRAY;
String myString = "";
LineSet myLineSet = LineSet.createLineSet(myString);
@Override
public void setText(@NotNull CharSequence text) {
@@ -327,7 +326,7 @@ public abstract class EditorTextFieldCellRenderer implements TableCellRenderer,
myChars = new char[s.length()];
s.getChars(0, s.length(), myChars, 0);
myString = new String(myChars);
myLineSet.documentCreated(this);
myLineSet = LineSet.createLineSet(myString);
}
@Override