blob: d7574d262e5d3215cb9e7cd8080e1d1d67d6b5a9 [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 org.jetbrains.annotations.NotNull;
/**
* 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.
*/
public class LineSet{
private SegmentArrayWithData mySegments = new SegmentArrayWithData();
private static final int MODIFIED_MASK = 0x4;
private static final int SEPARATOR_MASK = 0x3;
public int findLineIndex(int offset) {
int lineIndex = mySegments.findSegmentIndex(offset);
assert lineIndex >= 0;
return lineIndex;
}
public LineIterator createIterator() {
return new LineIteratorImpl(this);
}
public final int getLineStart(int index) {
int lineStart = mySegments.getSegmentStart(index);
assert lineStart >= 0;
return lineStart;
}
public final int getLineEnd(int index) {
return mySegments.getSegmentEnd(index);
}
final boolean isModified(int index) {
return (mySegments.getSegmentData(index) & MODIFIED_MASK) != 0;
}
final void setModified(int index) {
setSegmentModified(mySegments, index);
}
final int getSeparatorLength(int index) {
return mySegments.getSegmentData(index) & SEPARATOR_MASK;
}
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();
}
}
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);
}
}
}