| /* |
| * 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); |
| } |
| } |
| |
| } |